@cexyio/cexy 0.1.0-dev.3 → 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
@@ -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
@@ -231,7 +247,8 @@ endpoints from a server. Browsers do not let scripts set `User-Agent`, so the SD
231
247
  `http://` / `ws://` solely for `localhost`, `127.0.0.1` or `::1` (local test servers).
232
248
  - The SDK **never follows HTTP redirects**. A 3xx answer throws a `CexyApiError` with code
233
249
  `UNEXPECTED_REDIRECT` (not retried), so credentials are never re-sent to another host and an order
234
- is never re-posted to a redirect target. If you pass your own `fetch`, it must honour `redirect: "manual"`.
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.
235
252
  - The SDK redacts the secret from `toString()`, `util.inspect`, `JSON.stringify` and error messages.
236
253
  - Keep keys in environment variables or a secret manager, not in code.
237
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
  }
@@ -415,7 +416,7 @@ var Transport = class {
415
416
  if (res.type === "opaqueredirect" || res.status >= 300 && res.status < 400 || res.redirected) {
416
417
  throw new CexyApiError({
417
418
  status: res.status,
418
- code: "UNEXPECTED_REDIRECT",
419
+ code: CLIENT_ERROR_CODES.UNEXPECTED_REDIRECT,
419
420
  message: `${info.method} ${info.path}: the server answered with a redirect (HTTP ${res.status}); the SDK does not follow redirects. Check baseUrl.`,
420
421
  retryable: false
421
422
  });
@@ -790,6 +791,8 @@ var WalletResource = class extends Resource {
790
791
  return this.data({ op: "deposit_address", query: params }, opts);
791
792
  }
792
793
  };
794
+ var CANCEL_RETRY_CODES = /* @__PURE__ */ new Set(["INVALID_STATE", "SERVICE_UNAVAILABLE"]);
795
+ var CANCEL_BACKOFF_S = [1, 2, 4, 8, 15];
793
796
  var ORDER_AMOUNT_FIELDS = ["price", "quantity", "quote_quantity", "stop_price"];
794
797
  var TradingResource = class extends Resource {
795
798
  /** Open orders, optionally filtered by market/status. */
@@ -896,16 +899,6 @@ var TradingResource = class extends Resource {
896
899
  }
897
900
  }
898
901
  }
899
- /**
900
- * Cancels every open order in one market: `cancelAll({ symbol: "BTC/USDT" })`.
901
- * To cancel across ALL markets, pass `symbol: null` explicitly: `cancelAll({ symbol: null })`.
902
- * Omitting `symbol` is an error, so an account-wide cancel never happens by accident
903
- * (the server itself treats `{}` as every market).
904
- *
905
- * The server limits cancel-all to 30 calls per minute per account. It is naturally
906
- * repeatable, so it is retried after network errors; a retry reports only what that retry
907
- * cancelled.
908
- */
909
902
  async cancelAll(params, opts) {
910
903
  const hasSymbol = !!params && typeof params === "object" && Object.prototype.hasOwnProperty.call(params, "symbol");
911
904
  const symbol = hasSymbol ? params.symbol : void 0;
@@ -913,12 +906,62 @@ var TradingResource = class extends Resource {
913
906
  if (typeof symbol === "string" && symbol !== "") body = { symbol };
914
907
  else if (hasSymbol && symbol === null) body = {};
915
908
  else throw new CexyConfigError('cancelAll(): pass { symbol: "BASE/QUOTE" }, or { symbol: null } to cancel in every market');
916
- 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
+ };
917
960
  }
918
961
  };
919
962
 
920
963
  // src/version.ts
921
- var VERSION = "0.1.0-dev.3";
964
+ var VERSION = "0.1.0-dev.4";
922
965
  var USER_AGENT = `cexy-typescript/${VERSION}`;
923
966
 
924
967
  // src/ws/emitter.ts
@@ -1699,6 +1742,7 @@ var CexyClient = class {
1699
1742
  userAgent: canSetUserAgent() ? [USER_AGENT, options.userAgentSuffix].filter(Boolean).join(" ") : null,
1700
1743
  sleep: sleep2,
1701
1744
  random: options.random ?? Math.random,
1745
+ now: options.now ?? Date.now,
1702
1746
  onRetry: options.onRetry
1703
1747
  });
1704
1748
  const t = this.#transport;
