@imbingox/acex 0.3.0-beta.4 → 0.3.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -9
- package/docs/api.md +47 -19
- package/package.json +3 -1
- package/src/adapters/binance/private-adapter.ts +16 -12
- package/src/adapters/juplend/lend-read.ts +150 -0
- package/src/adapters/juplend/private-adapter.ts +434 -163
- package/src/adapters/types.ts +3 -2
- package/src/client/context.ts +1 -1
- package/src/client/private-subscription-coordinator.ts +2 -2
- package/src/client/runtime.ts +4 -1
- package/src/managers/account-manager.ts +12 -8
- package/src/managers/order-manager.ts +8 -0
- package/src/types/account.ts +3 -2
- package/src/types/shared.ts +14 -9
|
@@ -18,42 +18,29 @@ import type {
|
|
|
18
18
|
RawRiskUpdate,
|
|
19
19
|
StreamHandle,
|
|
20
20
|
} from "../types.ts";
|
|
21
|
+
import { readJuplendPositions } from "./lend-read.ts";
|
|
21
22
|
|
|
22
|
-
interface
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
value?: number | string;
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface JuplendVaultResponse {
|
|
36
|
-
data?: JuplendVault[];
|
|
23
|
+
interface JuplendTokenMetadata {
|
|
24
|
+
address?: string;
|
|
25
|
+
id?: string;
|
|
26
|
+
symbol?: string;
|
|
27
|
+
uiSymbol?: string;
|
|
28
|
+
decimals?: number | string;
|
|
29
|
+
price?: number | string;
|
|
30
|
+
usdPrice?: number | string;
|
|
31
|
+
oraclePrice?: number | string;
|
|
37
32
|
}
|
|
38
33
|
|
|
39
|
-
interface
|
|
34
|
+
interface JuplendVaultMetadata {
|
|
40
35
|
id?: number | string;
|
|
41
36
|
vaultId?: number | string;
|
|
42
|
-
supplyToken?:
|
|
43
|
-
borrowToken?:
|
|
37
|
+
supplyToken?: JuplendTokenMetadata;
|
|
38
|
+
borrowToken?: JuplendTokenMetadata;
|
|
44
39
|
liquidationThreshold?: number | string;
|
|
45
|
-
loanToValue?: number | string;
|
|
46
40
|
supplyRate?: number | string;
|
|
47
41
|
borrowRate?: number | string;
|
|
48
42
|
}
|
|
49
43
|
|
|
50
|
-
interface JuplendToken {
|
|
51
|
-
symbol?: string;
|
|
52
|
-
asset?: string;
|
|
53
|
-
oraclePrice?: number | string;
|
|
54
|
-
price?: number | string;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
44
|
interface JuplendMappedAccount {
|
|
58
45
|
balances: RawBalanceUpdate[];
|
|
59
46
|
risk?: RawRiskUpdate;
|
|
@@ -68,88 +55,125 @@ interface BalanceAccumulator {
|
|
|
68
55
|
}
|
|
69
56
|
|
|
70
57
|
interface JuplendAccountOptions {
|
|
71
|
-
walletAddress
|
|
58
|
+
walletAddress?: string;
|
|
59
|
+
vaultId?: string;
|
|
72
60
|
positionId?: string;
|
|
73
61
|
}
|
|
74
62
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
63
|
+
interface JuplendPriceApiEntry {
|
|
64
|
+
usdPrice?: number | string;
|
|
65
|
+
price?: number | string;
|
|
66
|
+
decimals?: number | string;
|
|
67
|
+
}
|
|
80
68
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
|
87
|
-
|
|
69
|
+
interface JuplendTokenSearchEntry {
|
|
70
|
+
id?: string;
|
|
71
|
+
address?: string;
|
|
72
|
+
symbol?: string;
|
|
73
|
+
name?: string;
|
|
74
|
+
decimals?: number | string;
|
|
75
|
+
usdPrice?: number | string;
|
|
76
|
+
}
|
|
88
77
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
78
|
+
const JUP_API_BASE_URL = "https://api.jup.ag";
|
|
79
|
+
const JUP_LITE_API_BASE_URL = "https://lite-api.jup.ag";
|
|
80
|
+
const TOKENS_SEARCH_PATH = "/tokens/v2/search";
|
|
81
|
+
const PRICE_V3_PATH = "/price/v3";
|
|
82
|
+
const LEND_VAULTS_PATH = "/lend/v1/borrow/vaults";
|
|
83
|
+
const DEFAULT_POLL_INTERVAL_MS = 30_000;
|
|
84
|
+
const DEFAULT_HTTP_TIMEOUT_MS = 10_000;
|
|
85
|
+
// lend-read returns exchange-price-adjusted amounts on a fixed 1e9 scale,
|
|
86
|
+
// not mint-atomic token amounts.
|
|
87
|
+
const POSITION_AMOUNT_SCALE_DECIMALS = 9;
|
|
88
|
+
const VAULT_CACHE_TTL_MS = 60 * 60 * 1_000;
|
|
93
89
|
|
|
94
|
-
|
|
90
|
+
interface JuplendVaultEnrichmentCacheEntry {
|
|
91
|
+
loadedAt: number;
|
|
92
|
+
vaults: Map<string, JuplendVaultMetadata>;
|
|
93
|
+
enriched: boolean;
|
|
95
94
|
}
|
|
96
95
|
|
|
96
|
+
let enrichmentCache = new Map<string, JuplendVaultEnrichmentCacheEntry>();
|
|
97
|
+
let enrichmentCachePromise = new Map<
|
|
98
|
+
string,
|
|
99
|
+
Promise<Map<string, JuplendVaultMetadata>>
|
|
100
|
+
>();
|
|
101
|
+
|
|
97
102
|
function getJuplendAccountOptions(
|
|
98
103
|
accountOptions?: Record<string, unknown>,
|
|
99
104
|
): JuplendAccountOptions {
|
|
100
105
|
const walletAddress = accountOptions?.walletAddress;
|
|
101
|
-
if (typeof walletAddress !== "string"
|
|
102
|
-
throw new Error("options.walletAddress
|
|
106
|
+
if (walletAddress !== undefined && typeof walletAddress !== "string") {
|
|
107
|
+
throw new Error("options.walletAddress must be a string");
|
|
103
108
|
}
|
|
104
109
|
|
|
105
|
-
const
|
|
110
|
+
const vaultId = accountOptions?.vaultId;
|
|
111
|
+
if (vaultId !== undefined && typeof vaultId !== "string") {
|
|
112
|
+
throw new Error("options.vaultId must be a string");
|
|
113
|
+
}
|
|
114
|
+
const positionId = accountOptions?.positionId;
|
|
106
115
|
if (positionId !== undefined && typeof positionId !== "string") {
|
|
107
116
|
throw new Error("options.positionId must be a string");
|
|
108
117
|
}
|
|
109
118
|
|
|
119
|
+
const hasWalletAddress = Boolean(walletAddress);
|
|
120
|
+
const hasDirectPosition = Boolean(vaultId && positionId);
|
|
121
|
+
if (!hasWalletAddress && !hasDirectPosition) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
"options.walletAddress or options.vaultId + options.positionId required",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
110
127
|
return {
|
|
111
|
-
walletAddress,
|
|
128
|
+
walletAddress: walletAddress || undefined,
|
|
129
|
+
vaultId: vaultId || undefined,
|
|
112
130
|
positionId: positionId || undefined,
|
|
113
131
|
};
|
|
114
132
|
}
|
|
115
133
|
|
|
116
|
-
function toBigNumber(
|
|
117
|
-
|
|
134
|
+
function toBigNumber(
|
|
135
|
+
value: BigNumber.Value | undefined,
|
|
136
|
+
fallback = new BigNumber(0),
|
|
137
|
+
): BigNumber {
|
|
138
|
+
return value === undefined ? fallback : new BigNumber(value);
|
|
118
139
|
}
|
|
119
140
|
|
|
120
|
-
function normalizeThreshold(value:
|
|
141
|
+
function normalizeThreshold(value: BigNumber.Value | undefined): BigNumber {
|
|
121
142
|
const threshold = toBigNumber(value);
|
|
122
143
|
return threshold.gt(1) ? threshold.dividedBy(1000) : threshold;
|
|
123
144
|
}
|
|
124
145
|
|
|
125
|
-
function
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
function tokenPrice(token: JuplendToken | undefined): BigNumber | undefined {
|
|
130
|
-
const price = toBigNumber(token?.oraclePrice ?? token?.price);
|
|
131
|
-
return price.gt(0) ? price : undefined;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function extractPositionLink(
|
|
135
|
-
link: string | undefined,
|
|
136
|
-
): { vaultId: string; positionId: string } | undefined {
|
|
137
|
-
if (!link) {
|
|
146
|
+
function normalizeRate(
|
|
147
|
+
value: BigNumber.Value | undefined,
|
|
148
|
+
): BigNumber | undefined {
|
|
149
|
+
if (value === undefined) {
|
|
138
150
|
return undefined;
|
|
139
151
|
}
|
|
140
152
|
|
|
141
|
-
const
|
|
142
|
-
if (!
|
|
153
|
+
const rate = new BigNumber(value);
|
|
154
|
+
if (!rate.isFinite()) {
|
|
143
155
|
return undefined;
|
|
144
156
|
}
|
|
145
157
|
|
|
146
|
-
return
|
|
147
|
-
vaultId: match[1],
|
|
148
|
-
positionId: match[2],
|
|
149
|
-
};
|
|
158
|
+
return rate.gt(1) ? rate.dividedBy(10_000) : rate;
|
|
150
159
|
}
|
|
151
160
|
|
|
152
|
-
function
|
|
161
|
+
function tokenAsset(
|
|
162
|
+
token: JuplendTokenMetadata | undefined,
|
|
163
|
+
): string | undefined {
|
|
164
|
+
return token?.uiSymbol ?? token?.symbol;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function tokenPrice(
|
|
168
|
+
token: JuplendTokenMetadata | undefined,
|
|
169
|
+
): BigNumber | undefined {
|
|
170
|
+
const price = toBigNumber(
|
|
171
|
+
token?.usdPrice ?? token?.price ?? token?.oraclePrice,
|
|
172
|
+
);
|
|
173
|
+
return price.gt(0) ? price : undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getVaultId(vault: JuplendVaultMetadata): string | undefined {
|
|
153
177
|
const id = vault.id ?? vault.vaultId;
|
|
154
178
|
return id === undefined ? undefined : `${id}`;
|
|
155
179
|
}
|
|
@@ -222,7 +246,8 @@ function buildRisk(input: {
|
|
|
222
246
|
: undefined;
|
|
223
247
|
|
|
224
248
|
return {
|
|
225
|
-
|
|
249
|
+
netEquity: totalCollateralUsd.minus(totalDebtUsd).toString(10),
|
|
250
|
+
riskEquity: weightedLiquidationValueUsd.minus(totalDebtUsd).toString(10),
|
|
226
251
|
riskRatio,
|
|
227
252
|
receivedAt: input.receivedAt,
|
|
228
253
|
lending: {
|
|
@@ -236,119 +261,361 @@ function buildRisk(input: {
|
|
|
236
261
|
}
|
|
237
262
|
|
|
238
263
|
async function readJson<T>(url: string, init?: RequestInit): Promise<T> {
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
264
|
+
const controller = new AbortController();
|
|
265
|
+
const upstreamSignal = init?.signal;
|
|
266
|
+
let timedOut = false;
|
|
267
|
+
const onUpstreamAbort = () => {
|
|
268
|
+
controller.abort();
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
if (upstreamSignal?.aborted) {
|
|
272
|
+
controller.abort();
|
|
273
|
+
} else if (upstreamSignal) {
|
|
274
|
+
upstreamSignal.addEventListener("abort", onUpstreamAbort, { once: true });
|
|
242
275
|
}
|
|
243
276
|
|
|
244
|
-
|
|
277
|
+
const timeout = setTimeout(() => {
|
|
278
|
+
timedOut = true;
|
|
279
|
+
controller.abort();
|
|
280
|
+
}, DEFAULT_HTTP_TIMEOUT_MS);
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const response = await fetch(url, {
|
|
284
|
+
...init,
|
|
285
|
+
signal: controller.signal,
|
|
286
|
+
});
|
|
287
|
+
if (!response.ok) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
`Juplend HTTP ${response.status}: ${response.statusText}`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return (await response.json()) as T;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
296
|
+
throw new Error(
|
|
297
|
+
timedOut
|
|
298
|
+
? `Juplend fetch timeout after ${DEFAULT_HTTP_TIMEOUT_MS}ms`
|
|
299
|
+
: "Juplend fetch aborted",
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
throw error;
|
|
303
|
+
} finally {
|
|
304
|
+
clearTimeout(timeout);
|
|
305
|
+
if (upstreamSignal) {
|
|
306
|
+
upstreamSignal.removeEventListener("abort", onUpstreamAbort);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
245
309
|
}
|
|
246
310
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
311
|
+
function getJupApiKey(explicitApiKey?: string): string | undefined {
|
|
312
|
+
return explicitApiKey || process.env.JUP_API || undefined;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function getEnrichmentCacheKey(apiKey?: string): string {
|
|
316
|
+
return apiKey || "__no_jup_api_key__";
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function buildApiHeaders(apiKey?: string): Record<string, string> | undefined {
|
|
320
|
+
return apiKey ? { "x-api-key": apiKey } : undefined;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function withBaseUrl(baseUrl: string, path: string): string {
|
|
324
|
+
return new URL(path, `${baseUrl}/`).toString();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function loadVaultMetadataFromLiteApi(
|
|
328
|
+
apiKey?: string,
|
|
329
|
+
): Promise<Map<string, JuplendVaultMetadata>> {
|
|
330
|
+
const response = await readJson<
|
|
331
|
+
JuplendVaultMetadata[] | { data?: JuplendVaultMetadata[] }
|
|
332
|
+
>(withBaseUrl(JUP_LITE_API_BASE_URL, LEND_VAULTS_PATH), {
|
|
333
|
+
headers: buildApiHeaders(apiKey),
|
|
334
|
+
});
|
|
335
|
+
const rawVaults = Array.isArray(response) ? response : response.data;
|
|
336
|
+
const vaults = new Map<string, JuplendVaultMetadata>();
|
|
337
|
+
|
|
338
|
+
for (const vault of rawVaults ?? []) {
|
|
339
|
+
const id = getVaultId(vault);
|
|
340
|
+
if (id) {
|
|
341
|
+
vaults.set(id, vault);
|
|
342
|
+
}
|
|
250
343
|
}
|
|
251
344
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (id) {
|
|
262
|
-
vaults.set(id, vault);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
vaultCache = { loadedAt: now, vaults };
|
|
266
|
-
return vaults;
|
|
267
|
-
})
|
|
268
|
-
.finally(() => {
|
|
269
|
-
vaultCachePromise = undefined;
|
|
270
|
-
});
|
|
345
|
+
return vaults;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function loadTokenSearchMap(
|
|
349
|
+
mintAddresses: string[],
|
|
350
|
+
apiKey?: string,
|
|
351
|
+
): Promise<Map<string, JuplendTokenMetadata>> {
|
|
352
|
+
if (mintAddresses.length === 0) {
|
|
353
|
+
return new Map();
|
|
271
354
|
}
|
|
272
355
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
356
|
+
const query = encodeURIComponent(mintAddresses.join(","));
|
|
357
|
+
const response = await readJson<JuplendTokenSearchEntry[]>(
|
|
358
|
+
`${withBaseUrl(JUP_API_BASE_URL, TOKENS_SEARCH_PATH)}?query=${query}`,
|
|
359
|
+
{
|
|
360
|
+
headers: buildApiHeaders(apiKey),
|
|
361
|
+
},
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const tokens = new Map<string, JuplendTokenMetadata>();
|
|
365
|
+
for (const token of response ?? []) {
|
|
366
|
+
const mint = token.id ?? token.address;
|
|
367
|
+
if (!mint) {
|
|
368
|
+
continue;
|
|
278
369
|
}
|
|
279
|
-
|
|
370
|
+
|
|
371
|
+
tokens.set(mint, {
|
|
372
|
+
address: mint,
|
|
373
|
+
id: mint,
|
|
374
|
+
symbol: token.symbol,
|
|
375
|
+
uiSymbol: token.symbol,
|
|
376
|
+
decimals: token.decimals,
|
|
377
|
+
usdPrice: token.usdPrice,
|
|
378
|
+
oraclePrice: token.usdPrice,
|
|
379
|
+
});
|
|
280
380
|
}
|
|
381
|
+
|
|
382
|
+
return tokens;
|
|
281
383
|
}
|
|
282
384
|
|
|
283
|
-
async function
|
|
284
|
-
|
|
285
|
-
apiKey
|
|
286
|
-
): Promise<
|
|
287
|
-
|
|
288
|
-
|
|
385
|
+
async function loadPriceMap(
|
|
386
|
+
mintAddresses: string[],
|
|
387
|
+
apiKey?: string,
|
|
388
|
+
): Promise<Map<string, JuplendPriceApiEntry>> {
|
|
389
|
+
if (mintAddresses.length === 0) {
|
|
390
|
+
return new Map();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const ids = encodeURIComponent(mintAddresses.join(","));
|
|
394
|
+
const response = await readJson<Record<string, JuplendPriceApiEntry>>(
|
|
395
|
+
`${withBaseUrl(JUP_API_BASE_URL, PRICE_V3_PATH)}?ids=${ids}`,
|
|
289
396
|
{
|
|
290
|
-
headers:
|
|
291
|
-
"X-API-KEY": apiKey,
|
|
292
|
-
},
|
|
397
|
+
headers: buildApiHeaders(apiKey),
|
|
293
398
|
},
|
|
294
399
|
);
|
|
400
|
+
|
|
401
|
+
return new Map(Object.entries(response ?? {}));
|
|
295
402
|
}
|
|
296
403
|
|
|
297
|
-
function
|
|
298
|
-
|
|
299
|
-
|
|
404
|
+
function mergeTokenMetadata(
|
|
405
|
+
baseToken: JuplendTokenMetadata | undefined,
|
|
406
|
+
searchedToken: JuplendTokenMetadata | undefined,
|
|
407
|
+
pricedToken: JuplendPriceApiEntry | undefined,
|
|
408
|
+
): JuplendTokenMetadata | undefined {
|
|
409
|
+
if (!baseToken && !searchedToken && !pricedToken) {
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
...baseToken,
|
|
415
|
+
...searchedToken,
|
|
416
|
+
price:
|
|
417
|
+
pricedToken?.usdPrice ??
|
|
418
|
+
pricedToken?.price ??
|
|
419
|
+
searchedToken?.usdPrice ??
|
|
420
|
+
baseToken?.usdPrice ??
|
|
421
|
+
baseToken?.price ??
|
|
422
|
+
baseToken?.oraclePrice,
|
|
423
|
+
usdPrice:
|
|
424
|
+
pricedToken?.usdPrice ??
|
|
425
|
+
pricedToken?.price ??
|
|
426
|
+
searchedToken?.usdPrice ??
|
|
427
|
+
baseToken?.usdPrice ??
|
|
428
|
+
baseToken?.price ??
|
|
429
|
+
baseToken?.oraclePrice,
|
|
430
|
+
oraclePrice: baseToken?.oraclePrice,
|
|
431
|
+
decimals:
|
|
432
|
+
searchedToken?.decimals ?? pricedToken?.decimals ?? baseToken?.decimals,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function enrichVaultsWithJupApi(input: {
|
|
437
|
+
apiKey?: string;
|
|
438
|
+
baseVaults: Map<string, JuplendVaultMetadata>;
|
|
439
|
+
}): Promise<Map<string, JuplendVaultMetadata>> {
|
|
440
|
+
const mintAddresses = new Set<string>();
|
|
441
|
+
for (const vault of input.baseVaults.values()) {
|
|
442
|
+
const supplyMint = vault.supplyToken?.address;
|
|
443
|
+
const borrowMint = vault.borrowToken?.address;
|
|
444
|
+
if (supplyMint) {
|
|
445
|
+
mintAddresses.add(supplyMint);
|
|
446
|
+
}
|
|
447
|
+
if (borrowMint) {
|
|
448
|
+
mintAddresses.add(borrowMint);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const [tokenMap, priceMap] = await Promise.all([
|
|
453
|
+
loadTokenSearchMap([...mintAddresses], input.apiKey),
|
|
454
|
+
loadPriceMap([...mintAddresses], input.apiKey),
|
|
455
|
+
]);
|
|
456
|
+
|
|
457
|
+
const enriched = new Map<string, JuplendVaultMetadata>();
|
|
458
|
+
for (const [vaultId, vault] of input.baseVaults.entries()) {
|
|
459
|
+
const supplyMint = vault.supplyToken?.address;
|
|
460
|
+
const borrowMint = vault.borrowToken?.address;
|
|
461
|
+
|
|
462
|
+
enriched.set(vaultId, {
|
|
463
|
+
...vault,
|
|
464
|
+
supplyToken: mergeTokenMetadata(
|
|
465
|
+
vault.supplyToken,
|
|
466
|
+
supplyMint ? tokenMap.get(supplyMint) : undefined,
|
|
467
|
+
supplyMint ? priceMap.get(supplyMint) : undefined,
|
|
468
|
+
),
|
|
469
|
+
borrowToken: mergeTokenMetadata(
|
|
470
|
+
vault.borrowToken,
|
|
471
|
+
borrowMint ? tokenMap.get(borrowMint) : undefined,
|
|
472
|
+
borrowMint ? priceMap.get(borrowMint) : undefined,
|
|
473
|
+
),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return enriched;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function loadVaults(
|
|
481
|
+
now: number,
|
|
482
|
+
apiKey?: string,
|
|
483
|
+
): Promise<Map<string, JuplendVaultMetadata>> {
|
|
484
|
+
const cacheKey = getEnrichmentCacheKey(apiKey);
|
|
485
|
+
const cached = enrichmentCache.get(cacheKey);
|
|
486
|
+
const cacheFresh =
|
|
487
|
+
cached !== undefined && now - cached.loadedAt < VAULT_CACHE_TTL_MS;
|
|
488
|
+
if (cacheFresh && (cached.enriched || !apiKey)) {
|
|
489
|
+
return cached.vaults;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const inflight = enrichmentCachePromise.get(cacheKey);
|
|
493
|
+
if (!inflight) {
|
|
494
|
+
const nextPromise = (async () => {
|
|
495
|
+
const baseVaults = await loadVaultMetadataFromLiteApi(apiKey);
|
|
496
|
+
if (!apiKey) {
|
|
497
|
+
enrichmentCache.set(cacheKey, {
|
|
498
|
+
loadedAt: now,
|
|
499
|
+
vaults: baseVaults,
|
|
500
|
+
enriched: false,
|
|
501
|
+
});
|
|
502
|
+
return baseVaults;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
const enrichedVaults = await enrichVaultsWithJupApi({
|
|
507
|
+
apiKey,
|
|
508
|
+
baseVaults,
|
|
509
|
+
});
|
|
510
|
+
enrichmentCache.set(cacheKey, {
|
|
511
|
+
loadedAt: now,
|
|
512
|
+
vaults: enrichedVaults,
|
|
513
|
+
enriched: true,
|
|
514
|
+
});
|
|
515
|
+
return enrichedVaults;
|
|
516
|
+
} catch {
|
|
517
|
+
return baseVaults;
|
|
518
|
+
}
|
|
519
|
+
})().finally(() => {
|
|
520
|
+
enrichmentCachePromise.delete(cacheKey);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
enrichmentCachePromise.set(cacheKey, nextPromise);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
return await (enrichmentCachePromise.get(cacheKey) as Promise<
|
|
528
|
+
Map<string, JuplendVaultMetadata>
|
|
529
|
+
>);
|
|
530
|
+
} catch (error) {
|
|
531
|
+
const fallbackCached = enrichmentCache.get(cacheKey);
|
|
532
|
+
if (fallbackCached) {
|
|
533
|
+
return fallbackCached.vaults;
|
|
534
|
+
}
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function dividePositionAmount(value: BigNumber): BigNumber {
|
|
540
|
+
return value.dividedBy(new BigNumber(10).pow(POSITION_AMOUNT_SCALE_DECIMALS));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async function mapAccount(
|
|
544
|
+
accountOptions: JuplendAccountOptions,
|
|
300
545
|
receivedAt: number,
|
|
301
|
-
|
|
302
|
-
|
|
546
|
+
rpcUrl: string | undefined,
|
|
547
|
+
jupApiKey: string | undefined,
|
|
548
|
+
): Promise<JuplendMappedAccount> {
|
|
549
|
+
const [vaults, positionResult] = await Promise.all([
|
|
550
|
+
loadVaults(receivedAt, jupApiKey),
|
|
551
|
+
readJuplendPositions({
|
|
552
|
+
walletAddress: accountOptions.walletAddress,
|
|
553
|
+
vaultId: accountOptions.vaultId,
|
|
554
|
+
positionId: accountOptions.positionId,
|
|
555
|
+
explicitRpcUrl: rpcUrl,
|
|
556
|
+
}),
|
|
557
|
+
]);
|
|
558
|
+
|
|
303
559
|
const balances = new Map<string, BalanceAccumulator>();
|
|
304
560
|
let totalCollateralUsd = new BigNumber(0);
|
|
305
561
|
let totalDebtUsd = new BigNumber(0);
|
|
306
562
|
let weightedLiquidationValueUsd = new BigNumber(0);
|
|
307
563
|
|
|
308
|
-
for (const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (positionId && positionLink.positionId !== positionId) {
|
|
564
|
+
for (const position of positionResult.positions) {
|
|
565
|
+
if (
|
|
566
|
+
accountOptions.walletAddress &&
|
|
567
|
+
accountOptions.positionId &&
|
|
568
|
+
position.nftId !== accountOptions.positionId
|
|
569
|
+
) {
|
|
315
570
|
continue;
|
|
316
571
|
}
|
|
317
572
|
|
|
318
|
-
const vault = vaults.get(
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
573
|
+
const vault = vaults.get(position.vaultId);
|
|
574
|
+
const suppliedQuantity = dividePositionAmount(
|
|
575
|
+
toBigNumber(position.supplyAmount),
|
|
576
|
+
);
|
|
577
|
+
const borrowedQuantity = dividePositionAmount(
|
|
578
|
+
toBigNumber(position.borrowAmount),
|
|
579
|
+
);
|
|
322
580
|
|
|
323
|
-
const suppliedValue = toBigNumber(element.data?.suppliedValue);
|
|
324
|
-
const borrowedValue = toBigNumber(element.data?.borrowedValue);
|
|
325
581
|
const liquidationThreshold = normalizeThreshold(
|
|
326
|
-
|
|
327
|
-
);
|
|
328
|
-
totalCollateralUsd = totalCollateralUsd.plus(suppliedValue);
|
|
329
|
-
totalDebtUsd = totalDebtUsd.plus(borrowedValue);
|
|
330
|
-
weightedLiquidationValueUsd = weightedLiquidationValueUsd.plus(
|
|
331
|
-
suppliedValue.multipliedBy(liquidationThreshold),
|
|
582
|
+
position.liquidationThresholdRaw ?? vault?.liquidationThreshold,
|
|
332
583
|
);
|
|
333
584
|
|
|
334
|
-
const supplyAsset =
|
|
335
|
-
|
|
336
|
-
if (supplyAsset
|
|
585
|
+
const supplyAsset =
|
|
586
|
+
tokenAsset(vault?.supplyToken) ?? vault?.supplyToken?.address;
|
|
587
|
+
if (supplyAsset) {
|
|
337
588
|
const accumulator = setAccumulator(balances, supplyAsset);
|
|
338
|
-
accumulator.supplied = accumulator.supplied.plus(
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
589
|
+
accumulator.supplied = accumulator.supplied.plus(suppliedQuantity);
|
|
590
|
+
accumulator.supplyAPY =
|
|
591
|
+
normalizeRate(vault?.supplyRate ?? position.supplyRateRaw) ??
|
|
592
|
+
accumulator.supplyAPY;
|
|
342
593
|
}
|
|
343
594
|
|
|
344
|
-
const borrowAsset =
|
|
345
|
-
|
|
346
|
-
if (borrowAsset
|
|
595
|
+
const borrowAsset =
|
|
596
|
+
tokenAsset(vault?.borrowToken) ?? vault?.borrowToken?.address;
|
|
597
|
+
if (borrowAsset) {
|
|
347
598
|
const accumulator = setAccumulator(balances, borrowAsset);
|
|
348
|
-
accumulator.borrowed = accumulator.borrowed.plus(
|
|
349
|
-
|
|
599
|
+
accumulator.borrowed = accumulator.borrowed.plus(borrowedQuantity);
|
|
600
|
+
accumulator.borrowAPY =
|
|
601
|
+
normalizeRate(vault?.borrowRate ?? position.borrowRateRaw) ??
|
|
602
|
+
accumulator.borrowAPY;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const supplyPrice = tokenPrice(vault?.supplyToken);
|
|
606
|
+
if (supplyPrice) {
|
|
607
|
+
const collateralUsd = suppliedQuantity.multipliedBy(supplyPrice);
|
|
608
|
+
totalCollateralUsd = totalCollateralUsd.plus(collateralUsd);
|
|
609
|
+
weightedLiquidationValueUsd = weightedLiquidationValueUsd.plus(
|
|
610
|
+
collateralUsd.multipliedBy(liquidationThreshold),
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const borrowPrice = tokenPrice(vault?.borrowToken);
|
|
615
|
+
if (borrowPrice) {
|
|
616
|
+
totalDebtUsd = totalDebtUsd.plus(
|
|
617
|
+
borrowedQuantity.multipliedBy(borrowPrice),
|
|
350
618
|
);
|
|
351
|
-
accumulator.borrowAPY = toBigNumber(vault.borrowRate);
|
|
352
619
|
}
|
|
353
620
|
}
|
|
354
621
|
|
|
@@ -378,7 +645,7 @@ export class JuplendPrivateAdapter implements PrivateUserDataAdapter {
|
|
|
378
645
|
positions: "unsupported",
|
|
379
646
|
risk: "supported",
|
|
380
647
|
lending: "supported",
|
|
381
|
-
credentialsRequired:
|
|
648
|
+
credentialsRequired: false,
|
|
382
649
|
};
|
|
383
650
|
readonly orderCapabilities: VenueOrderCapabilities = {
|
|
384
651
|
supported: false,
|
|
@@ -396,22 +663,22 @@ export class JuplendPrivateAdapter implements PrivateUserDataAdapter {
|
|
|
396
663
|
reason: "read_only",
|
|
397
664
|
};
|
|
398
665
|
|
|
666
|
+
constructor(
|
|
667
|
+
private readonly rpcUrl?: string,
|
|
668
|
+
private readonly jupApiKey?: string,
|
|
669
|
+
) {}
|
|
670
|
+
|
|
399
671
|
async bootstrapAccount(
|
|
400
|
-
|
|
672
|
+
_credentials: AccountCredentials,
|
|
401
673
|
accountOptions?: Record<string, unknown>,
|
|
402
674
|
): Promise<RawAccountBootstrap> {
|
|
403
675
|
const receivedAt = Date.now();
|
|
404
|
-
const apiKey = requireApiKey(credentials);
|
|
405
676
|
const juplendOptions = getJuplendAccountOptions(accountOptions);
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
loadVaults(receivedAt),
|
|
409
|
-
]);
|
|
410
|
-
const mapped = mapAccount(
|
|
411
|
-
portfolio,
|
|
412
|
-
vaults,
|
|
677
|
+
const mapped = await mapAccount(
|
|
678
|
+
juplendOptions,
|
|
413
679
|
receivedAt,
|
|
414
|
-
|
|
680
|
+
this.rpcUrl,
|
|
681
|
+
getJupApiKey(this.jupApiKey),
|
|
415
682
|
);
|
|
416
683
|
|
|
417
684
|
return {
|
|
@@ -476,6 +743,7 @@ export class JuplendPrivateAdapter implements PrivateUserDataAdapter {
|
|
|
476
743
|
if (closed) {
|
|
477
744
|
return;
|
|
478
745
|
}
|
|
746
|
+
|
|
479
747
|
callbacks.onAccountSnapshot(bootstrap);
|
|
480
748
|
} catch (error) {
|
|
481
749
|
callbacks.onError(
|
|
@@ -512,6 +780,9 @@ export class JuplendPrivateAdapter implements PrivateUserDataAdapter {
|
|
|
512
780
|
}
|
|
513
781
|
|
|
514
782
|
export function resetJuplendVaultCacheForTests(): void {
|
|
515
|
-
|
|
516
|
-
|
|
783
|
+
enrichmentCache = new Map<string, JuplendVaultEnrichmentCacheEntry>();
|
|
784
|
+
enrichmentCachePromise = new Map<
|
|
785
|
+
string,
|
|
786
|
+
Promise<Map<string, JuplendVaultMetadata>>
|
|
787
|
+
>();
|
|
517
788
|
}
|