@cexyio/cexy 0.1.0-dev.4 → 0.1.0-dev.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.
- package/README.md +36 -8
- package/dist/index.cjs +165 -65
- package/dist/index.d.cts +181 -36
- package/dist/index.d.ts +181 -36
- package/dist/index.js +163 -66
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,6 +62,9 @@ await cexy.trading.cancelAll({ symbol: "BTC/USDT" }); // { symbol: null } = ever
|
|
|
62
62
|
`cancelAll` requires `symbol`: the server treats a missing symbol as "every market", so the SDK makes
|
|
63
63
|
you say so with `{ symbol: null }`. An unknown symbol throws `NotFoundError`.
|
|
64
64
|
|
|
65
|
+
It also cancels stop orders that have not triggered yet (status `pending_trigger`) and releases
|
|
66
|
+
their reservations, so nothing fires into the market after the call.
|
|
67
|
+
|
|
65
68
|
One call handles at most 500 orders and puts each in exactly one list: `cancelled`, `already_closed`
|
|
66
69
|
(it filled, was refused or was cancelled elsewhere first; not an error) or `failed`, with the reason in
|
|
67
70
|
`failures` (`INVALID_STATE` for an order still being placed). `has_more: true` means more orders remain.
|
|
@@ -75,8 +78,14 @@ if (res.stopped !== "done" || res.failed.length) console.warn("left over:", res.
|
|
|
75
78
|
|
|
76
79
|
`untilDone` repeats while `has_more` is true or a failure is `INVALID_STATE` / `SERVICE_UNAVAILABLE`.
|
|
77
80
|
After a call without progress it waits 1, 2, 4, 8, then 15 s, and it stops after `maxRounds` calls
|
|
78
|
-
(default 20) or before a wait would pass `timeBudgetMs` (default 120 000).
|
|
79
|
-
|
|
81
|
+
(default 20) or before a wait would pass `timeBudgetMs` (default 120 000). Every round is exactly one
|
|
82
|
+
request (the loop owns the retries, so it never sends more than `maxRounds` requests): a 429 round waits its
|
|
83
|
+
Retry-After, which counts against the budget; another retryable error (5xx, network) takes the next backoff
|
|
84
|
+
step; a wait that would pass the budget ends the loop with `stopped: "time_budget"` and `last_error_code`.
|
|
85
|
+
A wait imposed by the client rate limiter (e.g. `X-RateLimit-Remaining: 0` with a Reset) counts too: if it
|
|
86
|
+
would pass the budget the loop stops without calling, with `last_error_code: "RATE_LIMITED"`.
|
|
87
|
+
A non-retryable error (e.g. a key without the trade scope) throws `CancelAllInterruptedError` with the error
|
|
88
|
+
and the partial result. The server allows 30 cancel-all calls per minute per account.
|
|
80
89
|
|
|
81
90
|
Give both `apiKey` and `apiSecret`, or neither: passing only one throws at construction.
|
|
82
91
|
|
|
@@ -145,10 +154,13 @@ try {
|
|
|
145
154
|
|
|
146
155
|
- Timeout per attempt: `timeoutMs` (default 10 s). Retries: `maxRetries` (default 3), exponential backoff with full jitter.
|
|
147
156
|
- Retried: network errors, timeouts and responses with `retryable: true`.
|
|
148
|
-
- 429 waits at least `Retry-After` / `details.retry_after_seconds`.
|
|
157
|
+
- 429 waits at least `Retry-After` / `details.retry_after_seconds`. Server wait hints are untrusted: unusable
|
|
158
|
+
values are ignored, and a hint longer than 120 s (`MAX_SERVER_WAIT_MS`) is never waited: the call fails at
|
|
159
|
+
once with `RateLimitError`, whose `retryAfterMs` still has the server's value. The client-side rate limiter
|
|
160
|
+
never blocks longer than 120 s because of a server hint.
|
|
149
161
|
- GETs retry freely.
|
|
150
|
-
- **Orders:** safety rests on `client_order_id
|
|
151
|
-
|
|
162
|
+
- **Orders:** safety rests on `client_order_id`. The server does not honour `Idempotency-Key` on
|
|
163
|
+
`POST /trading/orders`, order cancels or cancel-all, so the SDK does not send it there. `placeOrder` always sends a
|
|
152
164
|
`client_order_id` (a UUID if you do not set one); it is unique per account and a repeat is refused
|
|
153
165
|
before any funds move. After an ambiguous failure (network error, timeout, 5xx) the SDK first looks the
|
|
154
166
|
order up by that id. If the order exists it is returned with `recovered: true`; only if it does not
|
|
@@ -157,9 +169,9 @@ try {
|
|
|
157
169
|
- **Cancels:** `cancelOrder` retries network errors; if a *retry* gets `INVALID_STATE`, the first attempt
|
|
158
170
|
already cancelled the order, so the SDK fetches and returns it. `cancelAll` is naturally repeatable and
|
|
159
171
|
is retried the same way (a retry reports only what it cancelled).
|
|
160
|
-
- **Pool join/exit** send an
|
|
161
|
-
it there, so they execute once. A 409 `CONCURRENT_MODIFICATION` (the same key
|
|
162
|
-
retried with the same key. Pass `{ idempotencyKey }` to control it yourself.
|
|
172
|
+
- **Pool join/exit** are the only requests that send an `Idempotency-Key` (auto-generated, reused on every
|
|
173
|
+
retry); the server honours it there, so they execute once. A 409 `CONCURRENT_MODIFICATION` (the same key
|
|
174
|
+
still in flight) is retried with the same key. Pass `{ idempotencyKey }` to control it yourself.
|
|
163
175
|
- `onRetry` lets you log retries.
|
|
164
176
|
|
|
165
177
|
Every method takes a last `RequestOptions` argument: `{ signal, timeoutMs, maxRetries, idempotencyKey }`.
|
|
@@ -176,6 +188,22 @@ for await (const order of cexy.trading.iterateOrderHistory({ symbol: "BTC/USDT",
|
|
|
176
188
|
// cap the total: cexy.account.iterateLedger({}, { maxItems: 500 })
|
|
177
189
|
```
|
|
178
190
|
|
|
191
|
+
Each ledger entry's `reference` says what caused it, as a union told apart by `type` (`deposit`,
|
|
192
|
+
`withdrawal`, `order`, `trade`, `transfer`, `adjustment`, `pool`, `futures_transfer`, `system`).
|
|
193
|
+
Newer types the SDK does not know yet arrive unchanged instead of failing; narrow with
|
|
194
|
+
`isLedgerReference`:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
import { isLedgerReference } from "@cexyio/cexy";
|
|
198
|
+
|
|
199
|
+
for await (const e of cexy.account.iterateLedger({}, { maxItems: 100 })) {
|
|
200
|
+
if (isLedgerReference(e.reference, "trade")) console.log(e.reference.trade_id);
|
|
201
|
+
else if (!isLedgerReference(e.reference)) console.log("new cause type:", e.reference.type);
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Ids (`OrderId`, `TradeId`, `UserId`, …) are plain strings; the SDK does not check their format.
|
|
206
|
+
|
|
179
207
|
## Rate limits
|
|
180
208
|
|
|
181
209
|
The client has a token-bucket limiter: **100 requests/minute without a key** (the server allows 120/min
|
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,16 @@ var OrderStateUnknownError = class extends CexyError {
|
|
|
31
31
|
this.clientOrderId = clientOrderId;
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
|
+
var CancelAllInterruptedError = class extends CexyError {
|
|
35
|
+
error;
|
|
36
|
+
partial;
|
|
37
|
+
constructor(error, partial) {
|
|
38
|
+
const what = error instanceof CexyApiError ? `${error.code}: ${error.message}` : String(error);
|
|
39
|
+
super(`cancelAll stopped after ${partial.rounds} round(s): ${what}`, { cause: error });
|
|
40
|
+
this.error = error;
|
|
41
|
+
this.partial = partial;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
34
44
|
var CexyApiError = class extends CexyError {
|
|
35
45
|
status;
|
|
36
46
|
code;
|
|
@@ -129,27 +139,30 @@ var KNOWN_CODES = /* @__PURE__ */ new Set([
|
|
|
129
139
|
"INTERNAL",
|
|
130
140
|
"SERVICE_UNAVAILABLE",
|
|
131
141
|
"UNDER_MAINTENANCE",
|
|
132
|
-
"ENGINE_OVERLOADED"
|
|
142
|
+
"ENGINE_OVERLOADED",
|
|
143
|
+
"PRICE_UNAVAILABLE"
|
|
133
144
|
]);
|
|
134
145
|
var CLIENT_ERROR_CODES = { UNEXPECTED_REDIRECT: "UNEXPECTED_REDIRECT" };
|
|
135
146
|
function isKnownErrorCode(code) {
|
|
136
147
|
return KNOWN_CODES.has(code);
|
|
137
148
|
}
|
|
138
149
|
var DEFAULT_RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
150
|
+
var MAX_SERVER_WAIT_MS = 12e4;
|
|
139
151
|
function retryAfterMs(headers, details) {
|
|
140
152
|
let best = null;
|
|
141
|
-
const h = headers?.get("retry-after");
|
|
153
|
+
const h = headers?.get("retry-after")?.trim();
|
|
142
154
|
if (h) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
155
|
+
if (/^\d+(\.\d+)?$/.test(h)) {
|
|
156
|
+
const ms = Number(h) * 1e3;
|
|
157
|
+
if (Number.isFinite(ms)) best = ms;
|
|
158
|
+
} else if (/[a-z]/i.test(h)) {
|
|
146
159
|
const at = Date.parse(h);
|
|
147
|
-
if (
|
|
160
|
+
if (Number.isFinite(at)) best = Math.max(0, at - Date.now());
|
|
148
161
|
}
|
|
149
162
|
}
|
|
150
163
|
const d = details?.["retry_after_seconds"];
|
|
151
|
-
const ds = typeof d === "number" ? d : typeof d === "string" ? Number(d) : NaN;
|
|
152
|
-
if (Number.isFinite(ds)) best = Math.max(best ?? 0, ds * 1e3);
|
|
164
|
+
const ds = typeof d === "number" ? d : typeof d === "string" && /^\d+(\.\d+)?$/.test(d.trim()) ? Number(d) : NaN;
|
|
165
|
+
if (Number.isFinite(ds) && ds >= 0 && Number.isFinite(ds * 1e3)) best = Math.max(best ?? 0, ds * 1e3);
|
|
153
166
|
return best;
|
|
154
167
|
}
|
|
155
168
|
function errorFromResponse(status, body, headers, redact = (t) => t) {
|
|
@@ -312,6 +325,7 @@ var OPERATIONS = {
|
|
|
312
325
|
};
|
|
313
326
|
|
|
314
327
|
// src/http.ts
|
|
328
|
+
var IDEMPOTENT_OPS = /* @__PURE__ */ new Set(["join_pool", "exit_pool"]);
|
|
315
329
|
var BACKOFF_BASE_MS = 500;
|
|
316
330
|
var BACKOFF_MAX_MS = 1e4;
|
|
317
331
|
var Transport = class {
|
|
@@ -320,16 +334,15 @@ var Transport = class {
|
|
|
320
334
|
this.config = config;
|
|
321
335
|
}
|
|
322
336
|
/**
|
|
323
|
-
* Sends a request with the standard retry policy:
|
|
324
|
-
*
|
|
325
|
-
* honours it
|
|
326
|
-
*
|
|
337
|
+
* Sends a request with the standard retry policy: retryable errors and network failures are
|
|
338
|
+
* retried. Pool join/exit carry an `Idempotency-Key` reused on every attempt (the server
|
|
339
|
+
* honours it there, which makes their retries safe); the other mutations routed here
|
|
340
|
+
* (cancel-all) are naturally repeatable and send no key. `placeOrder` and `cancelOrder` use
|
|
327
341
|
* `attempt()` with their own policies.
|
|
328
342
|
*/
|
|
329
343
|
async request(spec, opts = {}) {
|
|
330
344
|
const info = OPERATIONS[spec.op];
|
|
331
|
-
const
|
|
332
|
-
const idempotencyKey = isMutation ? spec.idempotencyKey ?? opts.idempotencyKey ?? newId() : void 0;
|
|
345
|
+
const idempotencyKey = IDEMPOTENT_OPS.has(spec.op) ? spec.idempotencyKey ?? opts.idempotencyKey ?? newId() : void 0;
|
|
333
346
|
const maxRetries = opts.maxRetries ?? this.config.maxRetries;
|
|
334
347
|
for (let attempt = 0; ; attempt++) {
|
|
335
348
|
try {
|
|
@@ -340,8 +353,13 @@ var Transport = class {
|
|
|
340
353
|
}
|
|
341
354
|
}
|
|
342
355
|
}
|
|
343
|
-
/**
|
|
356
|
+
/**
|
|
357
|
+
* Waits before retry number `attempt + 1`, honouring server hints. A server hint longer than
|
|
358
|
+
* `MAX_SERVER_WAIT_MS` is not waited: `err` is thrown at once (it still carries the hint).
|
|
359
|
+
*/
|
|
344
360
|
async backoff(op2, info, attempt, err, idempotencyKey, signal) {
|
|
361
|
+
const hint = serverHintMs(err);
|
|
362
|
+
if (hint !== null && hint > MAX_SERVER_WAIT_MS) throw err;
|
|
345
363
|
const delayMs = this.retryDelay(attempt, err);
|
|
346
364
|
this.config.onRetry?.({
|
|
347
365
|
operation: op2,
|
|
@@ -356,8 +374,8 @@ var Transport = class {
|
|
|
356
374
|
}
|
|
357
375
|
/** Full-jitter exponential backoff, or the server's hint plus a little jitter. */
|
|
358
376
|
retryDelay(attempt, err) {
|
|
359
|
-
const hint =
|
|
360
|
-
if (hint !== null && hint > 0) return Math.ceil(hint + this.config.random() * 250);
|
|
377
|
+
const hint = serverHintMs(err);
|
|
378
|
+
if (hint !== null && hint > 0 && hint <= MAX_SERVER_WAIT_MS) return Math.ceil(hint + this.config.random() * 250);
|
|
361
379
|
const cap = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** attempt);
|
|
362
380
|
return Math.ceil(this.config.random() * cap);
|
|
363
381
|
}
|
|
@@ -372,7 +390,7 @@ var Transport = class {
|
|
|
372
390
|
body = JSON.stringify(spec.body);
|
|
373
391
|
headers.set("Content-Type", "application/json");
|
|
374
392
|
}
|
|
375
|
-
if (
|
|
393
|
+
if (IDEMPOTENT_OPS.has(spec.op) && spec.idempotencyKey) headers.set("Idempotency-Key", spec.idempotencyKey);
|
|
376
394
|
if (info.auth === "api_key") {
|
|
377
395
|
const auth = this.config.authenticator;
|
|
378
396
|
if (!auth) {
|
|
@@ -455,6 +473,11 @@ var Transport = class {
|
|
|
455
473
|
return this.config.authenticator ? this.config.authenticator.redact(text) : text;
|
|
456
474
|
}
|
|
457
475
|
};
|
|
476
|
+
function serverHintMs(err) {
|
|
477
|
+
if (err instanceof RateLimitError) return err.retryAfterMs;
|
|
478
|
+
if (err instanceof CexyApiError) return retryAfterMs(void 0, err.details);
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
458
481
|
function isRetryable(err) {
|
|
459
482
|
if (err instanceof CexyConnectionError) return true;
|
|
460
483
|
if (err instanceof CexyApiError) return err.retryable || err.code === "CONCURRENT_MODIFICATION";
|
|
@@ -486,6 +509,7 @@ function errMessage(err) {
|
|
|
486
509
|
}
|
|
487
510
|
|
|
488
511
|
// src/limiter.ts
|
|
512
|
+
var MAX_BLOCK_MS = 12e4;
|
|
489
513
|
var RateLimiter = class {
|
|
490
514
|
#rpm;
|
|
491
515
|
#tokens;
|
|
@@ -519,14 +543,25 @@ var RateLimiter = class {
|
|
|
519
543
|
return;
|
|
520
544
|
}
|
|
521
545
|
const msPerToken = 6e4 / this.#rpm;
|
|
522
|
-
await this.#sleep(Math.ceil((1 - this.#tokens) * msPerToken), signal);
|
|
546
|
+
await this.#sleep(Math.min(MAX_BLOCK_MS, Math.ceil((1 - this.#tokens) * msPerToken)), signal);
|
|
523
547
|
}
|
|
524
548
|
}
|
|
549
|
+
/**
|
|
550
|
+
* How long `acquire()` would wait right now, in ms (0 when a request may go at once). Lets a
|
|
551
|
+
* caller with a time budget, such as the cancelAll untilDone loop, count the limiter's wait.
|
|
552
|
+
*/
|
|
553
|
+
pendingWaitMs() {
|
|
554
|
+
this.#refill();
|
|
555
|
+
const block = this.#blockedUntil - this.#now();
|
|
556
|
+
if (block > 0) return block;
|
|
557
|
+
if (this.#tokens >= 1) return 0;
|
|
558
|
+
return Math.min(MAX_BLOCK_MS, Math.ceil((1 - this.#tokens) * (6e4 / this.#rpm)));
|
|
559
|
+
}
|
|
525
560
|
/** Adapts to the server's rate-limit headers. Never raises the configured limit. */
|
|
526
561
|
update(headers) {
|
|
527
562
|
this.#refill();
|
|
528
563
|
const limit = num(headers.get("x-ratelimit-limit"));
|
|
529
|
-
if (limit !== null && limit
|
|
564
|
+
if (limit !== null && limit >= 1 && limit < this.#rpm) {
|
|
530
565
|
this.#rpm = limit;
|
|
531
566
|
this.#tokens = Math.min(this.#tokens, limit);
|
|
532
567
|
}
|
|
@@ -538,10 +573,13 @@ var RateLimiter = class {
|
|
|
538
573
|
else this.blockFor(6e4 / this.#rpm);
|
|
539
574
|
}
|
|
540
575
|
}
|
|
541
|
-
/**
|
|
576
|
+
/**
|
|
577
|
+
* Blocks all requests for `ms` (used for 429 Retry-After and `X-RateLimit-Reset`). Server
|
|
578
|
+
* hints are untrusted: non-finite values are ignored and the block is capped at 120 s.
|
|
579
|
+
*/
|
|
542
580
|
blockFor(ms) {
|
|
543
|
-
if (!(ms > 0)) return;
|
|
544
|
-
this.#blockedUntil = Math.max(this.#blockedUntil, this.#now() + ms);
|
|
581
|
+
if (!(ms > 0) || !Number.isFinite(ms)) return;
|
|
582
|
+
this.#blockedUntil = Math.max(this.#blockedUntil, this.#now() + Math.min(ms, MAX_BLOCK_MS));
|
|
545
583
|
}
|
|
546
584
|
#refill() {
|
|
547
585
|
const now = this.#now();
|
|
@@ -793,6 +831,12 @@ var WalletResource = class extends Resource {
|
|
|
793
831
|
};
|
|
794
832
|
var CANCEL_RETRY_CODES = /* @__PURE__ */ new Set(["INVALID_STATE", "SERVICE_UNAVAILABLE"]);
|
|
795
833
|
var CANCEL_BACKOFF_S = [1, 2, 4, 8, 15];
|
|
834
|
+
function errorCode(err) {
|
|
835
|
+
if (err instanceof CexyApiError) return err.code;
|
|
836
|
+
if (err instanceof CexyTimeoutError) return "TIMEOUT";
|
|
837
|
+
if (err instanceof CexyConnectionError) return "CONNECTION_ERROR";
|
|
838
|
+
return "ERROR";
|
|
839
|
+
}
|
|
796
840
|
var ORDER_AMOUNT_FIELDS = ["price", "quantity", "quote_quantity", "stop_price"];
|
|
797
841
|
var TradingResource = class extends Resource {
|
|
798
842
|
/** Open orders, optionally filtered by market/status. */
|
|
@@ -823,7 +867,7 @@ var TradingResource = class extends Resource {
|
|
|
823
867
|
*
|
|
824
868
|
* Retry safety rests on `client_order_id` (generated as a UUID when absent): it is unique
|
|
825
869
|
* per account and the server refuses a repeat before any funds move. The server does NOT
|
|
826
|
-
* honour `Idempotency-Key` on orders
|
|
870
|
+
* honour `Idempotency-Key` on orders, so none is sent. After an
|
|
827
871
|
* ambiguous failure (network error, timeout or 5xx) the SDK first looks the order up by
|
|
828
872
|
* `client_order_id` and returns it if it exists (`recovered: true`); only if it does not
|
|
829
873
|
* exist does it send the order again, with the same `client_order_id`, so a late-arriving
|
|
@@ -838,12 +882,11 @@ var TradingResource = class extends Resource {
|
|
|
838
882
|
assertAmountFields(order, ORDER_AMOUNT_FIELDS, "placeOrder");
|
|
839
883
|
const clientOrderId = order.client_order_id ?? newId();
|
|
840
884
|
const body = { ...order, client_order_id: clientOrderId };
|
|
841
|
-
const idempotencyKey = opts.idempotencyKey ?? newId();
|
|
842
885
|
const maxRetries = opts.maxRetries ?? this.t.config.maxRetries;
|
|
843
886
|
const info = OPERATIONS.place_order;
|
|
844
887
|
for (let attempt = 0; ; attempt++) {
|
|
845
888
|
try {
|
|
846
|
-
const raw = await this.t.attempt({ op: "place_order", body
|
|
889
|
+
const raw = await this.t.attempt({ op: "place_order", body }, opts);
|
|
847
890
|
const data = raw.data.data;
|
|
848
891
|
return { ...data, client_order_id: clientOrderId, recovered: false };
|
|
849
892
|
} catch (err) {
|
|
@@ -853,11 +896,11 @@ var TradingResource = class extends Resource {
|
|
|
853
896
|
const existing = await this.#lookup(clientOrderId, err, opts);
|
|
854
897
|
if (existing) return { order: existing, fills: [], client_order_id: clientOrderId, recovered: true };
|
|
855
898
|
if (duplicateAfterRetry || attempt >= maxRetries) throw err;
|
|
856
|
-
await this.t.backoff("place_order", info, attempt, err,
|
|
899
|
+
await this.t.backoff("place_order", info, attempt, err, void 0, opts.signal);
|
|
857
900
|
continue;
|
|
858
901
|
}
|
|
859
902
|
if (isRetryable(err) && attempt < maxRetries) {
|
|
860
|
-
await this.t.backoff("place_order", info, attempt, err,
|
|
903
|
+
await this.t.backoff("place_order", info, attempt, err, void 0, opts.signal);
|
|
861
904
|
continue;
|
|
862
905
|
}
|
|
863
906
|
throw err;
|
|
@@ -881,10 +924,9 @@ var TradingResource = class extends Resource {
|
|
|
881
924
|
async cancelOrder(orderId, opts = {}) {
|
|
882
925
|
const maxRetries = opts.maxRetries ?? this.t.config.maxRetries;
|
|
883
926
|
const info = OPERATIONS.cancel_order;
|
|
884
|
-
const idempotencyKey = opts.idempotencyKey ?? newId();
|
|
885
927
|
for (let attempt = 0; ; attempt++) {
|
|
886
928
|
try {
|
|
887
|
-
const raw = await this.t.attempt({ op: "cancel_order", pathParams: { order_id: orderId }
|
|
929
|
+
const raw = await this.t.attempt({ op: "cancel_order", pathParams: { order_id: orderId } }, opts);
|
|
888
930
|
return raw.data.data;
|
|
889
931
|
} catch (err) {
|
|
890
932
|
if (opts.signal?.aborted) throw err;
|
|
@@ -892,7 +934,7 @@ var TradingResource = class extends Resource {
|
|
|
892
934
|
return this.order(orderId, { signal: opts.signal, timeoutMs: opts.timeoutMs });
|
|
893
935
|
}
|
|
894
936
|
if (isRetryable(err) && attempt < maxRetries) {
|
|
895
|
-
await this.t.backoff("cancel_order", info, attempt, err,
|
|
937
|
+
await this.t.backoff("cancel_order", info, attempt, err, void 0, opts.signal);
|
|
896
938
|
continue;
|
|
897
939
|
}
|
|
898
940
|
throw err;
|
|
@@ -906,7 +948,7 @@ var TradingResource = class extends Resource {
|
|
|
906
948
|
if (typeof symbol === "string" && symbol !== "") body = { symbol };
|
|
907
949
|
else if (hasSymbol && symbol === null) body = {};
|
|
908
950
|
else throw new CexyConfigError('cancelAll(): pass { symbol: "BASE/QUOTE" }, or { symbol: null } to cancel in every market');
|
|
909
|
-
const once = () => this.data({ op: "cancel_all", body }, opts);
|
|
951
|
+
const once = (o) => this.data({ op: "cancel_all", body }, o ?? opts);
|
|
910
952
|
if (params.untilDone !== true) return once();
|
|
911
953
|
const maxRounds = params.maxRounds ?? 20;
|
|
912
954
|
const budgetMs = params.timeBudgetMs ?? 12e4;
|
|
@@ -915,53 +957,90 @@ var TradingResource = class extends Resource {
|
|
|
915
957
|
const now = this.t.config.now ?? Date.now;
|
|
916
958
|
const start = now();
|
|
917
959
|
const state = /* @__PURE__ */ new Map();
|
|
960
|
+
const roundOpts = { ...opts, maxRetries: 0 };
|
|
918
961
|
let rounds = 0;
|
|
919
962
|
let idle = 0;
|
|
920
|
-
let
|
|
963
|
+
let hasMore = false;
|
|
964
|
+
let lastErrorCode;
|
|
921
965
|
let stopped;
|
|
966
|
+
const result = () => {
|
|
967
|
+
const pick = (list) => [...state].filter(([, v]) => v.list === list).map(([id]) => id);
|
|
968
|
+
return {
|
|
969
|
+
cancelled: pick("cancelled"),
|
|
970
|
+
already_closed: pick("already_closed"),
|
|
971
|
+
failed: pick("failed"),
|
|
972
|
+
failures: [...state.values()].flatMap((v) => v.list === "failed" && v.failure ? [v.failure] : []),
|
|
973
|
+
has_more: hasMore,
|
|
974
|
+
rounds,
|
|
975
|
+
stopped,
|
|
976
|
+
...lastErrorCode !== void 0 ? { last_error_code: lastErrorCode } : {}
|
|
977
|
+
};
|
|
978
|
+
};
|
|
922
979
|
for (; ; ) {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
980
|
+
let waitMs = 0;
|
|
981
|
+
let res;
|
|
982
|
+
try {
|
|
983
|
+
res = await once(roundOpts);
|
|
984
|
+
} catch (err) {
|
|
985
|
+
rounds++;
|
|
986
|
+
if (opts?.signal?.aborted) throw err;
|
|
987
|
+
if (!isRetryable(err)) {
|
|
988
|
+
stopped = "done";
|
|
989
|
+
throw new CancelAllInterruptedError(err, result());
|
|
990
|
+
}
|
|
991
|
+
lastErrorCode = errorCode(err);
|
|
992
|
+
if (rounds >= maxRounds) {
|
|
993
|
+
stopped = "max_rounds";
|
|
994
|
+
break;
|
|
995
|
+
}
|
|
996
|
+
const hint = err instanceof RateLimitError ? err.retryAfterMs : null;
|
|
997
|
+
if (hint !== null) {
|
|
998
|
+
waitMs = hint;
|
|
999
|
+
if (hint > MAX_SERVER_WAIT_MS) {
|
|
1000
|
+
stopped = "time_budget";
|
|
1001
|
+
break;
|
|
1002
|
+
}
|
|
1003
|
+
} else {
|
|
1004
|
+
waitMs = (CANCEL_BACKOFF_S[Math.min(idle++, CANCEL_BACKOFF_S.length - 1)] ?? 15) * 1e3;
|
|
1005
|
+
}
|
|
936
1006
|
}
|
|
937
|
-
if (
|
|
938
|
-
|
|
939
|
-
|
|
1007
|
+
if (res) {
|
|
1008
|
+
rounds++;
|
|
1009
|
+
lastErrorCode = void 0;
|
|
1010
|
+
hasMore = res.has_more;
|
|
1011
|
+
const failures = res.failures ?? [];
|
|
1012
|
+
for (const id of res.cancelled ?? []) state.set(id, { list: "cancelled" });
|
|
1013
|
+
for (const id of res.already_closed ?? []) state.set(id, { list: "already_closed" });
|
|
1014
|
+
for (const id of res.failed ?? []) {
|
|
1015
|
+
const failure = failures.find((f) => f.order_id === id);
|
|
1016
|
+
state.set(id, failure ? { list: "failed", failure } : { list: "failed" });
|
|
1017
|
+
}
|
|
1018
|
+
const progress = (res.cancelled?.length ?? 0) + (res.already_closed?.length ?? 0) > 0;
|
|
1019
|
+
if (!res.has_more && !failures.some((f) => CANCEL_RETRY_CODES.has(f.code))) {
|
|
1020
|
+
stopped = "done";
|
|
1021
|
+
break;
|
|
1022
|
+
}
|
|
1023
|
+
if (rounds >= maxRounds) {
|
|
1024
|
+
stopped = "max_rounds";
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
if (progress) idle = 0;
|
|
1028
|
+
else waitMs = (CANCEL_BACKOFF_S[Math.min(idle++, CANCEL_BACKOFF_S.length - 1)] ?? 15) * 1e3;
|
|
940
1029
|
}
|
|
941
|
-
|
|
942
|
-
if (
|
|
943
|
-
else waitMs = (CANCEL_BACKOFF_S[Math.min(idle++, CANCEL_BACKOFF_S.length - 1)] ?? 15) * 1e3;
|
|
944
|
-
if (now() - start + waitMs >= budgetMs) {
|
|
1030
|
+
const limiterMs = this.t.config.limiter?.pendingWaitMs() ?? 0;
|
|
1031
|
+
if (now() - start + Math.max(waitMs, limiterMs) >= budgetMs) {
|
|
945
1032
|
stopped = "time_budget";
|
|
1033
|
+
if (limiterMs > waitMs) lastErrorCode = "RATE_LIMITED";
|
|
946
1034
|
break;
|
|
947
1035
|
}
|
|
948
1036
|
if (waitMs > 0) await this.t.config.sleep(waitMs, opts?.signal);
|
|
949
1037
|
}
|
|
950
|
-
|
|
951
|
-
return {
|
|
952
|
-
cancelled: pick("cancelled"),
|
|
953
|
-
already_closed: pick("already_closed"),
|
|
954
|
-
failed: pick("failed"),
|
|
955
|
-
failures: [...state.values()].flatMap((v) => v.list === "failed" && v.failure ? [v.failure] : []),
|
|
956
|
-
has_more: last.has_more,
|
|
957
|
-
rounds,
|
|
958
|
-
stopped
|
|
959
|
-
};
|
|
1038
|
+
return result();
|
|
960
1039
|
}
|
|
961
1040
|
};
|
|
962
1041
|
|
|
963
1042
|
// src/version.ts
|
|
964
|
-
var VERSION = "0.1.0-dev.
|
|
1043
|
+
var VERSION = "0.1.0-dev.6";
|
|
965
1044
|
var USER_AGENT = `cexy-typescript/${VERSION}`;
|
|
966
1045
|
|
|
967
1046
|
// src/ws/emitter.ts
|
|
@@ -1729,7 +1808,8 @@ var CexyClient = class {
|
|
|
1729
1808
|
const sleep2 = options.sleep ?? sleep;
|
|
1730
1809
|
const limiter = options.rateLimit === false ? null : new RateLimiter({
|
|
1731
1810
|
requestsPerMinute: options.rateLimit?.requestsPerMinute ?? (authenticator ? DEFAULT_RPM_WITH_KEY : DEFAULT_RPM_ANONYMOUS),
|
|
1732
|
-
sleep: sleep2
|
|
1811
|
+
sleep: sleep2,
|
|
1812
|
+
...options.now ? { now: options.now } : {}
|
|
1733
1813
|
});
|
|
1734
1814
|
this.#baseUrl = baseUrl;
|
|
1735
1815
|
this.#transport = new Transport({
|
|
@@ -1812,11 +1892,29 @@ function canSetUserAgent() {
|
|
|
1812
1892
|
return !(typeof g.window !== "undefined" && typeof g.window.document !== "undefined");
|
|
1813
1893
|
}
|
|
1814
1894
|
|
|
1895
|
+
// src/ledger.ts
|
|
1896
|
+
var KNOWN_LEDGER_REFERENCE_TYPES = /* @__PURE__ */ new Set([
|
|
1897
|
+
"deposit",
|
|
1898
|
+
"withdrawal",
|
|
1899
|
+
"order",
|
|
1900
|
+
"trade",
|
|
1901
|
+
"transfer",
|
|
1902
|
+
"adjustment",
|
|
1903
|
+
"pool",
|
|
1904
|
+
"futures_transfer",
|
|
1905
|
+
"system"
|
|
1906
|
+
]);
|
|
1907
|
+
function isLedgerReference(ref, type) {
|
|
1908
|
+
if (typeof ref !== "object" || ref === null || typeof ref.type !== "string") return false;
|
|
1909
|
+
return type === void 0 ? KNOWN_LEDGER_REFERENCE_TYPES.has(ref.type) : ref.type === type;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1815
1912
|
exports.AccountResource = AccountResource;
|
|
1816
1913
|
exports.ApiKeyAuthenticator = ApiKeyAuthenticator;
|
|
1817
1914
|
exports.AssetsResource = AssetsResource;
|
|
1818
1915
|
exports.AuthenticationError = AuthenticationError;
|
|
1819
1916
|
exports.CLIENT_ERROR_CODES = CLIENT_ERROR_CODES;
|
|
1917
|
+
exports.CancelAllInterruptedError = CancelAllInterruptedError;
|
|
1820
1918
|
exports.CexyApiError = CexyApiError;
|
|
1821
1919
|
exports.CexyClient = CexyClient;
|
|
1822
1920
|
exports.CexyConfigError = CexyConfigError;
|
|
@@ -1837,6 +1935,7 @@ exports.InvalidAmountError = InvalidAmountError;
|
|
|
1837
1935
|
exports.JurisdictionBlockedError = JurisdictionBlockedError;
|
|
1838
1936
|
exports.KNOWN_EVENT_TYPES = KNOWN_EVENT_TYPES;
|
|
1839
1937
|
exports.LiveOrderBook = LiveOrderBook;
|
|
1938
|
+
exports.MAX_SERVER_WAIT_MS = MAX_SERVER_WAIT_MS;
|
|
1840
1939
|
exports.MarketsResource = MarketsResource;
|
|
1841
1940
|
exports.NetworksResource = NetworksResource;
|
|
1842
1941
|
exports.NotFoundError = NotFoundError;
|
|
@@ -1860,6 +1959,7 @@ exports.assertAmountFields = assertAmountFields;
|
|
|
1860
1959
|
exports.errorFromResponse = errorFromResponse;
|
|
1861
1960
|
exports.isAmount = isAmount;
|
|
1862
1961
|
exports.isKnownErrorCode = isKnownErrorCode;
|
|
1962
|
+
exports.isLedgerReference = isLedgerReference;
|
|
1863
1963
|
exports.isLocalHost = isLocalHost;
|
|
1864
1964
|
exports.isRetryable = isRetryable;
|
|
1865
1965
|
exports.paginate = paginate;
|