@usherlabs/cex-broker 0.2.13 → 0.2.14

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.
@@ -330285,15 +330285,11 @@ function createExecuteActionHandler(deps) {
330285
330285
  var grpc13 = __toESM(require_src3(), 1);
330286
330286
 
330287
330287
  // src/helpers/binance-user-data-stream.ts
330288
+ import { Buffer as Buffer2 } from "node:buffer";
330288
330289
  import { createHmac } from "node:crypto";
330289
330290
  var BINANCE_SPOT_WS_API_URL = "wss://ws-api.binance.com:443/ws-api/v3";
330290
- function getWebSocketConstructor() {
330291
- const WebSocketCtor = globalThis.WebSocket;
330292
- if (!WebSocketCtor) {
330293
- throw new Error("WebSocket is not available in this runtime");
330294
- }
330295
- return WebSocketCtor;
330296
- }
330291
+ var DEFAULT_BINANCE_USER_DATA_MAX_BUFFERED_EVENTS = 16;
330292
+ var createWebSocket = (url3) => new wrapper_default(url3);
330297
330293
  function getExchangeString(exchange, key) {
330298
330294
  const value = exchange[key];
330299
330295
  if (typeof value !== "string" || value.length === 0) {
@@ -330319,24 +330315,106 @@ function getBinanceSpotWsApiUrl(exchange) {
330319
330315
  const urls = exchange.urls;
330320
330316
  return urls?.api?.ws?.["ws-api"]?.spot ?? BINANCE_SPOT_WS_API_URL;
330321
330317
  }
330318
+ function getRecord(value) {
330319
+ return typeof value === "object" && value !== null ? value : null;
330320
+ }
330321
+ function getMessage(value) {
330322
+ if (value instanceof Error) {
330323
+ return value.message;
330324
+ }
330325
+ if (typeof value === "string" && value.length > 0) {
330326
+ return value;
330327
+ }
330328
+ const record2 = getRecord(value);
330329
+ const message = record2?.message;
330330
+ return typeof message === "string" && message.length > 0 ? message : null;
330331
+ }
330332
+ function getOptionalExchangeString(exchange, key) {
330333
+ const value = exchange[key];
330334
+ return typeof value === "string" && value.length > 0 ? value : null;
330335
+ }
330336
+ function redactDiagnosticMessage(message, secretValues) {
330337
+ let redacted = message;
330338
+ for (const value of secretValues) {
330339
+ if (value.length > 0) {
330340
+ redacted = redacted.split(value).join("[redacted]");
330341
+ }
330342
+ }
330343
+ return redacted.replace(/(\b(?:apiKey|secret|signature)\b\s*=\s*)[^\s&,;)]+/gi, "$1[redacted]").replace(/("(?:apiKey|secret|signature)"\s*:\s*")[^"]*(")/gi, "$1[redacted]$2");
330344
+ }
330345
+ function formatBinanceUserDataWebSocketError(event, secretValues) {
330346
+ const record2 = getRecord(event);
330347
+ const message = getMessage(record2?.error) ?? getMessage(record2?.message) ?? getMessage(event);
330348
+ const safeMessage = message === null ? null : redactDiagnosticMessage(message, secretValues);
330349
+ return new Error(safeMessage ? `Binance user-data WebSocket error: ${safeMessage}` : "Binance user-data WebSocket error");
330350
+ }
330351
+ function getCloseReason(value) {
330352
+ if (typeof value === "string") {
330353
+ return value.length > 0 ? value : null;
330354
+ }
330355
+ if (Buffer2.isBuffer(value)) {
330356
+ const reason = value.toString("utf8");
330357
+ return reason.length > 0 ? reason : null;
330358
+ }
330359
+ if (value instanceof Uint8Array) {
330360
+ const reason = Buffer2.from(value).toString("utf8");
330361
+ return reason.length > 0 ? reason : null;
330362
+ }
330363
+ return null;
330364
+ }
330365
+ function formatBinanceUserDataWebSocketClose(codeOrEvent, reasonOrUndefined, secretValues) {
330366
+ const record2 = getRecord(codeOrEvent);
330367
+ const code = record2 ? record2.code : codeOrEvent;
330368
+ const reason = getCloseReason(record2 ? record2.reason : reasonOrUndefined);
330369
+ const safeReason = reason === null ? null : redactDiagnosticMessage(reason, secretValues);
330370
+ const details = [
330371
+ typeof code === "number" || typeof code === "string" ? `code=${code}` : null,
330372
+ safeReason ? `reason=${safeReason}` : null
330373
+ ].filter((detail) => detail !== null);
330374
+ return new Error(details.length > 0 ? `Binance user-data WebSocket closed unexpectedly (${details.join(", ")})` : "Binance user-data WebSocket closed unexpectedly");
330375
+ }
330376
+ function decodeMessageData(data) {
330377
+ if (typeof data === "string") {
330378
+ return data;
330379
+ }
330380
+ if (Buffer2.isBuffer(data)) {
330381
+ return data.toString("utf8");
330382
+ }
330383
+ if (data instanceof ArrayBuffer) {
330384
+ return Buffer2.from(data).toString("utf8");
330385
+ }
330386
+ if (ArrayBuffer.isView(data)) {
330387
+ return Buffer2.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
330388
+ }
330389
+ if (Array.isArray(data) && data.every((item) => Buffer2.isBuffer(item))) {
330390
+ return Buffer2.concat(data).toString("utf8");
330391
+ }
330392
+ return data;
330393
+ }
330322
330394
 
330323
330395
  class BinanceSpotUserDataStream {
330324
330396
  exchange;
330325
330397
  ws;
330398
+ secretValues;
330326
330399
  requestId = `user-data-${Date.now()}-${Math.random()}`;
330400
+ maxBufferedEvents;
330327
330401
  queue = [];
330328
330402
  waiters = [];
330329
330403
  closed = false;
330330
330404
  closeError = null;
330331
330405
  subscriptionId = null;
330332
- constructor(exchange) {
330406
+ constructor(exchange, options = {}) {
330333
330407
  this.exchange = exchange;
330334
- const WebSocketCtor = getWebSocketConstructor();
330335
- this.ws = new WebSocketCtor(getBinanceSpotWsApiUrl(exchange));
330336
- this.ws.onopen = () => this.subscribe();
330337
- this.ws.onmessage = (message) => this.handleMessage(message.data);
330338
- this.ws.onerror = (error48) => this.fail(error48 instanceof Error ? error48 : new Error("Binance user-data WebSocket error"));
330339
- this.ws.onclose = () => this.close();
330408
+ this.maxBufferedEvents = options.maxBufferedEvents ?? DEFAULT_BINANCE_USER_DATA_MAX_BUFFERED_EVENTS;
330409
+ this.secretValues = [
330410
+ getOptionalExchangeString(exchange, "apiKey"),
330411
+ getOptionalExchangeString(exchange, "secret")
330412
+ ].filter((value) => value !== null);
330413
+ this.ws = createWebSocket(getBinanceSpotWsApiUrl(exchange));
330414
+ this.ws.on("open", () => this.subscribe());
330415
+ this.ws.on("message", (data) => this.handleMessage(data));
330416
+ this.ws.on("error", (error48) => this.fail(formatBinanceUserDataWebSocketError(error48, this.secretValues)));
330417
+ this.ws.on("close", (code, reason) => this.handleClose(code, reason));
330340
330418
  }
330341
330419
  async* [Symbol.asyncIterator]() {
330342
330420
  while (true) {
@@ -330352,11 +330430,18 @@ class BinanceSpotUserDataStream {
330352
330430
  return;
330353
330431
  }
330354
330432
  this.closed = true;
330433
+ this.queue.length = 0;
330355
330434
  try {
330356
330435
  this.ws.close();
330357
330436
  } catch {}
330358
330437
  this.flushWaiters();
330359
330438
  }
330439
+ handleClose(code, reason) {
330440
+ if (this.closed) {
330441
+ return;
330442
+ }
330443
+ this.fail(formatBinanceUserDataWebSocketClose(code, reason, this.secretValues));
330444
+ }
330360
330445
  subscribe() {
330361
330446
  const apiKey = getExchangeString(this.exchange, "apiKey");
330362
330447
  const signedParams = signUserDataStreamParams(this.exchange, {
@@ -330370,9 +330455,13 @@ class BinanceSpotUserDataStream {
330370
330455
  }));
330371
330456
  }
330372
330457
  handleMessage(data) {
330458
+ if (this.closed) {
330459
+ return;
330460
+ }
330373
330461
  let message;
330374
330462
  try {
330375
- message = typeof data === "string" ? JSON.parse(data) : data;
330463
+ const decodedData = decodeMessageData(data);
330464
+ message = typeof decodedData === "string" ? JSON.parse(decodedData) : decodedData;
330376
330465
  } catch (error48) {
330377
330466
  this.fail(error48 instanceof Error ? error48 : new Error("Invalid Binance user-data message"));
330378
330467
  return;
@@ -330395,11 +330484,18 @@ class BinanceSpotUserDataStream {
330395
330484
  this.push({ subscriptionId, event: message.event });
330396
330485
  }
330397
330486
  push(event) {
330487
+ if (this.closed) {
330488
+ return;
330489
+ }
330398
330490
  const waiter = this.waiters.shift();
330399
330491
  if (waiter) {
330400
330492
  waiter.resolve(event);
330401
330493
  return;
330402
330494
  }
330495
+ if (this.queue.length >= this.maxBufferedEvents) {
330496
+ this.fail(new Error(`Binance user-data stream buffered event limit exceeded (${this.maxBufferedEvents}); downstream consumer is not keeping up`));
330497
+ return;
330498
+ }
330403
330499
  this.queue.push(event);
330404
330500
  }
330405
330501
  nextEvent() {
@@ -330423,6 +330519,7 @@ class BinanceSpotUserDataStream {
330423
330519
  }
330424
330520
  this.closeError = error48;
330425
330521
  this.closed = true;
330522
+ this.queue.length = 0;
330426
330523
  this.flushWaiters();
330427
330524
  try {
330428
330525
  this.ws.close();
@@ -330450,16 +330547,49 @@ function isBinanceOrderUserDataEvent(event) {
330450
330547
  function isBinanceSpotAccountSubscription(cex3, subscriptionType, marketTypeInput) {
330451
330548
  return cex3 === "binance" && parseMarketType(marketTypeInput) === "spot" && (subscriptionType === SubscriptionType.BALANCE || subscriptionType === SubscriptionType.ORDERS);
330452
330549
  }
330453
- function writeSubscribeFrame(call, isClosed, frame) {
330550
+ function waitForSubscribeDrain(call, isClosed) {
330551
+ if (isClosed() || call.destroyed) {
330552
+ return Promise.resolve(false);
330553
+ }
330554
+ return new Promise((resolve) => {
330555
+ let settled = false;
330556
+ const cleanup = () => {
330557
+ call.off("drain", onDrain);
330558
+ call.off("close", onClosed);
330559
+ call.off("cancelled", onClosed);
330560
+ call.off("end", onClosed);
330561
+ call.off("error", onClosed);
330562
+ };
330563
+ const settle2 = (drained) => {
330564
+ if (settled) {
330565
+ return;
330566
+ }
330567
+ settled = true;
330568
+ cleanup();
330569
+ resolve(drained && !isClosed() && !call.destroyed);
330570
+ };
330571
+ const onDrain = () => settle2(true);
330572
+ const onClosed = () => settle2(false);
330573
+ call.once("drain", onDrain);
330574
+ call.once("close", onClosed);
330575
+ call.once("cancelled", onClosed);
330576
+ call.once("end", onClosed);
330577
+ call.once("error", onClosed);
330578
+ });
330579
+ }
330580
+ async function writeSubscribeFrame(call, isClosed, frame) {
330454
330581
  if (isClosed() || call.destroyed) {
330455
330582
  return false;
330456
330583
  }
330457
- call.write(frame);
330458
- return true;
330584
+ const canContinue = call.write(frame);
330585
+ if (canContinue) {
330586
+ return true;
330587
+ }
330588
+ return waitForSubscribeDrain(call, isClosed);
330459
330589
  }
330460
- function writeSubscribeError(call, isClosed, frame) {
330461
- writeSubscribeFrame(call, isClosed, frame);
330462
- if (!isClosed() && !call.destroyed) {
330590
+ async function writeSubscribeError(call, isClosed, frame) {
330591
+ const frameWritten = await writeSubscribeFrame(call, isClosed, frame);
330592
+ if (frameWritten && !isClosed() && !call.destroyed) {
330463
330593
  call.end();
330464
330594
  }
330465
330595
  }
@@ -330506,7 +330636,7 @@ async function streamBinanceUserData(call, broker, symbol2, subscriptionType, is
330506
330636
  continue;
330507
330637
  }
330508
330638
  }
330509
- if (!writeSubscribeFrame(call, isClosed, {
330639
+ if (!await writeSubscribeFrame(call, isClosed, {
330510
330640
  data: JSON.stringify({
330511
330641
  subscriptionId: message.subscriptionId,
330512
330642
  event
@@ -330525,7 +330655,7 @@ async function streamBinanceUserData(call, broker, symbol2, subscriptionType, is
330525
330655
  async function runCcxtSubscribeLoop(call, isClosed, symbol2, subscriptionType, watch) {
330526
330656
  while (!isClosed()) {
330527
330657
  const data = await watch();
330528
- if (!writeSubscribeFrame(call, isClosed, {
330658
+ if (!await writeSubscribeFrame(call, isClosed, {
330529
330659
  data: JSON.stringify(data),
330530
330660
  timestamp: Date.now(),
330531
330661
  symbol: symbol2,
@@ -330591,7 +330721,7 @@ function createSubscribeHandler(deps) {
330591
330721
  type: subscriptionTypeName
330592
330722
  });
330593
330723
  if (!cex3 || !symbol2) {
330594
- writeSubscribeError(call, isStreamClosed, {
330724
+ await writeSubscribeError(call, isStreamClosed, {
330595
330725
  data: JSON.stringify({
330596
330726
  error: "cex, symbol, and type are required"
330597
330727
  }),
@@ -330605,7 +330735,7 @@ function createSubscribeHandler(deps) {
330605
330735
  const selectedBroker = selectBroker(brokers[normalizedCex], metadata) ?? createBroker(normalizedCex, metadata);
330606
330736
  const broker = selectedBroker ?? createPublicBroker(normalizedCex);
330607
330737
  if (!broker) {
330608
- writeSubscribeError(call, isStreamClosed, {
330738
+ await writeSubscribeError(call, isStreamClosed, {
330609
330739
  data: JSON.stringify({
330610
330740
  error: "Exchange not registered and no API metadata found"
330611
330741
  }),
@@ -330618,7 +330748,7 @@ function createSubscribeHandler(deps) {
330618
330748
  const resolvedSymbol = await resolveSubscriptionSymbol(broker, symbol2, options?.marketType);
330619
330749
  if (isBinanceSpotAccountSubscription(normalizedCex, subscriptionType, options?.marketType)) {
330620
330750
  if (!selectedBroker) {
330621
- writeSubscribeError(call, isStreamClosed, {
330751
+ await writeSubscribeError(call, isStreamClosed, {
330622
330752
  data: JSON.stringify({
330623
330753
  error: "Binance account subscriptions require API credentials"
330624
330754
  }),
@@ -330638,7 +330768,7 @@ function createSubscribeHandler(deps) {
330638
330768
  const depthLimit = parseOptionalDepthLimit(options?.depthLimit ?? options?.limit);
330639
330769
  const orderbook = depthLimit === undefined ? await broker.watchOrderBook(resolvedSymbol) : await broker.watchOrderBook(resolvedSymbol, depthLimit);
330640
330770
  const receivedTimestamp = Date.now();
330641
- if (!writeSubscribeFrame(call, isStreamClosed, {
330771
+ if (!await writeSubscribeFrame(call, isStreamClosed, {
330642
330772
  data: JSON.stringify(normalizeOrderBookSnapshot(orderbook, {
330643
330773
  exchange: normalizedCex,
330644
330774
  symbol: resolvedSymbol,
@@ -330655,7 +330785,7 @@ function createSubscribeHandler(deps) {
330655
330785
  } catch (error48) {
330656
330786
  log.error(`Error fetching orderbook for ${resolvedSymbol} on ${cex3}:`, error48);
330657
330787
  const message = getErrorMessage(error48);
330658
- writeSubscribeError(call, isStreamClosed, {
330788
+ await writeSubscribeError(call, isStreamClosed, {
330659
330789
  data: JSON.stringify({
330660
330790
  error: `Failed to fetch orderbook: ${message}`
330661
330791
  }),
@@ -330671,7 +330801,7 @@ function createSubscribeHandler(deps) {
330671
330801
  } catch (error48) {
330672
330802
  const message = getErrorMessage(error48);
330673
330803
  log.error(`Error fetching trades for ${resolvedSymbol} on ${cex3}:`, error48);
330674
- writeSubscribeError(call, isStreamClosed, {
330804
+ await writeSubscribeError(call, isStreamClosed, {
330675
330805
  data: JSON.stringify({
330676
330806
  error: `Failed to fetch trades: ${message}`
330677
330807
  }),
@@ -330687,7 +330817,7 @@ function createSubscribeHandler(deps) {
330687
330817
  } catch (error48) {
330688
330818
  const message = getErrorMessage(error48);
330689
330819
  log.error(`Error fetching ticker for ${resolvedSymbol} on ${cex3}:`, error48);
330690
- writeSubscribeError(call, isStreamClosed, {
330820
+ await writeSubscribeError(call, isStreamClosed, {
330691
330821
  data: JSON.stringify({
330692
330822
  error: `Failed to fetch ticker: ${message}`
330693
330823
  }),
@@ -330704,7 +330834,7 @@ function createSubscribeHandler(deps) {
330704
330834
  } catch (error48) {
330705
330835
  log.error(`Error fetching OHLCV for ${resolvedSymbol} on ${cex3}:`, error48);
330706
330836
  const message = getErrorMessage(error48);
330707
- writeSubscribeError(call, isStreamClosed, {
330837
+ await writeSubscribeError(call, isStreamClosed, {
330708
330838
  data: JSON.stringify({
330709
330839
  error: `Failed to fetch OHLCV: ${message}`
330710
330840
  }),
@@ -330720,7 +330850,7 @@ function createSubscribeHandler(deps) {
330720
330850
  } catch (error48) {
330721
330851
  const message = getErrorMessage(error48);
330722
330852
  log.error(`Error fetching balance for ${cex3}:`, error48);
330723
- writeSubscribeError(call, isStreamClosed, {
330853
+ await writeSubscribeError(call, isStreamClosed, {
330724
330854
  data: JSON.stringify({
330725
330855
  error: `Failed to fetch balance: ${message}`
330726
330856
  }),
@@ -330736,7 +330866,7 @@ function createSubscribeHandler(deps) {
330736
330866
  } catch (error48) {
330737
330867
  log.error(`Error fetching orders for ${resolvedSymbol} on ${cex3}:`, error48);
330738
330868
  const message = getErrorMessage(error48);
330739
- writeSubscribeError(call, isStreamClosed, {
330869
+ await writeSubscribeError(call, isStreamClosed, {
330740
330870
  data: JSON.stringify({
330741
330871
  error: `Failed to fetch orders: ${message}`
330742
330872
  }),
@@ -330747,7 +330877,7 @@ function createSubscribeHandler(deps) {
330747
330877
  }
330748
330878
  break;
330749
330879
  default:
330750
- writeSubscribeError(call, isStreamClosed, {
330880
+ await writeSubscribeError(call, isStreamClosed, {
330751
330881
  data: JSON.stringify({ error: "Invalid subscription type" }),
330752
330882
  timestamp: Date.now(),
330753
330883
  symbol: symbol2,
@@ -330757,7 +330887,7 @@ function createSubscribeHandler(deps) {
330757
330887
  } catch (error48) {
330758
330888
  log.error("Error in Subscribe stream:", error48);
330759
330889
  const message = getErrorMessage(error48);
330760
- writeSubscribeError(call, isStreamClosed, {
330890
+ await writeSubscribeError(call, isStreamClosed, {
330761
330891
  data: JSON.stringify({ error: `Internal server error: ${message}` }),
330762
330892
  timestamp: Date.now(),
330763
330893
  symbol: "",
@@ -1,22 +1,39 @@
1
1
  import type { Exchange } from "@usherlabs/ccxt";
2
2
  export declare const BINANCE_SPOT_WS_API_URL = "wss://ws-api.binance.com:443/ws-api/v3";
3
+ export declare const DEFAULT_BINANCE_USER_DATA_MAX_BUFFERED_EVENTS = 16;
3
4
  export type BinanceUserDataEvent = {
4
5
  subscriptionId: number;
5
6
  event: Record<string, unknown>;
6
7
  };
8
+ type WebSocketLike = {
9
+ on(event: "open", listener: () => void): unknown;
10
+ on(event: "message", listener: (data: unknown) => void): unknown;
11
+ on(event: "error", listener: (error: unknown) => void): unknown;
12
+ on(event: "close", listener: (code: unknown, reason: unknown) => void): unknown;
13
+ send(data: string): void;
14
+ close(code?: number, reason?: string): void;
15
+ };
16
+ type WebSocketFactory = (url: string) => WebSocketLike;
17
+ type BinanceSpotUserDataStreamOptions = {
18
+ maxBufferedEvents?: number;
19
+ };
20
+ export declare function setBinanceUserDataWebSocketFactoryForTests(factory: WebSocketFactory): () => void;
7
21
  export declare function getBinanceSpotWsApiUrl(exchange: Exchange): string;
8
22
  export declare class BinanceSpotUserDataStream implements AsyncIterable<BinanceUserDataEvent> {
9
23
  private readonly exchange;
10
24
  private readonly ws;
25
+ private readonly secretValues;
11
26
  private readonly requestId;
27
+ private readonly maxBufferedEvents;
12
28
  private readonly queue;
13
29
  private readonly waiters;
14
30
  private closed;
15
31
  private closeError;
16
32
  private subscriptionId;
17
- constructor(exchange: Exchange);
33
+ constructor(exchange: Exchange, options?: BinanceSpotUserDataStreamOptions);
18
34
  [Symbol.asyncIterator](): AsyncIterator<BinanceUserDataEvent>;
19
35
  close(): void;
36
+ private handleClose;
20
37
  private subscribe;
21
38
  private handleMessage;
22
39
  private push;
@@ -26,3 +43,4 @@ export declare class BinanceSpotUserDataStream implements AsyncIterable<BinanceU
26
43
  }
27
44
  export declare function isBinanceBalanceUserDataEvent(event: Record<string, unknown>): boolean;
28
45
  export declare function isBinanceOrderUserDataEvent(event: Record<string, unknown>): boolean;
46
+ export {};