@@ -1772,6 +1816,7 @@ exports.AccountResource = AccountResource;
1772
1816
  exports.ApiKeyAuthenticator = ApiKeyAuthenticator;
1773
1817
  exports.AssetsResource = AssetsResource;
1774
1818
  exports.AuthenticationError = AuthenticationError;
1819
+ exports.CLIENT_ERROR_CODES = CLIENT_ERROR_CODES;
1775
1820
  exports.CexyApiError = CexyApiError;
1776
1821
  exports.CexyClient = CexyClient;
1777
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 22+, 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.3";
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.3";
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 22+, 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.3";
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.3";
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
  }
@@ -413,7 +414,7 @@ var Transport = class {
413
414
  if (res.type === "opaqueredirect" || res.status >= 300 && res.status < 400 || res.redirected) {
414
415
  throw new CexyApiError({
415
416
  status: res.status,
416
- code: "UNEXPECTED_REDIRECT",
417
+ code: CLIENT_ERROR_CODES.UNEXPECTED_REDIRECT,
417
418
  message: `${info.method} ${info.path}: the server answered with a redirect (HTTP ${res.status}); the SDK does not follow redirects. Check baseUrl.`,
418
419
  retryable: false
419
420
  });
@@ -788,6 +789,8 @@ var WalletResource = class extends Resource {
788
789
  return this.data({ op: "deposit_address", query: params }, opts);
789
790
  }
790
791
  };
792
+ var CANCEL_RETRY_CODES = /* @__PURE__ */ new Set(["INVALID_STATE", "SERVICE_UNAVAILABLE"]);
793
+ var CANCEL_BACKOFF_S = [1, 2, 4, 8, 15];
791
794
  var ORDER_AMOUNT_FIELDS = ["price", "quantity", "quote_quantity", "stop_price"];
792
795
  var TradingResource = class extends Resource {
793
796
  /** Open orders, optionally filtered by market/status. */
@@ -894,16 +897,6 @@ var TradingResource = class extends Resource {
894
897
  }
895
898
  }
896
899
  }
897
- /**
898
- * Cancels every open order in one market: `cancelAll({ symbol: "BTC/USDT" })`.
899
- * To cancel across ALL markets, pass `symbol: null` explicitly: `cancelAll({ symbol: null })`.
900
- * Omitting `symbol` is an error, so an account-wide cancel never happens by accident
901
- * (the server itself treats `{}` as every market).
902
- *
903
- * The server limits cancel-all to 30 calls per minute per account. It is naturally
904
- * repeatable, so it is retried after network errors; a retry reports only what that retry
905
- * cancelled.
906
- */
907
900
  async cancelAll(params, opts) {
908
901
  const hasSymbol = !!params && typeof params === "object" && Object.prototype.hasOwnProperty.call(params, "symbol");
909
902
  const symbol = hasSymbol ? params.symbol : void 0;
@@ -911,12 +904,62 @@ var TradingResource = class extends Resource {
911
904
  if (typeof symbol === "string" && symbol !== "") body = { symbol };
912
905
  else if (hasSymbol && symbol === null) body = {};
913
906
  else throw new CexyConfigError('cancelAll(): pass { symbol: "BASE/QUOTE" }, or { symbol: null } to cancel in every market');
914
- 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
+ };
915
958
  }
916
959
  };
917
960
 
918
961
  // src/version.ts
919
- var VERSION = "0.1.0-dev.3";
962
+ var VERSION = "0.1.0-dev.4";
920
963
  var USER_AGENT = `cexy-typescript/${VERSION}`;
921
964
 
922
965
  // src/ws/emitter.ts
@@ -1697,6 +1740,7 @@ var CexyClient = class {
1697
1740
  userAgent: canSetUserAgent() ? [USER_AGENT, options.userAgentSuffix].filter(Boolean).join(" ") : null,
1698
1741
  sleep: sleep2,
1699
1742
  random: options.random ?? Math.random,
1743
+ now: options.now ?? Date.now,
1700
1744
  onRetry: options.onRetry
1701
1745
  });
1702
1746
  const t = this.#transport;
@@ -1766,4 +1810,4 @@ function canSetUserAgent() {
1766
1810
  return !(typeof g.window !== "undefined" && typeof g.window.document !== "undefined");
1767
1811
  }
1768
1812
 
1769
- 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.3",
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",