@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
package/src/client/runtime.ts
CHANGED
|
@@ -9,9 +9,14 @@ import type {
|
|
|
9
9
|
PrivateUserDataAdapter,
|
|
10
10
|
RawOrderUpdate,
|
|
11
11
|
} from "../adapters/types.ts";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
AcexError,
|
|
14
|
+
type AcexErrorCode,
|
|
15
|
+
buildAcexErrorDetails,
|
|
16
|
+
} from "../errors.ts";
|
|
13
17
|
import { AsyncEventBus } from "../internal/async-event-bus.ts";
|
|
14
18
|
import { matchesHealthFilter } from "../internal/filters.ts";
|
|
19
|
+
import { ReactiveRateLimiter } from "../internal/rate-limiter.ts";
|
|
15
20
|
import { AccountManagerImpl } from "../managers/account-manager.ts";
|
|
16
21
|
import { MarketManagerImpl } from "../managers/market-manager.ts";
|
|
17
22
|
import { OrderManagerImpl } from "../managers/order-manager.ts";
|
|
@@ -37,6 +42,7 @@ import type {
|
|
|
37
42
|
StopOptions,
|
|
38
43
|
Venue,
|
|
39
44
|
VenueCapabilities,
|
|
45
|
+
VenueOrderCapabilities,
|
|
40
46
|
} from "../types/index.ts";
|
|
41
47
|
import {
|
|
42
48
|
type ClientContext,
|
|
@@ -102,13 +108,20 @@ export class AcexClientImpl implements AcexClient, ClientContext {
|
|
|
102
108
|
constructor(options: CreateClientOptions = {}) {
|
|
103
109
|
activeClients.add(this);
|
|
104
110
|
|
|
105
|
-
const
|
|
111
|
+
const rateLimiter = options.rateLimiter ?? new ReactiveRateLimiter();
|
|
112
|
+
const marketAdapter = new BinanceMarketAdapter({ rateLimiter });
|
|
106
113
|
this.marketAdapters = new Map([[marketAdapter.venue, marketAdapter]]);
|
|
107
114
|
const privateAdapters = [
|
|
108
|
-
new BinancePrivateAdapter(
|
|
115
|
+
new BinancePrivateAdapter({
|
|
116
|
+
signingClock: options.clock,
|
|
117
|
+
rateLimiter,
|
|
118
|
+
}),
|
|
109
119
|
new JuplendPrivateAdapter(
|
|
110
120
|
options.account?.juplend?.rpcUrl,
|
|
111
121
|
options.account?.juplend?.jupApiKey,
|
|
122
|
+
{
|
|
123
|
+
pollIntervalMs: options.account?.juplend?.pollIntervalMs,
|
|
124
|
+
},
|
|
112
125
|
),
|
|
113
126
|
];
|
|
114
127
|
this.privateAdapters = new Map(
|
|
@@ -122,7 +135,7 @@ export class AcexClientImpl implements AcexClient, ClientContext {
|
|
|
122
135
|
l1ReconnectMaxDelayMs: options.market?.l1ReconnectMaxDelayMs,
|
|
123
136
|
});
|
|
124
137
|
this.accountManager = new AccountManagerImpl(this);
|
|
125
|
-
this.orderManager = new OrderManagerImpl(this);
|
|
138
|
+
this.orderManager = new OrderManagerImpl(this, options.order);
|
|
126
139
|
this.privateCoordinator = new PrivateSubscriptionCoordinator(
|
|
127
140
|
this,
|
|
128
141
|
privateAdapters,
|
|
@@ -293,9 +306,20 @@ export class AcexClientImpl implements AcexClient, ClientContext {
|
|
|
293
306
|
return account;
|
|
294
307
|
}
|
|
295
308
|
|
|
309
|
+
getPrivateOrderCapabilities(
|
|
310
|
+
venue: Venue,
|
|
311
|
+
): VenueOrderCapabilities | undefined {
|
|
312
|
+
return this.privateAdapters.get(venue)?.orderCapabilities;
|
|
313
|
+
}
|
|
314
|
+
|
|
296
315
|
ensurePrivateCredentials(accountId: string): void {
|
|
297
316
|
const account = this.getRegisteredAccount(accountId);
|
|
298
|
-
if (
|
|
317
|
+
if (
|
|
318
|
+
hasPrivateCredentials(
|
|
319
|
+
account.credentials,
|
|
320
|
+
this.getPrivateCredentialsRequired(account.venue),
|
|
321
|
+
)
|
|
322
|
+
) {
|
|
299
323
|
return;
|
|
300
324
|
}
|
|
301
325
|
|
|
@@ -339,7 +363,7 @@ export class AcexClientImpl implements AcexClient, ClientContext {
|
|
|
339
363
|
return this.getPrivateAdapter(account.venue).createOrder(
|
|
340
364
|
account.credentials ?? {},
|
|
341
365
|
request,
|
|
342
|
-
account.options,
|
|
366
|
+
{ ...account.options, accountId: account.accountId },
|
|
343
367
|
);
|
|
344
368
|
}
|
|
345
369
|
|
|
@@ -354,7 +378,7 @@ export class AcexClientImpl implements AcexClient, ClientContext {
|
|
|
354
378
|
return this.getPrivateAdapter(account.venue).cancelOrder(
|
|
355
379
|
account.credentials ?? {},
|
|
356
380
|
request,
|
|
357
|
-
account.options,
|
|
381
|
+
{ ...account.options, accountId: account.accountId },
|
|
358
382
|
);
|
|
359
383
|
}
|
|
360
384
|
|
|
@@ -367,7 +391,7 @@ export class AcexClientImpl implements AcexClient, ClientContext {
|
|
|
367
391
|
return this.getPrivateAdapter(account.venue).cancelAllOrders(
|
|
368
392
|
account.credentials ?? {},
|
|
369
393
|
request,
|
|
370
|
-
account.options,
|
|
394
|
+
{ ...account.options, accountId: account.accountId },
|
|
371
395
|
);
|
|
372
396
|
}
|
|
373
397
|
|
|
@@ -411,7 +435,9 @@ export class AcexClientImpl implements AcexClient, ClientContext {
|
|
|
411
435
|
message: string,
|
|
412
436
|
metadata?: Omit<AcexInternalError, "error" | "source" | "ts">,
|
|
413
437
|
): AcexError {
|
|
414
|
-
const error = new AcexError(code, message
|
|
438
|
+
const error = new AcexError(code, message, {
|
|
439
|
+
details: buildAcexErrorDetails(metadata),
|
|
440
|
+
});
|
|
415
441
|
this.errorBus.publish({
|
|
416
442
|
source: "client",
|
|
417
443
|
ts: this.now(),
|
|
@@ -424,7 +450,7 @@ export class AcexClientImpl implements AcexClient, ClientContext {
|
|
|
424
450
|
private getPrivateCommandAccount(accountId: string): RegisteredAccountRecord {
|
|
425
451
|
const account = this.getRegisteredAccount(accountId);
|
|
426
452
|
const adapter = this.getPrivateAdapter(account.venue);
|
|
427
|
-
if (adapter.
|
|
453
|
+
if (!adapter.orderCapabilities.supported) {
|
|
428
454
|
throw this.createError(
|
|
429
455
|
"VENUE_NOT_SUPPORTED",
|
|
430
456
|
`Venue does not support private order commands: ${account.venue}`,
|
|
@@ -432,7 +458,12 @@ export class AcexClientImpl implements AcexClient, ClientContext {
|
|
|
432
458
|
);
|
|
433
459
|
}
|
|
434
460
|
|
|
435
|
-
if (
|
|
461
|
+
if (
|
|
462
|
+
!hasPrivateCredentials(
|
|
463
|
+
account.credentials,
|
|
464
|
+
adapter.accountCapabilities.credentialsRequired,
|
|
465
|
+
)
|
|
466
|
+
) {
|
|
436
467
|
throw this.createError(
|
|
437
468
|
"CREDENTIALS_MISSING",
|
|
438
469
|
`Account credentials are required for private order commands: ${accountId}`,
|
|
@@ -443,6 +474,13 @@ export class AcexClientImpl implements AcexClient, ClientContext {
|
|
|
443
474
|
return account;
|
|
444
475
|
}
|
|
445
476
|
|
|
477
|
+
private getPrivateCredentialsRequired(venue: Venue): boolean {
|
|
478
|
+
return (
|
|
479
|
+
this.privateAdapters.get(venue)?.accountCapabilities
|
|
480
|
+
.credentialsRequired ?? true
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
446
484
|
private getPrivateAdapter(venue: Venue): PrivateUserDataAdapter {
|
|
447
485
|
const adapter = this.privateAdapters.get(venue);
|
|
448
486
|
if (!adapter) {
|
package/src/errors.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { isTransportError } from "./internal/http-client.ts";
|
|
2
|
+
import type { Venue } from "./types/shared.ts";
|
|
3
|
+
|
|
1
4
|
export type AcexErrorCode =
|
|
2
5
|
| "ACCOUNT_ALREADY_EXISTS"
|
|
3
6
|
| "ACCOUNT_BOOTSTRAP_FAILED"
|
|
@@ -6,6 +9,7 @@ export type AcexErrorCode =
|
|
|
6
9
|
| "CREDENTIALS_MISSING"
|
|
7
10
|
| "VENUE_NOT_SUPPORTED"
|
|
8
11
|
| "MARKET_CATALOG_LOAD_FAILED"
|
|
12
|
+
| "MARKET_SERVER_TIME_FETCH_FAILED"
|
|
9
13
|
| "MARKET_INACTIVE"
|
|
10
14
|
| "MARKET_FUNDING_RATE_UNSUPPORTED"
|
|
11
15
|
| "MARKET_NOT_FOUND"
|
|
@@ -16,12 +20,162 @@ export type AcexErrorCode =
|
|
|
16
20
|
| "ORDER_CREATE_FAILED"
|
|
17
21
|
| "ORDER_INPUT_INVALID";
|
|
18
22
|
|
|
23
|
+
export type AcexErrorTransportKind =
|
|
24
|
+
| "timeout"
|
|
25
|
+
| "http"
|
|
26
|
+
| "network"
|
|
27
|
+
| "rate_limited"
|
|
28
|
+
| "parse";
|
|
29
|
+
|
|
30
|
+
export interface AcexVenueErrorDetails {
|
|
31
|
+
readonly code?: string;
|
|
32
|
+
readonly message?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AcexErrorTransportDetails {
|
|
36
|
+
readonly kind?: AcexErrorTransportKind;
|
|
37
|
+
readonly status?: number;
|
|
38
|
+
readonly statusText?: string;
|
|
39
|
+
readonly retryAfterMs?: number;
|
|
40
|
+
readonly retryable?: boolean;
|
|
41
|
+
readonly attempts?: number;
|
|
42
|
+
readonly rawBody?: string;
|
|
43
|
+
readonly url?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AcexErrorDetails {
|
|
47
|
+
readonly venue?: Venue;
|
|
48
|
+
readonly accountId?: string;
|
|
49
|
+
readonly symbol?: string;
|
|
50
|
+
readonly venueError?: AcexVenueErrorDetails;
|
|
51
|
+
readonly transport?: AcexErrorTransportDetails;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface AcexErrorOptions {
|
|
55
|
+
readonly cause?: unknown;
|
|
56
|
+
readonly details?: AcexErrorDetails;
|
|
57
|
+
}
|
|
58
|
+
|
|
19
59
|
export class AcexError extends Error {
|
|
20
60
|
readonly code: AcexErrorCode;
|
|
61
|
+
readonly details?: AcexErrorDetails;
|
|
62
|
+
override readonly cause?: unknown;
|
|
21
63
|
|
|
22
|
-
constructor(
|
|
23
|
-
|
|
64
|
+
constructor(
|
|
65
|
+
code: AcexErrorCode,
|
|
66
|
+
message: string,
|
|
67
|
+
options: AcexErrorOptions = {},
|
|
68
|
+
) {
|
|
69
|
+
super(message, { cause: options.cause });
|
|
24
70
|
this.name = "AcexError";
|
|
25
71
|
this.code = code;
|
|
72
|
+
this.details = options.details;
|
|
73
|
+
this.cause = options.cause;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function buildAcexErrorDetails(
|
|
78
|
+
context?: Pick<AcexErrorDetails, "venue" | "accountId" | "symbol">,
|
|
79
|
+
cause?: unknown,
|
|
80
|
+
): AcexErrorDetails | undefined {
|
|
81
|
+
const transport = buildTransportDetails(cause);
|
|
82
|
+
const venueError = parseVenueErrorDetails(transport?.rawBody);
|
|
83
|
+
const details: AcexErrorDetails = {
|
|
84
|
+
venue: context?.venue,
|
|
85
|
+
accountId: context?.accountId,
|
|
86
|
+
symbol: context?.symbol,
|
|
87
|
+
venueError,
|
|
88
|
+
transport,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return hasDetails(details) ? details : undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function formatAcexErrorMessage(
|
|
95
|
+
message: string,
|
|
96
|
+
details?: AcexErrorDetails,
|
|
97
|
+
): string {
|
|
98
|
+
const venueErrorMessage = details?.venueError?.message?.trim();
|
|
99
|
+
if (!venueErrorMessage) {
|
|
100
|
+
return message;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const venue = details?.venue;
|
|
104
|
+
const venueLabel = venue ? formatVenueLabel(venue) : "Exchange";
|
|
105
|
+
return `${message} (${venueLabel} rejected: ${venueErrorMessage})`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildTransportDetails(
|
|
109
|
+
cause: unknown,
|
|
110
|
+
): AcexErrorTransportDetails | undefined {
|
|
111
|
+
if (!isTransportError(cause)) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return pruneUndefined({
|
|
116
|
+
kind: cause.kind,
|
|
117
|
+
status: cause.status,
|
|
118
|
+
statusText: cause.statusText,
|
|
119
|
+
retryAfterMs: cause.retryAfterMs,
|
|
120
|
+
retryable: cause.retryable,
|
|
121
|
+
attempts: cause.attempts,
|
|
122
|
+
rawBody: cause.rawBody,
|
|
123
|
+
url: cause.url,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseVenueErrorDetails(
|
|
128
|
+
rawBody: string | undefined,
|
|
129
|
+
): AcexVenueErrorDetails | undefined {
|
|
130
|
+
if (!rawBody) {
|
|
131
|
+
return undefined;
|
|
26
132
|
}
|
|
133
|
+
|
|
134
|
+
let parsed: unknown;
|
|
135
|
+
try {
|
|
136
|
+
parsed = JSON.parse(rawBody);
|
|
137
|
+
} catch {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const record = parsed as Record<string, unknown>;
|
|
146
|
+
const code = record.code;
|
|
147
|
+
const message = record.msg ?? record.message;
|
|
148
|
+
|
|
149
|
+
if (
|
|
150
|
+
(typeof code !== "string" && typeof code !== "number") ||
|
|
151
|
+
typeof message !== "string" ||
|
|
152
|
+
message.trim() === ""
|
|
153
|
+
) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
code: String(code),
|
|
159
|
+
message,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function hasDetails(details: AcexErrorDetails): boolean {
|
|
164
|
+
return Boolean(
|
|
165
|
+
details.venue ||
|
|
166
|
+
details.accountId ||
|
|
167
|
+
details.symbol ||
|
|
168
|
+
details.venueError ||
|
|
169
|
+
details.transport,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function formatVenueLabel(venue: Venue): string {
|
|
174
|
+
return `${venue.charAt(0).toUpperCase()}${venue.slice(1)}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function pruneUndefined<T extends Record<string, unknown>>(input: T): T {
|
|
178
|
+
return Object.fromEntries(
|
|
179
|
+
Object.entries(input).filter(([, value]) => value !== undefined),
|
|
180
|
+
) as T;
|
|
27
181
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
export { BigNumber } from "bignumber.js";
|
|
2
2
|
export { createClient } from "./client/create-client.ts";
|
|
3
|
-
export type {
|
|
3
|
+
export type {
|
|
4
|
+
AcexErrorCode,
|
|
5
|
+
AcexErrorDetails,
|
|
6
|
+
AcexErrorOptions,
|
|
7
|
+
AcexErrorTransportDetails,
|
|
8
|
+
AcexErrorTransportKind,
|
|
9
|
+
AcexVenueErrorDetails,
|
|
10
|
+
} from "./errors.ts";
|
|
4
11
|
export { AcexError } from "./errors.ts";
|
|
5
12
|
export * from "./types/index.ts";
|
|
@@ -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
|
+
}
|