@imbingox/acex 0.3.1-beta.0 → 0.4.0-beta.10
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 +11 -10
- package/docs/api.md +502 -1030
- package/package.json +1 -1
- package/src/adapters/binance/adapter.ts +19 -1
- package/src/adapters/binance/market-catalog.ts +93 -22
- package/src/adapters/binance/private-adapter.ts +302 -59
- package/src/adapters/binance/rate-limit.ts +47 -0
- package/src/adapters/binance/server-time.ts +106 -0
- package/src/adapters/juplend/private-adapter.ts +97 -68
- package/src/adapters/types.ts +25 -1
- package/src/client/context.ts +26 -9
- package/src/client/private-subscription-coordinator.ts +898 -63
- package/src/client/runtime.ts +49 -11
- package/src/client/venue-capabilities.ts +1 -0
- package/src/errors.ts +156 -2
- package/src/index.ts +8 -1
- package/src/internal/decimal.ts +19 -0
- package/src/internal/http-client.ts +608 -0
- package/src/internal/rate-limiter.ts +181 -0
- package/src/internal/watermark.ts +83 -0
- package/src/managers/account-manager.ts +267 -55
- package/src/managers/market-manager.ts +261 -60
- package/src/managers/order-manager.ts +798 -84
- package/src/types/account.ts +27 -28
- package/src/types/client.ts +1 -0
- package/src/types/market.ts +37 -12
- package/src/types/order.ts +7 -7
- package/src/types/shared.ts +66 -0
|
@@ -1,9 +1,18 @@
|
|
|
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
|
+
isTransportError,
|
|
8
|
+
} from "../../internal/http-client.ts";
|
|
3
9
|
import { createManagedWebSocket } from "../../internal/managed-websocket.ts";
|
|
4
10
|
import type {
|
|
5
11
|
AccountCredentials,
|
|
6
12
|
PositionSide,
|
|
13
|
+
RateLimiter,
|
|
14
|
+
RateLimitScope,
|
|
15
|
+
TimeProvider,
|
|
7
16
|
VenueAccountCapabilities,
|
|
8
17
|
VenueOrderCapabilities,
|
|
9
18
|
} from "../../types/index.ts";
|
|
@@ -11,20 +20,24 @@ import type {
|
|
|
11
20
|
CancelAllOrdersRequest,
|
|
12
21
|
CancelOrderRequest,
|
|
13
22
|
CreateOrderRequest,
|
|
23
|
+
FetchOrderRequest,
|
|
14
24
|
PrivateStreamCallbacks,
|
|
15
25
|
PrivateStreamOptions,
|
|
16
26
|
PrivateUserDataAdapter,
|
|
17
27
|
RawAccountBootstrap,
|
|
18
28
|
RawAccountUpdate,
|
|
19
29
|
RawBalanceUpdate,
|
|
30
|
+
RawOpenOrdersSnapshot,
|
|
20
31
|
RawOrderUpdate,
|
|
21
32
|
RawPositionUpdate,
|
|
22
33
|
RawRiskUpdate,
|
|
23
34
|
StreamHandle,
|
|
24
35
|
} from "../types.ts";
|
|
36
|
+
import { parseBinanceRateLimitUsage } from "./rate-limit.ts";
|
|
25
37
|
|
|
26
38
|
type TimerHandle = ReturnType<typeof setInterval>;
|
|
27
39
|
type SignedRequestMethod = "GET" | "POST" | "DELETE";
|
|
40
|
+
type FetchLike = typeof fetch;
|
|
28
41
|
|
|
29
42
|
interface BinancePapiBalance {
|
|
30
43
|
asset?: string;
|
|
@@ -144,7 +157,32 @@ type BinancePrivateMessage =
|
|
|
144
157
|
const BINANCE_PAPI_REST_BASE_URL = "https://papi.binance.com";
|
|
145
158
|
const BINANCE_PAPI_WS_BASE_URL = "wss://fstream.binance.com/pm/ws";
|
|
146
159
|
const DEFAULT_RECV_WINDOW = 5_000;
|
|
160
|
+
const DEFAULT_HTTP_TIMEOUT_MS = 10_000;
|
|
147
161
|
const USDM_QUOTE_ASSETS = ["FDUSD", "USDC", "BUSD", "USDT"];
|
|
162
|
+
const SAFE_READ_RETRY_POLICY: HttpRetryPolicy = {
|
|
163
|
+
idempotent: true,
|
|
164
|
+
maxAttempts: 3,
|
|
165
|
+
};
|
|
166
|
+
const NO_RETRY_POLICY: HttpRetryPolicy = {
|
|
167
|
+
idempotent: false,
|
|
168
|
+
maxAttempts: 1,
|
|
169
|
+
};
|
|
170
|
+
const LISTEN_KEY_KEEPALIVE_RETRY_POLICY: HttpRetryPolicy = {
|
|
171
|
+
idempotent: true,
|
|
172
|
+
maxAttempts: 3,
|
|
173
|
+
};
|
|
174
|
+
const BINANCE_ORDER_NOT_FOUND_CODES = new Set(["-2011", "-2013"]);
|
|
175
|
+
function getBinancePapiHttpMessages(timeoutMs: number): HttpClientMessages {
|
|
176
|
+
return {
|
|
177
|
+
http: ({ status, statusText, url, rawBody }) =>
|
|
178
|
+
`Binance PAPI request failed: ${status} ${statusText ?? ""} ${url}${
|
|
179
|
+
rawBody ? ` ${rawBody}` : ""
|
|
180
|
+
}`,
|
|
181
|
+
timeout: () => `Binance PAPI fetch timeout after ${timeoutMs}ms`,
|
|
182
|
+
aborted: () => "Binance PAPI fetch aborted",
|
|
183
|
+
parse: ({ url }) => `Binance PAPI response parse failed: ${url}`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
148
186
|
|
|
149
187
|
function requirePrivateCredentials(credentials: AccountCredentials): {
|
|
150
188
|
apiKey: string;
|
|
@@ -174,6 +212,14 @@ function getNumberOption(
|
|
|
174
212
|
: undefined;
|
|
175
213
|
}
|
|
176
214
|
|
|
215
|
+
function getStringOption(
|
|
216
|
+
options: Record<string, unknown> | undefined,
|
|
217
|
+
key: string,
|
|
218
|
+
): string | undefined {
|
|
219
|
+
const value = options?.[key];
|
|
220
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
177
223
|
function signQuery(query: string, secret: string): string {
|
|
178
224
|
return createHmac("sha256", secret).update(query).digest("hex");
|
|
179
225
|
}
|
|
@@ -393,6 +439,27 @@ function mapAccountRefresh(
|
|
|
393
439
|
};
|
|
394
440
|
}
|
|
395
441
|
|
|
442
|
+
function mapAccountBootstrap(
|
|
443
|
+
balances: BinancePapiBalance[],
|
|
444
|
+
account: BinancePapiAccount,
|
|
445
|
+
positions: BinancePapiUmPosition[],
|
|
446
|
+
receivedAt: number,
|
|
447
|
+
): RawAccountBootstrap {
|
|
448
|
+
return {
|
|
449
|
+
balances: balances.flatMap((balance) => {
|
|
450
|
+
const mapped = mapBalance(balance, receivedAt);
|
|
451
|
+
return mapped ? [mapped] : [];
|
|
452
|
+
}),
|
|
453
|
+
positions: positions.flatMap((position) => {
|
|
454
|
+
const mapped = mapUmPosition(position, receivedAt);
|
|
455
|
+
return mapped ? [mapped] : [];
|
|
456
|
+
}),
|
|
457
|
+
risk: mapAccountRisk(account, receivedAt, positions),
|
|
458
|
+
exchangeTs: account.updateTime,
|
|
459
|
+
receivedAt,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
396
463
|
function mapOpenOrder(
|
|
397
464
|
input: BinancePapiOpenOrder,
|
|
398
465
|
receivedAt: number,
|
|
@@ -521,19 +588,26 @@ function mapOrderUpdate(
|
|
|
521
588
|
};
|
|
522
589
|
}
|
|
523
590
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
throw new Error(
|
|
528
|
-
`Binance PAPI request failed: ${response.status} ${response.statusText} ${url} ${text}`,
|
|
529
|
-
);
|
|
591
|
+
function isBinanceOrderNotFound(error: unknown): boolean {
|
|
592
|
+
if (!isTransportError(error) || error.kind !== "http") {
|
|
593
|
+
return false;
|
|
530
594
|
}
|
|
531
595
|
|
|
532
|
-
if (
|
|
533
|
-
return
|
|
596
|
+
if (error.status !== 400 && error.status !== 404) {
|
|
597
|
+
return false;
|
|
534
598
|
}
|
|
535
599
|
|
|
536
|
-
|
|
600
|
+
const rawBody = error.rawBody;
|
|
601
|
+
if (!rawBody) {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
try {
|
|
606
|
+
const parsed = JSON.parse(rawBody) as { code?: unknown };
|
|
607
|
+
return BINANCE_ORDER_NOT_FOUND_CODES.has(`${parsed.code}`);
|
|
608
|
+
} catch {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
537
611
|
}
|
|
538
612
|
|
|
539
613
|
export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
@@ -569,6 +643,15 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
569
643
|
clientOrderId: true,
|
|
570
644
|
};
|
|
571
645
|
|
|
646
|
+
constructor(
|
|
647
|
+
private readonly options: {
|
|
648
|
+
readonly fetchFn?: FetchLike;
|
|
649
|
+
readonly httpTimeoutMs?: number;
|
|
650
|
+
readonly signingClock?: TimeProvider;
|
|
651
|
+
readonly rateLimiter?: RateLimiter;
|
|
652
|
+
} = {},
|
|
653
|
+
) {}
|
|
654
|
+
|
|
572
655
|
async bootstrapAccount(
|
|
573
656
|
credentials: AccountCredentials,
|
|
574
657
|
accountOptions?: Record<string, unknown>,
|
|
@@ -580,34 +663,35 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
580
663
|
"/papi/v1/balance",
|
|
581
664
|
credentials,
|
|
582
665
|
accountOptions,
|
|
666
|
+
undefined,
|
|
667
|
+
SAFE_READ_RETRY_POLICY,
|
|
583
668
|
),
|
|
584
669
|
this.signedRequest<BinancePapiAccount>(
|
|
585
670
|
"GET",
|
|
586
671
|
"/papi/v1/account",
|
|
587
672
|
credentials,
|
|
588
673
|
accountOptions,
|
|
674
|
+
undefined,
|
|
675
|
+
SAFE_READ_RETRY_POLICY,
|
|
589
676
|
),
|
|
590
677
|
this.signedRequest<BinancePapiUmPosition[]>(
|
|
591
678
|
"GET",
|
|
592
679
|
"/papi/v1/um/positionRisk",
|
|
593
680
|
credentials,
|
|
594
681
|
accountOptions,
|
|
682
|
+
undefined,
|
|
683
|
+
SAFE_READ_RETRY_POLICY,
|
|
595
684
|
),
|
|
596
685
|
]);
|
|
597
686
|
|
|
598
|
-
return
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
}),
|
|
607
|
-
risk: mapAccountRisk(account, receivedAt, positions),
|
|
608
|
-
exchangeTs: account.updateTime,
|
|
609
|
-
receivedAt,
|
|
610
|
-
};
|
|
687
|
+
return mapAccountBootstrap(balances, account, positions, receivedAt);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async reconcileAccount(
|
|
691
|
+
credentials: AccountCredentials,
|
|
692
|
+
accountOptions?: Record<string, unknown>,
|
|
693
|
+
): Promise<RawAccountBootstrap> {
|
|
694
|
+
return this.bootstrapAccount(credentials, accountOptions);
|
|
611
695
|
}
|
|
612
696
|
|
|
613
697
|
async refreshAccount(
|
|
@@ -621,12 +705,16 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
621
705
|
"/papi/v1/account",
|
|
622
706
|
credentials,
|
|
623
707
|
accountOptions,
|
|
708
|
+
undefined,
|
|
709
|
+
SAFE_READ_RETRY_POLICY,
|
|
624
710
|
),
|
|
625
711
|
this.signedRequest<BinancePapiUmPosition[]>(
|
|
626
712
|
"GET",
|
|
627
713
|
"/papi/v1/um/positionRisk",
|
|
628
714
|
credentials,
|
|
629
715
|
accountOptions,
|
|
716
|
+
undefined,
|
|
717
|
+
SAFE_READ_RETRY_POLICY,
|
|
630
718
|
),
|
|
631
719
|
]);
|
|
632
720
|
|
|
@@ -637,18 +725,61 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
637
725
|
credentials: AccountCredentials,
|
|
638
726
|
accountOptions?: Record<string, unknown>,
|
|
639
727
|
): Promise<RawOrderUpdate[]> {
|
|
728
|
+
const snapshot = await this.fetchOpenOrders(credentials, accountOptions);
|
|
729
|
+
return snapshot.orders;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async fetchOpenOrders(
|
|
733
|
+
credentials: AccountCredentials,
|
|
734
|
+
accountOptions?: Record<string, unknown>,
|
|
735
|
+
): Promise<RawOpenOrdersSnapshot> {
|
|
640
736
|
const receivedAt = Date.now();
|
|
641
737
|
const orders = await this.signedRequest<BinancePapiOpenOrder[]>(
|
|
642
738
|
"GET",
|
|
643
739
|
"/papi/v1/um/openOrders",
|
|
644
740
|
credentials,
|
|
645
741
|
accountOptions,
|
|
742
|
+
undefined,
|
|
743
|
+
SAFE_READ_RETRY_POLICY,
|
|
646
744
|
);
|
|
647
745
|
|
|
648
|
-
return
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
746
|
+
return {
|
|
747
|
+
orders: orders.flatMap((order) => {
|
|
748
|
+
const mapped = mapOpenOrder(order, receivedAt);
|
|
749
|
+
return mapped ? [mapped] : [];
|
|
750
|
+
}),
|
|
751
|
+
snapshotReceivedAt: receivedAt,
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async fetchOrder(
|
|
756
|
+
credentials: AccountCredentials,
|
|
757
|
+
request: FetchOrderRequest,
|
|
758
|
+
accountOptions?: Record<string, unknown>,
|
|
759
|
+
): Promise<RawOrderUpdate | undefined> {
|
|
760
|
+
const receivedAt = Date.now();
|
|
761
|
+
try {
|
|
762
|
+
const response = await this.signedRequest<BinancePapiOpenOrder>(
|
|
763
|
+
"GET",
|
|
764
|
+
"/papi/v1/um/order",
|
|
765
|
+
credentials,
|
|
766
|
+
accountOptions,
|
|
767
|
+
{
|
|
768
|
+
symbol: encodeUmSymbol(request.symbol),
|
|
769
|
+
orderId: request.orderId,
|
|
770
|
+
origClientOrderId: request.clientOrderId,
|
|
771
|
+
},
|
|
772
|
+
SAFE_READ_RETRY_POLICY,
|
|
773
|
+
);
|
|
774
|
+
|
|
775
|
+
return mapOpenOrder(response, receivedAt);
|
|
776
|
+
} catch (error) {
|
|
777
|
+
if (isBinanceOrderNotFound(error)) {
|
|
778
|
+
return undefined;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
throw error;
|
|
782
|
+
}
|
|
652
783
|
}
|
|
653
784
|
|
|
654
785
|
async createOrder(
|
|
@@ -681,6 +812,7 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
681
812
|
: `${request.reduceOnly}`,
|
|
682
813
|
positionSide: encodePositionSide(request.positionSide),
|
|
683
814
|
},
|
|
815
|
+
NO_RETRY_POLICY,
|
|
684
816
|
);
|
|
685
817
|
|
|
686
818
|
const mapped = mapOpenOrder(response, receivedAt);
|
|
@@ -709,6 +841,7 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
709
841
|
orderId: request.orderId,
|
|
710
842
|
origClientOrderId: request.clientOrderId,
|
|
711
843
|
},
|
|
844
|
+
NO_RETRY_POLICY,
|
|
712
845
|
);
|
|
713
846
|
|
|
714
847
|
const mapped = mapOpenOrder(response, receivedAt);
|
|
@@ -735,6 +868,7 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
735
868
|
{
|
|
736
869
|
symbol: encodeUmSymbol(request.symbol),
|
|
737
870
|
},
|
|
871
|
+
NO_RETRY_POLICY,
|
|
738
872
|
);
|
|
739
873
|
|
|
740
874
|
return responses.flatMap((response) => {
|
|
@@ -747,7 +881,7 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
747
881
|
credentials: AccountCredentials,
|
|
748
882
|
callbacks: PrivateStreamCallbacks,
|
|
749
883
|
options: PrivateStreamOptions,
|
|
750
|
-
|
|
884
|
+
accountOptions?: Record<string, unknown>,
|
|
751
885
|
): StreamHandle {
|
|
752
886
|
let closed = false;
|
|
753
887
|
let listenKey: string | undefined;
|
|
@@ -769,17 +903,19 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
769
903
|
|
|
770
904
|
const key = listenKey;
|
|
771
905
|
listenKey = undefined;
|
|
772
|
-
void this.closeUserDataStream(credentials, key).catch(
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
906
|
+
void this.closeUserDataStream(credentials, key, accountOptions).catch(
|
|
907
|
+
(error) => {
|
|
908
|
+
callbacks.onError(
|
|
909
|
+
error instanceof Error
|
|
910
|
+
? error
|
|
911
|
+
: new Error("Failed to close Binance PAPI listenKey"),
|
|
912
|
+
);
|
|
913
|
+
},
|
|
914
|
+
);
|
|
779
915
|
};
|
|
780
916
|
|
|
781
917
|
const ready = (async () => {
|
|
782
|
-
listenKey = await this.startUserDataStream(credentials);
|
|
918
|
+
listenKey = await this.startUserDataStream(credentials, accountOptions);
|
|
783
919
|
if (closed) {
|
|
784
920
|
closeListenKey();
|
|
785
921
|
return;
|
|
@@ -790,15 +926,17 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
790
926
|
return;
|
|
791
927
|
}
|
|
792
928
|
|
|
793
|
-
void this.keepAliveUserDataStream(
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
929
|
+
void this.keepAliveUserDataStream(
|
|
930
|
+
credentials,
|
|
931
|
+
listenKey,
|
|
932
|
+
accountOptions,
|
|
933
|
+
).catch((error) => {
|
|
934
|
+
callbacks.onError(
|
|
935
|
+
error instanceof Error
|
|
936
|
+
? error
|
|
937
|
+
: new Error("Failed to keep Binance PAPI listenKey alive"),
|
|
938
|
+
);
|
|
939
|
+
});
|
|
802
940
|
}, options.listenKeyKeepAliveMs);
|
|
803
941
|
|
|
804
942
|
websocket = createManagedWebSocket<BinancePrivateMessage>({
|
|
@@ -863,8 +1001,12 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
863
1001
|
credentials: AccountCredentials,
|
|
864
1002
|
accountOptions?: Record<string, unknown>,
|
|
865
1003
|
queryParams?: Record<string, string | undefined>,
|
|
1004
|
+
retryPolicy?: HttpRetryPolicy,
|
|
866
1005
|
): Promise<T> {
|
|
867
1006
|
const { apiKey, secret } = requirePrivateCredentials(credentials);
|
|
1007
|
+
const scope = this.rateLimitScope(method, path, accountOptions);
|
|
1008
|
+
await this.options.rateLimiter?.beforeRequest({ scope });
|
|
1009
|
+
|
|
868
1010
|
const params = new URLSearchParams();
|
|
869
1011
|
for (const [key, value] of Object.entries(queryParams ?? {})) {
|
|
870
1012
|
if (value !== undefined) {
|
|
@@ -873,7 +1015,11 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
873
1015
|
}
|
|
874
1016
|
params.set(
|
|
875
1017
|
"timestamp",
|
|
876
|
-
`${
|
|
1018
|
+
`${
|
|
1019
|
+
getNumberOption(accountOptions, "timestamp") ??
|
|
1020
|
+
this.options.signingClock?.now() ??
|
|
1021
|
+
Date.now()
|
|
1022
|
+
}`,
|
|
877
1023
|
);
|
|
878
1024
|
params.set(
|
|
879
1025
|
"recvWindow",
|
|
@@ -882,22 +1028,59 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
882
1028
|
params.set("signature", signQuery(params.toString(), secret));
|
|
883
1029
|
|
|
884
1030
|
const url = `${BINANCE_PAPI_REST_BASE_URL}${path}?${params.toString()}`;
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1031
|
+
const timeoutMs = this.options.httpTimeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
|
|
1032
|
+
try {
|
|
1033
|
+
const response = await httpRequest<T>({
|
|
1034
|
+
fetchFn: this.options.fetchFn,
|
|
1035
|
+
url,
|
|
1036
|
+
method,
|
|
1037
|
+
headers: {
|
|
1038
|
+
"X-MBX-APIKEY": apiKey,
|
|
1039
|
+
},
|
|
1040
|
+
timeoutMs,
|
|
1041
|
+
parseAs: "json",
|
|
1042
|
+
emptyBody: "empty_object",
|
|
1043
|
+
retryPolicy: retryPolicy ?? NO_RETRY_POLICY,
|
|
1044
|
+
messages: getBinancePapiHttpMessages(timeoutMs),
|
|
1045
|
+
});
|
|
891
1046
|
|
|
892
|
-
|
|
1047
|
+
await this.options.rateLimiter?.afterResponse(
|
|
1048
|
+
{ scope },
|
|
1049
|
+
{
|
|
1050
|
+
status: response.status,
|
|
1051
|
+
headers: response.headers,
|
|
1052
|
+
usage: parseBinanceRateLimitUsage(response.headers),
|
|
1053
|
+
},
|
|
1054
|
+
);
|
|
1055
|
+
|
|
1056
|
+
return response.body;
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
if (isTransportError(error)) {
|
|
1059
|
+
await this.options.rateLimiter?.onTransportError(
|
|
1060
|
+
{ scope },
|
|
1061
|
+
{
|
|
1062
|
+
status: error.status,
|
|
1063
|
+
headers: error.headers,
|
|
1064
|
+
retryAfterMs: error.retryAfterMs,
|
|
1065
|
+
usage: parseBinanceRateLimitUsage(error.headers),
|
|
1066
|
+
},
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
throw error;
|
|
1071
|
+
}
|
|
893
1072
|
}
|
|
894
1073
|
|
|
895
1074
|
private async startUserDataStream(
|
|
896
1075
|
credentials: AccountCredentials,
|
|
1076
|
+
accountOptions?: Record<string, unknown>,
|
|
897
1077
|
): Promise<string> {
|
|
898
1078
|
const response = await this.userStreamRequest<BinanceListenKeyResponse>(
|
|
899
1079
|
"POST",
|
|
900
1080
|
credentials,
|
|
1081
|
+
undefined,
|
|
1082
|
+
NO_RETRY_POLICY,
|
|
1083
|
+
accountOptions,
|
|
901
1084
|
);
|
|
902
1085
|
if (!response.listenKey) {
|
|
903
1086
|
throw new Error("Binance PAPI did not return a listenKey");
|
|
@@ -909,22 +1092,28 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
909
1092
|
private async keepAliveUserDataStream(
|
|
910
1093
|
credentials: AccountCredentials,
|
|
911
1094
|
listenKey: string,
|
|
1095
|
+
accountOptions?: Record<string, unknown>,
|
|
912
1096
|
): Promise<void> {
|
|
913
1097
|
await this.userStreamRequest<Record<string, never>>(
|
|
914
1098
|
"PUT",
|
|
915
1099
|
credentials,
|
|
916
1100
|
listenKey,
|
|
1101
|
+
LISTEN_KEY_KEEPALIVE_RETRY_POLICY,
|
|
1102
|
+
accountOptions,
|
|
917
1103
|
);
|
|
918
1104
|
}
|
|
919
1105
|
|
|
920
1106
|
private async closeUserDataStream(
|
|
921
1107
|
credentials: AccountCredentials,
|
|
922
1108
|
listenKey: string,
|
|
1109
|
+
accountOptions?: Record<string, unknown>,
|
|
923
1110
|
): Promise<void> {
|
|
924
1111
|
await this.userStreamRequest<Record<string, never>>(
|
|
925
1112
|
"DELETE",
|
|
926
1113
|
credentials,
|
|
927
1114
|
listenKey,
|
|
1115
|
+
NO_RETRY_POLICY,
|
|
1116
|
+
accountOptions,
|
|
928
1117
|
);
|
|
929
1118
|
}
|
|
930
1119
|
|
|
@@ -932,8 +1121,17 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
932
1121
|
method: "POST" | "PUT" | "DELETE",
|
|
933
1122
|
credentials: AccountCredentials,
|
|
934
1123
|
listenKey?: string,
|
|
1124
|
+
retryPolicy: HttpRetryPolicy = NO_RETRY_POLICY,
|
|
1125
|
+
accountOptions?: Record<string, unknown>,
|
|
935
1126
|
): Promise<T> {
|
|
936
1127
|
const { apiKey } = requirePrivateCredentials(credentials);
|
|
1128
|
+
const scope = this.rateLimitScope(
|
|
1129
|
+
method,
|
|
1130
|
+
"/papi/v1/listenKey",
|
|
1131
|
+
accountOptions,
|
|
1132
|
+
);
|
|
1133
|
+
await this.options.rateLimiter?.beforeRequest({ scope });
|
|
1134
|
+
|
|
937
1135
|
const params = new URLSearchParams();
|
|
938
1136
|
if (listenKey) {
|
|
939
1137
|
params.set("listenKey", listenKey);
|
|
@@ -943,13 +1141,58 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
943
1141
|
const url = `${BINANCE_PAPI_REST_BASE_URL}/papi/v1/listenKey${
|
|
944
1142
|
query ? `?${query}` : ""
|
|
945
1143
|
}`;
|
|
946
|
-
const
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1144
|
+
const timeoutMs = this.options.httpTimeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
|
|
1145
|
+
try {
|
|
1146
|
+
const response = await httpRequest<T>({
|
|
1147
|
+
fetchFn: this.options.fetchFn,
|
|
1148
|
+
url,
|
|
1149
|
+
method,
|
|
1150
|
+
headers: {
|
|
1151
|
+
"X-MBX-APIKEY": apiKey,
|
|
1152
|
+
},
|
|
1153
|
+
timeoutMs,
|
|
1154
|
+
parseAs: "json",
|
|
1155
|
+
emptyBody: "empty_object",
|
|
1156
|
+
retryPolicy,
|
|
1157
|
+
messages: getBinancePapiHttpMessages(timeoutMs),
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
await this.options.rateLimiter?.afterResponse(
|
|
1161
|
+
{ scope },
|
|
1162
|
+
{
|
|
1163
|
+
status: response.status,
|
|
1164
|
+
headers: response.headers,
|
|
1165
|
+
usage: parseBinanceRateLimitUsage(response.headers),
|
|
1166
|
+
},
|
|
1167
|
+
);
|
|
1168
|
+
|
|
1169
|
+
return response.body;
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
if (isTransportError(error)) {
|
|
1172
|
+
await this.options.rateLimiter?.onTransportError(
|
|
1173
|
+
{ scope },
|
|
1174
|
+
{
|
|
1175
|
+
status: error.status,
|
|
1176
|
+
headers: error.headers,
|
|
1177
|
+
retryAfterMs: error.retryAfterMs,
|
|
1178
|
+
usage: parseBinanceRateLimitUsage(error.headers),
|
|
1179
|
+
},
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
952
1182
|
|
|
953
|
-
|
|
1183
|
+
throw error;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
private rateLimitScope(
|
|
1188
|
+
method: string,
|
|
1189
|
+
path: string,
|
|
1190
|
+
accountOptions?: Record<string, unknown>,
|
|
1191
|
+
): RateLimitScope {
|
|
1192
|
+
return {
|
|
1193
|
+
venue: "binance",
|
|
1194
|
+
accountId: getStringOption(accountOptions, "accountId"),
|
|
1195
|
+
endpointKey: `${method} ${path}`,
|
|
1196
|
+
};
|
|
954
1197
|
}
|
|
955
1198
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { RateLimitUsage } from "../../types/index.ts";
|
|
2
|
+
|
|
3
|
+
const USED_WEIGHT_PREFIX = "x-mbx-used-weight-";
|
|
4
|
+
const ORDER_COUNT_PREFIX = "x-mbx-order-count-";
|
|
5
|
+
|
|
6
|
+
export function parseBinanceRateLimitUsage(
|
|
7
|
+
headers: Headers,
|
|
8
|
+
): RateLimitUsage | undefined {
|
|
9
|
+
const weight: Record<string, number> = {};
|
|
10
|
+
const orderCount: Record<string, number> = {};
|
|
11
|
+
|
|
12
|
+
for (const name of headers.keys()) {
|
|
13
|
+
const normalizedName = name.toLowerCase();
|
|
14
|
+
if (normalizedName.startsWith(USED_WEIGHT_PREFIX)) {
|
|
15
|
+
const interval = normalizedName.slice(USED_WEIGHT_PREFIX.length);
|
|
16
|
+
const value = parseHeaderNumber(headers.get(name));
|
|
17
|
+
if (interval && value !== undefined) {
|
|
18
|
+
weight[interval] = value;
|
|
19
|
+
}
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (normalizedName.startsWith(ORDER_COUNT_PREFIX)) {
|
|
24
|
+
const interval = normalizedName.slice(ORDER_COUNT_PREFIX.length);
|
|
25
|
+
const value = parseHeaderNumber(headers.get(name));
|
|
26
|
+
if (interval && value !== undefined) {
|
|
27
|
+
orderCount[interval] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return Object.keys(weight).length > 0 || Object.keys(orderCount).length > 0
|
|
33
|
+
? {
|
|
34
|
+
weight: Object.keys(weight).length > 0 ? weight : undefined,
|
|
35
|
+
orderCount: Object.keys(orderCount).length > 0 ? orderCount : undefined,
|
|
36
|
+
}
|
|
37
|
+
: undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseHeaderNumber(value: string | null): number | undefined {
|
|
41
|
+
if (value === null) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const parsed = Number(value);
|
|
46
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
47
|
+
}
|