@cexyio/cexy 0.1.0-dev.5 → 0.1.0-dev.7

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
@@ -46,6 +46,8 @@ const cexy = new CexyClient({
46
46
  });
47
47
 
48
48
  const balances = await cexy.account.balances();
49
+ // A sub-account's balances (parent account only; same shape, incl. held_incoming):
50
+ const subBalances = await cexy.account.subAccountBalances("sub-account-id");
49
51
  const open = await cexy.trading.openOrders({ symbol: "BTC/USDT" });
50
52
 
51
53
  const placed = await cexy.trading.placeOrder({
@@ -62,6 +64,9 @@ await cexy.trading.cancelAll({ symbol: "BTC/USDT" }); // { symbol: null } = ever
62
64
  `cancelAll` requires `symbol`: the server treats a missing symbol as "every market", so the SDK makes
63
65
  you say so with `{ symbol: null }`. An unknown symbol throws `NotFoundError`.
64
66
 
67
+ It also cancels stop orders that have not triggered yet (status `pending_trigger`) and releases
68
+ their reservations, so nothing fires into the market after the call.
69
+
65
70
  One call handles at most 500 orders and puts each in exactly one list: `cancelled`, `already_closed`
66
71
  (it filled, was refused or was cancelled elsewhere first; not an error) or `failed`, with the reason in
67
72
  `failures` (`INVALID_STATE` for an order still being placed). `has_more: true` means more orders remain.
@@ -79,6 +84,8 @@ After a call without progress it waits 1, 2, 4, 8, then 15 s, and it stops after
79
84
  request (the loop owns the retries, so it never sends more than `maxRounds` requests): a 429 round waits its
80
85
  Retry-After, which counts against the budget; another retryable error (5xx, network) takes the next backoff
81
86
  step; a wait that would pass the budget ends the loop with `stopped: "time_budget"` and `last_error_code`.
87
+ A wait imposed by the client rate limiter (e.g. `X-RateLimit-Remaining: 0` with a Reset) counts too: if it
88
+ would pass the budget the loop stops without calling, with `last_error_code: "RATE_LIMITED"`.
82
89
  A non-retryable error (e.g. a key without the trade scope) throws `CancelAllInterruptedError` with the error
83
90
  and the partial result. The server allows 30 cancel-all calls per minute per account.
84
91
 
@@ -89,7 +96,7 @@ Give both `apiKey` and `apiSecret`, or neither: passing only one throws at const
89
96
  | `markets` | `list`, `get`, `orderbook`, `trades`, `iterateTrades`, `candles` | public |
90
97
  | `assets`, `networks`, `fees`, `pools` | `list`, `get` / `list` / `get` / `list`, `get` | public |
91
98
  | `time()`, `config()` | | public |
92
- | `account` | `balances`, `balance`, `ledger`, `notifications`, `subAccounts`, `apiKeys` (+ iterators) | read |
99
+ | `account` | `balances`, `balance`, `ledger`, `notifications`, `subAccounts`, `subAccountBalances`, `apiKeys` (+ iterators) | read |
93
100
  | `exports` | `deposits`, `ledger`, `orders`, `trades`, `withdrawals` (CSV text) | read |
94
101
  | `wallet` | `deposits`, `deposit`, `withdrawals`, `withdrawal`, `withdrawalAddresses`, `depositAddress` (+ iterators) | read |
95
102
  | `trading` | `openOrders`, `order`, `orderByClientId`, `orderHistory`, `trades` (+ iterators) | read |
@@ -101,6 +108,13 @@ asset/network (later calls return the same one). Always use the `memo` too when
101
108
 
102
109
  There are no withdrawal or transfer methods: API keys cannot withdraw or transfer funds.
103
110
 
111
+ **Held incoming transfers.** Each balance has `held_incoming`: incoming internal transfers still
112
+ held, as `{ transfer_id, amount, available_at }`. Their sum is **already included in `locked`**, so
113
+ never add them to `locked` or `total` again. There are at most 100 entries, soonest `available_at`
114
+ first (millisecond precision), with no sender identity. An entry disappears once the transfer is
115
+ released (the amount moves to `available`) or cancelled by the exchange. It is always an array
116
+ (`[]` when none, including from servers that predate the field).
117
+
104
118
  ## Amounts
105
119
 
106
120
  Every amount is an exact decimal **string** (`"0.00150000"`), in responses and requests. JS numbers are
@@ -148,7 +162,8 @@ try {
148
162
  ## Retries and idempotency
149
163
 
150
164
  - Timeout per attempt: `timeoutMs` (default 10 s). Retries: `maxRetries` (default 3), exponential backoff with full jitter.
151
- - Retried: network errors, timeouts and responses with `retryable: true`.
165
+ - Retried: network errors, timeouts and responses with `retryable: true` (and 409 `CONCURRENT_MODIFICATION`).
166
+ A 4xx is never retried except 429 and 409 `CONCURRENT_MODIFICATION`, whatever its body says.
152
167
  - 429 waits at least `Retry-After` / `details.retry_after_seconds`. Server wait hints are untrusted: unusable
153
168
  values are ignored, and a hint longer than 120 s (`MAX_SERVER_WAIT_MS`) is never waited: the call fails at
154
169
  once with `RateLimitError`, whose `retryAfterMs` still has the server's value. The client-side rate limiter
@@ -183,6 +198,22 @@ for await (const order of cexy.trading.iterateOrderHistory({ symbol: "BTC/USDT",
183
198
  // cap the total: cexy.account.iterateLedger({}, { maxItems: 500 })
184
199
  ```
185
200
 
201
+ Each ledger entry's `reference` says what caused it, as a union told apart by `type` (`deposit`,
202
+ `withdrawal`, `order`, `trade`, `transfer`, `adjustment`, `pool`, `futures_transfer`, `system`).
203
+ Newer types the SDK does not know yet arrive unchanged instead of failing; narrow with
204
+ `isLedgerReference`:
205
+
206
+ ```ts
207
+ import { isLedgerReference } from "@cexyio/cexy";
208
+
209
+ for await (const e of cexy.account.iterateLedger({}, { maxItems: 100 })) {
210
+ if (isLedgerReference(e.reference, "trade")) console.log(e.reference.trade_id);
211
+ else if (!isLedgerReference(e.reference)) console.log("new cause type:", e.reference.type);
212
+ }
213
+ ```
214
+
215
+ Ids (`OrderId`, `TradeId`, `UserId`, …) are plain strings; the SDK does not check their format.
216
+
186
217
  ## Rate limits
187
218
 
188
219
  The client has a token-bucket limiter: **100 requests/minute without a key** (the server allows 120/min
@@ -264,7 +295,7 @@ Report vulnerabilities as described in [SECURITY.md](SECURITY.md).
264
295
  ## For tool builders
265
296
 
266
297
  The package exports its building blocks: the `OPERATIONS` table (method, path, auth and scope for each of the
267
- 40 operations), all model types, the error classes, `paginate()`, `RateLimiter`, the `Authenticator`
298
+ 41 operations), all model types, the error classes, `paginate()`, `RateLimiter`, the `Authenticator`
268
299
  interface (HMAC signing will plug in here) and `userAgentSuffix` to identify your tool.
269
300
 
270
301
  ## Development
package/dist/index.cjs CHANGED
@@ -139,13 +139,14 @@ var KNOWN_CODES = /* @__PURE__ */ new Set([
139
139
  "INTERNAL",
140
140
  "SERVICE_UNAVAILABLE",
141
141
  "UNDER_MAINTENANCE",
142
- "ENGINE_OVERLOADED"
142
+ "ENGINE_OVERLOADED",
143
+ "PRICE_UNAVAILABLE"
143
144
  ]);
144
145
  var CLIENT_ERROR_CODES = { UNEXPECTED_REDIRECT: "UNEXPECTED_REDIRECT" };
145
146
  function isKnownErrorCode(code) {
146
147
  return KNOWN_CODES.has(code);
147
148
  }
148
- var DEFAULT_RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
149
+ var DEFAULT_RETRYABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
149
150
  var MAX_SERVER_WAIT_MS = 12e4;
150
151
  function retryAfterMs(headers, details) {
151
152
  let best = null;
@@ -295,6 +296,7 @@ var OPERATIONS = {
295
296
  get_ledger: op("GET", "/api/v1/account/ledger", "api_key", "read", "account.ledger"),
296
297
  list_notifications: op("GET", "/api/v1/account/notifications", "api_key", "read", "account.notifications"),
297
298
  list_sub_accounts: op("GET", "/api/v1/account/sub-accounts", "api_key", "read", "account.subAccounts"),
299
+ sub_account_balances: op("GET", "/api/v1/account/sub-accounts/{id}/balances", "api_key", "read", "account.subAccountBalances"),
298
300
  list_api_keys: op("GET", "/api/v1/account/api-keys", "api_key", "read", "account.apiKeys"),
299
301
  // Exports (read)
300
302
  export_deposits: op("GET", "/api/v1/exports/deposits", "api_key", "read", "exports.deposits"),
@@ -325,6 +327,7 @@ var OPERATIONS = {
325
327
 
326
328
  // src/http.ts
327
329
  var IDEMPOTENT_OPS = /* @__PURE__ */ new Set(["join_pool", "exit_pool"]);
330
+ var REPEAT_SAFE_MUTATIONS = /* @__PURE__ */ new Set(["join_pool", "exit_pool", "cancel_all"]);
328
331
  var BACKOFF_BASE_MS = 500;
329
332
  var BACKOFF_MAX_MS = 1e4;
330
333
  var Transport = class {
@@ -336,13 +339,14 @@ var Transport = class {
336
339
  * Sends a request with the standard retry policy: retryable errors and network failures are
337
340
  * retried. Pool join/exit carry an `Idempotency-Key` reused on every attempt (the server
338
341
  * honours it there, which makes their retries safe); the other mutations routed here
339
- * (cancel-all) are naturally repeatable and send no key. `placeOrder` and `cancelOrder` use
340
- * `attempt()` with their own policies.
342
+ * (cancel-all) are naturally repeatable and send no key. Any other mutation is sent once.
343
+ * `placeOrder` and `cancelOrder` use `attempt()` with their own policies.
341
344
  */
342
345
  async request(spec, opts = {}) {
343
346
  const info = OPERATIONS[spec.op];
344
347
  const idempotencyKey = IDEMPOTENT_OPS.has(spec.op) ? spec.idempotencyKey ?? opts.idempotencyKey ?? newId() : void 0;
345
- const maxRetries = opts.maxRetries ?? this.config.maxRetries;
348
+ const repeatSafe = info.method === "GET" || REPEAT_SAFE_MUTATIONS.has(spec.op);
349
+ const maxRetries = repeatSafe ? opts.maxRetries ?? this.config.maxRetries : 0;
346
350
  for (let attempt = 0; ; attempt++) {
347
351
  try {
348
352
  return await this.attempt({ ...spec, idempotencyKey }, opts);
@@ -457,6 +461,7 @@ var Transport = class {
457
461
  const path = info.path.replace(/\{(\w+)\}/g, (_m, name) => {
458
462
  const v = pathParams[name];
459
463
  if (typeof v !== "string" || v === "") throw new CexyConfigError(`${info.sdkMethod}(): ${name} is required`);
464
+ if (v === "." || v === "..") throw new CexyConfigError(`${info.sdkMethod}(): ${name} must not be "." or ".."`);
460
465
  return encodeURIComponent(v);
461
466
  });
462
467
  const url = new URL(this.config.baseUrl.replace(/\/+$/, "") + path);
@@ -479,7 +484,11 @@ function serverHintMs(err) {
479
484
  }
480
485
  function isRetryable(err) {
481
486
  if (err instanceof CexyConnectionError) return true;
482
- if (err instanceof CexyApiError) return err.retryable || err.code === "CONCURRENT_MODIFICATION";
487
+ if (err instanceof CexyApiError) {
488
+ const concurrent = err.code === "CONCURRENT_MODIFICATION";
489
+ if (err.status >= 400 && err.status < 500 && err.status !== 429 && !(err.status === 409 && concurrent)) return false;
490
+ return err.retryable || concurrent;
491
+ }
483
492
  return false;
484
493
  }
485
494
  function isAmbiguous(err) {
@@ -545,6 +554,17 @@ var RateLimiter = class {
545
554
  await this.#sleep(Math.min(MAX_BLOCK_MS, Math.ceil((1 - this.#tokens) * msPerToken)), signal);
546
555
  }
547
556
  }
557
+ /**
558
+ * How long `acquire()` would wait right now, in ms (0 when a request may go at once). Lets a
559
+ * caller with a time budget, such as the cancelAll untilDone loop, count the limiter's wait.
560
+ */
561
+ pendingWaitMs() {
562
+ this.#refill();
563
+ const block = this.#blockedUntil - this.#now();
564
+ if (block > 0) return block;
565
+ if (this.#tokens >= 1) return 0;
566
+ return Math.min(MAX_BLOCK_MS, Math.ceil((1 - this.#tokens) * (6e4 / this.#rpm)));
567
+ }
548
568
  /** Adapts to the server's rate-limit headers. Never raises the configured limit. */
549
569
  update(headers) {
550
570
  this.#refill();
@@ -740,12 +760,22 @@ var PoolsResource = class extends Resource {
740
760
  return this.data({ op: "exit_pool", pathParams: { symbol }, body }, opts);
741
761
  }
742
762
  };
763
+ function withHeldIncoming(b) {
764
+ if (!b || typeof b !== "object" || Array.isArray(b.held_incoming)) return b;
765
+ return { ...b, held_incoming: [] };
766
+ }
743
767
  var AccountResource = class extends Resource {
744
- balances(opts) {
745
- return this.data({ op: "list_balances" }, opts);
768
+ /**
769
+ * Balances per asset. `held_incoming` lists incoming internal transfers still held; their sum is
770
+ * already included in `locked` (never add it again). Always an array (`[]` when none).
771
+ */
772
+ async balances(opts) {
773
+ const rows = await this.data({ op: "list_balances" }, opts);
774
+ return Array.isArray(rows) ? rows.map(withHeldIncoming) : rows;
746
775
  }
747
- balance(asset, opts) {
748
- return this.data({ op: "get_balance", pathParams: { asset } }, opts);
776
+ /** One asset's balance. `held_incoming`: incoming transfers still held, already inside `locked`. */
777
+ async balance(asset, opts) {
778
+ return withHeldIncoming(await this.data({ op: "get_balance", pathParams: { asset } }, opts));
749
779
  }
750
780
  ledger(params, opts) {
751
781
  return this.page({ op: "get_ledger", query: params }, opts);
@@ -762,6 +792,20 @@ var AccountResource = class extends Resource {
762
792
  subAccounts(opts) {
763
793
  return this.data({ op: "list_sub_accounts" }, opts);
764
794
  }
795
+ /**
796
+ * A sub-account's balances, read by its PARENT account: the same shape as `balances()`
797
+ * (zero balances omitted), including `held_incoming`, whose sum is already inside `locked`.
798
+ * The server currently returns them ordered by asset symbol; don't rely on the order.
799
+ * An id that is not one of the caller's sub-accounts (or a call with the sub-account's own
800
+ * key) gets `NotFoundError`; a sub-account's own key reads its balances with `balances()`.
801
+ * A malformed id gets 400 (`ValidationError`); a key without the `read` scope gets 403
802
+ * `FORBIDDEN` (`ForbiddenError`). `id` must be non-empty and not "." or ".."; it is sent as
803
+ * one URL path segment.
804
+ */
805
+ async subAccountBalances(id, opts) {
806
+ const rows = await this.data({ op: "sub_account_balances", pathParams: { id } }, opts);
807
+ return Array.isArray(rows) ? rows.map(withHeldIncoming) : rows;
808
+ }
765
809
  /** Your API keys (metadata only; secrets are never returned). */
766
810
  apiKeys(opts) {
767
811
  return this.data({ op: "list_api_keys" }, opts);
@@ -1015,8 +1059,10 @@ var TradingResource = class extends Resource {
1015
1059
  if (progress) idle = 0;
1016
1060
  else waitMs = (CANCEL_BACKOFF_S[Math.min(idle++, CANCEL_BACKOFF_S.length - 1)] ?? 15) * 1e3;
1017
1061
  }
1018
- if (now() - start + waitMs >= budgetMs) {
1062
+ const limiterMs = this.t.config.limiter?.pendingWaitMs() ?? 0;
1063
+ if (now() - start + Math.max(waitMs, limiterMs) >= budgetMs) {
1019
1064
  stopped = "time_budget";
1065
+ if (limiterMs > waitMs) lastErrorCode = "RATE_LIMITED";
1020
1066
  break;
1021
1067
  }
1022
1068
  if (waitMs > 0) await this.t.config.sleep(waitMs, opts?.signal);
@@ -1026,7 +1072,7 @@ var TradingResource = class extends Resource {
1026
1072
  };
1027
1073
 
1028
1074
  // src/version.ts
1029
- var VERSION = "0.1.0-dev.5";
1075
+ var VERSION = "0.1.0-dev.7";
1030
1076
  var USER_AGENT = `cexy-typescript/${VERSION}`;
1031
1077
 
1032
1078
  // src/ws/emitter.ts
@@ -1878,6 +1924,23 @@ function canSetUserAgent() {
1878
1924
  return !(typeof g.window !== "undefined" && typeof g.window.document !== "undefined");
1879
1925
  }
1880
1926
 
1927
+ // src/ledger.ts
1928
+ var KNOWN_LEDGER_REFERENCE_TYPES = /* @__PURE__ */ new Set([
1929
+ "deposit",
1930
+ "withdrawal",
1931
+ "order",
1932
+ "trade",
1933
+ "transfer",
1934
+ "adjustment",
1935
+ "pool",
1936
+ "futures_transfer",
1937
+ "system"
1938
+ ]);
1939
+ function isLedgerReference(ref, type) {
1940
+ if (typeof ref !== "object" || ref === null || typeof ref.type !== "string") return false;
1941
+ return type === void 0 ? KNOWN_LEDGER_REFERENCE_TYPES.has(ref.type) : ref.type === type;
1942
+ }
1943
+
1881
1944
  exports.AccountResource = AccountResource;
1882
1945
  exports.ApiKeyAuthenticator = ApiKeyAuthenticator;
1883
1946
  exports.AssetsResource = AssetsResource;
@@ -1928,6 +1991,7 @@ exports.assertAmountFields = assertAmountFields;
1928
1991
  exports.errorFromResponse = errorFromResponse;
1929
1992
  exports.isAmount = isAmount;
1930
1993
  exports.isKnownErrorCode = isKnownErrorCode;
1994
+ exports.isLedgerReference = isLedgerReference;
1931
1995
  exports.isLocalHost = isLocalHost;
1932
1996
  exports.isRetryable = isRetryable;
1933
1997
  exports.paginate = paginate;