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

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/dist/index.js CHANGED
@@ -29,6 +29,16 @@ var OrderStateUnknownError = class extends CexyError {
29
29
  this.clientOrderId = clientOrderId;
30
30
  }
31
31
  };
32
+ var CancelAllInterruptedError = class extends CexyError {
33
+ error;
34
+ partial;
35
+ constructor(error, partial) {
36
+ const what = error instanceof CexyApiError ? `${error.code}: ${error.message}` : String(error);
37
+ super(`cancelAll stopped after ${partial.rounds} round(s): ${what}`, { cause: error });
38
+ this.error = error;
39
+ this.partial = partial;
40
+ }
41
+ };
32
42
  var CexyApiError = class extends CexyError {
33
43
  status;
34
44
  code;
@@ -129,24 +139,27 @@ var KNOWN_CODES = /* @__PURE__ */ new Set([
129
139
  "UNDER_MAINTENANCE",
130
140
  "ENGINE_OVERLOADED"
131
141
  ]);
142
+ var CLIENT_ERROR_CODES = { UNEXPECTED_REDIRECT: "UNEXPECTED_REDIRECT" };
132
143
  function isKnownErrorCode(code) {
133
144
  return KNOWN_CODES.has(code);
134
145
  }
135
146
  var DEFAULT_RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
147
+ var MAX_SERVER_WAIT_MS = 12e4;
136
148
  function retryAfterMs(headers, details) {
137
149
  let best = null;
138
- const h = headers?.get("retry-after");
150
+ const h = headers?.get("retry-after")?.trim();
139
151
  if (h) {
140
- const secs = Number(h);
141
- if (Number.isFinite(secs)) best = Math.max(0, secs * 1e3);
142
- else {
152
+ if (/^\d+(\.\d+)?$/.test(h)) {
153
+ const ms = Number(h) * 1e3;
154
+ if (Number.isFinite(ms)) best = ms;
155
+ } else if (/[a-z]/i.test(h)) {
143
156
  const at = Date.parse(h);
144
- if (!Number.isNaN(at)) best = Math.max(0, at - Date.now());
157
+ if (Number.isFinite(at)) best = Math.max(0, at - Date.now());
145
158
  }
146
159
  }
147
160
  const d = details?.["retry_after_seconds"];
148
- const ds = typeof d === "number" ? d : typeof d === "string" ? Number(d) : NaN;
149
- if (Number.isFinite(ds)) best = Math.max(best ?? 0, ds * 1e3);
161
+ const ds = typeof d === "number" ? d : typeof d === "string" && /^\d+(\.\d+)?$/.test(d.trim()) ? Number(d) : NaN;
162
+ if (Number.isFinite(ds) && ds >= 0 && Number.isFinite(ds * 1e3)) best = Math.max(best ?? 0, ds * 1e3);
150
163
  return best;
151
164
  }
152
165
  function errorFromResponse(status, body, headers, redact = (t) => t) {
@@ -309,6 +322,7 @@ var OPERATIONS = {
309
322
  };
310
323
 
311
324
  // src/http.ts
325
+ var IDEMPOTENT_OPS = /* @__PURE__ */ new Set(["join_pool", "exit_pool"]);
312
326
  var BACKOFF_BASE_MS = 500;
313
327
  var BACKOFF_MAX_MS = 1e4;
314
328
  var Transport = class {
@@ -317,16 +331,15 @@ var Transport = class {
317
331
  this.config = config;
318
332
  }
319
333
  /**
320
- * Sends a request with the standard retry policy: GETs retry on retryable errors and
321
- * network failures. Mutations carry an `Idempotency-Key` reused on every attempt; the server
322
- * honours it on pool join/exit, which makes their retries safe. The other mutations routed
323
- * here (cancel-all) are naturally repeatable. `placeOrder` and `cancelOrder` use
334
+ * Sends a request with the standard retry policy: retryable errors and network failures are
335
+ * retried. Pool join/exit carry an `Idempotency-Key` reused on every attempt (the server
336
+ * honours it there, which makes their retries safe); the other mutations routed here
337
+ * (cancel-all) are naturally repeatable and send no key. `placeOrder` and `cancelOrder` use
324
338
  * `attempt()` with their own policies.
325
339
  */
326
340
  async request(spec, opts = {}) {
327
341
  const info = OPERATIONS[spec.op];
328
- const isMutation = info.method !== "GET";
329
- const idempotencyKey = isMutation ? spec.idempotencyKey ?? opts.idempotencyKey ?? newId() : void 0;
342
+ const idempotencyKey = IDEMPOTENT_OPS.has(spec.op) ? spec.idempotencyKey ?? opts.idempotencyKey ?? newId() : void 0;
330
343
  const maxRetries = opts.maxRetries ?? this.config.maxRetries;
331
344
  for (let attempt = 0; ; attempt++) {
332
345
  try {
@@ -337,8 +350,13 @@ var Transport = class {
337
350
  }
338
351
  }
339
352
  }
340
- /** Waits before retry number `attempt + 1`, honouring server hints. */
353
+ /**
354
+ * Waits before retry number `attempt + 1`, honouring server hints. A server hint longer than
355
+ * `MAX_SERVER_WAIT_MS` is not waited: `err` is thrown at once (it still carries the hint).
356
+ */
341
357
  async backoff(op2, info, attempt, err, idempotencyKey, signal) {
358
+ const hint = serverHintMs(err);
359
+ if (hint !== null && hint > MAX_SERVER_WAIT_MS) throw err;
342
360
  const delayMs = this.retryDelay(attempt, err);
343
361
  this.config.onRetry?.({
344
362
  operation: op2,
@@ -353,8 +371,8 @@ var Transport = class {
353
371
  }
354
372
  /** Full-jitter exponential backoff, or the server's hint plus a little jitter. */
355
373
  retryDelay(attempt, err) {
356
- const hint = err instanceof RateLimitError ? err.retryAfterMs : err instanceof CexyApiError ? retryAfterMs(void 0, err.details) : null;
357
- if (hint !== null && hint > 0) return Math.ceil(hint + this.config.random() * 250);
374
+ const hint = serverHintMs(err);
375
+ if (hint !== null && hint > 0 && hint <= MAX_SERVER_WAIT_MS) return Math.ceil(hint + this.config.random() * 250);
358
376
  const cap = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** attempt);
359
377
  return Math.ceil(this.config.random() * cap);
360
378
  }
@@ -369,7 +387,7 @@ var Transport = class {
369
387
  body = JSON.stringify(spec.body);
370
388
  headers.set("Content-Type", "application/json");
371
389
  }
372
- if (info.method !== "GET" && spec.idempotencyKey) headers.set("Idempotency-Key", spec.idempotencyKey);
390
+ if (IDEMPOTENT_OPS.has(spec.op) && spec.idempotencyKey) headers.set("Idempotency-Key", spec.idempotencyKey);
373
391
  if (info.auth === "api_key") {
374
392
  const auth = this.config.authenticator;
375
393
  if (!auth) {
@@ -413,7 +431,7 @@ var Transport = class {
413
431
  if (res.type === "opaqueredirect" || res.status >= 300 && res.status < 400 || res.redirected) {
414
432
  throw new CexyApiError({
415
433
  status: res.status,
416
- code: "UNEXPECTED_REDIRECT",
434
+ code: CLIENT_ERROR_CODES.UNEXPECTED_REDIRECT,
417
435
  message: `${info.method} ${info.path}: the server answered with a redirect (HTTP ${res.status}); the SDK does not follow redirects. Check baseUrl.`,
418
436
  retryable: false
419
437
  });
@@ -452,6 +470,11 @@ var Transport = class {
452
470
  return this.config.authenticator ? this.config.authenticator.redact(text) : text;
453
471
  }
454
472
  };
473
+ function serverHintMs(err) {
474
+ if (err instanceof RateLimitError) return err.retryAfterMs;
475
+ if (err instanceof CexyApiError) return retryAfterMs(void 0, err.details);
476
+ return null;
477
+ }
455
478
  function isRetryable(err) {
456
479
  if (err instanceof CexyConnectionError) return true;
457
480
  if (err instanceof CexyApiError) return err.retryable || err.code === "CONCURRENT_MODIFICATION";
@@ -483,6 +506,7 @@ function errMessage(err) {
483
506
  }
484
507
 
485
508
  // src/limiter.ts
509
+ var MAX_BLOCK_MS = 12e4;
486
510
  var RateLimiter = class {
487
511
  #rpm;
488
512
  #tokens;
@@ -516,14 +540,14 @@ var RateLimiter = class {
516
540
  return;
517
541
  }
518
542
  const msPerToken = 6e4 / this.#rpm;
519
- await this.#sleep(Math.ceil((1 - this.#tokens) * msPerToken), signal);
543
+ await this.#sleep(Math.min(MAX_BLOCK_MS, Math.ceil((1 - this.#tokens) * msPerToken)), signal);
520
544
  }
521
545
  }
522
546
  /** Adapts to the server's rate-limit headers. Never raises the configured limit. */
523
547
  update(headers) {
524
548
  this.#refill();
525
549
  const limit = num(headers.get("x-ratelimit-limit"));
526
- if (limit !== null && limit > 0 && limit < this.#rpm) {
550
+ if (limit !== null && limit >= 1 && limit < this.#rpm) {
527
551
  this.#rpm = limit;
528
552
  this.#tokens = Math.min(this.#tokens, limit);
529
553
  }
@@ -535,10 +559,13 @@ var RateLimiter = class {
535
559
  else this.blockFor(6e4 / this.#rpm);
536
560
  }
537
561
  }
538
- /** Blocks all requests for `ms` (used for 429 Retry-After). */
562
+ /**
563
+ * Blocks all requests for `ms` (used for 429 Retry-After and `X-RateLimit-Reset`). Server
564
+ * hints are untrusted: non-finite values are ignored and the block is capped at 120 s.
565
+ */
539
566
  blockFor(ms) {
540
- if (!(ms > 0)) return;
541
- this.#blockedUntil = Math.max(this.#blockedUntil, this.#now() + ms);
567
+ if (!(ms > 0) || !Number.isFinite(ms)) return;
568
+ this.#blockedUntil = Math.max(this.#blockedUntil, this.#now() + Math.min(ms, MAX_BLOCK_MS));
542
569
  }
543
570
  #refill() {
544
571
  const now = this.#now();
@@ -788,6 +815,14 @@ var WalletResource = class extends Resource {
788
815
  return this.data({ op: "deposit_address", query: params }, opts);
789
816
  }
790
817
  };
818
+ var CANCEL_RETRY_CODES = /* @__PURE__ */ new Set(["INVALID_STATE", "SERVICE_UNAVAILABLE"]);
819
+ var CANCEL_BACKOFF_S = [1, 2, 4, 8, 15];
820
+ function errorCode(err) {
821
+ if (err instanceof CexyApiError) return err.code;
822
+ if (err instanceof CexyTimeoutError) return "TIMEOUT";
823
+ if (err instanceof CexyConnectionError) return "CONNECTION_ERROR";
824
+ return "ERROR";
825
+ }
791
826
  var ORDER_AMOUNT_FIELDS = ["price", "quantity", "quote_quantity", "stop_price"];
792
827
  var TradingResource = class extends Resource {
793
828
  /** Open orders, optionally filtered by market/status. */
@@ -818,7 +853,7 @@ var TradingResource = class extends Resource {
818
853
  *
819
854
  * Retry safety rests on `client_order_id` (generated as a UUID when absent): it is unique
820
855
  * per account and the server refuses a repeat before any funds move. The server does NOT
821
- * honour `Idempotency-Key` on orders (the header is sent but gives no protection). After an
856
+ * honour `Idempotency-Key` on orders, so none is sent. After an
822
857
  * ambiguous failure (network error, timeout or 5xx) the SDK first looks the order up by
823
858
  * `client_order_id` and returns it if it exists (`recovered: true`); only if it does not
824
859
  * exist does it send the order again, with the same `client_order_id`, so a late-arriving
@@ -833,12 +868,11 @@ var TradingResource = class extends Resource {
833
868
  assertAmountFields(order, ORDER_AMOUNT_FIELDS, "placeOrder");
834
869
  const clientOrderId = order.client_order_id ?? newId();
835
870
  const body = { ...order, client_order_id: clientOrderId };
836
- const idempotencyKey = opts.idempotencyKey ?? newId();
837
871
  const maxRetries = opts.maxRetries ?? this.t.config.maxRetries;
838
872
  const info = OPERATIONS.place_order;
839
873
  for (let attempt = 0; ; attempt++) {
840
874
  try {
841
- const raw = await this.t.attempt({ op: "place_order", body, idempotencyKey }, opts);
875
+ const raw = await this.t.attempt({ op: "place_order", body }, opts);
842
876
  const data = raw.data.data;
843
877
  return { ...data, client_order_id: clientOrderId, recovered: false };
844
878
  } catch (err) {
@@ -848,11 +882,11 @@ var TradingResource = class extends Resource {
848
882
  const existing = await this.#lookup(clientOrderId, err, opts);
849
883
  if (existing) return { order: existing, fills: [], client_order_id: clientOrderId, recovered: true };
850
884
  if (duplicateAfterRetry || attempt >= maxRetries) throw err;
851
- await this.t.backoff("place_order", info, attempt, err, idempotencyKey, opts.signal);
885
+ await this.t.backoff("place_order", info, attempt, err, void 0, opts.signal);
852
886
  continue;
853
887
  }
854
888
  if (isRetryable(err) && attempt < maxRetries) {
855
- await this.t.backoff("place_order", info, attempt, err, idempotencyKey, opts.signal);
889
+ await this.t.backoff("place_order", info, attempt, err, void 0, opts.signal);
856
890
  continue;
857
891
  }
858
892
  throw err;
@@ -876,10 +910,9 @@ var TradingResource = class extends Resource {
876
910
  async cancelOrder(orderId, opts = {}) {
877
911
  const maxRetries = opts.maxRetries ?? this.t.config.maxRetries;
878
912
  const info = OPERATIONS.cancel_order;
879
- const idempotencyKey = opts.idempotencyKey ?? newId();
880
913
  for (let attempt = 0; ; attempt++) {
881
914
  try {
882
- const raw = await this.t.attempt({ op: "cancel_order", pathParams: { order_id: orderId }, idempotencyKey }, opts);
915
+ const raw = await this.t.attempt({ op: "cancel_order", pathParams: { order_id: orderId } }, opts);
883
916
  return raw.data.data;
884
917
  } catch (err) {
885
918
  if (opts.signal?.aborted) throw err;
@@ -887,23 +920,13 @@ var TradingResource = class extends Resource {
887
920
  return this.order(orderId, { signal: opts.signal, timeoutMs: opts.timeoutMs });
888
921
  }
889
922
  if (isRetryable(err) && attempt < maxRetries) {
890
- await this.t.backoff("cancel_order", info, attempt, err, idempotencyKey, opts.signal);
923
+ await this.t.backoff("cancel_order", info, attempt, err, void 0, opts.signal);
891
924
  continue;
892
925
  }
893
926
  throw err;
894
927
  }
895
928
  }
896
929
  }
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
930
  async cancelAll(params, opts) {
908
931
  const hasSymbol = !!params && typeof params === "object" && Object.prototype.hasOwnProperty.call(params, "symbol");
909
932
  const symbol = hasSymbol ? params.symbol : void 0;
@@ -911,12 +934,97 @@ var TradingResource = class extends Resource {
911
934
  if (typeof symbol === "string" && symbol !== "") body = { symbol };
912
935
  else if (hasSymbol && symbol === null) body = {};
913
936
  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);
937
+ const once = (o) => this.data({ op: "cancel_all", body }, o ?? opts);
938
+ if (params.untilDone !== true) return once();
939
+ const maxRounds = params.maxRounds ?? 20;
940
+ const budgetMs = params.timeBudgetMs ?? 12e4;
941
+ if (!(maxRounds >= 1)) throw new CexyConfigError("cancelAll(): maxRounds must be >= 1");
942
+ if (!(budgetMs > 0)) throw new CexyConfigError("cancelAll(): timeBudgetMs must be > 0");
943
+ const now = this.t.config.now ?? Date.now;
944
+ const start = now();
945
+ const state = /* @__PURE__ */ new Map();
946
+ const roundOpts = { ...opts, maxRetries: 0 };
947
+ let rounds = 0;
948
+ let idle = 0;
949
+ let hasMore = false;
950
+ let lastErrorCode;
951
+ let stopped;
952
+ const result = () => {
953
+ const pick = (list) => [...state].filter(([, v]) => v.list === list).map(([id]) => id);
954
+ return {
955
+ cancelled: pick("cancelled"),
956
+ already_closed: pick("already_closed"),
957
+ failed: pick("failed"),
958
+ failures: [...state.values()].flatMap((v) => v.list === "failed" && v.failure ? [v.failure] : []),
959
+ has_more: hasMore,
960
+ rounds,
961
+ stopped,
962
+ ...lastErrorCode !== void 0 ? { last_error_code: lastErrorCode } : {}
963
+ };
964
+ };
965
+ for (; ; ) {
966
+ let waitMs = 0;
967
+ let res;
968
+ try {
969
+ res = await once(roundOpts);
970
+ } catch (err) {
971
+ rounds++;
972
+ if (opts?.signal?.aborted) throw err;
973
+ if (!isRetryable(err)) {
974
+ stopped = "done";
975
+ throw new CancelAllInterruptedError(err, result());
976
+ }
977
+ lastErrorCode = errorCode(err);
978
+ if (rounds >= maxRounds) {
979
+ stopped = "max_rounds";
980
+ break;
981
+ }
982
+ const hint = err instanceof RateLimitError ? err.retryAfterMs : null;
983
+ if (hint !== null) {
984
+ waitMs = hint;
985
+ if (hint > MAX_SERVER_WAIT_MS) {
986
+ stopped = "time_budget";
987
+ break;
988
+ }
989
+ } else {
990
+ waitMs = (CANCEL_BACKOFF_S[Math.min(idle++, CANCEL_BACKOFF_S.length - 1)] ?? 15) * 1e3;
991
+ }
992
+ }
993
+ if (res) {
994
+ rounds++;
995
+ lastErrorCode = void 0;
996
+ hasMore = res.has_more;
997
+ const failures = res.failures ?? [];
998
+ for (const id of res.cancelled ?? []) state.set(id, { list: "cancelled" });
999
+ for (const id of res.already_closed ?? []) state.set(id, { list: "already_closed" });
1000
+ for (const id of res.failed ?? []) {
1001
+ const failure = failures.find((f) => f.order_id === id);
1002
+ state.set(id, failure ? { list: "failed", failure } : { list: "failed" });
1003
+ }
1004
+ const progress = (res.cancelled?.length ?? 0) + (res.already_closed?.length ?? 0) > 0;
1005
+ if (!res.has_more && !failures.some((f) => CANCEL_RETRY_CODES.has(f.code))) {
1006
+ stopped = "done";
1007
+ break;
1008
+ }
1009
+ if (rounds >= maxRounds) {
1010
+ stopped = "max_rounds";
1011
+ break;
1012
+ }
1013
+ if (progress) idle = 0;
1014
+ else waitMs = (CANCEL_BACKOFF_S[Math.min(idle++, CANCEL_BACKOFF_S.length - 1)] ?? 15) * 1e3;
1015
+ }
1016
+ if (now() - start + waitMs >= budgetMs) {
1017
+ stopped = "time_budget";
1018
+ break;
1019
+ }
1020
+ if (waitMs > 0) await this.t.config.sleep(waitMs, opts?.signal);
1021
+ }
1022
+ return result();
915
1023
  }
916
1024
  };
917
1025
 
918
1026
  // src/version.ts
919
- var VERSION = "0.1.0-dev.3";
1027
+ var VERSION = "0.1.0-dev.5";
920
1028
  var USER_AGENT = `cexy-typescript/${VERSION}`;
921
1029
 
922
1030
  // src/ws/emitter.ts
@@ -1684,7 +1792,8 @@ var CexyClient = class {
1684
1792
  const sleep2 = options.sleep ?? sleep;
1685
1793
  const limiter = options.rateLimit === false ? null : new RateLimiter({
1686
1794
  requestsPerMinute: options.rateLimit?.requestsPerMinute ?? (authenticator ? DEFAULT_RPM_WITH_KEY : DEFAULT_RPM_ANONYMOUS),
1687
- sleep: sleep2
1795
+ sleep: sleep2,
1796
+ ...options.now ? { now: options.now } : {}
1688
1797
  });
1689
1798
  this.#baseUrl = baseUrl;
1690
1799
  this.#transport = new Transport({
@@ -1697,6 +1806,7 @@ var CexyClient = class {
1697
1806
  userAgent: canSetUserAgent() ? [USER_AGENT, options.userAgentSuffix].filter(Boolean).join(" ") : null,
1698
1807
  sleep: sleep2,
1699
1808
  random: options.random ?? Math.random,
1809
+ now: options.now ?? Date.now,
1700
1810
  onRetry: options.onRetry
1701
1811
  });
1702
1812
  const t = this.#transport;
@@ -1766,4 +1876,4 @@ function canSetUserAgent() {
1766
1876
  return !(typeof g.window !== "undefined" && typeof g.window.document !== "undefined");
1767
1877
  }
1768
1878
 
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 };
1879
+ export { AccountResource, ApiKeyAuthenticator, AssetsResource, AuthenticationError, CLIENT_ERROR_CODES, CancelAllInterruptedError, 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, MAX_SERVER_WAIT_MS, 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.5",
4
4
  "description": "Official TypeScript/JavaScript SDK for the CEXY.io REST and WebSocket API",
5
5
  "license": "MIT",
6
6
  "author": "CEXY.io",