@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 +34 -3
- package/dist/index.cjs +76 -12
- package/dist/index.d.cts +234 -19
- package/dist/index.d.ts +234 -19
- package/dist/index.js +76 -13
- package/package.json +1 -1
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
|
-
|
|
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([
|
|
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.
|
|
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
|
|
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)
|
|
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
|
-
|
|
745
|
-
|
|
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
|
|
748
|
-
|
|
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
|
-
|
|
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.
|
|
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;
|