@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 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). The server allows 30 cancel-all
79
- calls per minute per account; a 429 is retried after its Retry-After, which counts against the budget.
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`, not on `Idempotency-Key` (the server does not honour
151
- that header on `POST /trading/orders`, order cancels or cancel-all). `placeOrder` always sends a
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 auto-generated `Idempotency-Key`, reused on every retry; the server honours
161
- it there, so they execute once. A 409 `CONCURRENT_MODIFICATION` (the same key still in flight) is
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
- const secs = Number(h);
144
- if (Number.isFinite(secs)) best = Math.max(0, secs * 1e3);
145
- else {
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 (!Number.isNaN(at)) best = Math.max(0, at - Date.now());
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: GETs retry on retryable errors and
324
- * network failures. Mutations carry an `Idempotency-Key` reused on every attempt; the server
325
- * honours it on pool join/exit, which makes their retries safe. The other mutations routed
326
- * here (cancel-all) are naturally repeatable. `placeOrder` and `cancelOrder` use
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 isMutation = info.method !== "GET";
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
- /** Waits before retry number `attempt + 1`, honouring server hints. */
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 = err instanceof RateLimitError ? err.retryAfterMs : err instanceof CexyApiError ? retryAfterMs(void 0, err.details) : null;
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 (info.method !== "GET" && spec.idempotencyKey) headers.set("Idempotency-Key", spec.idempotencyKey);
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 > 0 && limit < this.#rpm) {
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
- /** Blocks all requests for `ms` (used for 429 Retry-After). */
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 (the header is sent but gives no protection). After an
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, idempotencyKey }, opts);
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, idempotencyKey, opts.signal);
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, idempotencyKey, opts.signal);
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 }, idempotencyKey }, opts);
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, idempotencyKey, opts.signal);
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 last;
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
- last = await once();
924
- rounds++;
925
- const failures = last.failures ?? [];
926
- for (const id of last.cancelled ?? []) state.set(id, { list: "cancelled" });
927
- for (const id of last.already_closed ?? []) state.set(id, { list: "already_closed" });
928
- for (const id of last.failed ?? []) {
929
- const failure = failures.find((f) => f.order_id === id);
930
- state.set(id, failure ? { list: "failed", failure } : { list: "failed" });
931
- }
932
- const progress = (last.cancelled?.length ?? 0) + (last.already_closed?.length ?? 0) > 0;
933
- if (!last.has_more && !failures.some((f) => CANCEL_RETRY_CODES.has(f.code))) {
934
- stopped = "done";
935
- break;
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 (rounds >= maxRounds) {
938
- stopped = "max_rounds";
939
- break;
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
- let waitMs = 0;
942
- if (progress) idle = 0;
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
- const pick = (list) => [...state].filter(([, v]) => v.list === list).map(([id]) => id);
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.4";
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;