@usherlabs/cex-broker 0.2.36 → 0.2.37

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.
@@ -315527,7 +315527,7 @@ function compactUndefined2(record) {
315527
315527
  }
315528
315528
  function buildCommonArchiveTags(input) {
315529
315529
  return {
315530
- source: BROKER_WRITE_SOURCE,
315530
+ source: input.source ?? BROKER_WRITE_SOURCE,
315531
315531
  deployment_id: input.deploymentId,
315532
315532
  account_selector: input.accountSelector ?? "unknown",
315533
315533
  exchange: input.exchange.trim().toLowerCase() || "unknown",
@@ -315590,6 +315590,10 @@ function buildSubscribeStreamArchiveRow(input) {
315590
315590
  function quantityString(...values2) {
315591
315591
  return firstString2(...values2);
315592
315592
  }
315593
+ function extractBinanceInternalTransferId(response) {
315594
+ const record = asRecord(response);
315595
+ return firstString2(record?.txnId, record?.tranId);
315596
+ }
315593
315597
  function buildTransferEventArchiveRow(input) {
315594
315598
  const { tags, transfer } = input;
315595
315599
  return {
@@ -315622,6 +315626,7 @@ function normalizeCcxtTransactionForArchive(transaction) {
315622
315626
  const fee = asRecord(record?.fee);
315623
315627
  return compactUndefined2({
315624
315628
  externalId: firstString2(record?.id, info?.id, record?.txid, info?.txId),
315629
+ clientWithdrawalId: firstString2(info?.withdrawOrderId),
315625
315630
  txid: firstString2(record?.txid, info?.txId, info?.txid, info?.tx_hash),
315626
315631
  address: firstString2(record?.address, record?.addressTo, info?.address),
315627
315632
  network: firstString2(record?.network, info?.network),
@@ -315729,14 +315734,40 @@ var DEFAULT_BATCH_SIZE = 10;
315729
315734
  var DEFAULT_FLUSH_INTERVAL_MS = 1000;
315730
315735
  var DEFAULT_FORWARDER_TIMEOUT_MS = 3000;
315731
315736
  var SHED_WARN_INTERVAL_MS = 60000;
315737
+ var MARKET_FEEDS = new Set(["ORDERBOOK", "TICKER", "TRADES", "OHLCV"]);
315738
+ function archiveFeed(row) {
315739
+ const declared = row.row.feed ?? row.row.stream_type;
315740
+ if (typeof declared === "string" && MARKET_FEEDS.has(declared)) {
315741
+ return declared;
315742
+ }
315743
+ if (row.table === "market_data.orderbook_snapshots" || row.table.startsWith("market_data.cex_order_book_")) {
315744
+ return "ORDERBOOK";
315745
+ }
315746
+ if (row.table === "market_data.candles" || row.table === "market_data.cex_ohlcv") {
315747
+ return "OHLCV";
315748
+ }
315749
+ if (row.table === "market_data.cex_ticker_events")
315750
+ return "TICKER";
315751
+ if (row.table === "market_data.cex_trades")
315752
+ return "TRADES";
315753
+ return "NON_MARKET";
315754
+ }
315732
315755
  function isArchiveOtelLogsEnabled() {
315733
315756
  return process.env.CEX_BROKER_ARCHIVE_OTEL_LOGS_ENABLED === "true";
315734
315757
  }
315735
315758
  function resolveArchiveForwarderUrlFromEnv() {
315736
315759
  return process.env.CEX_BROKER_ARCHIVE_FORWARDER_URL?.trim() || undefined;
315737
315760
  }
315761
+ function resolveArchiveSourceFromEnv(value = process.env.CEX_BROKER_ARCHIVE_SOURCE) {
315762
+ const source = value?.trim() || BROKER_WRITE_SOURCE;
315763
+ if (source !== "broker_read" && source !== "broker_write") {
315764
+ throw new Error("CEX_BROKER_ARCHIVE_SOURCE must be broker_read or broker_write");
315765
+ }
315766
+ return source;
315767
+ }
315738
315768
 
315739
315769
  class BrokerExecutionArchiver {
315770
+ source;
315740
315771
  deploymentId;
315741
315772
  otelLogs;
315742
315773
  otelMetrics;
@@ -315757,10 +315788,12 @@ class BrokerExecutionArchiver {
315757
315788
  flushTimer = null;
315758
315789
  flushInFlight = null;
315759
315790
  lastShedWarnAtMs = 0;
315791
+ closing = false;
315760
315792
  closed = false;
315761
315793
  enabled;
315762
315794
  forwarderAuthToken;
315763
315795
  constructor(options) {
315796
+ this.source = options.source ?? BROKER_WRITE_SOURCE;
315764
315797
  this.deploymentId = options.deploymentId?.trim() || process.env.CEX_BROKER_DEPLOYMENT_ID?.trim() || "unknown";
315765
315798
  this.otelLogs = options.otelLogs;
315766
315799
  this.otelMetrics = options.otelMetrics;
@@ -315792,6 +315825,7 @@ class BrokerExecutionArchiver {
315792
315825
  this.flushTimer.unref?.();
315793
315826
  log.info("Broker execution archive enabled", {
315794
315827
  enabled: true,
315828
+ source: this.source,
315795
315829
  otel_mirror_enabled: Boolean(this.otelLogs?.isOtelEnabled())
315796
315830
  });
315797
315831
  } catch (error) {
@@ -315808,8 +315842,11 @@ class BrokerExecutionArchiver {
315808
315842
  getDeploymentId() {
315809
315843
  return this.deploymentId;
315810
315844
  }
315845
+ getSource() {
315846
+ return this.source;
315847
+ }
315811
315848
  isEnabled() {
315812
- return this.enabled && !this.closed;
315849
+ return this.enabled && !this.closing && !this.closed;
315813
315850
  }
315814
315851
  canPersistMarketMetadataSnapshot() {
315815
315852
  return this.isEnabled();
@@ -315821,6 +315858,10 @@ class BrokerExecutionArchiver {
315821
315858
  if (!this.enabled || this.closed) {
315822
315859
  return;
315823
315860
  }
315861
+ const archiveRow = {
315862
+ table: row.table,
315863
+ row: { ...row.row, source: this.source }
315864
+ };
315824
315865
  if (this.queue.length >= this.maxQueueSize) {
315825
315866
  const shedRow = this.queue[0];
315826
315867
  if (shedRow) {
@@ -315829,7 +315870,14 @@ class BrokerExecutionArchiver {
315829
315870
  }
315830
315871
  this.stats.shed += 1;
315831
315872
  this.recordArchiveMetric("cex_archive_rows_shed_total", {
315832
- table: shedRow?.table ?? "unknown"
315873
+ table: shedRow?.table ?? "unknown",
315874
+ source: this.source,
315875
+ feed: shedRow ? archiveFeed(shedRow) : "NON_MARKET"
315876
+ });
315877
+ this.recordArchiveMetric("cex_archive_queue_saturated_rows_total", {
315878
+ table: shedRow?.table ?? "unknown",
315879
+ source: this.source,
315880
+ feed: shedRow ? archiveFeed(shedRow) : "NON_MARKET"
315833
315881
  });
315834
315882
  const now3 = Date.now();
315835
315883
  if (now3 - this.lastShedWarnAtMs >= SHED_WARN_INTERVAL_MS) {
@@ -315841,10 +315889,12 @@ class BrokerExecutionArchiver {
315841
315889
  this.lastShedWarnAtMs = now3;
315842
315890
  }
315843
315891
  }
315844
- this.queue.push(row);
315892
+ this.queue.push(archiveRow);
315845
315893
  this.stats.enqueued += 1;
315846
315894
  this.recordArchiveMetric("cex_archive_rows_enqueued_total", {
315847
- table: row.table
315895
+ table: archiveRow.table,
315896
+ source: this.source,
315897
+ feed: archiveFeed(archiveRow)
315848
315898
  });
315849
315899
  if (this.queue.length >= this.batchSize) {
315850
315900
  this.flush();
@@ -315864,11 +315914,15 @@ class BrokerExecutionArchiver {
315864
315914
  return;
315865
315915
  }).finally(() => {
315866
315916
  this.flushInFlight = null;
315917
+ if (!this.closed && !this.closing && this.enabled && this.queue.length >= this.batchSize) {
315918
+ queueMicrotask(() => void this.flush());
315919
+ }
315867
315920
  });
315868
315921
  this.flushInFlight = inFlight;
315869
315922
  return inFlight;
315870
315923
  }
315871
315924
  async close() {
315925
+ this.closing = true;
315872
315926
  if (this.flushTimer) {
315873
315927
  clearInterval(this.flushTimer);
315874
315928
  this.flushTimer = null;
@@ -315933,7 +315987,14 @@ class BrokerExecutionArchiver {
315933
315987
  this.queue.shift();
315934
315988
  this.stats.shed += 1;
315935
315989
  this.recordArchiveMetric("cex_archive_rows_shed_total", {
315936
- table: dropped?.table ?? "unknown"
315990
+ table: dropped?.table ?? "unknown",
315991
+ source: this.source,
315992
+ feed: archiveFeed(dropped)
315993
+ });
315994
+ this.recordArchiveMetric("cex_archive_queue_saturated_rows_total", {
315995
+ table: dropped.table,
315996
+ source: this.source,
315997
+ feed: archiveFeed(dropped)
315937
315998
  });
315938
315999
  }
315939
316000
  }
@@ -315947,6 +316008,7 @@ class BrokerExecutionArchiver {
315947
316008
  const timestamp = new Date().toISOString();
315948
316009
  const records = rows.map((payload) => ({
315949
316010
  timestamp,
316011
+ source: this.source,
315950
316012
  deployment_id: this.deploymentId,
315951
316013
  reason,
315952
316014
  payload
@@ -315960,6 +316022,21 @@ class BrokerExecutionArchiver {
315960
316022
  throw new Error(`wrote ${written} of ${bytes2.length} bytes`);
315961
316023
  }
315962
316024
  fsyncSync(this.deadLetterFd);
316025
+ const byFeedAndTable = new Map;
316026
+ for (const row of rows) {
316027
+ const key = `${row.table}\x00${archiveFeed(row)}`;
316028
+ const grouped = byFeedAndTable.get(key) ?? { row, count: 0 };
316029
+ grouped.count += 1;
316030
+ byFeedAndTable.set(key, grouped);
316031
+ }
316032
+ for (const { row, count: count2 } of byFeedAndTable.values()) {
316033
+ this.recordArchiveMetric("cex_archive_rows_journaled_total", {
316034
+ table: row.table,
316035
+ source: this.source,
316036
+ feed: archiveFeed(row),
316037
+ reason
316038
+ }, count2);
316039
+ }
315963
316040
  } catch (error) {
315964
316041
  throw new BrokerExecutionArchiveDurabilityError(`Broker execution archive failed to durably record ${reason}; affected row(s) were retained`, { cause: error });
315965
316042
  }
@@ -315995,10 +316072,17 @@ class BrokerExecutionArchiver {
315995
316072
  recordFlushHealth(batch) {
315996
316073
  const countByTable = new Map;
315997
316074
  for (const entry of batch) {
315998
- countByTable.set(entry.table, (countByTable.get(entry.table) ?? 0) + 1);
315999
- }
316000
- for (const [table, count2] of countByTable) {
316001
- this.recordArchiveMetric("cex_archive_rows_flushed_total", { table }, count2);
316075
+ const key = `${entry.table}\x00${archiveFeed(entry)}`;
316076
+ const grouped = countByTable.get(key) ?? { row: entry, count: 0 };
316077
+ grouped.count += 1;
316078
+ countByTable.set(key, grouped);
316079
+ }
316080
+ for (const { row, count: count2 } of countByTable.values()) {
316081
+ this.recordArchiveMetric("cex_archive_rows_flushed_total", {
316082
+ table: row.table,
316083
+ source: this.source,
316084
+ feed: archiveFeed(row)
316085
+ }, count2);
316002
316086
  }
316003
316087
  this.recordArchiveGauge("cex_archive_last_flush_success", Math.floor(Date.now() / 1000));
316004
316088
  }
@@ -316032,7 +316116,7 @@ class BrokerExecutionArchiver {
316032
316116
  return Promise.resolve();
316033
316117
  }
316034
316118
  const body = JSON.stringify({
316035
- source: "broker_write",
316119
+ source: this.source,
316036
316120
  deployment_id: this.deploymentId,
316037
316121
  rows: batch
316038
316122
  });
@@ -316102,6 +316186,7 @@ function createBrokerExecutionArchiverFromEnv(otelLogs, otelMetrics) {
316102
316186
  const forwarderUrl = resolveArchiveForwarderUrlFromEnv();
316103
316187
  const archiveOtelLogs = isArchiveOtelLogsEnabled() ? otelLogs : undefined;
316104
316188
  return BrokerExecutionArchiver.create({
316189
+ source: resolveArchiveSourceFromEnv(),
316105
316190
  otelLogs: archiveOtelLogs,
316106
316191
  otelMetrics,
316107
316192
  forwarderUrl: forwarderUrl ?? "",
@@ -316240,6 +316325,7 @@ function archiveWithdrawalObservationsInBackground(archiver, tracker, input) {
316240
316325
  address: normalized.address,
316241
316326
  network: normalized.network,
316242
316327
  externalId: normalized.externalId,
316328
+ clientWithdrawalId: normalized.clientWithdrawalId,
316243
316329
  txid: normalized.txid,
316244
316330
  resultIndex,
316245
316331
  feeAmount: normalized.feeAmount,
@@ -331336,9 +331422,6 @@ function selectBrokerAccountForCex(normalizedCex, brokers, metadata) {
331336
331422
  return selectBrokerAccount(brokers[normalizedCex], metadata) ?? undefined;
331337
331423
  }
331338
331424
 
331339
- // src/handlers/execute-action/order-book-call.ts
331340
- var grpc4 = __toESM(require_src3(), 1);
331341
-
331342
331425
  // src/helpers/order-book.ts
331343
331426
  var ORDER_BOOK_CALL_METHODS = {
331344
331427
  FETCH_CAPABILITY: "fetch_order_book_capability",
@@ -331581,726 +331664,1200 @@ function buildHistoricalOrderBookUnsupported(payload) {
331581
331664
  }
331582
331665
 
331583
331666
  // src/handlers/execute-action/order-book-call.ts
331584
- async function handleOrderBookCall(ctx) {
331585
- const parsedOrderBookCall = parseOrderBookCallPayload(ctx.call.request.payload, {
331586
- exchange: ctx.normalizedCex,
331587
- symbol: ctx.symbol
331588
- });
331589
- if (parsedOrderBookCall.kind === "error") {
331590
- ctx.wrappedCallback({
331591
- code: grpc4.status.INVALID_ARGUMENT,
331592
- message: parsedOrderBookCall.message
331593
- }, null);
331594
- return true;
331667
+ var grpc4 = __toESM(require_src3(), 1);
331668
+
331669
+ // src/helpers/market-data-archive/capture-contract.ts
331670
+ import { createHash as createHash3 } from "node:crypto";
331671
+ var MARKET_CAPTURE_SCHEMA_VERSION = "1.0.0";
331672
+ var CHECKSUM_ALGORITHM = "sha256-canonical-json-v1";
331673
+ var ARCHIVE_SOURCES = ["broker_read", "broker_write"];
331674
+ var CAPTURE_FEEDS = [
331675
+ "ORDERBOOK",
331676
+ "TICKER",
331677
+ "TRADES",
331678
+ "OHLCV"
331679
+ ];
331680
+ var SOURCE_MODES = [
331681
+ "broker_live_stream_v1",
331682
+ "broker_live_sampling_v1",
331683
+ "broker_current_snapshot_v1",
331684
+ "broker_bootstrap_fetch_v1",
331685
+ "external_ccxt_fallback_v1",
331686
+ "external_hummingbot_fallback_v1",
331687
+ "legacy_migration_v1"
331688
+ ];
331689
+ var RAW_CAPTURE_SCOPES = [
331690
+ "ccxt_normalized_object",
331691
+ "broker_visible_payload",
331692
+ "exchange_wire_frame"
331693
+ ];
331694
+ var CHECKSUM_FIELDS = new Set([
331695
+ "normalized_row_checksum",
331696
+ "raw_checksum",
331697
+ "checksum"
331698
+ ]);
331699
+ function canonicalDecimal(value) {
331700
+ if (!Number.isFinite(value)) {
331701
+ throw new Error("Canonical numbers must be finite");
331595
331702
  }
331596
- if (parsedOrderBookCall.kind !== "order_book") {
331597
- return false;
331703
+ if (Object.is(value, -0)) {
331704
+ return "0";
331598
331705
  }
331599
- const orderBookBroker = ctx.selectedBrokerAccount?.exchange ?? createBroker(ctx.normalizedCex, ctx.metadata) ?? createPublicBroker(ctx.normalizedCex);
331600
- if (!orderBookBroker) {
331601
- ctx.wrappedCallback({
331602
- code: grpc4.status.INVALID_ARGUMENT,
331603
- message: `Unsupported exchange for order-book market data: ${ctx.normalizedCex}`
331604
- }, null);
331605
- return true;
331706
+ const rendered = String(value).toLowerCase();
331707
+ if (!rendered.includes("e")) {
331708
+ return rendered;
331606
331709
  }
331607
- ctx.applyVerityToBroker(orderBookBroker);
331608
- try {
331609
- const orderBookPayload = parsedOrderBookCall.payload;
331610
- if (orderBookPayload.method === ORDER_BOOK_CALL_METHODS.FETCH_CAPABILITY) {
331611
- ctx.wrappedCallback(null, {
331612
- proof: ctx.verity.proof,
331613
- result: JSON.stringify(buildOrderBookCapability(orderBookBroker, orderBookPayload))
331614
- });
331615
- return true;
331616
- }
331617
- if (orderBookPayload.method === ORDER_BOOK_CALL_METHODS.FETCH_HISTORICAL_SNAPSHOTS) {
331618
- ctx.wrappedCallback(null, {
331619
- proof: ctx.verity.proof,
331620
- result: JSON.stringify(buildHistoricalOrderBookUnsupported(orderBookPayload))
331621
- });
331622
- return true;
331623
- }
331624
- const fetchOrderBook = orderBookBroker.fetchOrderBook;
331625
- const canFetchOrderBook = typeof fetchOrderBook === "function" && orderBookBroker.has?.fetchOrderBook !== false;
331626
- if (!canFetchOrderBook) {
331627
- ctx.wrappedCallback({
331628
- code: grpc4.status.UNIMPLEMENTED,
331629
- message: `Order-book snapshot unsupported for ${ctx.normalizedCex}`
331630
- }, null);
331631
- return true;
331710
+ const [coefficient = "0", exponentText = "0"] = rendered.split("e");
331711
+ const exponent = Number.parseInt(exponentText, 10);
331712
+ const negative = coefficient.startsWith("-");
331713
+ const unsigned = negative ? coefficient.slice(1) : coefficient;
331714
+ const [integer2 = "0", fraction = ""] = unsigned.split(".");
331715
+ const digits = `${integer2}${fraction}`;
331716
+ const decimalIndex = integer2.length + exponent;
331717
+ let result;
331718
+ if (decimalIndex <= 0) {
331719
+ result = `0.${"0".repeat(-decimalIndex)}${digits}`;
331720
+ } else if (decimalIndex >= digits.length) {
331721
+ result = `${digits}${"0".repeat(decimalIndex - digits.length)}`;
331722
+ } else {
331723
+ result = `${digits.slice(0, decimalIndex)}.${digits.slice(decimalIndex)}`;
331724
+ }
331725
+ return negative ? `-${result}` : result;
331726
+ }
331727
+ function serializeCanonical(value, stack) {
331728
+ if (value === null)
331729
+ return "null";
331730
+ if (typeof value === "string")
331731
+ return JSON.stringify(value);
331732
+ if (typeof value === "boolean")
331733
+ return value ? "true" : "false";
331734
+ if (typeof value === "number")
331735
+ return canonicalDecimal(value);
331736
+ if (typeof value === "bigint")
331737
+ return value.toString(10);
331738
+ if (value instanceof Date) {
331739
+ if (Number.isNaN(value.getTime())) {
331740
+ throw new Error("Canonical timestamps must be valid");
331632
331741
  }
331633
- const receivedTimestamp = Date.now();
331634
- const rawOrderBook = await fetchOrderBook.call(orderBookBroker, orderBookPayload.symbol, orderBookPayload.depthLimit);
331635
- ctx.wrappedCallback(null, {
331636
- proof: ctx.verity.proof,
331637
- result: JSON.stringify(normalizeOrderBookSnapshot(rawOrderBook, {
331638
- exchange: orderBookPayload.exchange,
331639
- symbol: orderBookPayload.symbol,
331640
- depthLimit: orderBookPayload.depthLimit,
331641
- receivedTimestamp
331642
- }))
331643
- });
331644
- } catch (error48) {
331645
- safeLogError("Order-book Call failed", error48);
331646
- ctx.wrappedCallback({
331647
- code: mapCcxtErrorToGrpcStatus(error48) ?? grpc4.status.INTERNAL,
331648
- message: `Order-book Call failed: ${sanitizeErrorDetail(error48)}`
331649
- }, null);
331742
+ return value.getTime().toString(10);
331650
331743
  }
331651
- return true;
331744
+ if (Array.isArray(value)) {
331745
+ if (stack.has(value))
331746
+ throw new Error("Canonical values must be acyclic");
331747
+ stack.add(value);
331748
+ const result = `[${value.map((entry) => entry === undefined ? "null" : serializeCanonical(entry, stack)).join(",")}]`;
331749
+ stack.delete(value);
331750
+ return result;
331751
+ }
331752
+ if (typeof value === "object") {
331753
+ if (stack.has(value))
331754
+ throw new Error("Canonical values must be acyclic");
331755
+ stack.add(value);
331756
+ const entries = Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => left.localeCompare(right));
331757
+ const result = `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${serializeCanonical(entry, stack)}`).join(",")}}`;
331758
+ stack.delete(value);
331759
+ return result;
331760
+ }
331761
+ throw new Error(`Unsupported canonical value type: ${typeof value}`);
331652
331762
  }
331653
-
331654
- // src/handlers/execute-action/registry.ts
331655
- var grpc11 = __toESM(require_src3(), 1);
331656
-
331657
- // src/handlers/execute-action/internal-transfer.ts
331658
- var grpc5 = __toESM(require_src3(), 1);
331659
- async function handleInternalTransfer(ctx) {
331660
- const {
331661
- brokers,
331662
- metadata,
331663
- normalizedCex,
331664
- symbol: symbol2,
331665
- verity,
331666
- useVerity,
331667
- verityProverUrl,
331668
- brokerArchiver
331669
- } = ctx;
331670
- if (!symbol2) {
331671
- return ctx.wrappedCallback({
331672
- code: grpc5.status.INVALID_ARGUMENT,
331673
- message: `ValidationError: Symbol required`
331674
- }, null);
331763
+ function canonicalSerialize(value) {
331764
+ return serializeCanonical(value, new Set);
331765
+ }
331766
+ function omitChecksumFields(value) {
331767
+ if (Array.isArray(value))
331768
+ return value.map(omitChecksumFields);
331769
+ if (value && typeof value === "object" && !(value instanceof Date)) {
331770
+ return Object.fromEntries(Object.entries(value).filter(([key]) => !CHECKSUM_FIELDS.has(key)).map(([key, entry]) => [key, omitChecksumFields(entry)]));
331675
331771
  }
331676
- const transferPayload = parsePayloadForAction(ctx, InternalTransferPayloadSchema);
331677
- if (transferPayload === null)
331678
- return;
331679
- if (normalizedCex !== "binance") {
331680
- return ctx.wrappedCallback({
331681
- code: grpc5.status.UNIMPLEMENTED,
331682
- message: `InternalTransfer is only supported for Binance`
331683
- }, null);
331772
+ return value;
331773
+ }
331774
+ function sha256Canonical(value) {
331775
+ return createHash3("sha256").update(canonicalSerialize(omitChecksumFields(value))).digest("hex");
331776
+ }
331777
+ function normalizeTimestampMs(value, field) {
331778
+ let timestamp;
331779
+ if (value instanceof Date) {
331780
+ timestamp = value.getTime();
331781
+ } else if (typeof value === "number") {
331782
+ timestamp = value;
331783
+ } else if (typeof value === "string" && /^\d+$/.test(value.trim())) {
331784
+ timestamp = Number(value.trim());
331785
+ } else if (typeof value === "string") {
331786
+ timestamp = Date.parse(value);
331787
+ } else {
331788
+ timestamp = Number.NaN;
331684
331789
  }
331685
- const pool = brokers[normalizedCex];
331686
- if (!pool) {
331687
- return ctx.wrappedCallback({
331688
- code: grpc5.status.FAILED_PRECONDITION,
331689
- message: `No broker accounts configured for ${normalizedCex}`
331690
- }, null);
331790
+ if (!Number.isSafeInteger(timestamp) || timestamp < 0) {
331791
+ throw new Error(`${field} must be a non-negative millisecond timestamp`);
331691
331792
  }
331692
- const fromSelector = transferPayload.fromAccount ?? getCurrentBrokerSelector(metadata);
331693
- const toSelector = transferPayload.toAccount ?? "primary";
331694
- const sourceAccount = resolveBrokerAccount(pool, fromSelector);
331695
- if (!sourceAccount) {
331696
- return ctx.wrappedCallback({
331697
- code: grpc5.status.INVALID_ARGUMENT,
331698
- message: `Source account "${fromSelector}" is not configured`
331699
- }, null);
331793
+ return timestamp;
331794
+ }
331795
+ function assertCaptureContext(context2) {
331796
+ if (!ARCHIVE_SOURCES.includes(context2.source)) {
331797
+ throw new Error(`Unsupported archive source: ${context2.source}`);
331700
331798
  }
331701
- const destAccount = resolveBrokerAccount(pool, toSelector);
331702
- if (!destAccount) {
331703
- return ctx.wrappedCallback({
331704
- code: grpc5.status.INVALID_ARGUMENT,
331705
- message: `Destination account "${toSelector}" is not configured`
331706
- }, null);
331799
+ if (!CAPTURE_FEEDS.includes(context2.feed)) {
331800
+ throw new Error(`Unsupported capture feed: ${context2.feed}`);
331707
331801
  }
331708
- try {
331709
- if (useVerity) {
331710
- sourceAccount.exchange.setHttpClientOverride(buildHttpClientOverrideFromMetadata(metadata, verityProverUrl, (proof, notaryPubKey) => {
331711
- verity.proof = proof;
331712
- log.debug(`Verity proof:`, { proof, notaryPubKey });
331713
- }), verityHttpClientOverridePredicate);
331802
+ if (!SOURCE_MODES.includes(context2.sourceMode)) {
331803
+ throw new Error(`Unsupported source mode: ${context2.sourceMode}`);
331804
+ }
331805
+ for (const [field, value] of [
331806
+ ["deployment_id", context2.deploymentId],
331807
+ ["capture_bundle_id", context2.captureBundleId],
331808
+ ["exchange", context2.exchange],
331809
+ ["symbol", context2.symbol],
331810
+ ["provider", context2.provider]
331811
+ ]) {
331812
+ if (!value.trim())
331813
+ throw new Error(`${field} must not be empty`);
331814
+ }
331815
+ }
331816
+ function createRawCapture(context2, input) {
331817
+ assertCaptureContext(context2);
331818
+ if (!RAW_CAPTURE_SCOPES.includes(input.scope)) {
331819
+ throw new Error(`Unsupported raw capture scope: ${input.scope}`);
331820
+ }
331821
+ const eventTimeMs = normalizeTimestampMs(input.eventTimeMs, "event_time_ms");
331822
+ const receivedTimeMs = normalizeTimestampMs(input.receivedTimeMs, "received_time_ms");
331823
+ const redactedPayload = redactStreamPayload(input.payload);
331824
+ const rawChecksum = sha256Canonical(redactedPayload);
331825
+ const rawCaptureId = sha256Canonical({
331826
+ capture_bundle_id: context2.captureBundleId,
331827
+ exchange: context2.exchange.trim().toLowerCase(),
331828
+ feed: context2.feed,
331829
+ raw_capture_scope: input.scope,
331830
+ raw_payload_sha256: rawChecksum,
331831
+ schema_version: context2.schemaVersion,
331832
+ source_mode: context2.sourceMode,
331833
+ source_symbol: context2.symbol.trim(),
331834
+ source_time_ms: eventTimeMs
331835
+ });
331836
+ return {
331837
+ rawCaptureId,
331838
+ rawCaptureScope: input.scope,
331839
+ rawChecksum,
331840
+ redactedPayload,
331841
+ eventTimeMs,
331842
+ receivedTimeMs,
331843
+ checksumAlgorithm: context2.checksumAlgorithm
331844
+ };
331845
+ }
331846
+ function captureCoreFields(context2, rawCapture) {
331847
+ assertCaptureContext(context2);
331848
+ return {
331849
+ source: context2.source,
331850
+ deployment_id: context2.deploymentId,
331851
+ capture_bundle_id: context2.captureBundleId,
331852
+ exchange: context2.exchange.trim().toLowerCase(),
331853
+ symbol: context2.symbol.trim(),
331854
+ trading_pair: context2.symbol.trim().replace("/", "-"),
331855
+ source_symbol: context2.symbol.trim(),
331856
+ asset_type: context2.assetType,
331857
+ feed: context2.feed,
331858
+ provider: context2.provider,
331859
+ source_mode: context2.sourceMode,
331860
+ source_time_ms: rawCapture.eventTimeMs,
331861
+ received_time_ms: rawCapture.receivedTimeMs,
331862
+ raw_capture_id: rawCapture.rawCaptureId,
331863
+ raw_capture_scope: rawCapture.rawCaptureScope,
331864
+ schema_version: context2.schemaVersion,
331865
+ checksum_algorithm: context2.checksumAlgorithm,
331866
+ raw_checksum: rawCapture.rawChecksum,
331867
+ provenance_complete: context2.provenanceComplete ? 1 : 0
331868
+ };
331869
+ }
331870
+
331871
+ // src/helpers/market-data-archive/canonical-orderbook.ts
331872
+ class OrderBookValidationError extends Error {
331873
+ reason;
331874
+ constructor(reason) {
331875
+ super(`Invalid order-book evidence: ${reason}`);
331876
+ this.name = "OrderBookValidationError";
331877
+ this.reason = reason;
331878
+ }
331879
+ }
331880
+ function parseSequence(value) {
331881
+ if (value === undefined)
331882
+ return;
331883
+ const parsed = typeof value === "number" ? value : typeof value === "string" && /^\d+$/.test(value) ? Number(value) : Number.NaN;
331884
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
331885
+ throw new OrderBookValidationError("sequence must be a non-negative integer");
331886
+ }
331887
+ return parsed;
331888
+ }
331889
+ function validateSide(side, levels, depthLimit) {
331890
+ if (levels.length === 0) {
331891
+ throw new OrderBookValidationError(`${side} side is missing`);
331892
+ }
331893
+ const retained = levels.slice(0, depthLimit);
331894
+ for (let index2 = 0;index2 < retained.length; index2 += 1) {
331895
+ const entry = retained[index2];
331896
+ const price = entry?.[0];
331897
+ const amount = entry?.[1];
331898
+ if (!Array.isArray(entry) || entry.length < 2 || price === undefined || amount === undefined || !Number.isFinite(price) || !Number.isFinite(amount) || price <= 0 || amount <= 0) {
331899
+ throw new OrderBookValidationError(`${side} level ${index2} has a non-positive or non-finite price/amount`);
331714
331900
  }
331715
- const result = await transferBinanceInternal(sourceAccount, destAccount, symbol2, transferPayload.amount);
331716
- const normalized = normalizeCcxtTransactionForArchive(result);
331717
- archiveTransferEventInBackground(brokerArchiver, {
331718
- exchange: normalizedCex,
331719
- accountSelector: fromSelector,
331720
- assetSymbol: symbol2,
331721
- transfer: {
331722
- eventKind: "internal_transfer",
331723
- lifecycleAction: "submit_internal_transfer",
331724
- status: normalized.status ?? "ok",
331725
- amount: normalized.amount ?? String(transferPayload.amount),
331726
- network: "internal",
331727
- externalId: normalized.externalId,
331728
- payload: { from: fromSelector, to: toSelector, result }
331901
+ if (index2 > 0) {
331902
+ const previous = retained[index2 - 1]?.[0];
331903
+ if (previous === undefined || (side === "bid" ? price >= previous : price <= previous)) {
331904
+ throw new OrderBookValidationError(`${side} levels are not strictly ${side === "bid" ? "descending" : "ascending"}`);
331729
331905
  }
331730
- });
331731
- ctx.wrappedCallback(null, {
331732
- proof: verity.proof,
331733
- result: JSON.stringify(result)
331734
- });
331735
- } catch (error48) {
331736
- safeLogError("InternalTransfer failed", error48);
331737
- if (error48 instanceof BrokerAccountPreconditionError) {
331738
- return ctx.wrappedCallback({
331739
- code: grpc5.status.FAILED_PRECONDITION,
331740
- message: getErrorMessage(error48)
331741
- }, null);
331742
331906
  }
331743
- const msg = getErrorMessage(error48);
331744
- let code;
331745
- if (msg.includes("Unsupported transfer direction")) {
331746
- code = grpc5.status.INVALID_ARGUMENT;
331747
- } else if (msg.includes("unavailable in this CCXT build")) {
331748
- code = grpc5.status.UNIMPLEMENTED;
331749
- } else {
331750
- code = mapCcxtErrorToGrpcStatus(error48) ?? grpc5.status.INTERNAL;
331907
+ }
331908
+ return retained.map(([price, amount]) => ({
331909
+ price,
331910
+ amount
331911
+ }));
331912
+ }
331913
+ function normalizedBands(input) {
331914
+ const bands = input ?? [10, 25, 50, 100];
331915
+ for (const band of bands) {
331916
+ if (!Number.isFinite(band) || band <= 0 || !Number.isInteger(band)) {
331917
+ throw new OrderBookValidationError("measurement bands must be positive integer basis points");
331751
331918
  }
331752
- ctx.wrappedCallback({
331753
- code,
331754
- message: `InternalTransfer failed: ${sanitizeErrorDetail(error48)}`
331755
- }, null);
331756
331919
  }
331920
+ return [...new Set(bands)].sort((left, right) => left - right);
331757
331921
  }
331758
-
331759
- // src/handlers/execute-action/orders.ts
331760
- var grpc6 = __toESM(require_src3(), 1);
331761
-
331762
- // src/helpers/passive-order.ts
331763
- var PASSIVE_ORDER_ERROR_CODES = {
331764
- unsupported: "passive_order_unsupported",
331765
- rejected: "passive_order_rejected",
331766
- wouldCross: "passive_order_would_cross"
331767
- };
331768
- function identifiesWouldCross(message) {
331769
- const normalized = message.toLowerCase();
331770
- return normalized.includes("would immediately match and take") || /post[\s-]?only\b.*\bwould\b.*\bimmediately\b.*\b(?:execute|fill|match)/.test(normalized) || /post[\s-]?only\b.*\bwould\b.*\b(?:execute|fill|match)\w*\b.*\bimmediately/.test(normalized);
331922
+ function checksumRow(row) {
331923
+ return { ...row, normalized_row_checksum: sha256Canonical(row) };
331771
331924
  }
331772
- function identifiesUnsupported(message) {
331773
- const normalized = message.toLowerCase();
331774
- return /post[\s-]?only\b.*\b(?:not supported|unsupported|does not support)\b/.test(normalized) || /\b(?:not supported|unsupported|does not support)\b.*\bpost[\s-]?only\b/.test(normalized);
331925
+ function commonEvidenceFields(context2, rawCapture, input) {
331926
+ return {
331927
+ ...captureCoreFields(context2, rawCapture),
331928
+ source_time_ms: input.eventTimeMs,
331929
+ received_time_ms: input.receivedTimeMs,
331930
+ snapshot_id: input.snapshotId,
331931
+ construction_mode: "sampled_top_n_snapshot",
331932
+ gap_policy: "record_gap",
331933
+ depth_limit: input.depthLimit,
331934
+ sequence: input.sequence,
331935
+ exact_l2_reconstruction_complete: 0
331936
+ };
331775
331937
  }
331776
- function classifyPassiveOrderError(error48) {
331777
- if (error48 instanceof ccxt_default.InsufficientFunds) {
331778
- return "InsufficientFunds";
331938
+ function buildCanonicalOrderBookRows(input) {
331939
+ if (!Number.isSafeInteger(input.depthLimit) || input.depthLimit <= 0 || input.depthLimit > 500) {
331940
+ throw new OrderBookValidationError("depth limit must be an integer between 1 and 500");
331779
331941
  }
331780
- if (error48 instanceof ccxt_default.AuthenticationError) {
331781
- return "AuthenticationError";
331942
+ if (input.context.feed !== "ORDERBOOK") {
331943
+ throw new OrderBookValidationError("capture context feed is not ORDERBOOK");
331782
331944
  }
331783
- const message = getErrorMessage(error48);
331784
- if (error48 instanceof ccxt_default.OrderImmediatelyFillable || identifiesWouldCross(message)) {
331785
- return PASSIVE_ORDER_ERROR_CODES.wouldCross;
331945
+ if (input.constructionMode === "exact_l2_reconstruction") {
331946
+ throw new OrderBookValidationError("exact L2 requires a complete continuity proof and is unsupported");
331786
331947
  }
331787
- if (error48 instanceof ccxt_default.NotSupported || identifiesUnsupported(message)) {
331788
- return PASSIVE_ORDER_ERROR_CODES.unsupported;
331948
+ let eventTimeMs;
331949
+ let receivedTimeMs;
331950
+ try {
331951
+ eventTimeMs = normalizeTimestampMs(input.snapshot.timestamp, "source_time_ms");
331952
+ receivedTimeMs = normalizeTimestampMs(input.snapshot.receivedTimestamp, "received_time_ms");
331953
+ } catch (error48) {
331954
+ throw new OrderBookValidationError(error48 instanceof Error ? error48.message : "invalid timestamp");
331789
331955
  }
331790
- return PASSIVE_ORDER_ERROR_CODES.rejected;
331956
+ if (receivedTimeMs < eventTimeMs) {
331957
+ throw new OrderBookValidationError("received timestamp precedes source timestamp");
331958
+ }
331959
+ const bids = validateSide("bid", input.snapshot.bids, input.depthLimit);
331960
+ const asks = validateSide("ask", input.snapshot.asks, input.depthLimit);
331961
+ const bestBid = bids[0];
331962
+ const bestAsk = asks[0];
331963
+ if (bestBid.price >= bestAsk.price) {
331964
+ throw new OrderBookValidationError("book is crossed or locked");
331965
+ }
331966
+ const sequence = parseSequence(input.snapshot.sequence);
331967
+ const midPrice = (bestBid.price + bestAsk.price) / 2;
331968
+ const spread2 = bestAsk.price - bestBid.price;
331969
+ const spreadBps = spread2 / midPrice * 1e4;
331970
+ const snapshotId = sha256Canonical({
331971
+ exchange: input.context.exchange.trim().toLowerCase(),
331972
+ trading_pair: input.context.symbol.trim().replace("/", "-"),
331973
+ source_time_ms: eventTimeMs,
331974
+ sequence,
331975
+ depth_limit: input.depthLimit,
331976
+ bids,
331977
+ asks,
331978
+ schema_version: input.context.schemaVersion
331979
+ });
331980
+ const common = commonEvidenceFields(input.context, input.rawCapture, {
331981
+ snapshotId,
331982
+ sequence,
331983
+ depthLimit: input.depthLimit,
331984
+ eventTimeMs,
331985
+ receivedTimeMs
331986
+ });
331987
+ const levels = [
331988
+ ["bid", bids],
331989
+ ["ask", asks]
331990
+ ].flatMap(([side, sideLevels]) => sideLevels.map(({ price, amount }, levelIndex) => {
331991
+ const row = checksumRow({
331992
+ ...common,
331993
+ side,
331994
+ level_index: levelIndex,
331995
+ price,
331996
+ amount,
331997
+ notional: price * amount,
331998
+ mid_price: midPrice,
331999
+ spread_from_mid_bps: Math.abs((price - midPrice) / midPrice) * 1e4
332000
+ });
332001
+ return { table: "market_data.cex_order_book_levels", row };
332002
+ }));
332003
+ const bands = normalizedBands(input.measurementBandsBps);
332004
+ const bidDepth = bands.map((band) => {
332005
+ const minimumPrice = bestBid.price * (1 - band / 1e4);
332006
+ return bids.filter(({ price }) => price >= minimumPrice).reduce((sum3, { amount }) => sum3 + amount, 0);
332007
+ });
332008
+ const askDepth = bands.map((band) => {
332009
+ const maximumPrice = bestAsk.price * (1 + band / 1e4);
332010
+ return asks.filter(({ price }) => price <= maximumPrice).reduce((sum3, { amount }) => sum3 + amount, 0);
332011
+ });
332012
+ const summaryRow = checksumRow({
332013
+ ...common,
332014
+ best_bid: bestBid.price,
332015
+ best_ask: bestAsk.price,
332016
+ best_bid_amount: bestBid.amount,
332017
+ best_ask_amount: bestAsk.amount,
332018
+ mid_price: midPrice,
332019
+ spread: spread2,
332020
+ spread_bps: spreadBps,
332021
+ staleness_ms: receivedTimeMs - eventTimeMs,
332022
+ bid_level_count: bids.length,
332023
+ ask_level_count: asks.length,
332024
+ measurement_bands_bps: bands,
332025
+ bid_depth_by_band: bidDepth,
332026
+ ask_depth_by_band: askDepth
332027
+ });
332028
+ return {
332029
+ snapshotId,
332030
+ levels,
332031
+ summary: {
332032
+ table: "market_data.cex_order_book_depth_summary",
332033
+ row: summaryRow
332034
+ }
332035
+ };
331791
332036
  }
331792
332037
 
331793
- // src/handlers/execute-action/orders.ts
331794
- async function handleCreateOrder(ctx) {
331795
- const {
331796
- call,
331797
- wrappedCallback,
331798
- policy,
331799
- brokers,
331800
- metadata,
331801
- normalizedCex,
331802
- cex: cex3,
332038
+ // src/helpers/market-data-archive/capture-context.ts
332039
+ function createMarketCaptureContext(input) {
332040
+ const environment = input.environment ?? "development";
332041
+ const deploymentId = input.deploymentId.trim();
332042
+ if (!deploymentId)
332043
+ throw new Error("deployment_id must not be empty");
332044
+ const configuredBundle = input.captureBundleId?.trim();
332045
+ if (environment === "production" && !configuredBundle) {
332046
+ throw new Error("capture_bundle_id is required for production market capture");
332047
+ }
332048
+ const exchange = input.exchange.trim().toLowerCase();
332049
+ const symbol2 = input.symbol.trim();
332050
+ if (!exchange || !symbol2) {
332051
+ throw new Error("exchange and symbol are required for market capture");
332052
+ }
332053
+ return {
332054
+ source: input.source,
332055
+ deploymentId,
332056
+ captureBundleId: configuredBundle ?? `development:${deploymentId}`,
332057
+ exchange,
331803
332058
  symbol: symbol2,
331804
- selectedBrokerAccount,
331805
- broker,
331806
- verity,
331807
- applyVerityToBroker,
331808
- useVerity,
331809
- verityProverUrl,
331810
- otelMetrics,
331811
- brokerArchiver,
331812
- orderActivityTracker
331813
- } = ctx;
331814
- const verityProof = verity.proof;
331815
- const orderValue = parsePayloadForAction(ctx, CreateOrderPayloadSchema);
331816
- if (orderValue === null)
331817
- return;
331818
- const isPassiveOrder = orderValue.orderIntent === "passive_only";
331819
- if (isPassiveOrder && orderValue.orderType !== "limit") {
331820
- return ctx.wrappedCallback({
331821
- code: grpc6.status.INVALID_ARGUMENT,
331822
- message: "ValidationError: passive_only order intent requires a limit order"
331823
- }, null);
332059
+ assetType: input.assetType,
332060
+ feed: input.feed,
332061
+ provider: input.provider?.trim() || `ccxt:${exchange}`,
332062
+ sourceMode: input.sourceMode,
332063
+ schemaVersion: MARKET_CAPTURE_SCHEMA_VERSION,
332064
+ checksumAlgorithm: CHECKSUM_ALGORITHM,
332065
+ provenanceComplete: true,
332066
+ timeframe: input.timeframe,
332067
+ accountSelector: input.accountSelector
332068
+ };
332069
+ }
332070
+ function captureEnvironmentFromEnv(value = process.env.CEX_BROKER_MARKET_CAPTURE_ENVIRONMENT) {
332071
+ const environment = value?.trim() || "development";
332072
+ if (environment !== "development" && environment !== "production") {
332073
+ throw new Error("CEX_BROKER_MARKET_CAPTURE_ENVIRONMENT must be development or production");
331824
332074
  }
331825
- const createOrderParams = {
331826
- ...orderValue.params,
331827
- ...orderValue.clientOrderId !== undefined && {
331828
- clientOrderId: orderValue.clientOrderId
331829
- },
331830
- ...isPassiveOrder && { postOnly: true }
332075
+ return environment;
332076
+ }
332077
+
332078
+ // src/helpers/market-data-archive/ohlcv-bar-tracker.ts
332079
+ function isFiniteNumber(value) {
332080
+ return typeof value === "number" && Number.isFinite(value);
332081
+ }
332082
+ function parseOhlcvBar(value) {
332083
+ if (!Array.isArray(value) || value.length < 6) {
332084
+ return null;
332085
+ }
332086
+ const [openTimeMs, open, high, low, close, volume, quoteVolume] = value;
332087
+ if (!isFiniteNumber(openTimeMs) || !isFiniteNumber(open) || !isFiniteNumber(high) || !isFiniteNumber(low) || !isFiniteNumber(close) || !isFiniteNumber(volume)) {
332088
+ return null;
332089
+ }
332090
+ const bar = {
332091
+ openTimeMs,
332092
+ open,
332093
+ high,
332094
+ low,
332095
+ close,
332096
+ volume
331831
332097
  };
331832
- let resolvedOrderTelemetry = {};
331833
- let marketMetadataHash;
331834
- let submission = "not_attempted";
331835
- try {
331836
- if (!broker) {
331837
- return ctx.wrappedCallback({
331838
- code: grpc6.status.INVALID_ARGUMENT,
331839
- message: `Invalid CEX key: ${cex3}. Supported keys: ${Object.keys(brokers).join(", ")}`
331840
- }, null);
332098
+ if (isFiniteNumber(quoteVolume)) {
332099
+ bar.quoteVolume = quoteVolume;
332100
+ }
332101
+ return bar;
332102
+ }
332103
+ function extractOhlcvBars(payload) {
332104
+ if (!Array.isArray(payload) || payload.length === 0) {
332105
+ return [];
332106
+ }
332107
+ const rawBars = Array.isArray(payload[0]) ? payload : [payload];
332108
+ const byOpenTime = new Map;
332109
+ for (const entry of rawBars) {
332110
+ const bar = parseOhlcvBar(entry);
332111
+ if (bar) {
332112
+ byOpenTime.set(bar.openTimeMs, bar);
331841
332113
  }
331842
- const resolution = await resolveOrderExecution(policy, broker, cex3, orderValue.fromToken, orderValue.toToken, orderValue.amount, orderValue.price, orderValue.marketType);
331843
- if (!resolution.valid || !resolution.symbol || !resolution.side) {
331844
- return ctx.wrappedCallback({
331845
- code: grpc6.status.INVALID_ARGUMENT,
331846
- message: resolution.error ?? "Order rejected by policy: market or limits not satisfied"
331847
- }, null);
332114
+ }
332115
+ return [...byOpenTime.values()].sort((a, b2) => a.openTimeMs - b2.openTimeMs);
332116
+ }
332117
+ class OhlcvBarTracker {
332118
+ lastOpenTimeMs = null;
332119
+ lastBar = null;
332120
+ process(payload, brokerVersion) {
332121
+ const bars = extractOhlcvBars(payload);
332122
+ if (bars.length === 0) {
332123
+ return [];
331848
332124
  }
331849
- resolvedOrderTelemetry = {
331850
- symbol: resolution.symbol,
331851
- side: resolution.side,
331852
- requestedQuantity: resolution.amountBase ?? orderValue.amount
331853
- };
331854
- if (selectedBrokerAccount?.label) {
331855
- orderActivityTracker?.record(cex3, selectedBrokerAccount.label, resolution.symbol);
332125
+ if (bars.length === 1) {
332126
+ const [bar] = bars;
332127
+ return bar ? this.processSingleBar(bar, brokerVersion) : [];
331856
332128
  }
331857
- const telemetryIds = extractOrderTelemetryIds(createOrderParams);
331858
- const submissionTimestamp = new Date().toISOString();
331859
- marketMetadataHash = await captureMarketMetadataSnapshot(brokerArchiver, broker, {
331860
- exchange: cex3,
331861
- accountSelector: selectedBrokerAccount?.label,
331862
- symbol: resolution.symbol,
331863
- action: "CreateOrder",
331864
- brokerObservedTimestamp: submissionTimestamp,
331865
- ...telemetryIds
331866
- });
331867
- submission = "in_flight";
331868
- const order = await broker.createOrder(resolution.symbol, orderValue.orderType, resolution.side, resolution.amountBase ?? orderValue.amount, orderValue.price, createOrderParams);
331869
- submission = "placed";
331870
- const createOrderContext = {
331871
- action: "CreateOrder",
331872
- cex: cex3,
331873
- accountLabel: selectedBrokerAccount?.label,
331874
- symbol: resolvedOrderTelemetry.symbol,
331875
- side: resolvedOrderTelemetry.side,
331876
- orderType: orderValue.orderType,
331877
- requestedQuantity: resolvedOrderTelemetry.requestedQuantity,
331878
- requestedNotional: orderValue.amount * orderValue.price,
331879
- orderAuthor: orderValue.orderAuthor,
331880
- brokerObservedTimestamp: submissionTimestamp,
331881
- ...telemetryIds
331882
- };
331883
- emitOrderExecutionTelemetryInBackground(otelMetrics, createOrderContext, order);
331884
- archiveOrderExecutionInBackground(brokerArchiver, createOrderContext, order, undefined, { marketMetadataHash });
331885
- ctx.wrappedCallback(null, {
331886
- result: JSON.stringify({
331887
- ...order,
331888
- ...isPassiveOrder && {
331889
- passivePlacementOutcome: "accepted_passive"
331890
- }
331891
- })
332129
+ return this.processBatch(bars, brokerVersion);
332130
+ }
332131
+ processSingleBar(currentBar, brokerVersion) {
332132
+ if (this.lastOpenTimeMs !== null && currentBar.openTimeMs < this.lastOpenTimeMs) {
332133
+ return [];
332134
+ }
332135
+ const candidates = [];
332136
+ if (this.lastOpenTimeMs !== null && this.lastBar !== null && currentBar.openTimeMs !== this.lastOpenTimeMs) {
332137
+ candidates.push({
332138
+ bar: this.lastBar,
332139
+ isClosed: true,
332140
+ brokerVersion
332141
+ });
332142
+ }
332143
+ candidates.push({
332144
+ bar: currentBar,
332145
+ isClosed: false,
332146
+ brokerVersion
331892
332147
  });
331893
- } catch (error48) {
331894
- rethrowArchiveDurabilityError(error48);
331895
- safeLogRedactedError("Order Creation failed", error48);
331896
- const failedCreateContext = {
331897
- action: "CreateOrder",
331898
- cex: cex3,
331899
- accountLabel: selectedBrokerAccount?.label,
331900
- symbol: resolvedOrderTelemetry.symbol ?? symbol2,
331901
- side: resolvedOrderTelemetry.side,
331902
- orderType: orderValue.orderType,
331903
- requestedQuantity: resolvedOrderTelemetry.requestedQuantity ?? orderValue.amount,
331904
- requestedNotional: orderValue.amount * orderValue.price,
331905
- orderAuthor: orderValue.orderAuthor,
331906
- ...extractOrderTelemetryIds(createOrderParams)
331907
- };
331908
- emitOrderExecutionTelemetryInBackground(otelMetrics, failedCreateContext, undefined, error48);
331909
- archiveOrderExecutionInBackground(brokerArchiver, failedCreateContext, undefined, error48, { marketMetadataHash });
331910
- if (isPassiveOrder && submission === "in_flight") {
331911
- const stableErrorCode = classifyPassiveOrderError(error48);
331912
- return rejectWithGrpcError(ctx, error48, {
331913
- message: `${stableErrorCode}: ${sanitizeErrorDetail(error48)}`,
331914
- preferStableMessageOnly: true
332148
+ this.lastOpenTimeMs = currentBar.openTimeMs;
332149
+ this.lastBar = currentBar;
332150
+ return candidates;
332151
+ }
332152
+ processBatch(bars, brokerVersion) {
332153
+ const firstBar = bars[0];
332154
+ const lastBar = bars[bars.length - 1];
332155
+ if (!firstBar || !lastBar) {
332156
+ return [];
332157
+ }
332158
+ const lastOpenTimeMs = this.lastOpenTimeMs;
332159
+ if (lastOpenTimeMs !== null && lastBar.openTimeMs < lastOpenTimeMs) {
332160
+ return [];
332161
+ }
332162
+ const barsToProcess = lastOpenTimeMs === null ? bars : bars.filter((bar) => bar.openTimeMs >= lastOpenTimeMs);
332163
+ const firstBarToProcess = barsToProcess[0];
332164
+ const lastBarToProcess = barsToProcess[barsToProcess.length - 1];
332165
+ if (!firstBarToProcess || !lastBarToProcess) {
332166
+ return [];
332167
+ }
332168
+ const candidates = [];
332169
+ if (this.lastBar !== null && lastOpenTimeMs !== null && lastOpenTimeMs < firstBarToProcess.openTimeMs) {
332170
+ candidates.push({
332171
+ bar: this.lastBar,
332172
+ isClosed: true,
332173
+ brokerVersion
331915
332174
  });
331916
332175
  }
331917
- ctx.wrappedCallback({
331918
- code: grpc6.status.INTERNAL,
331919
- message: `Order Creation failed: ${sanitizeErrorDetail(error48)}`
331920
- }, null);
332176
+ for (let index2 = 0;index2 < barsToProcess.length - 1; index2 += 1) {
332177
+ const bar = barsToProcess[index2];
332178
+ if (bar) {
332179
+ candidates.push({
332180
+ bar,
332181
+ isClosed: true,
332182
+ brokerVersion
332183
+ });
332184
+ }
332185
+ }
332186
+ candidates.push({
332187
+ bar: lastBarToProcess,
332188
+ isClosed: false,
332189
+ brokerVersion
332190
+ });
332191
+ this.lastOpenTimeMs = lastBarToProcess.openTimeMs;
332192
+ this.lastBar = lastBarToProcess;
332193
+ return candidates;
331921
332194
  }
331922
332195
  }
331923
- async function handleGetOrderDetails(ctx) {
331924
- const {
331925
- call,
331926
- wrappedCallback,
331927
- policy,
331928
- brokers,
331929
- metadata,
331930
- normalizedCex,
331931
- cex: cex3,
331932
- symbol: symbol2,
331933
- selectedBrokerAccount,
331934
- broker,
331935
- verity,
331936
- applyVerityToBroker,
331937
- useVerity,
331938
- verityProverUrl,
331939
- otelMetrics,
331940
- brokerArchiver,
331941
- orderActivityTracker
331942
- } = ctx;
331943
- const verityProof = verity.proof;
331944
- const getOrderValue = parsePayloadForAction(ctx, GetOrderDetailsPayloadSchema);
331945
- if (getOrderValue === null)
331946
- return;
331947
- try {
331948
- if (!broker) {
331949
- return ctx.wrappedCallback({
331950
- code: grpc6.status.INVALID_ARGUMENT,
331951
- message: `Invalid CEX key: ${cex3}. Supported keys: ${Object.keys(brokers).join(", ")}`
331952
- }, null);
331953
- }
331954
- const orderDetails = await broker.fetchOrder(getOrderValue.orderId, symbol2, { ...getOrderValue.params });
331955
- if (selectedBrokerAccount?.label && symbol2) {
331956
- orderActivityTracker?.record(cex3, selectedBrokerAccount.label, symbol2);
331957
- }
331958
- const getOrderContext = {
331959
- action: "GetOrderDetails",
331960
- cex: cex3,
331961
- accountLabel: selectedBrokerAccount?.label,
331962
- symbol: symbol2,
331963
- ...extractOrderTelemetryIds(getOrderValue.params)
331964
- };
331965
- emitOrderExecutionTelemetryInBackground(otelMetrics, getOrderContext, orderDetails);
331966
- archiveOrderExecutionInBackground(brokerArchiver, getOrderContext, orderDetails);
331967
- ctx.wrappedCallback(null, {
331968
- result: JSON.stringify({
331969
- orderId: orderDetails.id,
331970
- status: orderDetails.status,
331971
- amount: orderDetails.amount,
331972
- filled: orderDetails.filled,
331973
- remaining: orderDetails.remaining,
331974
- symbol: orderDetails.symbol,
331975
- side: orderDetails.side,
331976
- price: orderDetails.price
331977
- })
331978
- });
331979
- } catch (error48) {
331980
- safeLogError(`Error fetching order details from ${cex3}`, error48);
331981
- const failedGetOrderContext = {
331982
- action: "GetOrderDetails",
331983
- cex: cex3,
331984
- accountLabel: selectedBrokerAccount?.label,
331985
- symbol: symbol2,
331986
- ...extractOrderTelemetryIds(getOrderValue.params)
331987
- };
331988
- emitOrderExecutionTelemetryInBackground(otelMetrics, failedGetOrderContext, undefined, error48);
331989
- archiveOrderExecutionInBackground(brokerArchiver, failedGetOrderContext, undefined, error48);
331990
- ctx.wrappedCallback({
331991
- code: grpc6.status.INTERNAL,
331992
- message: `Failed to fetch order details from ${cex3}: ${sanitizeErrorDetail(error48)}`
331993
- }, null);
332196
+
332197
+ // src/helpers/market-data-archive/orderbook-depth.ts
332198
+ var DEFAULT_ORDERBOOK_ARCHIVE_DEPTH_LIMIT = 25;
332199
+ var MAX_ORDERBOOK_ARCHIVE_DEPTH_LIMIT = 500;
332200
+ function getOrderbookArchiveDepthLimit() {
332201
+ const raw = process.env.CEX_BROKER_ORDERBOOK_ARCHIVE_DEPTH_LIMIT;
332202
+ if (!raw) {
332203
+ return DEFAULT_ORDERBOOK_ARCHIVE_DEPTH_LIMIT;
332204
+ }
332205
+ const parsed = Number.parseInt(raw, 10);
332206
+ if (!Number.isFinite(parsed) || parsed <= 0) {
332207
+ return DEFAULT_ORDERBOOK_ARCHIVE_DEPTH_LIMIT;
331994
332208
  }
332209
+ return Math.min(parsed, MAX_ORDERBOOK_ARCHIVE_DEPTH_LIMIT);
331995
332210
  }
331996
- async function handleCancelOrder(ctx) {
331997
- const {
331998
- call,
331999
- wrappedCallback,
332000
- policy,
332001
- brokers,
332002
- metadata,
332003
- normalizedCex,
332004
- cex: cex3,
332005
- symbol: symbol2,
332006
- selectedBrokerAccount,
332007
- broker,
332008
- verity,
332009
- applyVerityToBroker,
332010
- useVerity,
332011
- verityProverUrl,
332012
- otelMetrics,
332013
- brokerArchiver,
332014
- orderActivityTracker
332015
- } = ctx;
332016
- const verityProof = verity.proof;
332017
- const cancelOrderValue = parsePayloadForAction(ctx, CancelOrderPayloadSchema);
332018
- if (cancelOrderValue === null)
332019
- return;
332020
- try {
332021
- if (!broker) {
332022
- return ctx.wrappedCallback({
332023
- code: grpc6.status.INVALID_ARGUMENT,
332024
- message: `Invalid CEX key: ${cex3}. Supported keys: ${Object.keys(brokers).join(", ")}`
332025
- }, null);
332211
+
332212
+ // src/helpers/market-data-archive/orderbook-sampler.ts
332213
+ var DEFAULT_ORDERBOOK_INTERVAL_MS = 1000;
332214
+ function getOrderbookIntervalMs() {
332215
+ const raw = process.env.CEX_BROKER_ORDERBOOK_INTERVAL_MS ?? process.env.CEX_BROKER_ORDERBOOK_TOB_INTERVAL_MS;
332216
+ if (!raw) {
332217
+ return DEFAULT_ORDERBOOK_INTERVAL_MS;
332218
+ }
332219
+ const parsed = Number.parseInt(raw, 10);
332220
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_ORDERBOOK_INTERVAL_MS;
332221
+ }
332222
+ function isMarketArchiveEnabled() {
332223
+ return process.env.CEX_BROKER_MARKET_ARCHIVE_ENABLED !== "false";
332224
+ }
332225
+
332226
+ class OrderbookSampler {
332227
+ intervalMs;
332228
+ lastEmitMs = null;
332229
+ constructor(intervalMs = getOrderbookIntervalMs()) {
332230
+ this.intervalMs = intervalMs;
332231
+ }
332232
+ shouldEmit(nowMs = Date.now()) {
332233
+ if (this.lastEmitMs !== null && nowMs < this.lastEmitMs) {
332234
+ this.lastEmitMs = nowMs;
332235
+ return true;
332026
332236
  }
332027
- const cancelOrderContext = {
332028
- action: "CancelOrder",
332029
- cex: cex3,
332030
- accountLabel: selectedBrokerAccount?.label,
332031
- symbol: symbol2,
332032
- ...extractOrderTelemetryIds(cancelOrderValue.params)
332033
- };
332034
- const cancelledOrder = await broker.cancelOrder(cancelOrderValue.orderId, symbol2, cancelOrderValue.params ?? {});
332035
- if (selectedBrokerAccount?.label && symbol2) {
332036
- orderActivityTracker?.record(cex3, selectedBrokerAccount.label, symbol2);
332237
+ if (this.lastEmitMs !== null && nowMs - this.lastEmitMs < this.intervalMs) {
332238
+ return false;
332037
332239
  }
332038
- emitOrderExecutionTelemetryInBackground(otelMetrics, cancelOrderContext, cancelledOrder);
332039
- archiveOrderExecutionInBackground(brokerArchiver, cancelOrderContext, cancelledOrder);
332040
- ctx.wrappedCallback(null, {
332041
- result: JSON.stringify({ ...cancelledOrder })
332042
- });
332043
- } catch (error48) {
332044
- safeLogError(`Error cancelling order from ${cex3}`, error48);
332045
- const failedCancelContext = {
332046
- action: "CancelOrder",
332047
- cex: cex3,
332048
- accountLabel: selectedBrokerAccount?.label,
332049
- symbol: symbol2,
332050
- ...extractOrderTelemetryIds(cancelOrderValue.params)
332051
- };
332052
- emitOrderExecutionTelemetryInBackground(otelMetrics, failedCancelContext, undefined, error48);
332053
- archiveOrderExecutionInBackground(brokerArchiver, failedCancelContext, undefined, error48);
332054
- ctx.wrappedCallback({
332055
- code: grpc6.status.INTERNAL,
332056
- message: `Failed to cancel order from ${cex3}: ${sanitizeErrorDetail(error48)}`
332057
- }, null);
332240
+ this.lastEmitMs = nowMs;
332241
+ return true;
332058
332242
  }
332059
332243
  }
332060
- async function handleOrders(ctx) {
332061
- if (ctx.action === Action.CreateOrder)
332062
- return handleCreateOrder(ctx);
332063
- if (ctx.action === Action.GetOrderDetails)
332064
- return handleGetOrderDetails(ctx);
332065
- if (ctx.action === Action.CancelOrder)
332066
- return handleCancelOrder(ctx);
332067
- }
332068
332244
 
332069
- // src/handlers/execute-action/pass-through.ts
332070
- var grpc7 = __toESM(require_src3(), 1);
332071
- async function handleFetchCurrency(ctx) {
332072
- const {
332073
- call,
332074
- wrappedCallback,
332075
- policy,
332076
- brokers,
332077
- metadata,
332078
- normalizedCex,
332079
- cex: cex3,
332080
- symbol: symbol2,
332081
- selectedBrokerAccount,
332082
- broker,
332083
- verity,
332084
- applyVerityToBroker,
332085
- useVerity,
332086
- verityProverUrl,
332087
- otelMetrics
332088
- } = ctx;
332089
- const verityProof = verity.proof;
332090
- if (!symbol2) {
332091
- return ctx.wrappedCallback({
332092
- code: grpc7.status.INVALID_ARGUMENT,
332093
- message: `ValidationError: Symbol required`
332094
- }, null);
332245
+ // src/helpers/market-data-archive/parse-stream.ts
332246
+ function isFiniteNumber2(value) {
332247
+ return typeof value === "number" && Number.isFinite(value);
332248
+ }
332249
+ function toNumber2(value) {
332250
+ if (isFiniteNumber2(value)) {
332251
+ return value;
332095
332252
  }
332096
- try {
332097
- const assetCode = symbol2.trim().toUpperCase();
332098
- const currencyInfo = await fetchCurrencyMetadata(broker, assetCode);
332099
- if (!currencyInfo) {
332100
- return ctx.wrappedCallback({
332101
- code: grpc7.status.NOT_FOUND,
332102
- message: `venue_discovery_unavailable: currency not found for ${assetCode}`
332103
- }, null);
332253
+ if (typeof value === "string") {
332254
+ const parsed = Number.parseFloat(value);
332255
+ if (Number.isFinite(parsed)) {
332256
+ return parsed;
332104
332257
  }
332105
- const networkEvidence = buildTransferNetworkEvidence(currencyInfo);
332106
- ctx.wrappedCallback(null, {
332107
- proof: ctx.verity.proof,
332108
- result: JSON.stringify({
332109
- ...currencyInfo,
332110
- exchange: normalizedCex,
332111
- asset: assetCode,
332112
- code: currencyInfo.code ?? assetCode,
332113
- id: currencyInfo.id ?? null,
332114
- networks: networkEvidence.networks,
332115
- networkAliases: networkEvidence.aliases,
332116
- raw: currencyInfo
332117
- })
332118
- });
332119
- } catch (error48) {
332120
- safeLogError(`Error fetching currency ${symbol2} from ${cex3}`, error48);
332121
- const message = getErrorMessage(error48);
332122
- ctx.wrappedCallback({
332123
- code: stableGrpcErrorCode(message) ?? mapCcxtErrorToGrpcStatus(error48) ?? grpc7.status.INTERNAL,
332124
- message: message.startsWith("venue_discovery_unavailable:") ? message : `venue_discovery_unavailable: ${message}`
332125
- }, null);
332126
332258
  }
332259
+ return;
332127
332260
  }
332128
- async function handleFetchAccountId(ctx) {
332129
- const {
332130
- call,
332131
- wrappedCallback,
332132
- policy,
332133
- brokers,
332134
- metadata,
332135
- normalizedCex,
332136
- cex: cex3,
332137
- symbol: symbol2,
332138
- selectedBrokerAccount,
332139
- broker,
332140
- verity,
332141
- applyVerityToBroker,
332142
- useVerity,
332143
- verityProverUrl,
332144
- otelMetrics
332145
- } = ctx;
332146
- const verityProof = verity.proof;
332261
+ function toStringId(value) {
332262
+ if (typeof value === "string" && value.trim()) {
332263
+ return value.trim();
332264
+ }
332265
+ if (typeof value === "number" && Number.isFinite(value)) {
332266
+ return String(value);
332267
+ }
332268
+ return;
332269
+ }
332270
+ function scalarTimestampMs(value, fallbackMs) {
332271
+ const numeric = toNumber2(value);
332272
+ if (numeric !== undefined) {
332273
+ return numeric < 1000000000000 ? numeric * 1000 : numeric;
332274
+ }
332275
+ if (typeof value === "string") {
332276
+ const parsed = Date.parse(value);
332277
+ if (Number.isFinite(parsed)) {
332278
+ return parsed;
332279
+ }
332280
+ }
332281
+ return fallbackMs;
332282
+ }
332283
+ function parseTrade(value, fallbackMs = Date.now()) {
332284
+ const record2 = asRecord(value);
332285
+ if (!record2) {
332286
+ return null;
332287
+ }
332288
+ const tradeId = toStringId(record2.id);
332289
+ const price = toNumber2(record2.price);
332290
+ const amount = toNumber2(record2.amount);
332291
+ const side = typeof record2.side === "string" ? record2.side.toLowerCase() : undefined;
332292
+ if (!tradeId || price === undefined || amount === undefined || !side) {
332293
+ return null;
332294
+ }
332295
+ const parsed = {
332296
+ tradeId,
332297
+ eventTimeMs: scalarTimestampMs(record2.timestamp, fallbackMs),
332298
+ side,
332299
+ price,
332300
+ amount
332301
+ };
332302
+ const cost = toNumber2(record2.cost);
332303
+ if (cost !== undefined) {
332304
+ parsed.cost = cost;
332305
+ }
332306
+ if (typeof record2.takerOrMaker === "string") {
332307
+ parsed.takerOrMaker = record2.takerOrMaker;
332308
+ }
332309
+ return parsed;
332310
+ }
332311
+ function extractTrades(payload, fallbackMs = Date.now()) {
332312
+ if (Array.isArray(payload)) {
332313
+ return payload.map((entry) => parseTrade(entry, fallbackMs)).filter((entry) => entry !== null);
332314
+ }
332315
+ const single = parseTrade(payload, fallbackMs);
332316
+ return single ? [single] : [];
332317
+ }
332318
+ function parseTicker(value, fallbackMs) {
332319
+ const record2 = asRecord(value);
332320
+ if (!record2) {
332321
+ return null;
332322
+ }
332323
+ const parsed = {
332324
+ eventTimeMs: scalarTimestampMs(record2.timestamp, fallbackMs)
332325
+ };
332326
+ const fields = [
332327
+ ["last", record2.last],
332328
+ ["bid", record2.bid],
332329
+ ["ask", record2.ask],
332330
+ ["high", record2.high],
332331
+ ["low", record2.low],
332332
+ ["open", record2.open],
332333
+ ["close", record2.close],
332334
+ ["baseVolume", record2.baseVolume],
332335
+ ["quoteVolume", record2.quoteVolume],
332336
+ ["change", record2.change],
332337
+ ["percentage", record2.percentage]
332338
+ ];
332339
+ for (const [key, rawValue] of fields) {
332340
+ const numeric = toNumber2(rawValue);
332341
+ if (numeric !== undefined) {
332342
+ parsed[key] = numeric;
332343
+ }
332344
+ }
332345
+ return parsed;
332346
+ }
332347
+
332348
+ // src/helpers/market-data-archive/rows.ts
332349
+ function compactUndefined3(record2) {
332350
+ return Object.fromEntries(Object.entries(record2).filter(([, value]) => value !== undefined));
332351
+ }
332352
+ function withNormalizedChecksum(record2) {
332353
+ const compact = compactUndefined3(record2);
332354
+ return {
332355
+ ...compact,
332356
+ normalized_row_checksum: sha256Canonical(compact)
332357
+ };
332358
+ }
332359
+ function buildCanonicalCexStreamEventRow(context2, rawCapture) {
332360
+ const row = withNormalizedChecksum({
332361
+ ...captureCoreFields(context2, rawCapture),
332362
+ stream_type: context2.feed,
332363
+ event_time_ms: rawCapture.eventTimeMs,
332364
+ payload_encoding: "canonical_json_v1",
332365
+ payload_json: canonicalSerialize(rawCapture.redactedPayload)
332366
+ });
332367
+ return { table: "market_data.cex_stream_events", row };
332368
+ }
332369
+ function buildCanonicalTickerEventRow(context2, rawCapture, ticker) {
332370
+ if (context2.feed !== "TICKER") {
332371
+ throw new Error("Ticker row requires a TICKER capture context");
332372
+ }
332373
+ const row = withNormalizedChecksum({
332374
+ ...captureCoreFields(context2, rawCapture),
332375
+ source_time_ms: ticker.eventTimeMs,
332376
+ event_time_ms: ticker.eventTimeMs,
332377
+ last: ticker.last,
332378
+ bid: ticker.bid,
332379
+ ask: ticker.ask,
332380
+ high: ticker.high,
332381
+ low: ticker.low,
332382
+ open: ticker.open,
332383
+ close: ticker.close,
332384
+ base_volume: ticker.baseVolume,
332385
+ quote_volume: ticker.quoteVolume,
332386
+ change: ticker.change,
332387
+ percentage: ticker.percentage
332388
+ });
332389
+ return { table: "market_data.cex_ticker_events", row };
332390
+ }
332391
+ function buildCanonicalTradeRow(context2, rawCapture, trade) {
332392
+ if (context2.feed !== "TRADES") {
332393
+ throw new Error("Trade row requires a TRADES capture context");
332394
+ }
332395
+ const row = withNormalizedChecksum({
332396
+ ...captureCoreFields(context2, rawCapture),
332397
+ source_time_ms: trade.eventTimeMs,
332398
+ trade_id: trade.tradeId,
332399
+ event_time_ms: trade.eventTimeMs,
332400
+ side: trade.side,
332401
+ price: trade.price,
332402
+ amount: trade.amount,
332403
+ cost: trade.cost,
332404
+ taker_or_maker: trade.takerOrMaker
332405
+ });
332406
+ return { table: "market_data.cex_trades", row };
332407
+ }
332408
+ function buildCanonicalOhlcvRow(input) {
332409
+ if (input.context.feed !== "OHLCV") {
332410
+ throw new Error("OHLCV row requires an OHLCV capture context");
332411
+ }
332412
+ const row = withNormalizedChecksum({
332413
+ ...captureCoreFields(input.context, input.rawCapture),
332414
+ source_time_ms: input.bar.openTimeMs,
332415
+ timeframe: input.context.timeframe ?? "1m",
332416
+ open_time_ms: input.bar.openTimeMs,
332417
+ open: input.bar.open,
332418
+ high: input.bar.high,
332419
+ low: input.bar.low,
332420
+ close: input.bar.close,
332421
+ volume: input.bar.volume,
332422
+ quote_volume: input.bar.quoteVolume,
332423
+ is_closed: input.isClosed ? 1 : 0,
332424
+ broker_version: input.brokerVersion
332425
+ });
332426
+ return { table: "market_data.cex_ohlcv", row };
332427
+ }
332428
+ function buildCexStreamEventRow(input) {
332429
+ const receivedTimeMs = input.receivedTimestamp;
332430
+ const redactedPayload = redactStreamPayload(input.payload);
332431
+ const tags = buildCommonArchiveTags({
332432
+ source: input.source,
332433
+ deploymentId: input.deploymentId,
332434
+ accountSelector: input.accountSelector,
332435
+ exchange: input.exchange,
332436
+ symbol: input.symbol,
332437
+ brokerObservedTimestamp: new Date(receivedTimeMs).toISOString()
332438
+ });
332439
+ return {
332440
+ table: "market_data.cex_stream_events",
332441
+ row: compactUndefined3({
332442
+ ...tags,
332443
+ asset_type: input.assetType,
332444
+ stream_type: input.streamType,
332445
+ event_time_ms: input.eventTimeMs ?? receivedTimeMs,
332446
+ received_time_ms: receivedTimeMs,
332447
+ payload_json: JSON.stringify(redactedPayload)
332448
+ })
332449
+ };
332450
+ }
332451
+
332452
+ // src/helpers/market-data-archive/capture.ts
332453
+ async function recordWatchMetric(otelMetrics, metricName, labels) {
332147
332454
  try {
332148
- const accountId = await broker.fetchAccountId();
332149
- return ctx.wrappedCallback(null, {
332455
+ await otelMetrics?.recordCounter(metricName, 1, labels);
332456
+ } catch {}
332457
+ }
332458
+ function watchLabels(stream4, input, archiver, feed) {
332459
+ return {
332460
+ stream: stream4,
332461
+ feed,
332462
+ source: archiver?.getSource() ?? "disabled",
332463
+ exchange: input.exchange,
332464
+ symbol: input.symbol
332465
+ };
332466
+ }
332467
+ function resolveCaptureContext(archiver, input, feed, sourceMode) {
332468
+ return createMarketCaptureContext({
332469
+ source: archiver.getSource(),
332470
+ deploymentId: archiver.getDeploymentId(),
332471
+ captureBundleId: process.env.CEX_BROKER_CAPTURE_BUNDLE_ID,
332472
+ exchange: input.exchange,
332473
+ symbol: input.symbol,
332474
+ assetType: input.assetType,
332475
+ feed,
332476
+ provider: `ccxt:${input.exchange.trim().toLowerCase()}`,
332477
+ sourceMode,
332478
+ timeframe: input.timeframe,
332479
+ accountSelector: input.accountSelector,
332480
+ environment: captureEnvironmentFromEnv()
332481
+ });
332482
+ }
332483
+ function archiveOrderbookInBackground(archiver, otelMetrics, input, options) {
332484
+ const labels = watchLabels("orderbook", input, archiver, "ORDERBOOK");
332485
+ recordWatchMetric(otelMetrics, "cex_watch_frames_received_total", labels);
332486
+ if (!isMarketArchiveEnabled() || !archiver?.isEnabled()) {
332487
+ return;
332488
+ }
332489
+ if (options?.sampledOut) {
332490
+ recordWatchMetric(otelMetrics, "cex_watch_frames_sampled_out_total", labels);
332491
+ return;
332492
+ }
332493
+ queueMicrotask(() => {
332494
+ try {
332495
+ const context2 = resolveCaptureContext(archiver, input, "ORDERBOOK", options?.sourceMode ?? "broker_live_sampling_v1");
332496
+ const rawCapture = createRawCapture(context2, {
332497
+ payload: input.snapshot,
332498
+ eventTimeMs: input.snapshot.timestamp,
332499
+ receivedTimeMs: input.snapshot.receivedTimestamp,
332500
+ scope: "ccxt_normalized_object"
332501
+ });
332502
+ const canonical = buildCanonicalOrderBookRows({
332503
+ context: context2,
332504
+ snapshot: input.snapshot,
332505
+ rawCapture,
332506
+ depthLimit: options?.depthLimit ?? getOrderbookArchiveDepthLimit()
332507
+ });
332508
+ archiver.enqueue(buildCanonicalCexStreamEventRow(context2, rawCapture));
332509
+ for (const row of canonical.levels)
332510
+ archiver.enqueue(row);
332511
+ archiver.enqueue(canonical.summary);
332512
+ recordWatchMetric(otelMetrics, "cex_watch_frames_archived_total", labels);
332513
+ } catch (error48) {
332514
+ rethrowArchiveDurabilityError(error48);
332515
+ if (error48 instanceof OrderBookValidationError) {
332516
+ recordWatchMetric(otelMetrics, "cex_watch_frames_invalid_total", {
332517
+ ...labels,
332518
+ reason: error48.reason
332519
+ });
332520
+ }
332521
+ log.warn("Failed to archive orderbook snapshot", { error: error48 });
332522
+ }
332523
+ });
332524
+ }
332525
+ function archiveOhlcvInBackground(archiver, otelMetrics, tracker, input) {
332526
+ const labels = watchLabels("ohlcv", input, archiver, "OHLCV");
332527
+ recordWatchMetric(otelMetrics, "cex_watch_frames_received_total", labels);
332528
+ if (!isMarketArchiveEnabled() || !archiver?.isEnabled()) {
332529
+ return;
332530
+ }
332531
+ queueMicrotask(() => {
332532
+ try {
332533
+ const candidates = tracker.process(input.payload, input.receivedTimestamp);
332534
+ const context2 = resolveCaptureContext(archiver, input, "OHLCV", input.sourceMode ?? "broker_live_stream_v1");
332535
+ const rawCapture = candidates.length > 0 ? createRawCapture(context2, {
332536
+ payload: input.payload,
332537
+ eventTimeMs: candidates[0]?.bar.openTimeMs ?? input.receivedTimestamp,
332538
+ receivedTimeMs: input.receivedTimestamp,
332539
+ scope: "ccxt_normalized_object"
332540
+ }) : undefined;
332541
+ if (rawCapture) {
332542
+ archiver.enqueue(buildCanonicalCexStreamEventRow(context2, rawCapture));
332543
+ }
332544
+ for (const candidate of candidates) {
332545
+ if (rawCapture) {
332546
+ archiver.enqueue(buildCanonicalOhlcvRow({
332547
+ context: context2,
332548
+ rawCapture,
332549
+ bar: candidate.bar,
332550
+ isClosed: candidate.isClosed,
332551
+ brokerVersion: candidate.brokerVersion
332552
+ }));
332553
+ }
332554
+ }
332555
+ if (candidates.length > 0) {
332556
+ recordWatchMetric(otelMetrics, "cex_watch_frames_archived_total", labels);
332557
+ }
332558
+ } catch (error48) {
332559
+ rethrowArchiveDurabilityError(error48);
332560
+ log.warn("Failed to archive OHLCV candle", { error: error48 });
332561
+ }
332562
+ });
332563
+ }
332564
+ function createOrderbookSampler() {
332565
+ return new OrderbookSampler;
332566
+ }
332567
+ function createOhlcvBarTracker() {
332568
+ return new OhlcvBarTracker;
332569
+ }
332570
+ function archiveMarketRowsInBackground(archiver, otelMetrics, stream4, input, feed, enqueueRows) {
332571
+ const labels = watchLabels(stream4, input, archiver, feed);
332572
+ recordWatchMetric(otelMetrics, "cex_watch_frames_received_total", labels);
332573
+ if (!isMarketArchiveEnabled() || !archiver?.isEnabled()) {
332574
+ return;
332575
+ }
332576
+ queueMicrotask(() => {
332577
+ try {
332578
+ const rows = enqueueRows();
332579
+ for (const row of rows) {
332580
+ archiver.enqueue(row);
332581
+ }
332582
+ if (rows.length > 0) {
332583
+ recordWatchMetric(otelMetrics, "cex_watch_frames_archived_total", labels);
332584
+ }
332585
+ } catch (error48) {
332586
+ rethrowArchiveDurabilityError(error48);
332587
+ log.warn(`Failed to archive ${stream4} market data`, { error: error48 });
332588
+ }
332589
+ });
332590
+ }
332591
+ function archiveTradesInBackground(archiver, otelMetrics, input) {
332592
+ archiveMarketRowsInBackground(archiver, otelMetrics, "trades", input, "TRADES", () => {
332593
+ if (!archiver)
332594
+ return [];
332595
+ const trades = extractTrades(input.payload, input.receivedTimestamp);
332596
+ if (trades.length === 0)
332597
+ return [];
332598
+ const context2 = resolveCaptureContext(archiver, input, "TRADES", "broker_live_stream_v1");
332599
+ const raw = createRawCapture(context2, {
332600
+ payload: input.payload,
332601
+ eventTimeMs: trades[0]?.eventTimeMs ?? input.receivedTimestamp,
332602
+ receivedTimeMs: input.receivedTimestamp,
332603
+ scope: "ccxt_normalized_object"
332604
+ });
332605
+ return [
332606
+ buildCanonicalCexStreamEventRow(context2, raw),
332607
+ ...trades.map((trade) => buildCanonicalTradeRow(context2, raw, trade))
332608
+ ];
332609
+ });
332610
+ }
332611
+ function archiveTickerInBackground(archiver, otelMetrics, input) {
332612
+ archiveMarketRowsInBackground(archiver, otelMetrics, "ticker", input, "TICKER", () => {
332613
+ const ticker = parseTicker(input.payload, input.receivedTimestamp);
332614
+ if (!ticker || !archiver)
332615
+ return [];
332616
+ const context2 = resolveCaptureContext(archiver, input, "TICKER", "broker_live_stream_v1");
332617
+ const raw = createRawCapture(context2, {
332618
+ payload: input.payload,
332619
+ eventTimeMs: ticker.eventTimeMs,
332620
+ receivedTimeMs: input.receivedTimestamp,
332621
+ scope: "ccxt_normalized_object"
332622
+ });
332623
+ return [
332624
+ buildCanonicalCexStreamEventRow(context2, raw),
332625
+ buildCanonicalTickerEventRow(context2, raw, ticker)
332626
+ ];
332627
+ });
332628
+ }
332629
+ function archiveCexStreamEventInBackground(archiver, otelMetrics, input) {
332630
+ archiveMarketRowsInBackground(archiver, otelMetrics, "stream", input, input.streamType, () => [
332631
+ buildCexStreamEventRow({
332632
+ ...input,
332633
+ source: archiver?.getSource()
332634
+ })
332635
+ ]);
332636
+ }
332637
+
332638
+ // src/handlers/execute-action/order-book-call.ts
332639
+ async function handleOrderBookCall(ctx) {
332640
+ const parsedOrderBookCall = parseOrderBookCallPayload(ctx.call.request.payload, {
332641
+ exchange: ctx.normalizedCex,
332642
+ symbol: ctx.symbol
332643
+ });
332644
+ if (parsedOrderBookCall.kind === "error") {
332645
+ ctx.wrappedCallback({
332646
+ code: grpc4.status.INVALID_ARGUMENT,
332647
+ message: parsedOrderBookCall.message
332648
+ }, null);
332649
+ return true;
332650
+ }
332651
+ if (parsedOrderBookCall.kind !== "order_book") {
332652
+ return false;
332653
+ }
332654
+ const orderBookBroker = ctx.broker;
332655
+ if (!orderBookBroker) {
332656
+ ctx.wrappedCallback({
332657
+ code: grpc4.status.INVALID_ARGUMENT,
332658
+ message: `Unsupported exchange for order-book market data: ${ctx.normalizedCex}`
332659
+ }, null);
332660
+ return true;
332661
+ }
332662
+ ctx.applyVerityToBroker(orderBookBroker);
332663
+ try {
332664
+ const orderBookPayload = parsedOrderBookCall.payload;
332665
+ if (orderBookPayload.method === ORDER_BOOK_CALL_METHODS.FETCH_CAPABILITY) {
332666
+ ctx.wrappedCallback(null, {
332667
+ proof: ctx.verity.proof,
332668
+ result: JSON.stringify(buildOrderBookCapability(orderBookBroker, orderBookPayload))
332669
+ });
332670
+ return true;
332671
+ }
332672
+ if (orderBookPayload.method === ORDER_BOOK_CALL_METHODS.FETCH_HISTORICAL_SNAPSHOTS) {
332673
+ ctx.wrappedCallback(null, {
332674
+ proof: ctx.verity.proof,
332675
+ result: JSON.stringify(buildHistoricalOrderBookUnsupported(orderBookPayload))
332676
+ });
332677
+ return true;
332678
+ }
332679
+ const fetchOrderBook = orderBookBroker.fetchOrderBook;
332680
+ const canFetchOrderBook = typeof fetchOrderBook === "function" && orderBookBroker.has?.fetchOrderBook !== false;
332681
+ if (!canFetchOrderBook) {
332682
+ ctx.wrappedCallback({
332683
+ code: grpc4.status.UNIMPLEMENTED,
332684
+ message: `Order-book snapshot unsupported for ${ctx.normalizedCex}`
332685
+ }, null);
332686
+ return true;
332687
+ }
332688
+ const receivedTimestamp = Date.now();
332689
+ const rawOrderBook = await fetchOrderBook.call(orderBookBroker, orderBookPayload.symbol, orderBookPayload.depthLimit);
332690
+ const snapshot = normalizeOrderBookSnapshot(rawOrderBook, {
332691
+ exchange: orderBookPayload.exchange,
332692
+ symbol: orderBookPayload.symbol,
332693
+ depthLimit: orderBookPayload.depthLimit,
332694
+ receivedTimestamp
332695
+ });
332696
+ ctx.wrappedCallback(null, {
332150
332697
  proof: ctx.verity.proof,
332151
- result: JSON.stringify({ accountId })
332698
+ result: JSON.stringify(snapshot)
332699
+ });
332700
+ archiveOrderbookInBackground(ctx.brokerArchiver, ctx.otelMetrics, {
332701
+ exchange: orderBookPayload.exchange,
332702
+ symbol: orderBookPayload.symbol,
332703
+ assetType: "spot",
332704
+ accountSelector: ctx.selectedBrokerAccount?.label,
332705
+ deploymentId: ctx.brokerArchiver?.getDeploymentId() ?? "unarchived",
332706
+ snapshot
332707
+ }, {
332708
+ sourceMode: "broker_current_snapshot_v1",
332709
+ depthLimit: orderBookPayload.depthLimit
332152
332710
  });
332153
332711
  } catch (error48) {
332154
- safeLogError(`Error fetching account ID ${cex3}`, error48);
332712
+ safeLogError("Order-book Call failed", error48);
332155
332713
  ctx.wrappedCallback({
332156
- code: grpc7.status.INTERNAL,
332157
- message: `Error fetching account ID from ${cex3}`
332714
+ code: mapCcxtErrorToGrpcStatus(error48) ?? grpc4.status.INTERNAL,
332715
+ message: `Order-book Call failed: ${sanitizeErrorDetail(error48)}`
332158
332716
  }, null);
332159
332717
  }
332718
+ return true;
332160
332719
  }
332161
- async function handleFetchFees(ctx) {
332720
+
332721
+ // src/handlers/execute-action/registry.ts
332722
+ var grpc11 = __toESM(require_src3(), 1);
332723
+
332724
+ // src/handlers/execute-action/internal-transfer.ts
332725
+ var grpc5 = __toESM(require_src3(), 1);
332726
+ async function handleInternalTransfer(ctx) {
332162
332727
  const {
332163
- call,
332164
- wrappedCallback,
332165
- policy,
332166
332728
  brokers,
332167
332729
  metadata,
332168
332730
  normalizedCex,
332169
- cex: cex3,
332170
332731
  symbol: symbol2,
332171
- selectedBrokerAccount,
332172
- broker,
332173
332732
  verity,
332174
- applyVerityToBroker,
332175
332733
  useVerity,
332176
332734
  verityProverUrl,
332177
- otelMetrics
332735
+ brokerArchiver
332178
332736
  } = ctx;
332179
- const verityProof = verity.proof;
332180
332737
  if (!symbol2) {
332181
332738
  return ctx.wrappedCallback({
332182
- code: grpc7.status.INVALID_ARGUMENT,
332739
+ code: grpc5.status.INVALID_ARGUMENT,
332183
332740
  message: `ValidationError: Symbol required`
332184
332741
  }, null);
332185
332742
  }
332186
- const feesPayload = parsePayloadForAction(ctx, FetchFeesPayloadSchema);
332187
- if (feesPayload === null)
332743
+ const transferPayload = parsePayloadForAction(ctx, InternalTransferPayloadSchema);
332744
+ if (transferPayload === null)
332188
332745
  return;
332189
- const includeAllFees = feesPayload.includeAllFees || feesPayload.includeFundingFees === true;
332746
+ if (normalizedCex !== "binance") {
332747
+ return ctx.wrappedCallback({
332748
+ code: grpc5.status.UNIMPLEMENTED,
332749
+ message: `InternalTransfer is only supported for Binance`
332750
+ }, null);
332751
+ }
332752
+ const pool = brokers[normalizedCex];
332753
+ if (!pool) {
332754
+ return ctx.wrappedCallback({
332755
+ code: grpc5.status.FAILED_PRECONDITION,
332756
+ message: `No broker accounts configured for ${normalizedCex}`
332757
+ }, null);
332758
+ }
332759
+ const fromSelector = transferPayload.fromAccount ?? getCurrentBrokerSelector(metadata);
332760
+ const toSelector = transferPayload.toAccount ?? "primary";
332761
+ const sourceAccount = resolveBrokerAccount(pool, fromSelector);
332762
+ if (!sourceAccount) {
332763
+ return ctx.wrappedCallback({
332764
+ code: grpc5.status.INVALID_ARGUMENT,
332765
+ message: `Source account "${fromSelector}" is not configured`
332766
+ }, null);
332767
+ }
332768
+ const destAccount = resolveBrokerAccount(pool, toSelector);
332769
+ if (!destAccount) {
332770
+ return ctx.wrappedCallback({
332771
+ code: grpc5.status.INVALID_ARGUMENT,
332772
+ message: `Destination account "${toSelector}" is not configured`
332773
+ }, null);
332774
+ }
332190
332775
  try {
332191
- await broker.loadMarkets();
332192
- const fetchFundingFees = async (currencyCodes) => {
332193
- let fundingFeeSource2 = "unavailable";
332194
- const fundingFeesByCurrency2 = {};
332195
- if (broker.has.fetchDepositWithdrawFees) {
332196
- try {
332197
- const feeMap = await broker.fetchDepositWithdrawFees(currencyCodes);
332198
- for (const code of currencyCodes) {
332199
- const feeInfo = feeMap[code];
332200
- if (!feeInfo) {
332201
- continue;
332202
- }
332203
- const fallbackFee = feeInfo.fee !== undefined || feeInfo.percentage !== undefined ? {
332204
- fee: feeInfo.fee ?? null,
332205
- percentage: feeInfo.percentage ?? null
332206
- } : null;
332207
- fundingFeesByCurrency2[code] = {
332208
- deposit: feeInfo.deposit ?? fallbackFee,
332209
- withdraw: feeInfo.withdraw ?? fallbackFee,
332210
- networks: feeInfo.networks ?? {}
332211
- };
332212
- }
332213
- if (Object.keys(fundingFeesByCurrency2).length > 0) {
332214
- fundingFeeSource2 = "fetchDepositWithdrawFees";
332215
- }
332216
- } catch (error48) {
332217
- safeLogError(`Error fetching deposit/withdraw fee map for ${symbol2} from ${cex3}`, error48);
332218
- }
332219
- }
332220
- if (fundingFeeSource2 === "unavailable") {
332221
- try {
332222
- const currencies = await broker.fetchCurrencies();
332223
- for (const code of currencyCodes) {
332224
- const currency = currencies[code];
332225
- if (!currency) {
332226
- continue;
332227
- }
332228
- fundingFeesByCurrency2[code] = {
332229
- deposit: {
332230
- enabled: currency.deposit ?? null
332231
- },
332232
- withdraw: {
332233
- enabled: currency.withdraw ?? null,
332234
- fee: currency.fee ?? null,
332235
- limits: currency.limits?.withdraw ?? null
332236
- },
332237
- networks: currency.networks ?? {}
332238
- };
332239
- }
332240
- if (Object.keys(fundingFeesByCurrency2).length > 0) {
332241
- fundingFeeSource2 = "currencies";
332242
- }
332243
- } catch (error48) {
332244
- safeLogError(`Error fetching currency metadata for fees for ${symbol2} from ${cex3}`, error48);
332245
- }
332246
- }
332247
- return { fundingFeeSource: fundingFeeSource2, fundingFeesByCurrency: fundingFeesByCurrency2 };
332248
- };
332249
- const isMarketSymbol = symbol2.includes("/");
332250
- if (isMarketSymbol) {
332251
- const market = await broker.market(symbol2);
332252
- const generalFee = broker.fees ?? null;
332253
- const feeStatus = broker.fees ? "available" : "unknown";
332254
- if (!broker.fees) {
332255
- log.warn(`Fee metadata unavailable for ${cex3}`, { symbol: symbol2 });
332256
- }
332257
- if (!includeAllFees) {
332258
- return ctx.wrappedCallback(null, {
332259
- proof: ctx.verity.proof,
332260
- result: JSON.stringify({
332261
- feeScope: "market",
332262
- generalFee,
332263
- feeStatus,
332264
- market
332265
- })
332266
- });
332267
- }
332268
- const currencyCodes = Array.from(new Set([market.base, market.quote]));
332269
- const { fundingFeeSource: fundingFeeSource2, fundingFeesByCurrency: fundingFeesByCurrency2 } = await fetchFundingFees(currencyCodes);
332270
- return ctx.wrappedCallback(null, {
332271
- proof: ctx.verity.proof,
332272
- result: JSON.stringify({
332273
- feeScope: "market+funding",
332274
- generalFee,
332275
- feeStatus,
332276
- market,
332277
- fundingFeeSource: fundingFeeSource2,
332278
- fundingFeesByCurrency: fundingFeesByCurrency2
332279
- })
332280
- });
332776
+ if (useVerity) {
332777
+ sourceAccount.exchange.setHttpClientOverride(buildHttpClientOverrideFromMetadata(metadata, verityProverUrl, (proof, notaryPubKey) => {
332778
+ verity.proof = proof;
332779
+ log.debug(`Verity proof:`, { proof, notaryPubKey });
332780
+ }), verityHttpClientOverridePredicate);
332281
332781
  }
332282
- const tokenCode = symbol2.toUpperCase();
332283
- const { fundingFeeSource, fundingFeesByCurrency } = await fetchFundingFees([
332284
- tokenCode
332285
- ]);
332286
- return ctx.wrappedCallback(null, {
332287
- proof: ctx.verity.proof,
332288
- result: JSON.stringify({
332289
- feeScope: "token",
332290
- symbol: tokenCode,
332291
- fundingFeeSource,
332292
- fundingFeesByCurrency
332293
- })
332782
+ const result = await transferBinanceInternal(sourceAccount, destAccount, symbol2, transferPayload.amount);
332783
+ archiveTransferEventInBackground(brokerArchiver, {
332784
+ exchange: normalizedCex,
332785
+ accountSelector: fromSelector,
332786
+ assetSymbol: symbol2,
332787
+ transfer: {
332788
+ eventKind: "internal_transfer",
332789
+ lifecycleAction: "submit_internal_transfer",
332790
+ status: "ok",
332791
+ amount: String(transferPayload.amount),
332792
+ network: "internal",
332793
+ externalId: extractBinanceInternalTransferId(result),
332794
+ payload: { from: fromSelector, to: toSelector, result }
332795
+ }
332796
+ });
332797
+ ctx.wrappedCallback(null, {
332798
+ proof: verity.proof,
332799
+ result: JSON.stringify(result)
332294
332800
  });
332295
332801
  } catch (error48) {
332296
- safeLogError(`Error fetching fees for ${symbol2} from ${cex3}`, error48);
332802
+ safeLogError("InternalTransfer failed", error48);
332803
+ if (error48 instanceof BrokerAccountPreconditionError) {
332804
+ return ctx.wrappedCallback({
332805
+ code: grpc5.status.FAILED_PRECONDITION,
332806
+ message: getErrorMessage(error48)
332807
+ }, null);
332808
+ }
332809
+ const msg = getErrorMessage(error48);
332810
+ let code;
332811
+ if (msg.includes("Unsupported transfer direction")) {
332812
+ code = grpc5.status.INVALID_ARGUMENT;
332813
+ } else if (msg.includes("unavailable in this CCXT build")) {
332814
+ code = grpc5.status.UNIMPLEMENTED;
332815
+ } else {
332816
+ code = mapCcxtErrorToGrpcStatus(error48) ?? grpc5.status.INTERNAL;
332817
+ }
332297
332818
  ctx.wrappedCallback({
332298
- code: grpc7.status.INTERNAL,
332299
- message: `Error fetching fees from ${cex3}`
332819
+ code,
332820
+ message: `InternalTransfer failed: ${sanitizeErrorDetail(error48)}`
332300
332821
  }, null);
332301
332822
  }
332302
332823
  }
332303
- async function handleFetchDepositAddresses(ctx) {
332824
+
332825
+ // src/handlers/execute-action/orders.ts
332826
+ var grpc6 = __toESM(require_src3(), 1);
332827
+
332828
+ // src/helpers/passive-order.ts
332829
+ var PASSIVE_ORDER_ERROR_CODES = {
332830
+ unsupported: "passive_order_unsupported",
332831
+ rejected: "passive_order_rejected",
332832
+ wouldCross: "passive_order_would_cross"
332833
+ };
332834
+ function identifiesWouldCross(message) {
332835
+ const normalized = message.toLowerCase();
332836
+ return normalized.includes("would immediately match and take") || /post[\s-]?only\b.*\bwould\b.*\bimmediately\b.*\b(?:execute|fill|match)/.test(normalized) || /post[\s-]?only\b.*\bwould\b.*\b(?:execute|fill|match)\w*\b.*\bimmediately/.test(normalized);
332837
+ }
332838
+ function identifiesUnsupported(message) {
332839
+ const normalized = message.toLowerCase();
332840
+ return /post[\s-]?only\b.*\b(?:not supported|unsupported|does not support)\b/.test(normalized) || /\b(?:not supported|unsupported|does not support)\b.*\bpost[\s-]?only\b/.test(normalized);
332841
+ }
332842
+ function classifyPassiveOrderError(error48) {
332843
+ if (error48 instanceof ccxt_default.InsufficientFunds) {
332844
+ return "InsufficientFunds";
332845
+ }
332846
+ if (error48 instanceof ccxt_default.AuthenticationError) {
332847
+ return "AuthenticationError";
332848
+ }
332849
+ const message = getErrorMessage(error48);
332850
+ if (error48 instanceof ccxt_default.OrderImmediatelyFillable || identifiesWouldCross(message)) {
332851
+ return PASSIVE_ORDER_ERROR_CODES.wouldCross;
332852
+ }
332853
+ if (error48 instanceof ccxt_default.NotSupported || identifiesUnsupported(message)) {
332854
+ return PASSIVE_ORDER_ERROR_CODES.unsupported;
332855
+ }
332856
+ return PASSIVE_ORDER_ERROR_CODES.rejected;
332857
+ }
332858
+
332859
+ // src/handlers/execute-action/orders.ts
332860
+ async function handleCreateOrder(ctx) {
332304
332861
  const {
332305
332862
  call,
332306
332863
  wrappedCallback,
@@ -332316,70 +332873,193 @@ async function handleFetchDepositAddresses(ctx) {
332316
332873
  applyVerityToBroker,
332317
332874
  useVerity,
332318
332875
  verityProverUrl,
332319
- otelMetrics
332876
+ otelMetrics,
332877
+ brokerArchiver,
332878
+ orderActivityTracker
332320
332879
  } = ctx;
332321
332880
  const verityProof = verity.proof;
332322
- if (!symbol2) {
332323
- return ctx.wrappedCallback({
332324
- code: grpc7.status.INVALID_ARGUMENT,
332325
- message: `ValidationError: Symbol required`
332326
- }, null);
332327
- }
332328
- const fetchDepositAddresses = parsePayloadForAction(ctx, FetchDepositAddressesPayloadSchema);
332329
- if (fetchDepositAddresses === null)
332881
+ const orderValue = parsePayloadForAction(ctx, CreateOrderPayloadSchema);
332882
+ if (orderValue === null)
332330
332883
  return;
332331
- let depositNetwork;
332332
- try {
332333
- depositNetwork = await resolveTransferNetwork(broker, symbol2, fetchDepositAddresses.chain);
332334
- } catch (error48) {
332335
- const message = getErrorMessage(error48);
332336
- return ctx.wrappedCallback({
332337
- code: stableGrpcErrorCode(message) ?? grpc7.status.INVALID_ARGUMENT,
332338
- message
332339
- }, null);
332340
- }
332341
- const depositValidation = validateDeposit(policy, cex3, depositNetwork.brokerNetworkId, symbol2);
332342
- if (!depositValidation.valid) {
332884
+ const isPassiveOrder = orderValue.orderIntent === "passive_only";
332885
+ if (isPassiveOrder && orderValue.orderType !== "limit") {
332343
332886
  return ctx.wrappedCallback({
332344
- code: grpc7.status.PERMISSION_DENIED,
332345
- message: `policy_deposit_denied: ${depositValidation.error}`
332887
+ code: grpc6.status.INVALID_ARGUMENT,
332888
+ message: "ValidationError: passive_only order intent requires a limit order"
332346
332889
  }, null);
332347
332890
  }
332891
+ const createOrderParams = {
332892
+ ...orderValue.params,
332893
+ ...orderValue.clientOrderId !== undefined && {
332894
+ clientOrderId: orderValue.clientOrderId
332895
+ },
332896
+ ...isPassiveOrder && { postOnly: true }
332897
+ };
332898
+ let resolvedOrderTelemetry = {};
332899
+ let marketMetadataHash;
332900
+ let submission = "not_attempted";
332348
332901
  try {
332349
- const depositAddresses = broker.has.fetchDepositAddress === true ? [
332350
- await broker.fetchDepositAddress(symbol2, {
332351
- network: depositNetwork.exchangeNetworkId,
332352
- ...fetchDepositAddresses.params ?? {}
332902
+ if (!broker) {
332903
+ return ctx.wrappedCallback({
332904
+ code: grpc6.status.INVALID_ARGUMENT,
332905
+ message: `Invalid CEX key: ${cex3}. Supported keys: ${Object.keys(brokers).join(", ")}`
332906
+ }, null);
332907
+ }
332908
+ const resolution = await resolveOrderExecution(policy, broker, cex3, orderValue.fromToken, orderValue.toToken, orderValue.amount, orderValue.price, orderValue.marketType);
332909
+ if (!resolution.valid || !resolution.symbol || !resolution.side) {
332910
+ return ctx.wrappedCallback({
332911
+ code: grpc6.status.INVALID_ARGUMENT,
332912
+ message: resolution.error ?? "Order rejected by policy: market or limits not satisfied"
332913
+ }, null);
332914
+ }
332915
+ resolvedOrderTelemetry = {
332916
+ symbol: resolution.symbol,
332917
+ side: resolution.side,
332918
+ requestedQuantity: resolution.amountBase ?? orderValue.amount
332919
+ };
332920
+ if (selectedBrokerAccount?.label) {
332921
+ orderActivityTracker?.record(cex3, selectedBrokerAccount.label, resolution.symbol);
332922
+ }
332923
+ const telemetryIds = extractOrderTelemetryIds(createOrderParams);
332924
+ const submissionTimestamp = new Date().toISOString();
332925
+ marketMetadataHash = await captureMarketMetadataSnapshot(brokerArchiver, broker, {
332926
+ exchange: cex3,
332927
+ accountSelector: selectedBrokerAccount?.label,
332928
+ symbol: resolution.symbol,
332929
+ action: "CreateOrder",
332930
+ brokerObservedTimestamp: submissionTimestamp,
332931
+ ...telemetryIds
332932
+ });
332933
+ submission = "in_flight";
332934
+ const order = await broker.createOrder(resolution.symbol, orderValue.orderType, resolution.side, resolution.amountBase ?? orderValue.amount, orderValue.price, createOrderParams);
332935
+ submission = "placed";
332936
+ const createOrderContext = {
332937
+ action: "CreateOrder",
332938
+ cex: cex3,
332939
+ accountLabel: selectedBrokerAccount?.label,
332940
+ symbol: resolvedOrderTelemetry.symbol,
332941
+ side: resolvedOrderTelemetry.side,
332942
+ orderType: orderValue.orderType,
332943
+ requestedQuantity: resolvedOrderTelemetry.requestedQuantity,
332944
+ requestedNotional: orderValue.amount * orderValue.price,
332945
+ orderAuthor: orderValue.orderAuthor,
332946
+ brokerObservedTimestamp: submissionTimestamp,
332947
+ ...telemetryIds
332948
+ };
332949
+ emitOrderExecutionTelemetryInBackground(otelMetrics, createOrderContext, order);
332950
+ archiveOrderExecutionInBackground(brokerArchiver, createOrderContext, order, undefined, { marketMetadataHash });
332951
+ ctx.wrappedCallback(null, {
332952
+ result: JSON.stringify({
332953
+ ...order,
332954
+ ...isPassiveOrder && {
332955
+ passivePlacementOutcome: "accepted_passive"
332956
+ }
332353
332957
  })
332354
- ] : await broker.fetchDepositAddressesByNetwork(symbol2, {
332355
- network: depositNetwork.exchangeNetworkId,
332356
- ...fetchDepositAddresses.params ?? {}
332357
332958
  });
332358
- if (depositAddresses.length > 0) {
332359
- return ctx.wrappedCallback(null, {
332360
- proof: ctx.verity.proof,
332361
- result: JSON.stringify(depositAddresses.map((depositAddress) => ({
332362
- ...depositAddress,
332363
- operatorAlias: depositNetwork.operatorAlias,
332364
- brokerNetworkId: depositNetwork.brokerNetworkId,
332365
- exchangeNetworkId: depositNetwork.exchangeNetworkId
332366
- })))
332959
+ } catch (error48) {
332960
+ rethrowArchiveDurabilityError(error48);
332961
+ safeLogRedactedError("Order Creation failed", error48);
332962
+ const failedCreateContext = {
332963
+ action: "CreateOrder",
332964
+ cex: cex3,
332965
+ accountLabel: selectedBrokerAccount?.label,
332966
+ symbol: resolvedOrderTelemetry.symbol ?? symbol2,
332967
+ side: resolvedOrderTelemetry.side,
332968
+ orderType: orderValue.orderType,
332969
+ requestedQuantity: resolvedOrderTelemetry.requestedQuantity ?? orderValue.amount,
332970
+ requestedNotional: orderValue.amount * orderValue.price,
332971
+ orderAuthor: orderValue.orderAuthor,
332972
+ ...extractOrderTelemetryIds(createOrderParams)
332973
+ };
332974
+ emitOrderExecutionTelemetryInBackground(otelMetrics, failedCreateContext, undefined, error48);
332975
+ archiveOrderExecutionInBackground(brokerArchiver, failedCreateContext, undefined, error48, { marketMetadataHash });
332976
+ if (isPassiveOrder && submission === "in_flight") {
332977
+ const stableErrorCode = classifyPassiveOrderError(error48);
332978
+ return rejectWithGrpcError(ctx, error48, {
332979
+ message: `${stableErrorCode}: ${sanitizeErrorDetail(error48)}`,
332980
+ preferStableMessageOnly: true
332367
332981
  });
332368
332982
  }
332369
332983
  ctx.wrappedCallback({
332370
- code: grpc7.status.INTERNAL,
332371
- message: "Deposit confirmation failed"
332984
+ code: grpc6.status.INTERNAL,
332985
+ message: `Order Creation failed: ${sanitizeErrorDetail(error48)}`
332372
332986
  }, null);
332987
+ }
332988
+ }
332989
+ async function handleGetOrderDetails(ctx) {
332990
+ const {
332991
+ call,
332992
+ wrappedCallback,
332993
+ policy,
332994
+ brokers,
332995
+ metadata,
332996
+ normalizedCex,
332997
+ cex: cex3,
332998
+ symbol: symbol2,
332999
+ selectedBrokerAccount,
333000
+ broker,
333001
+ verity,
333002
+ applyVerityToBroker,
333003
+ useVerity,
333004
+ verityProverUrl,
333005
+ otelMetrics,
333006
+ brokerArchiver,
333007
+ orderActivityTracker
333008
+ } = ctx;
333009
+ const verityProof = verity.proof;
333010
+ const getOrderValue = parsePayloadForAction(ctx, GetOrderDetailsPayloadSchema);
333011
+ if (getOrderValue === null)
333012
+ return;
333013
+ try {
333014
+ if (!broker) {
333015
+ return ctx.wrappedCallback({
333016
+ code: grpc6.status.INVALID_ARGUMENT,
333017
+ message: `Invalid CEX key: ${cex3}. Supported keys: ${Object.keys(brokers).join(", ")}`
333018
+ }, null);
333019
+ }
333020
+ const orderDetails = await broker.fetchOrder(getOrderValue.orderId, symbol2, { ...getOrderValue.params });
333021
+ if (selectedBrokerAccount?.label && symbol2) {
333022
+ orderActivityTracker?.record(cex3, selectedBrokerAccount.label, symbol2);
333023
+ }
333024
+ const getOrderContext = {
333025
+ action: "GetOrderDetails",
333026
+ cex: cex3,
333027
+ accountLabel: selectedBrokerAccount?.label,
333028
+ symbol: symbol2,
333029
+ ...extractOrderTelemetryIds(getOrderValue.params)
333030
+ };
333031
+ emitOrderExecutionTelemetryInBackground(otelMetrics, getOrderContext, orderDetails);
333032
+ archiveOrderExecutionInBackground(brokerArchiver, getOrderContext, orderDetails);
333033
+ ctx.wrappedCallback(null, {
333034
+ result: JSON.stringify({
333035
+ orderId: orderDetails.id,
333036
+ status: orderDetails.status,
333037
+ amount: orderDetails.amount,
333038
+ filled: orderDetails.filled,
333039
+ remaining: orderDetails.remaining,
333040
+ symbol: orderDetails.symbol,
333041
+ side: orderDetails.side,
333042
+ price: orderDetails.price
333043
+ })
333044
+ });
332373
333045
  } catch (error48) {
332374
- safeLogError("Fetch Deposit Addresses confirmation failed", error48);
332375
- const message = getErrorMessage(error48);
333046
+ safeLogError(`Error fetching order details from ${cex3}`, error48);
333047
+ const failedGetOrderContext = {
333048
+ action: "GetOrderDetails",
333049
+ cex: cex3,
333050
+ accountLabel: selectedBrokerAccount?.label,
333051
+ symbol: symbol2,
333052
+ ...extractOrderTelemetryIds(getOrderValue.params)
333053
+ };
333054
+ emitOrderExecutionTelemetryInBackground(otelMetrics, failedGetOrderContext, undefined, error48);
333055
+ archiveOrderExecutionInBackground(brokerArchiver, failedGetOrderContext, undefined, error48);
332376
333056
  ctx.wrappedCallback({
332377
- code: grpc7.status.INTERNAL,
332378
- message: "Fetch Deposit Addresses confirmation failed: " + message
333057
+ code: grpc6.status.INTERNAL,
333058
+ message: `Failed to fetch order details from ${cex3}: ${sanitizeErrorDetail(error48)}`
332379
333059
  }, null);
332380
333060
  }
332381
333061
  }
332382
- async function handleFetchBalances(ctx) {
333062
+ async function handleCancelOrder(ctx) {
332383
333063
  const {
332384
333064
  call,
332385
333065
  wrappedCallback,
@@ -332395,63 +333075,66 @@ async function handleFetchBalances(ctx) {
332395
333075
  applyVerityToBroker,
332396
333076
  useVerity,
332397
333077
  verityProverUrl,
332398
- otelMetrics
333078
+ otelMetrics,
333079
+ brokerArchiver,
333080
+ orderActivityTracker
332399
333081
  } = ctx;
332400
333082
  const verityProof = verity.proof;
333083
+ const cancelOrderValue = parsePayloadForAction(ctx, CancelOrderPayloadSchema);
333084
+ if (cancelOrderValue === null)
333085
+ return;
332401
333086
  try {
332402
- const payload = call.request.payload || {};
332403
- const providedBalanceType = payload.balanceType;
332404
- const balanceType = (providedBalanceType ?? "total").toString();
332405
- const validBalanceTypes = new Set(["free", "used", "total"]);
332406
- if (!validBalanceTypes.has(balanceType)) {
333087
+ if (!broker) {
332407
333088
  return ctx.wrappedCallback({
332408
- code: grpc7.status.INVALID_ARGUMENT,
332409
- message: `ValidationError: invalid balanceType '${providedBalanceType}'. Expected one of: free | used | total`
333089
+ code: grpc6.status.INVALID_ARGUMENT,
333090
+ message: `Invalid CEX key: ${cex3}. Supported keys: ${Object.keys(brokers).join(", ")}`
332410
333091
  }, null);
332411
333092
  }
332412
- const params = { ...payload };
332413
- delete params.balanceType;
332414
- const marketType = parseMarketType(params.marketType);
332415
- delete params.marketType;
332416
- if (params.type === undefined) {
332417
- params.type = marketTypeToCcxtType(marketType);
332418
- }
332419
- let responseBalances = {};
332420
- if (balanceType === "free") {
332421
- const partial2 = await broker.fetchFreeBalance(params);
332422
- responseBalances = partial2 ?? {};
332423
- } else if (balanceType === "used") {
332424
- const partial2 = await broker.fetchUsedBalance(params);
332425
- responseBalances = partial2 ?? {};
332426
- } else if (balanceType === "total") {
332427
- const partial2 = await broker.fetchTotalBalance(params);
332428
- responseBalances = partial2 ?? {};
332429
- }
332430
- if (symbol2) {
332431
- if (typeof responseBalances[symbol2] === "number") {
332432
- responseBalances = {
332433
- [symbol2]: responseBalances[symbol2] ?? 0
332434
- };
332435
- } else {
332436
- responseBalances = {};
332437
- }
333093
+ const cancelOrderContext = {
333094
+ action: "CancelOrder",
333095
+ cex: cex3,
333096
+ accountLabel: selectedBrokerAccount?.label,
333097
+ symbol: symbol2,
333098
+ ...extractOrderTelemetryIds(cancelOrderValue.params)
333099
+ };
333100
+ const cancelledOrder = await broker.cancelOrder(cancelOrderValue.orderId, symbol2, cancelOrderValue.params ?? {});
333101
+ if (selectedBrokerAccount?.label && symbol2) {
333102
+ orderActivityTracker?.record(cex3, selectedBrokerAccount.label, symbol2);
332438
333103
  }
333104
+ emitOrderExecutionTelemetryInBackground(otelMetrics, cancelOrderContext, cancelledOrder);
333105
+ archiveOrderExecutionInBackground(brokerArchiver, cancelOrderContext, cancelledOrder);
332439
333106
  ctx.wrappedCallback(null, {
332440
- proof: ctx.verity.proof,
332441
- result: JSON.stringify({
332442
- balances: responseBalances,
332443
- balanceType
332444
- })
333107
+ result: JSON.stringify({ ...cancelledOrder })
332445
333108
  });
332446
333109
  } catch (error48) {
332447
- safeLogError(`Error fetching balance from ${cex3}`, error48);
333110
+ safeLogError(`Error cancelling order from ${cex3}`, error48);
333111
+ const failedCancelContext = {
333112
+ action: "CancelOrder",
333113
+ cex: cex3,
333114
+ accountLabel: selectedBrokerAccount?.label,
333115
+ symbol: symbol2,
333116
+ ...extractOrderTelemetryIds(cancelOrderValue.params)
333117
+ };
333118
+ emitOrderExecutionTelemetryInBackground(otelMetrics, failedCancelContext, undefined, error48);
333119
+ archiveOrderExecutionInBackground(brokerArchiver, failedCancelContext, undefined, error48);
332448
333120
  ctx.wrappedCallback({
332449
- code: grpc7.status.INTERNAL,
332450
- message: `Failed to fetch balance from ${cex3}`
333121
+ code: grpc6.status.INTERNAL,
333122
+ message: `Failed to cancel order from ${cex3}: ${sanitizeErrorDetail(error48)}`
332451
333123
  }, null);
332452
333124
  }
332453
333125
  }
332454
- async function handleFetchTicker(ctx) {
333126
+ async function handleOrders(ctx) {
333127
+ if (ctx.action === Action.CreateOrder)
333128
+ return handleCreateOrder(ctx);
333129
+ if (ctx.action === Action.GetOrderDetails)
333130
+ return handleGetOrderDetails(ctx);
333131
+ if (ctx.action === Action.CancelOrder)
333132
+ return handleCancelOrder(ctx);
333133
+ }
333134
+
333135
+ // src/handlers/execute-action/pass-through.ts
333136
+ var grpc7 = __toESM(require_src3(), 1);
333137
+ async function handleFetchCurrency(ctx) {
332455
333138
  const {
332456
333139
  call,
332457
333140
  wrappedCallback,
@@ -332477,241 +333160,213 @@ async function handleFetchTicker(ctx) {
332477
333160
  }, null);
332478
333161
  }
332479
333162
  try {
332480
- const ticker = await broker.fetchTicker(symbol2);
333163
+ const assetCode = symbol2.trim().toUpperCase();
333164
+ const currencyInfo = await fetchCurrencyMetadata(broker, assetCode);
333165
+ if (!currencyInfo) {
333166
+ return ctx.wrappedCallback({
333167
+ code: grpc7.status.NOT_FOUND,
333168
+ message: `venue_discovery_unavailable: currency not found for ${assetCode}`
333169
+ }, null);
333170
+ }
333171
+ const networkEvidence = buildTransferNetworkEvidence(currencyInfo);
332481
333172
  ctx.wrappedCallback(null, {
332482
333173
  proof: ctx.verity.proof,
332483
- result: JSON.stringify(ticker)
332484
- });
332485
- } catch (error48) {
332486
- safeLogError(`Error fetching ticker from ${cex3}`, error48);
332487
- ctx.wrappedCallback({
332488
- code: grpc7.status.INTERNAL,
332489
- message: `Failed to fetch ticker from ${cex3}`
332490
- }, null);
332491
- }
332492
- }
332493
- async function handlePassThrough(ctx) {
332494
- if (ctx.action === Action.FetchCurrency)
332495
- return handleFetchCurrency(ctx);
332496
- if (ctx.action === Action.FetchAccountId)
332497
- return handleFetchAccountId(ctx);
332498
- if (ctx.action === Action.FetchFees)
332499
- return handleFetchFees(ctx);
332500
- if (ctx.action === Action.FetchDepositAddresses)
332501
- return handleFetchDepositAddresses(ctx);
332502
- if (ctx.action === Action.FetchBalances)
332503
- return handleFetchBalances(ctx);
332504
- if (ctx.action === Action.FetchTicker)
332505
- return handleFetchTicker(ctx);
332506
- }
332507
-
332508
- // src/handlers/execute-action/perp-config.ts
332509
- var grpc8 = __toESM(require_src3(), 1);
332510
- function exchangeSupports(broker, capability) {
332511
- return broker.has?.[capability] === true;
332512
- }
332513
- function extractPerpConfigs(positions) {
332514
- return positions.map((position) => ({
332515
- symbol: typeof position.symbol === "string" ? position.symbol : undefined,
332516
- leverage: typeof position.leverage === "number" ? position.leverage : undefined,
332517
- marginMode: typeof position.marginMode === "string" ? position.marginMode : undefined
332518
- }));
332519
- }
332520
- async function handleGetPerpConfigState(ctx) {
332521
- const { wrappedCallback, cex: cex3, normalizedCex, broker } = ctx;
332522
- const payload = parsePayloadForAction(ctx, GetPerpConfigStatePayloadSchema);
332523
- if (payload === null) {
332524
- return;
332525
- }
332526
- if (!broker) {
332527
- return wrappedCallback({
332528
- code: grpc8.status.INVALID_ARGUMENT,
332529
- message: `Invalid CEX key: ${cex3}`
332530
- }, null);
332531
- }
332532
- const exchange = broker;
332533
- if (!exchangeSupports(exchange, "fetchPositions")) {
332534
- return wrappedCallback({
332535
- code: grpc8.status.UNIMPLEMENTED,
332536
- message: `${normalizedCex} does not support fetchPositions`
332537
- }, null);
332538
- }
332539
- try {
332540
- const symbols = payload.symbol ? [payload.symbol] : undefined;
332541
- const positions = await exchange.fetchPositions?.(symbols, payload.params);
332542
- ctx.wrappedCallback(null, {
332543
333174
  result: JSON.stringify({
333175
+ ...currencyInfo,
332544
333176
  exchange: normalizedCex,
332545
- configs: extractPerpConfigs(positions ?? []),
332546
- positions: positions ?? []
333177
+ asset: assetCode,
333178
+ code: currencyInfo.code ?? assetCode,
333179
+ id: currencyInfo.id ?? null,
333180
+ networks: networkEvidence.networks,
333181
+ networkAliases: networkEvidence.aliases,
333182
+ raw: currencyInfo
332547
333183
  })
332548
333184
  });
332549
333185
  } catch (error48) {
332550
- safeLogError(`GetPerpConfigState failed for ${cex3}`, error48);
333186
+ safeLogError(`Error fetching currency ${symbol2} from ${cex3}`, error48);
333187
+ const message = getErrorMessage(error48);
332551
333188
  ctx.wrappedCallback({
332552
- code: grpc8.status.INTERNAL,
332553
- message: `GetPerpConfigState failed: ${sanitizeErrorDetail(error48)}`
333189
+ code: stableGrpcErrorCode(message) ?? mapCcxtErrorToGrpcStatus(error48) ?? grpc7.status.INTERNAL,
333190
+ message: message.startsWith("venue_discovery_unavailable:") ? message : `venue_discovery_unavailable: ${message}`
332554
333191
  }, null);
332555
333192
  }
332556
333193
  }
332557
- async function handleSetPerpConfigState(ctx) {
332558
- const { wrappedCallback, cex: cex3, normalizedCex, broker } = ctx;
332559
- const payload = parsePayloadForAction(ctx, SetPerpConfigStatePayloadSchema);
332560
- if (payload === null) {
332561
- return;
332562
- }
332563
- if (!broker) {
332564
- return wrappedCallback({
332565
- code: grpc8.status.INVALID_ARGUMENT,
332566
- message: `Invalid CEX key: ${cex3}`
332567
- }, null);
332568
- }
332569
- const exchange = broker;
332570
- if (!exchangeSupports(exchange, "setLeverage")) {
332571
- return wrappedCallback({
332572
- code: grpc8.status.UNIMPLEMENTED,
332573
- message: `${normalizedCex} does not support setLeverage`
332574
- }, null);
332575
- }
333194
+ async function handleFetchAccountId(ctx) {
333195
+ const {
333196
+ call,
333197
+ wrappedCallback,
333198
+ policy,
333199
+ brokers,
333200
+ metadata,
333201
+ normalizedCex,
333202
+ cex: cex3,
333203
+ symbol: symbol2,
333204
+ selectedBrokerAccount,
333205
+ broker,
333206
+ verity,
333207
+ applyVerityToBroker,
333208
+ useVerity,
333209
+ verityProverUrl,
333210
+ otelMetrics
333211
+ } = ctx;
333212
+ const verityProof = verity.proof;
332576
333213
  try {
332577
- const response = await exchange.setLeverage?.(payload.leverage, payload.symbol, {
332578
- marginMode: payload.marginMode ?? "cross",
332579
- ...payload.params
332580
- });
332581
- ctx.wrappedCallback(null, {
332582
- result: JSON.stringify({
332583
- exchange: normalizedCex,
332584
- symbol: payload.symbol,
332585
- leverage: payload.leverage,
332586
- marginMode: payload.marginMode ?? "cross",
332587
- response
332588
- })
333214
+ const accountId = await broker.fetchAccountId();
333215
+ return ctx.wrappedCallback(null, {
333216
+ proof: ctx.verity.proof,
333217
+ result: JSON.stringify({ accountId })
332589
333218
  });
332590
333219
  } catch (error48) {
332591
- safeLogError(`SetPerpConfigState failed for ${cex3}`, error48);
333220
+ safeLogError(`Error fetching account ID ${cex3}`, error48);
332592
333221
  ctx.wrappedCallback({
332593
- code: grpc8.status.INTERNAL,
332594
- message: `SetPerpConfigState failed: ${sanitizeErrorDetail(error48)}`
333222
+ code: grpc7.status.INTERNAL,
333223
+ message: `Error fetching account ID from ${cex3}`
332595
333224
  }, null);
332596
333225
  }
332597
333226
  }
332598
- async function handlePerpConfig(ctx) {
332599
- if (ctx.action === Action.GetPerpConfigState) {
332600
- return handleGetPerpConfigState(ctx);
332601
- }
332602
- if (ctx.action === Action.SetPerpConfigState) {
332603
- return handleSetPerpConfigState(ctx);
333227
+ async function handleFetchFees(ctx) {
333228
+ const {
333229
+ call,
333230
+ wrappedCallback,
333231
+ policy,
333232
+ brokers,
333233
+ metadata,
333234
+ normalizedCex,
333235
+ cex: cex3,
333236
+ symbol: symbol2,
333237
+ selectedBrokerAccount,
333238
+ broker,
333239
+ verity,
333240
+ applyVerityToBroker,
333241
+ useVerity,
333242
+ verityProverUrl,
333243
+ otelMetrics
333244
+ } = ctx;
333245
+ const verityProof = verity.proof;
333246
+ if (!symbol2) {
333247
+ return ctx.wrappedCallback({
333248
+ code: grpc7.status.INVALID_ARGUMENT,
333249
+ message: `ValidationError: Symbol required`
333250
+ }, null);
332604
333251
  }
332605
- }
332606
-
332607
- // src/handlers/execute-action/treasury-call.ts
332608
- var grpc9 = __toESM(require_src3(), 1);
332609
- async function handleTreasuryCall(ctx) {
332610
- const { broker } = ctx;
332611
- const callValue = parsePayloadForAction(ctx, CallPayloadSchema);
332612
- if (callValue === null)
333252
+ const feesPayload = parsePayloadForAction(ctx, FetchFeesPayloadSchema);
333253
+ if (feesPayload === null)
332613
333254
  return;
332614
- let createOrderContext;
332615
- let marketMetadataHash;
333255
+ const includeAllFees = feesPayload.includeAllFees || feesPayload.includeFundingFees === true;
332616
333256
  try {
332617
- if (callValue.functionName.startsWith("_") || callValue.functionName.includes("constructor") || callValue.functionName.includes("prototype")) {
332618
- return ctx.wrappedCallback({
332619
- code: grpc9.status.PERMISSION_DENIED,
332620
- message: "Access to the requested function is denied"
332621
- }, null);
332622
- }
332623
- const argsArray = callArgs(callValue.args, callValue.params ?? {});
332624
- const treasuryDiscovery = await handleTreasuryDiscoveryCall(broker, callValue.functionName, callValue.args, callValue.params ?? {});
332625
- if (treasuryDiscovery.handled) {
332626
- return ctx.wrappedCallback(null, {
332627
- proof: ctx.verity.proof,
332628
- result: JSON.stringify(treasuryDiscovery.result)
332629
- });
332630
- }
332631
- const fn = broker[callValue.functionName];
332632
- if (typeof fn !== "function" || broker.has?.[callValue.functionName] === false) {
332633
- return ctx.wrappedCallback({
332634
- code: grpc9.status.INVALID_ARGUMENT,
332635
- message: `Function not found on broker: ${callValue.functionName}`
332636
- }, null);
332637
- }
332638
- if (callValue.functionName === "createOrder") {
332639
- const [symbol2, orderType, side, quantity, price] = callValue.args;
332640
- const requestedQuantity = asFiniteNumber(quantity);
332641
- const requestedPrice = asFiniteNumber(price);
332642
- const requestedNotional = requestedQuantity !== undefined && requestedPrice !== undefined ? asFiniteNumber(requestedQuantity * requestedPrice) : undefined;
332643
- const telemetryIds = extractOrderTelemetryIds(callValue.params);
332644
- const submissionTimestamp = new Date().toISOString();
332645
- createOrderContext = {
332646
- action: "CreateOrder",
332647
- cex: ctx.cex,
332648
- accountLabel: ctx.selectedBrokerAccount?.label,
332649
- symbol: asNonEmptyString(symbol2),
332650
- orderType: asNonEmptyString(orderType),
332651
- side: asNonEmptyString(side),
332652
- requestedQuantity,
332653
- requestedNotional,
332654
- orderAuthor: callValue.orderAuthor,
332655
- brokerObservedTimestamp: submissionTimestamp,
332656
- ...telemetryIds
332657
- };
332658
- if (createOrderContext.symbol !== undefined) {
332659
- marketMetadataHash = await captureMarketMetadataSnapshot(ctx.brokerArchiver, broker, {
332660
- exchange: ctx.cex,
332661
- accountSelector: ctx.selectedBrokerAccount?.label,
332662
- symbol: createOrderContext.symbol,
332663
- action: "CreateOrder",
332664
- brokerObservedTimestamp: submissionTimestamp,
332665
- ...telemetryIds
333257
+ await broker.loadMarkets();
333258
+ const fetchFundingFees = async (currencyCodes) => {
333259
+ let fundingFeeSource2 = "unavailable";
333260
+ const fundingFeesByCurrency2 = {};
333261
+ if (broker.has.fetchDepositWithdrawFees) {
333262
+ try {
333263
+ const feeMap = await broker.fetchDepositWithdrawFees(currencyCodes);
333264
+ for (const code of currencyCodes) {
333265
+ const feeInfo = feeMap[code];
333266
+ if (!feeInfo) {
333267
+ continue;
333268
+ }
333269
+ const fallbackFee = feeInfo.fee !== undefined || feeInfo.percentage !== undefined ? {
333270
+ fee: feeInfo.fee ?? null,
333271
+ percentage: feeInfo.percentage ?? null
333272
+ } : null;
333273
+ fundingFeesByCurrency2[code] = {
333274
+ deposit: feeInfo.deposit ?? fallbackFee,
333275
+ withdraw: feeInfo.withdraw ?? fallbackFee,
333276
+ networks: feeInfo.networks ?? {}
333277
+ };
333278
+ }
333279
+ if (Object.keys(fundingFeesByCurrency2).length > 0) {
333280
+ fundingFeeSource2 = "fetchDepositWithdrawFees";
333281
+ }
333282
+ } catch (error48) {
333283
+ safeLogError(`Error fetching deposit/withdraw fee map for ${symbol2} from ${cex3}`, error48);
333284
+ }
333285
+ }
333286
+ if (fundingFeeSource2 === "unavailable") {
333287
+ try {
333288
+ const currencies = await broker.fetchCurrencies();
333289
+ for (const code of currencyCodes) {
333290
+ const currency = currencies[code];
333291
+ if (!currency) {
333292
+ continue;
333293
+ }
333294
+ fundingFeesByCurrency2[code] = {
333295
+ deposit: {
333296
+ enabled: currency.deposit ?? null
333297
+ },
333298
+ withdraw: {
333299
+ enabled: currency.withdraw ?? null,
333300
+ fee: currency.fee ?? null,
333301
+ limits: currency.limits?.withdraw ?? null
333302
+ },
333303
+ networks: currency.networks ?? {}
333304
+ };
333305
+ }
333306
+ if (Object.keys(fundingFeesByCurrency2).length > 0) {
333307
+ fundingFeeSource2 = "currencies";
333308
+ }
333309
+ } catch (error48) {
333310
+ safeLogError(`Error fetching currency metadata for fees for ${symbol2} from ${cex3}`, error48);
333311
+ }
333312
+ }
333313
+ return { fundingFeeSource: fundingFeeSource2, fundingFeesByCurrency: fundingFeesByCurrency2 };
333314
+ };
333315
+ const isMarketSymbol = symbol2.includes("/");
333316
+ if (isMarketSymbol) {
333317
+ const market = await broker.market(symbol2);
333318
+ const generalFee = broker.fees ?? null;
333319
+ const feeStatus = broker.fees ? "available" : "unknown";
333320
+ if (!broker.fees) {
333321
+ log.warn(`Fee metadata unavailable for ${cex3}`, { symbol: symbol2 });
333322
+ }
333323
+ if (!includeAllFees) {
333324
+ return ctx.wrappedCallback(null, {
333325
+ proof: ctx.verity.proof,
333326
+ result: JSON.stringify({
333327
+ feeScope: "market",
333328
+ generalFee,
333329
+ feeStatus,
333330
+ market
333331
+ })
332666
333332
  });
332667
333333
  }
332668
- }
332669
- const result = await fn.apply(broker, argsArray);
332670
- if (createOrderContext !== undefined) {
332671
- emitOrderExecutionTelemetryInBackground(ctx.otelMetrics, createOrderContext, result);
332672
- archiveOrderExecutionInBackground(ctx.brokerArchiver, createOrderContext, result, undefined, { marketMetadataHash });
332673
- } else if (callValue.functionName === "fetchWithdrawals") {
332674
- archiveWithdrawalObservationsInBackground(ctx.brokerArchiver, ctx.withdrawalObservationTracker, {
332675
- exchange: ctx.normalizedCex,
332676
- accountSelector: ctx.selectedBrokerAccount?.label,
332677
- transactions: result
333334
+ const currencyCodes = Array.from(new Set([market.base, market.quote]));
333335
+ const { fundingFeeSource: fundingFeeSource2, fundingFeesByCurrency: fundingFeesByCurrency2 } = await fetchFundingFees(currencyCodes);
333336
+ return ctx.wrappedCallback(null, {
333337
+ proof: ctx.verity.proof,
333338
+ result: JSON.stringify({
333339
+ feeScope: "market+funding",
333340
+ generalFee,
333341
+ feeStatus,
333342
+ market,
333343
+ fundingFeeSource: fundingFeeSource2,
333344
+ fundingFeesByCurrency: fundingFeesByCurrency2
333345
+ })
332678
333346
  });
332679
333347
  }
332680
- ctx.wrappedCallback(null, {
333348
+ const tokenCode = symbol2.toUpperCase();
333349
+ const { fundingFeeSource, fundingFeesByCurrency } = await fetchFundingFees([
333350
+ tokenCode
333351
+ ]);
333352
+ return ctx.wrappedCallback(null, {
332681
333353
  proof: ctx.verity.proof,
332682
- result: JSON.stringify(result)
333354
+ result: JSON.stringify({
333355
+ feeScope: "token",
333356
+ symbol: tokenCode,
333357
+ fundingFeeSource,
333358
+ fundingFeesByCurrency
333359
+ })
332683
333360
  });
332684
333361
  } catch (error48) {
332685
- if (createOrderContext !== undefined) {
332686
- rethrowArchiveDurabilityError(error48);
332687
- emitOrderExecutionTelemetryInBackground(ctx.otelMetrics, createOrderContext, undefined, error48);
332688
- archiveOrderExecutionInBackground(ctx.brokerArchiver, createOrderContext, undefined, error48, { marketMetadataHash });
332689
- }
332690
- safeLogError("Call failed", error48);
332691
- rejectWithGrpcError(ctx, error48, {
332692
- message: getErrorMessage(error48),
332693
- preferStableMessageOnly: true,
332694
- appendClassName: true
332695
- });
332696
- }
332697
- }
332698
- function asNonEmptyString(value) {
332699
- return typeof value === "string" && value.trim() ? value : undefined;
332700
- }
332701
- function asFiniteNumber(value) {
332702
- if (typeof value === "number") {
332703
- return Number.isFinite(value) ? value : undefined;
332704
- }
332705
- if (typeof value !== "string" || !value.trim()) {
332706
- return;
333362
+ safeLogError(`Error fetching fees for ${symbol2} from ${cex3}`, error48);
333363
+ ctx.wrappedCallback({
333364
+ code: grpc7.status.INTERNAL,
333365
+ message: `Error fetching fees from ${cex3}`
333366
+ }, null);
332707
333367
  }
332708
- const parsed = Number(value);
332709
- return Number.isFinite(parsed) ? parsed : undefined;
332710
333368
  }
332711
-
332712
- // src/handlers/execute-action/withdraw.ts
332713
- var grpc10 = __toESM(require_src3(), 1);
332714
- async function handleWithdraw(ctx) {
333369
+ async function handleFetchDepositAddresses(ctx) {
332715
333370
  const {
332716
333371
  call,
332717
333372
  wrappedCallback,
@@ -332727,1183 +333382,998 @@ async function handleWithdraw(ctx) {
332727
333382
  applyVerityToBroker,
332728
333383
  useVerity,
332729
333384
  verityProverUrl,
332730
- otelMetrics,
332731
- brokerArchiver
333385
+ otelMetrics
332732
333386
  } = ctx;
332733
333387
  const verityProof = verity.proof;
332734
333388
  if (!symbol2) {
332735
333389
  return ctx.wrappedCallback({
332736
- code: grpc10.status.INVALID_ARGUMENT,
333390
+ code: grpc7.status.INVALID_ARGUMENT,
332737
333391
  message: `ValidationError: Symbol required`
332738
333392
  }, null);
332739
333393
  }
332740
- const transferValue = parsePayloadForAction(ctx, WithdrawPayloadSchema);
332741
- if (transferValue === null)
333394
+ const fetchDepositAddresses = parsePayloadForAction(ctx, FetchDepositAddressesPayloadSchema);
333395
+ if (fetchDepositAddresses === null)
332742
333396
  return;
332743
- let withdrawNetwork;
333397
+ let depositNetwork;
332744
333398
  try {
332745
- withdrawNetwork = await resolveTransferNetwork(broker, symbol2, transferValue.chain);
333399
+ depositNetwork = await resolveTransferNetwork(broker, symbol2, fetchDepositAddresses.chain);
332746
333400
  } catch (error48) {
332747
333401
  const message = getErrorMessage(error48);
332748
333402
  return ctx.wrappedCallback({
332749
- code: stableGrpcErrorCode(message) ?? grpc10.status.INVALID_ARGUMENT,
333403
+ code: stableGrpcErrorCode(message) ?? grpc7.status.INVALID_ARGUMENT,
332750
333404
  message
332751
333405
  }, null);
332752
333406
  }
332753
- const transferValidation = validateWithdraw(policy, cex3, withdrawNetwork.brokerNetworkId, transferValue.recipientAddress, transferValue.amount, symbol2);
332754
- if (!transferValidation.valid) {
332755
- return ctx.wrappedCallback({
332756
- code: grpc10.status.PERMISSION_DENIED,
332757
- message: `policy_withdrawal_denied: ${transferValidation.error}`
332758
- }, null);
332759
- }
332760
- const travelRule = resolveTravelRuleDecision(policy, cex3, transferValue.recipientAddress);
332761
- if (travelRule.mode === "denied") {
333407
+ const depositValidation = validateDeposit(policy, cex3, depositNetwork.brokerNetworkId, symbol2);
333408
+ if (!depositValidation.valid) {
332762
333409
  return ctx.wrappedCallback({
332763
- code: grpc10.status.FAILED_PRECONDITION,
332764
- message: `travel_rule_denied: ${travelRule.error}`
333410
+ code: grpc7.status.PERMISSION_DENIED,
333411
+ message: `policy_deposit_denied: ${depositValidation.error}`
332765
333412
  }, null);
332766
333413
  }
332767
- const withdrawOrderId = transferValue.params.withdrawOrderId;
332768
- const clientWithdrawalId = typeof withdrawOrderId === "string" && withdrawOrderId.length > 0 ? withdrawOrderId : undefined;
332769
333414
  try {
332770
- const transaction = travelRule.mode === "localentity" ? await withdrawViaLocalEntity(broker, {
332771
- code: symbol2,
332772
- amount: transferValue.amount,
332773
- address: transferValue.recipientAddress,
332774
- network: withdrawNetwork.exchangeNetworkId,
332775
- questionnaire: travelRule.questionnaire,
332776
- params: transferValue.params
332777
- }) : await broker.withdraw(symbol2, transferValue.amount, transferValue.recipientAddress, undefined, {
332778
- ...transferValue.params ?? {},
332779
- network: withdrawNetwork.exchangeNetworkId
332780
- });
332781
- log.info(`Withdraw Result: ${JSON.stringify(transaction)}`);
332782
- const normalized = normalizeCcxtTransactionForArchive(transaction);
332783
- archiveTransferEventInBackground(brokerArchiver, {
332784
- exchange: cex3,
332785
- accountSelector: selectedBrokerAccount?.label,
332786
- assetSymbol: normalized.assetSymbol ?? symbol2,
332787
- transfer: {
332788
- eventKind: "withdrawal",
332789
- lifecycleAction: "submit_withdrawal",
332790
- status: normalized.status,
332791
- amount: normalized.amount ?? String(transferValue.amount),
332792
- address: normalized.address ?? transferValue.recipientAddress,
332793
- network: normalized.network ?? withdrawNetwork.exchangeNetworkId,
332794
- externalId: normalized.externalId,
332795
- clientWithdrawalId,
332796
- txid: normalized.txid,
332797
- feeAmount: normalized.feeAmount,
332798
- feeCurrency: normalized.feeCurrency,
332799
- exchangeTimestamp: normalized.exchangeTimestamp,
332800
- payload: transaction
332801
- }
332802
- });
332803
- ctx.wrappedCallback(null, {
332804
- proof: ctx.verity.proof,
332805
- result: JSON.stringify({
332806
- ...transaction,
332807
- operatorAlias: withdrawNetwork.operatorAlias,
332808
- brokerNetworkId: withdrawNetwork.brokerNetworkId,
332809
- exchangeNetworkId: withdrawNetwork.exchangeNetworkId
333415
+ const depositAddresses = broker.has.fetchDepositAddress === true ? [
333416
+ await broker.fetchDepositAddress(symbol2, {
333417
+ network: depositNetwork.exchangeNetworkId,
333418
+ ...fetchDepositAddresses.params ?? {}
332810
333419
  })
333420
+ ] : await broker.fetchDepositAddressesByNetwork(symbol2, {
333421
+ network: depositNetwork.exchangeNetworkId,
333422
+ ...fetchDepositAddresses.params ?? {}
332811
333423
  });
332812
- } catch (error48) {
332813
- safeLogError("Withdraw failed", error48);
332814
- archiveTransferEventInBackground(brokerArchiver, {
332815
- exchange: cex3,
332816
- accountSelector: selectedBrokerAccount?.label,
332817
- assetSymbol: symbol2,
332818
- transfer: {
332819
- eventKind: "withdrawal",
332820
- lifecycleAction: "submit_withdrawal",
332821
- status: "failed",
332822
- amount: String(transferValue.amount),
332823
- address: transferValue.recipientAddress,
332824
- network: withdrawNetwork.exchangeNetworkId,
332825
- clientWithdrawalId,
332826
- errorSummary: getErrorMessage(error48),
332827
- payload: { recipientAddress: transferValue.recipientAddress }
332828
- }
332829
- });
332830
- const code = mapCcxtErrorToGrpcStatus(error48) ?? grpc10.status.INTERNAL;
333424
+ if (depositAddresses.length > 0) {
333425
+ return ctx.wrappedCallback(null, {
333426
+ proof: ctx.verity.proof,
333427
+ result: JSON.stringify(depositAddresses.map((depositAddress) => ({
333428
+ ...depositAddress,
333429
+ operatorAlias: depositNetwork.operatorAlias,
333430
+ brokerNetworkId: depositNetwork.brokerNetworkId,
333431
+ exchangeNetworkId: depositNetwork.exchangeNetworkId
333432
+ })))
333433
+ });
333434
+ }
332831
333435
  ctx.wrappedCallback({
332832
- code,
332833
- message: `Withdraw failed: ${sanitizeErrorDetail(error48)}`
333436
+ code: grpc7.status.INTERNAL,
333437
+ message: "Deposit confirmation failed"
332834
333438
  }, null);
332835
- }
332836
- }
332837
-
332838
- // src/handlers/execute-action/registry.ts
332839
- var ACTION_HANDLERS = {
332840
- [Action.Deposit]: handleDeposit,
332841
- [Action.Withdraw]: handleWithdraw,
332842
- [Action.Call]: handleTreasuryCall,
332843
- [Action.InternalTransfer]: handleInternalTransfer,
332844
- [Action.CreateOrder]: handleOrders,
332845
- [Action.GetOrderDetails]: handleOrders,
332846
- [Action.CancelOrder]: handleOrders,
332847
- [Action.FetchCurrency]: handlePassThrough,
332848
- [Action.FetchAccountId]: handlePassThrough,
332849
- [Action.FetchFees]: handlePassThrough,
332850
- [Action.FetchDepositAddresses]: handlePassThrough,
332851
- [Action.FetchBalances]: handlePassThrough,
332852
- [Action.FetchTicker]: handlePassThrough,
332853
- [Action.GetPerpConfigState]: handlePerpConfig,
332854
- [Action.SetPerpConfigState]: handlePerpConfig
332855
- };
332856
- async function dispatchExecuteAction(ctx) {
332857
- const handler = ACTION_HANDLERS[ctx.action];
332858
- if (!handler) {
333439
+ } catch (error48) {
333440
+ safeLogError("Fetch Deposit Addresses confirmation failed", error48);
333441
+ const message = getErrorMessage(error48);
332859
333442
  ctx.wrappedCallback({
332860
- code: grpc11.status.INVALID_ARGUMENT,
332861
- message: "Invalid Action"
333443
+ code: grpc7.status.INTERNAL,
333444
+ message: "Fetch Deposit Addresses confirmation failed: " + message
332862
333445
  }, null);
332863
- return;
332864
333446
  }
332865
- await handler(ctx);
332866
333447
  }
332867
-
332868
- // src/handlers/execute-action/handler.ts
332869
- function createExecuteActionHandler(deps) {
333448
+ async function handleFetchBalances(ctx) {
332870
333449
  const {
333450
+ call,
333451
+ wrappedCallback,
332871
333452
  policy,
332872
333453
  brokers,
332873
- whitelistIps,
332874
- useVerity,
332875
- verityProverUrl,
332876
- otelMetrics,
332877
- brokerArchiver,
332878
- orderActivityTracker
332879
- } = deps;
332880
- const withdrawalObservationTracker = deps.withdrawalObservationTracker ?? new WithdrawalObservationTracker;
332881
- return async (call, callback) => {
332882
- const startTime = Date.now();
332883
- const { action: rawAction, cex: cex3, symbol: symbol2 } = call.request;
332884
- const action = resolveAction(rawAction);
332885
- let actionCompleted = false;
332886
- const wrappedCallback = (error48, value) => {
332887
- if (!actionCompleted) {
332888
- actionCompleted = true;
332889
- const latency = Date.now() - startTime;
332890
- const actionName = getActionName(action);
332891
- otelMetrics?.recordHistogram("execute_action_duration_ms", latency, {
332892
- action: actionName,
332893
- cex: cex3 || "unknown"
332894
- });
332895
- if (error48) {
332896
- otelMetrics?.recordCounter("execute_action_errors_total", 1, {
332897
- action: actionName,
332898
- cex: cex3 || "unknown",
332899
- error_type: error48.code ? grpc12.status[error48.code] || "unknown" : "unknown"
332900
- });
332901
- } else {
332902
- otelMetrics?.recordCounter("execute_action_success_total", 1, {
332903
- action: actionName,
332904
- cex: cex3 || "unknown"
332905
- });
332906
- }
332907
- }
332908
- callback(error48, value);
332909
- };
332910
- try {
332911
- log.info(`Request - ExecuteAction:`, { action, cex: cex3, symbol: symbol2 });
332912
- otelMetrics?.recordCounter("execute_action_requests_total", 1, {
332913
- action: getActionName(action),
332914
- cex: cex3 || "unknown"
332915
- });
332916
- if (!authenticateRequest(call, whitelistIps)) {
332917
- return wrappedCallback({
332918
- code: grpc12.status.PERMISSION_DENIED,
332919
- message: "Access denied: Unauthorized IP"
332920
- }, null);
332921
- }
332922
- if (!action || !cex3) {
332923
- return wrappedCallback({
332924
- code: grpc12.status.INVALID_ARGUMENT,
332925
- message: "`action` AND `cex` fields are required"
332926
- }, null);
332927
- }
332928
- const normalizedCex = cex3.trim().toLowerCase();
332929
- const metadata = call.metadata;
332930
- const selectedBrokerAccount = selectBrokerAccountForCex(normalizedCex, brokers, metadata);
332931
- const verity = { proof: "" };
332932
- const applyVerityToBroker = (targetBroker) => {
332933
- if (!useVerity)
332934
- return;
332935
- const override = buildHttpClientOverrideFromMetadata(metadata, verityProverUrl, (proof, notaryPubKey) => {
332936
- verity.proof = proof;
332937
- log.debug(`Verity proof:`, { proof, notaryPubKey });
332938
- });
332939
- targetBroker.setHttpClientOverride(override, verityHttpClientOverridePredicate);
332940
- };
332941
- const preludeCtx = {
332942
- call,
332943
- wrappedCallback,
332944
- action,
332945
- policy,
332946
- brokers,
332947
- metadata,
332948
- normalizedCex,
332949
- cex: cex3,
332950
- symbol: symbol2,
332951
- selectedBrokerAccount,
332952
- broker: selectedBrokerAccount?.exchange ?? createBroker(normalizedCex, metadata),
332953
- verity,
332954
- applyVerityToBroker,
332955
- useVerity,
332956
- verityProverUrl,
332957
- otelMetrics,
332958
- brokerArchiver,
332959
- orderActivityTracker,
332960
- withdrawalObservationTracker
332961
- };
332962
- if (action === Action.Call) {
332963
- const handled = await handleOrderBookCall(preludeCtx);
332964
- if (handled)
332965
- return;
332966
- }
332967
- const broker = selectedBrokerAccount?.exchange ?? createBroker(normalizedCex, metadata);
332968
- if (!broker) {
332969
- return wrappedCallback({
332970
- code: grpc12.status.UNAUTHENTICATED,
332971
- message: `This Exchange is not registered and No API metadata was found`
332972
- }, null);
332973
- }
332974
- applyVerityToBroker(broker);
332975
- const ctx = { ...preludeCtx, broker };
332976
- await dispatchExecuteAction(ctx);
332977
- } catch (error48) {
332978
- safeLogError("ExecuteAction unhandled error", error48);
332979
- return wrappedCallback({
332980
- code: grpc12.status.INTERNAL,
332981
- message: "ExecuteAction failed unexpectedly"
333454
+ metadata,
333455
+ normalizedCex,
333456
+ cex: cex3,
333457
+ symbol: symbol2,
333458
+ selectedBrokerAccount,
333459
+ broker,
333460
+ verity,
333461
+ applyVerityToBroker,
333462
+ useVerity,
333463
+ verityProverUrl,
333464
+ otelMetrics
333465
+ } = ctx;
333466
+ const verityProof = verity.proof;
333467
+ try {
333468
+ const payload = call.request.payload || {};
333469
+ const providedBalanceType = payload.balanceType;
333470
+ const balanceType = (providedBalanceType ?? "total").toString();
333471
+ const validBalanceTypes = new Set(["free", "used", "total"]);
333472
+ if (!validBalanceTypes.has(balanceType)) {
333473
+ return ctx.wrappedCallback({
333474
+ code: grpc7.status.INVALID_ARGUMENT,
333475
+ message: `ValidationError: invalid balanceType '${providedBalanceType}'. Expected one of: free | used | total`
332982
333476
  }, null);
332983
333477
  }
332984
- };
332985
- }
332986
- // src/handlers/subscribe/broker-lifecycle.ts
332987
- class SubscribeBrokerLifecycle {
332988
- #brokers = new Map;
332989
- #closing = new Map;
332990
- #shuttingDown = false;
332991
- register(broker, context2) {
332992
- this.#brokers.set(broker, context2);
332993
- if (this.#shuttingDown) {
332994
- this.close(broker);
333478
+ const params = { ...payload };
333479
+ delete params.balanceType;
333480
+ const marketType = parseMarketType(params.marketType);
333481
+ delete params.marketType;
333482
+ if (params.type === undefined) {
333483
+ params.type = marketTypeToCcxtType(marketType);
332995
333484
  }
332996
- }
332997
- close(broker) {
332998
- const existing = this.#closing.get(broker);
332999
- if (existing) {
333000
- return existing;
333485
+ let responseBalances = {};
333486
+ if (balanceType === "free") {
333487
+ const partial2 = await broker.fetchFreeBalance(params);
333488
+ responseBalances = partial2 ?? {};
333489
+ } else if (balanceType === "used") {
333490
+ const partial2 = await broker.fetchUsedBalance(params);
333491
+ responseBalances = partial2 ?? {};
333492
+ } else if (balanceType === "total") {
333493
+ const partial2 = await broker.fetchTotalBalance(params);
333494
+ responseBalances = partial2 ?? {};
333001
333495
  }
333002
- const context2 = this.#brokers.get(broker) ?? {
333003
- cex: "unknown",
333004
- symbol: "unknown"
333005
- };
333006
- this.#brokers.delete(broker);
333007
- const closing = (async () => {
333008
- try {
333009
- await broker.close();
333010
- log.debug("Request-scoped Subscribe broker closed", context2);
333011
- return "closed";
333012
- } catch (error48) {
333013
- log.warn("Failed to close request-scoped Subscribe broker", {
333014
- ...context2,
333015
- error: error48
333016
- });
333017
- return "failed";
333018
- } finally {
333019
- this.#closing.delete(broker);
333496
+ if (symbol2) {
333497
+ if (typeof responseBalances[symbol2] === "number") {
333498
+ responseBalances = {
333499
+ [symbol2]: responseBalances[symbol2] ?? 0
333500
+ };
333501
+ } else {
333502
+ responseBalances = {};
333020
333503
  }
333021
- })();
333022
- this.#closing.set(broker, closing);
333023
- return closing;
333024
- }
333025
- async closeAll() {
333026
- this.#shuttingDown = true;
333027
- let failed = 0;
333028
- while (this.#brokers.size > 0 || this.#closing.size > 0) {
333029
- const inFlight = [...this.#closing.values()];
333030
- const fresh = [...this.#brokers.keys()].map((broker) => this.close(broker));
333031
- const outcomes = await Promise.all([...fresh, ...inFlight]);
333032
- failed += outcomes.filter((outcome) => outcome === "failed").length;
333033
- }
333034
- if (failed > 0) {
333035
- throw new Error(`${failed} request-scoped Subscribe broker(s) failed to close`);
333036
333504
  }
333505
+ ctx.wrappedCallback(null, {
333506
+ proof: ctx.verity.proof,
333507
+ result: JSON.stringify({
333508
+ balances: responseBalances,
333509
+ balanceType
333510
+ })
333511
+ });
333512
+ } catch (error48) {
333513
+ safeLogError(`Error fetching balance from ${cex3}`, error48);
333514
+ ctx.wrappedCallback({
333515
+ code: grpc7.status.INTERNAL,
333516
+ message: `Failed to fetch balance from ${cex3}`
333517
+ }, null);
333037
333518
  }
333038
333519
  }
333039
- // src/handlers/subscribe/handler.ts
333040
- var grpc13 = __toESM(require_src3(), 1);
333041
-
333042
- // src/helpers/binance-user-data-stream.ts
333043
- import { Buffer as Buffer2 } from "node:buffer";
333044
- import { createHmac } from "node:crypto";
333045
- var BINANCE_SPOT_WS_API_URL = "wss://ws-api.binance.com:443/ws-api/v3";
333046
- var DEFAULT_BINANCE_USER_DATA_MAX_BUFFERED_EVENTS = 16;
333047
- var createWebSocket = (url3) => new wrapper_default(url3);
333048
- var userDataRequestCounter = 0;
333049
- function getExchangeString(exchange, key) {
333050
- const value = exchange[key];
333051
- if (typeof value !== "string" || value.length === 0) {
333052
- throw new Error(`Binance user-data stream requires exchange.${key}`);
333053
- }
333054
- return value;
333055
- }
333056
- function sortedQuery(params) {
333057
- return Object.entries(params).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`).join("&");
333058
- }
333059
- function signUserDataStreamParams(exchange, params) {
333060
- const signParams = exchange.signParams;
333061
- if (typeof signParams === "function") {
333062
- return signParams.call(exchange, params);
333063
- }
333064
- const secret = getExchangeString(exchange, "secret");
333065
- return {
333066
- ...params,
333067
- signature: createHmac("sha256", secret).update(sortedQuery(params)).digest("hex")
333068
- };
333069
- }
333070
- function getBinanceSpotWsApiUrl(exchange) {
333071
- const urls = exchange.urls;
333072
- return urls?.api?.ws?.["ws-api"]?.spot ?? BINANCE_SPOT_WS_API_URL;
333073
- }
333074
- function getRecord(value) {
333075
- return typeof value === "object" && value !== null ? value : null;
333076
- }
333077
- function getMessage(value) {
333078
- if (value instanceof Error) {
333079
- return value.message;
333080
- }
333081
- if (typeof value === "string" && value.length > 0) {
333082
- return value;
333520
+ async function handleFetchTicker(ctx) {
333521
+ const {
333522
+ call,
333523
+ wrappedCallback,
333524
+ policy,
333525
+ brokers,
333526
+ metadata,
333527
+ normalizedCex,
333528
+ cex: cex3,
333529
+ symbol: symbol2,
333530
+ selectedBrokerAccount,
333531
+ broker,
333532
+ verity,
333533
+ applyVerityToBroker,
333534
+ useVerity,
333535
+ verityProverUrl,
333536
+ otelMetrics
333537
+ } = ctx;
333538
+ const verityProof = verity.proof;
333539
+ if (!symbol2) {
333540
+ return ctx.wrappedCallback({
333541
+ code: grpc7.status.INVALID_ARGUMENT,
333542
+ message: `ValidationError: Symbol required`
333543
+ }, null);
333083
333544
  }
333084
- const record2 = getRecord(value);
333085
- const message = record2?.message;
333086
- return typeof message === "string" && message.length > 0 ? message : null;
333087
- }
333088
- function getOptionalExchangeString(exchange, key) {
333089
- const value = exchange[key];
333090
- return typeof value === "string" && value.length > 0 ? value : null;
333091
- }
333092
- function redactDiagnosticMessage(message, secretValues) {
333093
- let redacted = message;
333094
- for (const value of secretValues) {
333095
- if (value.length > 0) {
333096
- redacted = redacted.split(value).join("[redacted]");
333097
- }
333545
+ try {
333546
+ const ticker = await broker.fetchTicker(symbol2);
333547
+ ctx.wrappedCallback(null, {
333548
+ proof: ctx.verity.proof,
333549
+ result: JSON.stringify(ticker)
333550
+ });
333551
+ } catch (error48) {
333552
+ safeLogError(`Error fetching ticker from ${cex3}`, error48);
333553
+ ctx.wrappedCallback({
333554
+ code: grpc7.status.INTERNAL,
333555
+ message: `Failed to fetch ticker from ${cex3}`
333556
+ }, null);
333098
333557
  }
333099
- return redacted.replace(/(\b(?:apiKey|secret|signature)\b\s*=\s*)[^\s&,;)]+/gi, "$1[redacted]").replace(/("(?:apiKey|secret|signature)"\s*:\s*")[^"]*(")/gi, "$1[redacted]$2");
333100
333558
  }
333101
- function formatBinanceUserDataWebSocketError(event, secretValues) {
333102
- const record2 = getRecord(event);
333103
- const message = getMessage(record2?.error) ?? getMessage(record2?.message) ?? getMessage(event);
333104
- const safeMessage = message === null ? null : redactDiagnosticMessage(message, secretValues);
333105
- return new Error(safeMessage ? `Binance user-data WebSocket error: ${safeMessage}` : "Binance user-data WebSocket error");
333559
+ async function handlePassThrough(ctx) {
333560
+ if (ctx.action === Action.FetchCurrency)
333561
+ return handleFetchCurrency(ctx);
333562
+ if (ctx.action === Action.FetchAccountId)
333563
+ return handleFetchAccountId(ctx);
333564
+ if (ctx.action === Action.FetchFees)
333565
+ return handleFetchFees(ctx);
333566
+ if (ctx.action === Action.FetchDepositAddresses)
333567
+ return handleFetchDepositAddresses(ctx);
333568
+ if (ctx.action === Action.FetchBalances)
333569
+ return handleFetchBalances(ctx);
333570
+ if (ctx.action === Action.FetchTicker)
333571
+ return handleFetchTicker(ctx);
333106
333572
  }
333107
- function getCloseReason(value) {
333108
- if (typeof value === "string") {
333109
- return value.length > 0 ? value : null;
333110
- }
333111
- if (Buffer2.isBuffer(value)) {
333112
- const reason = value.toString("utf8");
333113
- return reason.length > 0 ? reason : null;
333114
- }
333115
- if (value instanceof Uint8Array) {
333116
- const reason = Buffer2.from(value).toString("utf8");
333117
- return reason.length > 0 ? reason : null;
333118
- }
333119
- return null;
333573
+
333574
+ // src/handlers/execute-action/perp-config.ts
333575
+ var grpc8 = __toESM(require_src3(), 1);
333576
+ function exchangeSupports(broker, capability) {
333577
+ return broker.has?.[capability] === true;
333120
333578
  }
333121
- function formatBinanceUserDataWebSocketClose(codeOrEvent, reasonOrUndefined, secretValues) {
333122
- const record2 = getRecord(codeOrEvent);
333123
- const code = record2 ? record2.code : codeOrEvent;
333124
- const reason = getCloseReason(record2 ? record2.reason : reasonOrUndefined);
333125
- const safeReason = reason === null ? null : redactDiagnosticMessage(reason, secretValues);
333126
- const details = [
333127
- typeof code === "number" || typeof code === "string" ? `code=${code}` : null,
333128
- safeReason ? `reason=${safeReason}` : null
333129
- ].filter((detail) => detail !== null);
333130
- return new Error(details.length > 0 ? `Binance user-data WebSocket closed unexpectedly (${details.join(", ")})` : "Binance user-data WebSocket closed unexpectedly");
333579
+ function extractPerpConfigs(positions) {
333580
+ return positions.map((position) => ({
333581
+ symbol: typeof position.symbol === "string" ? position.symbol : undefined,
333582
+ leverage: typeof position.leverage === "number" ? position.leverage : undefined,
333583
+ marginMode: typeof position.marginMode === "string" ? position.marginMode : undefined
333584
+ }));
333131
333585
  }
333132
- function decodeMessageData(data) {
333133
- if (typeof data === "string") {
333134
- return data;
333135
- }
333136
- if (Buffer2.isBuffer(data)) {
333137
- return data.toString("utf8");
333586
+ async function handleGetPerpConfigState(ctx) {
333587
+ const { wrappedCallback, cex: cex3, normalizedCex, broker } = ctx;
333588
+ const payload = parsePayloadForAction(ctx, GetPerpConfigStatePayloadSchema);
333589
+ if (payload === null) {
333590
+ return;
333138
333591
  }
333139
- if (data instanceof ArrayBuffer) {
333140
- return Buffer2.from(data).toString("utf8");
333592
+ if (!broker) {
333593
+ return wrappedCallback({
333594
+ code: grpc8.status.INVALID_ARGUMENT,
333595
+ message: `Invalid CEX key: ${cex3}`
333596
+ }, null);
333141
333597
  }
333142
- if (ArrayBuffer.isView(data)) {
333143
- return Buffer2.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
333598
+ const exchange = broker;
333599
+ if (!exchangeSupports(exchange, "fetchPositions")) {
333600
+ return wrappedCallback({
333601
+ code: grpc8.status.UNIMPLEMENTED,
333602
+ message: `${normalizedCex} does not support fetchPositions`
333603
+ }, null);
333144
333604
  }
333145
- if (Array.isArray(data) && data.every((item) => Buffer2.isBuffer(item))) {
333146
- return Buffer2.concat(data).toString("utf8");
333605
+ try {
333606
+ const symbols = payload.symbol ? [payload.symbol] : undefined;
333607
+ const positions = await exchange.fetchPositions?.(symbols, payload.params);
333608
+ ctx.wrappedCallback(null, {
333609
+ result: JSON.stringify({
333610
+ exchange: normalizedCex,
333611
+ configs: extractPerpConfigs(positions ?? []),
333612
+ positions: positions ?? []
333613
+ })
333614
+ });
333615
+ } catch (error48) {
333616
+ safeLogError(`GetPerpConfigState failed for ${cex3}`, error48);
333617
+ ctx.wrappedCallback({
333618
+ code: grpc8.status.INTERNAL,
333619
+ message: `GetPerpConfigState failed: ${sanitizeErrorDetail(error48)}`
333620
+ }, null);
333147
333621
  }
333148
- return data;
333149
333622
  }
333150
-
333151
- class BinanceSpotUserDataStream {
333152
- exchange;
333153
- ws;
333154
- secretValues;
333155
- requestId = `user-data-${Date.now()}-${userDataRequestCounter++}`;
333156
- maxBufferedEvents;
333157
- queue = [];
333158
- waiters = [];
333159
- closed = false;
333160
- closeError = null;
333161
- subscriptionId = null;
333162
- constructor(exchange, options = {}) {
333163
- this.exchange = exchange;
333164
- this.maxBufferedEvents = options.maxBufferedEvents ?? DEFAULT_BINANCE_USER_DATA_MAX_BUFFERED_EVENTS;
333165
- this.secretValues = [
333166
- getOptionalExchangeString(exchange, "apiKey"),
333167
- getOptionalExchangeString(exchange, "secret")
333168
- ].filter((value) => value !== null);
333169
- this.ws = createWebSocket(getBinanceSpotWsApiUrl(exchange));
333170
- this.ws.on("open", () => this.subscribe());
333171
- this.ws.on("message", (data) => this.handleMessage(data));
333172
- this.ws.on("error", (error48) => this.fail(formatBinanceUserDataWebSocketError(error48, this.secretValues)));
333173
- this.ws.on("close", (code, reason) => this.handleClose(code, reason));
333174
- }
333175
- async* [Symbol.asyncIterator]() {
333176
- while (true) {
333177
- const event = await this.nextEvent();
333178
- if (!event) {
333179
- break;
333180
- }
333181
- yield event;
333182
- }
333623
+ async function handleSetPerpConfigState(ctx) {
333624
+ const { wrappedCallback, cex: cex3, normalizedCex, broker } = ctx;
333625
+ const payload = parsePayloadForAction(ctx, SetPerpConfigStatePayloadSchema);
333626
+ if (payload === null) {
333627
+ return;
333183
333628
  }
333184
- close() {
333185
- if (this.closed) {
333186
- return;
333187
- }
333188
- this.closed = true;
333189
- this.queue.length = 0;
333190
- try {
333191
- this.ws.close();
333192
- } catch {}
333193
- this.flushWaiters();
333629
+ if (!broker) {
333630
+ return wrappedCallback({
333631
+ code: grpc8.status.INVALID_ARGUMENT,
333632
+ message: `Invalid CEX key: ${cex3}`
333633
+ }, null);
333194
333634
  }
333195
- handleClose(code, reason) {
333196
- if (this.closed) {
333197
- return;
333198
- }
333199
- this.fail(formatBinanceUserDataWebSocketClose(code, reason, this.secretValues));
333635
+ const exchange = broker;
333636
+ if (!exchangeSupports(exchange, "setLeverage")) {
333637
+ return wrappedCallback({
333638
+ code: grpc8.status.UNIMPLEMENTED,
333639
+ message: `${normalizedCex} does not support setLeverage`
333640
+ }, null);
333200
333641
  }
333201
- subscribe() {
333202
- const apiKey = getExchangeString(this.exchange, "apiKey");
333203
- const signedParams = signUserDataStreamParams(this.exchange, {
333204
- apiKey,
333205
- timestamp: Date.now()
333642
+ try {
333643
+ const response = await exchange.setLeverage?.(payload.leverage, payload.symbol, {
333644
+ marginMode: payload.marginMode ?? "cross",
333645
+ ...payload.params
333206
333646
  });
333207
- this.ws.send(JSON.stringify({
333208
- id: this.requestId,
333209
- method: "userDataStream.subscribe.signature",
333210
- params: signedParams
333211
- }));
333647
+ ctx.wrappedCallback(null, {
333648
+ result: JSON.stringify({
333649
+ exchange: normalizedCex,
333650
+ symbol: payload.symbol,
333651
+ leverage: payload.leverage,
333652
+ marginMode: payload.marginMode ?? "cross",
333653
+ response
333654
+ })
333655
+ });
333656
+ } catch (error48) {
333657
+ safeLogError(`SetPerpConfigState failed for ${cex3}`, error48);
333658
+ ctx.wrappedCallback({
333659
+ code: grpc8.status.INTERNAL,
333660
+ message: `SetPerpConfigState failed: ${sanitizeErrorDetail(error48)}`
333661
+ }, null);
333212
333662
  }
333213
- handleMessage(data) {
333214
- if (this.closed) {
333215
- return;
333216
- }
333217
- let message;
333218
- try {
333219
- const decodedData = decodeMessageData(data);
333220
- message = typeof decodedData === "string" ? JSON.parse(decodedData) : decodedData;
333221
- } catch (error48) {
333222
- this.fail(error48 instanceof Error ? error48 : new Error("Invalid Binance user-data message"));
333223
- return;
333224
- }
333225
- if ("id" in message && message.id === this.requestId) {
333226
- if (message.status !== 200) {
333227
- this.fail(new Error(message.error?.msg ?? message.error?.message ?? `Binance user-data subscription failed with status ${message.status}`));
333228
- return;
333229
- }
333230
- this.subscriptionId = message.result?.subscriptionId ?? null;
333231
- return;
333232
- }
333233
- if ("status" in message && typeof message.status === "number" && message.status !== 200) {
333234
- const errorMessage = message.error?.msg ?? message.error?.message ?? `Binance user-data request failed with status ${message.status}`;
333235
- const errorCode2 = message.error?.code;
333236
- this.fail(new Error(typeof errorCode2 === "number" ? `${errorMessage} (code ${errorCode2})` : errorMessage));
333237
- return;
333238
- }
333239
- if (!("event" in message) || !message.event) {
333240
- return;
333241
- }
333242
- const subscriptionId = message.subscriptionId ?? this.subscriptionId;
333243
- if (subscriptionId === null || subscriptionId === undefined) {
333244
- return;
333245
- }
333246
- this.push({ subscriptionId, event: message.event });
333663
+ }
333664
+ async function handlePerpConfig(ctx) {
333665
+ if (ctx.action === Action.GetPerpConfigState) {
333666
+ return handleGetPerpConfigState(ctx);
333247
333667
  }
333248
- push(event) {
333249
- if (this.closed) {
333250
- return;
333251
- }
333252
- const waiter = this.waiters.shift();
333253
- if (waiter) {
333254
- waiter.resolve(event);
333255
- return;
333668
+ if (ctx.action === Action.SetPerpConfigState) {
333669
+ return handleSetPerpConfigState(ctx);
333670
+ }
333671
+ }
333672
+
333673
+ // src/handlers/execute-action/treasury-call.ts
333674
+ var grpc9 = __toESM(require_src3(), 1);
333675
+ async function handleTreasuryCall(ctx) {
333676
+ const { broker } = ctx;
333677
+ const callValue = parsePayloadForAction(ctx, CallPayloadSchema);
333678
+ if (callValue === null)
333679
+ return;
333680
+ let createOrderContext;
333681
+ let marketMetadataHash;
333682
+ try {
333683
+ if (callValue.functionName.startsWith("_") || callValue.functionName.includes("constructor") || callValue.functionName.includes("prototype")) {
333684
+ return ctx.wrappedCallback({
333685
+ code: grpc9.status.PERMISSION_DENIED,
333686
+ message: "Access to the requested function is denied"
333687
+ }, null);
333256
333688
  }
333257
- if (this.queue.length >= this.maxBufferedEvents) {
333258
- this.fail(new Error(`Binance user-data stream buffered event limit exceeded (${this.maxBufferedEvents}); downstream consumer is not keeping up`));
333259
- return;
333689
+ const argsArray = callArgs(callValue.args, callValue.params ?? {});
333690
+ const treasuryDiscovery = await handleTreasuryDiscoveryCall(broker, callValue.functionName, callValue.args, callValue.params ?? {});
333691
+ if (treasuryDiscovery.handled) {
333692
+ return ctx.wrappedCallback(null, {
333693
+ proof: ctx.verity.proof,
333694
+ result: JSON.stringify(treasuryDiscovery.result)
333695
+ });
333260
333696
  }
333261
- this.queue.push(event);
333262
- }
333263
- nextEvent() {
333264
- const event = this.queue.shift();
333265
- if (event) {
333266
- return Promise.resolve(event);
333697
+ const fn = broker[callValue.functionName];
333698
+ if (typeof fn !== "function" || broker.has?.[callValue.functionName] === false) {
333699
+ return ctx.wrappedCallback({
333700
+ code: grpc9.status.INVALID_ARGUMENT,
333701
+ message: `Function not found on broker: ${callValue.functionName}`
333702
+ }, null);
333267
333703
  }
333268
- if (this.closeError) {
333269
- return Promise.reject(this.closeError);
333704
+ if (callValue.functionName === "createOrder") {
333705
+ const [symbol2, orderType, side, quantity, price] = callValue.args;
333706
+ const requestedQuantity = asFiniteNumber(quantity);
333707
+ const requestedPrice = asFiniteNumber(price);
333708
+ const requestedNotional = requestedQuantity !== undefined && requestedPrice !== undefined ? asFiniteNumber(requestedQuantity * requestedPrice) : undefined;
333709
+ const telemetryIds = extractOrderTelemetryIds(callValue.params);
333710
+ const submissionTimestamp = new Date().toISOString();
333711
+ createOrderContext = {
333712
+ action: "CreateOrder",
333713
+ cex: ctx.cex,
333714
+ accountLabel: ctx.selectedBrokerAccount?.label,
333715
+ symbol: asNonEmptyString(symbol2),
333716
+ orderType: asNonEmptyString(orderType),
333717
+ side: asNonEmptyString(side),
333718
+ requestedQuantity,
333719
+ requestedNotional,
333720
+ orderAuthor: callValue.orderAuthor,
333721
+ brokerObservedTimestamp: submissionTimestamp,
333722
+ ...telemetryIds
333723
+ };
333724
+ if (createOrderContext.symbol !== undefined) {
333725
+ marketMetadataHash = await captureMarketMetadataSnapshot(ctx.brokerArchiver, broker, {
333726
+ exchange: ctx.cex,
333727
+ accountSelector: ctx.selectedBrokerAccount?.label,
333728
+ symbol: createOrderContext.symbol,
333729
+ action: "CreateOrder",
333730
+ brokerObservedTimestamp: submissionTimestamp,
333731
+ ...telemetryIds
333732
+ });
333733
+ }
333270
333734
  }
333271
- if (this.closed) {
333272
- return Promise.resolve(null);
333735
+ const result = await fn.apply(broker, argsArray);
333736
+ if (createOrderContext !== undefined) {
333737
+ emitOrderExecutionTelemetryInBackground(ctx.otelMetrics, createOrderContext, result);
333738
+ archiveOrderExecutionInBackground(ctx.brokerArchiver, createOrderContext, result, undefined, { marketMetadataHash });
333739
+ } else if (callValue.functionName === "fetchWithdrawals") {
333740
+ archiveWithdrawalObservationsInBackground(ctx.brokerArchiver, ctx.withdrawalObservationTracker, {
333741
+ exchange: ctx.normalizedCex,
333742
+ accountSelector: ctx.selectedBrokerAccount?.label,
333743
+ transactions: result
333744
+ });
333273
333745
  }
333274
- return new Promise((resolve, reject) => {
333275
- this.waiters.push({ resolve, reject });
333746
+ ctx.wrappedCallback(null, {
333747
+ proof: ctx.verity.proof,
333748
+ result: JSON.stringify(result)
333276
333749
  });
333277
- }
333278
- fail(error48) {
333279
- if (this.closeError) {
333280
- return;
333281
- }
333282
- this.closeError = error48;
333283
- this.closed = true;
333284
- this.queue.length = 0;
333285
- this.flushWaiters();
333286
- try {
333287
- this.ws.close();
333288
- } catch {}
333289
- }
333290
- flushWaiters() {
333291
- const error48 = this.closeError;
333292
- for (const waiter of this.waiters.splice(0)) {
333293
- if (error48) {
333294
- waiter.reject(error48);
333295
- } else {
333296
- waiter.resolve(null);
333297
- }
333750
+ } catch (error48) {
333751
+ if (createOrderContext !== undefined) {
333752
+ rethrowArchiveDurabilityError(error48);
333753
+ emitOrderExecutionTelemetryInBackground(ctx.otelMetrics, createOrderContext, undefined, error48);
333754
+ archiveOrderExecutionInBackground(ctx.brokerArchiver, createOrderContext, undefined, error48, { marketMetadataHash });
333298
333755
  }
333756
+ safeLogError("Call failed", error48);
333757
+ rejectWithGrpcError(ctx, error48, {
333758
+ message: getErrorMessage(error48),
333759
+ preferStableMessageOnly: true,
333760
+ appendClassName: true
333761
+ });
333299
333762
  }
333300
333763
  }
333301
- function isBinanceBalanceUserDataEvent(event) {
333302
- return event.e === "outboundAccountPosition" || event.e === "balanceUpdate" || event.e === "externalLockUpdate";
333303
- }
333304
- function isBinanceOrderUserDataEvent(event) {
333305
- return event.e === "executionReport" || event.e === "listStatus";
333306
- }
333307
-
333308
- // src/helpers/market-data-archive/ohlcv-bar-tracker.ts
333309
- function isFiniteNumber(value) {
333310
- return typeof value === "number" && Number.isFinite(value);
333764
+ function asNonEmptyString(value) {
333765
+ return typeof value === "string" && value.trim() ? value : undefined;
333311
333766
  }
333312
- function parseOhlcvBar(value) {
333313
- if (!Array.isArray(value) || value.length < 6) {
333314
- return null;
333315
- }
333316
- const [openTimeMs, open, high, low, close, volume, quoteVolume] = value;
333317
- if (!isFiniteNumber(openTimeMs) || !isFiniteNumber(open) || !isFiniteNumber(high) || !isFiniteNumber(low) || !isFiniteNumber(close) || !isFiniteNumber(volume)) {
333318
- return null;
333767
+ function asFiniteNumber(value) {
333768
+ if (typeof value === "number") {
333769
+ return Number.isFinite(value) ? value : undefined;
333319
333770
  }
333320
- const bar = {
333321
- openTimeMs,
333322
- open,
333323
- high,
333324
- low,
333325
- close,
333326
- volume
333327
- };
333328
- if (isFiniteNumber(quoteVolume)) {
333329
- bar.quoteVolume = quoteVolume;
333771
+ if (typeof value !== "string" || !value.trim()) {
333772
+ return;
333330
333773
  }
333331
- return bar;
333774
+ const parsed = Number(value);
333775
+ return Number.isFinite(parsed) ? parsed : undefined;
333332
333776
  }
333333
- function extractOhlcvBars(payload) {
333334
- if (!Array.isArray(payload) || payload.length === 0) {
333335
- return [];
333777
+
333778
+ // src/handlers/execute-action/withdraw.ts
333779
+ var grpc10 = __toESM(require_src3(), 1);
333780
+ async function handleWithdraw(ctx) {
333781
+ const {
333782
+ call,
333783
+ wrappedCallback,
333784
+ policy,
333785
+ brokers,
333786
+ metadata,
333787
+ normalizedCex,
333788
+ cex: cex3,
333789
+ symbol: symbol2,
333790
+ selectedBrokerAccount,
333791
+ broker,
333792
+ verity,
333793
+ applyVerityToBroker,
333794
+ useVerity,
333795
+ verityProverUrl,
333796
+ otelMetrics,
333797
+ brokerArchiver
333798
+ } = ctx;
333799
+ const verityProof = verity.proof;
333800
+ if (!symbol2) {
333801
+ return ctx.wrappedCallback({
333802
+ code: grpc10.status.INVALID_ARGUMENT,
333803
+ message: `ValidationError: Symbol required`
333804
+ }, null);
333336
333805
  }
333337
- const rawBars = Array.isArray(payload[0]) ? payload : [payload];
333338
- const byOpenTime = new Map;
333339
- for (const entry of rawBars) {
333340
- const bar = parseOhlcvBar(entry);
333341
- if (bar) {
333342
- byOpenTime.set(bar.openTimeMs, bar);
333343
- }
333806
+ const transferValue = parsePayloadForAction(ctx, WithdrawPayloadSchema);
333807
+ if (transferValue === null)
333808
+ return;
333809
+ let withdrawNetwork;
333810
+ try {
333811
+ withdrawNetwork = await resolveTransferNetwork(broker, symbol2, transferValue.chain);
333812
+ } catch (error48) {
333813
+ const message = getErrorMessage(error48);
333814
+ return ctx.wrappedCallback({
333815
+ code: stableGrpcErrorCode(message) ?? grpc10.status.INVALID_ARGUMENT,
333816
+ message
333817
+ }, null);
333344
333818
  }
333345
- return [...byOpenTime.values()].sort((a, b2) => a.openTimeMs - b2.openTimeMs);
333346
- }
333347
- class OhlcvBarTracker {
333348
- lastOpenTimeMs = null;
333349
- lastBar = null;
333350
- process(payload, brokerVersion) {
333351
- const bars = extractOhlcvBars(payload);
333352
- if (bars.length === 0) {
333353
- return [];
333354
- }
333355
- if (bars.length === 1) {
333356
- const [bar] = bars;
333357
- return bar ? this.processSingleBar(bar, brokerVersion) : [];
333358
- }
333359
- return this.processBatch(bars, brokerVersion);
333819
+ const transferValidation = validateWithdraw(policy, cex3, withdrawNetwork.brokerNetworkId, transferValue.recipientAddress, transferValue.amount, symbol2);
333820
+ if (!transferValidation.valid) {
333821
+ return ctx.wrappedCallback({
333822
+ code: grpc10.status.PERMISSION_DENIED,
333823
+ message: `policy_withdrawal_denied: ${transferValidation.error}`
333824
+ }, null);
333360
333825
  }
333361
- processSingleBar(currentBar, brokerVersion) {
333362
- if (this.lastOpenTimeMs !== null && currentBar.openTimeMs < this.lastOpenTimeMs) {
333363
- return [];
333364
- }
333365
- const candidates = [];
333366
- if (this.lastOpenTimeMs !== null && this.lastBar !== null && currentBar.openTimeMs !== this.lastOpenTimeMs) {
333367
- candidates.push({
333368
- bar: this.lastBar,
333369
- isClosed: true,
333370
- brokerVersion
333371
- });
333372
- }
333373
- candidates.push({
333374
- bar: currentBar,
333375
- isClosed: false,
333376
- brokerVersion
333377
- });
333378
- this.lastOpenTimeMs = currentBar.openTimeMs;
333379
- this.lastBar = currentBar;
333380
- return candidates;
333826
+ const travelRule = resolveTravelRuleDecision(policy, cex3, transferValue.recipientAddress);
333827
+ if (travelRule.mode === "denied") {
333828
+ return ctx.wrappedCallback({
333829
+ code: grpc10.status.FAILED_PRECONDITION,
333830
+ message: `travel_rule_denied: ${travelRule.error}`
333831
+ }, null);
333381
333832
  }
333382
- processBatch(bars, brokerVersion) {
333383
- const firstBar = bars[0];
333384
- const lastBar = bars[bars.length - 1];
333385
- if (!firstBar || !lastBar) {
333386
- return [];
333387
- }
333388
- const lastOpenTimeMs = this.lastOpenTimeMs;
333389
- if (lastOpenTimeMs !== null && lastBar.openTimeMs < lastOpenTimeMs) {
333390
- return [];
333391
- }
333392
- const barsToProcess = lastOpenTimeMs === null ? bars : bars.filter((bar) => bar.openTimeMs >= lastOpenTimeMs);
333393
- const firstBarToProcess = barsToProcess[0];
333394
- const lastBarToProcess = barsToProcess[barsToProcess.length - 1];
333395
- if (!firstBarToProcess || !lastBarToProcess) {
333396
- return [];
333397
- }
333398
- const candidates = [];
333399
- if (this.lastBar !== null && lastOpenTimeMs !== null && lastOpenTimeMs < firstBarToProcess.openTimeMs) {
333400
- candidates.push({
333401
- bar: this.lastBar,
333402
- isClosed: true,
333403
- brokerVersion
333404
- });
333405
- }
333406
- for (let index2 = 0;index2 < barsToProcess.length - 1; index2 += 1) {
333407
- const bar = barsToProcess[index2];
333408
- if (bar) {
333409
- candidates.push({
333410
- bar,
333411
- isClosed: true,
333412
- brokerVersion
333413
- });
333833
+ const withdrawOrderId = transferValue.params.withdrawOrderId;
333834
+ const clientWithdrawalId = typeof withdrawOrderId === "string" && withdrawOrderId.length > 0 ? withdrawOrderId : undefined;
333835
+ try {
333836
+ const transaction = travelRule.mode === "localentity" ? await withdrawViaLocalEntity(broker, {
333837
+ code: symbol2,
333838
+ amount: transferValue.amount,
333839
+ address: transferValue.recipientAddress,
333840
+ network: withdrawNetwork.exchangeNetworkId,
333841
+ questionnaire: travelRule.questionnaire,
333842
+ params: transferValue.params
333843
+ }) : await broker.withdraw(symbol2, transferValue.amount, transferValue.recipientAddress, undefined, {
333844
+ ...transferValue.params ?? {},
333845
+ network: withdrawNetwork.exchangeNetworkId
333846
+ });
333847
+ log.info(`Withdraw Result: ${JSON.stringify(transaction)}`);
333848
+ const normalized = normalizeCcxtTransactionForArchive(transaction);
333849
+ archiveTransferEventInBackground(brokerArchiver, {
333850
+ exchange: cex3,
333851
+ accountSelector: selectedBrokerAccount?.label,
333852
+ assetSymbol: normalized.assetSymbol ?? symbol2,
333853
+ transfer: {
333854
+ eventKind: "withdrawal",
333855
+ lifecycleAction: "submit_withdrawal",
333856
+ status: normalized.status,
333857
+ amount: normalized.amount ?? String(transferValue.amount),
333858
+ address: normalized.address ?? transferValue.recipientAddress,
333859
+ network: normalized.network ?? withdrawNetwork.exchangeNetworkId,
333860
+ externalId: normalized.externalId,
333861
+ clientWithdrawalId,
333862
+ txid: normalized.txid,
333863
+ feeAmount: normalized.feeAmount,
333864
+ feeCurrency: normalized.feeCurrency,
333865
+ exchangeTimestamp: normalized.exchangeTimestamp,
333866
+ payload: transaction
333867
+ }
333868
+ });
333869
+ ctx.wrappedCallback(null, {
333870
+ proof: ctx.verity.proof,
333871
+ result: JSON.stringify({
333872
+ ...transaction,
333873
+ operatorAlias: withdrawNetwork.operatorAlias,
333874
+ brokerNetworkId: withdrawNetwork.brokerNetworkId,
333875
+ exchangeNetworkId: withdrawNetwork.exchangeNetworkId
333876
+ })
333877
+ });
333878
+ } catch (error48) {
333879
+ safeLogError("Withdraw failed", error48);
333880
+ archiveTransferEventInBackground(brokerArchiver, {
333881
+ exchange: cex3,
333882
+ accountSelector: selectedBrokerAccount?.label,
333883
+ assetSymbol: symbol2,
333884
+ transfer: {
333885
+ eventKind: "withdrawal",
333886
+ lifecycleAction: "submit_withdrawal",
333887
+ status: "failed",
333888
+ amount: String(transferValue.amount),
333889
+ address: transferValue.recipientAddress,
333890
+ network: withdrawNetwork.exchangeNetworkId,
333891
+ clientWithdrawalId,
333892
+ errorSummary: getErrorMessage(error48),
333893
+ payload: { recipientAddress: transferValue.recipientAddress }
333414
333894
  }
333415
- }
333416
- candidates.push({
333417
- bar: lastBarToProcess,
333418
- isClosed: false,
333419
- brokerVersion
333420
333895
  });
333421
- this.lastOpenTimeMs = lastBarToProcess.openTimeMs;
333422
- this.lastBar = lastBarToProcess;
333423
- return candidates;
333424
- }
333425
- }
333426
-
333427
- // src/helpers/market-data-archive/orderbook-sampler.ts
333428
- var DEFAULT_ORDERBOOK_INTERVAL_MS = 1000;
333429
- function getOrderbookIntervalMs() {
333430
- const raw = process.env.CEX_BROKER_ORDERBOOK_INTERVAL_MS ?? process.env.CEX_BROKER_ORDERBOOK_TOB_INTERVAL_MS;
333431
- if (!raw) {
333432
- return DEFAULT_ORDERBOOK_INTERVAL_MS;
333896
+ const code = mapCcxtErrorToGrpcStatus(error48) ?? grpc10.status.INTERNAL;
333897
+ ctx.wrappedCallback({
333898
+ code,
333899
+ message: `Withdraw failed: ${sanitizeErrorDetail(error48)}`
333900
+ }, null);
333433
333901
  }
333434
- const parsed = Number.parseInt(raw, 10);
333435
- return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_ORDERBOOK_INTERVAL_MS;
333436
- }
333437
- function isMarketArchiveEnabled() {
333438
- return process.env.CEX_BROKER_MARKET_ARCHIVE_ENABLED !== "false";
333439
333902
  }
333440
333903
 
333441
- class OrderbookSampler {
333442
- intervalMs;
333443
- lastEmitMs = null;
333444
- constructor(intervalMs = getOrderbookIntervalMs()) {
333445
- this.intervalMs = intervalMs;
333446
- }
333447
- shouldEmit(nowMs = Date.now()) {
333448
- if (this.lastEmitMs !== null && nowMs < this.lastEmitMs) {
333449
- this.lastEmitMs = nowMs;
333450
- return true;
333451
- }
333452
- if (this.lastEmitMs !== null && nowMs - this.lastEmitMs < this.intervalMs) {
333453
- return false;
333454
- }
333455
- this.lastEmitMs = nowMs;
333456
- return true;
333904
+ // src/handlers/execute-action/registry.ts
333905
+ var ACTION_HANDLERS = {
333906
+ [Action.Deposit]: handleDeposit,
333907
+ [Action.Withdraw]: handleWithdraw,
333908
+ [Action.Call]: handleTreasuryCall,
333909
+ [Action.InternalTransfer]: handleInternalTransfer,
333910
+ [Action.CreateOrder]: handleOrders,
333911
+ [Action.GetOrderDetails]: handleOrders,
333912
+ [Action.CancelOrder]: handleOrders,
333913
+ [Action.FetchCurrency]: handlePassThrough,
333914
+ [Action.FetchAccountId]: handlePassThrough,
333915
+ [Action.FetchFees]: handlePassThrough,
333916
+ [Action.FetchDepositAddresses]: handlePassThrough,
333917
+ [Action.FetchBalances]: handlePassThrough,
333918
+ [Action.FetchTicker]: handlePassThrough,
333919
+ [Action.GetPerpConfigState]: handlePerpConfig,
333920
+ [Action.SetPerpConfigState]: handlePerpConfig
333921
+ };
333922
+ async function dispatchExecuteAction(ctx) {
333923
+ const handler = ACTION_HANDLERS[ctx.action];
333924
+ if (!handler) {
333925
+ ctx.wrappedCallback({
333926
+ code: grpc11.status.INVALID_ARGUMENT,
333927
+ message: "Invalid Action"
333928
+ }, null);
333929
+ return;
333457
333930
  }
333931
+ await handler(ctx);
333458
333932
  }
333459
333933
 
333460
- // src/helpers/market-data-archive/parse-stream.ts
333461
- function isFiniteNumber2(value) {
333462
- return typeof value === "number" && Number.isFinite(value);
333463
- }
333464
- function toNumber2(value) {
333465
- if (isFiniteNumber2(value)) {
333466
- return value;
333467
- }
333468
- if (typeof value === "string") {
333469
- const parsed = Number.parseFloat(value);
333470
- if (Number.isFinite(parsed)) {
333471
- return parsed;
333472
- }
333473
- }
333474
- return;
333475
- }
333476
- function toStringId(value) {
333477
- if (typeof value === "string" && value.trim()) {
333478
- return value.trim();
333479
- }
333480
- if (typeof value === "number" && Number.isFinite(value)) {
333481
- return String(value);
333482
- }
333483
- return;
333484
- }
333485
- function scalarTimestampMs(value, fallbackMs) {
333486
- const numeric = toNumber2(value);
333487
- if (numeric !== undefined) {
333488
- return numeric < 1000000000000 ? numeric * 1000 : numeric;
333489
- }
333490
- if (typeof value === "string") {
333491
- const parsed = Date.parse(value);
333492
- if (Number.isFinite(parsed)) {
333493
- return parsed;
333494
- }
333495
- }
333496
- return fallbackMs;
333497
- }
333498
- function parseTrade(value, fallbackMs = Date.now()) {
333499
- const record2 = asRecord(value);
333500
- if (!record2) {
333501
- return null;
333502
- }
333503
- const tradeId = toStringId(record2.id);
333504
- const price = toNumber2(record2.price);
333505
- const amount = toNumber2(record2.amount);
333506
- const side = typeof record2.side === "string" ? record2.side.toLowerCase() : undefined;
333507
- if (!tradeId || price === undefined || amount === undefined || !side) {
333508
- return null;
333509
- }
333510
- const parsed = {
333511
- tradeId,
333512
- eventTimeMs: scalarTimestampMs(record2.timestamp, fallbackMs),
333513
- side,
333514
- price,
333515
- amount
333516
- };
333517
- const cost = toNumber2(record2.cost);
333518
- if (cost !== undefined) {
333519
- parsed.cost = cost;
333520
- }
333521
- if (typeof record2.takerOrMaker === "string") {
333522
- parsed.takerOrMaker = record2.takerOrMaker;
333523
- }
333524
- return parsed;
333525
- }
333526
- function extractTrades(payload, fallbackMs = Date.now()) {
333527
- if (Array.isArray(payload)) {
333528
- return payload.map((entry) => parseTrade(entry, fallbackMs)).filter((entry) => entry !== null);
333529
- }
333530
- const single = parseTrade(payload, fallbackMs);
333531
- return single ? [single] : [];
333934
+ // src/handlers/execute-action/handler.ts
333935
+ function isPublicMarketDataAction(action, payload) {
333936
+ if (action !== Action.Call)
333937
+ return false;
333938
+ return isOrderBookCallMethod(payload?.method ?? payload?.functionName);
333532
333939
  }
333533
- function parseTicker(value, fallbackMs) {
333534
- const record2 = asRecord(value);
333535
- if (!record2) {
333536
- return null;
333537
- }
333538
- const parsed = {
333539
- eventTimeMs: scalarTimestampMs(record2.timestamp, fallbackMs)
333540
- };
333541
- const fields = [
333542
- ["last", record2.last],
333543
- ["bid", record2.bid],
333544
- ["ask", record2.ask],
333545
- ["high", record2.high],
333546
- ["low", record2.low],
333547
- ["open", record2.open],
333548
- ["close", record2.close],
333549
- ["baseVolume", record2.baseVolume],
333550
- ["quoteVolume", record2.quoteVolume],
333551
- ["change", record2.change],
333552
- ["percentage", record2.percentage]
333553
- ];
333554
- for (const [key, rawValue] of fields) {
333555
- const numeric = toNumber2(rawValue);
333556
- if (numeric !== undefined) {
333557
- parsed[key] = numeric;
333940
+ function createExecuteActionHandler(deps) {
333941
+ const {
333942
+ policy,
333943
+ brokers,
333944
+ whitelistIps,
333945
+ useVerity,
333946
+ verityProverUrl,
333947
+ otelMetrics,
333948
+ brokerArchiver,
333949
+ orderActivityTracker
333950
+ } = deps;
333951
+ const withdrawalObservationTracker = deps.withdrawalObservationTracker ?? new WithdrawalObservationTracker;
333952
+ return async (call, callback) => {
333953
+ const startTime = Date.now();
333954
+ const { action: rawAction, cex: cex3, symbol: symbol2 } = call.request;
333955
+ const action = resolveAction(rawAction);
333956
+ let actionCompleted = false;
333957
+ const wrappedCallback = (error48, value) => {
333958
+ if (!actionCompleted) {
333959
+ actionCompleted = true;
333960
+ const latency = Date.now() - startTime;
333961
+ const actionName = getActionName(action);
333962
+ otelMetrics?.recordHistogram("execute_action_duration_ms", latency, {
333963
+ action: actionName,
333964
+ cex: cex3 || "unknown"
333965
+ });
333966
+ if (error48) {
333967
+ otelMetrics?.recordCounter("execute_action_errors_total", 1, {
333968
+ action: actionName,
333969
+ cex: cex3 || "unknown",
333970
+ error_type: error48.code ? grpc12.status[error48.code] || "unknown" : "unknown"
333971
+ });
333972
+ } else {
333973
+ otelMetrics?.recordCounter("execute_action_success_total", 1, {
333974
+ action: actionName,
333975
+ cex: cex3 || "unknown"
333976
+ });
333977
+ }
333978
+ }
333979
+ callback(error48, value);
333980
+ };
333981
+ try {
333982
+ log.info(`Request - ExecuteAction:`, { action, cex: cex3, symbol: symbol2 });
333983
+ otelMetrics?.recordCounter("execute_action_requests_total", 1, {
333984
+ action: getActionName(action),
333985
+ cex: cex3 || "unknown"
333986
+ });
333987
+ if (!authenticateRequest(call, whitelistIps)) {
333988
+ return wrappedCallback({
333989
+ code: grpc12.status.PERMISSION_DENIED,
333990
+ message: "Access denied: Unauthorized IP"
333991
+ }, null);
333992
+ }
333993
+ if (!action || !cex3) {
333994
+ return wrappedCallback({
333995
+ code: grpc12.status.INVALID_ARGUMENT,
333996
+ message: "`action` AND `cex` fields are required"
333997
+ }, null);
333998
+ }
333999
+ const normalizedCex = cex3.trim().toLowerCase();
334000
+ const metadata = call.metadata;
334001
+ const selectedBrokerAccount = selectBrokerAccountForCex(normalizedCex, brokers, metadata);
334002
+ const broker = selectedBrokerAccount?.exchange ?? createBroker(normalizedCex, call.metadata) ?? (isPublicMarketDataAction(action, call.request.payload) ? createPublicBroker(normalizedCex) : null);
334003
+ if (!broker) {
334004
+ return wrappedCallback({
334005
+ code: grpc12.status.UNAUTHENTICATED,
334006
+ message: `This Exchange is not registered and No API metadata was found`
334007
+ }, null);
334008
+ }
334009
+ const verity = { proof: "" };
334010
+ const applyVerityToBroker = (targetBroker) => {
334011
+ if (!useVerity)
334012
+ return;
334013
+ const override = buildHttpClientOverrideFromMetadata(metadata, verityProverUrl, (proof, notaryPubKey) => {
334014
+ verity.proof = proof;
334015
+ log.debug(`Verity proof:`, { proof, notaryPubKey });
334016
+ });
334017
+ targetBroker.setHttpClientOverride(override, verityHttpClientOverridePredicate);
334018
+ };
334019
+ const preludeCtx = {
334020
+ call,
334021
+ wrappedCallback,
334022
+ action,
334023
+ policy,
334024
+ brokers,
334025
+ metadata,
334026
+ normalizedCex,
334027
+ cex: cex3,
334028
+ symbol: symbol2,
334029
+ selectedBrokerAccount,
334030
+ broker,
334031
+ verity,
334032
+ applyVerityToBroker,
334033
+ useVerity,
334034
+ verityProverUrl,
334035
+ otelMetrics,
334036
+ brokerArchiver,
334037
+ orderActivityTracker,
334038
+ withdrawalObservationTracker
334039
+ };
334040
+ if (action === Action.Call) {
334041
+ const handled = await handleOrderBookCall(preludeCtx);
334042
+ if (handled)
334043
+ return;
334044
+ }
334045
+ applyVerityToBroker(broker);
334046
+ const ctx = { ...preludeCtx, broker };
334047
+ await dispatchExecuteAction(ctx);
334048
+ } catch (error48) {
334049
+ safeLogError("ExecuteAction unhandled error", error48);
334050
+ return wrappedCallback({
334051
+ code: grpc12.status.INTERNAL,
334052
+ message: "ExecuteAction failed unexpectedly"
334053
+ }, null);
333558
334054
  }
333559
- }
333560
- return parsed;
334055
+ };
333561
334056
  }
333562
-
333563
- // src/helpers/market-data-archive/orderbook-depth.ts
333564
- var DEFAULT_ORDERBOOK_ARCHIVE_DEPTH_LIMIT = 25;
333565
- var MAX_ORDERBOOK_ARCHIVE_DEPTH_LIMIT = 500;
333566
- function getOrderbookArchiveDepthLimit() {
333567
- const raw = process.env.CEX_BROKER_ORDERBOOK_ARCHIVE_DEPTH_LIMIT;
333568
- if (!raw) {
333569
- return DEFAULT_ORDERBOOK_ARCHIVE_DEPTH_LIMIT;
334057
+ // src/handlers/subscribe/broker-lifecycle.ts
334058
+ class SubscribeBrokerLifecycle {
334059
+ #brokers = new Map;
334060
+ #closing = new Map;
334061
+ #shuttingDown = false;
334062
+ register(broker, context2) {
334063
+ this.#brokers.set(broker, context2);
334064
+ if (this.#shuttingDown) {
334065
+ this.close(broker);
334066
+ }
333570
334067
  }
333571
- const parsed = Number.parseInt(raw, 10);
333572
- if (!Number.isFinite(parsed) || parsed <= 0) {
333573
- return DEFAULT_ORDERBOOK_ARCHIVE_DEPTH_LIMIT;
334068
+ close(broker) {
334069
+ const existing = this.#closing.get(broker);
334070
+ if (existing) {
334071
+ return existing;
334072
+ }
334073
+ const context2 = this.#brokers.get(broker) ?? {
334074
+ cex: "unknown",
334075
+ symbol: "unknown"
334076
+ };
334077
+ this.#brokers.delete(broker);
334078
+ const closing = (async () => {
334079
+ try {
334080
+ await broker.close();
334081
+ log.debug("Request-scoped Subscribe broker closed", context2);
334082
+ return "closed";
334083
+ } catch (error48) {
334084
+ log.warn("Failed to close request-scoped Subscribe broker", {
334085
+ ...context2,
334086
+ error: error48
334087
+ });
334088
+ return "failed";
334089
+ } finally {
334090
+ this.#closing.delete(broker);
334091
+ }
334092
+ })();
334093
+ this.#closing.set(broker, closing);
334094
+ return closing;
333574
334095
  }
333575
- return Math.min(parsed, MAX_ORDERBOOK_ARCHIVE_DEPTH_LIMIT);
333576
- }
333577
- function splitOrderBookSide(levels, limit) {
333578
- const prices = [];
333579
- const sizes = [];
333580
- for (const level of levels.slice(0, limit)) {
333581
- if (!Array.isArray(level) || level.length < 2) {
333582
- continue;
334096
+ async closeAll() {
334097
+ this.#shuttingDown = true;
334098
+ let failed = 0;
334099
+ while (this.#brokers.size > 0 || this.#closing.size > 0) {
334100
+ const inFlight = [...this.#closing.values()];
334101
+ const fresh = [...this.#brokers.keys()].map((broker) => this.close(broker));
334102
+ const outcomes = await Promise.all([...fresh, ...inFlight]);
334103
+ failed += outcomes.filter((outcome) => outcome === "failed").length;
333583
334104
  }
333584
- const price = level[0];
333585
- const size = level[1];
333586
- if (price === undefined || size === undefined || !Number.isFinite(price) || !Number.isFinite(size)) {
333587
- continue;
334105
+ if (failed > 0) {
334106
+ throw new Error(`${failed} request-scoped Subscribe broker(s) failed to close`);
333588
334107
  }
333589
- prices.push(price);
333590
- sizes.push(size);
333591
334108
  }
333592
- return { prices, sizes };
333593
334109
  }
334110
+ // src/handlers/subscribe/handler.ts
334111
+ var grpc13 = __toESM(require_src3(), 1);
333594
334112
 
333595
- // src/helpers/market-data-archive/rows.ts
333596
- function compactUndefined3(record2) {
333597
- return Object.fromEntries(Object.entries(record2).filter(([, value]) => value !== undefined));
333598
- }
333599
- function scalarTimestampMs2(value) {
333600
- if (typeof value === "number" && Number.isFinite(value)) {
333601
- return value;
333602
- }
333603
- if (typeof value === "string") {
333604
- if (/^\d+$/.test(value)) {
333605
- const numeric = Number.parseInt(value, 10);
333606
- if (Number.isFinite(numeric)) {
333607
- return numeric;
333608
- }
333609
- }
333610
- const parsed = Date.parse(value);
333611
- if (Number.isFinite(parsed)) {
333612
- return parsed;
333613
- }
334113
+ // src/helpers/binance-user-data-stream.ts
334114
+ import { Buffer as Buffer2 } from "node:buffer";
334115
+ import { createHmac } from "node:crypto";
334116
+ var BINANCE_SPOT_WS_API_URL = "wss://ws-api.binance.com:443/ws-api/v3";
334117
+ var DEFAULT_BINANCE_USER_DATA_MAX_BUFFERED_EVENTS = 16;
334118
+ var createWebSocket = (url3) => new wrapper_default(url3);
334119
+ var userDataRequestCounter = 0;
334120
+ function getExchangeString(exchange, key) {
334121
+ const value = exchange[key];
334122
+ if (typeof value !== "string" || value.length === 0) {
334123
+ throw new Error(`Binance user-data stream requires exchange.${key}`);
333614
334124
  }
333615
- return Date.now();
334125
+ return value;
333616
334126
  }
333617
- function parseSequence(value) {
333618
- if (typeof value === "number" && Number.isFinite(value)) {
333619
- return value;
333620
- }
333621
- if (typeof value === "string" && /^\d+$/.test(value)) {
333622
- return Number.parseInt(value, 10);
333623
- }
333624
- return;
334127
+ function sortedQuery(params) {
334128
+ return Object.entries(params).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`).join("&");
333625
334129
  }
333626
- function topOfBookLevel(levels) {
333627
- const level = levels[0];
333628
- if (!level || level.length < 2) {
333629
- return null;
333630
- }
333631
- const price = level[0];
333632
- const size = level[1];
333633
- if (price === undefined || size === undefined || !Number.isFinite(price) || !Number.isFinite(size)) {
333634
- return null;
334130
+ function signUserDataStreamParams(exchange, params) {
334131
+ const signParams = exchange.signParams;
334132
+ if (typeof signParams === "function") {
334133
+ return signParams.call(exchange, params);
333635
334134
  }
333636
- return { price, size };
334135
+ const secret = getExchangeString(exchange, "secret");
334136
+ return {
334137
+ ...params,
334138
+ signature: createHmac("sha256", secret).update(sortedQuery(params)).digest("hex")
334139
+ };
333637
334140
  }
333638
- function computeSpreadBps(bestBid, bestAsk) {
333639
- if (bestBid <= 0 || bestAsk <= 0 || bestAsk < bestBid) {
333640
- return 0;
333641
- }
333642
- const mid = (bestBid + bestAsk) / 2;
333643
- if (mid <= 0) {
333644
- return 0;
333645
- }
333646
- return (bestAsk - bestBid) / mid * 1e4;
334141
+ function getBinanceSpotWsApiUrl(exchange) {
334142
+ const urls = exchange.urls;
334143
+ return urls?.api?.ws?.["ws-api"]?.spot ?? BINANCE_SPOT_WS_API_URL;
333647
334144
  }
333648
- function buildOrderbookArchiveTags(input, receivedTimeMs) {
333649
- return buildCommonArchiveTags({
333650
- deploymentId: input.deploymentId,
333651
- accountSelector: input.accountSelector,
333652
- exchange: input.exchange,
333653
- symbol: input.symbol,
333654
- brokerObservedTimestamp: new Date(receivedTimeMs).toISOString()
333655
- });
334145
+ function getRecord(value) {
334146
+ return typeof value === "object" && value !== null ? value : null;
333656
334147
  }
333657
- function buildOrderbookSnapshotRow(input) {
333658
- const bid = topOfBookLevel(input.snapshot.bids);
333659
- const ask = topOfBookLevel(input.snapshot.asks);
333660
- if (!bid || !ask) {
333661
- return null;
334148
+ function getMessage(value) {
334149
+ if (value instanceof Error) {
334150
+ return value.message;
333662
334151
  }
333663
- const archiveDepthLimit = getOrderbookArchiveDepthLimit();
333664
- const bids = splitOrderBookSide(input.snapshot.bids, archiveDepthLimit);
333665
- const asks = splitOrderBookSide(input.snapshot.asks, archiveDepthLimit);
333666
- if (bids.prices.length === 0 || asks.prices.length === 0) {
333667
- return null;
334152
+ if (typeof value === "string" && value.length > 0) {
334153
+ return value;
333668
334154
  }
333669
- const eventTimeMs = scalarTimestampMs2(input.snapshot.timestamp);
333670
- const receivedTimeMs = input.snapshot.receivedTimestamp;
333671
- const mid = (bid.price + ask.price) / 2;
333672
- const sequence = parseSequence(input.snapshot.sequence);
333673
- return {
333674
- table: "market_data.orderbook_snapshots",
333675
- row: compactUndefined3({
333676
- ...buildOrderbookArchiveTags(input, receivedTimeMs),
333677
- asset_type: input.assetType,
333678
- event_time_ms: eventTimeMs,
333679
- received_time_ms: receivedTimeMs,
333680
- best_bid: bid.price,
333681
- best_ask: ask.price,
333682
- bid_size: bid.size,
333683
- ask_size: ask.size,
333684
- mid,
333685
- spread_bps: computeSpreadBps(bid.price, ask.price),
333686
- depth_limit: archiveDepthLimit,
333687
- bid_levels: bids.prices.length,
333688
- ask_levels: asks.prices.length,
333689
- bids_price: bids.prices,
333690
- bids_size: bids.sizes,
333691
- asks_price: asks.prices,
333692
- asks_size: asks.sizes,
333693
- sequence
333694
- })
333695
- };
333696
- }
333697
- function buildCandleRow(input) {
333698
- const { context: context2, bar, isClosed, brokerVersion, receivedTimestamp } = input;
333699
- const tags = buildCommonArchiveTags({
333700
- deploymentId: context2.deploymentId,
333701
- accountSelector: context2.accountSelector,
333702
- exchange: context2.exchange,
333703
- symbol: context2.symbol,
333704
- brokerObservedTimestamp: new Date(receivedTimestamp).toISOString()
333705
- });
333706
- return {
333707
- table: "market_data.candles",
333708
- row: compactUndefined3({
333709
- ...tags,
333710
- asset_type: context2.assetType,
333711
- timeframe: context2.timeframe ?? "1m",
333712
- open_time_ms: bar.openTimeMs,
333713
- open: bar.open,
333714
- high: bar.high,
333715
- low: bar.low,
333716
- close: bar.close,
333717
- volume: bar.volume,
333718
- quote_volume: bar.quoteVolume,
333719
- is_closed: isClosed ? 1 : 0,
333720
- broker_version: brokerVersion
333721
- })
333722
- };
334155
+ const record2 = getRecord(value);
334156
+ const message = record2?.message;
334157
+ return typeof message === "string" && message.length > 0 ? message : null;
333723
334158
  }
333724
- function buildCexStreamEventRow(input) {
333725
- const receivedTimeMs = input.receivedTimestamp;
333726
- const redactedPayload = redactStreamPayload(input.payload);
333727
- const tags = buildCommonArchiveTags({
333728
- deploymentId: input.deploymentId,
333729
- accountSelector: input.accountSelector,
333730
- exchange: input.exchange,
333731
- symbol: input.symbol,
333732
- brokerObservedTimestamp: new Date(receivedTimeMs).toISOString()
333733
- });
333734
- return {
333735
- table: "market_data.cex_stream_events",
333736
- row: compactUndefined3({
333737
- ...tags,
333738
- asset_type: input.assetType,
333739
- stream_type: input.streamType,
333740
- event_time_ms: input.eventTimeMs ?? receivedTimeMs,
333741
- received_time_ms: receivedTimeMs,
333742
- payload_json: JSON.stringify(redactedPayload)
333743
- })
333744
- };
334159
+ function getOptionalExchangeString(exchange, key) {
334160
+ const value = exchange[key];
334161
+ return typeof value === "string" && value.length > 0 ? value : null;
333745
334162
  }
333746
- function buildCexTickerEventRow(input, ticker) {
333747
- const receivedTimeMs = input.receivedTimestamp;
333748
- const tags = buildCommonArchiveTags({
333749
- deploymentId: input.deploymentId,
333750
- accountSelector: input.accountSelector,
333751
- exchange: input.exchange,
333752
- symbol: input.symbol,
333753
- brokerObservedTimestamp: new Date(receivedTimeMs).toISOString()
333754
- });
333755
- return {
333756
- table: "market_data.cex_ticker_events",
333757
- row: compactUndefined3({
333758
- ...tags,
333759
- asset_type: input.assetType,
333760
- event_time_ms: ticker.eventTimeMs,
333761
- received_time_ms: receivedTimeMs,
333762
- last: ticker.last,
333763
- bid: ticker.bid,
333764
- ask: ticker.ask,
333765
- high: ticker.high,
333766
- low: ticker.low,
333767
- open: ticker.open,
333768
- close: ticker.close,
333769
- base_volume: ticker.baseVolume,
333770
- quote_volume: ticker.quoteVolume,
333771
- change: ticker.change,
333772
- percentage: ticker.percentage,
333773
- payload_json: JSON.stringify(redactStreamPayload(input.payload))
333774
- })
333775
- };
334163
+ function redactDiagnosticMessage(message, secretValues) {
334164
+ let redacted = message;
334165
+ for (const value of secretValues) {
334166
+ if (value.length > 0) {
334167
+ redacted = redacted.split(value).join("[redacted]");
334168
+ }
334169
+ }
334170
+ return redacted.replace(/(\b(?:apiKey|secret|signature)\b\s*=\s*)[^\s&,;)]+/gi, "$1[redacted]").replace(/("(?:apiKey|secret|signature)"\s*:\s*")[^"]*(")/gi, "$1[redacted]$2");
333776
334171
  }
333777
- function buildCexTradeRow(input, trade) {
333778
- const receivedTimeMs = input.receivedTimestamp;
333779
- const tags = buildCommonArchiveTags({
333780
- deploymentId: input.deploymentId,
333781
- accountSelector: input.accountSelector,
333782
- exchange: input.exchange,
333783
- symbol: input.symbol,
333784
- brokerObservedTimestamp: new Date(receivedTimeMs).toISOString()
333785
- });
333786
- return {
333787
- table: "market_data.cex_trades",
333788
- row: compactUndefined3({
333789
- ...tags,
333790
- asset_type: input.assetType,
333791
- trade_id: trade.tradeId,
333792
- event_time_ms: trade.eventTimeMs,
333793
- received_time_ms: receivedTimeMs,
333794
- side: trade.side,
333795
- price: trade.price,
333796
- amount: trade.amount,
333797
- cost: trade.cost,
333798
- taker_or_maker: trade.takerOrMaker
333799
- })
333800
- };
334172
+ function formatBinanceUserDataWebSocketError(event, secretValues) {
334173
+ const record2 = getRecord(event);
334174
+ const message = getMessage(record2?.error) ?? getMessage(record2?.message) ?? getMessage(event);
334175
+ const safeMessage = message === null ? null : redactDiagnosticMessage(message, secretValues);
334176
+ return new Error(safeMessage ? `Binance user-data WebSocket error: ${safeMessage}` : "Binance user-data WebSocket error");
333801
334177
  }
333802
-
333803
- // src/helpers/market-data-archive/capture.ts
333804
- async function recordWatchMetric(otelMetrics, metricName, labels) {
333805
- try {
333806
- await otelMetrics?.recordCounter(metricName, 1, labels);
333807
- } catch {}
334178
+ function getCloseReason(value) {
334179
+ if (typeof value === "string") {
334180
+ return value.length > 0 ? value : null;
334181
+ }
334182
+ if (Buffer2.isBuffer(value)) {
334183
+ const reason = value.toString("utf8");
334184
+ return reason.length > 0 ? reason : null;
334185
+ }
334186
+ if (value instanceof Uint8Array) {
334187
+ const reason = Buffer2.from(value).toString("utf8");
334188
+ return reason.length > 0 ? reason : null;
334189
+ }
334190
+ return null;
333808
334191
  }
333809
- function watchLabels(stream4, input) {
333810
- return {
333811
- stream: stream4,
333812
- exchange: input.exchange,
333813
- symbol: input.symbol
333814
- };
334192
+ function formatBinanceUserDataWebSocketClose(codeOrEvent, reasonOrUndefined, secretValues) {
334193
+ const record2 = getRecord(codeOrEvent);
334194
+ const code = record2 ? record2.code : codeOrEvent;
334195
+ const reason = getCloseReason(record2 ? record2.reason : reasonOrUndefined);
334196
+ const safeReason = reason === null ? null : redactDiagnosticMessage(reason, secretValues);
334197
+ const details = [
334198
+ typeof code === "number" || typeof code === "string" ? `code=${code}` : null,
334199
+ safeReason ? `reason=${safeReason}` : null
334200
+ ].filter((detail) => detail !== null);
334201
+ return new Error(details.length > 0 ? `Binance user-data WebSocket closed unexpectedly (${details.join(", ")})` : "Binance user-data WebSocket closed unexpectedly");
333815
334202
  }
333816
- function archiveOrderbookInBackground(archiver, otelMetrics, input, options) {
333817
- const labels = watchLabels("orderbook", input);
333818
- recordWatchMetric(otelMetrics, "cex_watch_frames_received_total", labels);
333819
- if (!isMarketArchiveEnabled() || !archiver?.isEnabled()) {
333820
- return;
334203
+ function decodeMessageData(data) {
334204
+ if (typeof data === "string") {
334205
+ return data;
333821
334206
  }
333822
- if (options?.sampledOut) {
333823
- recordWatchMetric(otelMetrics, "cex_watch_frames_sampled_out_total", labels);
333824
- return;
334207
+ if (Buffer2.isBuffer(data)) {
334208
+ return data.toString("utf8");
333825
334209
  }
333826
- queueMicrotask(() => {
333827
- try {
333828
- const row = buildOrderbookSnapshotRow(input);
333829
- if (row) {
333830
- archiver.enqueue(row);
333831
- recordWatchMetric(otelMetrics, "cex_watch_frames_archived_total", labels);
334210
+ if (data instanceof ArrayBuffer) {
334211
+ return Buffer2.from(data).toString("utf8");
334212
+ }
334213
+ if (ArrayBuffer.isView(data)) {
334214
+ return Buffer2.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
334215
+ }
334216
+ if (Array.isArray(data) && data.every((item) => Buffer2.isBuffer(item))) {
334217
+ return Buffer2.concat(data).toString("utf8");
334218
+ }
334219
+ return data;
334220
+ }
334221
+
334222
+ class BinanceSpotUserDataStream {
334223
+ exchange;
334224
+ ws;
334225
+ secretValues;
334226
+ requestId = `user-data-${Date.now()}-${userDataRequestCounter++}`;
334227
+ maxBufferedEvents;
334228
+ queue = [];
334229
+ waiters = [];
334230
+ closed = false;
334231
+ closeError = null;
334232
+ subscriptionId = null;
334233
+ constructor(exchange, options = {}) {
334234
+ this.exchange = exchange;
334235
+ this.maxBufferedEvents = options.maxBufferedEvents ?? DEFAULT_BINANCE_USER_DATA_MAX_BUFFERED_EVENTS;
334236
+ this.secretValues = [
334237
+ getOptionalExchangeString(exchange, "apiKey"),
334238
+ getOptionalExchangeString(exchange, "secret")
334239
+ ].filter((value) => value !== null);
334240
+ this.ws = createWebSocket(getBinanceSpotWsApiUrl(exchange));
334241
+ this.ws.on("open", () => this.subscribe());
334242
+ this.ws.on("message", (data) => this.handleMessage(data));
334243
+ this.ws.on("error", (error48) => this.fail(formatBinanceUserDataWebSocketError(error48, this.secretValues)));
334244
+ this.ws.on("close", (code, reason) => this.handleClose(code, reason));
334245
+ }
334246
+ async* [Symbol.asyncIterator]() {
334247
+ while (true) {
334248
+ const event = await this.nextEvent();
334249
+ if (!event) {
334250
+ break;
333832
334251
  }
333833
- } catch (error48) {
333834
- rethrowArchiveDurabilityError(error48);
333835
- log.warn("Failed to archive orderbook snapshot", { error: error48 });
334252
+ yield event;
333836
334253
  }
333837
- });
333838
- }
333839
- function archiveOhlcvInBackground(archiver, otelMetrics, tracker, input) {
333840
- const labels = watchLabels("ohlcv", input);
333841
- recordWatchMetric(otelMetrics, "cex_watch_frames_received_total", labels);
333842
- if (!isMarketArchiveEnabled() || !archiver?.isEnabled()) {
333843
- return;
333844
334254
  }
333845
- queueMicrotask(() => {
334255
+ close() {
334256
+ if (this.closed) {
334257
+ return;
334258
+ }
334259
+ this.closed = true;
334260
+ this.queue.length = 0;
333846
334261
  try {
333847
- const candidates = tracker.process(input.payload, input.receivedTimestamp);
333848
- for (const candidate of candidates) {
333849
- const row = buildCandleRow({
333850
- context: input,
333851
- bar: candidate.bar,
333852
- isClosed: candidate.isClosed,
333853
- brokerVersion: candidate.brokerVersion,
333854
- receivedTimestamp: input.receivedTimestamp
333855
- });
333856
- archiver.enqueue(row);
333857
- }
333858
- if (candidates.length > 0) {
333859
- recordWatchMetric(otelMetrics, "cex_watch_frames_archived_total", labels);
333860
- }
333861
- } catch (error48) {
333862
- rethrowArchiveDurabilityError(error48);
333863
- log.warn("Failed to archive OHLCV candle", { error: error48 });
334262
+ this.ws.close();
334263
+ } catch {}
334264
+ this.flushWaiters();
334265
+ }
334266
+ handleClose(code, reason) {
334267
+ if (this.closed) {
334268
+ return;
333864
334269
  }
333865
- });
333866
- }
333867
- function createOrderbookSampler() {
333868
- return new OrderbookSampler;
333869
- }
333870
- function createOhlcvBarTracker() {
333871
- return new OhlcvBarTracker;
333872
- }
333873
- function archiveMarketRowsInBackground(archiver, otelMetrics, stream4, input, enqueueRows) {
333874
- const labels = watchLabels(stream4, input);
333875
- recordWatchMetric(otelMetrics, "cex_watch_frames_received_total", labels);
333876
- if (!isMarketArchiveEnabled() || !archiver?.isEnabled()) {
333877
- return;
334270
+ this.fail(formatBinanceUserDataWebSocketClose(code, reason, this.secretValues));
333878
334271
  }
333879
- queueMicrotask(() => {
334272
+ subscribe() {
334273
+ const apiKey = getExchangeString(this.exchange, "apiKey");
334274
+ const signedParams = signUserDataStreamParams(this.exchange, {
334275
+ apiKey,
334276
+ timestamp: Date.now()
334277
+ });
334278
+ this.ws.send(JSON.stringify({
334279
+ id: this.requestId,
334280
+ method: "userDataStream.subscribe.signature",
334281
+ params: signedParams
334282
+ }));
334283
+ }
334284
+ handleMessage(data) {
334285
+ if (this.closed) {
334286
+ return;
334287
+ }
334288
+ let message;
333880
334289
  try {
333881
- const rows = enqueueRows();
333882
- for (const row of rows) {
333883
- archiver.enqueue(row);
334290
+ const decodedData = decodeMessageData(data);
334291
+ message = typeof decodedData === "string" ? JSON.parse(decodedData) : decodedData;
334292
+ } catch (error48) {
334293
+ this.fail(error48 instanceof Error ? error48 : new Error("Invalid Binance user-data message"));
334294
+ return;
334295
+ }
334296
+ if ("id" in message && message.id === this.requestId) {
334297
+ if (message.status !== 200) {
334298
+ this.fail(new Error(message.error?.msg ?? message.error?.message ?? `Binance user-data subscription failed with status ${message.status}`));
334299
+ return;
333884
334300
  }
333885
- if (rows.length > 0) {
333886
- recordWatchMetric(otelMetrics, "cex_watch_frames_archived_total", labels);
334301
+ this.subscriptionId = message.result?.subscriptionId ?? null;
334302
+ return;
334303
+ }
334304
+ if ("status" in message && typeof message.status === "number" && message.status !== 200) {
334305
+ const errorMessage = message.error?.msg ?? message.error?.message ?? `Binance user-data request failed with status ${message.status}`;
334306
+ const errorCode2 = message.error?.code;
334307
+ this.fail(new Error(typeof errorCode2 === "number" ? `${errorMessage} (code ${errorCode2})` : errorMessage));
334308
+ return;
334309
+ }
334310
+ if (!("event" in message) || !message.event) {
334311
+ return;
334312
+ }
334313
+ const subscriptionId = message.subscriptionId ?? this.subscriptionId;
334314
+ if (subscriptionId === null || subscriptionId === undefined) {
334315
+ return;
334316
+ }
334317
+ this.push({ subscriptionId, event: message.event });
334318
+ }
334319
+ push(event) {
334320
+ if (this.closed) {
334321
+ return;
334322
+ }
334323
+ const waiter = this.waiters.shift();
334324
+ if (waiter) {
334325
+ waiter.resolve(event);
334326
+ return;
334327
+ }
334328
+ if (this.queue.length >= this.maxBufferedEvents) {
334329
+ this.fail(new Error(`Binance user-data stream buffered event limit exceeded (${this.maxBufferedEvents}); downstream consumer is not keeping up`));
334330
+ return;
334331
+ }
334332
+ this.queue.push(event);
334333
+ }
334334
+ nextEvent() {
334335
+ const event = this.queue.shift();
334336
+ if (event) {
334337
+ return Promise.resolve(event);
334338
+ }
334339
+ if (this.closeError) {
334340
+ return Promise.reject(this.closeError);
334341
+ }
334342
+ if (this.closed) {
334343
+ return Promise.resolve(null);
334344
+ }
334345
+ return new Promise((resolve, reject) => {
334346
+ this.waiters.push({ resolve, reject });
334347
+ });
334348
+ }
334349
+ fail(error48) {
334350
+ if (this.closeError) {
334351
+ return;
334352
+ }
334353
+ this.closeError = error48;
334354
+ this.closed = true;
334355
+ this.queue.length = 0;
334356
+ this.flushWaiters();
334357
+ try {
334358
+ this.ws.close();
334359
+ } catch {}
334360
+ }
334361
+ flushWaiters() {
334362
+ const error48 = this.closeError;
334363
+ for (const waiter of this.waiters.splice(0)) {
334364
+ if (error48) {
334365
+ waiter.reject(error48);
334366
+ } else {
334367
+ waiter.resolve(null);
333887
334368
  }
333888
- } catch (error48) {
333889
- rethrowArchiveDurabilityError(error48);
333890
- log.warn(`Failed to archive ${stream4} market data`, { error: error48 });
333891
334369
  }
333892
- });
333893
- }
333894
- function archiveTradesInBackground(archiver, otelMetrics, input) {
333895
- archiveMarketRowsInBackground(archiver, otelMetrics, "trades", input, () => extractTrades(input.payload, input.receivedTimestamp).map((trade) => buildCexTradeRow(input, trade)));
334370
+ }
333896
334371
  }
333897
- function archiveTickerInBackground(archiver, otelMetrics, input) {
333898
- archiveMarketRowsInBackground(archiver, otelMetrics, "ticker", input, () => {
333899
- const ticker = parseTicker(input.payload, input.receivedTimestamp);
333900
- return ticker ? [buildCexTickerEventRow(input, ticker)] : [];
333901
- });
334372
+ function isBinanceBalanceUserDataEvent(event) {
334373
+ return event.e === "outboundAccountPosition" || event.e === "balanceUpdate" || event.e === "externalLockUpdate";
333902
334374
  }
333903
- function archiveCexStreamEventInBackground(archiver, otelMetrics, input) {
333904
- archiveMarketRowsInBackground(archiver, otelMetrics, "stream", input, () => [
333905
- buildCexStreamEventRow(input)
333906
- ]);
334375
+ function isBinanceOrderUserDataEvent(event) {
334376
+ return event.e === "executionReport" || event.e === "listStatus";
333907
334377
  }
333908
334378
  // src/helpers/market-data-archive/ohlcv-bootstrap.ts
333909
334379
  var DEFAULT_OHLCV_BOOTSTRAP_LIMIT = 100;
@@ -333942,6 +334412,7 @@ async function bootstrapOhlcvHistory(broker, archiver, otelMetrics, tracker, inp
333942
334412
  const receivedTimestamp = Date.now();
333943
334413
  archiveOhlcvInBackground(archiver, otelMetrics, tracker, {
333944
334414
  ...input,
334415
+ sourceMode: "broker_bootstrap_fetch_v1",
333945
334416
  payload,
333946
334417
  receivedTimestamp
333947
334418
  });
@@ -335001,7 +335472,7 @@ class CEXBroker {
335001
335472
  if (this.otelMetrics?.isOtelEnabled()) {
335002
335473
  await this.otelMetrics.initialize();
335003
335474
  }
335004
- this.server = getServer(this.policy, this.brokers, this.whitelistIps, this.useVerity, this.#verityProverUrl, this.otelMetrics, this.brokerArchiver, this.orderActivityTracker, this.withdrawalObservationTracker);
335475
+ this.server = getServer(this.policy, this.brokers, this.whitelistIps, this.useVerity, this.#verityProverUrl, this.otelMetrics, this.brokerArchiver, this.orderActivityTracker, this.withdrawalObservationTracker, undefined);
335005
335476
  this.server.bindAsync(`0.0.0.0:${this.port}`, grpc15.ServerCredentials.createInsecure(), (err2, port) => {
335006
335477
  if (err2) {
335007
335478
  log.error(err2);