@imbingox/acex 0.3.0-beta.1 → 0.3.0-beta.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imbingox/acex",
3
- "version": "0.3.0-beta.1",
3
+ "version": "0.3.0-beta.3",
4
4
  "description": "Multi-exchange trading SDK for market data, account, and order management",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,7 +16,8 @@
16
16
  },
17
17
  "files": [
18
18
  "index.ts",
19
- "src/"
19
+ "src/",
20
+ "docs/api.md"
20
21
  ],
21
22
  "scripts": {
22
23
  "changeset": "changeset",
@@ -30,6 +31,8 @@
30
31
  "test:live:account": "bun run scripts/live-account-smoke.ts",
31
32
  "test:live:account:smoke": "bun run scripts/live-account-smoke.ts --duration 10",
32
33
  "test:live:account:soak": "bun run scripts/live-account-smoke.ts --duration 60 --disconnect-after 5",
34
+ "test:live:juplend": "bun run scripts/live-juplend-account-smoke.ts",
35
+ "test:live:juplend:smoke": "bun run scripts/live-juplend-account-smoke.ts --duration 35 --show-amounts",
33
36
  "test:live:market": "bun run scripts/live-market-smoke.ts",
34
37
  "test:live:market:smoke": "bun run scripts/live-market-smoke.ts --duration 10",
35
38
  "test:live:market:soak": "bun run scripts/live-market-smoke.ts --duration 60 --disconnect-after 5 --disconnect-target perp",
@@ -1,4 +1,7 @@
1
- import type { MarketDefinition } from "../../types/index.ts";
1
+ import type {
2
+ MarketDefinition,
3
+ VenueMarketCapabilities,
4
+ } from "../../types/index.ts";
2
5
  import type {
3
6
  FundingRateStreamCallbacks,
4
7
  FundingRateStreamOptions,
@@ -15,7 +18,13 @@ import {
15
18
  } from "./market-catalog.ts";
16
19
 
17
20
  export class BinanceMarketAdapter implements MarketAdapter {
18
- readonly exchange = "binance" as const;
21
+ readonly venue = "binance" as const;
22
+ readonly marketCapabilities: VenueMarketCapabilities = {
23
+ catalog: "supported",
24
+ l1Book: "supported",
25
+ fundingRate: "market_dependent",
26
+ marketTypes: ["spot", "swap", "future"],
27
+ };
19
28
 
20
29
  private readonly definitions = new Map<string, BinanceMarketDefinition>();
21
30
 
@@ -135,7 +135,7 @@ function normalizeSpotSymbol(
135
135
  const notionalValue = notionalFilter?.minNotional ?? notionalFilter?.notional;
136
136
 
137
137
  return {
138
- exchange: "binance",
138
+ venue: "binance",
139
139
  family: "spot",
140
140
  symbol: `${symbol.baseAsset}/${symbol.quoteAsset}`,
141
141
  id: symbol.symbol,
@@ -180,7 +180,7 @@ function normalizeDerivativesSymbol(
180
180
  const notionalValue = notionalFilter?.minNotional ?? notionalFilter?.notional;
181
181
 
182
182
  return {
183
- exchange: "binance",
183
+ venue: "binance",
184
184
  family,
185
185
  symbol: buildFuturesSymbol(
186
186
  symbol.baseAsset,
@@ -1,6 +1,12 @@
1
1
  import { createHmac } from "node:crypto";
2
+ import BigNumber from "bignumber.js";
2
3
  import { createManagedWebSocket } from "../../internal/managed-websocket.ts";
3
- import type { AccountCredentials, PositionSide } from "../../types/index.ts";
4
+ import type {
5
+ AccountCredentials,
6
+ PositionSide,
7
+ VenueAccountCapabilities,
8
+ VenueOrderCapabilities,
9
+ } from "../../types/index.ts";
4
10
  import type {
5
11
  CancelAllOrdersRequest,
6
12
  CancelOrderRequest,
@@ -280,9 +286,13 @@ function mapAccountRisk(
280
286
  input: BinancePapiAccount,
281
287
  receivedAt: number,
282
288
  ): RawRiskUpdate | undefined {
289
+ const uniMmr = firstString(input.uniMMR);
290
+ const riskRatio = uniMmr
291
+ ? new BigNumber(1).dividedBy(uniMmr).toString(10)
292
+ : undefined;
283
293
  const risk: RawRiskUpdate = {
284
294
  equity: firstString(input.accountEquity, input.totalEquity),
285
- marginRatio: input.uniMMR,
295
+ riskRatio,
286
296
  initialMargin: firstString(
287
297
  input.accountInitialMargin,
288
298
  input.totalInitialMargin,
@@ -297,7 +307,7 @@ function mapAccountRisk(
297
307
 
298
308
  if (
299
309
  !risk.equity &&
300
- !risk.marginRatio &&
310
+ !risk.riskRatio &&
301
311
  !risk.initialMargin &&
302
312
  !risk.maintenanceMargin
303
313
  ) {
@@ -473,7 +483,37 @@ async function readJson<T>(response: Response, url: string): Promise<T> {
473
483
  }
474
484
 
475
485
  export class BinancePrivateAdapter implements PrivateUserDataAdapter {
476
- readonly exchange = "binance" as const;
486
+ readonly venue = "binance" as const;
487
+ readonly readOnly = false;
488
+ readonly notes = [
489
+ "Capabilities describe the current SDK runtime, not Binance's full exchange API surface.",
490
+ "Funding rate support depends on the market type.",
491
+ "Order commands currently target Binance PAPI UM USD-M symbols; venue-level order.supported does not mean every Binance market type is orderable.",
492
+ ];
493
+ readonly accountCapabilities: VenueAccountCapabilities = {
494
+ register: "supported",
495
+ snapshot: "supported",
496
+ updates: "websocket",
497
+ balances: "supported",
498
+ positions: "supported",
499
+ risk: "supported",
500
+ lending: "unsupported",
501
+ credentialsRequired: true,
502
+ };
503
+ readonly orderCapabilities: VenueOrderCapabilities = {
504
+ supported: true,
505
+ openOrders: "supported",
506
+ updates: "websocket",
507
+ create: "supported",
508
+ cancel: "supported",
509
+ cancelAll: "symbol",
510
+ orderTypes: ["limit", "market"],
511
+ timeInForce: ["gtc", "post_only"],
512
+ postOnly: true,
513
+ reduceOnly: true,
514
+ positionSide: "required_for_hedge",
515
+ clientOrderId: true,
516
+ };
477
517
 
478
518
  async bootstrapAccount(
479
519
  credentials: AccountCredentials,
@@ -0,0 +1,517 @@
1
+ import BigNumber from "bignumber.js";
2
+ import { AcexError } from "../../errors.ts";
3
+ import type {
4
+ AccountCredentials,
5
+ VenueAccountCapabilities,
6
+ VenueOrderCapabilities,
7
+ } from "../../types/index.ts";
8
+ import type {
9
+ CancelAllOrdersRequest,
10
+ CancelOrderRequest,
11
+ CreateOrderRequest,
12
+ PrivateStreamCallbacks,
13
+ PrivateStreamOptions,
14
+ PrivateUserDataAdapter,
15
+ RawAccountBootstrap,
16
+ RawBalanceUpdate,
17
+ RawOrderUpdate,
18
+ RawRiskUpdate,
19
+ StreamHandle,
20
+ } from "../types.ts";
21
+
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[];
37
+ }
38
+
39
+ interface JuplendVault {
40
+ id?: number | string;
41
+ vaultId?: number | string;
42
+ supplyToken?: JuplendToken;
43
+ borrowToken?: JuplendToken;
44
+ liquidationThreshold?: number | string;
45
+ loanToValue?: number | string;
46
+ supplyRate?: number | string;
47
+ borrowRate?: number | string;
48
+ }
49
+
50
+ interface JuplendToken {
51
+ symbol?: string;
52
+ asset?: string;
53
+ oraclePrice?: number | string;
54
+ price?: number | string;
55
+ }
56
+
57
+ interface JuplendMappedAccount {
58
+ balances: RawBalanceUpdate[];
59
+ risk?: RawRiskUpdate;
60
+ }
61
+
62
+ interface BalanceAccumulator {
63
+ asset: string;
64
+ supplied: BigNumber;
65
+ borrowed: BigNumber;
66
+ supplyAPY?: BigNumber;
67
+ borrowAPY?: BigNumber;
68
+ }
69
+
70
+ interface JuplendAccountOptions {
71
+ walletAddress: string;
72
+ positionId?: string;
73
+ }
74
+
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\/([^/?#]+)/;
80
+
81
+ let vaultCache:
82
+ | {
83
+ loadedAt: number;
84
+ vaults: Map<string, JuplendVault>;
85
+ }
86
+ | undefined;
87
+ let vaultCachePromise: Promise<Map<string, JuplendVault>> | undefined;
88
+
89
+ function requireApiKey(credentials: AccountCredentials): string {
90
+ if (!credentials.apiKey) {
91
+ throw new Error("credentials.apiKey required");
92
+ }
93
+
94
+ return credentials.apiKey;
95
+ }
96
+
97
+ function getJuplendAccountOptions(
98
+ accountOptions?: Record<string, unknown>,
99
+ ): JuplendAccountOptions {
100
+ const walletAddress = accountOptions?.walletAddress;
101
+ if (typeof walletAddress !== "string" || !walletAddress) {
102
+ throw new Error("options.walletAddress required");
103
+ }
104
+
105
+ const positionId = accountOptions.positionId;
106
+ if (positionId !== undefined && typeof positionId !== "string") {
107
+ throw new Error("options.positionId must be a string");
108
+ }
109
+
110
+ return {
111
+ walletAddress,
112
+ positionId: positionId || undefined,
113
+ };
114
+ }
115
+
116
+ function toBigNumber(value: number | string | undefined): BigNumber {
117
+ return value === undefined ? new BigNumber(0) : new BigNumber(value);
118
+ }
119
+
120
+ function normalizeThreshold(value: number | string | undefined): BigNumber {
121
+ const threshold = toBigNumber(value);
122
+ return threshold.gt(1) ? threshold.dividedBy(1000) : threshold;
123
+ }
124
+
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) {
138
+ return undefined;
139
+ }
140
+
141
+ const match = LINK_PATTERN.exec(link);
142
+ if (!match?.[1] || !match[2]) {
143
+ return undefined;
144
+ }
145
+
146
+ return {
147
+ vaultId: match[1],
148
+ positionId: match[2],
149
+ };
150
+ }
151
+
152
+ function getVaultId(vault: JuplendVault): string | undefined {
153
+ const id = vault.id ?? vault.vaultId;
154
+ return id === undefined ? undefined : `${id}`;
155
+ }
156
+
157
+ function setAccumulator(
158
+ map: Map<string, BalanceAccumulator>,
159
+ asset: string,
160
+ ): BalanceAccumulator {
161
+ const existing = map.get(asset);
162
+ if (existing) {
163
+ return existing;
164
+ }
165
+
166
+ const next: BalanceAccumulator = {
167
+ asset,
168
+ supplied: new BigNumber(0),
169
+ borrowed: new BigNumber(0),
170
+ };
171
+ map.set(asset, next);
172
+ return next;
173
+ }
174
+
175
+ function buildBalances(
176
+ balances: Map<string, BalanceAccumulator>,
177
+ receivedAt: number,
178
+ ): RawBalanceUpdate[] {
179
+ return [...balances.values()].map((balance) => {
180
+ const netAsset = balance.supplied.minus(balance.borrowed);
181
+ return {
182
+ asset: balance.asset,
183
+ free: "0",
184
+ used: "0",
185
+ total: netAsset.toString(10),
186
+ receivedAt,
187
+ lending: {
188
+ supplied: balance.supplied.toString(10),
189
+ borrowed: balance.borrowed.toString(10),
190
+ interest: "0",
191
+ netAsset: netAsset.toString(10),
192
+ supplyAPY: balance.supplyAPY?.toString(10),
193
+ borrowAPY: balance.borrowAPY?.toString(10),
194
+ },
195
+ };
196
+ });
197
+ }
198
+
199
+ function buildRisk(input: {
200
+ totalCollateralUsd: BigNumber;
201
+ totalDebtUsd: BigNumber;
202
+ weightedLiquidationValueUsd: BigNumber;
203
+ receivedAt: number;
204
+ }): RawRiskUpdate | undefined {
205
+ const { totalCollateralUsd, totalDebtUsd, weightedLiquidationValueUsd } =
206
+ input;
207
+ if (totalCollateralUsd.isZero() && totalDebtUsd.isZero()) {
208
+ return undefined;
209
+ }
210
+
211
+ const riskRatio = weightedLiquidationValueUsd.isZero()
212
+ ? undefined
213
+ : totalDebtUsd.dividedBy(weightedLiquidationValueUsd).toString(10);
214
+ const ltv = totalCollateralUsd.isZero()
215
+ ? undefined
216
+ : totalDebtUsd.dividedBy(totalCollateralUsd).toString(10);
217
+ const liquidationThreshold = totalCollateralUsd.isZero()
218
+ ? undefined
219
+ : weightedLiquidationValueUsd.dividedBy(totalCollateralUsd).toString(10);
220
+ const healthFactor = riskRatio
221
+ ? new BigNumber(1).dividedBy(riskRatio).toString(10)
222
+ : undefined;
223
+
224
+ return {
225
+ equity: totalCollateralUsd.minus(totalDebtUsd).toString(10),
226
+ riskRatio,
227
+ receivedAt: input.receivedAt,
228
+ lending: {
229
+ healthFactor,
230
+ ltv,
231
+ liquidationThreshold,
232
+ totalCollateralUSD: totalCollateralUsd.toString(10),
233
+ totalDebtUSD: totalDebtUsd.toString(10),
234
+ },
235
+ };
236
+ }
237
+
238
+ 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}`);
242
+ }
243
+
244
+ return (await response.json()) as T;
245
+ }
246
+
247
+ async function loadVaults(now: number): Promise<Map<string, JuplendVault>> {
248
+ if (vaultCache && now - vaultCache.loadedAt < VAULT_CACHE_TTL_MS) {
249
+ return vaultCache.vaults;
250
+ }
251
+
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
+ });
271
+ }
272
+
273
+ try {
274
+ return await vaultCachePromise;
275
+ } catch (error) {
276
+ if (vaultCache) {
277
+ return vaultCache.vaults;
278
+ }
279
+ throw error;
280
+ }
281
+ }
282
+
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`,
289
+ {
290
+ headers: {
291
+ "X-API-KEY": apiKey,
292
+ },
293
+ },
294
+ );
295
+ }
296
+
297
+ function mapAccount(
298
+ portfolio: JuplendPortfolioResponse,
299
+ vaults: Map<string, JuplendVault>,
300
+ receivedAt: number,
301
+ positionId?: string,
302
+ ): JuplendMappedAccount {
303
+ const balances = new Map<string, BalanceAccumulator>();
304
+ let totalCollateralUsd = new BigNumber(0);
305
+ let totalDebtUsd = new BigNumber(0);
306
+ let weightedLiquidationValueUsd = new BigNumber(0);
307
+
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) {
315
+ continue;
316
+ }
317
+
318
+ const vault = vaults.get(positionLink.vaultId);
319
+ if (!vault) {
320
+ continue;
321
+ }
322
+
323
+ const suppliedValue = toBigNumber(element.data?.suppliedValue);
324
+ const borrowedValue = toBigNumber(element.data?.borrowedValue);
325
+ 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),
332
+ );
333
+
334
+ const supplyAsset = tokenAsset(vault.supplyToken);
335
+ const supplyPrice = tokenPrice(vault.supplyToken);
336
+ if (supplyAsset && supplyPrice) {
337
+ const accumulator = setAccumulator(balances, supplyAsset);
338
+ accumulator.supplied = accumulator.supplied.plus(
339
+ suppliedValue.dividedBy(supplyPrice),
340
+ );
341
+ accumulator.supplyAPY = toBigNumber(vault.supplyRate);
342
+ }
343
+
344
+ const borrowAsset = tokenAsset(vault.borrowToken);
345
+ const borrowPrice = tokenPrice(vault.borrowToken);
346
+ if (borrowAsset && borrowPrice) {
347
+ const accumulator = setAccumulator(balances, borrowAsset);
348
+ accumulator.borrowed = accumulator.borrowed.plus(
349
+ borrowedValue.dividedBy(borrowPrice),
350
+ );
351
+ accumulator.borrowAPY = toBigNumber(vault.borrowRate);
352
+ }
353
+ }
354
+
355
+ return {
356
+ balances: buildBalances(balances, receivedAt),
357
+ risk: buildRisk({
358
+ totalCollateralUsd,
359
+ totalDebtUsd,
360
+ weightedLiquidationValueUsd,
361
+ receivedAt,
362
+ }),
363
+ };
364
+ }
365
+
366
+ export class JuplendPrivateAdapter implements PrivateUserDataAdapter {
367
+ readonly venue = "juplend" as const;
368
+ readonly readOnly = true;
369
+ readonly notes = [
370
+ "Juplend support is limited to read-only lending account views.",
371
+ "Order and market data managers are not supported for this venue.",
372
+ ];
373
+ readonly accountCapabilities: VenueAccountCapabilities = {
374
+ register: "supported",
375
+ snapshot: "supported",
376
+ updates: "polling",
377
+ balances: "supported",
378
+ positions: "unsupported",
379
+ risk: "supported",
380
+ lending: "supported",
381
+ credentialsRequired: true,
382
+ };
383
+ readonly orderCapabilities: VenueOrderCapabilities = {
384
+ supported: false,
385
+ openOrders: "unsupported",
386
+ updates: "unsupported",
387
+ create: "unsupported",
388
+ cancel: "unsupported",
389
+ cancelAll: "unsupported",
390
+ orderTypes: [],
391
+ timeInForce: [],
392
+ postOnly: false,
393
+ reduceOnly: false,
394
+ positionSide: "unsupported",
395
+ clientOrderId: false,
396
+ reason: "read_only",
397
+ };
398
+
399
+ async bootstrapAccount(
400
+ credentials: AccountCredentials,
401
+ accountOptions?: Record<string, unknown>,
402
+ ): Promise<RawAccountBootstrap> {
403
+ const receivedAt = Date.now();
404
+ const apiKey = requireApiKey(credentials);
405
+ 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,
413
+ receivedAt,
414
+ juplendOptions.positionId,
415
+ );
416
+
417
+ return {
418
+ balances: mapped.balances,
419
+ positions: [],
420
+ risk: mapped.risk,
421
+ receivedAt,
422
+ };
423
+ }
424
+
425
+ bootstrapOpenOrders(): Promise<RawOrderUpdate[]> {
426
+ return Promise.resolve([]);
427
+ }
428
+
429
+ createOrder(
430
+ _credentials: AccountCredentials,
431
+ _request: CreateOrderRequest,
432
+ ): Promise<RawOrderUpdate> {
433
+ throw new AcexError(
434
+ "VENUE_NOT_SUPPORTED",
435
+ "Juplend is read-only and does not support createOrder",
436
+ );
437
+ }
438
+
439
+ cancelOrder(
440
+ _credentials: AccountCredentials,
441
+ _request: CancelOrderRequest,
442
+ ): Promise<RawOrderUpdate> {
443
+ throw new AcexError(
444
+ "VENUE_NOT_SUPPORTED",
445
+ "Juplend is read-only and does not support cancelOrder",
446
+ );
447
+ }
448
+
449
+ cancelAllOrders(
450
+ _credentials: AccountCredentials,
451
+ _request: CancelAllOrdersRequest,
452
+ ): Promise<RawOrderUpdate[]> {
453
+ throw new AcexError(
454
+ "VENUE_NOT_SUPPORTED",
455
+ "Juplend is read-only and does not support cancelAllOrders",
456
+ );
457
+ }
458
+
459
+ createPrivateStream(
460
+ credentials: AccountCredentials,
461
+ callbacks: PrivateStreamCallbacks,
462
+ options: PrivateStreamOptions,
463
+ accountOptions?: Record<string, unknown>,
464
+ ): StreamHandle {
465
+ let closed = false;
466
+ let timer: ReturnType<typeof setTimeout> | undefined;
467
+ const pollIntervalMs =
468
+ options.juplendPollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
469
+
470
+ const poll = async (): Promise<void> => {
471
+ try {
472
+ const bootstrap = await this.bootstrapAccount(
473
+ credentials,
474
+ accountOptions,
475
+ );
476
+ if (closed) {
477
+ return;
478
+ }
479
+ callbacks.onAccountSnapshot(bootstrap);
480
+ } catch (error) {
481
+ callbacks.onError(
482
+ error instanceof Error ? error : new Error("Juplend polling failed"),
483
+ );
484
+ }
485
+ };
486
+
487
+ const scheduleNextPoll = (): void => {
488
+ if (closed) {
489
+ return;
490
+ }
491
+
492
+ timer = setTimeout(() => {
493
+ void poll().finally(scheduleNextPoll);
494
+ }, pollIntervalMs);
495
+ };
496
+
497
+ const ready = Promise.resolve().then(() => {
498
+ scheduleNextPoll();
499
+ });
500
+
501
+ return {
502
+ ready,
503
+ close() {
504
+ closed = true;
505
+ if (timer) {
506
+ clearTimeout(timer);
507
+ timer = undefined;
508
+ }
509
+ },
510
+ };
511
+ }
512
+ }
513
+
514
+ export function resetJuplendVaultCacheForTests(): void {
515
+ vaultCache = undefined;
516
+ vaultCachePromise = undefined;
517
+ }