@cexyio/cexy 0.1.0-dev.6 → 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({
@@ -94,7 +96,7 @@ Give both `apiKey` and `apiSecret`, or neither: passing only one throws at const
94
96
  | `markets` | `list`, `get`, `orderbook`, `trades`, `iterateTrades`, `candles` | public |
95
97
  | `assets`, `networks`, `fees`, `pools` | `list`, `get` / `list` / `get` / `list`, `get` | public |
96
98
  | `time()`, `config()` | | public |
97
- | `account` | `balances`, `balance`, `ledger`, `notifications`, `subAccounts`, `apiKeys` (+ iterators) | read |
99
+ | `account` | `balances`, `balance`, `ledger`, `notifications`, `subAccounts`, `subAccountBalances`, `apiKeys` (+ iterators) | read |
98
100
  | `exports` | `deposits`, `ledger`, `orders`, `trades`, `withdrawals` (CSV text) | read |
99
101
  | `wallet` | `deposits`, `deposit`, `withdrawals`, `withdrawal`, `withdrawalAddresses`, `depositAddress` (+ iterators) | read |
100
102
  | `trading` | `openOrders`, `order`, `orderByClientId`, `orderHistory`, `trades` (+ iterators) | read |
@@ -106,6 +108,13 @@ asset/network (later calls return the same one). Always use the `memo` too when
106
108
 
107
109
  There are no withdrawal or transfer methods: API keys cannot withdraw or transfer funds.
108
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
+
109
118
  ## Amounts
110
119
 
111
120
  Every amount is an exact decimal **string** (`"0.00150000"`), in responses and requests. JS numbers are
@@ -153,7 +162,8 @@ try {
153
162
  ## Retries and idempotency
154
163
 
155
164
  - Timeout per attempt: `timeoutMs` (default 10 s). Retries: `maxRetries` (default 3), exponential backoff with full jitter.
156
- - 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.
157
167
  - 429 waits at least `Retry-After` / `details.retry_after_seconds`. Server wait hints are untrusted: unusable
158
168
  values are ignored, and a hint longer than 120 s (`MAX_SERVER_WAIT_MS`) is never waited: the call fails at
159
169
  once with `RateLimitError`, whose `retryAfterMs` still has the server's value. The client-side rate limiter
@@ -285,7 +295,7 @@ Report vulnerabilities as described in [SECURITY.md](SECURITY.md).
285
295
  ## For tool builders
286
296
 
287
297
  The package exports its building blocks: the `OPERATIONS` table (method, path, auth and scope for each of the
288
- 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`
289
299
  interface (HMAC signing will plug in here) and `userAgentSuffix` to identify your tool.
290
300
 
291
301
  ## Development
package/dist/index.cjs CHANGED
@@ -146,7 +146,7 @@ var CLIENT_ERROR_CODES = { UNEXPECTED_REDIRECT: "UNEXPECTED_REDIRECT" };
146
146
  function isKnownErrorCode(code) {
147
147
  return KNOWN_CODES.has(code);
148
148
  }
149
- 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]);
150
150
  var MAX_SERVER_WAIT_MS = 12e4;
151
151
  function retryAfterMs(headers, details) {
152
152
  let best = null;
@@ -296,6 +296,7 @@ var OPERATIONS = {
296
296
  get_ledger: op("GET", "/api/v1/account/ledger", "api_key", "read", "account.ledger"),
297
297
  list_notifications: op("GET", "/api/v1/account/notifications", "api_key", "read", "account.notifications"),
298
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"),
299
300
  list_api_keys: op("GET", "/api/v1/account/api-keys", "api_key", "read", "account.apiKeys"),
300
301
  // Exports (read)
301
302
  export_deposits: op("GET", "/api/v1/exports/deposits", "api_key", "read", "exports.deposits"),
@@ -326,6 +327,7 @@ var OPERATIONS = {
326
327
 
327
328
  // src/http.ts
328
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"]);
329
331
  var BACKOFF_BASE_MS = 500;
330
332
  var BACKOFF_MAX_MS = 1e4;
331
333
  var Transport = class {
@@ -337,13 +339,14 @@ var Transport = class {
337
339
  * Sends a request with the standard retry policy: retryable errors and network failures are
338
340
  * retried. Pool join/exit carry an `Idempotency-Key` reused on every attempt (the server
339
341
  * honours it there, which makes their retries safe); the other mutations routed here
340
- * (cancel-all) are naturally repeatable and send no key. `placeOrder` and `cancelOrder` use
341
- * `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.
342
344
  */
343
345
  async request(spec, opts = {}) {
344
346
  const info = OPERATIONS[spec.op];
345
347
  const idempotencyKey = IDEMPOTENT_OPS.has(spec.op) ? spec.idempotencyKey ?? opts.idempotencyKey ?? newId() : void 0;
346
- 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;
347
350
  for (let attempt = 0; ; attempt++) {
348
351
  try {
349
352
  return await this.attempt({ ...spec, idempotencyKey }, opts);
@@ -458,6 +461,7 @@ var Transport = class {
458
461
  const path = info.path.replace(/\{(\w+)\}/g, (_m, name) => {
459
462
  const v = pathParams[name];
460
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 ".."`);
461
465
  return encodeURIComponent(v);
462
466
  });
463
467
  const url = new URL(this.config.baseUrl.replace(/\/+$/, "") + path);
@@ -480,7 +484,11 @@ function serverHintMs(err) {
480
484
  }
481
485
  function isRetryable(err) {
482
486
  if (err instanceof CexyConnectionError) return true;
483
- 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
+ }
484
492
  return false;
485
493
  }
486
494
  function isAmbiguous(err) {
@@ -752,12 +760,22 @@ var PoolsResource = class extends Resource {
752
760
  return this.data({ op: "exit_pool", pathParams: { symbol }, body }, opts);
753
761
  }
754
762
  };
763
+ function withHeldIncoming(b) {
764
+ if (!b || typeof b !== "object" || Array.isArray(b.held_incoming)) return b;
765
+ return { ...b, held_incoming: [] };
766
+ }
755
767
  var AccountResource = class extends Resource {
756
- balances(opts) {
757
- 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;
758
775
  }
759
- balance(asset, opts) {
760
- 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));
761
779
  }
762
780
  ledger(params, opts) {
763
781
  return this.page({ op: "get_ledger", query: params }, opts);
@@ -774,6 +792,20 @@ var AccountResource = class extends Resource {
774
792
  subAccounts(opts) {
775
793
  return this.data({ op: "list_sub_accounts" }, opts);
776
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
+ }
777
809
  /** Your API keys (metadata only; secrets are never returned). */
778
810
  apiKeys(opts) {
779
811
  return this.data({ op: "list_api_keys" }, opts);
@@ -1040,7 +1072,7 @@ var TradingResource = class extends Resource {
1040
1072
  };
1041
1073
 
1042
1074
  // src/version.ts
1043
- var VERSION = "0.1.0-dev.6";
1075
+ var VERSION = "0.1.0-dev.7";
1044
1076
  var USER_AGENT = `cexy-typescript/${VERSION}`;
1045
1077
 
1046
1078
  // src/ws/emitter.ts
package/dist/index.d.cts CHANGED
@@ -102,6 +102,7 @@ declare const OPERATIONS: {
102
102
  readonly get_ledger: OperationInfo;
103
103
  readonly list_notifications: OperationInfo;
104
104
  readonly list_sub_accounts: OperationInfo;
105
+ readonly sub_account_balances: OperationInfo;
105
106
  readonly list_api_keys: OperationInfo;
106
107
  readonly export_deposits: OperationInfo;
107
108
  readonly export_ledger: OperationInfo;
@@ -189,8 +190,8 @@ declare class Transport {
189
190
  * Sends a request with the standard retry policy: retryable errors and network failures are
190
191
  * retried. Pool join/exit carry an `Idempotency-Key` reused on every attempt (the server
191
192
  * honours it there, which makes their retries safe); the other mutations routed here
192
- * (cancel-all) are naturally repeatable and send no key. `placeOrder` and `cancelOrder` use
193
- * `attempt()` with their own policies.
193
+ * (cancel-all) are naturally repeatable and send no key. Any other mutation is sent once.
194
+ * `placeOrder` and `cancelOrder` use `attempt()` with their own policies.
194
195
  */
195
196
  request(spec: CallSpec, opts?: RequestOptions): Promise<RawResponse>;
196
197
  /**
@@ -205,7 +206,11 @@ declare class Transport {
205
206
  buildUrl(info: OperationInfo, pathParams?: Record<string, string>, query?: Record<string, unknown>): URL;
206
207
  redact(text: string): string;
207
208
  }
208
- /** Retryable = a network failure/timeout, or an API error with `retryable: true` (incl. 409 CONCURRENT_MODIFICATION). */
209
+ /**
210
+ * Retryable = a network failure/timeout, or an API error with `retryable: true` (incl. 409
211
+ * CONCURRENT_MODIFICATION). A 4xx is never retryable except 429 and 409 CONCURRENT_MODIFICATION,
212
+ * whatever its body says.
213
+ */
209
214
  declare function isRetryable(err: unknown): boolean;
210
215
 
211
216
  interface paths {
@@ -314,6 +319,26 @@ interface paths {
314
319
  patch?: never;
315
320
  trace?: never;
316
321
  };
322
+ "/api/v1/account/sub-accounts/{id}/balances": {
323
+ parameters: {
324
+ query?: never;
325
+ header?: never;
326
+ path?: never;
327
+ cookie?: never;
328
+ };
329
+ /**
330
+ * A sub-account's balances, for its parent.
331
+ * @description The same shape as `/account/balances`, zero balances omitted: what the sub-account holds, so its funds can be shown and moved back. Only the parent may read it; any id that is not one of the caller's sub-accounts is not found.
332
+ */
333
+ get: operations["sub_account_balances"];
334
+ put?: never;
335
+ post?: never;
336
+ delete?: never;
337
+ options?: never;
338
+ head?: never;
339
+ patch?: never;
340
+ trace?: never;
341
+ };
317
342
  "/api/v1/assets": {
318
343
  parameters: {
319
344
  query?: never;
@@ -1041,6 +1066,8 @@ interface components {
1041
1066
  /** @description Asset symbol. */
1042
1067
  asset: string;
1043
1068
  available: components["schemas"]["Amount"];
1069
+ /** @description Internal transfers to this account still held, soonest released first; empty when none. Their sum is part of `locked`. Shows at most 100. */
1070
+ held_incoming: components["schemas"]["HeldIncomingResponse"][];
1044
1071
  locked: components["schemas"]["Amount"];
1045
1072
  pending: components["schemas"]["Amount"];
1046
1073
  total: components["schemas"]["Amount"];
@@ -1326,6 +1353,17 @@ interface components {
1326
1353
  * @example 507f1f77bcf86cd799439011
1327
1354
  */
1328
1355
  FuturesTransferId: string;
1356
+ /** @description An internal transfer credited to `locked` and not yet available. */
1357
+ HeldIncomingResponse: {
1358
+ amount: components["schemas"]["Amount"];
1359
+ /**
1360
+ * Format: date-time
1361
+ * @description When it becomes available, unless an operator cancels it before then.
1362
+ */
1363
+ available_at: string;
1364
+ /** @description The transfer. */
1365
+ transfer_id: string;
1366
+ };
1329
1367
  /** @description Adds liquidity to a pool. */
1330
1368
  JoinPoolRequest: {
1331
1369
  base_amount: components["schemas"]["Amount"];
@@ -2081,6 +2119,49 @@ interface operations {
2081
2119
  };
2082
2120
  };
2083
2121
  };
2122
+ sub_account_balances: {
2123
+ parameters: {
2124
+ query?: never;
2125
+ header?: never;
2126
+ path: {
2127
+ /** @description Sub-account id */
2128
+ id: string;
2129
+ };
2130
+ cookie?: never;
2131
+ };
2132
+ requestBody?: never;
2133
+ responses: {
2134
+ /** @description The sub-account's balances */
2135
+ 200: {
2136
+ headers: {
2137
+ [name: string]: unknown;
2138
+ };
2139
+ content: {
2140
+ "application/json": {
2141
+ data: components["schemas"]["BalanceResponse"][];
2142
+ };
2143
+ };
2144
+ };
2145
+ /** @description Malformed sub-account id */
2146
+ 400: {
2147
+ headers: {
2148
+ [name: string]: unknown;
2149
+ };
2150
+ content: {
2151
+ "application/json": components["schemas"]["ErrorResponse"];
2152
+ };
2153
+ };
2154
+ /** @description No such sub-account on this account */
2155
+ 404: {
2156
+ headers: {
2157
+ [name: string]: unknown;
2158
+ };
2159
+ content: {
2160
+ "application/json": components["schemas"]["ErrorResponse"];
2161
+ };
2162
+ };
2163
+ };
2164
+ };
2084
2165
  list_assets: {
2085
2166
  parameters: {
2086
2167
  query?: never;
@@ -3301,6 +3382,8 @@ type ApiScope = S["ApiScope"];
3301
3382
  type Asset = S["AssetResponse"];
3302
3383
  type AssetNetwork = S["AssetNetworkResponse"];
3303
3384
  type Balance = S["BalanceResponse"];
3385
+ /** An incoming internal transfer still held (see `Balance.held_incoming`). */
3386
+ type HeldIncoming = S["HeldIncomingResponse"];
3304
3387
  type CancelAllRequest = S["CancelAllRequest"];
3305
3388
  type CancelAllResult = S["CancelAllResponse"];
3306
3389
  /** Why one order could not be cancelled (`CancelAllResult.failures`). */
@@ -3458,13 +3541,29 @@ declare class PoolsResource extends Resource {
3458
3541
  exit(symbol: string, body: ExitPoolRequest, opts?: RequestOptions): Promise<ExitPoolResult>;
3459
3542
  }
3460
3543
  declare class AccountResource extends Resource {
3544
+ /**
3545
+ * Balances per asset. `held_incoming` lists incoming internal transfers still held; their sum is
3546
+ * already included in `locked` (never add it again). Always an array (`[]` when none).
3547
+ */
3461
3548
  balances(opts?: RequestOptions): Promise<Balance[]>;
3549
+ /** One asset's balance. `held_incoming`: incoming transfers still held, already inside `locked`. */
3462
3550
  balance(asset: string, opts?: RequestOptions): Promise<Balance>;
3463
3551
  ledger(params?: Q<"get_ledger">, opts?: RequestOptions): Promise<Page<LedgerEntry>>;
3464
3552
  iterateLedger(params?: Q<"get_ledger">, iter?: IterateOptions & RequestOptions): AsyncGenerator<LedgerEntry, void, undefined>;
3465
3553
  notifications(params?: Q<"list_notifications">, opts?: RequestOptions): Promise<Page<Notification>>;
3466
3554
  iterateNotifications(params?: Q<"list_notifications">, iter?: IterateOptions & RequestOptions): AsyncGenerator<Notification, void, undefined>;
3467
3555
  subAccounts(opts?: RequestOptions): Promise<SubAccount[]>;
3556
+ /**
3557
+ * A sub-account's balances, read by its PARENT account: the same shape as `balances()`
3558
+ * (zero balances omitted), including `held_incoming`, whose sum is already inside `locked`.
3559
+ * The server currently returns them ordered by asset symbol; don't rely on the order.
3560
+ * An id that is not one of the caller's sub-accounts (or a call with the sub-account's own
3561
+ * key) gets `NotFoundError`; a sub-account's own key reads its balances with `balances()`.
3562
+ * A malformed id gets 400 (`ValidationError`); a key without the `read` scope gets 403
3563
+ * `FORBIDDEN` (`ForbiddenError`). `id` must be non-empty and not "." or ".."; it is sent as
3564
+ * one URL path segment.
3565
+ */
3566
+ subAccountBalances(id: string, opts?: RequestOptions): Promise<Balance[]>;
3468
3567
  /** Your API keys (metadata only; secrets are never returned). */
3469
3568
  apiKeys(opts?: RequestOptions): Promise<ApiKey[]>;
3470
3569
  }
@@ -4167,11 +4266,11 @@ declare function isLedgerReference<T extends LedgerReferenceType>(ref: LedgerRef
4167
4266
  }>;
4168
4267
 
4169
4268
  /** SDK version, kept in sync with package.json (a test enforces this). */
4170
- declare const VERSION = "0.1.0-dev.6";
4269
+ declare const VERSION = "0.1.0-dev.7";
4171
4270
  /** Default User-Agent product token. */
4172
- declare const USER_AGENT = "cexy-typescript/0.1.0-dev.6";
4271
+ declare const USER_AGENT = "cexy-typescript/0.1.0-dev.7";
4173
4272
 
4174
4273
  /** True for loopback hosts, the only ones where plain-text transport may be allowed. */
4175
4274
  declare function isLocalHost(hostname: string): boolean;
4176
4275
 
4177
- 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, CancelAllInterruptedError, 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 DepositId, 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, type FuturesTransferId, InvalidAmountError, type IterateOptions, type JoinPoolRequest, type JoinPoolResult, JurisdictionBlockedError, KNOWN_EVENT_TYPES, type KnownErrorCode, type KnownLedgerReference, type LedgerEntry, type LedgerEntryKind, type LedgerReference, type LedgerReferenceType, type LiquidityRole, type Listener, LiveOrderBook, type LiveOrderBookEvents, type LiveOrderBookOptions, MAX_SERVER_WAIT_MS, 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 OrderId, type OrderSide, OrderStateUnknownError, type OrderStatus, type OrderType, PRIVATE_CHANNELS, type Page, type PlaceOrderRequest, type PlaceOrderResponse, type PlaceOrderResult, type PongFrame, type Pool, type PoolId, 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 TradeId, type TradeNewEvent, TradingResource, type TriggerDirection, TypedEmitter, USER_AGENT, type UnknownLedgerReference, UnprocessableError, type UnsubscribedFrame, type UserId, VERSION, ValidationError, WS_BOOK_DEPTH, WalletResource, type WebSocketConstructor, type WebSocketLike, type WelcomeFrame, type Withdrawal, type WithdrawalAddress, type WithdrawalId, type WithdrawalStatus, type WithdrawalUpdatedEvent, type WsEvent, type WsLogger, assertAmountFields, type components, errorFromResponse, isAmount, isKnownErrorCode, isLedgerReference, isLocalHost, isRetryable, type operations, paginate, type paths };
4276
+ 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, CancelAllInterruptedError, 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 DepositId, 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, type FuturesTransferId, type HeldIncoming, InvalidAmountError, type IterateOptions, type JoinPoolRequest, type JoinPoolResult, JurisdictionBlockedError, KNOWN_EVENT_TYPES, type KnownErrorCode, type KnownLedgerReference, type LedgerEntry, type LedgerEntryKind, type LedgerReference, type LedgerReferenceType, type LiquidityRole, type Listener, LiveOrderBook, type LiveOrderBookEvents, type LiveOrderBookOptions, MAX_SERVER_WAIT_MS, 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 OrderId, type OrderSide, OrderStateUnknownError, type OrderStatus, type OrderType, PRIVATE_CHANNELS, type Page, type PlaceOrderRequest, type PlaceOrderResponse, type PlaceOrderResult, type PongFrame, type Pool, type PoolId, 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 TradeId, type TradeNewEvent, TradingResource, type TriggerDirection, TypedEmitter, USER_AGENT, type UnknownLedgerReference, UnprocessableError, type UnsubscribedFrame, type UserId, VERSION, ValidationError, WS_BOOK_DEPTH, WalletResource, type WebSocketConstructor, type WebSocketLike, type WelcomeFrame, type Withdrawal, type WithdrawalAddress, type WithdrawalId, type WithdrawalStatus, type WithdrawalUpdatedEvent, type WsEvent, type WsLogger, assertAmountFields, type components, errorFromResponse, isAmount, isKnownErrorCode, isLedgerReference, isLocalHost, isRetryable, type operations, paginate, type paths };
package/dist/index.d.ts CHANGED
@@ -102,6 +102,7 @@ declare const OPERATIONS: {
102
102
  readonly get_ledger: OperationInfo;
103
103
  readonly list_notifications: OperationInfo;
104
104
  readonly list_sub_accounts: OperationInfo;
105
+ readonly sub_account_balances: OperationInfo;
105
106
  readonly list_api_keys: OperationInfo;
106
107
  readonly export_deposits: OperationInfo;
107
108
  readonly export_ledger: OperationInfo;
@@ -189,8 +190,8 @@ declare class Transport {
189
190
  * Sends a request with the standard retry policy: retryable errors and network failures are
190
191
  * retried. Pool join/exit carry an `Idempotency-Key` reused on every attempt (the server
191
192
  * honours it there, which makes their retries safe); the other mutations routed here
192
- * (cancel-all) are naturally repeatable and send no key. `placeOrder` and `cancelOrder` use
193
- * `attempt()` with their own policies.
193
+ * (cancel-all) are naturally repeatable and send no key. Any other mutation is sent once.
194
+ * `placeOrder` and `cancelOrder` use `attempt()` with their own policies.
194
195
  */
195
196
  request(spec: CallSpec, opts?: RequestOptions): Promise<RawResponse>;
196
197
  /**
@@ -205,7 +206,11 @@ declare class Transport {
205
206
  buildUrl(info: OperationInfo, pathParams?: Record<string, string>, query?: Record<string, unknown>): URL;
206
207
  redact(text: string): string;
207
208
  }
208
- /** Retryable = a network failure/timeout, or an API error with `retryable: true` (incl. 409 CONCURRENT_MODIFICATION). */
209
+ /**
210
+ * Retryable = a network failure/timeout, or an API error with `retryable: true` (incl. 409
211
+ * CONCURRENT_MODIFICATION). A 4xx is never retryable except 429 and 409 CONCURRENT_MODIFICATION,
212
+ * whatever its body says.
213
+ */
209
214
  declare function isRetryable(err: unknown): boolean;
210
215
 
211
216
  interface paths {
@@ -314,6 +319,26 @@ interface paths {
314
319
  patch?: never;
315
320
  trace?: never;
316
321
  };
322
+ "/api/v1/account/sub-accounts/{id}/balances": {
323
+ parameters: {
324
+ query?: never;
325
+ header?: never;
326
+ path?: never;
327
+ cookie?: never;
328
+ };
329
+ /**
330
+ * A sub-account's balances, for its parent.
331
+ * @description The same shape as `/account/balances`, zero balances omitted: what the sub-account holds, so its funds can be shown and moved back. Only the parent may read it; any id that is not one of the caller's sub-accounts is not found.
332
+ */
333
+ get: operations["sub_account_balances"];
334
+ put?: never;
335
+ post?: never;
336
+ delete?: never;
337
+ options?: never;
338
+ head?: never;
339
+ patch?: never;
340
+ trace?: never;
341
+ };
317
342
  "/api/v1/assets": {
318
343
  parameters: {
319
344
  query?: never;
@@ -1041,6 +1066,8 @@ interface components {
1041
1066
  /** @description Asset symbol. */
1042
1067
  asset: string;
1043
1068
  available: components["schemas"]["Amount"];
1069
+ /** @description Internal transfers to this account still held, soonest released first; empty when none. Their sum is part of `locked`. Shows at most 100. */
1070
+ held_incoming: components["schemas"]["HeldIncomingResponse"][];
1044
1071
  locked: components["schemas"]["Amount"];
1045
1072
  pending: components["schemas"]["Amount"];
1046
1073
  total: components["schemas"]["Amount"];
@@ -1326,6 +1353,17 @@ interface components {
1326
1353
  * @example 507f1f77bcf86cd799439011
1327
1354
  */
1328
1355
  FuturesTransferId: string;
1356
+ /** @description An internal transfer credited to `locked` and not yet available. */
1357
+ HeldIncomingResponse: {
1358
+ amount: components["schemas"]["Amount"];
1359
+ /**
1360
+ * Format: date-time
1361
+ * @description When it becomes available, unless an operator cancels it before then.
1362
+ */
1363
+ available_at: string;
1364
+ /** @description The transfer. */
1365
+ transfer_id: string;
1366
+ };
1329
1367
  /** @description Adds liquidity to a pool. */
1330
1368
  JoinPoolRequest: {
1331
1369
  base_amount: components["schemas"]["Amount"];
@@ -2081,6 +2119,49 @@ interface operations {
2081
2119
  };
2082
2120
  };
2083
2121
  };
2122
+ sub_account_balances: {
2123
+ parameters: {
2124
+ query?: never;
2125
+ header?: never;
2126
+ path: {
2127
+ /** @description Sub-account id */
2128
+ id: string;
2129
+ };
2130
+ cookie?: never;
2131
+ };
2132
+ requestBody?: never;
2133
+ responses: {
2134
+ /** @description The sub-account's balances */
2135
+ 200: {
2136
+ headers: {
2137
+ [name: string]: unknown;
2138
+ };
2139
+ content: {
2140
+ "application/json": {
2141
+ data: components["schemas"]["BalanceResponse"][];
2142
+ };
2143
+ };
2144
+ };
2145
+ /** @description Malformed sub-account id */
2146
+ 400: {
2147
+ headers: {
2148
+ [name: string]: unknown;
2149
+ };
2150
+ content: {
2151
+ "application/json": components["schemas"]["ErrorResponse"];
2152
+ };
2153
+ };
2154
+ /** @description No such sub-account on this account */
2155
+ 404: {
2156
+ headers: {
2157
+ [name: string]: unknown;
2158
+ };
2159
+ content: {
2160
+ "application/json": components["schemas"]["ErrorResponse"];
2161
+ };
2162
+ };
2163
+ };
2164
+ };
2084
2165
  list_assets: {
2085
2166
  parameters: {
2086
2167
  query?: never;
@@ -3301,6 +3382,8 @@ type ApiScope = S["ApiScope"];
3301
3382
  type Asset = S["AssetResponse"];
3302
3383
  type AssetNetwork = S["AssetNetworkResponse"];
3303
3384
  type Balance = S["BalanceResponse"];
3385
+ /** An incoming internal transfer still held (see `Balance.held_incoming`). */
3386
+ type HeldIncoming = S["HeldIncomingResponse"];
3304
3387
  type CancelAllRequest = S["CancelAllRequest"];
3305
3388
  type CancelAllResult = S["CancelAllResponse"];
3306
3389
  /** Why one order could not be cancelled (`CancelAllResult.failures`). */
@@ -3458,13 +3541,29 @@ declare class PoolsResource extends Resource {
3458
3541
  exit(symbol: string, body: ExitPoolRequest, opts?: RequestOptions): Promise<ExitPoolResult>;
3459
3542
  }
3460
3543
  declare class AccountResource extends Resource {
3544
+ /**
3545
+ * Balances per asset. `held_incoming` lists incoming internal transfers still held; their sum is
3546
+ * already included in `locked` (never add it again). Always an array (`[]` when none).
3547
+ */
3461
3548
  balances(opts?: RequestOptions): Promise<Balance[]>;
3549
+ /** One asset's balance. `held_incoming`: incoming transfers still held, already inside `locked`. */
3462
3550
  balance(asset: string, opts?: RequestOptions): Promise<Balance>;
3463
3551
  ledger(params?: Q<"get_ledger">, opts?: RequestOptions): Promise<Page<LedgerEntry>>;
3464
3552
  iterateLedger(params?: Q<"get_ledger">, iter?: IterateOptions & RequestOptions): AsyncGenerator<LedgerEntry, void, undefined>;
3465
3553
  notifications(params?: Q<"list_notifications">, opts?: RequestOptions): Promise<Page<Notification>>;
3466
3554
  iterateNotifications(params?: Q<"list_notifications">, iter?: IterateOptions & RequestOptions): AsyncGenerator<Notification, void, undefined>;
3467
3555
  subAccounts(opts?: RequestOptions): Promise<SubAccount[]>;
3556
+ /**
3557
+ * A sub-account's balances, read by its PARENT account: the same shape as `balances()`
3558
+ * (zero balances omitted), including `held_incoming`, whose sum is already inside `locked`.
3559
+ * The server currently returns them ordered by asset symbol; don't rely on the order.
3560
+ * An id that is not one of the caller's sub-accounts (or a call with the sub-account's own
3561
+ * key) gets `NotFoundError`; a sub-account's own key reads its balances with `balances()`.
3562
+ * A malformed id gets 400 (`ValidationError`); a key without the `read` scope gets 403
3563
+ * `FORBIDDEN` (`ForbiddenError`). `id` must be non-empty and not "." or ".."; it is sent as
3564
+ * one URL path segment.
3565
+ */
3566
+ subAccountBalances(id: string, opts?: RequestOptions): Promise<Balance[]>;
3468
3567
  /** Your API keys (metadata only; secrets are never returned). */
3469
3568
  apiKeys(opts?: RequestOptions): Promise<ApiKey[]>;
3470
3569
  }
@@ -4167,11 +4266,11 @@ declare function isLedgerReference<T extends LedgerReferenceType>(ref: LedgerRef
4167
4266
  }>;
4168
4267
 
4169
4268
  /** SDK version, kept in sync with package.json (a test enforces this). */
4170
- declare const VERSION = "0.1.0-dev.6";
4269
+ declare const VERSION = "0.1.0-dev.7";
4171
4270
  /** Default User-Agent product token. */
4172
- declare const USER_AGENT = "cexy-typescript/0.1.0-dev.6";
4271
+ declare const USER_AGENT = "cexy-typescript/0.1.0-dev.7";
4173
4272
 
4174
4273
  /** True for loopback hosts, the only ones where plain-text transport may be allowed. */
4175
4274
  declare function isLocalHost(hostname: string): boolean;
4176
4275
 
4177
- 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, CancelAllInterruptedError, 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 DepositId, 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, type FuturesTransferId, InvalidAmountError, type IterateOptions, type JoinPoolRequest, type JoinPoolResult, JurisdictionBlockedError, KNOWN_EVENT_TYPES, type KnownErrorCode, type KnownLedgerReference, type LedgerEntry, type LedgerEntryKind, type LedgerReference, type LedgerReferenceType, type LiquidityRole, type Listener, LiveOrderBook, type LiveOrderBookEvents, type LiveOrderBookOptions, MAX_SERVER_WAIT_MS, 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 OrderId, type OrderSide, OrderStateUnknownError, type OrderStatus, type OrderType, PRIVATE_CHANNELS, type Page, type PlaceOrderRequest, type PlaceOrderResponse, type PlaceOrderResult, type PongFrame, type Pool, type PoolId, 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 TradeId, type TradeNewEvent, TradingResource, type TriggerDirection, TypedEmitter, USER_AGENT, type UnknownLedgerReference, UnprocessableError, type UnsubscribedFrame, type UserId, VERSION, ValidationError, WS_BOOK_DEPTH, WalletResource, type WebSocketConstructor, type WebSocketLike, type WelcomeFrame, type Withdrawal, type WithdrawalAddress, type WithdrawalId, type WithdrawalStatus, type WithdrawalUpdatedEvent, type WsEvent, type WsLogger, assertAmountFields, type components, errorFromResponse, isAmount, isKnownErrorCode, isLedgerReference, isLocalHost, isRetryable, type operations, paginate, type paths };
4276
+ 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, CancelAllInterruptedError, 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 DepositId, 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, type FuturesTransferId, type HeldIncoming, InvalidAmountError, type IterateOptions, type JoinPoolRequest, type JoinPoolResult, JurisdictionBlockedError, KNOWN_EVENT_TYPES, type KnownErrorCode, type KnownLedgerReference, type LedgerEntry, type LedgerEntryKind, type LedgerReference, type LedgerReferenceType, type LiquidityRole, type Listener, LiveOrderBook, type LiveOrderBookEvents, type LiveOrderBookOptions, MAX_SERVER_WAIT_MS, 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 OrderId, type OrderSide, OrderStateUnknownError, type OrderStatus, type OrderType, PRIVATE_CHANNELS, type Page, type PlaceOrderRequest, type PlaceOrderResponse, type PlaceOrderResult, type PongFrame, type Pool, type PoolId, 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 TradeId, type TradeNewEvent, TradingResource, type TriggerDirection, TypedEmitter, USER_AGENT, type UnknownLedgerReference, UnprocessableError, type UnsubscribedFrame, type UserId, VERSION, ValidationError, WS_BOOK_DEPTH, WalletResource, type WebSocketConstructor, type WebSocketLike, type WelcomeFrame, type Withdrawal, type WithdrawalAddress, type WithdrawalId, type WithdrawalStatus, type WithdrawalUpdatedEvent, type WsEvent, type WsLogger, assertAmountFields, type components, errorFromResponse, isAmount, isKnownErrorCode, isLedgerReference, isLocalHost, isRetryable, type operations, paginate, type paths };
package/dist/index.js CHANGED
@@ -144,7 +144,7 @@ var CLIENT_ERROR_CODES = { UNEXPECTED_REDIRECT: "UNEXPECTED_REDIRECT" };
144
144
  function isKnownErrorCode(code) {
145
145
  return KNOWN_CODES.has(code);
146
146
  }
147
- var DEFAULT_RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
147
+ var DEFAULT_RETRYABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
148
148
  var MAX_SERVER_WAIT_MS = 12e4;
149
149
  function retryAfterMs(headers, details) {
150
150
  let best = null;
@@ -294,6 +294,7 @@ var OPERATIONS = {
294
294
  get_ledger: op("GET", "/api/v1/account/ledger", "api_key", "read", "account.ledger"),
295
295
  list_notifications: op("GET", "/api/v1/account/notifications", "api_key", "read", "account.notifications"),
296
296
  list_sub_accounts: op("GET", "/api/v1/account/sub-accounts", "api_key", "read", "account.subAccounts"),
297
+ sub_account_balances: op("GET", "/api/v1/account/sub-accounts/{id}/balances", "api_key", "read", "account.subAccountBalances"),
297
298
  list_api_keys: op("GET", "/api/v1/account/api-keys", "api_key", "read", "account.apiKeys"),
298
299
  // Exports (read)
299
300
  export_deposits: op("GET", "/api/v1/exports/deposits", "api_key", "read", "exports.deposits"),
@@ -324,6 +325,7 @@ var OPERATIONS = {
324
325
 
325
326
  // src/http.ts
326
327
  var IDEMPOTENT_OPS = /* @__PURE__ */ new Set(["join_pool", "exit_pool"]);
328
+ var REPEAT_SAFE_MUTATIONS = /* @__PURE__ */ new Set(["join_pool", "exit_pool", "cancel_all"]);
327
329
  var BACKOFF_BASE_MS = 500;
328
330
  var BACKOFF_MAX_MS = 1e4;
329
331
  var Transport = class {
@@ -335,13 +337,14 @@ var Transport = class {
335
337
  * Sends a request with the standard retry policy: retryable errors and network failures are
336
338
  * retried. Pool join/exit carry an `Idempotency-Key` reused on every attempt (the server
337
339
  * honours it there, which makes their retries safe); the other mutations routed here
338
- * (cancel-all) are naturally repeatable and send no key. `placeOrder` and `cancelOrder` use
339
- * `attempt()` with their own policies.
340
+ * (cancel-all) are naturally repeatable and send no key. Any other mutation is sent once.
341
+ * `placeOrder` and `cancelOrder` use `attempt()` with their own policies.
340
342
  */
341
343
  async request(spec, opts = {}) {
342
344
  const info = OPERATIONS[spec.op];
343
345
  const idempotencyKey = IDEMPOTENT_OPS.has(spec.op) ? spec.idempotencyKey ?? opts.idempotencyKey ?? newId() : void 0;
344
- const maxRetries = opts.maxRetries ?? this.config.maxRetries;
346
+ const repeatSafe = info.method === "GET" || REPEAT_SAFE_MUTATIONS.has(spec.op);
347
+ const maxRetries = repeatSafe ? opts.maxRetries ?? this.config.maxRetries : 0;
345
348
  for (let attempt = 0; ; attempt++) {
346
349
  try {
347
350
  return await this.attempt({ ...spec, idempotencyKey }, opts);
@@ -456,6 +459,7 @@ var Transport = class {
456
459
  const path = info.path.replace(/\{(\w+)\}/g, (_m, name) => {
457
460
  const v = pathParams[name];
458
461
  if (typeof v !== "string" || v === "") throw new CexyConfigError(`${info.sdkMethod}(): ${name} is required`);
462
+ if (v === "." || v === "..") throw new CexyConfigError(`${info.sdkMethod}(): ${name} must not be "." or ".."`);
459
463
  return encodeURIComponent(v);
460
464
  });
461
465
  const url = new URL(this.config.baseUrl.replace(/\/+$/, "") + path);
@@ -478,7 +482,11 @@ function serverHintMs(err) {
478
482
  }
479
483
  function isRetryable(err) {
480
484
  if (err instanceof CexyConnectionError) return true;
481
- if (err instanceof CexyApiError) return err.retryable || err.code === "CONCURRENT_MODIFICATION";
485
+ if (err instanceof CexyApiError) {
486
+ const concurrent = err.code === "CONCURRENT_MODIFICATION";
487
+ if (err.status >= 400 && err.status < 500 && err.status !== 429 && !(err.status === 409 && concurrent)) return false;
488
+ return err.retryable || concurrent;
489
+ }
482
490
  return false;
483
491
  }
484
492
  function isAmbiguous(err) {
@@ -750,12 +758,22 @@ var PoolsResource = class extends Resource {
750
758
  return this.data({ op: "exit_pool", pathParams: { symbol }, body }, opts);
751
759
  }
752
760
  };
761
+ function withHeldIncoming(b) {
762
+ if (!b || typeof b !== "object" || Array.isArray(b.held_incoming)) return b;
763
+ return { ...b, held_incoming: [] };
764
+ }
753
765
  var AccountResource = class extends Resource {
754
- balances(opts) {
755
- return this.data({ op: "list_balances" }, opts);
766
+ /**
767
+ * Balances per asset. `held_incoming` lists incoming internal transfers still held; their sum is
768
+ * already included in `locked` (never add it again). Always an array (`[]` when none).
769
+ */
770
+ async balances(opts) {
771
+ const rows = await this.data({ op: "list_balances" }, opts);
772
+ return Array.isArray(rows) ? rows.map(withHeldIncoming) : rows;
756
773
  }
757
- balance(asset, opts) {
758
- return this.data({ op: "get_balance", pathParams: { asset } }, opts);
774
+ /** One asset's balance. `held_incoming`: incoming transfers still held, already inside `locked`. */
775
+ async balance(asset, opts) {
776
+ return withHeldIncoming(await this.data({ op: "get_balance", pathParams: { asset } }, opts));
759
777
  }
760
778
  ledger(params, opts) {
761
779
  return this.page({ op: "get_ledger", query: params }, opts);
@@ -772,6 +790,20 @@ var AccountResource = class extends Resource {
772
790
  subAccounts(opts) {
773
791
  return this.data({ op: "list_sub_accounts" }, opts);
774
792
  }
793
+ /**
794
+ * A sub-account's balances, read by its PARENT account: the same shape as `balances()`
795
+ * (zero balances omitted), including `held_incoming`, whose sum is already inside `locked`.
796
+ * The server currently returns them ordered by asset symbol; don't rely on the order.
797
+ * An id that is not one of the caller's sub-accounts (or a call with the sub-account's own
798
+ * key) gets `NotFoundError`; a sub-account's own key reads its balances with `balances()`.
799
+ * A malformed id gets 400 (`ValidationError`); a key without the `read` scope gets 403
800
+ * `FORBIDDEN` (`ForbiddenError`). `id` must be non-empty and not "." or ".."; it is sent as
801
+ * one URL path segment.
802
+ */
803
+ async subAccountBalances(id, opts) {
804
+ const rows = await this.data({ op: "sub_account_balances", pathParams: { id } }, opts);
805
+ return Array.isArray(rows) ? rows.map(withHeldIncoming) : rows;
806
+ }
775
807
  /** Your API keys (metadata only; secrets are never returned). */
776
808
  apiKeys(opts) {
777
809
  return this.data({ op: "list_api_keys" }, opts);
@@ -1038,7 +1070,7 @@ var TradingResource = class extends Resource {
1038
1070
  };
1039
1071
 
1040
1072
  // src/version.ts
1041
- var VERSION = "0.1.0-dev.6";
1073
+ var VERSION = "0.1.0-dev.7";
1042
1074
  var USER_AGENT = `cexy-typescript/${VERSION}`;
1043
1075
 
1044
1076
  // src/ws/emitter.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cexyio/cexy",
3
- "version": "0.1.0-dev.6",
3
+ "version": "0.1.0-dev.7",
4
4
  "description": "Official TypeScript/JavaScript SDK for the CEXY.io REST and WebSocket API",
5
5
  "license": "MIT",
6
6
  "author": "CEXY.io",