@imbingox/acex 0.3.1-beta.0 → 0.4.0-beta.2
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 +7 -7
- package/docs/api.md +66 -66
- package/package.json +1 -1
- package/src/adapters/binance/market-catalog.ts +42 -21
- package/src/adapters/binance/private-adapter.ts +78 -19
- package/src/adapters/juplend/private-adapter.ts +91 -55
- package/src/client/private-subscription-coordinator.ts +31 -7
- package/src/internal/decimal.ts +19 -0
- package/src/internal/http-client.ts +608 -0
- package/src/managers/account-manager.ts +40 -32
- package/src/managers/market-manager.ts +37 -26
- package/src/managers/order-manager.ts +7 -8
- package/src/types/account.ts +27 -28
- package/src/types/market.ts +12 -12
- package/src/types/order.ts +6 -7
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { createHmac } from "node:crypto";
|
|
2
2
|
import BigNumber from "bignumber.js";
|
|
3
|
+
import {
|
|
4
|
+
type HttpClientMessages,
|
|
5
|
+
type HttpRetryPolicy,
|
|
6
|
+
httpRequest,
|
|
7
|
+
} from "../../internal/http-client.ts";
|
|
3
8
|
import { createManagedWebSocket } from "../../internal/managed-websocket.ts";
|
|
4
9
|
import type {
|
|
5
10
|
AccountCredentials,
|
|
@@ -25,6 +30,7 @@ import type {
|
|
|
25
30
|
|
|
26
31
|
type TimerHandle = ReturnType<typeof setInterval>;
|
|
27
32
|
type SignedRequestMethod = "GET" | "POST" | "DELETE";
|
|
33
|
+
type FetchLike = typeof fetch;
|
|
28
34
|
|
|
29
35
|
interface BinancePapiBalance {
|
|
30
36
|
asset?: string;
|
|
@@ -144,7 +150,31 @@ type BinancePrivateMessage =
|
|
|
144
150
|
const BINANCE_PAPI_REST_BASE_URL = "https://papi.binance.com";
|
|
145
151
|
const BINANCE_PAPI_WS_BASE_URL = "wss://fstream.binance.com/pm/ws";
|
|
146
152
|
const DEFAULT_RECV_WINDOW = 5_000;
|
|
153
|
+
const DEFAULT_HTTP_TIMEOUT_MS = 10_000;
|
|
147
154
|
const USDM_QUOTE_ASSETS = ["FDUSD", "USDC", "BUSD", "USDT"];
|
|
155
|
+
const SAFE_READ_RETRY_POLICY: HttpRetryPolicy = {
|
|
156
|
+
idempotent: true,
|
|
157
|
+
maxAttempts: 3,
|
|
158
|
+
};
|
|
159
|
+
const NO_RETRY_POLICY: HttpRetryPolicy = {
|
|
160
|
+
idempotent: false,
|
|
161
|
+
maxAttempts: 1,
|
|
162
|
+
};
|
|
163
|
+
const LISTEN_KEY_KEEPALIVE_RETRY_POLICY: HttpRetryPolicy = {
|
|
164
|
+
idempotent: true,
|
|
165
|
+
maxAttempts: 3,
|
|
166
|
+
};
|
|
167
|
+
function getBinancePapiHttpMessages(timeoutMs: number): HttpClientMessages {
|
|
168
|
+
return {
|
|
169
|
+
http: ({ status, statusText, url, rawBody }) =>
|
|
170
|
+
`Binance PAPI request failed: ${status} ${statusText ?? ""} ${url}${
|
|
171
|
+
rawBody ? ` ${rawBody}` : ""
|
|
172
|
+
}`,
|
|
173
|
+
timeout: () => `Binance PAPI fetch timeout after ${timeoutMs}ms`,
|
|
174
|
+
aborted: () => "Binance PAPI fetch aborted",
|
|
175
|
+
parse: ({ url }) => `Binance PAPI response parse failed: ${url}`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
148
178
|
|
|
149
179
|
function requirePrivateCredentials(credentials: AccountCredentials): {
|
|
150
180
|
apiKey: string;
|
|
@@ -521,21 +551,6 @@ function mapOrderUpdate(
|
|
|
521
551
|
};
|
|
522
552
|
}
|
|
523
553
|
|
|
524
|
-
async function readJson<T>(response: Response, url: string): Promise<T> {
|
|
525
|
-
const text = await response.text();
|
|
526
|
-
if (!response.ok) {
|
|
527
|
-
throw new Error(
|
|
528
|
-
`Binance PAPI request failed: ${response.status} ${response.statusText} ${url} ${text}`,
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
if (!text) {
|
|
533
|
-
return {} as T;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
return JSON.parse(text) as T;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
554
|
export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
540
555
|
readonly venue = "binance" as const;
|
|
541
556
|
readonly readOnly = false;
|
|
@@ -569,6 +584,13 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
569
584
|
clientOrderId: true,
|
|
570
585
|
};
|
|
571
586
|
|
|
587
|
+
constructor(
|
|
588
|
+
private readonly options: {
|
|
589
|
+
readonly fetchFn?: FetchLike;
|
|
590
|
+
readonly httpTimeoutMs?: number;
|
|
591
|
+
} = {},
|
|
592
|
+
) {}
|
|
593
|
+
|
|
572
594
|
async bootstrapAccount(
|
|
573
595
|
credentials: AccountCredentials,
|
|
574
596
|
accountOptions?: Record<string, unknown>,
|
|
@@ -580,18 +602,24 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
580
602
|
"/papi/v1/balance",
|
|
581
603
|
credentials,
|
|
582
604
|
accountOptions,
|
|
605
|
+
undefined,
|
|
606
|
+
SAFE_READ_RETRY_POLICY,
|
|
583
607
|
),
|
|
584
608
|
this.signedRequest<BinancePapiAccount>(
|
|
585
609
|
"GET",
|
|
586
610
|
"/papi/v1/account",
|
|
587
611
|
credentials,
|
|
588
612
|
accountOptions,
|
|
613
|
+
undefined,
|
|
614
|
+
SAFE_READ_RETRY_POLICY,
|
|
589
615
|
),
|
|
590
616
|
this.signedRequest<BinancePapiUmPosition[]>(
|
|
591
617
|
"GET",
|
|
592
618
|
"/papi/v1/um/positionRisk",
|
|
593
619
|
credentials,
|
|
594
620
|
accountOptions,
|
|
621
|
+
undefined,
|
|
622
|
+
SAFE_READ_RETRY_POLICY,
|
|
595
623
|
),
|
|
596
624
|
]);
|
|
597
625
|
|
|
@@ -621,12 +649,16 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
621
649
|
"/papi/v1/account",
|
|
622
650
|
credentials,
|
|
623
651
|
accountOptions,
|
|
652
|
+
undefined,
|
|
653
|
+
SAFE_READ_RETRY_POLICY,
|
|
624
654
|
),
|
|
625
655
|
this.signedRequest<BinancePapiUmPosition[]>(
|
|
626
656
|
"GET",
|
|
627
657
|
"/papi/v1/um/positionRisk",
|
|
628
658
|
credentials,
|
|
629
659
|
accountOptions,
|
|
660
|
+
undefined,
|
|
661
|
+
SAFE_READ_RETRY_POLICY,
|
|
630
662
|
),
|
|
631
663
|
]);
|
|
632
664
|
|
|
@@ -643,6 +675,8 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
643
675
|
"/papi/v1/um/openOrders",
|
|
644
676
|
credentials,
|
|
645
677
|
accountOptions,
|
|
678
|
+
undefined,
|
|
679
|
+
SAFE_READ_RETRY_POLICY,
|
|
646
680
|
);
|
|
647
681
|
|
|
648
682
|
return orders.flatMap((order) => {
|
|
@@ -681,6 +715,7 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
681
715
|
: `${request.reduceOnly}`,
|
|
682
716
|
positionSide: encodePositionSide(request.positionSide),
|
|
683
717
|
},
|
|
718
|
+
NO_RETRY_POLICY,
|
|
684
719
|
);
|
|
685
720
|
|
|
686
721
|
const mapped = mapOpenOrder(response, receivedAt);
|
|
@@ -709,6 +744,7 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
709
744
|
orderId: request.orderId,
|
|
710
745
|
origClientOrderId: request.clientOrderId,
|
|
711
746
|
},
|
|
747
|
+
NO_RETRY_POLICY,
|
|
712
748
|
);
|
|
713
749
|
|
|
714
750
|
const mapped = mapOpenOrder(response, receivedAt);
|
|
@@ -735,6 +771,7 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
735
771
|
{
|
|
736
772
|
symbol: encodeUmSymbol(request.symbol),
|
|
737
773
|
},
|
|
774
|
+
NO_RETRY_POLICY,
|
|
738
775
|
);
|
|
739
776
|
|
|
740
777
|
return responses.flatMap((response) => {
|
|
@@ -863,6 +900,7 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
863
900
|
credentials: AccountCredentials,
|
|
864
901
|
accountOptions?: Record<string, unknown>,
|
|
865
902
|
queryParams?: Record<string, string | undefined>,
|
|
903
|
+
retryPolicy?: HttpRetryPolicy,
|
|
866
904
|
): Promise<T> {
|
|
867
905
|
const { apiKey, secret } = requirePrivateCredentials(credentials);
|
|
868
906
|
const params = new URLSearchParams();
|
|
@@ -882,14 +920,22 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
882
920
|
params.set("signature", signQuery(params.toString(), secret));
|
|
883
921
|
|
|
884
922
|
const url = `${BINANCE_PAPI_REST_BASE_URL}${path}?${params.toString()}`;
|
|
885
|
-
const
|
|
923
|
+
const timeoutMs = this.options.httpTimeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
|
|
924
|
+
const response = await httpRequest<T>({
|
|
925
|
+
fetchFn: this.options.fetchFn,
|
|
926
|
+
url,
|
|
886
927
|
method,
|
|
887
928
|
headers: {
|
|
888
929
|
"X-MBX-APIKEY": apiKey,
|
|
889
930
|
},
|
|
931
|
+
timeoutMs,
|
|
932
|
+
parseAs: "json",
|
|
933
|
+
emptyBody: "empty_object",
|
|
934
|
+
retryPolicy: retryPolicy ?? NO_RETRY_POLICY,
|
|
935
|
+
messages: getBinancePapiHttpMessages(timeoutMs),
|
|
890
936
|
});
|
|
891
937
|
|
|
892
|
-
return
|
|
938
|
+
return response.body;
|
|
893
939
|
}
|
|
894
940
|
|
|
895
941
|
private async startUserDataStream(
|
|
@@ -898,6 +944,8 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
898
944
|
const response = await this.userStreamRequest<BinanceListenKeyResponse>(
|
|
899
945
|
"POST",
|
|
900
946
|
credentials,
|
|
947
|
+
undefined,
|
|
948
|
+
NO_RETRY_POLICY,
|
|
901
949
|
);
|
|
902
950
|
if (!response.listenKey) {
|
|
903
951
|
throw new Error("Binance PAPI did not return a listenKey");
|
|
@@ -914,6 +962,7 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
914
962
|
"PUT",
|
|
915
963
|
credentials,
|
|
916
964
|
listenKey,
|
|
965
|
+
LISTEN_KEY_KEEPALIVE_RETRY_POLICY,
|
|
917
966
|
);
|
|
918
967
|
}
|
|
919
968
|
|
|
@@ -925,6 +974,7 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
925
974
|
"DELETE",
|
|
926
975
|
credentials,
|
|
927
976
|
listenKey,
|
|
977
|
+
NO_RETRY_POLICY,
|
|
928
978
|
);
|
|
929
979
|
}
|
|
930
980
|
|
|
@@ -932,6 +982,7 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
932
982
|
method: "POST" | "PUT" | "DELETE",
|
|
933
983
|
credentials: AccountCredentials,
|
|
934
984
|
listenKey?: string,
|
|
985
|
+
retryPolicy: HttpRetryPolicy = NO_RETRY_POLICY,
|
|
935
986
|
): Promise<T> {
|
|
936
987
|
const { apiKey } = requirePrivateCredentials(credentials);
|
|
937
988
|
const params = new URLSearchParams();
|
|
@@ -943,13 +994,21 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
943
994
|
const url = `${BINANCE_PAPI_REST_BASE_URL}/papi/v1/listenKey${
|
|
944
995
|
query ? `?${query}` : ""
|
|
945
996
|
}`;
|
|
946
|
-
const
|
|
997
|
+
const timeoutMs = this.options.httpTimeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
|
|
998
|
+
const response = await httpRequest<T>({
|
|
999
|
+
fetchFn: this.options.fetchFn,
|
|
1000
|
+
url,
|
|
947
1001
|
method,
|
|
948
1002
|
headers: {
|
|
949
1003
|
"X-MBX-APIKEY": apiKey,
|
|
950
1004
|
},
|
|
1005
|
+
timeoutMs,
|
|
1006
|
+
parseAs: "json",
|
|
1007
|
+
emptyBody: "empty_object",
|
|
1008
|
+
retryPolicy,
|
|
1009
|
+
messages: getBinancePapiHttpMessages(timeoutMs),
|
|
951
1010
|
});
|
|
952
1011
|
|
|
953
|
-
return
|
|
1012
|
+
return response.body;
|
|
954
1013
|
}
|
|
955
1014
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import BigNumber from "bignumber.js";
|
|
2
2
|
import { AcexError } from "../../errors.ts";
|
|
3
|
+
import {
|
|
4
|
+
type HttpClientMessages,
|
|
5
|
+
httpRequest,
|
|
6
|
+
} from "../../internal/http-client.ts";
|
|
3
7
|
import type {
|
|
4
8
|
AccountCredentials,
|
|
5
9
|
VenueAccountCapabilities,
|
|
@@ -66,6 +70,8 @@ interface JuplendPriceApiEntry {
|
|
|
66
70
|
decimals?: number | string;
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
type FetchLike = typeof fetch;
|
|
74
|
+
|
|
69
75
|
interface JuplendTokenSearchEntry {
|
|
70
76
|
id?: string;
|
|
71
77
|
address?: string;
|
|
@@ -86,6 +92,13 @@ const DEFAULT_HTTP_TIMEOUT_MS = 10_000;
|
|
|
86
92
|
// not mint-atomic token amounts.
|
|
87
93
|
const POSITION_AMOUNT_SCALE_DECIMALS = 9;
|
|
88
94
|
const VAULT_CACHE_TTL_MS = 60 * 60 * 1_000;
|
|
95
|
+
function getJuplendHttpMessages(timeoutMs: number): HttpClientMessages {
|
|
96
|
+
return {
|
|
97
|
+
http: ({ status, statusText }) => `Juplend HTTP ${status}: ${statusText}`,
|
|
98
|
+
timeout: () => `Juplend fetch timeout after ${timeoutMs}ms`,
|
|
99
|
+
aborted: () => "Juplend fetch aborted",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
89
102
|
|
|
90
103
|
interface JuplendVaultEnrichmentCacheEntry {
|
|
91
104
|
loadedAt: number;
|
|
@@ -260,52 +273,30 @@ function buildRisk(input: {
|
|
|
260
273
|
};
|
|
261
274
|
}
|
|
262
275
|
|
|
263
|
-
async function
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
signal: controller.signal,
|
|
286
|
-
});
|
|
287
|
-
if (!response.ok) {
|
|
288
|
-
throw new Error(
|
|
289
|
-
`Juplend HTTP ${response.status}: ${response.statusText}`,
|
|
290
|
-
);
|
|
291
|
-
}
|
|
276
|
+
async function requestJuplendJson<T>(
|
|
277
|
+
url: string,
|
|
278
|
+
init: RequestInit | undefined,
|
|
279
|
+
fetchFn: FetchLike | undefined,
|
|
280
|
+
timeoutMs: number,
|
|
281
|
+
): Promise<T> {
|
|
282
|
+
const response = await httpRequest<T>({
|
|
283
|
+
fetchFn,
|
|
284
|
+
url,
|
|
285
|
+
method: init?.method,
|
|
286
|
+
headers: init?.headers,
|
|
287
|
+
body: init?.body,
|
|
288
|
+
signal: init?.signal ?? undefined,
|
|
289
|
+
timeoutMs,
|
|
290
|
+
parseAs: "json",
|
|
291
|
+
jsonParseMode: "response",
|
|
292
|
+
retryPolicy: {
|
|
293
|
+
idempotent: true,
|
|
294
|
+
maxAttempts: 3,
|
|
295
|
+
},
|
|
296
|
+
messages: getJuplendHttpMessages(timeoutMs),
|
|
297
|
+
});
|
|
292
298
|
|
|
293
|
-
|
|
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
|
-
}
|
|
299
|
+
return response.body;
|
|
309
300
|
}
|
|
310
301
|
|
|
311
302
|
function getJupApiKey(explicitApiKey?: string): string | undefined {
|
|
@@ -326,12 +317,19 @@ function withBaseUrl(baseUrl: string, path: string): string {
|
|
|
326
317
|
|
|
327
318
|
async function loadVaultMetadataFromLiteApi(
|
|
328
319
|
apiKey?: string,
|
|
320
|
+
fetchFn?: FetchLike,
|
|
321
|
+
timeoutMs = DEFAULT_HTTP_TIMEOUT_MS,
|
|
329
322
|
): Promise<Map<string, JuplendVaultMetadata>> {
|
|
330
|
-
const response = await
|
|
323
|
+
const response = await requestJuplendJson<
|
|
331
324
|
JuplendVaultMetadata[] | { data?: JuplendVaultMetadata[] }
|
|
332
|
-
>(
|
|
333
|
-
|
|
334
|
-
|
|
325
|
+
>(
|
|
326
|
+
withBaseUrl(JUP_LITE_API_BASE_URL, LEND_VAULTS_PATH),
|
|
327
|
+
{
|
|
328
|
+
headers: buildApiHeaders(apiKey),
|
|
329
|
+
},
|
|
330
|
+
fetchFn,
|
|
331
|
+
timeoutMs,
|
|
332
|
+
);
|
|
335
333
|
const rawVaults = Array.isArray(response) ? response : response.data;
|
|
336
334
|
const vaults = new Map<string, JuplendVaultMetadata>();
|
|
337
335
|
|
|
@@ -348,17 +346,21 @@ async function loadVaultMetadataFromLiteApi(
|
|
|
348
346
|
async function loadTokenSearchMap(
|
|
349
347
|
mintAddresses: string[],
|
|
350
348
|
apiKey?: string,
|
|
349
|
+
fetchFn?: FetchLike,
|
|
350
|
+
timeoutMs = DEFAULT_HTTP_TIMEOUT_MS,
|
|
351
351
|
): Promise<Map<string, JuplendTokenMetadata>> {
|
|
352
352
|
if (mintAddresses.length === 0) {
|
|
353
353
|
return new Map();
|
|
354
354
|
}
|
|
355
355
|
|
|
356
356
|
const query = encodeURIComponent(mintAddresses.join(","));
|
|
357
|
-
const response = await
|
|
357
|
+
const response = await requestJuplendJson<JuplendTokenSearchEntry[]>(
|
|
358
358
|
`${withBaseUrl(JUP_API_BASE_URL, TOKENS_SEARCH_PATH)}?query=${query}`,
|
|
359
359
|
{
|
|
360
360
|
headers: buildApiHeaders(apiKey),
|
|
361
361
|
},
|
|
362
|
+
fetchFn,
|
|
363
|
+
timeoutMs,
|
|
362
364
|
);
|
|
363
365
|
|
|
364
366
|
const tokens = new Map<string, JuplendTokenMetadata>();
|
|
@@ -385,17 +387,23 @@ async function loadTokenSearchMap(
|
|
|
385
387
|
async function loadPriceMap(
|
|
386
388
|
mintAddresses: string[],
|
|
387
389
|
apiKey?: string,
|
|
390
|
+
fetchFn?: FetchLike,
|
|
391
|
+
timeoutMs = DEFAULT_HTTP_TIMEOUT_MS,
|
|
388
392
|
): Promise<Map<string, JuplendPriceApiEntry>> {
|
|
389
393
|
if (mintAddresses.length === 0) {
|
|
390
394
|
return new Map();
|
|
391
395
|
}
|
|
392
396
|
|
|
393
397
|
const ids = encodeURIComponent(mintAddresses.join(","));
|
|
394
|
-
const response = await
|
|
398
|
+
const response = await requestJuplendJson<
|
|
399
|
+
Record<string, JuplendPriceApiEntry>
|
|
400
|
+
>(
|
|
395
401
|
`${withBaseUrl(JUP_API_BASE_URL, PRICE_V3_PATH)}?ids=${ids}`,
|
|
396
402
|
{
|
|
397
403
|
headers: buildApiHeaders(apiKey),
|
|
398
404
|
},
|
|
405
|
+
fetchFn,
|
|
406
|
+
timeoutMs,
|
|
399
407
|
);
|
|
400
408
|
|
|
401
409
|
return new Map(Object.entries(response ?? {}));
|
|
@@ -436,6 +444,8 @@ function mergeTokenMetadata(
|
|
|
436
444
|
async function enrichVaultsWithJupApi(input: {
|
|
437
445
|
apiKey?: string;
|
|
438
446
|
baseVaults: Map<string, JuplendVaultMetadata>;
|
|
447
|
+
fetchFn?: FetchLike;
|
|
448
|
+
timeoutMs: number;
|
|
439
449
|
}): Promise<Map<string, JuplendVaultMetadata>> {
|
|
440
450
|
const mintAddresses = new Set<string>();
|
|
441
451
|
for (const vault of input.baseVaults.values()) {
|
|
@@ -450,8 +460,18 @@ async function enrichVaultsWithJupApi(input: {
|
|
|
450
460
|
}
|
|
451
461
|
|
|
452
462
|
const [tokenMap, priceMap] = await Promise.all([
|
|
453
|
-
loadTokenSearchMap(
|
|
454
|
-
|
|
463
|
+
loadTokenSearchMap(
|
|
464
|
+
[...mintAddresses],
|
|
465
|
+
input.apiKey,
|
|
466
|
+
input.fetchFn,
|
|
467
|
+
input.timeoutMs,
|
|
468
|
+
),
|
|
469
|
+
loadPriceMap(
|
|
470
|
+
[...mintAddresses],
|
|
471
|
+
input.apiKey,
|
|
472
|
+
input.fetchFn,
|
|
473
|
+
input.timeoutMs,
|
|
474
|
+
),
|
|
455
475
|
]);
|
|
456
476
|
|
|
457
477
|
const enriched = new Map<string, JuplendVaultMetadata>();
|
|
@@ -480,6 +500,8 @@ async function enrichVaultsWithJupApi(input: {
|
|
|
480
500
|
async function loadVaults(
|
|
481
501
|
now: number,
|
|
482
502
|
apiKey?: string,
|
|
503
|
+
fetchFn?: FetchLike,
|
|
504
|
+
timeoutMs = DEFAULT_HTTP_TIMEOUT_MS,
|
|
483
505
|
): Promise<Map<string, JuplendVaultMetadata>> {
|
|
484
506
|
const cacheKey = getEnrichmentCacheKey(apiKey);
|
|
485
507
|
const cached = enrichmentCache.get(cacheKey);
|
|
@@ -492,7 +514,11 @@ async function loadVaults(
|
|
|
492
514
|
const inflight = enrichmentCachePromise.get(cacheKey);
|
|
493
515
|
if (!inflight) {
|
|
494
516
|
const nextPromise = (async () => {
|
|
495
|
-
const baseVaults = await loadVaultMetadataFromLiteApi(
|
|
517
|
+
const baseVaults = await loadVaultMetadataFromLiteApi(
|
|
518
|
+
apiKey,
|
|
519
|
+
fetchFn,
|
|
520
|
+
timeoutMs,
|
|
521
|
+
);
|
|
496
522
|
if (!apiKey) {
|
|
497
523
|
enrichmentCache.set(cacheKey, {
|
|
498
524
|
loadedAt: now,
|
|
@@ -506,6 +532,8 @@ async function loadVaults(
|
|
|
506
532
|
const enrichedVaults = await enrichVaultsWithJupApi({
|
|
507
533
|
apiKey,
|
|
508
534
|
baseVaults,
|
|
535
|
+
fetchFn,
|
|
536
|
+
timeoutMs,
|
|
509
537
|
});
|
|
510
538
|
enrichmentCache.set(cacheKey, {
|
|
511
539
|
loadedAt: now,
|
|
@@ -545,9 +573,11 @@ async function mapAccount(
|
|
|
545
573
|
receivedAt: number,
|
|
546
574
|
rpcUrl: string | undefined,
|
|
547
575
|
jupApiKey: string | undefined,
|
|
576
|
+
fetchFn: FetchLike | undefined,
|
|
577
|
+
timeoutMs: number,
|
|
548
578
|
): Promise<JuplendMappedAccount> {
|
|
549
579
|
const [vaults, positionResult] = await Promise.all([
|
|
550
|
-
loadVaults(receivedAt, jupApiKey),
|
|
580
|
+
loadVaults(receivedAt, jupApiKey, fetchFn, timeoutMs),
|
|
551
581
|
readJuplendPositions({
|
|
552
582
|
walletAddress: accountOptions.walletAddress,
|
|
553
583
|
vaultId: accountOptions.vaultId,
|
|
@@ -666,6 +696,10 @@ export class JuplendPrivateAdapter implements PrivateUserDataAdapter {
|
|
|
666
696
|
constructor(
|
|
667
697
|
private readonly rpcUrl?: string,
|
|
668
698
|
private readonly jupApiKey?: string,
|
|
699
|
+
private readonly options: {
|
|
700
|
+
readonly fetchFn?: FetchLike;
|
|
701
|
+
readonly httpTimeoutMs?: number;
|
|
702
|
+
} = {},
|
|
669
703
|
) {}
|
|
670
704
|
|
|
671
705
|
async bootstrapAccount(
|
|
@@ -679,6 +713,8 @@ export class JuplendPrivateAdapter implements PrivateUserDataAdapter {
|
|
|
679
713
|
receivedAt,
|
|
680
714
|
this.rpcUrl,
|
|
681
715
|
getJupApiKey(this.jupApiKey),
|
|
716
|
+
this.options.fetchFn,
|
|
717
|
+
this.options.httpTimeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS,
|
|
682
718
|
);
|
|
683
719
|
|
|
684
720
|
return {
|
|
@@ -3,7 +3,12 @@ import type {
|
|
|
3
3
|
StreamHandle,
|
|
4
4
|
} from "../adapters/types.ts";
|
|
5
5
|
import { AcexError } from "../errors.ts";
|
|
6
|
-
import
|
|
6
|
+
import { isTransportError, redactSecrets } from "../internal/http-client.ts";
|
|
7
|
+
import type {
|
|
8
|
+
AccountRuntimeOptions,
|
|
9
|
+
PrivateRuntimeReason,
|
|
10
|
+
Venue,
|
|
11
|
+
} from "../types/index.ts";
|
|
7
12
|
import type {
|
|
8
13
|
ClientContext,
|
|
9
14
|
PrivateAccountDataConsumer,
|
|
@@ -41,6 +46,23 @@ function normalizePositiveInterval(
|
|
|
41
46
|
: fallback;
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
function transportReason(
|
|
50
|
+
error: unknown,
|
|
51
|
+
fallback: PrivateRuntimeReason,
|
|
52
|
+
): PrivateRuntimeReason {
|
|
53
|
+
return isTransportError(error) && error.kind === "rate_limited"
|
|
54
|
+
? "rate_limited"
|
|
55
|
+
: fallback;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function bootstrapErrorDetail(error: unknown): string {
|
|
59
|
+
if (!(error instanceof Error) || !error.message) {
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return ` (${redactSecrets(error.message)})`;
|
|
64
|
+
}
|
|
65
|
+
|
|
44
66
|
export class PrivateSubscriptionCoordinator {
|
|
45
67
|
private readonly context: ClientContext;
|
|
46
68
|
private readonly adapters: Map<Venue, PrivateUserDataAdapter>;
|
|
@@ -435,7 +457,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
435
457
|
{
|
|
436
458
|
runtimeStatus: "degraded",
|
|
437
459
|
ready: record.accountReady,
|
|
438
|
-
reason: "http_failed",
|
|
460
|
+
reason: transportReason(error, "http_failed"),
|
|
439
461
|
},
|
|
440
462
|
);
|
|
441
463
|
}
|
|
@@ -559,7 +581,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
559
581
|
{
|
|
560
582
|
runtimeStatus: "degraded",
|
|
561
583
|
ready: record.accountReady,
|
|
562
|
-
reason: "http_failed",
|
|
584
|
+
reason: transportReason(error, "http_failed"),
|
|
563
585
|
},
|
|
564
586
|
);
|
|
565
587
|
}
|
|
@@ -676,11 +698,13 @@ export class PrivateSubscriptionCoordinator {
|
|
|
676
698
|
{
|
|
677
699
|
runtimeStatus: "degraded",
|
|
678
700
|
ready: false,
|
|
679
|
-
reason:
|
|
701
|
+
reason: transportReason(
|
|
702
|
+
error,
|
|
703
|
+
record.venue === "juplend" ? "http_failed" : "auth_failed",
|
|
704
|
+
),
|
|
680
705
|
},
|
|
681
706
|
);
|
|
682
|
-
const reason =
|
|
683
|
-
error instanceof Error && error.message ? ` (${error.message})` : "";
|
|
707
|
+
const reason = bootstrapErrorDetail(error);
|
|
684
708
|
throw new AcexError(
|
|
685
709
|
"ACCOUNT_BOOTSTRAP_FAILED",
|
|
686
710
|
`Failed to bootstrap account data: ${record.accountId}${reason}`,
|
|
@@ -727,7 +751,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
727
751
|
{
|
|
728
752
|
runtimeStatus: "degraded",
|
|
729
753
|
ready: false,
|
|
730
|
-
reason: "auth_failed",
|
|
754
|
+
reason: transportReason(error, "auth_failed"),
|
|
731
755
|
},
|
|
732
756
|
);
|
|
733
757
|
throw new AcexError(
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import BigNumber from "bignumber.js";
|
|
2
|
+
import type { DecimalInput } from "../types/index.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Convert a decimal value to its canonical string form: full precision, no
|
|
6
|
+
* scientific notation, no trailing zeros.
|
|
7
|
+
*
|
|
8
|
+
* Throws on non-finite input (NaN / Infinity) so producers can never leak
|
|
9
|
+
* sentinel strings into public output fields. Call sites that legitimately
|
|
10
|
+
* accept non-finite input (e.g. order-input validation) must guard before
|
|
11
|
+
* calling this.
|
|
12
|
+
*/
|
|
13
|
+
export function toCanonical(value: DecimalInput): string {
|
|
14
|
+
const bn = new BigNumber(value);
|
|
15
|
+
if (!bn.isFinite()) {
|
|
16
|
+
throw new RangeError(`invalid non-finite DecimalInput: ${bn.toString()}`);
|
|
17
|
+
}
|
|
18
|
+
return bn.toFixed();
|
|
19
|
+
}
|