@cexyio/cexy 0.1.0-dev.2 → 0.1.0-dev.4

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
@@ -5,7 +5,7 @@ The official TypeScript/JavaScript SDK for the [CEXY.io](https://cexy.io) REST a
5
5
  - Typed models generated from the public OpenAPI spec ([cexy-api-spec](https://github.com/cexyio/cexy-api-spec)).
6
6
  - Safe by default: retries with backoff, order placement that never duplicates (via `client_order_id`), a client-side rate limiter.
7
7
  - A WebSocket client with heartbeat, reconnect and a live order book that applies the sync rules for you.
8
- - ESM and CommonJS, Node 20+, zero runtime dependencies.
8
+ - ESM and CommonJS, Node 22+, zero runtime dependencies.
9
9
 
10
10
  > **Status: 0.x.** The API is not yet frozen. It stays 0.x until the exchange ships HMAC request signing,
11
11
  > which will change how credentials are sent.
@@ -15,7 +15,7 @@ The official TypeScript/JavaScript SDK for the [CEXY.io](https://cexy.io) REST a
15
15
 
16
16
  ```bash
17
17
  npm install @cexyio/cexy@next
18
- # Node only, optional: lets the WebSocket client send a User-Agent (recommended before Node 22)
18
+ # Node only, optional: lets the WebSocket client send a User-Agent
19
19
  npm install ws
20
20
  ```
21
21
 
@@ -60,7 +60,23 @@ await cexy.trading.cancelAll({ symbol: "BTC/USDT" }); // { symbol: null } = ever
60
60
  ```
61
61
 
62
62
  `cancelAll` requires `symbol`: the server treats a missing symbol as "every market", so the SDK makes
63
- you say so with `{ symbol: null }`. The server allows cancel-all 30 times per minute per account.
63
+ you say so with `{ symbol: null }`. An unknown symbol throws `NotFoundError`.
64
+
65
+ One call handles at most 500 orders and puts each in exactly one list: `cancelled`, `already_closed`
66
+ (it filled, was refused or was cancelled elsewhere first; not an error) or `failed`, with the reason in
67
+ `failures` (`INVALID_STATE` for an order still being placed). `has_more: true` means more orders remain.
68
+ To let the SDK repeat the call for you:
69
+
70
+ ```ts
71
+ const res = await cexy.trading.cancelAll({ symbol: null, untilDone: true });
72
+ // res.stopped: "done" | "max_rounds" | "time_budget"; res.rounds: calls made
73
+ if (res.stopped !== "done" || res.failed.length) console.warn("left over:", res.failures);
74
+ ```
75
+
76
+ `untilDone` repeats while `has_more` is true or a failure is `INVALID_STATE` / `SERVICE_UNAVAILABLE`.
77
+ 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.
64
80
 
65
81
  Give both `apiKey` and `apiSecret`, or neither: passing only one throws at construction.
66
82
 
@@ -110,7 +126,7 @@ Every API failure throws a `CexyApiError` (or a subclass) with `status`, `code`,
110
126
  | `UnprocessableError` | 422: `INSUFFICIENT_FUNDS`, `MARKET_UNAVAILABLE`, ... |
111
127
  | `RateLimitError` | 429, with `retryAfterMs` |
112
128
  | `ServerError` | 5xx |
113
- | `CexyApiError` | any code this SDK version does not know yet |
129
+ | `CexyApiError` | any code this SDK version does not know yet, and `UNEXPECTED_REDIRECT` (the server answered with a 3xx; set by the SDK, see `CLIENT_ERROR_CODES`) |
114
130
 
115
131
  Local problems use `CexyConfigError`, `InvalidAmountError`, `CexyConnectionError` / `CexyTimeoutError`
116
132
  and `OrderStateUnknownError`. `ErrorCode` is a union of the known codes plus `string`, because new codes
@@ -215,7 +231,7 @@ use public channels and poll REST for private state. If the session is revoked,
215
231
  `authLost`; public channels keep working.
216
232
 
217
233
  In Node the client uses the optional `ws` package when installed (so it can send the SDK User-Agent),
218
- otherwise the global `WebSocket` (Node 22+, browsers).
234
+ otherwise the global `WebSocket` (Node, browsers).
219
235
 
220
236
  ## Browsers
221
237
 
@@ -229,6 +245,10 @@ endpoints from a server. Browsers do not let scripts set `User-Agent`, so the SD
229
245
  - Credentials go only in the `X-API-Key` / `X-API-Secret` headers and only on private endpoints; never in URLs.
230
246
  - Only `https://` base URLs and `wss://` WebSocket URLs are accepted. `allowInsecure: true` permits
231
247
  `http://` / `ws://` solely for `localhost`, `127.0.0.1` or `::1` (local test servers).
248
+ - The SDK **never follows HTTP redirects**. A 3xx answer throws a `CexyApiError` with code
249
+ `UNEXPECTED_REDIRECT` (not retried), so credentials are never re-sent to another host and an order
250
+ is never re-posted to a redirect target. If you pass your own `fetch`, it must honour `redirect: "manual"`: one that follows redirects
251
+ anyway has already sent your credentials by the time the SDK notices.
232
252
  - The SDK redacts the secret from `toString()`, `util.inspect`, `JSON.stringify` and error messages.
233
253
  - Keep keys in environment variables or a secret manager, not in code.
234
254
 
package/dist/index.cjs CHANGED
@@ -131,6 +131,7 @@ var KNOWN_CODES = /* @__PURE__ */ new Set([
131
131
  "UNDER_MAINTENANCE",
132
132
  "ENGINE_OVERLOADED"
133
133
  ]);
134
+ var CLIENT_ERROR_CODES = { UNEXPECTED_REDIRECT: "UNEXPECTED_REDIRECT" };
134
135
  function isKnownErrorCode(code) {
135
136
  return KNOWN_CODES.has(code);
136
137
  }
@@ -394,7 +395,13 @@ var Transport = class {
394
395
  let res;
395
396
  let text;
396
397
  try {
397
- res = await this.config.fetch(url.toString(), { method: info.method, headers, body, signal: ctrl.signal });
398
+ res = await this.config.fetch(url.toString(), {
399
+ method: info.method,
400
+ headers,
401
+ body,
402
+ signal: ctrl.signal,
403
+ redirect: "manual"
404
+ });
398
405
  text = await res.text();
399
406
  } catch (err) {
400
407
  if (opts.signal?.aborted) throw opts.signal.reason ?? err;
@@ -406,6 +413,14 @@ var Transport = class {
406
413
  clearTimeout(timer);
407
414
  opts.signal?.removeEventListener("abort", onAbort);
408
415
  }
416
+ if (res.type === "opaqueredirect" || res.status >= 300 && res.status < 400 || res.redirected) {
417
+ throw new CexyApiError({
418
+ status: res.status,
419
+ code: CLIENT_ERROR_CODES.UNEXPECTED_REDIRECT,
420
+ message: `${info.method} ${info.path}: the server answered with a redirect (HTTP ${res.status}); the SDK does not follow redirects. Check baseUrl.`,
421
+ retryable: false
422
+ });
423
+ }
409
424
  this.config.limiter?.update(res.headers);
410
425
  if (!res.ok) {
411
426
  const parsed = safeJson(text);
@@ -776,6 +791,8 @@ var WalletResource = class extends Resource {
776
791
  return this.data({ op: "deposit_address", query: params }, opts);
777
792
  }
778
793
  };
794
+ var CANCEL_RETRY_CODES = /* @__PURE__ */ new Set(["INVALID_STATE", "SERVICE_UNAVAILABLE"]);
795
+ var CANCEL_BACKOFF_S = [1, 2, 4, 8, 15];
779
796
  var ORDER_AMOUNT_FIELDS = ["price", "quantity", "quote_quantity", "stop_price"];
780
797
  var TradingResource = class extends Resource {
781
798
  /** Open orders, optionally filtered by market/status. */
@@ -882,16 +899,6 @@ var TradingResource = class extends Resource {
882
899
  }
883
900
  }
884
901
  }
885
- /**
886
- * Cancels every open order in one market: `cancelAll({ symbol: "BTC/USDT" })`.
887
- * To cancel across ALL markets, pass `symbol: null` explicitly: `cancelAll({ symbol: null })`.
888
- * Omitting `symbol` is an error, so an account-wide cancel never happens by accident
889
- * (the server itself treats `{}` as every market).
890
- *
891
- * The server limits cancel-all to 30 calls per minute per account. It is naturally
892
- * repeatable, so it is retried after network errors; a retry reports only what that retry
893
- * cancelled.
894
- */
895
902
  async cancelAll(params, opts) {
896
903
  const hasSymbol = !!params && typeof params === "object" && Object.prototype.hasOwnProperty.call(params, "symbol");
897
904
  const symbol = hasSymbol ? params.symbol : void 0;
@@ -899,12 +906,62 @@ var TradingResource = class extends Resource {
899
906
  if (typeof symbol === "string" && symbol !== "") body = { symbol };
900
907
  else if (hasSymbol && symbol === null) body = {};
901
908
  else throw new CexyConfigError('cancelAll(): pass { symbol: "BASE/QUOTE" }, or { symbol: null } to cancel in every market');
902
- return this.data({ op: "cancel_all", body }, opts);
909
+ const once = () => this.data({ op: "cancel_all", body }, opts);
910
+ if (params.untilDone !== true) return once();
911
+ const maxRounds = params.maxRounds ?? 20;
912
+ const budgetMs = params.timeBudgetMs ?? 12e4;
913
+ if (!(maxRounds >= 1)) throw new CexyConfigError("cancelAll(): maxRounds must be >= 1");
914
+ if (!(budgetMs > 0)) throw new CexyConfigError("cancelAll(): timeBudgetMs must be > 0");
915
+ const now = this.t.config.now ?? Date.now;
916
+ const start = now();
917
+ const state = /* @__PURE__ */ new Map();
918
+ let rounds = 0;
919
+ let idle = 0;
920
+ let last;
921
+ let stopped;
922
+ 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;
936
+ }
937
+ if (rounds >= maxRounds) {
938
+ stopped = "max_rounds";
939
+ break;
940
+ }
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) {
945
+ stopped = "time_budget";
946
+ break;
947
+ }
948
+ if (waitMs > 0) await this.t.config.sleep(waitMs, opts?.signal);
949
+ }
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
+ };
903
960
  }
904
961
  };
905
962
 
906
963
  // src/version.ts
907
- var VERSION = "0.1.0-dev.2";
964
+ var VERSION = "0.1.0-dev.4";
908
965
  var USER_AGENT = `cexy-typescript/${VERSION}`;
909
966
 
910
967
  // src/ws/emitter.ts
@@ -1664,7 +1721,7 @@ var CexyClient = class {
1664
1721
  assertSecureUrl(parsed, "https:", "http:", options.allowInsecure === true, "baseUrl");
1665
1722
  this.#allowInsecure = options.allowInsecure === true;
1666
1723
  const fetchImpl = options.fetch ?? globalThis.fetch;
1667
- if (!fetchImpl) throw new CexyConfigError("no global fetch found (Node 20+ required); pass options.fetch");
1724
+ if (!fetchImpl) throw new CexyConfigError("no global fetch found (Node 22+ required); pass options.fetch");
1668
1725
  const timeoutMs = options.timeoutMs ?? 1e4;
1669
1726
  const maxRetries = options.maxRetries ?? 3;
1670
1727
  if (!(timeoutMs > 0)) throw new CexyConfigError("timeoutMs must be > 0");
@@ -1685,6 +1742,7 @@ var CexyClient = class {
1685
1742
  userAgent: canSetUserAgent() ? [USER_AGENT, options.userAgentSuffix].filter(Boolean).join(" ") : null,
1686
1743
  sleep: sleep2,
1687
1744
  random: options.random ?? Math.random,
1745
+ now: options.now ?? Date.now,
1688
1746
  onRetry: options.onRetry
1689
1747
  });
1690
1748
  const t = this.#transport;
@@ -1758,6 +1816,7 @@ exports.AccountResource = AccountResource;
1758
1816
  exports.ApiKeyAuthenticator = ApiKeyAuthenticator;
1759
1817
  exports.AssetsResource = AssetsResource;
1760
1818
  exports.AuthenticationError = AuthenticationError;
1819
+ exports.CLIENT_ERROR_CODES = CLIENT_ERROR_CODES;
1761
1820
  exports.CexyApiError = CexyApiError;
1762
1821
  exports.CexyClient = CexyClient;
1763
1822
  exports.CexyConfigError = CexyConfigError;
package/dist/index.d.cts CHANGED
@@ -163,6 +163,8 @@ interface TransportConfig {
163
163
  userAgent: string | null;
164
164
  sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
165
165
  random: () => number;
166
+ /** Milliseconds clock (default `Date.now`); injectable for tests. */
167
+ now?: () => number;
166
168
  onRetry?: ((info: RetryInfo) => void) | undefined;
167
169
  }
168
170
  interface CallSpec {
@@ -730,7 +732,11 @@ interface paths {
730
732
  put?: never;
731
733
  /**
732
734
  * Cancels every open order, optionally within one market.
733
- * @description Best-effort: a failure on one order does not stop the rest, and both outcomes are reported. A panic-button endpoint that stops at the first problem is worse than useless.
735
+ * @description Best-effort: a failure on one order does not stop the rest, and every outcome is reported. A panic-button endpoint that stops at the first problem is worse than useless.
736
+ *
737
+ * An order still being placed when the call starts (status `pending`, for as long as its own placement request runs) is waited for, up to 500 ms per call in total: cancelled if it opens, reported in `already_closed` if it fills or is refused, and in `failed` with code `INVALID_STATE` if it is still being placed at the deadline. Orders placed after the call starts are not part of it. At most 500 orders per call; `has_more` says there are others.
738
+ *
739
+ * Limited to 30 calls a minute per account, on top of the general request limit.
734
740
  */
735
741
  post: operations["cancel_all"];
736
742
  delete?: never;
@@ -1037,12 +1043,31 @@ interface components {
1037
1043
  /** @description Limit the cancellation to one market. Omitted or `null`, every market's open orders are cancelled. */
1038
1044
  symbol?: string | null;
1039
1045
  };
1040
- /** @description What a bulk cancellation achieved. */
1046
+ /**
1047
+ * @description What a bulk cancellation achieved.
1048
+ *
1049
+ * Every order the call handled is in exactly one of `cancelled`, `already_closed` and `failed`.
1050
+ */
1041
1051
  CancelAllResponse: {
1052
+ /** @description Orders that closed on their own (filled, rejected, cancelled elsewhere) before this call reached them. Nothing was done to them, and they are not failures. */
1053
+ already_closed: string[];
1042
1054
  /** @description Orders cancelled. */
1043
1055
  cancelled: string[];
1044
- /** @description Orders that could not be cancelled. Each failure is logged server-side. */
1056
+ /** @description Orders that could not be cancelled in this call. Check `failures` for why, then refresh or call again. */
1045
1057
  failed: string[];
1058
+ /** @description Why each order in `failed` could not be cancelled. */
1059
+ failures: components["schemas"]["CancelFailureResponse"][];
1060
+ /** @description More open orders exist than one call handles (500). Call again. */
1061
+ has_more: boolean;
1062
+ };
1063
+ /** @description Why one order could not be cancelled. */
1064
+ CancelFailureResponse: {
1065
+ /** @description The error code of the attempt, as in any error response: e.g. `INVALID_STATE` for an order still being placed when the wait ran out, or `MARKET_UNAVAILABLE`. */
1066
+ code: string;
1067
+ /** @description Its message. */
1068
+ message: string;
1069
+ /** @description The order. */
1070
+ order_id: string;
1046
1071
  };
1047
1072
  /**
1048
1073
  * @description Candle/kline intervals for market data.
@@ -2877,6 +2902,24 @@ interface operations {
2877
2902
  };
2878
2903
  };
2879
2904
  };
2905
+ /** @description No such market */
2906
+ 404: {
2907
+ headers: {
2908
+ [name: string]: unknown;
2909
+ };
2910
+ content: {
2911
+ "application/json": components["schemas"]["ErrorResponse"];
2912
+ };
2913
+ };
2914
+ /** @description Over 30 calls a minute for this account; `details.retry_after_seconds` says when to retry */
2915
+ 429: {
2916
+ headers: {
2917
+ [name: string]: unknown;
2918
+ };
2919
+ content: {
2920
+ "application/json": components["schemas"]["ErrorResponse"];
2921
+ };
2922
+ };
2880
2923
  };
2881
2924
  };
2882
2925
  order_history: {
@@ -3181,6 +3224,21 @@ type AssetNetwork = S["AssetNetworkResponse"];
3181
3224
  type Balance = S["BalanceResponse"];
3182
3225
  type CancelAllRequest = S["CancelAllRequest"];
3183
3226
  type CancelAllResult = S["CancelAllResponse"];
3227
+ /** Why one order could not be cancelled (`CancelAllResult.failures`). */
3228
+ type CancelFailure = S["CancelFailureResponse"];
3229
+ /** Why a `cancelAll({ ..., untilDone: true })` loop ended. */
3230
+ type CancelAllStopReason = "done" | "max_rounds" | "time_budget";
3231
+ /**
3232
+ * Merged result of a `cancelAll({ ..., untilDone: true })` loop. Every order is in exactly one
3233
+ * of `cancelled`, `already_closed` and `failed`, in its latest state; `has_more` is the last
3234
+ * round's.
3235
+ */
3236
+ interface CancelAllUntilDoneResult extends CancelAllResult {
3237
+ /** Number of cancel-all calls made. */
3238
+ rounds: number;
3239
+ /** `done`: nothing left to retry; otherwise the loop's limit that ended it. */
3240
+ stopped: CancelAllStopReason;
3241
+ }
3184
3242
  type Candle = S["CandleResponse"];
3185
3243
  type CandleInterval = S["CandleInterval"];
3186
3244
  type DepositAddress = S["DepositAddressResponse"];
@@ -3344,6 +3402,14 @@ interface PlaceOrderResult extends PlaceOrderResponse {
3344
3402
  */
3345
3403
  interface CancelAllParams {
3346
3404
  symbol: string | null;
3405
+ /**
3406
+ * Repeat the call until nothing is left to retry (see `cancelAll`). Default false: one call.
3407
+ */
3408
+ untilDone?: boolean;
3409
+ /** With `untilDone`: at most this many calls. Default 20. */
3410
+ maxRounds?: number;
3411
+ /** With `untilDone`: stop before a wait would take the loop to this many ms. Default 120000. */
3412
+ timeBudgetMs?: number;
3347
3413
  }
3348
3414
  declare class TradingResource extends Resource {
3349
3415
  #private;
@@ -3380,12 +3446,27 @@ declare class TradingResource extends Resource {
3380
3446
  * Cancels every open order in one market: `cancelAll({ symbol: "BTC/USDT" })`.
3381
3447
  * To cancel across ALL markets, pass `symbol: null` explicitly: `cancelAll({ symbol: null })`.
3382
3448
  * Omitting `symbol` is an error, so an account-wide cancel never happens by accident
3383
- * (the server itself treats `{}` as every market).
3449
+ * (the server itself treats `{}` as every market). An unknown symbol throws `NotFoundError`.
3450
+ *
3451
+ * One call handles at most 500 orders. Every order it handled is in exactly one list:
3452
+ * `cancelled`; `already_closed` (it filled, was refused or was cancelled elsewhere first:
3453
+ * not an error); or `failed`, with the reason in `failures` (`INVALID_STATE` for an order
3454
+ * still being placed when the server's 500 ms wait ran out). `has_more` says more orders
3455
+ * remain: call again.
3456
+ *
3457
+ * With `untilDone: true` the SDK does that for you. It repeats while `has_more` is true or a
3458
+ * failure is `INVALID_STATE` / `SERVICE_UNAVAILABLE`. After a call that made no progress it
3459
+ * waits 1, 2, 4, 8 and then 15 s, starting over after any progress. It stops after
3460
+ * `maxRounds` calls or before a wait would pass `timeBudgetMs`, and returns the merged
3461
+ * result with `rounds` and `stopped`. Other failure codes are returned, never retried.
3384
3462
  *
3385
- * The server limits cancel-all to 30 calls per minute per account. It is naturally
3386
- * repeatable, so it is retried after network errors; a retry reports only what that retry
3387
- * cancelled.
3463
+ * The server allows 30 cancel-all calls per minute per account (a 429 is retried after its
3464
+ * Retry-After). The call is naturally repeatable and needs no Idempotency-Key, so it is
3465
+ * retried after network errors; a retry reports only what that retry did.
3388
3466
  */
3467
+ cancelAll(params: CancelAllParams & {
3468
+ untilDone: true;
3469
+ }, opts?: RequestOptions): Promise<CancelAllUntilDoneResult>;
3389
3470
  cancelAll(params: CancelAllParams, opts?: RequestOptions): Promise<CancelAllResult>;
3390
3471
  }
3391
3472
 
@@ -3484,7 +3565,16 @@ declare class RateLimitError extends CexyApiError {
3484
3565
  /** 5xx */
3485
3566
  declare class ServerError extends CexyApiError {
3486
3567
  }
3487
- /** True for codes listed in errors.yaml. */
3568
+ /**
3569
+ * Codes the SDK itself sets on a `CexyApiError`; the API never sends them, so they are not in
3570
+ * errors.yaml and `isKnownErrorCode()` returns false for them.
3571
+ * - `UNEXPECTED_REDIRECT`: the server answered with a 3xx. The SDK never follows redirects (the
3572
+ * credentials would go to the redirect target); not retryable.
3573
+ */
3574
+ declare const CLIENT_ERROR_CODES: {
3575
+ readonly UNEXPECTED_REDIRECT: "UNEXPECTED_REDIRECT";
3576
+ };
3577
+ /** True for codes listed in errors.yaml (server codes; see `CLIENT_ERROR_CODES` for the SDK's own). */
3488
3578
  declare function isKnownErrorCode(code: string): code is KnownErrorCode;
3489
3579
  /**
3490
3580
  * Builds the right error subclass for an HTTP error response.
@@ -3844,7 +3934,12 @@ interface CexyClientOptions {
3844
3934
  timeoutMs?: number;
3845
3935
  /** Retries after the first attempt for retryable failures. Default 3. */
3846
3936
  maxRetries?: number;
3847
- /** A `fetch` implementation. Default: the global `fetch` (Node 20+, browsers). */
3937
+ /**
3938
+ * A `fetch` implementation. Default: the global `fetch` (Node 22+, browsers).
3939
+ * It must honour `redirect: "manual"` (the SDK never follows redirects). A fetch that follows
3940
+ * them anyway has already sent the credentials to the redirect target by the time the SDK sees
3941
+ * `response.redirected` and throws `UNEXPECTED_REDIRECT`.
3942
+ */
3848
3943
  fetch?: FetchLike;
3849
3944
  /**
3850
3945
  * Client-side rate limit in requests per minute, or `false` to disable. Default 100/min
@@ -3866,6 +3961,8 @@ interface CexyClientOptions {
3866
3961
  sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
3867
3962
  /** @internal Deterministic jitter in tests. */
3868
3963
  random?: () => number;
3964
+ /** @internal Replace the clock in tests (milliseconds). */
3965
+ now?: () => number;
3869
3966
  }
3870
3967
  /**
3871
3968
  * CEXY.io REST client.
@@ -3925,11 +4022,11 @@ declare function isAmount(value: unknown): value is Amount;
3925
4022
  declare function assertAmountFields(body: Record<string, unknown>, fields: readonly string[], context: string): void;
3926
4023
 
3927
4024
  /** SDK version, kept in sync with package.json (a test enforces this). */
3928
- declare const VERSION = "0.1.0-dev.2";
4025
+ declare const VERSION = "0.1.0-dev.4";
3929
4026
  /** Default User-Agent product token. */
3930
- declare const USER_AGENT = "cexy-typescript/0.1.0-dev.2";
4027
+ declare const USER_AGENT = "cexy-typescript/0.1.0-dev.4";
3931
4028
 
3932
4029
  /** True for loopback hosts, the only ones where plain-text transport may be allowed. */
3933
4030
  declare function isLocalHost(hostname: string): boolean;
3934
4031
 
3935
- export { AccountResource, type Amount, type ApiKey, ApiKeyAuthenticator, type ApiScope, type Asset, type AssetNetwork, AssetsResource, type AuthRequest, type AuthResult, type AuthenticatedFrame, AuthenticationError, type Authenticator, type Balance, type BalanceUpdatedEvent, type BookLevel, type CancelAllParams, type CancelAllRequest, type CancelAllResult, type Candle, type CandleInterval, CexyApiError, type CexyApiErrorInit, CexyClient, type CexyClientOptions, CexyConfigError, CexyConnectionError, CexyError, CexyTimeoutError, CexyWebSocket, CexyWebSocketError, type CexyWebSocketEvents, type CexyWebSocketOptions, type CloseInfo, ConflictError, type CursorParams, DEFAULT_BASE_URL, DEFAULT_RPM_ANONYMOUS, DEFAULT_RPM_WITH_KEY, DEFAULT_WS_URL, type Deposit, type DepositAddress, type DepositEvent, type DepositStatus, type ErrorBody, type ErrorCode, type ErrorFrame, type EventMap, type ExchangeConfig, type ExitPoolRequest, type ExitPoolResult, ExportsResource, type FeeSchedule, FeesResource, type FetchLike, type Fill, ForbiddenError, InvalidAmountError, type IterateOptions, type JoinPoolRequest, type JoinPoolResult, JurisdictionBlockedError, KNOWN_EVENT_TYPES, type KnownErrorCode, type LedgerEntry, type LedgerEntryKind, type LiquidityRole, type Listener, LiveOrderBook, type LiveOrderBookEvents, type LiveOrderBookOptions, type MaintenanceState, type Market, type MarketStatus, type MarketStatusEvent, MarketsResource, type Network, NetworksResource, NotFoundError, type Notification, type NotificationKind, OPERATIONS, type OperationAuth, type OperationId, type OperationInfo, type OperationScope, type Order, type OrderBook, type OrderBookUpdateData, type OrderBookUpdateEvent, type OrderEvent, type OrderSide, OrderStateUnknownError, type OrderStatus, type OrderType, PRIVATE_CHANNELS, type Page, type PlaceOrderRequest, type PlaceOrderResponse, type PlaceOrderResult, type PongFrame, type Pool, type PoolStatus, PoolsResource, type PublicTrade, type QueryOf, RateLimitError, RateLimiter, type RateLimiterOptions, type RateLimiterState, type ReconnectOptions, type RequestOptions, type ResyncReason, type RetryInfo, SUPPORTED_PROTOCOL_VERSION, ServerError, type ServerTime, type SessionRevokedData, type SessionRevokedEvent, type SnapshotSource, type SortDirection, type SubAccount, type SubscribeResult, type SubscribedFrame, type TickerUpdateEvent, type TimeInForce, type TradeNewEvent, TradingResource, type TriggerDirection, TypedEmitter, USER_AGENT, UnprocessableError, type UnsubscribedFrame, VERSION, ValidationError, WS_BOOK_DEPTH, WalletResource, type WebSocketConstructor, type WebSocketLike, type WelcomeFrame, type Withdrawal, type WithdrawalAddress, type WithdrawalStatus, type WithdrawalUpdatedEvent, type WsEvent, type WsLogger, assertAmountFields, type components, errorFromResponse, isAmount, isKnownErrorCode, isLocalHost, isRetryable, type operations, paginate, type paths };
4032
+ export { AccountResource, type Amount, type ApiKey, ApiKeyAuthenticator, type ApiScope, type Asset, type AssetNetwork, AssetsResource, type AuthRequest, type AuthResult, type AuthenticatedFrame, AuthenticationError, type Authenticator, type Balance, type BalanceUpdatedEvent, type BookLevel, CLIENT_ERROR_CODES, type CancelAllParams, type CancelAllRequest, type CancelAllResult, type CancelAllStopReason, type CancelAllUntilDoneResult, type CancelFailure, type Candle, type CandleInterval, CexyApiError, type CexyApiErrorInit, CexyClient, type CexyClientOptions, CexyConfigError, CexyConnectionError, CexyError, CexyTimeoutError, CexyWebSocket, CexyWebSocketError, type CexyWebSocketEvents, type CexyWebSocketOptions, type CloseInfo, ConflictError, type CursorParams, DEFAULT_BASE_URL, DEFAULT_RPM_ANONYMOUS, DEFAULT_RPM_WITH_KEY, DEFAULT_WS_URL, type Deposit, type DepositAddress, type DepositEvent, type DepositStatus, type ErrorBody, type ErrorCode, type ErrorFrame, type EventMap, type ExchangeConfig, type ExitPoolRequest, type ExitPoolResult, ExportsResource, type FeeSchedule, FeesResource, type FetchLike, type Fill, ForbiddenError, InvalidAmountError, type IterateOptions, type JoinPoolRequest, type JoinPoolResult, JurisdictionBlockedError, KNOWN_EVENT_TYPES, type KnownErrorCode, type LedgerEntry, type LedgerEntryKind, type LiquidityRole, type Listener, LiveOrderBook, type LiveOrderBookEvents, type LiveOrderBookOptions, type MaintenanceState, type Market, type MarketStatus, type MarketStatusEvent, MarketsResource, type Network, NetworksResource, NotFoundError, type Notification, type NotificationKind, OPERATIONS, type OperationAuth, type OperationId, type OperationInfo, type OperationScope, type Order, type OrderBook, type OrderBookUpdateData, type OrderBookUpdateEvent, type OrderEvent, type OrderSide, OrderStateUnknownError, type OrderStatus, type OrderType, PRIVATE_CHANNELS, type Page, type PlaceOrderRequest, type PlaceOrderResponse, type PlaceOrderResult, type PongFrame, type Pool, type PoolStatus, PoolsResource, type PublicTrade, type QueryOf, RateLimitError, RateLimiter, type RateLimiterOptions, type RateLimiterState, type ReconnectOptions, type RequestOptions, type ResyncReason, type RetryInfo, SUPPORTED_PROTOCOL_VERSION, ServerError, type ServerTime, type SessionRevokedData, type SessionRevokedEvent, type SnapshotSource, type SortDirection, type SubAccount, type SubscribeResult, type SubscribedFrame, type TickerUpdateEvent, type TimeInForce, type TradeNewEvent, TradingResource, type TriggerDirection, TypedEmitter, USER_AGENT, UnprocessableError, type UnsubscribedFrame, VERSION, ValidationError, WS_BOOK_DEPTH, WalletResource, type WebSocketConstructor, type WebSocketLike, type WelcomeFrame, type Withdrawal, type WithdrawalAddress, type WithdrawalStatus, type WithdrawalUpdatedEvent, type WsEvent, type WsLogger, assertAmountFields, type components, errorFromResponse, isAmount, isKnownErrorCode, isLocalHost, isRetryable, type operations, paginate, type paths };
package/dist/index.d.ts CHANGED
@@ -163,6 +163,8 @@ interface TransportConfig {
163
163
  userAgent: string | null;
164
164
  sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
165
165
  random: () => number;
166
+ /** Milliseconds clock (default `Date.now`); injectable for tests. */
167
+ now?: () => number;
166
168
  onRetry?: ((info: RetryInfo) => void) | undefined;
167
169
  }
168
170
  interface CallSpec {
@@ -730,7 +732,11 @@ interface paths {
730
732
  put?: never;
731
733
  /**
732
734
  * Cancels every open order, optionally within one market.
733
- * @description Best-effort: a failure on one order does not stop the rest, and both outcomes are reported. A panic-button endpoint that stops at the first problem is worse than useless.
735
+ * @description Best-effort: a failure on one order does not stop the rest, and every outcome is reported. A panic-button endpoint that stops at the first problem is worse than useless.
736
+ *
737
+ * An order still being placed when the call starts (status `pending`, for as long as its own placement request runs) is waited for, up to 500 ms per call in total: cancelled if it opens, reported in `already_closed` if it fills or is refused, and in `failed` with code `INVALID_STATE` if it is still being placed at the deadline. Orders placed after the call starts are not part of it. At most 500 orders per call; `has_more` says there are others.
738
+ *
739
+ * Limited to 30 calls a minute per account, on top of the general request limit.
734
740
  */
735
741
  post: operations["cancel_all"];
736
742
  delete?: never;
@@ -1037,12 +1043,31 @@ interface components {
1037
1043
  /** @description Limit the cancellation to one market. Omitted or `null`, every market's open orders are cancelled. */
1038
1044
  symbol?: string | null;
1039
1045
  };
1040
- /** @description What a bulk cancellation achieved. */
1046
+ /**
1047
+ * @description What a bulk cancellation achieved.
1048
+ *
1049
+ * Every order the call handled is in exactly one of `cancelled`, `already_closed` and `failed`.
1050
+ */
1041
1051
  CancelAllResponse: {
1052
+ /** @description Orders that closed on their own (filled, rejected, cancelled elsewhere) before this call reached them. Nothing was done to them, and they are not failures. */
1053
+ already_closed: string[];
1042
1054
  /** @description Orders cancelled. */
1043
1055
  cancelled: string[];
1044
- /** @description Orders that could not be cancelled. Each failure is logged server-side. */
1056
+ /** @description Orders that could not be cancelled in this call. Check `failures` for why, then refresh or call again. */
1045
1057
  failed: string[];
1058
+ /** @description Why each order in `failed` could not be cancelled. */
1059
+ failures: components["schemas"]["CancelFailureResponse"][];
1060
+ /** @description More open orders exist than one call handles (500). Call again. */
1061
+ has_more: boolean;
1062
+ };
1063
+ /** @description Why one order could not be cancelled. */
1064
+ CancelFailureResponse: {
1065
+ /** @description The error code of the attempt, as in any error response: e.g. `INVALID_STATE` for an order still being placed when the wait ran out, or `MARKET_UNAVAILABLE`. */
1066
+ code: string;
1067
+ /** @description Its message. */
1068
+ message: string;
1069
+ /** @description The order. */
1070
+ order_id: string;
1046
1071
  };
1047
1072
  /**
1048
1073
  * @description Candle/kline intervals for market data.
@@ -2877,6 +2902,24 @@ interface operations {
2877
2902
  };
2878
2903
  };
2879
2904
  };
2905
+ /** @description No such market */
2906
+ 404: {
2907
+ headers: {
2908
+ [name: string]: unknown;
2909
+ };
2910
+ content: {
2911
+ "application/json": components["schemas"]["ErrorResponse"];
2912
+ };
2913
+ };
2914
+ /** @description Over 30 calls a minute for this account; `details.retry_after_seconds` says when to retry */
2915
+ 429: {
2916
+ headers: {
2917
+ [name: string]: unknown;
2918
+ };
2919
+ content: {
2920
+ "application/json": components["schemas"]["ErrorResponse"];
2921
+ };
2922
+ };
2880
2923
  };
2881
2924
  };
2882
2925
  order_history: {
@@ -3181,6 +3224,21 @@ type AssetNetwork = S["AssetNetworkResponse"];
3181
3224
  type Balance = S["BalanceResponse"];
3182
3225
  type CancelAllRequest = S["CancelAllRequest"];
3183
3226
  type CancelAllResult = S["CancelAllResponse"];
3227
+ /** Why one order could not be cancelled (`CancelAllResult.failures`). */
3228
+ type CancelFailure = S["CancelFailureResponse"];
3229
+ /** Why a `cancelAll({ ..., untilDone: true })` loop ended. */
3230
+ type CancelAllStopReason = "done" | "max_rounds" | "time_budget";
3231
+ /**
3232
+ * Merged result of a `cancelAll({ ..., untilDone: true })` loop. Every order is in exactly one
3233
+ * of `cancelled`, `already_closed` and `failed`, in its latest state; `has_more` is the last
3234
+ * round's.
3235
+ */
3236
+ interface CancelAllUntilDoneResult extends CancelAllResult {
3237
+ /** Number of cancel-all calls made. */
3238
+ rounds: number;
3239
+ /** `done`: nothing left to retry; otherwise the loop's limit that ended it. */
3240
+ stopped: CancelAllStopReason;
3241
+ }
3184
3242
  type Candle = S["CandleResponse"];
3185
3243
  type CandleInterval = S["CandleInterval"];
3186
3244
  type DepositAddress = S["DepositAddressResponse"];
@@ -3344,6 +3402,14 @@ interface PlaceOrderResult extends PlaceOrderResponse {
3344
3402
  */
3345
3403
  interface CancelAllParams {
3346
3404
  symbol: string | null;
3405
+ /**
3406
+ * Repeat the call until nothing is left to retry (see `cancelAll`). Default false: one call.
3407
+ */
3408
+ untilDone?: boolean;
3409
+ /** With `untilDone`: at most this many calls. Default 20. */
3410
+ maxRounds?: number;
3411
+ /** With `untilDone`: stop before a wait would take the loop to this many ms. Default 120000. */
3412
+ timeBudgetMs?: number;
3347
3413
  }
3348
3414
  declare class TradingResource extends Resource {
3349
3415
  #private;
@@ -3380,12 +3446,27 @@ declare class TradingResource extends Resource {
3380
3446
  * Cancels every open order in one market: `cancelAll({ symbol: "BTC/USDT" })`.
3381
3447
  * To cancel across ALL markets, pass `symbol: null` explicitly: `cancelAll({ symbol: null })`.
3382
3448
  * Omitting `symbol` is an error, so an account-wide cancel never happens by accident
3383
- * (the server itself treats `{}` as every market).
3449
+ * (the server itself treats `{}` as every market). An unknown symbol throws `NotFoundError`.
3450
+ *
3451
+ * One call handles at most 500 orders. Every order it handled is in exactly one list:
3452
+ * `cancelled`; `already_closed` (it filled, was refused or was cancelled elsewhere first:
3453
+ * not an error); or `failed`, with the reason in `failures` (`INVALID_STATE` for an order
3454
+ * still being placed when the server's 500 ms wait ran out). `has_more` says more orders
3455
+ * remain: call again.
3456
+ *
3457
+ * With `untilDone: true` the SDK does that for you. It repeats while `has_more` is true or a
3458
+ * failure is `INVALID_STATE` / `SERVICE_UNAVAILABLE`. After a call that made no progress it
3459
+ * waits 1, 2, 4, 8 and then 15 s, starting over after any progress. It stops after
3460
+ * `maxRounds` calls or before a wait would pass `timeBudgetMs`, and returns the merged
3461
+ * result with `rounds` and `stopped`. Other failure codes are returned, never retried.
3384
3462
  *
3385
- * The server limits cancel-all to 30 calls per minute per account. It is naturally
3386
- * repeatable, so it is retried after network errors; a retry reports only what that retry
3387
- * cancelled.
3463
+ * The server allows 30 cancel-all calls per minute per account (a 429 is retried after its
3464
+ * Retry-After). The call is naturally repeatable and needs no Idempotency-Key, so it is
3465
+ * retried after network errors; a retry reports only what that retry did.
3388
3466
  */
3467
+ cancelAll(params: CancelAllParams & {
3468
+ untilDone: true;
3469
+ }, opts?: RequestOptions): Promise<CancelAllUntilDoneResult>;
3389
3470
  cancelAll(params: CancelAllParams, opts?: RequestOptions): Promise<CancelAllResult>;
3390
3471
  }
3391
3472
 
@@ -3484,7 +3565,16 @@ declare class RateLimitError extends CexyApiError {
3484
3565
  /** 5xx */
3485
3566
  declare class ServerError extends CexyApiError {
3486
3567
  }
3487
- /** True for codes listed in errors.yaml. */
3568
+ /**
3569
+ * Codes the SDK itself sets on a `CexyApiError`; the API never sends them, so they are not in
3570
+ * errors.yaml and `isKnownErrorCode()` returns false for them.
3571
+ * - `UNEXPECTED_REDIRECT`: the server answered with a 3xx. The SDK never follows redirects (the
3572
+ * credentials would go to the redirect target); not retryable.
3573
+ */
3574
+ declare const CLIENT_ERROR_CODES: {
3575
+ readonly UNEXPECTED_REDIRECT: "UNEXPECTED_REDIRECT";
3576
+ };
3577
+ /** True for codes listed in errors.yaml (server codes; see `CLIENT_ERROR_CODES` for the SDK's own). */
3488
3578
  declare function isKnownErrorCode(code: string): code is KnownErrorCode;
3489
3579
  /**
3490
3580
  * Builds the right error subclass for an HTTP error response.
@@ -3844,7 +3934,12 @@ interface CexyClientOptions {
3844
3934
  timeoutMs?: number;
3845
3935
  /** Retries after the first attempt for retryable failures. Default 3. */
3846
3936
  maxRetries?: number;
3847
- /** A `fetch` implementation. Default: the global `fetch` (Node 20+, browsers). */
3937
+ /**
3938
+ * A `fetch` implementation. Default: the global `fetch` (Node 22+, browsers).
3939
+ * It must honour `redirect: "manual"` (the SDK never follows redirects). A fetch that follows
3940
+ * them anyway has already sent the credentials to the redirect target by the time the SDK sees
3941
+ * `response.redirected` and throws `UNEXPECTED_REDIRECT`.
3942
+ */
3848
3943
  fetch?: FetchLike;
3849
3944
  /**
3850
3945
  * Client-side rate limit in requests per minute, or `false` to disable. Default 100/min
@@ -3866,6 +3961,8 @@ interface CexyClientOptions {
3866
3961
  sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
3867
3962
  /** @internal Deterministic jitter in tests. */
3868
3963
  random?: () => number;
3964
+ /** @internal Replace the clock in tests (milliseconds). */
3965
+ now?: () => number;
3869
3966
  }
3870
3967
  /**
3871
3968
  * CEXY.io REST client.
@@ -3925,11 +4022,11 @@ declare function isAmount(value: unknown): value is Amount;
3925
4022
  declare function assertAmountFields(body: Record<string, unknown>, fields: readonly string[], context: string): void;
3926
4023
 
3927
4024
  /** SDK version, kept in sync with package.json (a test enforces this). */
3928
- declare const VERSION = "0.1.0-dev.2";
4025
+ declare const VERSION = "0.1.0-dev.4";
3929
4026
  /** Default User-Agent product token. */
3930
- declare const USER_AGENT = "cexy-typescript/0.1.0-dev.2";
4027
+ declare const USER_AGENT = "cexy-typescript/0.1.0-dev.4";
3931
4028
 
3932
4029
  /** True for loopback hosts, the only ones where plain-text transport may be allowed. */
3933
4030
  declare function isLocalHost(hostname: string): boolean;
3934
4031
 
3935
- export { AccountResource, type Amount, type ApiKey, ApiKeyAuthenticator, type ApiScope, type Asset, type AssetNetwork, AssetsResource, type AuthRequest, type AuthResult, type AuthenticatedFrame, AuthenticationError, type Authenticator, type Balance, type BalanceUpdatedEvent, type BookLevel, type CancelAllParams, type CancelAllRequest, type CancelAllResult, type Candle, type CandleInterval, CexyApiError, type CexyApiErrorInit, CexyClient, type CexyClientOptions, CexyConfigError, CexyConnectionError, CexyError, CexyTimeoutError, CexyWebSocket, CexyWebSocketError, type CexyWebSocketEvents, type CexyWebSocketOptions, type CloseInfo, ConflictError, type CursorParams, DEFAULT_BASE_URL, DEFAULT_RPM_ANONYMOUS, DEFAULT_RPM_WITH_KEY, DEFAULT_WS_URL, type Deposit, type DepositAddress, type DepositEvent, type DepositStatus, type ErrorBody, type ErrorCode, type ErrorFrame, type EventMap, type ExchangeConfig, type ExitPoolRequest, type ExitPoolResult, ExportsResource, type FeeSchedule, FeesResource, type FetchLike, type Fill, ForbiddenError, InvalidAmountError, type IterateOptions, type JoinPoolRequest, type JoinPoolResult, JurisdictionBlockedError, KNOWN_EVENT_TYPES, type KnownErrorCode, type LedgerEntry, type LedgerEntryKind, type LiquidityRole, type Listener, LiveOrderBook, type LiveOrderBookEvents, type LiveOrderBookOptions, type MaintenanceState, type Market, type MarketStatus, type MarketStatusEvent, MarketsResource, type Network, NetworksResource, NotFoundError, type Notification, type NotificationKind, OPERATIONS, type OperationAuth, type OperationId, type OperationInfo, type OperationScope, type Order, type OrderBook, type OrderBookUpdateData, type OrderBookUpdateEvent, type OrderEvent, type OrderSide, OrderStateUnknownError, type OrderStatus, type OrderType, PRIVATE_CHANNELS, type Page, type PlaceOrderRequest, type PlaceOrderResponse, type PlaceOrderResult, type PongFrame, type Pool, type PoolStatus, PoolsResource, type PublicTrade, type QueryOf, RateLimitError, RateLimiter, type RateLimiterOptions, type RateLimiterState, type ReconnectOptions, type RequestOptions, type ResyncReason, type RetryInfo, SUPPORTED_PROTOCOL_VERSION, ServerError, type ServerTime, type SessionRevokedData, type SessionRevokedEvent, type SnapshotSource, type SortDirection, type SubAccount, type SubscribeResult, type SubscribedFrame, type TickerUpdateEvent, type TimeInForce, type TradeNewEvent, TradingResource, type TriggerDirection, TypedEmitter, USER_AGENT, UnprocessableError, type UnsubscribedFrame, VERSION, ValidationError, WS_BOOK_DEPTH, WalletResource, type WebSocketConstructor, type WebSocketLike, type WelcomeFrame, type Withdrawal, type WithdrawalAddress, type WithdrawalStatus, type WithdrawalUpdatedEvent, type WsEvent, type WsLogger, assertAmountFields, type components, errorFromResponse, isAmount, isKnownErrorCode, isLocalHost, isRetryable, type operations, paginate, type paths };
4032
+ export { AccountResource, type Amount, type ApiKey, ApiKeyAuthenticator, type ApiScope, type Asset, type AssetNetwork, AssetsResource, type AuthRequest, type AuthResult, type AuthenticatedFrame, AuthenticationError, type Authenticator, type Balance, type BalanceUpdatedEvent, type BookLevel, CLIENT_ERROR_CODES, type CancelAllParams, type CancelAllRequest, type CancelAllResult, type CancelAllStopReason, type CancelAllUntilDoneResult, type CancelFailure, type Candle, type CandleInterval, CexyApiError, type CexyApiErrorInit, CexyClient, type CexyClientOptions, CexyConfigError, CexyConnectionError, CexyError, CexyTimeoutError, CexyWebSocket, CexyWebSocketError, type CexyWebSocketEvents, type CexyWebSocketOptions, type CloseInfo, ConflictError, type CursorParams, DEFAULT_BASE_URL, DEFAULT_RPM_ANONYMOUS, DEFAULT_RPM_WITH_KEY, DEFAULT_WS_URL, type Deposit, type DepositAddress, type DepositEvent, type DepositStatus, type ErrorBody, type ErrorCode, type ErrorFrame, type EventMap, type ExchangeConfig, type ExitPoolRequest, type ExitPoolResult, ExportsResource, type FeeSchedule, FeesResource, type FetchLike, type Fill, ForbiddenError, InvalidAmountError, type IterateOptions, type JoinPoolRequest, type JoinPoolResult, JurisdictionBlockedError, KNOWN_EVENT_TYPES, type KnownErrorCode, type LedgerEntry, type LedgerEntryKind, type LiquidityRole, type Listener, LiveOrderBook, type LiveOrderBookEvents, type LiveOrderBookOptions, type MaintenanceState, type Market, type MarketStatus, type MarketStatusEvent, MarketsResource, type Network, NetworksResource, NotFoundError, type Notification, type NotificationKind, OPERATIONS, type OperationAuth, type OperationId, type OperationInfo, type OperationScope, type Order, type OrderBook, type OrderBookUpdateData, type OrderBookUpdateEvent, type OrderEvent, type OrderSide, OrderStateUnknownError, type OrderStatus, type OrderType, PRIVATE_CHANNELS, type Page, type PlaceOrderRequest, type PlaceOrderResponse, type PlaceOrderResult, type PongFrame, type Pool, type PoolStatus, PoolsResource, type PublicTrade, type QueryOf, RateLimitError, RateLimiter, type RateLimiterOptions, type RateLimiterState, type ReconnectOptions, type RequestOptions, type ResyncReason, type RetryInfo, SUPPORTED_PROTOCOL_VERSION, ServerError, type ServerTime, type SessionRevokedData, type SessionRevokedEvent, type SnapshotSource, type SortDirection, type SubAccount, type SubscribeResult, type SubscribedFrame, type TickerUpdateEvent, type TimeInForce, type TradeNewEvent, TradingResource, type TriggerDirection, TypedEmitter, USER_AGENT, UnprocessableError, type UnsubscribedFrame, VERSION, ValidationError, WS_BOOK_DEPTH, WalletResource, type WebSocketConstructor, type WebSocketLike, type WelcomeFrame, type Withdrawal, type WithdrawalAddress, type WithdrawalStatus, type WithdrawalUpdatedEvent, type WsEvent, type WsLogger, assertAmountFields, type components, errorFromResponse, isAmount, isKnownErrorCode, isLocalHost, isRetryable, type operations, paginate, type paths };
package/dist/index.js CHANGED
@@ -129,6 +129,7 @@ var KNOWN_CODES = /* @__PURE__ */ new Set([
129
129
  "UNDER_MAINTENANCE",
130
130
  "ENGINE_OVERLOADED"
131
131
  ]);
132
+ var CLIENT_ERROR_CODES = { UNEXPECTED_REDIRECT: "UNEXPECTED_REDIRECT" };
132
133
  function isKnownErrorCode(code) {
133
134
  return KNOWN_CODES.has(code);
134
135
  }
@@ -392,7 +393,13 @@ var Transport = class {
392
393
  let res;
393
394
  let text;
394
395
  try {
395
- res = await this.config.fetch(url.toString(), { method: info.method, headers, body, signal: ctrl.signal });
396
+ res = await this.config.fetch(url.toString(), {
397
+ method: info.method,
398
+ headers,
399
+ body,
400
+ signal: ctrl.signal,
401
+ redirect: "manual"
402
+ });
396
403
  text = await res.text();
397
404
  } catch (err) {
398
405
  if (opts.signal?.aborted) throw opts.signal.reason ?? err;
@@ -404,6 +411,14 @@ var Transport = class {
404
411
  clearTimeout(timer);
405
412
  opts.signal?.removeEventListener("abort", onAbort);
406
413
  }
414
+ if (res.type === "opaqueredirect" || res.status >= 300 && res.status < 400 || res.redirected) {
415
+ throw new CexyApiError({
416
+ status: res.status,
417
+ code: CLIENT_ERROR_CODES.UNEXPECTED_REDIRECT,
418
+ message: `${info.method} ${info.path}: the server answered with a redirect (HTTP ${res.status}); the SDK does not follow redirects. Check baseUrl.`,
419
+ retryable: false
420
+ });
421
+ }
407
422
  this.config.limiter?.update(res.headers);
408
423
  if (!res.ok) {
409
424
  const parsed = safeJson(text);
@@ -774,6 +789,8 @@ var WalletResource = class extends Resource {
774
789
  return this.data({ op: "deposit_address", query: params }, opts);
775
790
  }
776
791
  };
792
+ var CANCEL_RETRY_CODES = /* @__PURE__ */ new Set(["INVALID_STATE", "SERVICE_UNAVAILABLE"]);
793
+ var CANCEL_BACKOFF_S = [1, 2, 4, 8, 15];
777
794
  var ORDER_AMOUNT_FIELDS = ["price", "quantity", "quote_quantity", "stop_price"];
778
795
  var TradingResource = class extends Resource {
779
796
  /** Open orders, optionally filtered by market/status. */
@@ -880,16 +897,6 @@ var TradingResource = class extends Resource {
880
897
  }
881
898
  }
882
899
  }
883
- /**
884
- * Cancels every open order in one market: `cancelAll({ symbol: "BTC/USDT" })`.
885
- * To cancel across ALL markets, pass `symbol: null` explicitly: `cancelAll({ symbol: null })`.
886
- * Omitting `symbol` is an error, so an account-wide cancel never happens by accident
887
- * (the server itself treats `{}` as every market).
888
- *
889
- * The server limits cancel-all to 30 calls per minute per account. It is naturally
890
- * repeatable, so it is retried after network errors; a retry reports only what that retry
891
- * cancelled.
892
- */
893
900
  async cancelAll(params, opts) {
894
901
  const hasSymbol = !!params && typeof params === "object" && Object.prototype.hasOwnProperty.call(params, "symbol");
895
902
  const symbol = hasSymbol ? params.symbol : void 0;
@@ -897,12 +904,62 @@ var TradingResource = class extends Resource {
897
904
  if (typeof symbol === "string" && symbol !== "") body = { symbol };
898
905
  else if (hasSymbol && symbol === null) body = {};
899
906
  else throw new CexyConfigError('cancelAll(): pass { symbol: "BASE/QUOTE" }, or { symbol: null } to cancel in every market');
900
- return this.data({ op: "cancel_all", body }, opts);
907
+ const once = () => this.data({ op: "cancel_all", body }, opts);
908
+ if (params.untilDone !== true) return once();
909
+ const maxRounds = params.maxRounds ?? 20;
910
+ const budgetMs = params.timeBudgetMs ?? 12e4;
911
+ if (!(maxRounds >= 1)) throw new CexyConfigError("cancelAll(): maxRounds must be >= 1");
912
+ if (!(budgetMs > 0)) throw new CexyConfigError("cancelAll(): timeBudgetMs must be > 0");
913
+ const now = this.t.config.now ?? Date.now;
914
+ const start = now();
915
+ const state = /* @__PURE__ */ new Map();
916
+ let rounds = 0;
917
+ let idle = 0;
918
+ let last;
919
+ let stopped;
920
+ for (; ; ) {
921
+ last = await once();
922
+ rounds++;
923
+ const failures = last.failures ?? [];
924
+ for (const id of last.cancelled ?? []) state.set(id, { list: "cancelled" });
925
+ for (const id of last.already_closed ?? []) state.set(id, { list: "already_closed" });
926
+ for (const id of last.failed ?? []) {
927
+ const failure = failures.find((f) => f.order_id === id);
928
+ state.set(id, failure ? { list: "failed", failure } : { list: "failed" });
929
+ }
930
+ const progress = (last.cancelled?.length ?? 0) + (last.already_closed?.length ?? 0) > 0;
931
+ if (!last.has_more && !failures.some((f) => CANCEL_RETRY_CODES.has(f.code))) {
932
+ stopped = "done";
933
+ break;
934
+ }
935
+ if (rounds >= maxRounds) {
936
+ stopped = "max_rounds";
937
+ break;
938
+ }
939
+ let waitMs = 0;
940
+ if (progress) idle = 0;
941
+ else waitMs = (CANCEL_BACKOFF_S[Math.min(idle++, CANCEL_BACKOFF_S.length - 1)] ?? 15) * 1e3;
942
+ if (now() - start + waitMs >= budgetMs) {
943
+ stopped = "time_budget";
944
+ break;
945
+ }
946
+ if (waitMs > 0) await this.t.config.sleep(waitMs, opts?.signal);
947
+ }
948
+ const pick = (list) => [...state].filter(([, v]) => v.list === list).map(([id]) => id);
949
+ return {
950
+ cancelled: pick("cancelled"),
951
+ already_closed: pick("already_closed"),
952
+ failed: pick("failed"),
953
+ failures: [...state.values()].flatMap((v) => v.list === "failed" && v.failure ? [v.failure] : []),
954
+ has_more: last.has_more,
955
+ rounds,
956
+ stopped
957
+ };
901
958
  }
902
959
  };
903
960
 
904
961
  // src/version.ts
905
- var VERSION = "0.1.0-dev.2";
962
+ var VERSION = "0.1.0-dev.4";
906
963
  var USER_AGENT = `cexy-typescript/${VERSION}`;
907
964
 
908
965
  // src/ws/emitter.ts
@@ -1662,7 +1719,7 @@ var CexyClient = class {
1662
1719
  assertSecureUrl(parsed, "https:", "http:", options.allowInsecure === true, "baseUrl");
1663
1720
  this.#allowInsecure = options.allowInsecure === true;
1664
1721
  const fetchImpl = options.fetch ?? globalThis.fetch;
1665
- if (!fetchImpl) throw new CexyConfigError("no global fetch found (Node 20+ required); pass options.fetch");
1722
+ if (!fetchImpl) throw new CexyConfigError("no global fetch found (Node 22+ required); pass options.fetch");
1666
1723
  const timeoutMs = options.timeoutMs ?? 1e4;
1667
1724
  const maxRetries = options.maxRetries ?? 3;
1668
1725
  if (!(timeoutMs > 0)) throw new CexyConfigError("timeoutMs must be > 0");
@@ -1683,6 +1740,7 @@ var CexyClient = class {
1683
1740
  userAgent: canSetUserAgent() ? [USER_AGENT, options.userAgentSuffix].filter(Boolean).join(" ") : null,
1684
1741
  sleep: sleep2,
1685
1742
  random: options.random ?? Math.random,
1743
+ now: options.now ?? Date.now,
1686
1744
  onRetry: options.onRetry
1687
1745
  });
1688
1746
  const t = this.#transport;
@@ -1752,4 +1810,4 @@ function canSetUserAgent() {
1752
1810
  return !(typeof g.window !== "undefined" && typeof g.window.document !== "undefined");
1753
1811
  }
1754
1812
 
1755
- export { AccountResource, ApiKeyAuthenticator, AssetsResource, AuthenticationError, CexyApiError, CexyClient, CexyConfigError, CexyConnectionError, CexyError, CexyTimeoutError, CexyWebSocket, CexyWebSocketError, ConflictError, DEFAULT_BASE_URL, DEFAULT_RPM_ANONYMOUS, DEFAULT_RPM_WITH_KEY, DEFAULT_WS_URL, ExportsResource, FeesResource, ForbiddenError, InvalidAmountError, JurisdictionBlockedError, KNOWN_EVENT_TYPES, LiveOrderBook, MarketsResource, NetworksResource, NotFoundError, OPERATIONS, OrderStateUnknownError, PRIVATE_CHANNELS, PoolsResource, RateLimitError, RateLimiter, SUPPORTED_PROTOCOL_VERSION, ServerError, TradingResource, TypedEmitter, USER_AGENT, UnprocessableError, VERSION, ValidationError, WS_BOOK_DEPTH, WalletResource, assertAmountFields, errorFromResponse, isAmount, isKnownErrorCode, isLocalHost, isRetryable, paginate };
1813
+ export { AccountResource, ApiKeyAuthenticator, AssetsResource, AuthenticationError, CLIENT_ERROR_CODES, CexyApiError, CexyClient, CexyConfigError, CexyConnectionError, CexyError, CexyTimeoutError, CexyWebSocket, CexyWebSocketError, ConflictError, DEFAULT_BASE_URL, DEFAULT_RPM_ANONYMOUS, DEFAULT_RPM_WITH_KEY, DEFAULT_WS_URL, ExportsResource, FeesResource, ForbiddenError, InvalidAmountError, JurisdictionBlockedError, KNOWN_EVENT_TYPES, LiveOrderBook, MarketsResource, NetworksResource, NotFoundError, OPERATIONS, OrderStateUnknownError, PRIVATE_CHANNELS, PoolsResource, RateLimitError, RateLimiter, SUPPORTED_PROTOCOL_VERSION, ServerError, TradingResource, TypedEmitter, USER_AGENT, UnprocessableError, VERSION, ValidationError, WS_BOOK_DEPTH, WalletResource, assertAmountFields, errorFromResponse, isAmount, isKnownErrorCode, isLocalHost, isRetryable, paginate };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cexyio/cexy",
3
- "version": "0.1.0-dev.2",
3
+ "version": "0.1.0-dev.4",
4
4
  "description": "Official TypeScript/JavaScript SDK for the CEXY.io REST and WebSocket API",
5
5
  "license": "MIT",
6
6
  "author": "CEXY.io",
@@ -43,7 +43,7 @@
43
43
  ],
44
44
  "sideEffects": false,
45
45
  "engines": {
46
- "node": ">=20"
46
+ "node": ">=22"
47
47
  },
48
48
  "scripts": {
49
49
  "generate": "node scripts/generate.mjs",
@@ -65,16 +65,16 @@
65
65
  }
66
66
  },
67
67
  "devDependencies": {
68
- "@eslint/js": "^9.39.0",
69
- "@types/node": "^20.19.0",
68
+ "@eslint/js": "^10.0.1",
69
+ "@types/node": "^22.20.4",
70
70
  "@types/ws": "^8.18.0",
71
- "eslint": "^9.39.0",
71
+ "eslint": "^10.11.0",
72
72
  "openapi-typescript": "^7.13.0",
73
73
  "tsup": "^8.5.0",
74
74
  "typescript": "~5.9.3",
75
75
  "typescript-eslint": "^8.70.0",
76
- "vite": "^6.4.0",
77
- "vitest": "^3.2.7",
76
+ "vite": "^8.3.0",
77
+ "vitest": "^5.0.2",
78
78
  "ws": "^8.18.0"
79
79
  },
80
80
  "publishConfig": {