@usherlabs/cex-broker 0.2.13 → 0.2.15

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.
@@ -0,0 +1,44 @@
1
+ import type { Exchange } from "@usherlabs/ccxt";
2
+ import Joi from "joi";
3
+ import type { PolicyConfig, TravelRuleQuestionnaire } from "../types";
4
+ export declare const australiaQuestionnaireSchema: Joi.ObjectSchema<any>;
5
+ export type TravelRuleDecision = {
6
+ mode: "standard";
7
+ } | {
8
+ mode: "localentity";
9
+ questionnaire: TravelRuleQuestionnaire;
10
+ } | {
11
+ mode: "denied";
12
+ error: string;
13
+ };
14
+ /**
15
+ * Decides whether a withdrawal must use Binance's travel-rule endpoint.
16
+ *
17
+ * Travel rule is opt-in per exchange via the `enabled` flag, so a non-AU account
18
+ * keeps using the standard endpoint. When enabled, the destination address must
19
+ * have a configured questionnaire; if it does not we fail closed rather than fall
20
+ * back to the standard endpoint (which would just reproduce the -4104 rejection).
21
+ */
22
+ export declare function resolveTravelRuleDecision(policy: PolicyConfig, exchange: string, recipientAddress: string): TravelRuleDecision;
23
+ /**
24
+ * Registers Binance's `localentity/withdraw/apply` endpoint on the exchange
25
+ * instance. No-op for non-Binance exchanges. Idempotent — ccxt just reassigns
26
+ * the generated implicit method if called again.
27
+ */
28
+ export declare function registerBinanceTravelRuleWithdrawEndpoint(exchange: Exchange): void;
29
+ type LocalEntityWithdrawArgs = {
30
+ code: string;
31
+ amount: number;
32
+ address: string;
33
+ network: string;
34
+ questionnaire: TravelRuleQuestionnaire;
35
+ params?: Record<string, string | number>;
36
+ };
37
+ /**
38
+ * Mirrors ccxt's `binance.withdraw` currency/network/precision handling, but
39
+ * targets the travel-rule endpoint and attaches the questionnaire. ccxt's
40
+ * `urlencode` applies `encodeURIComponent` to the value, satisfying Binance's
41
+ * requirement that the questionnaire JSON be URL-encoded in the request body.
42
+ */
43
+ export declare function withdrawViaLocalEntity(broker: Exchange, args: LocalEntityWithdrawArgs): Promise<import("@usherlabs/ccxt").Transaction>;
44
+ export {};
package/dist/index.js CHANGED
@@ -270096,11 +270096,11 @@ var ccxt = Object.assign({ version: version2, Exchange, Precise, exchanges: Obje
270096
270096
  var ccxt_default = ccxt;
270097
270097
 
270098
270098
  // src/index.ts
270099
- var import_joi2 = __toESM(require_lib4(), 1);
270099
+ var import_joi3 = __toESM(require_lib4(), 1);
270100
270100
  import { unwatchFile, watchFile } from "fs";
270101
270101
 
270102
270102
  // src/helpers/index.ts
270103
- var import_joi = __toESM(require_lib4(), 1);
270103
+ var import_joi2 = __toESM(require_lib4(), 1);
270104
270104
  import fs from "fs";
270105
270105
 
270106
270106
  // src/helpers/exchange-credentials.ts
@@ -272784,6 +272784,81 @@ if (process.env.LOG_LEVEL !== "debug") {
272784
272784
  }
272785
272785
  var log = baseLogger;
272786
272786
 
272787
+ // src/helpers/travel-rule.ts
272788
+ var import_joi = __toESM(require_lib4(), 1);
272789
+ var isAnotherBeneficiary = import_joi.default.number().valid(2).required();
272790
+ var isIndividual = import_joi.default.number().valid(0).required();
272791
+ var isCorporate = import_joi.default.number().valid(1).required();
272792
+ var isSendToVasp = import_joi.default.number().valid(2).required();
272793
+ var isVaspOthers = import_joi.default.string().valid("others").required();
272794
+ function requiredWhen(base2, ref, is) {
272795
+ return base2.when(ref, {
272796
+ is,
272797
+ then: import_joi.default.required(),
272798
+ otherwise: import_joi.default.forbidden()
272799
+ });
272800
+ }
272801
+ var australiaQuestionnaireSchema = import_joi.default.object({
272802
+ isAddressOwner: import_joi.default.number().valid(1, 2).required(),
272803
+ sendTo: import_joi.default.number().valid(1, 2).required(),
272804
+ declaration: import_joi.default.boolean().valid(true).required(),
272805
+ bnfType: requiredWhen(import_joi.default.number().valid(0, 1), "isAddressOwner", isAnotherBeneficiary),
272806
+ bnfFirstName: requiredWhen(import_joi.default.string(), "bnfType", isIndividual),
272807
+ bnfLastName: requiredWhen(import_joi.default.string(), "bnfType", isIndividual),
272808
+ country: requiredWhen(import_joi.default.string(), "bnfType", isIndividual),
272809
+ city: requiredWhen(import_joi.default.string(), "bnfType", isIndividual),
272810
+ bnfCorpName: requiredWhen(import_joi.default.string(), "bnfType", isCorporate),
272811
+ bnfCorpCountry: requiredWhen(import_joi.default.string(), "bnfType", isCorporate),
272812
+ bnfCorpCity: requiredWhen(import_joi.default.string(), "bnfType", isCorporate),
272813
+ vasp: requiredWhen(import_joi.default.string(), "sendTo", isSendToVasp),
272814
+ vaspName: requiredWhen(import_joi.default.string(), "vasp", isVaspOthers)
272815
+ });
272816
+ function resolveTravelRuleDecision(policy, exchange, recipientAddress) {
272817
+ const rules = policy.travelRule?.rule ?? [];
272818
+ const exchangeNorm = exchange.trim().toUpperCase();
272819
+ const entry = rules.find((rule) => rule.exchange.trim().toUpperCase() === exchangeNorm);
272820
+ if (!entry || !entry.enabled) {
272821
+ return { mode: "standard" };
272822
+ }
272823
+ const addressNorm = recipientAddress.trim().toLowerCase();
272824
+ const match = Object.entries(entry.addresses).find(([address]) => address.trim().toLowerCase() === addressNorm);
272825
+ if (!match) {
272826
+ return {
272827
+ mode: "denied",
272828
+ error: `no travel-rule questionnaire configured for ${exchangeNorm} address ${recipientAddress}`
272829
+ };
272830
+ }
272831
+ return { mode: "localentity", questionnaire: match[1].questionnaire };
272832
+ }
272833
+ function registerBinanceTravelRuleWithdrawEndpoint(exchange) {
272834
+ if (exchange.id !== "binance") {
272835
+ return;
272836
+ }
272837
+ exchange.defineRestApi({ sapi: { post: { "localentity/withdraw/apply": 4.0002 } } }, "request");
272838
+ }
272839
+ async function withdrawViaLocalEntity(broker, args) {
272840
+ const exchange = broker;
272841
+ if (typeof exchange.sapiPostLocalentityWithdrawApply !== "function") {
272842
+ throw new Error("binance_localentity_withdraw_unavailable: travel-rule withdraw endpoint is not registered on this exchange instance");
272843
+ }
272844
+ broker.checkAddress(args.address);
272845
+ await broker.loadMarkets();
272846
+ const currency = broker.currency(args.code);
272847
+ const networks = broker.safeDict(broker.options, "networks", {});
272848
+ const networkUpper = args.network.trim().toUpperCase();
272849
+ const mappedNetwork = broker.safeString(networks, networkUpper, networkUpper);
272850
+ const request = {
272851
+ ...args.params,
272852
+ coin: currency.id,
272853
+ address: args.address,
272854
+ amount: broker.currencyToPrecision(args.code, args.amount),
272855
+ network: mappedNetwork,
272856
+ questionnaire: JSON.stringify(args.questionnaire)
272857
+ };
272858
+ const response = await exchange.sapiPostLocalentityWithdrawApply(request);
272859
+ return broker.parseTransaction(response, currency);
272860
+ }
272861
+
272787
272862
  // src/helpers/broker.ts
272788
272863
  class BrokerAccountPreconditionError extends Error {
272789
272864
  constructor(message) {
@@ -272808,6 +272883,7 @@ function applyCommonExchangeConfig(exchange) {
272808
272883
  recvWindow: 60000,
272809
272884
  adjustForTimeDifference: true
272810
272885
  });
272886
+ registerBinanceTravelRuleWithdrawEndpoint(exchange);
272811
272887
  }
272812
272888
  function createBroker(cex3, credsOrMetadata) {
272813
272889
  let apiKey;
@@ -273224,36 +273300,47 @@ var verityHttpClientOverridePredicate = ({
273224
273300
  function loadPolicy(policyPath) {
273225
273301
  try {
273226
273302
  const policyData = fs.readFileSync(policyPath, "utf8");
273227
- const withdrawRuleEntrySchema = import_joi.default.object({
273228
- exchange: import_joi.default.string().required(),
273229
- network: import_joi.default.string().required(),
273230
- whitelist: import_joi.default.array().items(import_joi.default.string()).required(),
273231
- coins: import_joi.default.array().items(import_joi.default.string()).optional()
273303
+ const withdrawRuleEntrySchema = import_joi2.default.object({
273304
+ exchange: import_joi2.default.string().required(),
273305
+ network: import_joi2.default.string().required(),
273306
+ whitelist: import_joi2.default.array().items(import_joi2.default.string()).required(),
273307
+ coins: import_joi2.default.array().items(import_joi2.default.string()).optional()
273232
273308
  });
273233
- const depositRuleEntrySchema = import_joi.default.object({
273234
- exchange: import_joi.default.string().required(),
273235
- network: import_joi.default.string().required(),
273236
- coins: import_joi.default.array().items(import_joi.default.string()).optional()
273309
+ const depositRuleEntrySchema = import_joi2.default.object({
273310
+ exchange: import_joi2.default.string().required(),
273311
+ network: import_joi2.default.string().required(),
273312
+ coins: import_joi2.default.array().items(import_joi2.default.string()).optional()
273237
273313
  });
273238
- const orderRuleSchema = import_joi.default.object({
273239
- markets: import_joi.default.array().items(import_joi.default.string()).required(),
273240
- limits: import_joi.default.array().items(import_joi.default.object({
273241
- from: import_joi.default.string().required(),
273242
- to: import_joi.default.string().required(),
273243
- min: import_joi.default.number().required(),
273244
- max: import_joi.default.number().required()
273314
+ const orderRuleSchema = import_joi2.default.object({
273315
+ markets: import_joi2.default.array().items(import_joi2.default.string()).required(),
273316
+ limits: import_joi2.default.array().items(import_joi2.default.object({
273317
+ from: import_joi2.default.string().required(),
273318
+ to: import_joi2.default.string().required(),
273319
+ min: import_joi2.default.number().required(),
273320
+ max: import_joi2.default.number().required()
273245
273321
  })).default([])
273246
273322
  });
273247
- const policyConfigSchema = import_joi.default.object({
273248
- withdraw: import_joi.default.object({
273249
- rule: import_joi.default.array().items(withdrawRuleEntrySchema).min(1).required()
273323
+ const travelRuleEntrySchema = import_joi2.default.object({
273324
+ exchange: import_joi2.default.string().uppercase().valid("BINANCE").required(),
273325
+ enabled: import_joi2.default.boolean().required(),
273326
+ description: import_joi2.default.string().optional(),
273327
+ addresses: import_joi2.default.object().pattern(import_joi2.default.string(), import_joi2.default.object({
273328
+ questionnaire: australiaQuestionnaireSchema.required()
273329
+ })).required()
273330
+ });
273331
+ const policyConfigSchema = import_joi2.default.object({
273332
+ withdraw: import_joi2.default.object({
273333
+ rule: import_joi2.default.array().items(withdrawRuleEntrySchema).min(1).required()
273250
273334
  }).required(),
273251
- deposit: import_joi.default.object({
273252
- rule: import_joi.default.array().items(depositRuleEntrySchema).optional()
273335
+ deposit: import_joi2.default.object({
273336
+ rule: import_joi2.default.array().items(depositRuleEntrySchema).optional()
273253
273337
  }).required(),
273254
- order: import_joi.default.object({
273338
+ order: import_joi2.default.object({
273255
273339
  rule: orderRuleSchema.required()
273256
- }).required()
273340
+ }).required(),
273341
+ travelRule: import_joi2.default.object({
273342
+ rule: import_joi2.default.array().items(travelRuleEntrySchema).required()
273343
+ }).optional()
273257
273344
  });
273258
273345
  const { error, value } = policyConfigSchema.validate(JSON.parse(policyData));
273259
273346
  if (error) {
@@ -293067,8 +293154,22 @@ async function handleWithdraw(ctx) {
293067
293154
  message: `policy_withdrawal_denied: ${transferValidation.error}`
293068
293155
  }, null);
293069
293156
  }
293157
+ const travelRule = resolveTravelRuleDecision(policy, cex3, transferValue.recipientAddress);
293158
+ if (travelRule.mode === "denied") {
293159
+ return ctx.wrappedCallback({
293160
+ code: grpc10.status.FAILED_PRECONDITION,
293161
+ message: `travel_rule_denied: ${travelRule.error}`
293162
+ }, null);
293163
+ }
293070
293164
  try {
293071
- const transaction = await broker.withdraw(symbol2, transferValue.amount, transferValue.recipientAddress, undefined, {
293165
+ const transaction = travelRule.mode === "localentity" ? await withdrawViaLocalEntity(broker, {
293166
+ code: symbol2,
293167
+ amount: transferValue.amount,
293168
+ address: transferValue.recipientAddress,
293169
+ network: withdrawNetwork.exchangeNetworkId,
293170
+ questionnaire: travelRule.questionnaire,
293171
+ params: transferValue.params
293172
+ }) : await broker.withdraw(symbol2, transferValue.amount, transferValue.recipientAddress, undefined, {
293072
293173
  ...transferValue.params ?? {},
293073
293174
  network: withdrawNetwork.exchangeNetworkId
293074
293175
  });
@@ -293238,15 +293339,12 @@ function createExecuteActionHandler(deps) {
293238
293339
  import * as grpc13 from "@grpc/grpc-js";
293239
293340
 
293240
293341
  // src/helpers/binance-user-data-stream.ts
293342
+ import { Buffer as Buffer2 } from "buffer";
293241
293343
  import { createHmac } from "crypto";
293344
+ import WebSocket2 from "ws";
293242
293345
  var BINANCE_SPOT_WS_API_URL = "wss://ws-api.binance.com:443/ws-api/v3";
293243
- function getWebSocketConstructor() {
293244
- const WebSocketCtor = globalThis.WebSocket;
293245
- if (!WebSocketCtor) {
293246
- throw new Error("WebSocket is not available in this runtime");
293247
- }
293248
- return WebSocketCtor;
293249
- }
293346
+ var DEFAULT_BINANCE_USER_DATA_MAX_BUFFERED_EVENTS = 16;
293347
+ var createWebSocket = (url3) => new WebSocket2(url3);
293250
293348
  function getExchangeString(exchange, key) {
293251
293349
  const value = exchange[key];
293252
293350
  if (typeof value !== "string" || value.length === 0) {
@@ -293272,24 +293370,106 @@ function getBinanceSpotWsApiUrl(exchange) {
293272
293370
  const urls = exchange.urls;
293273
293371
  return urls?.api?.ws?.["ws-api"]?.spot ?? BINANCE_SPOT_WS_API_URL;
293274
293372
  }
293373
+ function getRecord(value) {
293374
+ return typeof value === "object" && value !== null ? value : null;
293375
+ }
293376
+ function getMessage(value) {
293377
+ if (value instanceof Error) {
293378
+ return value.message;
293379
+ }
293380
+ if (typeof value === "string" && value.length > 0) {
293381
+ return value;
293382
+ }
293383
+ const record2 = getRecord(value);
293384
+ const message = record2?.message;
293385
+ return typeof message === "string" && message.length > 0 ? message : null;
293386
+ }
293387
+ function getOptionalExchangeString(exchange, key) {
293388
+ const value = exchange[key];
293389
+ return typeof value === "string" && value.length > 0 ? value : null;
293390
+ }
293391
+ function redactDiagnosticMessage(message, secretValues) {
293392
+ let redacted = message;
293393
+ for (const value of secretValues) {
293394
+ if (value.length > 0) {
293395
+ redacted = redacted.split(value).join("[redacted]");
293396
+ }
293397
+ }
293398
+ return redacted.replace(/(\b(?:apiKey|secret|signature)\b\s*=\s*)[^\s&,;)]+/gi, "$1[redacted]").replace(/("(?:apiKey|secret|signature)"\s*:\s*")[^"]*(")/gi, "$1[redacted]$2");
293399
+ }
293400
+ function formatBinanceUserDataWebSocketError(event, secretValues) {
293401
+ const record2 = getRecord(event);
293402
+ const message = getMessage(record2?.error) ?? getMessage(record2?.message) ?? getMessage(event);
293403
+ const safeMessage = message === null ? null : redactDiagnosticMessage(message, secretValues);
293404
+ return new Error(safeMessage ? `Binance user-data WebSocket error: ${safeMessage}` : "Binance user-data WebSocket error");
293405
+ }
293406
+ function getCloseReason(value) {
293407
+ if (typeof value === "string") {
293408
+ return value.length > 0 ? value : null;
293409
+ }
293410
+ if (Buffer2.isBuffer(value)) {
293411
+ const reason = value.toString("utf8");
293412
+ return reason.length > 0 ? reason : null;
293413
+ }
293414
+ if (value instanceof Uint8Array) {
293415
+ const reason = Buffer2.from(value).toString("utf8");
293416
+ return reason.length > 0 ? reason : null;
293417
+ }
293418
+ return null;
293419
+ }
293420
+ function formatBinanceUserDataWebSocketClose(codeOrEvent, reasonOrUndefined, secretValues) {
293421
+ const record2 = getRecord(codeOrEvent);
293422
+ const code = record2 ? record2.code : codeOrEvent;
293423
+ const reason = getCloseReason(record2 ? record2.reason : reasonOrUndefined);
293424
+ const safeReason = reason === null ? null : redactDiagnosticMessage(reason, secretValues);
293425
+ const details = [
293426
+ typeof code === "number" || typeof code === "string" ? `code=${code}` : null,
293427
+ safeReason ? `reason=${safeReason}` : null
293428
+ ].filter((detail) => detail !== null);
293429
+ return new Error(details.length > 0 ? `Binance user-data WebSocket closed unexpectedly (${details.join(", ")})` : "Binance user-data WebSocket closed unexpectedly");
293430
+ }
293431
+ function decodeMessageData(data) {
293432
+ if (typeof data === "string") {
293433
+ return data;
293434
+ }
293435
+ if (Buffer2.isBuffer(data)) {
293436
+ return data.toString("utf8");
293437
+ }
293438
+ if (data instanceof ArrayBuffer) {
293439
+ return Buffer2.from(data).toString("utf8");
293440
+ }
293441
+ if (ArrayBuffer.isView(data)) {
293442
+ return Buffer2.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
293443
+ }
293444
+ if (Array.isArray(data) && data.every((item) => Buffer2.isBuffer(item))) {
293445
+ return Buffer2.concat(data).toString("utf8");
293446
+ }
293447
+ return data;
293448
+ }
293275
293449
 
293276
293450
  class BinanceSpotUserDataStream {
293277
293451
  exchange;
293278
293452
  ws;
293453
+ secretValues;
293279
293454
  requestId = `user-data-${Date.now()}-${Math.random()}`;
293455
+ maxBufferedEvents;
293280
293456
  queue = [];
293281
293457
  waiters = [];
293282
293458
  closed = false;
293283
293459
  closeError = null;
293284
293460
  subscriptionId = null;
293285
- constructor(exchange) {
293461
+ constructor(exchange, options = {}) {
293286
293462
  this.exchange = exchange;
293287
- const WebSocketCtor = getWebSocketConstructor();
293288
- this.ws = new WebSocketCtor(getBinanceSpotWsApiUrl(exchange));
293289
- this.ws.onopen = () => this.subscribe();
293290
- this.ws.onmessage = (message) => this.handleMessage(message.data);
293291
- this.ws.onerror = (error48) => this.fail(error48 instanceof Error ? error48 : new Error("Binance user-data WebSocket error"));
293292
- this.ws.onclose = () => this.close();
293463
+ this.maxBufferedEvents = options.maxBufferedEvents ?? DEFAULT_BINANCE_USER_DATA_MAX_BUFFERED_EVENTS;
293464
+ this.secretValues = [
293465
+ getOptionalExchangeString(exchange, "apiKey"),
293466
+ getOptionalExchangeString(exchange, "secret")
293467
+ ].filter((value) => value !== null);
293468
+ this.ws = createWebSocket(getBinanceSpotWsApiUrl(exchange));
293469
+ this.ws.on("open", () => this.subscribe());
293470
+ this.ws.on("message", (data) => this.handleMessage(data));
293471
+ this.ws.on("error", (error48) => this.fail(formatBinanceUserDataWebSocketError(error48, this.secretValues)));
293472
+ this.ws.on("close", (code, reason) => this.handleClose(code, reason));
293293
293473
  }
293294
293474
  async* [Symbol.asyncIterator]() {
293295
293475
  while (true) {
@@ -293305,11 +293485,18 @@ class BinanceSpotUserDataStream {
293305
293485
  return;
293306
293486
  }
293307
293487
  this.closed = true;
293488
+ this.queue.length = 0;
293308
293489
  try {
293309
293490
  this.ws.close();
293310
293491
  } catch {}
293311
293492
  this.flushWaiters();
293312
293493
  }
293494
+ handleClose(code, reason) {
293495
+ if (this.closed) {
293496
+ return;
293497
+ }
293498
+ this.fail(formatBinanceUserDataWebSocketClose(code, reason, this.secretValues));
293499
+ }
293313
293500
  subscribe() {
293314
293501
  const apiKey = getExchangeString(this.exchange, "apiKey");
293315
293502
  const signedParams = signUserDataStreamParams(this.exchange, {
@@ -293323,9 +293510,13 @@ class BinanceSpotUserDataStream {
293323
293510
  }));
293324
293511
  }
293325
293512
  handleMessage(data) {
293513
+ if (this.closed) {
293514
+ return;
293515
+ }
293326
293516
  let message;
293327
293517
  try {
293328
- message = typeof data === "string" ? JSON.parse(data) : data;
293518
+ const decodedData = decodeMessageData(data);
293519
+ message = typeof decodedData === "string" ? JSON.parse(decodedData) : decodedData;
293329
293520
  } catch (error48) {
293330
293521
  this.fail(error48 instanceof Error ? error48 : new Error("Invalid Binance user-data message"));
293331
293522
  return;
@@ -293348,11 +293539,18 @@ class BinanceSpotUserDataStream {
293348
293539
  this.push({ subscriptionId, event: message.event });
293349
293540
  }
293350
293541
  push(event) {
293542
+ if (this.closed) {
293543
+ return;
293544
+ }
293351
293545
  const waiter = this.waiters.shift();
293352
293546
  if (waiter) {
293353
293547
  waiter.resolve(event);
293354
293548
  return;
293355
293549
  }
293550
+ if (this.queue.length >= this.maxBufferedEvents) {
293551
+ this.fail(new Error(`Binance user-data stream buffered event limit exceeded (${this.maxBufferedEvents}); downstream consumer is not keeping up`));
293552
+ return;
293553
+ }
293356
293554
  this.queue.push(event);
293357
293555
  }
293358
293556
  nextEvent() {
@@ -293376,6 +293574,7 @@ class BinanceSpotUserDataStream {
293376
293574
  }
293377
293575
  this.closeError = error48;
293378
293576
  this.closed = true;
293577
+ this.queue.length = 0;
293379
293578
  this.flushWaiters();
293380
293579
  try {
293381
293580
  this.ws.close();
@@ -293403,16 +293602,49 @@ function isBinanceOrderUserDataEvent(event) {
293403
293602
  function isBinanceSpotAccountSubscription(cex3, subscriptionType, marketTypeInput) {
293404
293603
  return cex3 === "binance" && parseMarketType(marketTypeInput) === "spot" && (subscriptionType === SubscriptionType.BALANCE || subscriptionType === SubscriptionType.ORDERS);
293405
293604
  }
293406
- function writeSubscribeFrame(call, isClosed, frame) {
293605
+ function waitForSubscribeDrain(call, isClosed) {
293606
+ if (isClosed() || call.destroyed) {
293607
+ return Promise.resolve(false);
293608
+ }
293609
+ return new Promise((resolve) => {
293610
+ let settled = false;
293611
+ const cleanup = () => {
293612
+ call.off("drain", onDrain);
293613
+ call.off("close", onClosed);
293614
+ call.off("cancelled", onClosed);
293615
+ call.off("end", onClosed);
293616
+ call.off("error", onClosed);
293617
+ };
293618
+ const settle2 = (drained) => {
293619
+ if (settled) {
293620
+ return;
293621
+ }
293622
+ settled = true;
293623
+ cleanup();
293624
+ resolve(drained && !isClosed() && !call.destroyed);
293625
+ };
293626
+ const onDrain = () => settle2(true);
293627
+ const onClosed = () => settle2(false);
293628
+ call.once("drain", onDrain);
293629
+ call.once("close", onClosed);
293630
+ call.once("cancelled", onClosed);
293631
+ call.once("end", onClosed);
293632
+ call.once("error", onClosed);
293633
+ });
293634
+ }
293635
+ async function writeSubscribeFrame(call, isClosed, frame) {
293407
293636
  if (isClosed() || call.destroyed) {
293408
293637
  return false;
293409
293638
  }
293410
- call.write(frame);
293411
- return true;
293639
+ const canContinue = call.write(frame);
293640
+ if (canContinue) {
293641
+ return true;
293642
+ }
293643
+ return waitForSubscribeDrain(call, isClosed);
293412
293644
  }
293413
- function writeSubscribeError(call, isClosed, frame) {
293414
- writeSubscribeFrame(call, isClosed, frame);
293415
- if (!isClosed() && !call.destroyed) {
293645
+ async function writeSubscribeError(call, isClosed, frame) {
293646
+ const frameWritten = await writeSubscribeFrame(call, isClosed, frame);
293647
+ if (frameWritten && !isClosed() && !call.destroyed) {
293416
293648
  call.end();
293417
293649
  }
293418
293650
  }
@@ -293459,7 +293691,7 @@ async function streamBinanceUserData(call, broker, symbol2, subscriptionType, is
293459
293691
  continue;
293460
293692
  }
293461
293693
  }
293462
- if (!writeSubscribeFrame(call, isClosed, {
293694
+ if (!await writeSubscribeFrame(call, isClosed, {
293463
293695
  data: JSON.stringify({
293464
293696
  subscriptionId: message.subscriptionId,
293465
293697
  event
@@ -293478,7 +293710,7 @@ async function streamBinanceUserData(call, broker, symbol2, subscriptionType, is
293478
293710
  async function runCcxtSubscribeLoop(call, isClosed, symbol2, subscriptionType, watch) {
293479
293711
  while (!isClosed()) {
293480
293712
  const data = await watch();
293481
- if (!writeSubscribeFrame(call, isClosed, {
293713
+ if (!await writeSubscribeFrame(call, isClosed, {
293482
293714
  data: JSON.stringify(data),
293483
293715
  timestamp: Date.now(),
293484
293716
  symbol: symbol2,
@@ -293544,7 +293776,7 @@ function createSubscribeHandler(deps) {
293544
293776
  type: subscriptionTypeName
293545
293777
  });
293546
293778
  if (!cex3 || !symbol2) {
293547
- writeSubscribeError(call, isStreamClosed, {
293779
+ await writeSubscribeError(call, isStreamClosed, {
293548
293780
  data: JSON.stringify({
293549
293781
  error: "cex, symbol, and type are required"
293550
293782
  }),
@@ -293558,7 +293790,7 @@ function createSubscribeHandler(deps) {
293558
293790
  const selectedBroker = selectBroker(brokers[normalizedCex], metadata) ?? createBroker(normalizedCex, metadata);
293559
293791
  const broker = selectedBroker ?? createPublicBroker(normalizedCex);
293560
293792
  if (!broker) {
293561
- writeSubscribeError(call, isStreamClosed, {
293793
+ await writeSubscribeError(call, isStreamClosed, {
293562
293794
  data: JSON.stringify({
293563
293795
  error: "Exchange not registered and no API metadata found"
293564
293796
  }),
@@ -293571,7 +293803,7 @@ function createSubscribeHandler(deps) {
293571
293803
  const resolvedSymbol = await resolveSubscriptionSymbol(broker, symbol2, options?.marketType);
293572
293804
  if (isBinanceSpotAccountSubscription(normalizedCex, subscriptionType, options?.marketType)) {
293573
293805
  if (!selectedBroker) {
293574
- writeSubscribeError(call, isStreamClosed, {
293806
+ await writeSubscribeError(call, isStreamClosed, {
293575
293807
  data: JSON.stringify({
293576
293808
  error: "Binance account subscriptions require API credentials"
293577
293809
  }),
@@ -293591,7 +293823,7 @@ function createSubscribeHandler(deps) {
293591
293823
  const depthLimit = parseOptionalDepthLimit(options?.depthLimit ?? options?.limit);
293592
293824
  const orderbook = depthLimit === undefined ? await broker.watchOrderBook(resolvedSymbol) : await broker.watchOrderBook(resolvedSymbol, depthLimit);
293593
293825
  const receivedTimestamp = Date.now();
293594
- if (!writeSubscribeFrame(call, isStreamClosed, {
293826
+ if (!await writeSubscribeFrame(call, isStreamClosed, {
293595
293827
  data: JSON.stringify(normalizeOrderBookSnapshot(orderbook, {
293596
293828
  exchange: normalizedCex,
293597
293829
  symbol: resolvedSymbol,
@@ -293608,7 +293840,7 @@ function createSubscribeHandler(deps) {
293608
293840
  } catch (error48) {
293609
293841
  log.error(`Error fetching orderbook for ${resolvedSymbol} on ${cex3}:`, error48);
293610
293842
  const message = getErrorMessage(error48);
293611
- writeSubscribeError(call, isStreamClosed, {
293843
+ await writeSubscribeError(call, isStreamClosed, {
293612
293844
  data: JSON.stringify({
293613
293845
  error: `Failed to fetch orderbook: ${message}`
293614
293846
  }),
@@ -293624,7 +293856,7 @@ function createSubscribeHandler(deps) {
293624
293856
  } catch (error48) {
293625
293857
  const message = getErrorMessage(error48);
293626
293858
  log.error(`Error fetching trades for ${resolvedSymbol} on ${cex3}:`, error48);
293627
- writeSubscribeError(call, isStreamClosed, {
293859
+ await writeSubscribeError(call, isStreamClosed, {
293628
293860
  data: JSON.stringify({
293629
293861
  error: `Failed to fetch trades: ${message}`
293630
293862
  }),
@@ -293640,7 +293872,7 @@ function createSubscribeHandler(deps) {
293640
293872
  } catch (error48) {
293641
293873
  const message = getErrorMessage(error48);
293642
293874
  log.error(`Error fetching ticker for ${resolvedSymbol} on ${cex3}:`, error48);
293643
- writeSubscribeError(call, isStreamClosed, {
293875
+ await writeSubscribeError(call, isStreamClosed, {
293644
293876
  data: JSON.stringify({
293645
293877
  error: `Failed to fetch ticker: ${message}`
293646
293878
  }),
@@ -293657,7 +293889,7 @@ function createSubscribeHandler(deps) {
293657
293889
  } catch (error48) {
293658
293890
  log.error(`Error fetching OHLCV for ${resolvedSymbol} on ${cex3}:`, error48);
293659
293891
  const message = getErrorMessage(error48);
293660
- writeSubscribeError(call, isStreamClosed, {
293892
+ await writeSubscribeError(call, isStreamClosed, {
293661
293893
  data: JSON.stringify({
293662
293894
  error: `Failed to fetch OHLCV: ${message}`
293663
293895
  }),
@@ -293673,7 +293905,7 @@ function createSubscribeHandler(deps) {
293673
293905
  } catch (error48) {
293674
293906
  const message = getErrorMessage(error48);
293675
293907
  log.error(`Error fetching balance for ${cex3}:`, error48);
293676
- writeSubscribeError(call, isStreamClosed, {
293908
+ await writeSubscribeError(call, isStreamClosed, {
293677
293909
  data: JSON.stringify({
293678
293910
  error: `Failed to fetch balance: ${message}`
293679
293911
  }),
@@ -293689,7 +293921,7 @@ function createSubscribeHandler(deps) {
293689
293921
  } catch (error48) {
293690
293922
  log.error(`Error fetching orders for ${resolvedSymbol} on ${cex3}:`, error48);
293691
293923
  const message = getErrorMessage(error48);
293692
- writeSubscribeError(call, isStreamClosed, {
293924
+ await writeSubscribeError(call, isStreamClosed, {
293693
293925
  data: JSON.stringify({
293694
293926
  error: `Failed to fetch orders: ${message}`
293695
293927
  }),
@@ -293700,7 +293932,7 @@ function createSubscribeHandler(deps) {
293700
293932
  }
293701
293933
  break;
293702
293934
  default:
293703
- writeSubscribeError(call, isStreamClosed, {
293935
+ await writeSubscribeError(call, isStreamClosed, {
293704
293936
  data: JSON.stringify({ error: "Invalid subscription type" }),
293705
293937
  timestamp: Date.now(),
293706
293938
  symbol: symbol2,
@@ -293710,7 +293942,7 @@ function createSubscribeHandler(deps) {
293710
293942
  } catch (error48) {
293711
293943
  log.error("Error in Subscribe stream:", error48);
293712
293944
  const message = getErrorMessage(error48);
293713
- writeSubscribeError(call, isStreamClosed, {
293945
+ await writeSubscribeError(call, isStreamClosed, {
293714
293946
  data: JSON.stringify({ error: `Internal server error: ${message}` }),
293715
293947
  timestamp: Date.now(),
293716
293948
  symbol: "",
@@ -294085,20 +294317,20 @@ class CEXBroker {
294085
294317
  this.brokers = createBrokerPool(configMap);
294086
294318
  }
294087
294319
  loadExchangeCredentials(creds) {
294088
- const schema = import_joi2.default.object().pattern(import_joi2.default.string().allow(...BrokerList).required(), import_joi2.default.object({
294089
- apiKey: import_joi2.default.string().required(),
294090
- apiSecret: import_joi2.default.string().required(),
294091
- role: import_joi2.default.string().valid("master", "subaccount").optional(),
294092
- email: import_joi2.default.string().optional(),
294093
- subAccountId: import_joi2.default.string().optional(),
294094
- uid: import_joi2.default.string().optional(),
294095
- secondaryKeys: import_joi2.default.array().items(import_joi2.default.object({
294096
- apiKey: import_joi2.default.string().required(),
294097
- apiSecret: import_joi2.default.string().required(),
294098
- role: import_joi2.default.string().valid("master", "subaccount").optional(),
294099
- email: import_joi2.default.string().optional(),
294100
- subAccountId: import_joi2.default.string().optional(),
294101
- uid: import_joi2.default.string().optional()
294320
+ const schema = import_joi3.default.object().pattern(import_joi3.default.string().allow(...BrokerList).required(), import_joi3.default.object({
294321
+ apiKey: import_joi3.default.string().required(),
294322
+ apiSecret: import_joi3.default.string().required(),
294323
+ role: import_joi3.default.string().valid("master", "subaccount").optional(),
294324
+ email: import_joi3.default.string().optional(),
294325
+ subAccountId: import_joi3.default.string().optional(),
294326
+ uid: import_joi3.default.string().optional(),
294327
+ secondaryKeys: import_joi3.default.array().items(import_joi3.default.object({
294328
+ apiKey: import_joi3.default.string().required(),
294329
+ apiSecret: import_joi3.default.string().required(),
294330
+ role: import_joi3.default.string().valid("master", "subaccount").optional(),
294331
+ email: import_joi3.default.string().optional(),
294332
+ subAccountId: import_joi3.default.string().optional(),
294333
+ uid: import_joi3.default.string().optional()
294102
294334
  })).default([])
294103
294335
  })).required();
294104
294336
  const { value, error: error48 } = schema.validate(creds);
@@ -294185,4 +294417,4 @@ export {
294185
294417
  CEXBroker as default
294186
294418
  };
294187
294419
 
294188
- //# debugId=18334E4E9BE8A34F64756E2164756E21
294420
+ //# debugId=8F76D79C41F9E70764756E2164756E21