@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.
@@ -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 JuplendPortfolioResponse {
23
- elements?: JuplendPortfolioElement[];
24
- }
25
-
26
- interface JuplendPortfolioElement {
27
- data?: {
28
- link?: string;
29
- suppliedValue?: number | string;
30
- borrowedValue?: number | string;
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 JuplendVault {
34
+ interface JuplendVaultMetadata {
40
35
  id?: number | string;
41
36
  vaultId?: number | string;
42
- supplyToken?: JuplendToken;
43
- borrowToken?: JuplendToken;
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: string;
58
+ walletAddress?: string;
59
+ vaultId?: string;
72
60
  positionId?: string;
73
61
  }
74
62
 
75
- const PORTFOLIO_BASE_URL = "https://api.jup.ag/portfolio/v1";
76
- const VAULTS_URL = "https://lite-api.jup.ag/lend/v1/borrow/vaults";
77
- const DEFAULT_POLL_INTERVAL_MS = 30_000;
78
- const VAULT_CACHE_TTL_MS = 60 * 60 * 1_000;
79
- const LINK_PATTERN = /\/borrow\/([^/]+)\/nfts\/([^/?#]+)/;
63
+ interface JuplendPriceApiEntry {
64
+ usdPrice?: number | string;
65
+ price?: number | string;
66
+ decimals?: number | string;
67
+ }
80
68
 
81
- let vaultCache:
82
- | {
83
- loadedAt: number;
84
- vaults: Map<string, JuplendVault>;
85
- }
86
- | undefined;
87
- let vaultCachePromise: Promise<Map<string, JuplendVault>> | undefined;
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
- function requireApiKey(credentials: AccountCredentials): string {
90
- if (!credentials.apiKey) {
91
- throw new Error("credentials.apiKey required");
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
- return credentials.apiKey;
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" || !walletAddress) {
102
- throw new Error("options.walletAddress required");
106
+ if (walletAddress !== undefined && typeof walletAddress !== "string") {
107
+ throw new Error("options.walletAddress must be a string");
103
108
  }
104
109
 
105
- const positionId = accountOptions.positionId;
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(value: number | string | undefined): BigNumber {
117
- return value === undefined ? new BigNumber(0) : new BigNumber(value);
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: number | string | undefined): BigNumber {
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 tokenAsset(token: JuplendToken | undefined): string | undefined {
126
- return token?.symbol ?? token?.asset;
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 match = LINK_PATTERN.exec(link);
142
- if (!match?.[1] || !match[2]) {
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 getVaultId(vault: JuplendVault): string | undefined {
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
- equity: totalCollateralUsd.minus(totalDebtUsd).toString(10),
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 response = await fetch(url, init);
240
- if (!response.ok) {
241
- throw new Error(`Juplend HTTP ${response.status}: ${response.statusText}`);
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
- return (await response.json()) as T;
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
- async function loadVaults(now: number): Promise<Map<string, JuplendVault>> {
248
- if (vaultCache && now - vaultCache.loadedAt < VAULT_CACHE_TTL_MS) {
249
- return vaultCache.vaults;
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
- if (!vaultCachePromise) {
253
- vaultCachePromise = readJson<JuplendVaultResponse | JuplendVault[]>(
254
- VAULTS_URL,
255
- )
256
- .then((response) => {
257
- const rawVaults = Array.isArray(response) ? response : response.data;
258
- const vaults = new Map<string, JuplendVault>();
259
- for (const vault of rawVaults ?? []) {
260
- const id = getVaultId(vault);
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
- try {
274
- return await vaultCachePromise;
275
- } catch (error) {
276
- if (vaultCache) {
277
- return vaultCache.vaults;
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
- throw error;
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 loadPortfolio(
284
- walletAddress: string,
285
- apiKey: string,
286
- ): Promise<JuplendPortfolioResponse> {
287
- return readJson<JuplendPortfolioResponse>(
288
- `${PORTFOLIO_BASE_URL}/positions/${walletAddress}?platforms=jupiter-exchange`,
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 mapAccount(
298
- portfolio: JuplendPortfolioResponse,
299
- vaults: Map<string, JuplendVault>,
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
- positionId?: string,
302
- ): JuplendMappedAccount {
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 element of portfolio.elements ?? []) {
309
- const positionLink = extractPositionLink(element.data?.link);
310
- if (!positionLink) {
311
- continue;
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(positionLink.vaultId);
319
- if (!vault) {
320
- continue;
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
- vault.liquidationThreshold ?? vault.loanToValue,
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 = tokenAsset(vault.supplyToken);
335
- const supplyPrice = tokenPrice(vault.supplyToken);
336
- if (supplyAsset && supplyPrice) {
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
- suppliedValue.dividedBy(supplyPrice),
340
- );
341
- accumulator.supplyAPY = toBigNumber(vault.supplyRate);
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 = tokenAsset(vault.borrowToken);
345
- const borrowPrice = tokenPrice(vault.borrowToken);
346
- if (borrowAsset && borrowPrice) {
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
- borrowedValue.dividedBy(borrowPrice),
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: true,
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
- credentials: AccountCredentials,
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 [portfolio, vaults] = await Promise.all([
407
- loadPortfolio(juplendOptions.walletAddress, apiKey),
408
- loadVaults(receivedAt),
409
- ]);
410
- const mapped = mapAccount(
411
- portfolio,
412
- vaults,
677
+ const mapped = await mapAccount(
678
+ juplendOptions,
413
679
  receivedAt,
414
- juplendOptions.positionId,
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
- vaultCache = undefined;
516
- vaultCachePromise = undefined;
783
+ enrichmentCache = new Map<string, JuplendVaultEnrichmentCacheEntry>();
784
+ enrichmentCachePromise = new Map<
785
+ string,
786
+ Promise<Map<string, JuplendVaultMetadata>>
787
+ >();
517
788
  }