@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.
package/dist/index.js CHANGED
@@ -291041,7 +291041,7 @@ function compactUndefined2(record) {
291041
291041
  }
291042
291042
  function buildCommonArchiveTags(input) {
291043
291043
  return {
291044
- source: BROKER_WRITE_SOURCE,
291044
+ source: input.source ?? BROKER_WRITE_SOURCE,
291045
291045
  deployment_id: input.deploymentId,
291046
291046
  account_selector: input.accountSelector ?? "unknown",
291047
291047
  exchange: input.exchange.trim().toLowerCase() || "unknown",
@@ -291104,6 +291104,10 @@ function buildSubscribeStreamArchiveRow(input) {
291104
291104
  function quantityString(...values2) {
291105
291105
  return firstString2(...values2);
291106
291106
  }
291107
+ function extractBinanceInternalTransferId(response) {
291108
+ const record = asRecord(response);
291109
+ return firstString2(record?.txnId, record?.tranId);
291110
+ }
291107
291111
  function buildTransferEventArchiveRow(input) {
291108
291112
  const { tags, transfer } = input;
291109
291113
  return {
@@ -291136,6 +291140,7 @@ function normalizeCcxtTransactionForArchive(transaction) {
291136
291140
  const fee = asRecord(record?.fee);
291137
291141
  return compactUndefined2({
291138
291142
  externalId: firstString2(record?.id, info?.id, record?.txid, info?.txId),
291143
+ clientWithdrawalId: firstString2(info?.withdrawOrderId),
291139
291144
  txid: firstString2(record?.txid, info?.txId, info?.txid, info?.tx_hash),
291140
291145
  address: firstString2(record?.address, record?.addressTo, info?.address),
291141
291146
  network: firstString2(record?.network, info?.network),
@@ -291243,14 +291248,40 @@ var DEFAULT_BATCH_SIZE = 10;
291243
291248
  var DEFAULT_FLUSH_INTERVAL_MS = 1000;
291244
291249
  var DEFAULT_FORWARDER_TIMEOUT_MS = 3000;
291245
291250
  var SHED_WARN_INTERVAL_MS = 60000;
291251
+ var MARKET_FEEDS = new Set(["ORDERBOOK", "TICKER", "TRADES", "OHLCV"]);
291252
+ function archiveFeed(row) {
291253
+ const declared = row.row.feed ?? row.row.stream_type;
291254
+ if (typeof declared === "string" && MARKET_FEEDS.has(declared)) {
291255
+ return declared;
291256
+ }
291257
+ if (row.table === "market_data.orderbook_snapshots" || row.table.startsWith("market_data.cex_order_book_")) {
291258
+ return "ORDERBOOK";
291259
+ }
291260
+ if (row.table === "market_data.candles" || row.table === "market_data.cex_ohlcv") {
291261
+ return "OHLCV";
291262
+ }
291263
+ if (row.table === "market_data.cex_ticker_events")
291264
+ return "TICKER";
291265
+ if (row.table === "market_data.cex_trades")
291266
+ return "TRADES";
291267
+ return "NON_MARKET";
291268
+ }
291246
291269
  function isArchiveOtelLogsEnabled() {
291247
291270
  return process.env.CEX_BROKER_ARCHIVE_OTEL_LOGS_ENABLED === "true";
291248
291271
  }
291249
291272
  function resolveArchiveForwarderUrlFromEnv() {
291250
291273
  return process.env.CEX_BROKER_ARCHIVE_FORWARDER_URL?.trim() || undefined;
291251
291274
  }
291275
+ function resolveArchiveSourceFromEnv(value = process.env.CEX_BROKER_ARCHIVE_SOURCE) {
291276
+ const source = value?.trim() || BROKER_WRITE_SOURCE;
291277
+ if (source !== "broker_read" && source !== "broker_write") {
291278
+ throw new Error("CEX_BROKER_ARCHIVE_SOURCE must be broker_read or broker_write");
291279
+ }
291280
+ return source;
291281
+ }
291252
291282
 
291253
291283
  class BrokerExecutionArchiver {
291284
+ source;
291254
291285
  deploymentId;
291255
291286
  otelLogs;
291256
291287
  otelMetrics;
@@ -291271,10 +291302,12 @@ class BrokerExecutionArchiver {
291271
291302
  flushTimer = null;
291272
291303
  flushInFlight = null;
291273
291304
  lastShedWarnAtMs = 0;
291305
+ closing = false;
291274
291306
  closed = false;
291275
291307
  enabled;
291276
291308
  forwarderAuthToken;
291277
291309
  constructor(options) {
291310
+ this.source = options.source ?? BROKER_WRITE_SOURCE;
291278
291311
  this.deploymentId = options.deploymentId?.trim() || process.env.CEX_BROKER_DEPLOYMENT_ID?.trim() || "unknown";
291279
291312
  this.otelLogs = options.otelLogs;
291280
291313
  this.otelMetrics = options.otelMetrics;
@@ -291306,6 +291339,7 @@ class BrokerExecutionArchiver {
291306
291339
  this.flushTimer.unref?.();
291307
291340
  log.info("Broker execution archive enabled", {
291308
291341
  enabled: true,
291342
+ source: this.source,
291309
291343
  otel_mirror_enabled: Boolean(this.otelLogs?.isOtelEnabled())
291310
291344
  });
291311
291345
  } catch (error) {
@@ -291322,8 +291356,11 @@ class BrokerExecutionArchiver {
291322
291356
  getDeploymentId() {
291323
291357
  return this.deploymentId;
291324
291358
  }
291359
+ getSource() {
291360
+ return this.source;
291361
+ }
291325
291362
  isEnabled() {
291326
- return this.enabled && !this.closed;
291363
+ return this.enabled && !this.closing && !this.closed;
291327
291364
  }
291328
291365
  canPersistMarketMetadataSnapshot() {
291329
291366
  return this.isEnabled();
@@ -291335,6 +291372,10 @@ class BrokerExecutionArchiver {
291335
291372
  if (!this.enabled || this.closed) {
291336
291373
  return;
291337
291374
  }
291375
+ const archiveRow = {
291376
+ table: row.table,
291377
+ row: { ...row.row, source: this.source }
291378
+ };
291338
291379
  if (this.queue.length >= this.maxQueueSize) {
291339
291380
  const shedRow = this.queue[0];
291340
291381
  if (shedRow) {
@@ -291343,7 +291384,14 @@ class BrokerExecutionArchiver {
291343
291384
  }
291344
291385
  this.stats.shed += 1;
291345
291386
  this.recordArchiveMetric("cex_archive_rows_shed_total", {
291346
- table: shedRow?.table ?? "unknown"
291387
+ table: shedRow?.table ?? "unknown",
291388
+ source: this.source,
291389
+ feed: shedRow ? archiveFeed(shedRow) : "NON_MARKET"
291390
+ });
291391
+ this.recordArchiveMetric("cex_archive_queue_saturated_rows_total", {
291392
+ table: shedRow?.table ?? "unknown",
291393
+ source: this.source,
291394
+ feed: shedRow ? archiveFeed(shedRow) : "NON_MARKET"
291347
291395
  });
291348
291396
  const now3 = Date.now();
291349
291397
  if (now3 - this.lastShedWarnAtMs >= SHED_WARN_INTERVAL_MS) {
@@ -291355,10 +291403,12 @@ class BrokerExecutionArchiver {
291355
291403
  this.lastShedWarnAtMs = now3;
291356
291404
  }
291357
291405
  }
291358
- this.queue.push(row);
291406
+ this.queue.push(archiveRow);
291359
291407
  this.stats.enqueued += 1;
291360
291408
  this.recordArchiveMetric("cex_archive_rows_enqueued_total", {
291361
- table: row.table
291409
+ table: archiveRow.table,
291410
+ source: this.source,
291411
+ feed: archiveFeed(archiveRow)
291362
291412
  });
291363
291413
  if (this.queue.length >= this.batchSize) {
291364
291414
  this.flush();
@@ -291378,11 +291428,15 @@ class BrokerExecutionArchiver {
291378
291428
  return;
291379
291429
  }).finally(() => {
291380
291430
  this.flushInFlight = null;
291431
+ if (!this.closed && !this.closing && this.enabled && this.queue.length >= this.batchSize) {
291432
+ queueMicrotask(() => void this.flush());
291433
+ }
291381
291434
  });
291382
291435
  this.flushInFlight = inFlight;
291383
291436
  return inFlight;
291384
291437
  }
291385
291438
  async close() {
291439
+ this.closing = true;
291386
291440
  if (this.flushTimer) {
291387
291441
  clearInterval(this.flushTimer);
291388
291442
  this.flushTimer = null;
@@ -291447,7 +291501,14 @@ class BrokerExecutionArchiver {
291447
291501
  this.queue.shift();
291448
291502
  this.stats.shed += 1;
291449
291503
  this.recordArchiveMetric("cex_archive_rows_shed_total", {
291450
- table: dropped?.table ?? "unknown"
291504
+ table: dropped?.table ?? "unknown",
291505
+ source: this.source,
291506
+ feed: archiveFeed(dropped)
291507
+ });
291508
+ this.recordArchiveMetric("cex_archive_queue_saturated_rows_total", {
291509
+ table: dropped.table,
291510
+ source: this.source,
291511
+ feed: archiveFeed(dropped)
291451
291512
  });
291452
291513
  }
291453
291514
  }
@@ -291461,6 +291522,7 @@ class BrokerExecutionArchiver {
291461
291522
  const timestamp = new Date().toISOString();
291462
291523
  const records = rows.map((payload) => ({
291463
291524
  timestamp,
291525
+ source: this.source,
291464
291526
  deployment_id: this.deploymentId,
291465
291527
  reason,
291466
291528
  payload
@@ -291474,6 +291536,21 @@ class BrokerExecutionArchiver {
291474
291536
  throw new Error(`wrote ${written} of ${bytes2.length} bytes`);
291475
291537
  }
291476
291538
  fsyncSync(this.deadLetterFd);
291539
+ const byFeedAndTable = new Map;
291540
+ for (const row of rows) {
291541
+ const key = `${row.table}\x00${archiveFeed(row)}`;
291542
+ const grouped = byFeedAndTable.get(key) ?? { row, count: 0 };
291543
+ grouped.count += 1;
291544
+ byFeedAndTable.set(key, grouped);
291545
+ }
291546
+ for (const { row, count: count2 } of byFeedAndTable.values()) {
291547
+ this.recordArchiveMetric("cex_archive_rows_journaled_total", {
291548
+ table: row.table,
291549
+ source: this.source,
291550
+ feed: archiveFeed(row),
291551
+ reason
291552
+ }, count2);
291553
+ }
291477
291554
  } catch (error) {
291478
291555
  throw new BrokerExecutionArchiveDurabilityError(`Broker execution archive failed to durably record ${reason}; affected row(s) were retained`, { cause: error });
291479
291556
  }
@@ -291509,10 +291586,17 @@ class BrokerExecutionArchiver {
291509
291586
  recordFlushHealth(batch) {
291510
291587
  const countByTable = new Map;
291511
291588
  for (const entry of batch) {
291512
- countByTable.set(entry.table, (countByTable.get(entry.table) ?? 0) + 1);
291513
- }
291514
- for (const [table, count2] of countByTable) {
291515
- this.recordArchiveMetric("cex_archive_rows_flushed_total", { table }, count2);
291589
+ const key = `${entry.table}\x00${archiveFeed(entry)}`;
291590
+ const grouped = countByTable.get(key) ?? { row: entry, count: 0 };
291591
+ grouped.count += 1;
291592
+ countByTable.set(key, grouped);
291593
+ }
291594
+ for (const { row, count: count2 } of countByTable.values()) {
291595
+ this.recordArchiveMetric("cex_archive_rows_flushed_total", {
291596
+ table: row.table,
291597
+ source: this.source,
291598
+ feed: archiveFeed(row)
291599
+ }, count2);
291516
291600
  }
291517
291601
  this.recordArchiveGauge("cex_archive_last_flush_success", Math.floor(Date.now() / 1000));
291518
291602
  }
@@ -291546,7 +291630,7 @@ class BrokerExecutionArchiver {
291546
291630
  return Promise.resolve();
291547
291631
  }
291548
291632
  const body = JSON.stringify({
291549
- source: "broker_write",
291633
+ source: this.source,
291550
291634
  deployment_id: this.deploymentId,
291551
291635
  rows: batch
291552
291636
  });
@@ -291616,6 +291700,7 @@ function createBrokerExecutionArchiverFromEnv(otelLogs, otelMetrics) {
291616
291700
  const forwarderUrl = resolveArchiveForwarderUrlFromEnv();
291617
291701
  const archiveOtelLogs = isArchiveOtelLogsEnabled() ? otelLogs : undefined;
291618
291702
  return BrokerExecutionArchiver.create({
291703
+ source: resolveArchiveSourceFromEnv(),
291619
291704
  otelLogs: archiveOtelLogs,
291620
291705
  otelMetrics,
291621
291706
  forwarderUrl: forwarderUrl ?? "",
@@ -291754,6 +291839,7 @@ function archiveWithdrawalObservationsInBackground(archiver, tracker, input) {
291754
291839
  address: normalized.address,
291755
291840
  network: normalized.network,
291756
291841
  externalId: normalized.externalId,
291842
+ clientWithdrawalId: normalized.clientWithdrawalId,
291757
291843
  txid: normalized.txid,
291758
291844
  resultIndex,
291759
291845
  feeAmount: normalized.feeAmount,
@@ -306850,9 +306936,6 @@ function selectBrokerAccountForCex(normalizedCex, brokers, metadata) {
306850
306936
  return selectBrokerAccount(brokers[normalizedCex], metadata) ?? undefined;
306851
306937
  }
306852
306938
 
306853
- // src/handlers/execute-action/order-book-call.ts
306854
- import * as grpc4 from "@grpc/grpc-js";
306855
-
306856
306939
  // src/helpers/order-book.ts
306857
306940
  var ORDER_BOOK_CALL_METHODS = {
306858
306941
  FETCH_CAPABILITY: "fetch_order_book_capability",
@@ -307095,726 +307178,1200 @@ function buildHistoricalOrderBookUnsupported(payload) {
307095
307178
  }
307096
307179
 
307097
307180
  // src/handlers/execute-action/order-book-call.ts
307098
- async function handleOrderBookCall(ctx) {
307099
- const parsedOrderBookCall = parseOrderBookCallPayload(ctx.call.request.payload, {
307100
- exchange: ctx.normalizedCex,
307101
- symbol: ctx.symbol
307102
- });
307103
- if (parsedOrderBookCall.kind === "error") {
307104
- ctx.wrappedCallback({
307105
- code: grpc4.status.INVALID_ARGUMENT,
307106
- message: parsedOrderBookCall.message
307107
- }, null);
307108
- return true;
307181
+ import * as grpc4 from "@grpc/grpc-js";
307182
+
307183
+ // src/helpers/market-data-archive/capture-contract.ts
307184
+ import { createHash as createHash3 } from "node:crypto";
307185
+ var MARKET_CAPTURE_SCHEMA_VERSION = "1.0.0";
307186
+ var CHECKSUM_ALGORITHM = "sha256-canonical-json-v1";
307187
+ var ARCHIVE_SOURCES = ["broker_read", "broker_write"];
307188
+ var CAPTURE_FEEDS = [
307189
+ "ORDERBOOK",
307190
+ "TICKER",
307191
+ "TRADES",
307192
+ "OHLCV"
307193
+ ];
307194
+ var SOURCE_MODES = [
307195
+ "broker_live_stream_v1",
307196
+ "broker_live_sampling_v1",
307197
+ "broker_current_snapshot_v1",
307198
+ "broker_bootstrap_fetch_v1",
307199
+ "external_ccxt_fallback_v1",
307200
+ "external_hummingbot_fallback_v1",
307201
+ "legacy_migration_v1"
307202
+ ];
307203
+ var RAW_CAPTURE_SCOPES = [
307204
+ "ccxt_normalized_object",
307205
+ "broker_visible_payload",
307206
+ "exchange_wire_frame"
307207
+ ];
307208
+ var CHECKSUM_FIELDS = new Set([
307209
+ "normalized_row_checksum",
307210
+ "raw_checksum",
307211
+ "checksum"
307212
+ ]);
307213
+ function canonicalDecimal(value) {
307214
+ if (!Number.isFinite(value)) {
307215
+ throw new Error("Canonical numbers must be finite");
307109
307216
  }
307110
- if (parsedOrderBookCall.kind !== "order_book") {
307111
- return false;
307217
+ if (Object.is(value, -0)) {
307218
+ return "0";
307112
307219
  }
307113
- const orderBookBroker = ctx.selectedBrokerAccount?.exchange ?? createBroker(ctx.normalizedCex, ctx.metadata) ?? createPublicBroker(ctx.normalizedCex);
307114
- if (!orderBookBroker) {
307115
- ctx.wrappedCallback({
307116
- code: grpc4.status.INVALID_ARGUMENT,
307117
- message: `Unsupported exchange for order-book market data: ${ctx.normalizedCex}`
307118
- }, null);
307119
- return true;
307220
+ const rendered = String(value).toLowerCase();
307221
+ if (!rendered.includes("e")) {
307222
+ return rendered;
307120
307223
  }
307121
- ctx.applyVerityToBroker(orderBookBroker);
307122
- try {
307123
- const orderBookPayload = parsedOrderBookCall.payload;
307124
- if (orderBookPayload.method === ORDER_BOOK_CALL_METHODS.FETCH_CAPABILITY) {
307125
- ctx.wrappedCallback(null, {
307126
- proof: ctx.verity.proof,
307127
- result: JSON.stringify(buildOrderBookCapability(orderBookBroker, orderBookPayload))
307128
- });
307129
- return true;
307130
- }
307131
- if (orderBookPayload.method === ORDER_BOOK_CALL_METHODS.FETCH_HISTORICAL_SNAPSHOTS) {
307132
- ctx.wrappedCallback(null, {
307133
- proof: ctx.verity.proof,
307134
- result: JSON.stringify(buildHistoricalOrderBookUnsupported(orderBookPayload))
307135
- });
307136
- return true;
307137
- }
307138
- const fetchOrderBook = orderBookBroker.fetchOrderBook;
307139
- const canFetchOrderBook = typeof fetchOrderBook === "function" && orderBookBroker.has?.fetchOrderBook !== false;
307140
- if (!canFetchOrderBook) {
307141
- ctx.wrappedCallback({
307142
- code: grpc4.status.UNIMPLEMENTED,
307143
- message: `Order-book snapshot unsupported for ${ctx.normalizedCex}`
307144
- }, null);
307145
- return true;
307224
+ const [coefficient = "0", exponentText = "0"] = rendered.split("e");
307225
+ const exponent = Number.parseInt(exponentText, 10);
307226
+ const negative = coefficient.startsWith("-");
307227
+ const unsigned = negative ? coefficient.slice(1) : coefficient;
307228
+ const [integer2 = "0", fraction = ""] = unsigned.split(".");
307229
+ const digits = `${integer2}${fraction}`;
307230
+ const decimalIndex = integer2.length + exponent;
307231
+ let result;
307232
+ if (decimalIndex <= 0) {
307233
+ result = `0.${"0".repeat(-decimalIndex)}${digits}`;
307234
+ } else if (decimalIndex >= digits.length) {
307235
+ result = `${digits}${"0".repeat(decimalIndex - digits.length)}`;
307236
+ } else {
307237
+ result = `${digits.slice(0, decimalIndex)}.${digits.slice(decimalIndex)}`;
307238
+ }
307239
+ return negative ? `-${result}` : result;
307240
+ }
307241
+ function serializeCanonical(value, stack) {
307242
+ if (value === null)
307243
+ return "null";
307244
+ if (typeof value === "string")
307245
+ return JSON.stringify(value);
307246
+ if (typeof value === "boolean")
307247
+ return value ? "true" : "false";
307248
+ if (typeof value === "number")
307249
+ return canonicalDecimal(value);
307250
+ if (typeof value === "bigint")
307251
+ return value.toString(10);
307252
+ if (value instanceof Date) {
307253
+ if (Number.isNaN(value.getTime())) {
307254
+ throw new Error("Canonical timestamps must be valid");
307146
307255
  }
307147
- const receivedTimestamp = Date.now();
307148
- const rawOrderBook = await fetchOrderBook.call(orderBookBroker, orderBookPayload.symbol, orderBookPayload.depthLimit);
307149
- ctx.wrappedCallback(null, {
307150
- proof: ctx.verity.proof,
307151
- result: JSON.stringify(normalizeOrderBookSnapshot(rawOrderBook, {
307152
- exchange: orderBookPayload.exchange,
307153
- symbol: orderBookPayload.symbol,
307154
- depthLimit: orderBookPayload.depthLimit,
307155
- receivedTimestamp
307156
- }))
307157
- });
307158
- } catch (error48) {
307159
- safeLogError("Order-book Call failed", error48);
307160
- ctx.wrappedCallback({
307161
- code: mapCcxtErrorToGrpcStatus(error48) ?? grpc4.status.INTERNAL,
307162
- message: `Order-book Call failed: ${sanitizeErrorDetail(error48)}`
307163
- }, null);
307256
+ return value.getTime().toString(10);
307164
307257
  }
307165
- return true;
307258
+ if (Array.isArray(value)) {
307259
+ if (stack.has(value))
307260
+ throw new Error("Canonical values must be acyclic");
307261
+ stack.add(value);
307262
+ const result = `[${value.map((entry) => entry === undefined ? "null" : serializeCanonical(entry, stack)).join(",")}]`;
307263
+ stack.delete(value);
307264
+ return result;
307265
+ }
307266
+ if (typeof value === "object") {
307267
+ if (stack.has(value))
307268
+ throw new Error("Canonical values must be acyclic");
307269
+ stack.add(value);
307270
+ const entries = Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => left.localeCompare(right));
307271
+ const result = `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${serializeCanonical(entry, stack)}`).join(",")}}`;
307272
+ stack.delete(value);
307273
+ return result;
307274
+ }
307275
+ throw new Error(`Unsupported canonical value type: ${typeof value}`);
307166
307276
  }
307167
-
307168
- // src/handlers/execute-action/registry.ts
307169
- import * as grpc11 from "@grpc/grpc-js";
307170
-
307171
- // src/handlers/execute-action/internal-transfer.ts
307172
- import * as grpc5 from "@grpc/grpc-js";
307173
- async function handleInternalTransfer(ctx) {
307174
- const {
307175
- brokers,
307176
- metadata,
307177
- normalizedCex,
307178
- symbol: symbol2,
307179
- verity,
307180
- useVerity,
307181
- verityProverUrl,
307182
- brokerArchiver
307183
- } = ctx;
307184
- if (!symbol2) {
307185
- return ctx.wrappedCallback({
307186
- code: grpc5.status.INVALID_ARGUMENT,
307187
- message: `ValidationError: Symbol required`
307188
- }, null);
307277
+ function canonicalSerialize(value) {
307278
+ return serializeCanonical(value, new Set);
307279
+ }
307280
+ function omitChecksumFields(value) {
307281
+ if (Array.isArray(value))
307282
+ return value.map(omitChecksumFields);
307283
+ if (value && typeof value === "object" && !(value instanceof Date)) {
307284
+ return Object.fromEntries(Object.entries(value).filter(([key]) => !CHECKSUM_FIELDS.has(key)).map(([key, entry]) => [key, omitChecksumFields(entry)]));
307189
307285
  }
307190
- const transferPayload = parsePayloadForAction(ctx, InternalTransferPayloadSchema);
307191
- if (transferPayload === null)
307192
- return;
307193
- if (normalizedCex !== "binance") {
307194
- return ctx.wrappedCallback({
307195
- code: grpc5.status.UNIMPLEMENTED,
307196
- message: `InternalTransfer is only supported for Binance`
307197
- }, null);
307286
+ return value;
307287
+ }
307288
+ function sha256Canonical(value) {
307289
+ return createHash3("sha256").update(canonicalSerialize(omitChecksumFields(value))).digest("hex");
307290
+ }
307291
+ function normalizeTimestampMs(value, field) {
307292
+ let timestamp;
307293
+ if (value instanceof Date) {
307294
+ timestamp = value.getTime();
307295
+ } else if (typeof value === "number") {
307296
+ timestamp = value;
307297
+ } else if (typeof value === "string" && /^\d+$/.test(value.trim())) {
307298
+ timestamp = Number(value.trim());
307299
+ } else if (typeof value === "string") {
307300
+ timestamp = Date.parse(value);
307301
+ } else {
307302
+ timestamp = Number.NaN;
307198
307303
  }
307199
- const pool = brokers[normalizedCex];
307200
- if (!pool) {
307201
- return ctx.wrappedCallback({
307202
- code: grpc5.status.FAILED_PRECONDITION,
307203
- message: `No broker accounts configured for ${normalizedCex}`
307204
- }, null);
307304
+ if (!Number.isSafeInteger(timestamp) || timestamp < 0) {
307305
+ throw new Error(`${field} must be a non-negative millisecond timestamp`);
307205
307306
  }
307206
- const fromSelector = transferPayload.fromAccount ?? getCurrentBrokerSelector(metadata);
307207
- const toSelector = transferPayload.toAccount ?? "primary";
307208
- const sourceAccount = resolveBrokerAccount(pool, fromSelector);
307209
- if (!sourceAccount) {
307210
- return ctx.wrappedCallback({
307211
- code: grpc5.status.INVALID_ARGUMENT,
307212
- message: `Source account "${fromSelector}" is not configured`
307213
- }, null);
307307
+ return timestamp;
307308
+ }
307309
+ function assertCaptureContext(context2) {
307310
+ if (!ARCHIVE_SOURCES.includes(context2.source)) {
307311
+ throw new Error(`Unsupported archive source: ${context2.source}`);
307214
307312
  }
307215
- const destAccount = resolveBrokerAccount(pool, toSelector);
307216
- if (!destAccount) {
307217
- return ctx.wrappedCallback({
307218
- code: grpc5.status.INVALID_ARGUMENT,
307219
- message: `Destination account "${toSelector}" is not configured`
307220
- }, null);
307313
+ if (!CAPTURE_FEEDS.includes(context2.feed)) {
307314
+ throw new Error(`Unsupported capture feed: ${context2.feed}`);
307221
307315
  }
307222
- try {
307223
- if (useVerity) {
307224
- sourceAccount.exchange.setHttpClientOverride(buildHttpClientOverrideFromMetadata(metadata, verityProverUrl, (proof, notaryPubKey) => {
307225
- verity.proof = proof;
307226
- log.debug(`Verity proof:`, { proof, notaryPubKey });
307227
- }), verityHttpClientOverridePredicate);
307316
+ if (!SOURCE_MODES.includes(context2.sourceMode)) {
307317
+ throw new Error(`Unsupported source mode: ${context2.sourceMode}`);
307318
+ }
307319
+ for (const [field, value] of [
307320
+ ["deployment_id", context2.deploymentId],
307321
+ ["capture_bundle_id", context2.captureBundleId],
307322
+ ["exchange", context2.exchange],
307323
+ ["symbol", context2.symbol],
307324
+ ["provider", context2.provider]
307325
+ ]) {
307326
+ if (!value.trim())
307327
+ throw new Error(`${field} must not be empty`);
307328
+ }
307329
+ }
307330
+ function createRawCapture(context2, input) {
307331
+ assertCaptureContext(context2);
307332
+ if (!RAW_CAPTURE_SCOPES.includes(input.scope)) {
307333
+ throw new Error(`Unsupported raw capture scope: ${input.scope}`);
307334
+ }
307335
+ const eventTimeMs = normalizeTimestampMs(input.eventTimeMs, "event_time_ms");
307336
+ const receivedTimeMs = normalizeTimestampMs(input.receivedTimeMs, "received_time_ms");
307337
+ const redactedPayload = redactStreamPayload(input.payload);
307338
+ const rawChecksum = sha256Canonical(redactedPayload);
307339
+ const rawCaptureId = sha256Canonical({
307340
+ capture_bundle_id: context2.captureBundleId,
307341
+ exchange: context2.exchange.trim().toLowerCase(),
307342
+ feed: context2.feed,
307343
+ raw_capture_scope: input.scope,
307344
+ raw_payload_sha256: rawChecksum,
307345
+ schema_version: context2.schemaVersion,
307346
+ source_mode: context2.sourceMode,
307347
+ source_symbol: context2.symbol.trim(),
307348
+ source_time_ms: eventTimeMs
307349
+ });
307350
+ return {
307351
+ rawCaptureId,
307352
+ rawCaptureScope: input.scope,
307353
+ rawChecksum,
307354
+ redactedPayload,
307355
+ eventTimeMs,
307356
+ receivedTimeMs,
307357
+ checksumAlgorithm: context2.checksumAlgorithm
307358
+ };
307359
+ }
307360
+ function captureCoreFields(context2, rawCapture) {
307361
+ assertCaptureContext(context2);
307362
+ return {
307363
+ source: context2.source,
307364
+ deployment_id: context2.deploymentId,
307365
+ capture_bundle_id: context2.captureBundleId,
307366
+ exchange: context2.exchange.trim().toLowerCase(),
307367
+ symbol: context2.symbol.trim(),
307368
+ trading_pair: context2.symbol.trim().replace("/", "-"),
307369
+ source_symbol: context2.symbol.trim(),
307370
+ asset_type: context2.assetType,
307371
+ feed: context2.feed,
307372
+ provider: context2.provider,
307373
+ source_mode: context2.sourceMode,
307374
+ source_time_ms: rawCapture.eventTimeMs,
307375
+ received_time_ms: rawCapture.receivedTimeMs,
307376
+ raw_capture_id: rawCapture.rawCaptureId,
307377
+ raw_capture_scope: rawCapture.rawCaptureScope,
307378
+ schema_version: context2.schemaVersion,
307379
+ checksum_algorithm: context2.checksumAlgorithm,
307380
+ raw_checksum: rawCapture.rawChecksum,
307381
+ provenance_complete: context2.provenanceComplete ? 1 : 0
307382
+ };
307383
+ }
307384
+
307385
+ // src/helpers/market-data-archive/canonical-orderbook.ts
307386
+ class OrderBookValidationError extends Error {
307387
+ reason;
307388
+ constructor(reason) {
307389
+ super(`Invalid order-book evidence: ${reason}`);
307390
+ this.name = "OrderBookValidationError";
307391
+ this.reason = reason;
307392
+ }
307393
+ }
307394
+ function parseSequence(value) {
307395
+ if (value === undefined)
307396
+ return;
307397
+ const parsed = typeof value === "number" ? value : typeof value === "string" && /^\d+$/.test(value) ? Number(value) : Number.NaN;
307398
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
307399
+ throw new OrderBookValidationError("sequence must be a non-negative integer");
307400
+ }
307401
+ return parsed;
307402
+ }
307403
+ function validateSide(side, levels, depthLimit) {
307404
+ if (levels.length === 0) {
307405
+ throw new OrderBookValidationError(`${side} side is missing`);
307406
+ }
307407
+ const retained = levels.slice(0, depthLimit);
307408
+ for (let index2 = 0;index2 < retained.length; index2 += 1) {
307409
+ const entry = retained[index2];
307410
+ const price = entry?.[0];
307411
+ const amount = entry?.[1];
307412
+ if (!Array.isArray(entry) || entry.length < 2 || price === undefined || amount === undefined || !Number.isFinite(price) || !Number.isFinite(amount) || price <= 0 || amount <= 0) {
307413
+ throw new OrderBookValidationError(`${side} level ${index2} has a non-positive or non-finite price/amount`);
307228
307414
  }
307229
- const result = await transferBinanceInternal(sourceAccount, destAccount, symbol2, transferPayload.amount);
307230
- const normalized = normalizeCcxtTransactionForArchive(result);
307231
- archiveTransferEventInBackground(brokerArchiver, {
307232
- exchange: normalizedCex,
307233
- accountSelector: fromSelector,
307234
- assetSymbol: symbol2,
307235
- transfer: {
307236
- eventKind: "internal_transfer",
307237
- lifecycleAction: "submit_internal_transfer",
307238
- status: normalized.status ?? "ok",
307239
- amount: normalized.amount ?? String(transferPayload.amount),
307240
- network: "internal",
307241
- externalId: normalized.externalId,
307242
- payload: { from: fromSelector, to: toSelector, result }
307415
+ if (index2 > 0) {
307416
+ const previous = retained[index2 - 1]?.[0];
307417
+ if (previous === undefined || (side === "bid" ? price >= previous : price <= previous)) {
307418
+ throw new OrderBookValidationError(`${side} levels are not strictly ${side === "bid" ? "descending" : "ascending"}`);
307243
307419
  }
307244
- });
307245
- ctx.wrappedCallback(null, {
307246
- proof: verity.proof,
307247
- result: JSON.stringify(result)
307248
- });
307249
- } catch (error48) {
307250
- safeLogError("InternalTransfer failed", error48);
307251
- if (error48 instanceof BrokerAccountPreconditionError) {
307252
- return ctx.wrappedCallback({
307253
- code: grpc5.status.FAILED_PRECONDITION,
307254
- message: getErrorMessage(error48)
307255
- }, null);
307256
307420
  }
307257
- const msg = getErrorMessage(error48);
307258
- let code;
307259
- if (msg.includes("Unsupported transfer direction")) {
307260
- code = grpc5.status.INVALID_ARGUMENT;
307261
- } else if (msg.includes("unavailable in this CCXT build")) {
307262
- code = grpc5.status.UNIMPLEMENTED;
307263
- } else {
307264
- code = mapCcxtErrorToGrpcStatus(error48) ?? grpc5.status.INTERNAL;
307421
+ }
307422
+ return retained.map(([price, amount]) => ({
307423
+ price,
307424
+ amount
307425
+ }));
307426
+ }
307427
+ function normalizedBands(input) {
307428
+ const bands = input ?? [10, 25, 50, 100];
307429
+ for (const band of bands) {
307430
+ if (!Number.isFinite(band) || band <= 0 || !Number.isInteger(band)) {
307431
+ throw new OrderBookValidationError("measurement bands must be positive integer basis points");
307265
307432
  }
307266
- ctx.wrappedCallback({
307267
- code,
307268
- message: `InternalTransfer failed: ${sanitizeErrorDetail(error48)}`
307269
- }, null);
307270
307433
  }
307434
+ return [...new Set(bands)].sort((left, right) => left - right);
307271
307435
  }
307272
-
307273
- // src/handlers/execute-action/orders.ts
307274
- import * as grpc6 from "@grpc/grpc-js";
307275
-
307276
- // src/helpers/passive-order.ts
307277
- var PASSIVE_ORDER_ERROR_CODES = {
307278
- unsupported: "passive_order_unsupported",
307279
- rejected: "passive_order_rejected",
307280
- wouldCross: "passive_order_would_cross"
307281
- };
307282
- function identifiesWouldCross(message) {
307283
- const normalized = message.toLowerCase();
307284
- 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);
307436
+ function checksumRow(row) {
307437
+ return { ...row, normalized_row_checksum: sha256Canonical(row) };
307285
307438
  }
307286
- function identifiesUnsupported(message) {
307287
- const normalized = message.toLowerCase();
307288
- 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);
307439
+ function commonEvidenceFields(context2, rawCapture, input) {
307440
+ return {
307441
+ ...captureCoreFields(context2, rawCapture),
307442
+ source_time_ms: input.eventTimeMs,
307443
+ received_time_ms: input.receivedTimeMs,
307444
+ snapshot_id: input.snapshotId,
307445
+ construction_mode: "sampled_top_n_snapshot",
307446
+ gap_policy: "record_gap",
307447
+ depth_limit: input.depthLimit,
307448
+ sequence: input.sequence,
307449
+ exact_l2_reconstruction_complete: 0
307450
+ };
307289
307451
  }
307290
- function classifyPassiveOrderError(error48) {
307291
- if (error48 instanceof ccxt_default.InsufficientFunds) {
307292
- return "InsufficientFunds";
307452
+ function buildCanonicalOrderBookRows(input) {
307453
+ if (!Number.isSafeInteger(input.depthLimit) || input.depthLimit <= 0 || input.depthLimit > 500) {
307454
+ throw new OrderBookValidationError("depth limit must be an integer between 1 and 500");
307293
307455
  }
307294
- if (error48 instanceof ccxt_default.AuthenticationError) {
307295
- return "AuthenticationError";
307456
+ if (input.context.feed !== "ORDERBOOK") {
307457
+ throw new OrderBookValidationError("capture context feed is not ORDERBOOK");
307296
307458
  }
307297
- const message = getErrorMessage(error48);
307298
- if (error48 instanceof ccxt_default.OrderImmediatelyFillable || identifiesWouldCross(message)) {
307299
- return PASSIVE_ORDER_ERROR_CODES.wouldCross;
307459
+ if (input.constructionMode === "exact_l2_reconstruction") {
307460
+ throw new OrderBookValidationError("exact L2 requires a complete continuity proof and is unsupported");
307300
307461
  }
307301
- if (error48 instanceof ccxt_default.NotSupported || identifiesUnsupported(message)) {
307302
- return PASSIVE_ORDER_ERROR_CODES.unsupported;
307462
+ let eventTimeMs;
307463
+ let receivedTimeMs;
307464
+ try {
307465
+ eventTimeMs = normalizeTimestampMs(input.snapshot.timestamp, "source_time_ms");
307466
+ receivedTimeMs = normalizeTimestampMs(input.snapshot.receivedTimestamp, "received_time_ms");
307467
+ } catch (error48) {
307468
+ throw new OrderBookValidationError(error48 instanceof Error ? error48.message : "invalid timestamp");
307303
307469
  }
307304
- return PASSIVE_ORDER_ERROR_CODES.rejected;
307470
+ if (receivedTimeMs < eventTimeMs) {
307471
+ throw new OrderBookValidationError("received timestamp precedes source timestamp");
307472
+ }
307473
+ const bids = validateSide("bid", input.snapshot.bids, input.depthLimit);
307474
+ const asks = validateSide("ask", input.snapshot.asks, input.depthLimit);
307475
+ const bestBid = bids[0];
307476
+ const bestAsk = asks[0];
307477
+ if (bestBid.price >= bestAsk.price) {
307478
+ throw new OrderBookValidationError("book is crossed or locked");
307479
+ }
307480
+ const sequence = parseSequence(input.snapshot.sequence);
307481
+ const midPrice = (bestBid.price + bestAsk.price) / 2;
307482
+ const spread2 = bestAsk.price - bestBid.price;
307483
+ const spreadBps = spread2 / midPrice * 1e4;
307484
+ const snapshotId = sha256Canonical({
307485
+ exchange: input.context.exchange.trim().toLowerCase(),
307486
+ trading_pair: input.context.symbol.trim().replace("/", "-"),
307487
+ source_time_ms: eventTimeMs,
307488
+ sequence,
307489
+ depth_limit: input.depthLimit,
307490
+ bids,
307491
+ asks,
307492
+ schema_version: input.context.schemaVersion
307493
+ });
307494
+ const common = commonEvidenceFields(input.context, input.rawCapture, {
307495
+ snapshotId,
307496
+ sequence,
307497
+ depthLimit: input.depthLimit,
307498
+ eventTimeMs,
307499
+ receivedTimeMs
307500
+ });
307501
+ const levels = [
307502
+ ["bid", bids],
307503
+ ["ask", asks]
307504
+ ].flatMap(([side, sideLevels]) => sideLevels.map(({ price, amount }, levelIndex) => {
307505
+ const row = checksumRow({
307506
+ ...common,
307507
+ side,
307508
+ level_index: levelIndex,
307509
+ price,
307510
+ amount,
307511
+ notional: price * amount,
307512
+ mid_price: midPrice,
307513
+ spread_from_mid_bps: Math.abs((price - midPrice) / midPrice) * 1e4
307514
+ });
307515
+ return { table: "market_data.cex_order_book_levels", row };
307516
+ }));
307517
+ const bands = normalizedBands(input.measurementBandsBps);
307518
+ const bidDepth = bands.map((band) => {
307519
+ const minimumPrice = bestBid.price * (1 - band / 1e4);
307520
+ return bids.filter(({ price }) => price >= minimumPrice).reduce((sum3, { amount }) => sum3 + amount, 0);
307521
+ });
307522
+ const askDepth = bands.map((band) => {
307523
+ const maximumPrice = bestAsk.price * (1 + band / 1e4);
307524
+ return asks.filter(({ price }) => price <= maximumPrice).reduce((sum3, { amount }) => sum3 + amount, 0);
307525
+ });
307526
+ const summaryRow = checksumRow({
307527
+ ...common,
307528
+ best_bid: bestBid.price,
307529
+ best_ask: bestAsk.price,
307530
+ best_bid_amount: bestBid.amount,
307531
+ best_ask_amount: bestAsk.amount,
307532
+ mid_price: midPrice,
307533
+ spread: spread2,
307534
+ spread_bps: spreadBps,
307535
+ staleness_ms: receivedTimeMs - eventTimeMs,
307536
+ bid_level_count: bids.length,
307537
+ ask_level_count: asks.length,
307538
+ measurement_bands_bps: bands,
307539
+ bid_depth_by_band: bidDepth,
307540
+ ask_depth_by_band: askDepth
307541
+ });
307542
+ return {
307543
+ snapshotId,
307544
+ levels,
307545
+ summary: {
307546
+ table: "market_data.cex_order_book_depth_summary",
307547
+ row: summaryRow
307548
+ }
307549
+ };
307305
307550
  }
307306
307551
 
307307
- // src/handlers/execute-action/orders.ts
307308
- async function handleCreateOrder(ctx) {
307309
- const {
307310
- call,
307311
- wrappedCallback,
307312
- policy,
307313
- brokers,
307314
- metadata,
307315
- normalizedCex,
307316
- cex: cex3,
307552
+ // src/helpers/market-data-archive/capture-context.ts
307553
+ function createMarketCaptureContext(input) {
307554
+ const environment = input.environment ?? "development";
307555
+ const deploymentId = input.deploymentId.trim();
307556
+ if (!deploymentId)
307557
+ throw new Error("deployment_id must not be empty");
307558
+ const configuredBundle = input.captureBundleId?.trim();
307559
+ if (environment === "production" && !configuredBundle) {
307560
+ throw new Error("capture_bundle_id is required for production market capture");
307561
+ }
307562
+ const exchange = input.exchange.trim().toLowerCase();
307563
+ const symbol2 = input.symbol.trim();
307564
+ if (!exchange || !symbol2) {
307565
+ throw new Error("exchange and symbol are required for market capture");
307566
+ }
307567
+ return {
307568
+ source: input.source,
307569
+ deploymentId,
307570
+ captureBundleId: configuredBundle ?? `development:${deploymentId}`,
307571
+ exchange,
307317
307572
  symbol: symbol2,
307318
- selectedBrokerAccount,
307319
- broker,
307320
- verity,
307321
- applyVerityToBroker,
307322
- useVerity,
307323
- verityProverUrl,
307324
- otelMetrics,
307325
- brokerArchiver,
307326
- orderActivityTracker
307327
- } = ctx;
307328
- const verityProof = verity.proof;
307329
- const orderValue = parsePayloadForAction(ctx, CreateOrderPayloadSchema);
307330
- if (orderValue === null)
307331
- return;
307332
- const isPassiveOrder = orderValue.orderIntent === "passive_only";
307333
- if (isPassiveOrder && orderValue.orderType !== "limit") {
307334
- return ctx.wrappedCallback({
307335
- code: grpc6.status.INVALID_ARGUMENT,
307336
- message: "ValidationError: passive_only order intent requires a limit order"
307337
- }, null);
307573
+ assetType: input.assetType,
307574
+ feed: input.feed,
307575
+ provider: input.provider?.trim() || `ccxt:${exchange}`,
307576
+ sourceMode: input.sourceMode,
307577
+ schemaVersion: MARKET_CAPTURE_SCHEMA_VERSION,
307578
+ checksumAlgorithm: CHECKSUM_ALGORITHM,
307579
+ provenanceComplete: true,
307580
+ timeframe: input.timeframe,
307581
+ accountSelector: input.accountSelector
307582
+ };
307583
+ }
307584
+ function captureEnvironmentFromEnv(value = process.env.CEX_BROKER_MARKET_CAPTURE_ENVIRONMENT) {
307585
+ const environment = value?.trim() || "development";
307586
+ if (environment !== "development" && environment !== "production") {
307587
+ throw new Error("CEX_BROKER_MARKET_CAPTURE_ENVIRONMENT must be development or production");
307338
307588
  }
307339
- const createOrderParams = {
307340
- ...orderValue.params,
307341
- ...orderValue.clientOrderId !== undefined && {
307342
- clientOrderId: orderValue.clientOrderId
307343
- },
307344
- ...isPassiveOrder && { postOnly: true }
307589
+ return environment;
307590
+ }
307591
+
307592
+ // src/helpers/market-data-archive/ohlcv-bar-tracker.ts
307593
+ function isFiniteNumber(value) {
307594
+ return typeof value === "number" && Number.isFinite(value);
307595
+ }
307596
+ function parseOhlcvBar(value) {
307597
+ if (!Array.isArray(value) || value.length < 6) {
307598
+ return null;
307599
+ }
307600
+ const [openTimeMs, open, high, low, close, volume, quoteVolume] = value;
307601
+ if (!isFiniteNumber(openTimeMs) || !isFiniteNumber(open) || !isFiniteNumber(high) || !isFiniteNumber(low) || !isFiniteNumber(close) || !isFiniteNumber(volume)) {
307602
+ return null;
307603
+ }
307604
+ const bar = {
307605
+ openTimeMs,
307606
+ open,
307607
+ high,
307608
+ low,
307609
+ close,
307610
+ volume
307345
307611
  };
307346
- let resolvedOrderTelemetry = {};
307347
- let marketMetadataHash;
307348
- let submission = "not_attempted";
307349
- try {
307350
- if (!broker) {
307351
- return ctx.wrappedCallback({
307352
- code: grpc6.status.INVALID_ARGUMENT,
307353
- message: `Invalid CEX key: ${cex3}. Supported keys: ${Object.keys(brokers).join(", ")}`
307354
- }, null);
307612
+ if (isFiniteNumber(quoteVolume)) {
307613
+ bar.quoteVolume = quoteVolume;
307614
+ }
307615
+ return bar;
307616
+ }
307617
+ function extractOhlcvBars(payload) {
307618
+ if (!Array.isArray(payload) || payload.length === 0) {
307619
+ return [];
307620
+ }
307621
+ const rawBars = Array.isArray(payload[0]) ? payload : [payload];
307622
+ const byOpenTime = new Map;
307623
+ for (const entry of rawBars) {
307624
+ const bar = parseOhlcvBar(entry);
307625
+ if (bar) {
307626
+ byOpenTime.set(bar.openTimeMs, bar);
307355
307627
  }
307356
- const resolution = await resolveOrderExecution(policy, broker, cex3, orderValue.fromToken, orderValue.toToken, orderValue.amount, orderValue.price, orderValue.marketType);
307357
- if (!resolution.valid || !resolution.symbol || !resolution.side) {
307358
- return ctx.wrappedCallback({
307359
- code: grpc6.status.INVALID_ARGUMENT,
307360
- message: resolution.error ?? "Order rejected by policy: market or limits not satisfied"
307361
- }, null);
307628
+ }
307629
+ return [...byOpenTime.values()].sort((a, b2) => a.openTimeMs - b2.openTimeMs);
307630
+ }
307631
+ class OhlcvBarTracker {
307632
+ lastOpenTimeMs = null;
307633
+ lastBar = null;
307634
+ process(payload, brokerVersion) {
307635
+ const bars = extractOhlcvBars(payload);
307636
+ if (bars.length === 0) {
307637
+ return [];
307362
307638
  }
307363
- resolvedOrderTelemetry = {
307364
- symbol: resolution.symbol,
307365
- side: resolution.side,
307366
- requestedQuantity: resolution.amountBase ?? orderValue.amount
307367
- };
307368
- if (selectedBrokerAccount?.label) {
307369
- orderActivityTracker?.record(cex3, selectedBrokerAccount.label, resolution.symbol);
307639
+ if (bars.length === 1) {
307640
+ const [bar] = bars;
307641
+ return bar ? this.processSingleBar(bar, brokerVersion) : [];
307370
307642
  }
307371
- const telemetryIds = extractOrderTelemetryIds(createOrderParams);
307372
- const submissionTimestamp = new Date().toISOString();
307373
- marketMetadataHash = await captureMarketMetadataSnapshot(brokerArchiver, broker, {
307374
- exchange: cex3,
307375
- accountSelector: selectedBrokerAccount?.label,
307376
- symbol: resolution.symbol,
307377
- action: "CreateOrder",
307378
- brokerObservedTimestamp: submissionTimestamp,
307379
- ...telemetryIds
307380
- });
307381
- submission = "in_flight";
307382
- const order = await broker.createOrder(resolution.symbol, orderValue.orderType, resolution.side, resolution.amountBase ?? orderValue.amount, orderValue.price, createOrderParams);
307383
- submission = "placed";
307384
- const createOrderContext = {
307385
- action: "CreateOrder",
307386
- cex: cex3,
307387
- accountLabel: selectedBrokerAccount?.label,
307388
- symbol: resolvedOrderTelemetry.symbol,
307389
- side: resolvedOrderTelemetry.side,
307390
- orderType: orderValue.orderType,
307391
- requestedQuantity: resolvedOrderTelemetry.requestedQuantity,
307392
- requestedNotional: orderValue.amount * orderValue.price,
307393
- orderAuthor: orderValue.orderAuthor,
307394
- brokerObservedTimestamp: submissionTimestamp,
307395
- ...telemetryIds
307396
- };
307397
- emitOrderExecutionTelemetryInBackground(otelMetrics, createOrderContext, order);
307398
- archiveOrderExecutionInBackground(brokerArchiver, createOrderContext, order, undefined, { marketMetadataHash });
307399
- ctx.wrappedCallback(null, {
307400
- result: JSON.stringify({
307401
- ...order,
307402
- ...isPassiveOrder && {
307403
- passivePlacementOutcome: "accepted_passive"
307404
- }
307405
- })
307643
+ return this.processBatch(bars, brokerVersion);
307644
+ }
307645
+ processSingleBar(currentBar, brokerVersion) {
307646
+ if (this.lastOpenTimeMs !== null && currentBar.openTimeMs < this.lastOpenTimeMs) {
307647
+ return [];
307648
+ }
307649
+ const candidates = [];
307650
+ if (this.lastOpenTimeMs !== null && this.lastBar !== null && currentBar.openTimeMs !== this.lastOpenTimeMs) {
307651
+ candidates.push({
307652
+ bar: this.lastBar,
307653
+ isClosed: true,
307654
+ brokerVersion
307655
+ });
307656
+ }
307657
+ candidates.push({
307658
+ bar: currentBar,
307659
+ isClosed: false,
307660
+ brokerVersion
307406
307661
  });
307407
- } catch (error48) {
307408
- rethrowArchiveDurabilityError(error48);
307409
- safeLogRedactedError("Order Creation failed", error48);
307410
- const failedCreateContext = {
307411
- action: "CreateOrder",
307412
- cex: cex3,
307413
- accountLabel: selectedBrokerAccount?.label,
307414
- symbol: resolvedOrderTelemetry.symbol ?? symbol2,
307415
- side: resolvedOrderTelemetry.side,
307416
- orderType: orderValue.orderType,
307417
- requestedQuantity: resolvedOrderTelemetry.requestedQuantity ?? orderValue.amount,
307418
- requestedNotional: orderValue.amount * orderValue.price,
307419
- orderAuthor: orderValue.orderAuthor,
307420
- ...extractOrderTelemetryIds(createOrderParams)
307421
- };
307422
- emitOrderExecutionTelemetryInBackground(otelMetrics, failedCreateContext, undefined, error48);
307423
- archiveOrderExecutionInBackground(brokerArchiver, failedCreateContext, undefined, error48, { marketMetadataHash });
307424
- if (isPassiveOrder && submission === "in_flight") {
307425
- const stableErrorCode = classifyPassiveOrderError(error48);
307426
- return rejectWithGrpcError(ctx, error48, {
307427
- message: `${stableErrorCode}: ${sanitizeErrorDetail(error48)}`,
307428
- preferStableMessageOnly: true
307662
+ this.lastOpenTimeMs = currentBar.openTimeMs;
307663
+ this.lastBar = currentBar;
307664
+ return candidates;
307665
+ }
307666
+ processBatch(bars, brokerVersion) {
307667
+ const firstBar = bars[0];
307668
+ const lastBar = bars[bars.length - 1];
307669
+ if (!firstBar || !lastBar) {
307670
+ return [];
307671
+ }
307672
+ const lastOpenTimeMs = this.lastOpenTimeMs;
307673
+ if (lastOpenTimeMs !== null && lastBar.openTimeMs < lastOpenTimeMs) {
307674
+ return [];
307675
+ }
307676
+ const barsToProcess = lastOpenTimeMs === null ? bars : bars.filter((bar) => bar.openTimeMs >= lastOpenTimeMs);
307677
+ const firstBarToProcess = barsToProcess[0];
307678
+ const lastBarToProcess = barsToProcess[barsToProcess.length - 1];
307679
+ if (!firstBarToProcess || !lastBarToProcess) {
307680
+ return [];
307681
+ }
307682
+ const candidates = [];
307683
+ if (this.lastBar !== null && lastOpenTimeMs !== null && lastOpenTimeMs < firstBarToProcess.openTimeMs) {
307684
+ candidates.push({
307685
+ bar: this.lastBar,
307686
+ isClosed: true,
307687
+ brokerVersion
307429
307688
  });
307430
307689
  }
307431
- ctx.wrappedCallback({
307432
- code: grpc6.status.INTERNAL,
307433
- message: `Order Creation failed: ${sanitizeErrorDetail(error48)}`
307434
- }, null);
307690
+ for (let index2 = 0;index2 < barsToProcess.length - 1; index2 += 1) {
307691
+ const bar = barsToProcess[index2];
307692
+ if (bar) {
307693
+ candidates.push({
307694
+ bar,
307695
+ isClosed: true,
307696
+ brokerVersion
307697
+ });
307698
+ }
307699
+ }
307700
+ candidates.push({
307701
+ bar: lastBarToProcess,
307702
+ isClosed: false,
307703
+ brokerVersion
307704
+ });
307705
+ this.lastOpenTimeMs = lastBarToProcess.openTimeMs;
307706
+ this.lastBar = lastBarToProcess;
307707
+ return candidates;
307435
307708
  }
307436
307709
  }
307437
- async function handleGetOrderDetails(ctx) {
307438
- const {
307439
- call,
307440
- wrappedCallback,
307441
- policy,
307442
- brokers,
307443
- metadata,
307444
- normalizedCex,
307445
- cex: cex3,
307446
- symbol: symbol2,
307447
- selectedBrokerAccount,
307448
- broker,
307449
- verity,
307450
- applyVerityToBroker,
307451
- useVerity,
307452
- verityProverUrl,
307453
- otelMetrics,
307454
- brokerArchiver,
307455
- orderActivityTracker
307456
- } = ctx;
307457
- const verityProof = verity.proof;
307458
- const getOrderValue = parsePayloadForAction(ctx, GetOrderDetailsPayloadSchema);
307459
- if (getOrderValue === null)
307460
- return;
307461
- try {
307462
- if (!broker) {
307463
- return ctx.wrappedCallback({
307464
- code: grpc6.status.INVALID_ARGUMENT,
307465
- message: `Invalid CEX key: ${cex3}. Supported keys: ${Object.keys(brokers).join(", ")}`
307466
- }, null);
307467
- }
307468
- const orderDetails = await broker.fetchOrder(getOrderValue.orderId, symbol2, { ...getOrderValue.params });
307469
- if (selectedBrokerAccount?.label && symbol2) {
307470
- orderActivityTracker?.record(cex3, selectedBrokerAccount.label, symbol2);
307471
- }
307472
- const getOrderContext = {
307473
- action: "GetOrderDetails",
307474
- cex: cex3,
307475
- accountLabel: selectedBrokerAccount?.label,
307476
- symbol: symbol2,
307477
- ...extractOrderTelemetryIds(getOrderValue.params)
307478
- };
307479
- emitOrderExecutionTelemetryInBackground(otelMetrics, getOrderContext, orderDetails);
307480
- archiveOrderExecutionInBackground(brokerArchiver, getOrderContext, orderDetails);
307481
- ctx.wrappedCallback(null, {
307482
- result: JSON.stringify({
307483
- orderId: orderDetails.id,
307484
- status: orderDetails.status,
307485
- amount: orderDetails.amount,
307486
- filled: orderDetails.filled,
307487
- remaining: orderDetails.remaining,
307488
- symbol: orderDetails.symbol,
307489
- side: orderDetails.side,
307490
- price: orderDetails.price
307491
- })
307492
- });
307493
- } catch (error48) {
307494
- safeLogError(`Error fetching order details from ${cex3}`, error48);
307495
- const failedGetOrderContext = {
307496
- action: "GetOrderDetails",
307497
- cex: cex3,
307498
- accountLabel: selectedBrokerAccount?.label,
307499
- symbol: symbol2,
307500
- ...extractOrderTelemetryIds(getOrderValue.params)
307501
- };
307502
- emitOrderExecutionTelemetryInBackground(otelMetrics, failedGetOrderContext, undefined, error48);
307503
- archiveOrderExecutionInBackground(brokerArchiver, failedGetOrderContext, undefined, error48);
307504
- ctx.wrappedCallback({
307505
- code: grpc6.status.INTERNAL,
307506
- message: `Failed to fetch order details from ${cex3}: ${sanitizeErrorDetail(error48)}`
307507
- }, null);
307710
+
307711
+ // src/helpers/market-data-archive/orderbook-depth.ts
307712
+ var DEFAULT_ORDERBOOK_ARCHIVE_DEPTH_LIMIT = 25;
307713
+ var MAX_ORDERBOOK_ARCHIVE_DEPTH_LIMIT = 500;
307714
+ function getOrderbookArchiveDepthLimit() {
307715
+ const raw = process.env.CEX_BROKER_ORDERBOOK_ARCHIVE_DEPTH_LIMIT;
307716
+ if (!raw) {
307717
+ return DEFAULT_ORDERBOOK_ARCHIVE_DEPTH_LIMIT;
307718
+ }
307719
+ const parsed = Number.parseInt(raw, 10);
307720
+ if (!Number.isFinite(parsed) || parsed <= 0) {
307721
+ return DEFAULT_ORDERBOOK_ARCHIVE_DEPTH_LIMIT;
307508
307722
  }
307723
+ return Math.min(parsed, MAX_ORDERBOOK_ARCHIVE_DEPTH_LIMIT);
307509
307724
  }
307510
- async function handleCancelOrder(ctx) {
307511
- const {
307512
- call,
307513
- wrappedCallback,
307514
- policy,
307515
- brokers,
307516
- metadata,
307517
- normalizedCex,
307518
- cex: cex3,
307519
- symbol: symbol2,
307520
- selectedBrokerAccount,
307521
- broker,
307522
- verity,
307523
- applyVerityToBroker,
307524
- useVerity,
307525
- verityProverUrl,
307526
- otelMetrics,
307527
- brokerArchiver,
307528
- orderActivityTracker
307529
- } = ctx;
307530
- const verityProof = verity.proof;
307531
- const cancelOrderValue = parsePayloadForAction(ctx, CancelOrderPayloadSchema);
307532
- if (cancelOrderValue === null)
307533
- return;
307534
- try {
307535
- if (!broker) {
307536
- return ctx.wrappedCallback({
307537
- code: grpc6.status.INVALID_ARGUMENT,
307538
- message: `Invalid CEX key: ${cex3}. Supported keys: ${Object.keys(brokers).join(", ")}`
307539
- }, null);
307725
+
307726
+ // src/helpers/market-data-archive/orderbook-sampler.ts
307727
+ var DEFAULT_ORDERBOOK_INTERVAL_MS = 1000;
307728
+ function getOrderbookIntervalMs() {
307729
+ const raw = process.env.CEX_BROKER_ORDERBOOK_INTERVAL_MS ?? process.env.CEX_BROKER_ORDERBOOK_TOB_INTERVAL_MS;
307730
+ if (!raw) {
307731
+ return DEFAULT_ORDERBOOK_INTERVAL_MS;
307732
+ }
307733
+ const parsed = Number.parseInt(raw, 10);
307734
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_ORDERBOOK_INTERVAL_MS;
307735
+ }
307736
+ function isMarketArchiveEnabled() {
307737
+ return process.env.CEX_BROKER_MARKET_ARCHIVE_ENABLED !== "false";
307738
+ }
307739
+
307740
+ class OrderbookSampler {
307741
+ intervalMs;
307742
+ lastEmitMs = null;
307743
+ constructor(intervalMs = getOrderbookIntervalMs()) {
307744
+ this.intervalMs = intervalMs;
307745
+ }
307746
+ shouldEmit(nowMs = Date.now()) {
307747
+ if (this.lastEmitMs !== null && nowMs < this.lastEmitMs) {
307748
+ this.lastEmitMs = nowMs;
307749
+ return true;
307540
307750
  }
307541
- const cancelOrderContext = {
307542
- action: "CancelOrder",
307543
- cex: cex3,
307544
- accountLabel: selectedBrokerAccount?.label,
307545
- symbol: symbol2,
307546
- ...extractOrderTelemetryIds(cancelOrderValue.params)
307547
- };
307548
- const cancelledOrder = await broker.cancelOrder(cancelOrderValue.orderId, symbol2, cancelOrderValue.params ?? {});
307549
- if (selectedBrokerAccount?.label && symbol2) {
307550
- orderActivityTracker?.record(cex3, selectedBrokerAccount.label, symbol2);
307751
+ if (this.lastEmitMs !== null && nowMs - this.lastEmitMs < this.intervalMs) {
307752
+ return false;
307551
307753
  }
307552
- emitOrderExecutionTelemetryInBackground(otelMetrics, cancelOrderContext, cancelledOrder);
307553
- archiveOrderExecutionInBackground(brokerArchiver, cancelOrderContext, cancelledOrder);
307554
- ctx.wrappedCallback(null, {
307555
- result: JSON.stringify({ ...cancelledOrder })
307556
- });
307557
- } catch (error48) {
307558
- safeLogError(`Error cancelling order from ${cex3}`, error48);
307559
- const failedCancelContext = {
307560
- action: "CancelOrder",
307561
- cex: cex3,
307562
- accountLabel: selectedBrokerAccount?.label,
307563
- symbol: symbol2,
307564
- ...extractOrderTelemetryIds(cancelOrderValue.params)
307565
- };
307566
- emitOrderExecutionTelemetryInBackground(otelMetrics, failedCancelContext, undefined, error48);
307567
- archiveOrderExecutionInBackground(brokerArchiver, failedCancelContext, undefined, error48);
307568
- ctx.wrappedCallback({
307569
- code: grpc6.status.INTERNAL,
307570
- message: `Failed to cancel order from ${cex3}: ${sanitizeErrorDetail(error48)}`
307571
- }, null);
307754
+ this.lastEmitMs = nowMs;
307755
+ return true;
307572
307756
  }
307573
307757
  }
307574
- async function handleOrders(ctx) {
307575
- if (ctx.action === Action.CreateOrder)
307576
- return handleCreateOrder(ctx);
307577
- if (ctx.action === Action.GetOrderDetails)
307578
- return handleGetOrderDetails(ctx);
307579
- if (ctx.action === Action.CancelOrder)
307580
- return handleCancelOrder(ctx);
307581
- }
307582
307758
 
307583
- // src/handlers/execute-action/pass-through.ts
307584
- import * as grpc7 from "@grpc/grpc-js";
307585
- async function handleFetchCurrency(ctx) {
307586
- const {
307587
- call,
307588
- wrappedCallback,
307589
- policy,
307590
- brokers,
307591
- metadata,
307592
- normalizedCex,
307593
- cex: cex3,
307594
- symbol: symbol2,
307595
- selectedBrokerAccount,
307596
- broker,
307597
- verity,
307598
- applyVerityToBroker,
307599
- useVerity,
307600
- verityProverUrl,
307601
- otelMetrics
307602
- } = ctx;
307603
- const verityProof = verity.proof;
307604
- if (!symbol2) {
307605
- return ctx.wrappedCallback({
307606
- code: grpc7.status.INVALID_ARGUMENT,
307607
- message: `ValidationError: Symbol required`
307608
- }, null);
307759
+ // src/helpers/market-data-archive/parse-stream.ts
307760
+ function isFiniteNumber2(value) {
307761
+ return typeof value === "number" && Number.isFinite(value);
307762
+ }
307763
+ function toNumber2(value) {
307764
+ if (isFiniteNumber2(value)) {
307765
+ return value;
307609
307766
  }
307610
- try {
307611
- const assetCode = symbol2.trim().toUpperCase();
307612
- const currencyInfo = await fetchCurrencyMetadata(broker, assetCode);
307613
- if (!currencyInfo) {
307614
- return ctx.wrappedCallback({
307615
- code: grpc7.status.NOT_FOUND,
307616
- message: `venue_discovery_unavailable: currency not found for ${assetCode}`
307617
- }, null);
307767
+ if (typeof value === "string") {
307768
+ const parsed = Number.parseFloat(value);
307769
+ if (Number.isFinite(parsed)) {
307770
+ return parsed;
307618
307771
  }
307619
- const networkEvidence = buildTransferNetworkEvidence(currencyInfo);
307620
- ctx.wrappedCallback(null, {
307621
- proof: ctx.verity.proof,
307622
- result: JSON.stringify({
307623
- ...currencyInfo,
307624
- exchange: normalizedCex,
307625
- asset: assetCode,
307626
- code: currencyInfo.code ?? assetCode,
307627
- id: currencyInfo.id ?? null,
307628
- networks: networkEvidence.networks,
307629
- networkAliases: networkEvidence.aliases,
307630
- raw: currencyInfo
307631
- })
307632
- });
307633
- } catch (error48) {
307634
- safeLogError(`Error fetching currency ${symbol2} from ${cex3}`, error48);
307635
- const message = getErrorMessage(error48);
307636
- ctx.wrappedCallback({
307637
- code: stableGrpcErrorCode(message) ?? mapCcxtErrorToGrpcStatus(error48) ?? grpc7.status.INTERNAL,
307638
- message: message.startsWith("venue_discovery_unavailable:") ? message : `venue_discovery_unavailable: ${message}`
307639
- }, null);
307640
307772
  }
307773
+ return;
307641
307774
  }
307642
- async function handleFetchAccountId(ctx) {
307643
- const {
307644
- call,
307645
- wrappedCallback,
307646
- policy,
307647
- brokers,
307648
- metadata,
307649
- normalizedCex,
307650
- cex: cex3,
307651
- symbol: symbol2,
307652
- selectedBrokerAccount,
307653
- broker,
307654
- verity,
307655
- applyVerityToBroker,
307656
- useVerity,
307657
- verityProverUrl,
307658
- otelMetrics
307659
- } = ctx;
307660
- const verityProof = verity.proof;
307775
+ function toStringId(value) {
307776
+ if (typeof value === "string" && value.trim()) {
307777
+ return value.trim();
307778
+ }
307779
+ if (typeof value === "number" && Number.isFinite(value)) {
307780
+ return String(value);
307781
+ }
307782
+ return;
307783
+ }
307784
+ function scalarTimestampMs(value, fallbackMs) {
307785
+ const numeric = toNumber2(value);
307786
+ if (numeric !== undefined) {
307787
+ return numeric < 1000000000000 ? numeric * 1000 : numeric;
307788
+ }
307789
+ if (typeof value === "string") {
307790
+ const parsed = Date.parse(value);
307791
+ if (Number.isFinite(parsed)) {
307792
+ return parsed;
307793
+ }
307794
+ }
307795
+ return fallbackMs;
307796
+ }
307797
+ function parseTrade(value, fallbackMs = Date.now()) {
307798
+ const record2 = asRecord(value);
307799
+ if (!record2) {
307800
+ return null;
307801
+ }
307802
+ const tradeId = toStringId(record2.id);
307803
+ const price = toNumber2(record2.price);
307804
+ const amount = toNumber2(record2.amount);
307805
+ const side = typeof record2.side === "string" ? record2.side.toLowerCase() : undefined;
307806
+ if (!tradeId || price === undefined || amount === undefined || !side) {
307807
+ return null;
307808
+ }
307809
+ const parsed = {
307810
+ tradeId,
307811
+ eventTimeMs: scalarTimestampMs(record2.timestamp, fallbackMs),
307812
+ side,
307813
+ price,
307814
+ amount
307815
+ };
307816
+ const cost = toNumber2(record2.cost);
307817
+ if (cost !== undefined) {
307818
+ parsed.cost = cost;
307819
+ }
307820
+ if (typeof record2.takerOrMaker === "string") {
307821
+ parsed.takerOrMaker = record2.takerOrMaker;
307822
+ }
307823
+ return parsed;
307824
+ }
307825
+ function extractTrades(payload, fallbackMs = Date.now()) {
307826
+ if (Array.isArray(payload)) {
307827
+ return payload.map((entry) => parseTrade(entry, fallbackMs)).filter((entry) => entry !== null);
307828
+ }
307829
+ const single = parseTrade(payload, fallbackMs);
307830
+ return single ? [single] : [];
307831
+ }
307832
+ function parseTicker(value, fallbackMs) {
307833
+ const record2 = asRecord(value);
307834
+ if (!record2) {
307835
+ return null;
307836
+ }
307837
+ const parsed = {
307838
+ eventTimeMs: scalarTimestampMs(record2.timestamp, fallbackMs)
307839
+ };
307840
+ const fields = [
307841
+ ["last", record2.last],
307842
+ ["bid", record2.bid],
307843
+ ["ask", record2.ask],
307844
+ ["high", record2.high],
307845
+ ["low", record2.low],
307846
+ ["open", record2.open],
307847
+ ["close", record2.close],
307848
+ ["baseVolume", record2.baseVolume],
307849
+ ["quoteVolume", record2.quoteVolume],
307850
+ ["change", record2.change],
307851
+ ["percentage", record2.percentage]
307852
+ ];
307853
+ for (const [key, rawValue] of fields) {
307854
+ const numeric = toNumber2(rawValue);
307855
+ if (numeric !== undefined) {
307856
+ parsed[key] = numeric;
307857
+ }
307858
+ }
307859
+ return parsed;
307860
+ }
307861
+
307862
+ // src/helpers/market-data-archive/rows.ts
307863
+ function compactUndefined3(record2) {
307864
+ return Object.fromEntries(Object.entries(record2).filter(([, value]) => value !== undefined));
307865
+ }
307866
+ function withNormalizedChecksum(record2) {
307867
+ const compact = compactUndefined3(record2);
307868
+ return {
307869
+ ...compact,
307870
+ normalized_row_checksum: sha256Canonical(compact)
307871
+ };
307872
+ }
307873
+ function buildCanonicalCexStreamEventRow(context2, rawCapture) {
307874
+ const row = withNormalizedChecksum({
307875
+ ...captureCoreFields(context2, rawCapture),
307876
+ stream_type: context2.feed,
307877
+ event_time_ms: rawCapture.eventTimeMs,
307878
+ payload_encoding: "canonical_json_v1",
307879
+ payload_json: canonicalSerialize(rawCapture.redactedPayload)
307880
+ });
307881
+ return { table: "market_data.cex_stream_events", row };
307882
+ }
307883
+ function buildCanonicalTickerEventRow(context2, rawCapture, ticker) {
307884
+ if (context2.feed !== "TICKER") {
307885
+ throw new Error("Ticker row requires a TICKER capture context");
307886
+ }
307887
+ const row = withNormalizedChecksum({
307888
+ ...captureCoreFields(context2, rawCapture),
307889
+ source_time_ms: ticker.eventTimeMs,
307890
+ event_time_ms: ticker.eventTimeMs,
307891
+ last: ticker.last,
307892
+ bid: ticker.bid,
307893
+ ask: ticker.ask,
307894
+ high: ticker.high,
307895
+ low: ticker.low,
307896
+ open: ticker.open,
307897
+ close: ticker.close,
307898
+ base_volume: ticker.baseVolume,
307899
+ quote_volume: ticker.quoteVolume,
307900
+ change: ticker.change,
307901
+ percentage: ticker.percentage
307902
+ });
307903
+ return { table: "market_data.cex_ticker_events", row };
307904
+ }
307905
+ function buildCanonicalTradeRow(context2, rawCapture, trade) {
307906
+ if (context2.feed !== "TRADES") {
307907
+ throw new Error("Trade row requires a TRADES capture context");
307908
+ }
307909
+ const row = withNormalizedChecksum({
307910
+ ...captureCoreFields(context2, rawCapture),
307911
+ source_time_ms: trade.eventTimeMs,
307912
+ trade_id: trade.tradeId,
307913
+ event_time_ms: trade.eventTimeMs,
307914
+ side: trade.side,
307915
+ price: trade.price,
307916
+ amount: trade.amount,
307917
+ cost: trade.cost,
307918
+ taker_or_maker: trade.takerOrMaker
307919
+ });
307920
+ return { table: "market_data.cex_trades", row };
307921
+ }
307922
+ function buildCanonicalOhlcvRow(input) {
307923
+ if (input.context.feed !== "OHLCV") {
307924
+ throw new Error("OHLCV row requires an OHLCV capture context");
307925
+ }
307926
+ const row = withNormalizedChecksum({
307927
+ ...captureCoreFields(input.context, input.rawCapture),
307928
+ source_time_ms: input.bar.openTimeMs,
307929
+ timeframe: input.context.timeframe ?? "1m",
307930
+ open_time_ms: input.bar.openTimeMs,
307931
+ open: input.bar.open,
307932
+ high: input.bar.high,
307933
+ low: input.bar.low,
307934
+ close: input.bar.close,
307935
+ volume: input.bar.volume,
307936
+ quote_volume: input.bar.quoteVolume,
307937
+ is_closed: input.isClosed ? 1 : 0,
307938
+ broker_version: input.brokerVersion
307939
+ });
307940
+ return { table: "market_data.cex_ohlcv", row };
307941
+ }
307942
+ function buildCexStreamEventRow(input) {
307943
+ const receivedTimeMs = input.receivedTimestamp;
307944
+ const redactedPayload = redactStreamPayload(input.payload);
307945
+ const tags = buildCommonArchiveTags({
307946
+ source: input.source,
307947
+ deploymentId: input.deploymentId,
307948
+ accountSelector: input.accountSelector,
307949
+ exchange: input.exchange,
307950
+ symbol: input.symbol,
307951
+ brokerObservedTimestamp: new Date(receivedTimeMs).toISOString()
307952
+ });
307953
+ return {
307954
+ table: "market_data.cex_stream_events",
307955
+ row: compactUndefined3({
307956
+ ...tags,
307957
+ asset_type: input.assetType,
307958
+ stream_type: input.streamType,
307959
+ event_time_ms: input.eventTimeMs ?? receivedTimeMs,
307960
+ received_time_ms: receivedTimeMs,
307961
+ payload_json: JSON.stringify(redactedPayload)
307962
+ })
307963
+ };
307964
+ }
307965
+
307966
+ // src/helpers/market-data-archive/capture.ts
307967
+ async function recordWatchMetric(otelMetrics, metricName, labels) {
307661
307968
  try {
307662
- const accountId = await broker.fetchAccountId();
307663
- return ctx.wrappedCallback(null, {
307969
+ await otelMetrics?.recordCounter(metricName, 1, labels);
307970
+ } catch {}
307971
+ }
307972
+ function watchLabels(stream4, input, archiver, feed) {
307973
+ return {
307974
+ stream: stream4,
307975
+ feed,
307976
+ source: archiver?.getSource() ?? "disabled",
307977
+ exchange: input.exchange,
307978
+ symbol: input.symbol
307979
+ };
307980
+ }
307981
+ function resolveCaptureContext(archiver, input, feed, sourceMode) {
307982
+ return createMarketCaptureContext({
307983
+ source: archiver.getSource(),
307984
+ deploymentId: archiver.getDeploymentId(),
307985
+ captureBundleId: process.env.CEX_BROKER_CAPTURE_BUNDLE_ID,
307986
+ exchange: input.exchange,
307987
+ symbol: input.symbol,
307988
+ assetType: input.assetType,
307989
+ feed,
307990
+ provider: `ccxt:${input.exchange.trim().toLowerCase()}`,
307991
+ sourceMode,
307992
+ timeframe: input.timeframe,
307993
+ accountSelector: input.accountSelector,
307994
+ environment: captureEnvironmentFromEnv()
307995
+ });
307996
+ }
307997
+ function archiveOrderbookInBackground(archiver, otelMetrics, input, options) {
307998
+ const labels = watchLabels("orderbook", input, archiver, "ORDERBOOK");
307999
+ recordWatchMetric(otelMetrics, "cex_watch_frames_received_total", labels);
308000
+ if (!isMarketArchiveEnabled() || !archiver?.isEnabled()) {
308001
+ return;
308002
+ }
308003
+ if (options?.sampledOut) {
308004
+ recordWatchMetric(otelMetrics, "cex_watch_frames_sampled_out_total", labels);
308005
+ return;
308006
+ }
308007
+ queueMicrotask(() => {
308008
+ try {
308009
+ const context2 = resolveCaptureContext(archiver, input, "ORDERBOOK", options?.sourceMode ?? "broker_live_sampling_v1");
308010
+ const rawCapture = createRawCapture(context2, {
308011
+ payload: input.snapshot,
308012
+ eventTimeMs: input.snapshot.timestamp,
308013
+ receivedTimeMs: input.snapshot.receivedTimestamp,
308014
+ scope: "ccxt_normalized_object"
308015
+ });
308016
+ const canonical = buildCanonicalOrderBookRows({
308017
+ context: context2,
308018
+ snapshot: input.snapshot,
308019
+ rawCapture,
308020
+ depthLimit: options?.depthLimit ?? getOrderbookArchiveDepthLimit()
308021
+ });
308022
+ archiver.enqueue(buildCanonicalCexStreamEventRow(context2, rawCapture));
308023
+ for (const row of canonical.levels)
308024
+ archiver.enqueue(row);
308025
+ archiver.enqueue(canonical.summary);
308026
+ recordWatchMetric(otelMetrics, "cex_watch_frames_archived_total", labels);
308027
+ } catch (error48) {
308028
+ rethrowArchiveDurabilityError(error48);
308029
+ if (error48 instanceof OrderBookValidationError) {
308030
+ recordWatchMetric(otelMetrics, "cex_watch_frames_invalid_total", {
308031
+ ...labels,
308032
+ reason: error48.reason
308033
+ });
308034
+ }
308035
+ log.warn("Failed to archive orderbook snapshot", { error: error48 });
308036
+ }
308037
+ });
308038
+ }
308039
+ function archiveOhlcvInBackground(archiver, otelMetrics, tracker, input) {
308040
+ const labels = watchLabels("ohlcv", input, archiver, "OHLCV");
308041
+ recordWatchMetric(otelMetrics, "cex_watch_frames_received_total", labels);
308042
+ if (!isMarketArchiveEnabled() || !archiver?.isEnabled()) {
308043
+ return;
308044
+ }
308045
+ queueMicrotask(() => {
308046
+ try {
308047
+ const candidates = tracker.process(input.payload, input.receivedTimestamp);
308048
+ const context2 = resolveCaptureContext(archiver, input, "OHLCV", input.sourceMode ?? "broker_live_stream_v1");
308049
+ const rawCapture = candidates.length > 0 ? createRawCapture(context2, {
308050
+ payload: input.payload,
308051
+ eventTimeMs: candidates[0]?.bar.openTimeMs ?? input.receivedTimestamp,
308052
+ receivedTimeMs: input.receivedTimestamp,
308053
+ scope: "ccxt_normalized_object"
308054
+ }) : undefined;
308055
+ if (rawCapture) {
308056
+ archiver.enqueue(buildCanonicalCexStreamEventRow(context2, rawCapture));
308057
+ }
308058
+ for (const candidate of candidates) {
308059
+ if (rawCapture) {
308060
+ archiver.enqueue(buildCanonicalOhlcvRow({
308061
+ context: context2,
308062
+ rawCapture,
308063
+ bar: candidate.bar,
308064
+ isClosed: candidate.isClosed,
308065
+ brokerVersion: candidate.brokerVersion
308066
+ }));
308067
+ }
308068
+ }
308069
+ if (candidates.length > 0) {
308070
+ recordWatchMetric(otelMetrics, "cex_watch_frames_archived_total", labels);
308071
+ }
308072
+ } catch (error48) {
308073
+ rethrowArchiveDurabilityError(error48);
308074
+ log.warn("Failed to archive OHLCV candle", { error: error48 });
308075
+ }
308076
+ });
308077
+ }
308078
+ function createOrderbookSampler() {
308079
+ return new OrderbookSampler;
308080
+ }
308081
+ function createOhlcvBarTracker() {
308082
+ return new OhlcvBarTracker;
308083
+ }
308084
+ function archiveMarketRowsInBackground(archiver, otelMetrics, stream4, input, feed, enqueueRows) {
308085
+ const labels = watchLabels(stream4, input, archiver, feed);
308086
+ recordWatchMetric(otelMetrics, "cex_watch_frames_received_total", labels);
308087
+ if (!isMarketArchiveEnabled() || !archiver?.isEnabled()) {
308088
+ return;
308089
+ }
308090
+ queueMicrotask(() => {
308091
+ try {
308092
+ const rows = enqueueRows();
308093
+ for (const row of rows) {
308094
+ archiver.enqueue(row);
308095
+ }
308096
+ if (rows.length > 0) {
308097
+ recordWatchMetric(otelMetrics, "cex_watch_frames_archived_total", labels);
308098
+ }
308099
+ } catch (error48) {
308100
+ rethrowArchiveDurabilityError(error48);
308101
+ log.warn(`Failed to archive ${stream4} market data`, { error: error48 });
308102
+ }
308103
+ });
308104
+ }
308105
+ function archiveTradesInBackground(archiver, otelMetrics, input) {
308106
+ archiveMarketRowsInBackground(archiver, otelMetrics, "trades", input, "TRADES", () => {
308107
+ if (!archiver)
308108
+ return [];
308109
+ const trades = extractTrades(input.payload, input.receivedTimestamp);
308110
+ if (trades.length === 0)
308111
+ return [];
308112
+ const context2 = resolveCaptureContext(archiver, input, "TRADES", "broker_live_stream_v1");
308113
+ const raw = createRawCapture(context2, {
308114
+ payload: input.payload,
308115
+ eventTimeMs: trades[0]?.eventTimeMs ?? input.receivedTimestamp,
308116
+ receivedTimeMs: input.receivedTimestamp,
308117
+ scope: "ccxt_normalized_object"
308118
+ });
308119
+ return [
308120
+ buildCanonicalCexStreamEventRow(context2, raw),
308121
+ ...trades.map((trade) => buildCanonicalTradeRow(context2, raw, trade))
308122
+ ];
308123
+ });
308124
+ }
308125
+ function archiveTickerInBackground(archiver, otelMetrics, input) {
308126
+ archiveMarketRowsInBackground(archiver, otelMetrics, "ticker", input, "TICKER", () => {
308127
+ const ticker = parseTicker(input.payload, input.receivedTimestamp);
308128
+ if (!ticker || !archiver)
308129
+ return [];
308130
+ const context2 = resolveCaptureContext(archiver, input, "TICKER", "broker_live_stream_v1");
308131
+ const raw = createRawCapture(context2, {
308132
+ payload: input.payload,
308133
+ eventTimeMs: ticker.eventTimeMs,
308134
+ receivedTimeMs: input.receivedTimestamp,
308135
+ scope: "ccxt_normalized_object"
308136
+ });
308137
+ return [
308138
+ buildCanonicalCexStreamEventRow(context2, raw),
308139
+ buildCanonicalTickerEventRow(context2, raw, ticker)
308140
+ ];
308141
+ });
308142
+ }
308143
+ function archiveCexStreamEventInBackground(archiver, otelMetrics, input) {
308144
+ archiveMarketRowsInBackground(archiver, otelMetrics, "stream", input, input.streamType, () => [
308145
+ buildCexStreamEventRow({
308146
+ ...input,
308147
+ source: archiver?.getSource()
308148
+ })
308149
+ ]);
308150
+ }
308151
+
308152
+ // src/handlers/execute-action/order-book-call.ts
308153
+ async function handleOrderBookCall(ctx) {
308154
+ const parsedOrderBookCall = parseOrderBookCallPayload(ctx.call.request.payload, {
308155
+ exchange: ctx.normalizedCex,
308156
+ symbol: ctx.symbol
308157
+ });
308158
+ if (parsedOrderBookCall.kind === "error") {
308159
+ ctx.wrappedCallback({
308160
+ code: grpc4.status.INVALID_ARGUMENT,
308161
+ message: parsedOrderBookCall.message
308162
+ }, null);
308163
+ return true;
308164
+ }
308165
+ if (parsedOrderBookCall.kind !== "order_book") {
308166
+ return false;
308167
+ }
308168
+ const orderBookBroker = ctx.broker;
308169
+ if (!orderBookBroker) {
308170
+ ctx.wrappedCallback({
308171
+ code: grpc4.status.INVALID_ARGUMENT,
308172
+ message: `Unsupported exchange for order-book market data: ${ctx.normalizedCex}`
308173
+ }, null);
308174
+ return true;
308175
+ }
308176
+ ctx.applyVerityToBroker(orderBookBroker);
308177
+ try {
308178
+ const orderBookPayload = parsedOrderBookCall.payload;
308179
+ if (orderBookPayload.method === ORDER_BOOK_CALL_METHODS.FETCH_CAPABILITY) {
308180
+ ctx.wrappedCallback(null, {
308181
+ proof: ctx.verity.proof,
308182
+ result: JSON.stringify(buildOrderBookCapability(orderBookBroker, orderBookPayload))
308183
+ });
308184
+ return true;
308185
+ }
308186
+ if (orderBookPayload.method === ORDER_BOOK_CALL_METHODS.FETCH_HISTORICAL_SNAPSHOTS) {
308187
+ ctx.wrappedCallback(null, {
308188
+ proof: ctx.verity.proof,
308189
+ result: JSON.stringify(buildHistoricalOrderBookUnsupported(orderBookPayload))
308190
+ });
308191
+ return true;
308192
+ }
308193
+ const fetchOrderBook = orderBookBroker.fetchOrderBook;
308194
+ const canFetchOrderBook = typeof fetchOrderBook === "function" && orderBookBroker.has?.fetchOrderBook !== false;
308195
+ if (!canFetchOrderBook) {
308196
+ ctx.wrappedCallback({
308197
+ code: grpc4.status.UNIMPLEMENTED,
308198
+ message: `Order-book snapshot unsupported for ${ctx.normalizedCex}`
308199
+ }, null);
308200
+ return true;
308201
+ }
308202
+ const receivedTimestamp = Date.now();
308203
+ const rawOrderBook = await fetchOrderBook.call(orderBookBroker, orderBookPayload.symbol, orderBookPayload.depthLimit);
308204
+ const snapshot = normalizeOrderBookSnapshot(rawOrderBook, {
308205
+ exchange: orderBookPayload.exchange,
308206
+ symbol: orderBookPayload.symbol,
308207
+ depthLimit: orderBookPayload.depthLimit,
308208
+ receivedTimestamp
308209
+ });
308210
+ ctx.wrappedCallback(null, {
307664
308211
  proof: ctx.verity.proof,
307665
- result: JSON.stringify({ accountId })
308212
+ result: JSON.stringify(snapshot)
308213
+ });
308214
+ archiveOrderbookInBackground(ctx.brokerArchiver, ctx.otelMetrics, {
308215
+ exchange: orderBookPayload.exchange,
308216
+ symbol: orderBookPayload.symbol,
308217
+ assetType: "spot",
308218
+ accountSelector: ctx.selectedBrokerAccount?.label,
308219
+ deploymentId: ctx.brokerArchiver?.getDeploymentId() ?? "unarchived",
308220
+ snapshot
308221
+ }, {
308222
+ sourceMode: "broker_current_snapshot_v1",
308223
+ depthLimit: orderBookPayload.depthLimit
307666
308224
  });
307667
308225
  } catch (error48) {
307668
- safeLogError(`Error fetching account ID ${cex3}`, error48);
308226
+ safeLogError("Order-book Call failed", error48);
307669
308227
  ctx.wrappedCallback({
307670
- code: grpc7.status.INTERNAL,
307671
- message: `Error fetching account ID from ${cex3}`
308228
+ code: mapCcxtErrorToGrpcStatus(error48) ?? grpc4.status.INTERNAL,
308229
+ message: `Order-book Call failed: ${sanitizeErrorDetail(error48)}`
307672
308230
  }, null);
307673
308231
  }
308232
+ return true;
307674
308233
  }
307675
- async function handleFetchFees(ctx) {
308234
+
308235
+ // src/handlers/execute-action/registry.ts
308236
+ import * as grpc11 from "@grpc/grpc-js";
308237
+
308238
+ // src/handlers/execute-action/internal-transfer.ts
308239
+ import * as grpc5 from "@grpc/grpc-js";
308240
+ async function handleInternalTransfer(ctx) {
307676
308241
  const {
307677
- call,
307678
- wrappedCallback,
307679
- policy,
307680
308242
  brokers,
307681
308243
  metadata,
307682
308244
  normalizedCex,
307683
- cex: cex3,
307684
308245
  symbol: symbol2,
307685
- selectedBrokerAccount,
307686
- broker,
307687
308246
  verity,
307688
- applyVerityToBroker,
307689
308247
  useVerity,
307690
308248
  verityProverUrl,
307691
- otelMetrics
308249
+ brokerArchiver
307692
308250
  } = ctx;
307693
- const verityProof = verity.proof;
307694
308251
  if (!symbol2) {
307695
308252
  return ctx.wrappedCallback({
307696
- code: grpc7.status.INVALID_ARGUMENT,
308253
+ code: grpc5.status.INVALID_ARGUMENT,
307697
308254
  message: `ValidationError: Symbol required`
307698
308255
  }, null);
307699
308256
  }
307700
- const feesPayload = parsePayloadForAction(ctx, FetchFeesPayloadSchema);
307701
- if (feesPayload === null)
308257
+ const transferPayload = parsePayloadForAction(ctx, InternalTransferPayloadSchema);
308258
+ if (transferPayload === null)
307702
308259
  return;
307703
- const includeAllFees = feesPayload.includeAllFees || feesPayload.includeFundingFees === true;
308260
+ if (normalizedCex !== "binance") {
308261
+ return ctx.wrappedCallback({
308262
+ code: grpc5.status.UNIMPLEMENTED,
308263
+ message: `InternalTransfer is only supported for Binance`
308264
+ }, null);
308265
+ }
308266
+ const pool = brokers[normalizedCex];
308267
+ if (!pool) {
308268
+ return ctx.wrappedCallback({
308269
+ code: grpc5.status.FAILED_PRECONDITION,
308270
+ message: `No broker accounts configured for ${normalizedCex}`
308271
+ }, null);
308272
+ }
308273
+ const fromSelector = transferPayload.fromAccount ?? getCurrentBrokerSelector(metadata);
308274
+ const toSelector = transferPayload.toAccount ?? "primary";
308275
+ const sourceAccount = resolveBrokerAccount(pool, fromSelector);
308276
+ if (!sourceAccount) {
308277
+ return ctx.wrappedCallback({
308278
+ code: grpc5.status.INVALID_ARGUMENT,
308279
+ message: `Source account "${fromSelector}" is not configured`
308280
+ }, null);
308281
+ }
308282
+ const destAccount = resolveBrokerAccount(pool, toSelector);
308283
+ if (!destAccount) {
308284
+ return ctx.wrappedCallback({
308285
+ code: grpc5.status.INVALID_ARGUMENT,
308286
+ message: `Destination account "${toSelector}" is not configured`
308287
+ }, null);
308288
+ }
307704
308289
  try {
307705
- await broker.loadMarkets();
307706
- const fetchFundingFees = async (currencyCodes) => {
307707
- let fundingFeeSource2 = "unavailable";
307708
- const fundingFeesByCurrency2 = {};
307709
- if (broker.has.fetchDepositWithdrawFees) {
307710
- try {
307711
- const feeMap = await broker.fetchDepositWithdrawFees(currencyCodes);
307712
- for (const code of currencyCodes) {
307713
- const feeInfo = feeMap[code];
307714
- if (!feeInfo) {
307715
- continue;
307716
- }
307717
- const fallbackFee = feeInfo.fee !== undefined || feeInfo.percentage !== undefined ? {
307718
- fee: feeInfo.fee ?? null,
307719
- percentage: feeInfo.percentage ?? null
307720
- } : null;
307721
- fundingFeesByCurrency2[code] = {
307722
- deposit: feeInfo.deposit ?? fallbackFee,
307723
- withdraw: feeInfo.withdraw ?? fallbackFee,
307724
- networks: feeInfo.networks ?? {}
307725
- };
307726
- }
307727
- if (Object.keys(fundingFeesByCurrency2).length > 0) {
307728
- fundingFeeSource2 = "fetchDepositWithdrawFees";
307729
- }
307730
- } catch (error48) {
307731
- safeLogError(`Error fetching deposit/withdraw fee map for ${symbol2} from ${cex3}`, error48);
307732
- }
307733
- }
307734
- if (fundingFeeSource2 === "unavailable") {
307735
- try {
307736
- const currencies = await broker.fetchCurrencies();
307737
- for (const code of currencyCodes) {
307738
- const currency = currencies[code];
307739
- if (!currency) {
307740
- continue;
307741
- }
307742
- fundingFeesByCurrency2[code] = {
307743
- deposit: {
307744
- enabled: currency.deposit ?? null
307745
- },
307746
- withdraw: {
307747
- enabled: currency.withdraw ?? null,
307748
- fee: currency.fee ?? null,
307749
- limits: currency.limits?.withdraw ?? null
307750
- },
307751
- networks: currency.networks ?? {}
307752
- };
307753
- }
307754
- if (Object.keys(fundingFeesByCurrency2).length > 0) {
307755
- fundingFeeSource2 = "currencies";
307756
- }
307757
- } catch (error48) {
307758
- safeLogError(`Error fetching currency metadata for fees for ${symbol2} from ${cex3}`, error48);
307759
- }
307760
- }
307761
- return { fundingFeeSource: fundingFeeSource2, fundingFeesByCurrency: fundingFeesByCurrency2 };
307762
- };
307763
- const isMarketSymbol = symbol2.includes("/");
307764
- if (isMarketSymbol) {
307765
- const market = await broker.market(symbol2);
307766
- const generalFee = broker.fees ?? null;
307767
- const feeStatus = broker.fees ? "available" : "unknown";
307768
- if (!broker.fees) {
307769
- log.warn(`Fee metadata unavailable for ${cex3}`, { symbol: symbol2 });
307770
- }
307771
- if (!includeAllFees) {
307772
- return ctx.wrappedCallback(null, {
307773
- proof: ctx.verity.proof,
307774
- result: JSON.stringify({
307775
- feeScope: "market",
307776
- generalFee,
307777
- feeStatus,
307778
- market
307779
- })
307780
- });
307781
- }
307782
- const currencyCodes = Array.from(new Set([market.base, market.quote]));
307783
- const { fundingFeeSource: fundingFeeSource2, fundingFeesByCurrency: fundingFeesByCurrency2 } = await fetchFundingFees(currencyCodes);
307784
- return ctx.wrappedCallback(null, {
307785
- proof: ctx.verity.proof,
307786
- result: JSON.stringify({
307787
- feeScope: "market+funding",
307788
- generalFee,
307789
- feeStatus,
307790
- market,
307791
- fundingFeeSource: fundingFeeSource2,
307792
- fundingFeesByCurrency: fundingFeesByCurrency2
307793
- })
307794
- });
308290
+ if (useVerity) {
308291
+ sourceAccount.exchange.setHttpClientOverride(buildHttpClientOverrideFromMetadata(metadata, verityProverUrl, (proof, notaryPubKey) => {
308292
+ verity.proof = proof;
308293
+ log.debug(`Verity proof:`, { proof, notaryPubKey });
308294
+ }), verityHttpClientOverridePredicate);
307795
308295
  }
307796
- const tokenCode = symbol2.toUpperCase();
307797
- const { fundingFeeSource, fundingFeesByCurrency } = await fetchFundingFees([
307798
- tokenCode
307799
- ]);
307800
- return ctx.wrappedCallback(null, {
307801
- proof: ctx.verity.proof,
307802
- result: JSON.stringify({
307803
- feeScope: "token",
307804
- symbol: tokenCode,
307805
- fundingFeeSource,
307806
- fundingFeesByCurrency
307807
- })
308296
+ const result = await transferBinanceInternal(sourceAccount, destAccount, symbol2, transferPayload.amount);
308297
+ archiveTransferEventInBackground(brokerArchiver, {
308298
+ exchange: normalizedCex,
308299
+ accountSelector: fromSelector,
308300
+ assetSymbol: symbol2,
308301
+ transfer: {
308302
+ eventKind: "internal_transfer",
308303
+ lifecycleAction: "submit_internal_transfer",
308304
+ status: "ok",
308305
+ amount: String(transferPayload.amount),
308306
+ network: "internal",
308307
+ externalId: extractBinanceInternalTransferId(result),
308308
+ payload: { from: fromSelector, to: toSelector, result }
308309
+ }
308310
+ });
308311
+ ctx.wrappedCallback(null, {
308312
+ proof: verity.proof,
308313
+ result: JSON.stringify(result)
307808
308314
  });
307809
308315
  } catch (error48) {
307810
- safeLogError(`Error fetching fees for ${symbol2} from ${cex3}`, error48);
308316
+ safeLogError("InternalTransfer failed", error48);
308317
+ if (error48 instanceof BrokerAccountPreconditionError) {
308318
+ return ctx.wrappedCallback({
308319
+ code: grpc5.status.FAILED_PRECONDITION,
308320
+ message: getErrorMessage(error48)
308321
+ }, null);
308322
+ }
308323
+ const msg = getErrorMessage(error48);
308324
+ let code;
308325
+ if (msg.includes("Unsupported transfer direction")) {
308326
+ code = grpc5.status.INVALID_ARGUMENT;
308327
+ } else if (msg.includes("unavailable in this CCXT build")) {
308328
+ code = grpc5.status.UNIMPLEMENTED;
308329
+ } else {
308330
+ code = mapCcxtErrorToGrpcStatus(error48) ?? grpc5.status.INTERNAL;
308331
+ }
307811
308332
  ctx.wrappedCallback({
307812
- code: grpc7.status.INTERNAL,
307813
- message: `Error fetching fees from ${cex3}`
308333
+ code,
308334
+ message: `InternalTransfer failed: ${sanitizeErrorDetail(error48)}`
307814
308335
  }, null);
307815
308336
  }
307816
308337
  }
307817
- async function handleFetchDepositAddresses(ctx) {
308338
+
308339
+ // src/handlers/execute-action/orders.ts
308340
+ import * as grpc6 from "@grpc/grpc-js";
308341
+
308342
+ // src/helpers/passive-order.ts
308343
+ var PASSIVE_ORDER_ERROR_CODES = {
308344
+ unsupported: "passive_order_unsupported",
308345
+ rejected: "passive_order_rejected",
308346
+ wouldCross: "passive_order_would_cross"
308347
+ };
308348
+ function identifiesWouldCross(message) {
308349
+ const normalized = message.toLowerCase();
308350
+ 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);
308351
+ }
308352
+ function identifiesUnsupported(message) {
308353
+ const normalized = message.toLowerCase();
308354
+ 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);
308355
+ }
308356
+ function classifyPassiveOrderError(error48) {
308357
+ if (error48 instanceof ccxt_default.InsufficientFunds) {
308358
+ return "InsufficientFunds";
308359
+ }
308360
+ if (error48 instanceof ccxt_default.AuthenticationError) {
308361
+ return "AuthenticationError";
308362
+ }
308363
+ const message = getErrorMessage(error48);
308364
+ if (error48 instanceof ccxt_default.OrderImmediatelyFillable || identifiesWouldCross(message)) {
308365
+ return PASSIVE_ORDER_ERROR_CODES.wouldCross;
308366
+ }
308367
+ if (error48 instanceof ccxt_default.NotSupported || identifiesUnsupported(message)) {
308368
+ return PASSIVE_ORDER_ERROR_CODES.unsupported;
308369
+ }
308370
+ return PASSIVE_ORDER_ERROR_CODES.rejected;
308371
+ }
308372
+
308373
+ // src/handlers/execute-action/orders.ts
308374
+ async function handleCreateOrder(ctx) {
307818
308375
  const {
307819
308376
  call,
307820
308377
  wrappedCallback,
@@ -307830,70 +308387,193 @@ async function handleFetchDepositAddresses(ctx) {
307830
308387
  applyVerityToBroker,
307831
308388
  useVerity,
307832
308389
  verityProverUrl,
307833
- otelMetrics
308390
+ otelMetrics,
308391
+ brokerArchiver,
308392
+ orderActivityTracker
307834
308393
  } = ctx;
307835
308394
  const verityProof = verity.proof;
307836
- if (!symbol2) {
307837
- return ctx.wrappedCallback({
307838
- code: grpc7.status.INVALID_ARGUMENT,
307839
- message: `ValidationError: Symbol required`
307840
- }, null);
307841
- }
307842
- const fetchDepositAddresses = parsePayloadForAction(ctx, FetchDepositAddressesPayloadSchema);
307843
- if (fetchDepositAddresses === null)
308395
+ const orderValue = parsePayloadForAction(ctx, CreateOrderPayloadSchema);
308396
+ if (orderValue === null)
307844
308397
  return;
307845
- let depositNetwork;
307846
- try {
307847
- depositNetwork = await resolveTransferNetwork(broker, symbol2, fetchDepositAddresses.chain);
307848
- } catch (error48) {
307849
- const message = getErrorMessage(error48);
307850
- return ctx.wrappedCallback({
307851
- code: stableGrpcErrorCode(message) ?? grpc7.status.INVALID_ARGUMENT,
307852
- message
307853
- }, null);
307854
- }
307855
- const depositValidation = validateDeposit(policy, cex3, depositNetwork.brokerNetworkId, symbol2);
307856
- if (!depositValidation.valid) {
308398
+ const isPassiveOrder = orderValue.orderIntent === "passive_only";
308399
+ if (isPassiveOrder && orderValue.orderType !== "limit") {
307857
308400
  return ctx.wrappedCallback({
307858
- code: grpc7.status.PERMISSION_DENIED,
307859
- message: `policy_deposit_denied: ${depositValidation.error}`
308401
+ code: grpc6.status.INVALID_ARGUMENT,
308402
+ message: "ValidationError: passive_only order intent requires a limit order"
307860
308403
  }, null);
307861
308404
  }
308405
+ const createOrderParams = {
308406
+ ...orderValue.params,
308407
+ ...orderValue.clientOrderId !== undefined && {
308408
+ clientOrderId: orderValue.clientOrderId
308409
+ },
308410
+ ...isPassiveOrder && { postOnly: true }
308411
+ };
308412
+ let resolvedOrderTelemetry = {};
308413
+ let marketMetadataHash;
308414
+ let submission = "not_attempted";
307862
308415
  try {
307863
- const depositAddresses = broker.has.fetchDepositAddress === true ? [
307864
- await broker.fetchDepositAddress(symbol2, {
307865
- network: depositNetwork.exchangeNetworkId,
307866
- ...fetchDepositAddresses.params ?? {}
308416
+ if (!broker) {
308417
+ return ctx.wrappedCallback({
308418
+ code: grpc6.status.INVALID_ARGUMENT,
308419
+ message: `Invalid CEX key: ${cex3}. Supported keys: ${Object.keys(brokers).join(", ")}`
308420
+ }, null);
308421
+ }
308422
+ const resolution = await resolveOrderExecution(policy, broker, cex3, orderValue.fromToken, orderValue.toToken, orderValue.amount, orderValue.price, orderValue.marketType);
308423
+ if (!resolution.valid || !resolution.symbol || !resolution.side) {
308424
+ return ctx.wrappedCallback({
308425
+ code: grpc6.status.INVALID_ARGUMENT,
308426
+ message: resolution.error ?? "Order rejected by policy: market or limits not satisfied"
308427
+ }, null);
308428
+ }
308429
+ resolvedOrderTelemetry = {
308430
+ symbol: resolution.symbol,
308431
+ side: resolution.side,
308432
+ requestedQuantity: resolution.amountBase ?? orderValue.amount
308433
+ };
308434
+ if (selectedBrokerAccount?.label) {
308435
+ orderActivityTracker?.record(cex3, selectedBrokerAccount.label, resolution.symbol);
308436
+ }
308437
+ const telemetryIds = extractOrderTelemetryIds(createOrderParams);
308438
+ const submissionTimestamp = new Date().toISOString();
308439
+ marketMetadataHash = await captureMarketMetadataSnapshot(brokerArchiver, broker, {
308440
+ exchange: cex3,
308441
+ accountSelector: selectedBrokerAccount?.label,
308442
+ symbol: resolution.symbol,
308443
+ action: "CreateOrder",
308444
+ brokerObservedTimestamp: submissionTimestamp,
308445
+ ...telemetryIds
308446
+ });
308447
+ submission = "in_flight";
308448
+ const order = await broker.createOrder(resolution.symbol, orderValue.orderType, resolution.side, resolution.amountBase ?? orderValue.amount, orderValue.price, createOrderParams);
308449
+ submission = "placed";
308450
+ const createOrderContext = {
308451
+ action: "CreateOrder",
308452
+ cex: cex3,
308453
+ accountLabel: selectedBrokerAccount?.label,
308454
+ symbol: resolvedOrderTelemetry.symbol,
308455
+ side: resolvedOrderTelemetry.side,
308456
+ orderType: orderValue.orderType,
308457
+ requestedQuantity: resolvedOrderTelemetry.requestedQuantity,
308458
+ requestedNotional: orderValue.amount * orderValue.price,
308459
+ orderAuthor: orderValue.orderAuthor,
308460
+ brokerObservedTimestamp: submissionTimestamp,
308461
+ ...telemetryIds
308462
+ };
308463
+ emitOrderExecutionTelemetryInBackground(otelMetrics, createOrderContext, order);
308464
+ archiveOrderExecutionInBackground(brokerArchiver, createOrderContext, order, undefined, { marketMetadataHash });
308465
+ ctx.wrappedCallback(null, {
308466
+ result: JSON.stringify({
308467
+ ...order,
308468
+ ...isPassiveOrder && {
308469
+ passivePlacementOutcome: "accepted_passive"
308470
+ }
307867
308471
  })
307868
- ] : await broker.fetchDepositAddressesByNetwork(symbol2, {
307869
- network: depositNetwork.exchangeNetworkId,
307870
- ...fetchDepositAddresses.params ?? {}
307871
308472
  });
307872
- if (depositAddresses.length > 0) {
307873
- return ctx.wrappedCallback(null, {
307874
- proof: ctx.verity.proof,
307875
- result: JSON.stringify(depositAddresses.map((depositAddress) => ({
307876
- ...depositAddress,
307877
- operatorAlias: depositNetwork.operatorAlias,
307878
- brokerNetworkId: depositNetwork.brokerNetworkId,
307879
- exchangeNetworkId: depositNetwork.exchangeNetworkId
307880
- })))
308473
+ } catch (error48) {
308474
+ rethrowArchiveDurabilityError(error48);
308475
+ safeLogRedactedError("Order Creation failed", error48);
308476
+ const failedCreateContext = {
308477
+ action: "CreateOrder",
308478
+ cex: cex3,
308479
+ accountLabel: selectedBrokerAccount?.label,
308480
+ symbol: resolvedOrderTelemetry.symbol ?? symbol2,
308481
+ side: resolvedOrderTelemetry.side,
308482
+ orderType: orderValue.orderType,
308483
+ requestedQuantity: resolvedOrderTelemetry.requestedQuantity ?? orderValue.amount,
308484
+ requestedNotional: orderValue.amount * orderValue.price,
308485
+ orderAuthor: orderValue.orderAuthor,
308486
+ ...extractOrderTelemetryIds(createOrderParams)
308487
+ };
308488
+ emitOrderExecutionTelemetryInBackground(otelMetrics, failedCreateContext, undefined, error48);
308489
+ archiveOrderExecutionInBackground(brokerArchiver, failedCreateContext, undefined, error48, { marketMetadataHash });
308490
+ if (isPassiveOrder && submission === "in_flight") {
308491
+ const stableErrorCode = classifyPassiveOrderError(error48);
308492
+ return rejectWithGrpcError(ctx, error48, {
308493
+ message: `${stableErrorCode}: ${sanitizeErrorDetail(error48)}`,
308494
+ preferStableMessageOnly: true
307881
308495
  });
307882
308496
  }
307883
308497
  ctx.wrappedCallback({
307884
- code: grpc7.status.INTERNAL,
307885
- message: "Deposit confirmation failed"
308498
+ code: grpc6.status.INTERNAL,
308499
+ message: `Order Creation failed: ${sanitizeErrorDetail(error48)}`
307886
308500
  }, null);
308501
+ }
308502
+ }
308503
+ async function handleGetOrderDetails(ctx) {
308504
+ const {
308505
+ call,
308506
+ wrappedCallback,
308507
+ policy,
308508
+ brokers,
308509
+ metadata,
308510
+ normalizedCex,
308511
+ cex: cex3,
308512
+ symbol: symbol2,
308513
+ selectedBrokerAccount,
308514
+ broker,
308515
+ verity,
308516
+ applyVerityToBroker,
308517
+ useVerity,
308518
+ verityProverUrl,
308519
+ otelMetrics,
308520
+ brokerArchiver,
308521
+ orderActivityTracker
308522
+ } = ctx;
308523
+ const verityProof = verity.proof;
308524
+ const getOrderValue = parsePayloadForAction(ctx, GetOrderDetailsPayloadSchema);
308525
+ if (getOrderValue === null)
308526
+ return;
308527
+ try {
308528
+ if (!broker) {
308529
+ return ctx.wrappedCallback({
308530
+ code: grpc6.status.INVALID_ARGUMENT,
308531
+ message: `Invalid CEX key: ${cex3}. Supported keys: ${Object.keys(brokers).join(", ")}`
308532
+ }, null);
308533
+ }
308534
+ const orderDetails = await broker.fetchOrder(getOrderValue.orderId, symbol2, { ...getOrderValue.params });
308535
+ if (selectedBrokerAccount?.label && symbol2) {
308536
+ orderActivityTracker?.record(cex3, selectedBrokerAccount.label, symbol2);
308537
+ }
308538
+ const getOrderContext = {
308539
+ action: "GetOrderDetails",
308540
+ cex: cex3,
308541
+ accountLabel: selectedBrokerAccount?.label,
308542
+ symbol: symbol2,
308543
+ ...extractOrderTelemetryIds(getOrderValue.params)
308544
+ };
308545
+ emitOrderExecutionTelemetryInBackground(otelMetrics, getOrderContext, orderDetails);
308546
+ archiveOrderExecutionInBackground(brokerArchiver, getOrderContext, orderDetails);
308547
+ ctx.wrappedCallback(null, {
308548
+ result: JSON.stringify({
308549
+ orderId: orderDetails.id,
308550
+ status: orderDetails.status,
308551
+ amount: orderDetails.amount,
308552
+ filled: orderDetails.filled,
308553
+ remaining: orderDetails.remaining,
308554
+ symbol: orderDetails.symbol,
308555
+ side: orderDetails.side,
308556
+ price: orderDetails.price
308557
+ })
308558
+ });
307887
308559
  } catch (error48) {
307888
- safeLogError("Fetch Deposit Addresses confirmation failed", error48);
307889
- const message = getErrorMessage(error48);
308560
+ safeLogError(`Error fetching order details from ${cex3}`, error48);
308561
+ const failedGetOrderContext = {
308562
+ action: "GetOrderDetails",
308563
+ cex: cex3,
308564
+ accountLabel: selectedBrokerAccount?.label,
308565
+ symbol: symbol2,
308566
+ ...extractOrderTelemetryIds(getOrderValue.params)
308567
+ };
308568
+ emitOrderExecutionTelemetryInBackground(otelMetrics, failedGetOrderContext, undefined, error48);
308569
+ archiveOrderExecutionInBackground(brokerArchiver, failedGetOrderContext, undefined, error48);
307890
308570
  ctx.wrappedCallback({
307891
- code: grpc7.status.INTERNAL,
307892
- message: "Fetch Deposit Addresses confirmation failed: " + message
308571
+ code: grpc6.status.INTERNAL,
308572
+ message: `Failed to fetch order details from ${cex3}: ${sanitizeErrorDetail(error48)}`
307893
308573
  }, null);
307894
308574
  }
307895
308575
  }
307896
- async function handleFetchBalances(ctx) {
308576
+ async function handleCancelOrder(ctx) {
307897
308577
  const {
307898
308578
  call,
307899
308579
  wrappedCallback,
@@ -307909,63 +308589,66 @@ async function handleFetchBalances(ctx) {
307909
308589
  applyVerityToBroker,
307910
308590
  useVerity,
307911
308591
  verityProverUrl,
307912
- otelMetrics
308592
+ otelMetrics,
308593
+ brokerArchiver,
308594
+ orderActivityTracker
307913
308595
  } = ctx;
307914
308596
  const verityProof = verity.proof;
308597
+ const cancelOrderValue = parsePayloadForAction(ctx, CancelOrderPayloadSchema);
308598
+ if (cancelOrderValue === null)
308599
+ return;
307915
308600
  try {
307916
- const payload = call.request.payload || {};
307917
- const providedBalanceType = payload.balanceType;
307918
- const balanceType = (providedBalanceType ?? "total").toString();
307919
- const validBalanceTypes = new Set(["free", "used", "total"]);
307920
- if (!validBalanceTypes.has(balanceType)) {
308601
+ if (!broker) {
307921
308602
  return ctx.wrappedCallback({
307922
- code: grpc7.status.INVALID_ARGUMENT,
307923
- message: `ValidationError: invalid balanceType '${providedBalanceType}'. Expected one of: free | used | total`
308603
+ code: grpc6.status.INVALID_ARGUMENT,
308604
+ message: `Invalid CEX key: ${cex3}. Supported keys: ${Object.keys(brokers).join(", ")}`
307924
308605
  }, null);
307925
308606
  }
307926
- const params = { ...payload };
307927
- delete params.balanceType;
307928
- const marketType = parseMarketType(params.marketType);
307929
- delete params.marketType;
307930
- if (params.type === undefined) {
307931
- params.type = marketTypeToCcxtType(marketType);
307932
- }
307933
- let responseBalances = {};
307934
- if (balanceType === "free") {
307935
- const partial2 = await broker.fetchFreeBalance(params);
307936
- responseBalances = partial2 ?? {};
307937
- } else if (balanceType === "used") {
307938
- const partial2 = await broker.fetchUsedBalance(params);
307939
- responseBalances = partial2 ?? {};
307940
- } else if (balanceType === "total") {
307941
- const partial2 = await broker.fetchTotalBalance(params);
307942
- responseBalances = partial2 ?? {};
307943
- }
307944
- if (symbol2) {
307945
- if (typeof responseBalances[symbol2] === "number") {
307946
- responseBalances = {
307947
- [symbol2]: responseBalances[symbol2] ?? 0
307948
- };
307949
- } else {
307950
- responseBalances = {};
307951
- }
308607
+ const cancelOrderContext = {
308608
+ action: "CancelOrder",
308609
+ cex: cex3,
308610
+ accountLabel: selectedBrokerAccount?.label,
308611
+ symbol: symbol2,
308612
+ ...extractOrderTelemetryIds(cancelOrderValue.params)
308613
+ };
308614
+ const cancelledOrder = await broker.cancelOrder(cancelOrderValue.orderId, symbol2, cancelOrderValue.params ?? {});
308615
+ if (selectedBrokerAccount?.label && symbol2) {
308616
+ orderActivityTracker?.record(cex3, selectedBrokerAccount.label, symbol2);
307952
308617
  }
308618
+ emitOrderExecutionTelemetryInBackground(otelMetrics, cancelOrderContext, cancelledOrder);
308619
+ archiveOrderExecutionInBackground(brokerArchiver, cancelOrderContext, cancelledOrder);
307953
308620
  ctx.wrappedCallback(null, {
307954
- proof: ctx.verity.proof,
307955
- result: JSON.stringify({
307956
- balances: responseBalances,
307957
- balanceType
307958
- })
308621
+ result: JSON.stringify({ ...cancelledOrder })
307959
308622
  });
307960
308623
  } catch (error48) {
307961
- safeLogError(`Error fetching balance from ${cex3}`, error48);
308624
+ safeLogError(`Error cancelling order from ${cex3}`, error48);
308625
+ const failedCancelContext = {
308626
+ action: "CancelOrder",
308627
+ cex: cex3,
308628
+ accountLabel: selectedBrokerAccount?.label,
308629
+ symbol: symbol2,
308630
+ ...extractOrderTelemetryIds(cancelOrderValue.params)
308631
+ };
308632
+ emitOrderExecutionTelemetryInBackground(otelMetrics, failedCancelContext, undefined, error48);
308633
+ archiveOrderExecutionInBackground(brokerArchiver, failedCancelContext, undefined, error48);
307962
308634
  ctx.wrappedCallback({
307963
- code: grpc7.status.INTERNAL,
307964
- message: `Failed to fetch balance from ${cex3}`
308635
+ code: grpc6.status.INTERNAL,
308636
+ message: `Failed to cancel order from ${cex3}: ${sanitizeErrorDetail(error48)}`
307965
308637
  }, null);
307966
308638
  }
307967
308639
  }
307968
- async function handleFetchTicker(ctx) {
308640
+ async function handleOrders(ctx) {
308641
+ if (ctx.action === Action.CreateOrder)
308642
+ return handleCreateOrder(ctx);
308643
+ if (ctx.action === Action.GetOrderDetails)
308644
+ return handleGetOrderDetails(ctx);
308645
+ if (ctx.action === Action.CancelOrder)
308646
+ return handleCancelOrder(ctx);
308647
+ }
308648
+
308649
+ // src/handlers/execute-action/pass-through.ts
308650
+ import * as grpc7 from "@grpc/grpc-js";
308651
+ async function handleFetchCurrency(ctx) {
307969
308652
  const {
307970
308653
  call,
307971
308654
  wrappedCallback,
@@ -307991,241 +308674,213 @@ async function handleFetchTicker(ctx) {
307991
308674
  }, null);
307992
308675
  }
307993
308676
  try {
307994
- const ticker = await broker.fetchTicker(symbol2);
308677
+ const assetCode = symbol2.trim().toUpperCase();
308678
+ const currencyInfo = await fetchCurrencyMetadata(broker, assetCode);
308679
+ if (!currencyInfo) {
308680
+ return ctx.wrappedCallback({
308681
+ code: grpc7.status.NOT_FOUND,
308682
+ message: `venue_discovery_unavailable: currency not found for ${assetCode}`
308683
+ }, null);
308684
+ }
308685
+ const networkEvidence = buildTransferNetworkEvidence(currencyInfo);
307995
308686
  ctx.wrappedCallback(null, {
307996
308687
  proof: ctx.verity.proof,
307997
- result: JSON.stringify(ticker)
307998
- });
307999
- } catch (error48) {
308000
- safeLogError(`Error fetching ticker from ${cex3}`, error48);
308001
- ctx.wrappedCallback({
308002
- code: grpc7.status.INTERNAL,
308003
- message: `Failed to fetch ticker from ${cex3}`
308004
- }, null);
308005
- }
308006
- }
308007
- async function handlePassThrough(ctx) {
308008
- if (ctx.action === Action.FetchCurrency)
308009
- return handleFetchCurrency(ctx);
308010
- if (ctx.action === Action.FetchAccountId)
308011
- return handleFetchAccountId(ctx);
308012
- if (ctx.action === Action.FetchFees)
308013
- return handleFetchFees(ctx);
308014
- if (ctx.action === Action.FetchDepositAddresses)
308015
- return handleFetchDepositAddresses(ctx);
308016
- if (ctx.action === Action.FetchBalances)
308017
- return handleFetchBalances(ctx);
308018
- if (ctx.action === Action.FetchTicker)
308019
- return handleFetchTicker(ctx);
308020
- }
308021
-
308022
- // src/handlers/execute-action/perp-config.ts
308023
- import * as grpc8 from "@grpc/grpc-js";
308024
- function exchangeSupports(broker, capability) {
308025
- return broker.has?.[capability] === true;
308026
- }
308027
- function extractPerpConfigs(positions) {
308028
- return positions.map((position) => ({
308029
- symbol: typeof position.symbol === "string" ? position.symbol : undefined,
308030
- leverage: typeof position.leverage === "number" ? position.leverage : undefined,
308031
- marginMode: typeof position.marginMode === "string" ? position.marginMode : undefined
308032
- }));
308033
- }
308034
- async function handleGetPerpConfigState(ctx) {
308035
- const { wrappedCallback, cex: cex3, normalizedCex, broker } = ctx;
308036
- const payload = parsePayloadForAction(ctx, GetPerpConfigStatePayloadSchema);
308037
- if (payload === null) {
308038
- return;
308039
- }
308040
- if (!broker) {
308041
- return wrappedCallback({
308042
- code: grpc8.status.INVALID_ARGUMENT,
308043
- message: `Invalid CEX key: ${cex3}`
308044
- }, null);
308045
- }
308046
- const exchange = broker;
308047
- if (!exchangeSupports(exchange, "fetchPositions")) {
308048
- return wrappedCallback({
308049
- code: grpc8.status.UNIMPLEMENTED,
308050
- message: `${normalizedCex} does not support fetchPositions`
308051
- }, null);
308052
- }
308053
- try {
308054
- const symbols = payload.symbol ? [payload.symbol] : undefined;
308055
- const positions = await exchange.fetchPositions?.(symbols, payload.params);
308056
- ctx.wrappedCallback(null, {
308057
308688
  result: JSON.stringify({
308689
+ ...currencyInfo,
308058
308690
  exchange: normalizedCex,
308059
- configs: extractPerpConfigs(positions ?? []),
308060
- positions: positions ?? []
308691
+ asset: assetCode,
308692
+ code: currencyInfo.code ?? assetCode,
308693
+ id: currencyInfo.id ?? null,
308694
+ networks: networkEvidence.networks,
308695
+ networkAliases: networkEvidence.aliases,
308696
+ raw: currencyInfo
308061
308697
  })
308062
308698
  });
308063
308699
  } catch (error48) {
308064
- safeLogError(`GetPerpConfigState failed for ${cex3}`, error48);
308700
+ safeLogError(`Error fetching currency ${symbol2} from ${cex3}`, error48);
308701
+ const message = getErrorMessage(error48);
308065
308702
  ctx.wrappedCallback({
308066
- code: grpc8.status.INTERNAL,
308067
- message: `GetPerpConfigState failed: ${sanitizeErrorDetail(error48)}`
308703
+ code: stableGrpcErrorCode(message) ?? mapCcxtErrorToGrpcStatus(error48) ?? grpc7.status.INTERNAL,
308704
+ message: message.startsWith("venue_discovery_unavailable:") ? message : `venue_discovery_unavailable: ${message}`
308068
308705
  }, null);
308069
308706
  }
308070
308707
  }
308071
- async function handleSetPerpConfigState(ctx) {
308072
- const { wrappedCallback, cex: cex3, normalizedCex, broker } = ctx;
308073
- const payload = parsePayloadForAction(ctx, SetPerpConfigStatePayloadSchema);
308074
- if (payload === null) {
308075
- return;
308076
- }
308077
- if (!broker) {
308078
- return wrappedCallback({
308079
- code: grpc8.status.INVALID_ARGUMENT,
308080
- message: `Invalid CEX key: ${cex3}`
308081
- }, null);
308082
- }
308083
- const exchange = broker;
308084
- if (!exchangeSupports(exchange, "setLeverage")) {
308085
- return wrappedCallback({
308086
- code: grpc8.status.UNIMPLEMENTED,
308087
- message: `${normalizedCex} does not support setLeverage`
308088
- }, null);
308089
- }
308708
+ async function handleFetchAccountId(ctx) {
308709
+ const {
308710
+ call,
308711
+ wrappedCallback,
308712
+ policy,
308713
+ brokers,
308714
+ metadata,
308715
+ normalizedCex,
308716
+ cex: cex3,
308717
+ symbol: symbol2,
308718
+ selectedBrokerAccount,
308719
+ broker,
308720
+ verity,
308721
+ applyVerityToBroker,
308722
+ useVerity,
308723
+ verityProverUrl,
308724
+ otelMetrics
308725
+ } = ctx;
308726
+ const verityProof = verity.proof;
308090
308727
  try {
308091
- const response = await exchange.setLeverage?.(payload.leverage, payload.symbol, {
308092
- marginMode: payload.marginMode ?? "cross",
308093
- ...payload.params
308094
- });
308095
- ctx.wrappedCallback(null, {
308096
- result: JSON.stringify({
308097
- exchange: normalizedCex,
308098
- symbol: payload.symbol,
308099
- leverage: payload.leverage,
308100
- marginMode: payload.marginMode ?? "cross",
308101
- response
308102
- })
308728
+ const accountId = await broker.fetchAccountId();
308729
+ return ctx.wrappedCallback(null, {
308730
+ proof: ctx.verity.proof,
308731
+ result: JSON.stringify({ accountId })
308103
308732
  });
308104
308733
  } catch (error48) {
308105
- safeLogError(`SetPerpConfigState failed for ${cex3}`, error48);
308734
+ safeLogError(`Error fetching account ID ${cex3}`, error48);
308106
308735
  ctx.wrappedCallback({
308107
- code: grpc8.status.INTERNAL,
308108
- message: `SetPerpConfigState failed: ${sanitizeErrorDetail(error48)}`
308736
+ code: grpc7.status.INTERNAL,
308737
+ message: `Error fetching account ID from ${cex3}`
308109
308738
  }, null);
308110
308739
  }
308111
308740
  }
308112
- async function handlePerpConfig(ctx) {
308113
- if (ctx.action === Action.GetPerpConfigState) {
308114
- return handleGetPerpConfigState(ctx);
308115
- }
308116
- if (ctx.action === Action.SetPerpConfigState) {
308117
- return handleSetPerpConfigState(ctx);
308741
+ async function handleFetchFees(ctx) {
308742
+ const {
308743
+ call,
308744
+ wrappedCallback,
308745
+ policy,
308746
+ brokers,
308747
+ metadata,
308748
+ normalizedCex,
308749
+ cex: cex3,
308750
+ symbol: symbol2,
308751
+ selectedBrokerAccount,
308752
+ broker,
308753
+ verity,
308754
+ applyVerityToBroker,
308755
+ useVerity,
308756
+ verityProverUrl,
308757
+ otelMetrics
308758
+ } = ctx;
308759
+ const verityProof = verity.proof;
308760
+ if (!symbol2) {
308761
+ return ctx.wrappedCallback({
308762
+ code: grpc7.status.INVALID_ARGUMENT,
308763
+ message: `ValidationError: Symbol required`
308764
+ }, null);
308118
308765
  }
308119
- }
308120
-
308121
- // src/handlers/execute-action/treasury-call.ts
308122
- import * as grpc9 from "@grpc/grpc-js";
308123
- async function handleTreasuryCall(ctx) {
308124
- const { broker } = ctx;
308125
- const callValue = parsePayloadForAction(ctx, CallPayloadSchema);
308126
- if (callValue === null)
308766
+ const feesPayload = parsePayloadForAction(ctx, FetchFeesPayloadSchema);
308767
+ if (feesPayload === null)
308127
308768
  return;
308128
- let createOrderContext;
308129
- let marketMetadataHash;
308769
+ const includeAllFees = feesPayload.includeAllFees || feesPayload.includeFundingFees === true;
308130
308770
  try {
308131
- if (callValue.functionName.startsWith("_") || callValue.functionName.includes("constructor") || callValue.functionName.includes("prototype")) {
308132
- return ctx.wrappedCallback({
308133
- code: grpc9.status.PERMISSION_DENIED,
308134
- message: "Access to the requested function is denied"
308135
- }, null);
308136
- }
308137
- const argsArray = callArgs(callValue.args, callValue.params ?? {});
308138
- const treasuryDiscovery = await handleTreasuryDiscoveryCall(broker, callValue.functionName, callValue.args, callValue.params ?? {});
308139
- if (treasuryDiscovery.handled) {
308140
- return ctx.wrappedCallback(null, {
308141
- proof: ctx.verity.proof,
308142
- result: JSON.stringify(treasuryDiscovery.result)
308143
- });
308144
- }
308145
- const fn = broker[callValue.functionName];
308146
- if (typeof fn !== "function" || broker.has?.[callValue.functionName] === false) {
308147
- return ctx.wrappedCallback({
308148
- code: grpc9.status.INVALID_ARGUMENT,
308149
- message: `Function not found on broker: ${callValue.functionName}`
308150
- }, null);
308151
- }
308152
- if (callValue.functionName === "createOrder") {
308153
- const [symbol2, orderType, side, quantity, price] = callValue.args;
308154
- const requestedQuantity = asFiniteNumber(quantity);
308155
- const requestedPrice = asFiniteNumber(price);
308156
- const requestedNotional = requestedQuantity !== undefined && requestedPrice !== undefined ? asFiniteNumber(requestedQuantity * requestedPrice) : undefined;
308157
- const telemetryIds = extractOrderTelemetryIds(callValue.params);
308158
- const submissionTimestamp = new Date().toISOString();
308159
- createOrderContext = {
308160
- action: "CreateOrder",
308161
- cex: ctx.cex,
308162
- accountLabel: ctx.selectedBrokerAccount?.label,
308163
- symbol: asNonEmptyString(symbol2),
308164
- orderType: asNonEmptyString(orderType),
308165
- side: asNonEmptyString(side),
308166
- requestedQuantity,
308167
- requestedNotional,
308168
- orderAuthor: callValue.orderAuthor,
308169
- brokerObservedTimestamp: submissionTimestamp,
308170
- ...telemetryIds
308171
- };
308172
- if (createOrderContext.symbol !== undefined) {
308173
- marketMetadataHash = await captureMarketMetadataSnapshot(ctx.brokerArchiver, broker, {
308174
- exchange: ctx.cex,
308175
- accountSelector: ctx.selectedBrokerAccount?.label,
308176
- symbol: createOrderContext.symbol,
308177
- action: "CreateOrder",
308178
- brokerObservedTimestamp: submissionTimestamp,
308179
- ...telemetryIds
308771
+ await broker.loadMarkets();
308772
+ const fetchFundingFees = async (currencyCodes) => {
308773
+ let fundingFeeSource2 = "unavailable";
308774
+ const fundingFeesByCurrency2 = {};
308775
+ if (broker.has.fetchDepositWithdrawFees) {
308776
+ try {
308777
+ const feeMap = await broker.fetchDepositWithdrawFees(currencyCodes);
308778
+ for (const code of currencyCodes) {
308779
+ const feeInfo = feeMap[code];
308780
+ if (!feeInfo) {
308781
+ continue;
308782
+ }
308783
+ const fallbackFee = feeInfo.fee !== undefined || feeInfo.percentage !== undefined ? {
308784
+ fee: feeInfo.fee ?? null,
308785
+ percentage: feeInfo.percentage ?? null
308786
+ } : null;
308787
+ fundingFeesByCurrency2[code] = {
308788
+ deposit: feeInfo.deposit ?? fallbackFee,
308789
+ withdraw: feeInfo.withdraw ?? fallbackFee,
308790
+ networks: feeInfo.networks ?? {}
308791
+ };
308792
+ }
308793
+ if (Object.keys(fundingFeesByCurrency2).length > 0) {
308794
+ fundingFeeSource2 = "fetchDepositWithdrawFees";
308795
+ }
308796
+ } catch (error48) {
308797
+ safeLogError(`Error fetching deposit/withdraw fee map for ${symbol2} from ${cex3}`, error48);
308798
+ }
308799
+ }
308800
+ if (fundingFeeSource2 === "unavailable") {
308801
+ try {
308802
+ const currencies = await broker.fetchCurrencies();
308803
+ for (const code of currencyCodes) {
308804
+ const currency = currencies[code];
308805
+ if (!currency) {
308806
+ continue;
308807
+ }
308808
+ fundingFeesByCurrency2[code] = {
308809
+ deposit: {
308810
+ enabled: currency.deposit ?? null
308811
+ },
308812
+ withdraw: {
308813
+ enabled: currency.withdraw ?? null,
308814
+ fee: currency.fee ?? null,
308815
+ limits: currency.limits?.withdraw ?? null
308816
+ },
308817
+ networks: currency.networks ?? {}
308818
+ };
308819
+ }
308820
+ if (Object.keys(fundingFeesByCurrency2).length > 0) {
308821
+ fundingFeeSource2 = "currencies";
308822
+ }
308823
+ } catch (error48) {
308824
+ safeLogError(`Error fetching currency metadata for fees for ${symbol2} from ${cex3}`, error48);
308825
+ }
308826
+ }
308827
+ return { fundingFeeSource: fundingFeeSource2, fundingFeesByCurrency: fundingFeesByCurrency2 };
308828
+ };
308829
+ const isMarketSymbol = symbol2.includes("/");
308830
+ if (isMarketSymbol) {
308831
+ const market = await broker.market(symbol2);
308832
+ const generalFee = broker.fees ?? null;
308833
+ const feeStatus = broker.fees ? "available" : "unknown";
308834
+ if (!broker.fees) {
308835
+ log.warn(`Fee metadata unavailable for ${cex3}`, { symbol: symbol2 });
308836
+ }
308837
+ if (!includeAllFees) {
308838
+ return ctx.wrappedCallback(null, {
308839
+ proof: ctx.verity.proof,
308840
+ result: JSON.stringify({
308841
+ feeScope: "market",
308842
+ generalFee,
308843
+ feeStatus,
308844
+ market
308845
+ })
308180
308846
  });
308181
308847
  }
308182
- }
308183
- const result = await fn.apply(broker, argsArray);
308184
- if (createOrderContext !== undefined) {
308185
- emitOrderExecutionTelemetryInBackground(ctx.otelMetrics, createOrderContext, result);
308186
- archiveOrderExecutionInBackground(ctx.brokerArchiver, createOrderContext, result, undefined, { marketMetadataHash });
308187
- } else if (callValue.functionName === "fetchWithdrawals") {
308188
- archiveWithdrawalObservationsInBackground(ctx.brokerArchiver, ctx.withdrawalObservationTracker, {
308189
- exchange: ctx.normalizedCex,
308190
- accountSelector: ctx.selectedBrokerAccount?.label,
308191
- transactions: result
308848
+ const currencyCodes = Array.from(new Set([market.base, market.quote]));
308849
+ const { fundingFeeSource: fundingFeeSource2, fundingFeesByCurrency: fundingFeesByCurrency2 } = await fetchFundingFees(currencyCodes);
308850
+ return ctx.wrappedCallback(null, {
308851
+ proof: ctx.verity.proof,
308852
+ result: JSON.stringify({
308853
+ feeScope: "market+funding",
308854
+ generalFee,
308855
+ feeStatus,
308856
+ market,
308857
+ fundingFeeSource: fundingFeeSource2,
308858
+ fundingFeesByCurrency: fundingFeesByCurrency2
308859
+ })
308192
308860
  });
308193
308861
  }
308194
- ctx.wrappedCallback(null, {
308862
+ const tokenCode = symbol2.toUpperCase();
308863
+ const { fundingFeeSource, fundingFeesByCurrency } = await fetchFundingFees([
308864
+ tokenCode
308865
+ ]);
308866
+ return ctx.wrappedCallback(null, {
308195
308867
  proof: ctx.verity.proof,
308196
- result: JSON.stringify(result)
308868
+ result: JSON.stringify({
308869
+ feeScope: "token",
308870
+ symbol: tokenCode,
308871
+ fundingFeeSource,
308872
+ fundingFeesByCurrency
308873
+ })
308197
308874
  });
308198
308875
  } catch (error48) {
308199
- if (createOrderContext !== undefined) {
308200
- rethrowArchiveDurabilityError(error48);
308201
- emitOrderExecutionTelemetryInBackground(ctx.otelMetrics, createOrderContext, undefined, error48);
308202
- archiveOrderExecutionInBackground(ctx.brokerArchiver, createOrderContext, undefined, error48, { marketMetadataHash });
308203
- }
308204
- safeLogError("Call failed", error48);
308205
- rejectWithGrpcError(ctx, error48, {
308206
- message: getErrorMessage(error48),
308207
- preferStableMessageOnly: true,
308208
- appendClassName: true
308209
- });
308210
- }
308211
- }
308212
- function asNonEmptyString(value) {
308213
- return typeof value === "string" && value.trim() ? value : undefined;
308214
- }
308215
- function asFiniteNumber(value) {
308216
- if (typeof value === "number") {
308217
- return Number.isFinite(value) ? value : undefined;
308218
- }
308219
- if (typeof value !== "string" || !value.trim()) {
308220
- return;
308876
+ safeLogError(`Error fetching fees for ${symbol2} from ${cex3}`, error48);
308877
+ ctx.wrappedCallback({
308878
+ code: grpc7.status.INTERNAL,
308879
+ message: `Error fetching fees from ${cex3}`
308880
+ }, null);
308221
308881
  }
308222
- const parsed = Number(value);
308223
- return Number.isFinite(parsed) ? parsed : undefined;
308224
308882
  }
308225
-
308226
- // src/handlers/execute-action/withdraw.ts
308227
- import * as grpc10 from "@grpc/grpc-js";
308228
- async function handleWithdraw(ctx) {
308883
+ async function handleFetchDepositAddresses(ctx) {
308229
308884
  const {
308230
308885
  call,
308231
308886
  wrappedCallback,
@@ -308241,1183 +308896,998 @@ async function handleWithdraw(ctx) {
308241
308896
  applyVerityToBroker,
308242
308897
  useVerity,
308243
308898
  verityProverUrl,
308244
- otelMetrics,
308245
- brokerArchiver
308899
+ otelMetrics
308246
308900
  } = ctx;
308247
308901
  const verityProof = verity.proof;
308248
308902
  if (!symbol2) {
308249
308903
  return ctx.wrappedCallback({
308250
- code: grpc10.status.INVALID_ARGUMENT,
308904
+ code: grpc7.status.INVALID_ARGUMENT,
308251
308905
  message: `ValidationError: Symbol required`
308252
308906
  }, null);
308253
308907
  }
308254
- const transferValue = parsePayloadForAction(ctx, WithdrawPayloadSchema);
308255
- if (transferValue === null)
308908
+ const fetchDepositAddresses = parsePayloadForAction(ctx, FetchDepositAddressesPayloadSchema);
308909
+ if (fetchDepositAddresses === null)
308256
308910
  return;
308257
- let withdrawNetwork;
308911
+ let depositNetwork;
308258
308912
  try {
308259
- withdrawNetwork = await resolveTransferNetwork(broker, symbol2, transferValue.chain);
308913
+ depositNetwork = await resolveTransferNetwork(broker, symbol2, fetchDepositAddresses.chain);
308260
308914
  } catch (error48) {
308261
308915
  const message = getErrorMessage(error48);
308262
308916
  return ctx.wrappedCallback({
308263
- code: stableGrpcErrorCode(message) ?? grpc10.status.INVALID_ARGUMENT,
308917
+ code: stableGrpcErrorCode(message) ?? grpc7.status.INVALID_ARGUMENT,
308264
308918
  message
308265
308919
  }, null);
308266
308920
  }
308267
- const transferValidation = validateWithdraw(policy, cex3, withdrawNetwork.brokerNetworkId, transferValue.recipientAddress, transferValue.amount, symbol2);
308268
- if (!transferValidation.valid) {
308269
- return ctx.wrappedCallback({
308270
- code: grpc10.status.PERMISSION_DENIED,
308271
- message: `policy_withdrawal_denied: ${transferValidation.error}`
308272
- }, null);
308273
- }
308274
- const travelRule = resolveTravelRuleDecision(policy, cex3, transferValue.recipientAddress);
308275
- if (travelRule.mode === "denied") {
308921
+ const depositValidation = validateDeposit(policy, cex3, depositNetwork.brokerNetworkId, symbol2);
308922
+ if (!depositValidation.valid) {
308276
308923
  return ctx.wrappedCallback({
308277
- code: grpc10.status.FAILED_PRECONDITION,
308278
- message: `travel_rule_denied: ${travelRule.error}`
308924
+ code: grpc7.status.PERMISSION_DENIED,
308925
+ message: `policy_deposit_denied: ${depositValidation.error}`
308279
308926
  }, null);
308280
308927
  }
308281
- const withdrawOrderId = transferValue.params.withdrawOrderId;
308282
- const clientWithdrawalId = typeof withdrawOrderId === "string" && withdrawOrderId.length > 0 ? withdrawOrderId : undefined;
308283
308928
  try {
308284
- const transaction = travelRule.mode === "localentity" ? await withdrawViaLocalEntity(broker, {
308285
- code: symbol2,
308286
- amount: transferValue.amount,
308287
- address: transferValue.recipientAddress,
308288
- network: withdrawNetwork.exchangeNetworkId,
308289
- questionnaire: travelRule.questionnaire,
308290
- params: transferValue.params
308291
- }) : await broker.withdraw(symbol2, transferValue.amount, transferValue.recipientAddress, undefined, {
308292
- ...transferValue.params ?? {},
308293
- network: withdrawNetwork.exchangeNetworkId
308294
- });
308295
- log.info(`Withdraw Result: ${JSON.stringify(transaction)}`);
308296
- const normalized = normalizeCcxtTransactionForArchive(transaction);
308297
- archiveTransferEventInBackground(brokerArchiver, {
308298
- exchange: cex3,
308299
- accountSelector: selectedBrokerAccount?.label,
308300
- assetSymbol: normalized.assetSymbol ?? symbol2,
308301
- transfer: {
308302
- eventKind: "withdrawal",
308303
- lifecycleAction: "submit_withdrawal",
308304
- status: normalized.status,
308305
- amount: normalized.amount ?? String(transferValue.amount),
308306
- address: normalized.address ?? transferValue.recipientAddress,
308307
- network: normalized.network ?? withdrawNetwork.exchangeNetworkId,
308308
- externalId: normalized.externalId,
308309
- clientWithdrawalId,
308310
- txid: normalized.txid,
308311
- feeAmount: normalized.feeAmount,
308312
- feeCurrency: normalized.feeCurrency,
308313
- exchangeTimestamp: normalized.exchangeTimestamp,
308314
- payload: transaction
308315
- }
308316
- });
308317
- ctx.wrappedCallback(null, {
308318
- proof: ctx.verity.proof,
308319
- result: JSON.stringify({
308320
- ...transaction,
308321
- operatorAlias: withdrawNetwork.operatorAlias,
308322
- brokerNetworkId: withdrawNetwork.brokerNetworkId,
308323
- exchangeNetworkId: withdrawNetwork.exchangeNetworkId
308929
+ const depositAddresses = broker.has.fetchDepositAddress === true ? [
308930
+ await broker.fetchDepositAddress(symbol2, {
308931
+ network: depositNetwork.exchangeNetworkId,
308932
+ ...fetchDepositAddresses.params ?? {}
308324
308933
  })
308934
+ ] : await broker.fetchDepositAddressesByNetwork(symbol2, {
308935
+ network: depositNetwork.exchangeNetworkId,
308936
+ ...fetchDepositAddresses.params ?? {}
308325
308937
  });
308326
- } catch (error48) {
308327
- safeLogError("Withdraw failed", error48);
308328
- archiveTransferEventInBackground(brokerArchiver, {
308329
- exchange: cex3,
308330
- accountSelector: selectedBrokerAccount?.label,
308331
- assetSymbol: symbol2,
308332
- transfer: {
308333
- eventKind: "withdrawal",
308334
- lifecycleAction: "submit_withdrawal",
308335
- status: "failed",
308336
- amount: String(transferValue.amount),
308337
- address: transferValue.recipientAddress,
308338
- network: withdrawNetwork.exchangeNetworkId,
308339
- clientWithdrawalId,
308340
- errorSummary: getErrorMessage(error48),
308341
- payload: { recipientAddress: transferValue.recipientAddress }
308342
- }
308343
- });
308344
- const code = mapCcxtErrorToGrpcStatus(error48) ?? grpc10.status.INTERNAL;
308938
+ if (depositAddresses.length > 0) {
308939
+ return ctx.wrappedCallback(null, {
308940
+ proof: ctx.verity.proof,
308941
+ result: JSON.stringify(depositAddresses.map((depositAddress) => ({
308942
+ ...depositAddress,
308943
+ operatorAlias: depositNetwork.operatorAlias,
308944
+ brokerNetworkId: depositNetwork.brokerNetworkId,
308945
+ exchangeNetworkId: depositNetwork.exchangeNetworkId
308946
+ })))
308947
+ });
308948
+ }
308345
308949
  ctx.wrappedCallback({
308346
- code,
308347
- message: `Withdraw failed: ${sanitizeErrorDetail(error48)}`
308950
+ code: grpc7.status.INTERNAL,
308951
+ message: "Deposit confirmation failed"
308348
308952
  }, null);
308349
- }
308350
- }
308351
-
308352
- // src/handlers/execute-action/registry.ts
308353
- var ACTION_HANDLERS = {
308354
- [Action.Deposit]: handleDeposit,
308355
- [Action.Withdraw]: handleWithdraw,
308356
- [Action.Call]: handleTreasuryCall,
308357
- [Action.InternalTransfer]: handleInternalTransfer,
308358
- [Action.CreateOrder]: handleOrders,
308359
- [Action.GetOrderDetails]: handleOrders,
308360
- [Action.CancelOrder]: handleOrders,
308361
- [Action.FetchCurrency]: handlePassThrough,
308362
- [Action.FetchAccountId]: handlePassThrough,
308363
- [Action.FetchFees]: handlePassThrough,
308364
- [Action.FetchDepositAddresses]: handlePassThrough,
308365
- [Action.FetchBalances]: handlePassThrough,
308366
- [Action.FetchTicker]: handlePassThrough,
308367
- [Action.GetPerpConfigState]: handlePerpConfig,
308368
- [Action.SetPerpConfigState]: handlePerpConfig
308369
- };
308370
- async function dispatchExecuteAction(ctx) {
308371
- const handler = ACTION_HANDLERS[ctx.action];
308372
- if (!handler) {
308953
+ } catch (error48) {
308954
+ safeLogError("Fetch Deposit Addresses confirmation failed", error48);
308955
+ const message = getErrorMessage(error48);
308373
308956
  ctx.wrappedCallback({
308374
- code: grpc11.status.INVALID_ARGUMENT,
308375
- message: "Invalid Action"
308957
+ code: grpc7.status.INTERNAL,
308958
+ message: "Fetch Deposit Addresses confirmation failed: " + message
308376
308959
  }, null);
308377
- return;
308378
308960
  }
308379
- await handler(ctx);
308380
308961
  }
308381
-
308382
- // src/handlers/execute-action/handler.ts
308383
- function createExecuteActionHandler(deps) {
308962
+ async function handleFetchBalances(ctx) {
308384
308963
  const {
308964
+ call,
308965
+ wrappedCallback,
308385
308966
  policy,
308386
308967
  brokers,
308387
- whitelistIps,
308388
- useVerity,
308389
- verityProverUrl,
308390
- otelMetrics,
308391
- brokerArchiver,
308392
- orderActivityTracker
308393
- } = deps;
308394
- const withdrawalObservationTracker = deps.withdrawalObservationTracker ?? new WithdrawalObservationTracker;
308395
- return async (call, callback) => {
308396
- const startTime = Date.now();
308397
- const { action: rawAction, cex: cex3, symbol: symbol2 } = call.request;
308398
- const action = resolveAction(rawAction);
308399
- let actionCompleted = false;
308400
- const wrappedCallback = (error48, value) => {
308401
- if (!actionCompleted) {
308402
- actionCompleted = true;
308403
- const latency = Date.now() - startTime;
308404
- const actionName = getActionName(action);
308405
- otelMetrics?.recordHistogram("execute_action_duration_ms", latency, {
308406
- action: actionName,
308407
- cex: cex3 || "unknown"
308408
- });
308409
- if (error48) {
308410
- otelMetrics?.recordCounter("execute_action_errors_total", 1, {
308411
- action: actionName,
308412
- cex: cex3 || "unknown",
308413
- error_type: error48.code ? grpc12.status[error48.code] || "unknown" : "unknown"
308414
- });
308415
- } else {
308416
- otelMetrics?.recordCounter("execute_action_success_total", 1, {
308417
- action: actionName,
308418
- cex: cex3 || "unknown"
308419
- });
308420
- }
308421
- }
308422
- callback(error48, value);
308423
- };
308424
- try {
308425
- log.info(`Request - ExecuteAction:`, { action, cex: cex3, symbol: symbol2 });
308426
- otelMetrics?.recordCounter("execute_action_requests_total", 1, {
308427
- action: getActionName(action),
308428
- cex: cex3 || "unknown"
308429
- });
308430
- if (!authenticateRequest(call, whitelistIps)) {
308431
- return wrappedCallback({
308432
- code: grpc12.status.PERMISSION_DENIED,
308433
- message: "Access denied: Unauthorized IP"
308434
- }, null);
308435
- }
308436
- if (!action || !cex3) {
308437
- return wrappedCallback({
308438
- code: grpc12.status.INVALID_ARGUMENT,
308439
- message: "`action` AND `cex` fields are required"
308440
- }, null);
308441
- }
308442
- const normalizedCex = cex3.trim().toLowerCase();
308443
- const metadata = call.metadata;
308444
- const selectedBrokerAccount = selectBrokerAccountForCex(normalizedCex, brokers, metadata);
308445
- const verity = { proof: "" };
308446
- const applyVerityToBroker = (targetBroker) => {
308447
- if (!useVerity)
308448
- return;
308449
- const override = buildHttpClientOverrideFromMetadata(metadata, verityProverUrl, (proof, notaryPubKey) => {
308450
- verity.proof = proof;
308451
- log.debug(`Verity proof:`, { proof, notaryPubKey });
308452
- });
308453
- targetBroker.setHttpClientOverride(override, verityHttpClientOverridePredicate);
308454
- };
308455
- const preludeCtx = {
308456
- call,
308457
- wrappedCallback,
308458
- action,
308459
- policy,
308460
- brokers,
308461
- metadata,
308462
- normalizedCex,
308463
- cex: cex3,
308464
- symbol: symbol2,
308465
- selectedBrokerAccount,
308466
- broker: selectedBrokerAccount?.exchange ?? createBroker(normalizedCex, metadata),
308467
- verity,
308468
- applyVerityToBroker,
308469
- useVerity,
308470
- verityProverUrl,
308471
- otelMetrics,
308472
- brokerArchiver,
308473
- orderActivityTracker,
308474
- withdrawalObservationTracker
308475
- };
308476
- if (action === Action.Call) {
308477
- const handled = await handleOrderBookCall(preludeCtx);
308478
- if (handled)
308479
- return;
308480
- }
308481
- const broker = selectedBrokerAccount?.exchange ?? createBroker(normalizedCex, metadata);
308482
- if (!broker) {
308483
- return wrappedCallback({
308484
- code: grpc12.status.UNAUTHENTICATED,
308485
- message: `This Exchange is not registered and No API metadata was found`
308486
- }, null);
308487
- }
308488
- applyVerityToBroker(broker);
308489
- const ctx = { ...preludeCtx, broker };
308490
- await dispatchExecuteAction(ctx);
308491
- } catch (error48) {
308492
- safeLogError("ExecuteAction unhandled error", error48);
308493
- return wrappedCallback({
308494
- code: grpc12.status.INTERNAL,
308495
- message: "ExecuteAction failed unexpectedly"
308968
+ metadata,
308969
+ normalizedCex,
308970
+ cex: cex3,
308971
+ symbol: symbol2,
308972
+ selectedBrokerAccount,
308973
+ broker,
308974
+ verity,
308975
+ applyVerityToBroker,
308976
+ useVerity,
308977
+ verityProverUrl,
308978
+ otelMetrics
308979
+ } = ctx;
308980
+ const verityProof = verity.proof;
308981
+ try {
308982
+ const payload = call.request.payload || {};
308983
+ const providedBalanceType = payload.balanceType;
308984
+ const balanceType = (providedBalanceType ?? "total").toString();
308985
+ const validBalanceTypes = new Set(["free", "used", "total"]);
308986
+ if (!validBalanceTypes.has(balanceType)) {
308987
+ return ctx.wrappedCallback({
308988
+ code: grpc7.status.INVALID_ARGUMENT,
308989
+ message: `ValidationError: invalid balanceType '${providedBalanceType}'. Expected one of: free | used | total`
308496
308990
  }, null);
308497
308991
  }
308498
- };
308499
- }
308500
- // src/handlers/subscribe/broker-lifecycle.ts
308501
- class SubscribeBrokerLifecycle {
308502
- #brokers = new Map;
308503
- #closing = new Map;
308504
- #shuttingDown = false;
308505
- register(broker, context2) {
308506
- this.#brokers.set(broker, context2);
308507
- if (this.#shuttingDown) {
308508
- this.close(broker);
308992
+ const params = { ...payload };
308993
+ delete params.balanceType;
308994
+ const marketType = parseMarketType(params.marketType);
308995
+ delete params.marketType;
308996
+ if (params.type === undefined) {
308997
+ params.type = marketTypeToCcxtType(marketType);
308509
308998
  }
308510
- }
308511
- close(broker) {
308512
- const existing = this.#closing.get(broker);
308513
- if (existing) {
308514
- return existing;
308999
+ let responseBalances = {};
309000
+ if (balanceType === "free") {
309001
+ const partial2 = await broker.fetchFreeBalance(params);
309002
+ responseBalances = partial2 ?? {};
309003
+ } else if (balanceType === "used") {
309004
+ const partial2 = await broker.fetchUsedBalance(params);
309005
+ responseBalances = partial2 ?? {};
309006
+ } else if (balanceType === "total") {
309007
+ const partial2 = await broker.fetchTotalBalance(params);
309008
+ responseBalances = partial2 ?? {};
308515
309009
  }
308516
- const context2 = this.#brokers.get(broker) ?? {
308517
- cex: "unknown",
308518
- symbol: "unknown"
308519
- };
308520
- this.#brokers.delete(broker);
308521
- const closing = (async () => {
308522
- try {
308523
- await broker.close();
308524
- log.debug("Request-scoped Subscribe broker closed", context2);
308525
- return "closed";
308526
- } catch (error48) {
308527
- log.warn("Failed to close request-scoped Subscribe broker", {
308528
- ...context2,
308529
- error: error48
308530
- });
308531
- return "failed";
308532
- } finally {
308533
- this.#closing.delete(broker);
309010
+ if (symbol2) {
309011
+ if (typeof responseBalances[symbol2] === "number") {
309012
+ responseBalances = {
309013
+ [symbol2]: responseBalances[symbol2] ?? 0
309014
+ };
309015
+ } else {
309016
+ responseBalances = {};
308534
309017
  }
308535
- })();
308536
- this.#closing.set(broker, closing);
308537
- return closing;
308538
- }
308539
- async closeAll() {
308540
- this.#shuttingDown = true;
308541
- let failed = 0;
308542
- while (this.#brokers.size > 0 || this.#closing.size > 0) {
308543
- const inFlight = [...this.#closing.values()];
308544
- const fresh = [...this.#brokers.keys()].map((broker) => this.close(broker));
308545
- const outcomes = await Promise.all([...fresh, ...inFlight]);
308546
- failed += outcomes.filter((outcome) => outcome === "failed").length;
308547
- }
308548
- if (failed > 0) {
308549
- throw new Error(`${failed} request-scoped Subscribe broker(s) failed to close`);
308550
309018
  }
309019
+ ctx.wrappedCallback(null, {
309020
+ proof: ctx.verity.proof,
309021
+ result: JSON.stringify({
309022
+ balances: responseBalances,
309023
+ balanceType
309024
+ })
309025
+ });
309026
+ } catch (error48) {
309027
+ safeLogError(`Error fetching balance from ${cex3}`, error48);
309028
+ ctx.wrappedCallback({
309029
+ code: grpc7.status.INTERNAL,
309030
+ message: `Failed to fetch balance from ${cex3}`
309031
+ }, null);
308551
309032
  }
308552
309033
  }
308553
- // src/handlers/subscribe/handler.ts
308554
- import * as grpc13 from "@grpc/grpc-js";
308555
-
308556
- // src/helpers/binance-user-data-stream.ts
308557
- import { Buffer as Buffer2 } from "node:buffer";
308558
- import { createHmac } from "node:crypto";
308559
- var BINANCE_SPOT_WS_API_URL = "wss://ws-api.binance.com:443/ws-api/v3";
308560
- var DEFAULT_BINANCE_USER_DATA_MAX_BUFFERED_EVENTS = 16;
308561
- var createWebSocket = (url3) => new wrapper_default(url3);
308562
- var userDataRequestCounter = 0;
308563
- function getExchangeString(exchange, key) {
308564
- const value = exchange[key];
308565
- if (typeof value !== "string" || value.length === 0) {
308566
- throw new Error(`Binance user-data stream requires exchange.${key}`);
308567
- }
308568
- return value;
308569
- }
308570
- function sortedQuery(params) {
308571
- return Object.entries(params).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`).join("&");
308572
- }
308573
- function signUserDataStreamParams(exchange, params) {
308574
- const signParams = exchange.signParams;
308575
- if (typeof signParams === "function") {
308576
- return signParams.call(exchange, params);
308577
- }
308578
- const secret = getExchangeString(exchange, "secret");
308579
- return {
308580
- ...params,
308581
- signature: createHmac("sha256", secret).update(sortedQuery(params)).digest("hex")
308582
- };
308583
- }
308584
- function getBinanceSpotWsApiUrl(exchange) {
308585
- const urls = exchange.urls;
308586
- return urls?.api?.ws?.["ws-api"]?.spot ?? BINANCE_SPOT_WS_API_URL;
308587
- }
308588
- function getRecord(value) {
308589
- return typeof value === "object" && value !== null ? value : null;
308590
- }
308591
- function getMessage(value) {
308592
- if (value instanceof Error) {
308593
- return value.message;
308594
- }
308595
- if (typeof value === "string" && value.length > 0) {
308596
- return value;
309034
+ async function handleFetchTicker(ctx) {
309035
+ const {
309036
+ call,
309037
+ wrappedCallback,
309038
+ policy,
309039
+ brokers,
309040
+ metadata,
309041
+ normalizedCex,
309042
+ cex: cex3,
309043
+ symbol: symbol2,
309044
+ selectedBrokerAccount,
309045
+ broker,
309046
+ verity,
309047
+ applyVerityToBroker,
309048
+ useVerity,
309049
+ verityProverUrl,
309050
+ otelMetrics
309051
+ } = ctx;
309052
+ const verityProof = verity.proof;
309053
+ if (!symbol2) {
309054
+ return ctx.wrappedCallback({
309055
+ code: grpc7.status.INVALID_ARGUMENT,
309056
+ message: `ValidationError: Symbol required`
309057
+ }, null);
308597
309058
  }
308598
- const record2 = getRecord(value);
308599
- const message = record2?.message;
308600
- return typeof message === "string" && message.length > 0 ? message : null;
308601
- }
308602
- function getOptionalExchangeString(exchange, key) {
308603
- const value = exchange[key];
308604
- return typeof value === "string" && value.length > 0 ? value : null;
308605
- }
308606
- function redactDiagnosticMessage(message, secretValues) {
308607
- let redacted = message;
308608
- for (const value of secretValues) {
308609
- if (value.length > 0) {
308610
- redacted = redacted.split(value).join("[redacted]");
308611
- }
309059
+ try {
309060
+ const ticker = await broker.fetchTicker(symbol2);
309061
+ ctx.wrappedCallback(null, {
309062
+ proof: ctx.verity.proof,
309063
+ result: JSON.stringify(ticker)
309064
+ });
309065
+ } catch (error48) {
309066
+ safeLogError(`Error fetching ticker from ${cex3}`, error48);
309067
+ ctx.wrappedCallback({
309068
+ code: grpc7.status.INTERNAL,
309069
+ message: `Failed to fetch ticker from ${cex3}`
309070
+ }, null);
308612
309071
  }
308613
- return redacted.replace(/(\b(?:apiKey|secret|signature)\b\s*=\s*)[^\s&,;)]+/gi, "$1[redacted]").replace(/("(?:apiKey|secret|signature)"\s*:\s*")[^"]*(")/gi, "$1[redacted]$2");
308614
309072
  }
308615
- function formatBinanceUserDataWebSocketError(event, secretValues) {
308616
- const record2 = getRecord(event);
308617
- const message = getMessage(record2?.error) ?? getMessage(record2?.message) ?? getMessage(event);
308618
- const safeMessage = message === null ? null : redactDiagnosticMessage(message, secretValues);
308619
- return new Error(safeMessage ? `Binance user-data WebSocket error: ${safeMessage}` : "Binance user-data WebSocket error");
309073
+ async function handlePassThrough(ctx) {
309074
+ if (ctx.action === Action.FetchCurrency)
309075
+ return handleFetchCurrency(ctx);
309076
+ if (ctx.action === Action.FetchAccountId)
309077
+ return handleFetchAccountId(ctx);
309078
+ if (ctx.action === Action.FetchFees)
309079
+ return handleFetchFees(ctx);
309080
+ if (ctx.action === Action.FetchDepositAddresses)
309081
+ return handleFetchDepositAddresses(ctx);
309082
+ if (ctx.action === Action.FetchBalances)
309083
+ return handleFetchBalances(ctx);
309084
+ if (ctx.action === Action.FetchTicker)
309085
+ return handleFetchTicker(ctx);
308620
309086
  }
308621
- function getCloseReason(value) {
308622
- if (typeof value === "string") {
308623
- return value.length > 0 ? value : null;
308624
- }
308625
- if (Buffer2.isBuffer(value)) {
308626
- const reason = value.toString("utf8");
308627
- return reason.length > 0 ? reason : null;
308628
- }
308629
- if (value instanceof Uint8Array) {
308630
- const reason = Buffer2.from(value).toString("utf8");
308631
- return reason.length > 0 ? reason : null;
308632
- }
308633
- return null;
309087
+
309088
+ // src/handlers/execute-action/perp-config.ts
309089
+ import * as grpc8 from "@grpc/grpc-js";
309090
+ function exchangeSupports(broker, capability) {
309091
+ return broker.has?.[capability] === true;
308634
309092
  }
308635
- function formatBinanceUserDataWebSocketClose(codeOrEvent, reasonOrUndefined, secretValues) {
308636
- const record2 = getRecord(codeOrEvent);
308637
- const code = record2 ? record2.code : codeOrEvent;
308638
- const reason = getCloseReason(record2 ? record2.reason : reasonOrUndefined);
308639
- const safeReason = reason === null ? null : redactDiagnosticMessage(reason, secretValues);
308640
- const details = [
308641
- typeof code === "number" || typeof code === "string" ? `code=${code}` : null,
308642
- safeReason ? `reason=${safeReason}` : null
308643
- ].filter((detail) => detail !== null);
308644
- return new Error(details.length > 0 ? `Binance user-data WebSocket closed unexpectedly (${details.join(", ")})` : "Binance user-data WebSocket closed unexpectedly");
309093
+ function extractPerpConfigs(positions) {
309094
+ return positions.map((position) => ({
309095
+ symbol: typeof position.symbol === "string" ? position.symbol : undefined,
309096
+ leverage: typeof position.leverage === "number" ? position.leverage : undefined,
309097
+ marginMode: typeof position.marginMode === "string" ? position.marginMode : undefined
309098
+ }));
308645
309099
  }
308646
- function decodeMessageData(data) {
308647
- if (typeof data === "string") {
308648
- return data;
308649
- }
308650
- if (Buffer2.isBuffer(data)) {
308651
- return data.toString("utf8");
309100
+ async function handleGetPerpConfigState(ctx) {
309101
+ const { wrappedCallback, cex: cex3, normalizedCex, broker } = ctx;
309102
+ const payload = parsePayloadForAction(ctx, GetPerpConfigStatePayloadSchema);
309103
+ if (payload === null) {
309104
+ return;
308652
309105
  }
308653
- if (data instanceof ArrayBuffer) {
308654
- return Buffer2.from(data).toString("utf8");
309106
+ if (!broker) {
309107
+ return wrappedCallback({
309108
+ code: grpc8.status.INVALID_ARGUMENT,
309109
+ message: `Invalid CEX key: ${cex3}`
309110
+ }, null);
308655
309111
  }
308656
- if (ArrayBuffer.isView(data)) {
308657
- return Buffer2.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
309112
+ const exchange = broker;
309113
+ if (!exchangeSupports(exchange, "fetchPositions")) {
309114
+ return wrappedCallback({
309115
+ code: grpc8.status.UNIMPLEMENTED,
309116
+ message: `${normalizedCex} does not support fetchPositions`
309117
+ }, null);
308658
309118
  }
308659
- if (Array.isArray(data) && data.every((item) => Buffer2.isBuffer(item))) {
308660
- return Buffer2.concat(data).toString("utf8");
309119
+ try {
309120
+ const symbols = payload.symbol ? [payload.symbol] : undefined;
309121
+ const positions = await exchange.fetchPositions?.(symbols, payload.params);
309122
+ ctx.wrappedCallback(null, {
309123
+ result: JSON.stringify({
309124
+ exchange: normalizedCex,
309125
+ configs: extractPerpConfigs(positions ?? []),
309126
+ positions: positions ?? []
309127
+ })
309128
+ });
309129
+ } catch (error48) {
309130
+ safeLogError(`GetPerpConfigState failed for ${cex3}`, error48);
309131
+ ctx.wrappedCallback({
309132
+ code: grpc8.status.INTERNAL,
309133
+ message: `GetPerpConfigState failed: ${sanitizeErrorDetail(error48)}`
309134
+ }, null);
308661
309135
  }
308662
- return data;
308663
309136
  }
308664
-
308665
- class BinanceSpotUserDataStream {
308666
- exchange;
308667
- ws;
308668
- secretValues;
308669
- requestId = `user-data-${Date.now()}-${userDataRequestCounter++}`;
308670
- maxBufferedEvents;
308671
- queue = [];
308672
- waiters = [];
308673
- closed = false;
308674
- closeError = null;
308675
- subscriptionId = null;
308676
- constructor(exchange, options = {}) {
308677
- this.exchange = exchange;
308678
- this.maxBufferedEvents = options.maxBufferedEvents ?? DEFAULT_BINANCE_USER_DATA_MAX_BUFFERED_EVENTS;
308679
- this.secretValues = [
308680
- getOptionalExchangeString(exchange, "apiKey"),
308681
- getOptionalExchangeString(exchange, "secret")
308682
- ].filter((value) => value !== null);
308683
- this.ws = createWebSocket(getBinanceSpotWsApiUrl(exchange));
308684
- this.ws.on("open", () => this.subscribe());
308685
- this.ws.on("message", (data) => this.handleMessage(data));
308686
- this.ws.on("error", (error48) => this.fail(formatBinanceUserDataWebSocketError(error48, this.secretValues)));
308687
- this.ws.on("close", (code, reason) => this.handleClose(code, reason));
308688
- }
308689
- async* [Symbol.asyncIterator]() {
308690
- while (true) {
308691
- const event = await this.nextEvent();
308692
- if (!event) {
308693
- break;
308694
- }
308695
- yield event;
308696
- }
309137
+ async function handleSetPerpConfigState(ctx) {
309138
+ const { wrappedCallback, cex: cex3, normalizedCex, broker } = ctx;
309139
+ const payload = parsePayloadForAction(ctx, SetPerpConfigStatePayloadSchema);
309140
+ if (payload === null) {
309141
+ return;
308697
309142
  }
308698
- close() {
308699
- if (this.closed) {
308700
- return;
308701
- }
308702
- this.closed = true;
308703
- this.queue.length = 0;
308704
- try {
308705
- this.ws.close();
308706
- } catch {}
308707
- this.flushWaiters();
309143
+ if (!broker) {
309144
+ return wrappedCallback({
309145
+ code: grpc8.status.INVALID_ARGUMENT,
309146
+ message: `Invalid CEX key: ${cex3}`
309147
+ }, null);
308708
309148
  }
308709
- handleClose(code, reason) {
308710
- if (this.closed) {
308711
- return;
308712
- }
308713
- this.fail(formatBinanceUserDataWebSocketClose(code, reason, this.secretValues));
309149
+ const exchange = broker;
309150
+ if (!exchangeSupports(exchange, "setLeverage")) {
309151
+ return wrappedCallback({
309152
+ code: grpc8.status.UNIMPLEMENTED,
309153
+ message: `${normalizedCex} does not support setLeverage`
309154
+ }, null);
308714
309155
  }
308715
- subscribe() {
308716
- const apiKey = getExchangeString(this.exchange, "apiKey");
308717
- const signedParams = signUserDataStreamParams(this.exchange, {
308718
- apiKey,
308719
- timestamp: Date.now()
309156
+ try {
309157
+ const response = await exchange.setLeverage?.(payload.leverage, payload.symbol, {
309158
+ marginMode: payload.marginMode ?? "cross",
309159
+ ...payload.params
308720
309160
  });
308721
- this.ws.send(JSON.stringify({
308722
- id: this.requestId,
308723
- method: "userDataStream.subscribe.signature",
308724
- params: signedParams
308725
- }));
309161
+ ctx.wrappedCallback(null, {
309162
+ result: JSON.stringify({
309163
+ exchange: normalizedCex,
309164
+ symbol: payload.symbol,
309165
+ leverage: payload.leverage,
309166
+ marginMode: payload.marginMode ?? "cross",
309167
+ response
309168
+ })
309169
+ });
309170
+ } catch (error48) {
309171
+ safeLogError(`SetPerpConfigState failed for ${cex3}`, error48);
309172
+ ctx.wrappedCallback({
309173
+ code: grpc8.status.INTERNAL,
309174
+ message: `SetPerpConfigState failed: ${sanitizeErrorDetail(error48)}`
309175
+ }, null);
308726
309176
  }
308727
- handleMessage(data) {
308728
- if (this.closed) {
308729
- return;
308730
- }
308731
- let message;
308732
- try {
308733
- const decodedData = decodeMessageData(data);
308734
- message = typeof decodedData === "string" ? JSON.parse(decodedData) : decodedData;
308735
- } catch (error48) {
308736
- this.fail(error48 instanceof Error ? error48 : new Error("Invalid Binance user-data message"));
308737
- return;
308738
- }
308739
- if ("id" in message && message.id === this.requestId) {
308740
- if (message.status !== 200) {
308741
- this.fail(new Error(message.error?.msg ?? message.error?.message ?? `Binance user-data subscription failed with status ${message.status}`));
308742
- return;
308743
- }
308744
- this.subscriptionId = message.result?.subscriptionId ?? null;
308745
- return;
308746
- }
308747
- if ("status" in message && typeof message.status === "number" && message.status !== 200) {
308748
- const errorMessage = message.error?.msg ?? message.error?.message ?? `Binance user-data request failed with status ${message.status}`;
308749
- const errorCode2 = message.error?.code;
308750
- this.fail(new Error(typeof errorCode2 === "number" ? `${errorMessage} (code ${errorCode2})` : errorMessage));
308751
- return;
308752
- }
308753
- if (!("event" in message) || !message.event) {
308754
- return;
308755
- }
308756
- const subscriptionId = message.subscriptionId ?? this.subscriptionId;
308757
- if (subscriptionId === null || subscriptionId === undefined) {
308758
- return;
308759
- }
308760
- this.push({ subscriptionId, event: message.event });
309177
+ }
309178
+ async function handlePerpConfig(ctx) {
309179
+ if (ctx.action === Action.GetPerpConfigState) {
309180
+ return handleGetPerpConfigState(ctx);
308761
309181
  }
308762
- push(event) {
308763
- if (this.closed) {
308764
- return;
308765
- }
308766
- const waiter = this.waiters.shift();
308767
- if (waiter) {
308768
- waiter.resolve(event);
308769
- return;
309182
+ if (ctx.action === Action.SetPerpConfigState) {
309183
+ return handleSetPerpConfigState(ctx);
309184
+ }
309185
+ }
309186
+
309187
+ // src/handlers/execute-action/treasury-call.ts
309188
+ import * as grpc9 from "@grpc/grpc-js";
309189
+ async function handleTreasuryCall(ctx) {
309190
+ const { broker } = ctx;
309191
+ const callValue = parsePayloadForAction(ctx, CallPayloadSchema);
309192
+ if (callValue === null)
309193
+ return;
309194
+ let createOrderContext;
309195
+ let marketMetadataHash;
309196
+ try {
309197
+ if (callValue.functionName.startsWith("_") || callValue.functionName.includes("constructor") || callValue.functionName.includes("prototype")) {
309198
+ return ctx.wrappedCallback({
309199
+ code: grpc9.status.PERMISSION_DENIED,
309200
+ message: "Access to the requested function is denied"
309201
+ }, null);
308770
309202
  }
308771
- if (this.queue.length >= this.maxBufferedEvents) {
308772
- this.fail(new Error(`Binance user-data stream buffered event limit exceeded (${this.maxBufferedEvents}); downstream consumer is not keeping up`));
308773
- return;
309203
+ const argsArray = callArgs(callValue.args, callValue.params ?? {});
309204
+ const treasuryDiscovery = await handleTreasuryDiscoveryCall(broker, callValue.functionName, callValue.args, callValue.params ?? {});
309205
+ if (treasuryDiscovery.handled) {
309206
+ return ctx.wrappedCallback(null, {
309207
+ proof: ctx.verity.proof,
309208
+ result: JSON.stringify(treasuryDiscovery.result)
309209
+ });
308774
309210
  }
308775
- this.queue.push(event);
308776
- }
308777
- nextEvent() {
308778
- const event = this.queue.shift();
308779
- if (event) {
308780
- return Promise.resolve(event);
309211
+ const fn = broker[callValue.functionName];
309212
+ if (typeof fn !== "function" || broker.has?.[callValue.functionName] === false) {
309213
+ return ctx.wrappedCallback({
309214
+ code: grpc9.status.INVALID_ARGUMENT,
309215
+ message: `Function not found on broker: ${callValue.functionName}`
309216
+ }, null);
308781
309217
  }
308782
- if (this.closeError) {
308783
- return Promise.reject(this.closeError);
309218
+ if (callValue.functionName === "createOrder") {
309219
+ const [symbol2, orderType, side, quantity, price] = callValue.args;
309220
+ const requestedQuantity = asFiniteNumber(quantity);
309221
+ const requestedPrice = asFiniteNumber(price);
309222
+ const requestedNotional = requestedQuantity !== undefined && requestedPrice !== undefined ? asFiniteNumber(requestedQuantity * requestedPrice) : undefined;
309223
+ const telemetryIds = extractOrderTelemetryIds(callValue.params);
309224
+ const submissionTimestamp = new Date().toISOString();
309225
+ createOrderContext = {
309226
+ action: "CreateOrder",
309227
+ cex: ctx.cex,
309228
+ accountLabel: ctx.selectedBrokerAccount?.label,
309229
+ symbol: asNonEmptyString(symbol2),
309230
+ orderType: asNonEmptyString(orderType),
309231
+ side: asNonEmptyString(side),
309232
+ requestedQuantity,
309233
+ requestedNotional,
309234
+ orderAuthor: callValue.orderAuthor,
309235
+ brokerObservedTimestamp: submissionTimestamp,
309236
+ ...telemetryIds
309237
+ };
309238
+ if (createOrderContext.symbol !== undefined) {
309239
+ marketMetadataHash = await captureMarketMetadataSnapshot(ctx.brokerArchiver, broker, {
309240
+ exchange: ctx.cex,
309241
+ accountSelector: ctx.selectedBrokerAccount?.label,
309242
+ symbol: createOrderContext.symbol,
309243
+ action: "CreateOrder",
309244
+ brokerObservedTimestamp: submissionTimestamp,
309245
+ ...telemetryIds
309246
+ });
309247
+ }
308784
309248
  }
308785
- if (this.closed) {
308786
- return Promise.resolve(null);
309249
+ const result = await fn.apply(broker, argsArray);
309250
+ if (createOrderContext !== undefined) {
309251
+ emitOrderExecutionTelemetryInBackground(ctx.otelMetrics, createOrderContext, result);
309252
+ archiveOrderExecutionInBackground(ctx.brokerArchiver, createOrderContext, result, undefined, { marketMetadataHash });
309253
+ } else if (callValue.functionName === "fetchWithdrawals") {
309254
+ archiveWithdrawalObservationsInBackground(ctx.brokerArchiver, ctx.withdrawalObservationTracker, {
309255
+ exchange: ctx.normalizedCex,
309256
+ accountSelector: ctx.selectedBrokerAccount?.label,
309257
+ transactions: result
309258
+ });
308787
309259
  }
308788
- return new Promise((resolve, reject) => {
308789
- this.waiters.push({ resolve, reject });
309260
+ ctx.wrappedCallback(null, {
309261
+ proof: ctx.verity.proof,
309262
+ result: JSON.stringify(result)
308790
309263
  });
308791
- }
308792
- fail(error48) {
308793
- if (this.closeError) {
308794
- return;
308795
- }
308796
- this.closeError = error48;
308797
- this.closed = true;
308798
- this.queue.length = 0;
308799
- this.flushWaiters();
308800
- try {
308801
- this.ws.close();
308802
- } catch {}
308803
- }
308804
- flushWaiters() {
308805
- const error48 = this.closeError;
308806
- for (const waiter of this.waiters.splice(0)) {
308807
- if (error48) {
308808
- waiter.reject(error48);
308809
- } else {
308810
- waiter.resolve(null);
308811
- }
309264
+ } catch (error48) {
309265
+ if (createOrderContext !== undefined) {
309266
+ rethrowArchiveDurabilityError(error48);
309267
+ emitOrderExecutionTelemetryInBackground(ctx.otelMetrics, createOrderContext, undefined, error48);
309268
+ archiveOrderExecutionInBackground(ctx.brokerArchiver, createOrderContext, undefined, error48, { marketMetadataHash });
308812
309269
  }
309270
+ safeLogError("Call failed", error48);
309271
+ rejectWithGrpcError(ctx, error48, {
309272
+ message: getErrorMessage(error48),
309273
+ preferStableMessageOnly: true,
309274
+ appendClassName: true
309275
+ });
308813
309276
  }
308814
309277
  }
308815
- function isBinanceBalanceUserDataEvent(event) {
308816
- return event.e === "outboundAccountPosition" || event.e === "balanceUpdate" || event.e === "externalLockUpdate";
308817
- }
308818
- function isBinanceOrderUserDataEvent(event) {
308819
- return event.e === "executionReport" || event.e === "listStatus";
308820
- }
308821
-
308822
- // src/helpers/market-data-archive/ohlcv-bar-tracker.ts
308823
- function isFiniteNumber(value) {
308824
- return typeof value === "number" && Number.isFinite(value);
309278
+ function asNonEmptyString(value) {
309279
+ return typeof value === "string" && value.trim() ? value : undefined;
308825
309280
  }
308826
- function parseOhlcvBar(value) {
308827
- if (!Array.isArray(value) || value.length < 6) {
308828
- return null;
308829
- }
308830
- const [openTimeMs, open, high, low, close, volume, quoteVolume] = value;
308831
- if (!isFiniteNumber(openTimeMs) || !isFiniteNumber(open) || !isFiniteNumber(high) || !isFiniteNumber(low) || !isFiniteNumber(close) || !isFiniteNumber(volume)) {
308832
- return null;
309281
+ function asFiniteNumber(value) {
309282
+ if (typeof value === "number") {
309283
+ return Number.isFinite(value) ? value : undefined;
308833
309284
  }
308834
- const bar = {
308835
- openTimeMs,
308836
- open,
308837
- high,
308838
- low,
308839
- close,
308840
- volume
308841
- };
308842
- if (isFiniteNumber(quoteVolume)) {
308843
- bar.quoteVolume = quoteVolume;
309285
+ if (typeof value !== "string" || !value.trim()) {
309286
+ return;
308844
309287
  }
308845
- return bar;
309288
+ const parsed = Number(value);
309289
+ return Number.isFinite(parsed) ? parsed : undefined;
308846
309290
  }
308847
- function extractOhlcvBars(payload) {
308848
- if (!Array.isArray(payload) || payload.length === 0) {
308849
- return [];
309291
+
309292
+ // src/handlers/execute-action/withdraw.ts
309293
+ import * as grpc10 from "@grpc/grpc-js";
309294
+ async function handleWithdraw(ctx) {
309295
+ const {
309296
+ call,
309297
+ wrappedCallback,
309298
+ policy,
309299
+ brokers,
309300
+ metadata,
309301
+ normalizedCex,
309302
+ cex: cex3,
309303
+ symbol: symbol2,
309304
+ selectedBrokerAccount,
309305
+ broker,
309306
+ verity,
309307
+ applyVerityToBroker,
309308
+ useVerity,
309309
+ verityProverUrl,
309310
+ otelMetrics,
309311
+ brokerArchiver
309312
+ } = ctx;
309313
+ const verityProof = verity.proof;
309314
+ if (!symbol2) {
309315
+ return ctx.wrappedCallback({
309316
+ code: grpc10.status.INVALID_ARGUMENT,
309317
+ message: `ValidationError: Symbol required`
309318
+ }, null);
308850
309319
  }
308851
- const rawBars = Array.isArray(payload[0]) ? payload : [payload];
308852
- const byOpenTime = new Map;
308853
- for (const entry of rawBars) {
308854
- const bar = parseOhlcvBar(entry);
308855
- if (bar) {
308856
- byOpenTime.set(bar.openTimeMs, bar);
308857
- }
309320
+ const transferValue = parsePayloadForAction(ctx, WithdrawPayloadSchema);
309321
+ if (transferValue === null)
309322
+ return;
309323
+ let withdrawNetwork;
309324
+ try {
309325
+ withdrawNetwork = await resolveTransferNetwork(broker, symbol2, transferValue.chain);
309326
+ } catch (error48) {
309327
+ const message = getErrorMessage(error48);
309328
+ return ctx.wrappedCallback({
309329
+ code: stableGrpcErrorCode(message) ?? grpc10.status.INVALID_ARGUMENT,
309330
+ message
309331
+ }, null);
308858
309332
  }
308859
- return [...byOpenTime.values()].sort((a, b2) => a.openTimeMs - b2.openTimeMs);
308860
- }
308861
- class OhlcvBarTracker {
308862
- lastOpenTimeMs = null;
308863
- lastBar = null;
308864
- process(payload, brokerVersion) {
308865
- const bars = extractOhlcvBars(payload);
308866
- if (bars.length === 0) {
308867
- return [];
308868
- }
308869
- if (bars.length === 1) {
308870
- const [bar] = bars;
308871
- return bar ? this.processSingleBar(bar, brokerVersion) : [];
308872
- }
308873
- return this.processBatch(bars, brokerVersion);
309333
+ const transferValidation = validateWithdraw(policy, cex3, withdrawNetwork.brokerNetworkId, transferValue.recipientAddress, transferValue.amount, symbol2);
309334
+ if (!transferValidation.valid) {
309335
+ return ctx.wrappedCallback({
309336
+ code: grpc10.status.PERMISSION_DENIED,
309337
+ message: `policy_withdrawal_denied: ${transferValidation.error}`
309338
+ }, null);
308874
309339
  }
308875
- processSingleBar(currentBar, brokerVersion) {
308876
- if (this.lastOpenTimeMs !== null && currentBar.openTimeMs < this.lastOpenTimeMs) {
308877
- return [];
308878
- }
308879
- const candidates = [];
308880
- if (this.lastOpenTimeMs !== null && this.lastBar !== null && currentBar.openTimeMs !== this.lastOpenTimeMs) {
308881
- candidates.push({
308882
- bar: this.lastBar,
308883
- isClosed: true,
308884
- brokerVersion
308885
- });
308886
- }
308887
- candidates.push({
308888
- bar: currentBar,
308889
- isClosed: false,
308890
- brokerVersion
308891
- });
308892
- this.lastOpenTimeMs = currentBar.openTimeMs;
308893
- this.lastBar = currentBar;
308894
- return candidates;
309340
+ const travelRule = resolveTravelRuleDecision(policy, cex3, transferValue.recipientAddress);
309341
+ if (travelRule.mode === "denied") {
309342
+ return ctx.wrappedCallback({
309343
+ code: grpc10.status.FAILED_PRECONDITION,
309344
+ message: `travel_rule_denied: ${travelRule.error}`
309345
+ }, null);
308895
309346
  }
308896
- processBatch(bars, brokerVersion) {
308897
- const firstBar = bars[0];
308898
- const lastBar = bars[bars.length - 1];
308899
- if (!firstBar || !lastBar) {
308900
- return [];
308901
- }
308902
- const lastOpenTimeMs = this.lastOpenTimeMs;
308903
- if (lastOpenTimeMs !== null && lastBar.openTimeMs < lastOpenTimeMs) {
308904
- return [];
308905
- }
308906
- const barsToProcess = lastOpenTimeMs === null ? bars : bars.filter((bar) => bar.openTimeMs >= lastOpenTimeMs);
308907
- const firstBarToProcess = barsToProcess[0];
308908
- const lastBarToProcess = barsToProcess[barsToProcess.length - 1];
308909
- if (!firstBarToProcess || !lastBarToProcess) {
308910
- return [];
308911
- }
308912
- const candidates = [];
308913
- if (this.lastBar !== null && lastOpenTimeMs !== null && lastOpenTimeMs < firstBarToProcess.openTimeMs) {
308914
- candidates.push({
308915
- bar: this.lastBar,
308916
- isClosed: true,
308917
- brokerVersion
308918
- });
308919
- }
308920
- for (let index2 = 0;index2 < barsToProcess.length - 1; index2 += 1) {
308921
- const bar = barsToProcess[index2];
308922
- if (bar) {
308923
- candidates.push({
308924
- bar,
308925
- isClosed: true,
308926
- brokerVersion
308927
- });
309347
+ const withdrawOrderId = transferValue.params.withdrawOrderId;
309348
+ const clientWithdrawalId = typeof withdrawOrderId === "string" && withdrawOrderId.length > 0 ? withdrawOrderId : undefined;
309349
+ try {
309350
+ const transaction = travelRule.mode === "localentity" ? await withdrawViaLocalEntity(broker, {
309351
+ code: symbol2,
309352
+ amount: transferValue.amount,
309353
+ address: transferValue.recipientAddress,
309354
+ network: withdrawNetwork.exchangeNetworkId,
309355
+ questionnaire: travelRule.questionnaire,
309356
+ params: transferValue.params
309357
+ }) : await broker.withdraw(symbol2, transferValue.amount, transferValue.recipientAddress, undefined, {
309358
+ ...transferValue.params ?? {},
309359
+ network: withdrawNetwork.exchangeNetworkId
309360
+ });
309361
+ log.info(`Withdraw Result: ${JSON.stringify(transaction)}`);
309362
+ const normalized = normalizeCcxtTransactionForArchive(transaction);
309363
+ archiveTransferEventInBackground(brokerArchiver, {
309364
+ exchange: cex3,
309365
+ accountSelector: selectedBrokerAccount?.label,
309366
+ assetSymbol: normalized.assetSymbol ?? symbol2,
309367
+ transfer: {
309368
+ eventKind: "withdrawal",
309369
+ lifecycleAction: "submit_withdrawal",
309370
+ status: normalized.status,
309371
+ amount: normalized.amount ?? String(transferValue.amount),
309372
+ address: normalized.address ?? transferValue.recipientAddress,
309373
+ network: normalized.network ?? withdrawNetwork.exchangeNetworkId,
309374
+ externalId: normalized.externalId,
309375
+ clientWithdrawalId,
309376
+ txid: normalized.txid,
309377
+ feeAmount: normalized.feeAmount,
309378
+ feeCurrency: normalized.feeCurrency,
309379
+ exchangeTimestamp: normalized.exchangeTimestamp,
309380
+ payload: transaction
309381
+ }
309382
+ });
309383
+ ctx.wrappedCallback(null, {
309384
+ proof: ctx.verity.proof,
309385
+ result: JSON.stringify({
309386
+ ...transaction,
309387
+ operatorAlias: withdrawNetwork.operatorAlias,
309388
+ brokerNetworkId: withdrawNetwork.brokerNetworkId,
309389
+ exchangeNetworkId: withdrawNetwork.exchangeNetworkId
309390
+ })
309391
+ });
309392
+ } catch (error48) {
309393
+ safeLogError("Withdraw failed", error48);
309394
+ archiveTransferEventInBackground(brokerArchiver, {
309395
+ exchange: cex3,
309396
+ accountSelector: selectedBrokerAccount?.label,
309397
+ assetSymbol: symbol2,
309398
+ transfer: {
309399
+ eventKind: "withdrawal",
309400
+ lifecycleAction: "submit_withdrawal",
309401
+ status: "failed",
309402
+ amount: String(transferValue.amount),
309403
+ address: transferValue.recipientAddress,
309404
+ network: withdrawNetwork.exchangeNetworkId,
309405
+ clientWithdrawalId,
309406
+ errorSummary: getErrorMessage(error48),
309407
+ payload: { recipientAddress: transferValue.recipientAddress }
308928
309408
  }
308929
- }
308930
- candidates.push({
308931
- bar: lastBarToProcess,
308932
- isClosed: false,
308933
- brokerVersion
308934
309409
  });
308935
- this.lastOpenTimeMs = lastBarToProcess.openTimeMs;
308936
- this.lastBar = lastBarToProcess;
308937
- return candidates;
308938
- }
308939
- }
308940
-
308941
- // src/helpers/market-data-archive/orderbook-sampler.ts
308942
- var DEFAULT_ORDERBOOK_INTERVAL_MS = 1000;
308943
- function getOrderbookIntervalMs() {
308944
- const raw = process.env.CEX_BROKER_ORDERBOOK_INTERVAL_MS ?? process.env.CEX_BROKER_ORDERBOOK_TOB_INTERVAL_MS;
308945
- if (!raw) {
308946
- return DEFAULT_ORDERBOOK_INTERVAL_MS;
309410
+ const code = mapCcxtErrorToGrpcStatus(error48) ?? grpc10.status.INTERNAL;
309411
+ ctx.wrappedCallback({
309412
+ code,
309413
+ message: `Withdraw failed: ${sanitizeErrorDetail(error48)}`
309414
+ }, null);
308947
309415
  }
308948
- const parsed = Number.parseInt(raw, 10);
308949
- return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_ORDERBOOK_INTERVAL_MS;
308950
- }
308951
- function isMarketArchiveEnabled() {
308952
- return process.env.CEX_BROKER_MARKET_ARCHIVE_ENABLED !== "false";
308953
309416
  }
308954
309417
 
308955
- class OrderbookSampler {
308956
- intervalMs;
308957
- lastEmitMs = null;
308958
- constructor(intervalMs = getOrderbookIntervalMs()) {
308959
- this.intervalMs = intervalMs;
308960
- }
308961
- shouldEmit(nowMs = Date.now()) {
308962
- if (this.lastEmitMs !== null && nowMs < this.lastEmitMs) {
308963
- this.lastEmitMs = nowMs;
308964
- return true;
308965
- }
308966
- if (this.lastEmitMs !== null && nowMs - this.lastEmitMs < this.intervalMs) {
308967
- return false;
308968
- }
308969
- this.lastEmitMs = nowMs;
308970
- return true;
309418
+ // src/handlers/execute-action/registry.ts
309419
+ var ACTION_HANDLERS = {
309420
+ [Action.Deposit]: handleDeposit,
309421
+ [Action.Withdraw]: handleWithdraw,
309422
+ [Action.Call]: handleTreasuryCall,
309423
+ [Action.InternalTransfer]: handleInternalTransfer,
309424
+ [Action.CreateOrder]: handleOrders,
309425
+ [Action.GetOrderDetails]: handleOrders,
309426
+ [Action.CancelOrder]: handleOrders,
309427
+ [Action.FetchCurrency]: handlePassThrough,
309428
+ [Action.FetchAccountId]: handlePassThrough,
309429
+ [Action.FetchFees]: handlePassThrough,
309430
+ [Action.FetchDepositAddresses]: handlePassThrough,
309431
+ [Action.FetchBalances]: handlePassThrough,
309432
+ [Action.FetchTicker]: handlePassThrough,
309433
+ [Action.GetPerpConfigState]: handlePerpConfig,
309434
+ [Action.SetPerpConfigState]: handlePerpConfig
309435
+ };
309436
+ async function dispatchExecuteAction(ctx) {
309437
+ const handler = ACTION_HANDLERS[ctx.action];
309438
+ if (!handler) {
309439
+ ctx.wrappedCallback({
309440
+ code: grpc11.status.INVALID_ARGUMENT,
309441
+ message: "Invalid Action"
309442
+ }, null);
309443
+ return;
308971
309444
  }
309445
+ await handler(ctx);
308972
309446
  }
308973
309447
 
308974
- // src/helpers/market-data-archive/parse-stream.ts
308975
- function isFiniteNumber2(value) {
308976
- return typeof value === "number" && Number.isFinite(value);
308977
- }
308978
- function toNumber2(value) {
308979
- if (isFiniteNumber2(value)) {
308980
- return value;
308981
- }
308982
- if (typeof value === "string") {
308983
- const parsed = Number.parseFloat(value);
308984
- if (Number.isFinite(parsed)) {
308985
- return parsed;
308986
- }
308987
- }
308988
- return;
308989
- }
308990
- function toStringId(value) {
308991
- if (typeof value === "string" && value.trim()) {
308992
- return value.trim();
308993
- }
308994
- if (typeof value === "number" && Number.isFinite(value)) {
308995
- return String(value);
308996
- }
308997
- return;
308998
- }
308999
- function scalarTimestampMs(value, fallbackMs) {
309000
- const numeric = toNumber2(value);
309001
- if (numeric !== undefined) {
309002
- return numeric < 1000000000000 ? numeric * 1000 : numeric;
309003
- }
309004
- if (typeof value === "string") {
309005
- const parsed = Date.parse(value);
309006
- if (Number.isFinite(parsed)) {
309007
- return parsed;
309008
- }
309009
- }
309010
- return fallbackMs;
309011
- }
309012
- function parseTrade(value, fallbackMs = Date.now()) {
309013
- const record2 = asRecord(value);
309014
- if (!record2) {
309015
- return null;
309016
- }
309017
- const tradeId = toStringId(record2.id);
309018
- const price = toNumber2(record2.price);
309019
- const amount = toNumber2(record2.amount);
309020
- const side = typeof record2.side === "string" ? record2.side.toLowerCase() : undefined;
309021
- if (!tradeId || price === undefined || amount === undefined || !side) {
309022
- return null;
309023
- }
309024
- const parsed = {
309025
- tradeId,
309026
- eventTimeMs: scalarTimestampMs(record2.timestamp, fallbackMs),
309027
- side,
309028
- price,
309029
- amount
309030
- };
309031
- const cost = toNumber2(record2.cost);
309032
- if (cost !== undefined) {
309033
- parsed.cost = cost;
309034
- }
309035
- if (typeof record2.takerOrMaker === "string") {
309036
- parsed.takerOrMaker = record2.takerOrMaker;
309037
- }
309038
- return parsed;
309039
- }
309040
- function extractTrades(payload, fallbackMs = Date.now()) {
309041
- if (Array.isArray(payload)) {
309042
- return payload.map((entry) => parseTrade(entry, fallbackMs)).filter((entry) => entry !== null);
309043
- }
309044
- const single = parseTrade(payload, fallbackMs);
309045
- return single ? [single] : [];
309448
+ // src/handlers/execute-action/handler.ts
309449
+ function isPublicMarketDataAction(action, payload) {
309450
+ if (action !== Action.Call)
309451
+ return false;
309452
+ return isOrderBookCallMethod(payload?.method ?? payload?.functionName);
309046
309453
  }
309047
- function parseTicker(value, fallbackMs) {
309048
- const record2 = asRecord(value);
309049
- if (!record2) {
309050
- return null;
309051
- }
309052
- const parsed = {
309053
- eventTimeMs: scalarTimestampMs(record2.timestamp, fallbackMs)
309054
- };
309055
- const fields = [
309056
- ["last", record2.last],
309057
- ["bid", record2.bid],
309058
- ["ask", record2.ask],
309059
- ["high", record2.high],
309060
- ["low", record2.low],
309061
- ["open", record2.open],
309062
- ["close", record2.close],
309063
- ["baseVolume", record2.baseVolume],
309064
- ["quoteVolume", record2.quoteVolume],
309065
- ["change", record2.change],
309066
- ["percentage", record2.percentage]
309067
- ];
309068
- for (const [key, rawValue] of fields) {
309069
- const numeric = toNumber2(rawValue);
309070
- if (numeric !== undefined) {
309071
- parsed[key] = numeric;
309454
+ function createExecuteActionHandler(deps) {
309455
+ const {
309456
+ policy,
309457
+ brokers,
309458
+ whitelistIps,
309459
+ useVerity,
309460
+ verityProverUrl,
309461
+ otelMetrics,
309462
+ brokerArchiver,
309463
+ orderActivityTracker
309464
+ } = deps;
309465
+ const withdrawalObservationTracker = deps.withdrawalObservationTracker ?? new WithdrawalObservationTracker;
309466
+ return async (call, callback) => {
309467
+ const startTime = Date.now();
309468
+ const { action: rawAction, cex: cex3, symbol: symbol2 } = call.request;
309469
+ const action = resolveAction(rawAction);
309470
+ let actionCompleted = false;
309471
+ const wrappedCallback = (error48, value) => {
309472
+ if (!actionCompleted) {
309473
+ actionCompleted = true;
309474
+ const latency = Date.now() - startTime;
309475
+ const actionName = getActionName(action);
309476
+ otelMetrics?.recordHistogram("execute_action_duration_ms", latency, {
309477
+ action: actionName,
309478
+ cex: cex3 || "unknown"
309479
+ });
309480
+ if (error48) {
309481
+ otelMetrics?.recordCounter("execute_action_errors_total", 1, {
309482
+ action: actionName,
309483
+ cex: cex3 || "unknown",
309484
+ error_type: error48.code ? grpc12.status[error48.code] || "unknown" : "unknown"
309485
+ });
309486
+ } else {
309487
+ otelMetrics?.recordCounter("execute_action_success_total", 1, {
309488
+ action: actionName,
309489
+ cex: cex3 || "unknown"
309490
+ });
309491
+ }
309492
+ }
309493
+ callback(error48, value);
309494
+ };
309495
+ try {
309496
+ log.info(`Request - ExecuteAction:`, { action, cex: cex3, symbol: symbol2 });
309497
+ otelMetrics?.recordCounter("execute_action_requests_total", 1, {
309498
+ action: getActionName(action),
309499
+ cex: cex3 || "unknown"
309500
+ });
309501
+ if (!authenticateRequest(call, whitelistIps)) {
309502
+ return wrappedCallback({
309503
+ code: grpc12.status.PERMISSION_DENIED,
309504
+ message: "Access denied: Unauthorized IP"
309505
+ }, null);
309506
+ }
309507
+ if (!action || !cex3) {
309508
+ return wrappedCallback({
309509
+ code: grpc12.status.INVALID_ARGUMENT,
309510
+ message: "`action` AND `cex` fields are required"
309511
+ }, null);
309512
+ }
309513
+ const normalizedCex = cex3.trim().toLowerCase();
309514
+ const metadata = call.metadata;
309515
+ const selectedBrokerAccount = selectBrokerAccountForCex(normalizedCex, brokers, metadata);
309516
+ const broker = selectedBrokerAccount?.exchange ?? createBroker(normalizedCex, call.metadata) ?? (isPublicMarketDataAction(action, call.request.payload) ? createPublicBroker(normalizedCex) : null);
309517
+ if (!broker) {
309518
+ return wrappedCallback({
309519
+ code: grpc12.status.UNAUTHENTICATED,
309520
+ message: `This Exchange is not registered and No API metadata was found`
309521
+ }, null);
309522
+ }
309523
+ const verity = { proof: "" };
309524
+ const applyVerityToBroker = (targetBroker) => {
309525
+ if (!useVerity)
309526
+ return;
309527
+ const override = buildHttpClientOverrideFromMetadata(metadata, verityProverUrl, (proof, notaryPubKey) => {
309528
+ verity.proof = proof;
309529
+ log.debug(`Verity proof:`, { proof, notaryPubKey });
309530
+ });
309531
+ targetBroker.setHttpClientOverride(override, verityHttpClientOverridePredicate);
309532
+ };
309533
+ const preludeCtx = {
309534
+ call,
309535
+ wrappedCallback,
309536
+ action,
309537
+ policy,
309538
+ brokers,
309539
+ metadata,
309540
+ normalizedCex,
309541
+ cex: cex3,
309542
+ symbol: symbol2,
309543
+ selectedBrokerAccount,
309544
+ broker,
309545
+ verity,
309546
+ applyVerityToBroker,
309547
+ useVerity,
309548
+ verityProverUrl,
309549
+ otelMetrics,
309550
+ brokerArchiver,
309551
+ orderActivityTracker,
309552
+ withdrawalObservationTracker
309553
+ };
309554
+ if (action === Action.Call) {
309555
+ const handled = await handleOrderBookCall(preludeCtx);
309556
+ if (handled)
309557
+ return;
309558
+ }
309559
+ applyVerityToBroker(broker);
309560
+ const ctx = { ...preludeCtx, broker };
309561
+ await dispatchExecuteAction(ctx);
309562
+ } catch (error48) {
309563
+ safeLogError("ExecuteAction unhandled error", error48);
309564
+ return wrappedCallback({
309565
+ code: grpc12.status.INTERNAL,
309566
+ message: "ExecuteAction failed unexpectedly"
309567
+ }, null);
309072
309568
  }
309073
- }
309074
- return parsed;
309569
+ };
309075
309570
  }
309076
-
309077
- // src/helpers/market-data-archive/orderbook-depth.ts
309078
- var DEFAULT_ORDERBOOK_ARCHIVE_DEPTH_LIMIT = 25;
309079
- var MAX_ORDERBOOK_ARCHIVE_DEPTH_LIMIT = 500;
309080
- function getOrderbookArchiveDepthLimit() {
309081
- const raw = process.env.CEX_BROKER_ORDERBOOK_ARCHIVE_DEPTH_LIMIT;
309082
- if (!raw) {
309083
- return DEFAULT_ORDERBOOK_ARCHIVE_DEPTH_LIMIT;
309571
+ // src/handlers/subscribe/broker-lifecycle.ts
309572
+ class SubscribeBrokerLifecycle {
309573
+ #brokers = new Map;
309574
+ #closing = new Map;
309575
+ #shuttingDown = false;
309576
+ register(broker, context2) {
309577
+ this.#brokers.set(broker, context2);
309578
+ if (this.#shuttingDown) {
309579
+ this.close(broker);
309580
+ }
309084
309581
  }
309085
- const parsed = Number.parseInt(raw, 10);
309086
- if (!Number.isFinite(parsed) || parsed <= 0) {
309087
- return DEFAULT_ORDERBOOK_ARCHIVE_DEPTH_LIMIT;
309582
+ close(broker) {
309583
+ const existing = this.#closing.get(broker);
309584
+ if (existing) {
309585
+ return existing;
309586
+ }
309587
+ const context2 = this.#brokers.get(broker) ?? {
309588
+ cex: "unknown",
309589
+ symbol: "unknown"
309590
+ };
309591
+ this.#brokers.delete(broker);
309592
+ const closing = (async () => {
309593
+ try {
309594
+ await broker.close();
309595
+ log.debug("Request-scoped Subscribe broker closed", context2);
309596
+ return "closed";
309597
+ } catch (error48) {
309598
+ log.warn("Failed to close request-scoped Subscribe broker", {
309599
+ ...context2,
309600
+ error: error48
309601
+ });
309602
+ return "failed";
309603
+ } finally {
309604
+ this.#closing.delete(broker);
309605
+ }
309606
+ })();
309607
+ this.#closing.set(broker, closing);
309608
+ return closing;
309088
309609
  }
309089
- return Math.min(parsed, MAX_ORDERBOOK_ARCHIVE_DEPTH_LIMIT);
309090
- }
309091
- function splitOrderBookSide(levels, limit) {
309092
- const prices = [];
309093
- const sizes = [];
309094
- for (const level of levels.slice(0, limit)) {
309095
- if (!Array.isArray(level) || level.length < 2) {
309096
- continue;
309610
+ async closeAll() {
309611
+ this.#shuttingDown = true;
309612
+ let failed = 0;
309613
+ while (this.#brokers.size > 0 || this.#closing.size > 0) {
309614
+ const inFlight = [...this.#closing.values()];
309615
+ const fresh = [...this.#brokers.keys()].map((broker) => this.close(broker));
309616
+ const outcomes = await Promise.all([...fresh, ...inFlight]);
309617
+ failed += outcomes.filter((outcome) => outcome === "failed").length;
309097
309618
  }
309098
- const price = level[0];
309099
- const size = level[1];
309100
- if (price === undefined || size === undefined || !Number.isFinite(price) || !Number.isFinite(size)) {
309101
- continue;
309619
+ if (failed > 0) {
309620
+ throw new Error(`${failed} request-scoped Subscribe broker(s) failed to close`);
309102
309621
  }
309103
- prices.push(price);
309104
- sizes.push(size);
309105
309622
  }
309106
- return { prices, sizes };
309107
309623
  }
309624
+ // src/handlers/subscribe/handler.ts
309625
+ import * as grpc13 from "@grpc/grpc-js";
309108
309626
 
309109
- // src/helpers/market-data-archive/rows.ts
309110
- function compactUndefined3(record2) {
309111
- return Object.fromEntries(Object.entries(record2).filter(([, value]) => value !== undefined));
309112
- }
309113
- function scalarTimestampMs2(value) {
309114
- if (typeof value === "number" && Number.isFinite(value)) {
309115
- return value;
309116
- }
309117
- if (typeof value === "string") {
309118
- if (/^\d+$/.test(value)) {
309119
- const numeric = Number.parseInt(value, 10);
309120
- if (Number.isFinite(numeric)) {
309121
- return numeric;
309122
- }
309123
- }
309124
- const parsed = Date.parse(value);
309125
- if (Number.isFinite(parsed)) {
309126
- return parsed;
309127
- }
309627
+ // src/helpers/binance-user-data-stream.ts
309628
+ import { Buffer as Buffer2 } from "node:buffer";
309629
+ import { createHmac } from "node:crypto";
309630
+ var BINANCE_SPOT_WS_API_URL = "wss://ws-api.binance.com:443/ws-api/v3";
309631
+ var DEFAULT_BINANCE_USER_DATA_MAX_BUFFERED_EVENTS = 16;
309632
+ var createWebSocket = (url3) => new wrapper_default(url3);
309633
+ var userDataRequestCounter = 0;
309634
+ function getExchangeString(exchange, key) {
309635
+ const value = exchange[key];
309636
+ if (typeof value !== "string" || value.length === 0) {
309637
+ throw new Error(`Binance user-data stream requires exchange.${key}`);
309128
309638
  }
309129
- return Date.now();
309639
+ return value;
309130
309640
  }
309131
- function parseSequence(value) {
309132
- if (typeof value === "number" && Number.isFinite(value)) {
309133
- return value;
309134
- }
309135
- if (typeof value === "string" && /^\d+$/.test(value)) {
309136
- return Number.parseInt(value, 10);
309137
- }
309138
- return;
309641
+ function sortedQuery(params) {
309642
+ return Object.entries(params).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`).join("&");
309139
309643
  }
309140
- function topOfBookLevel(levels) {
309141
- const level = levels[0];
309142
- if (!level || level.length < 2) {
309143
- return null;
309144
- }
309145
- const price = level[0];
309146
- const size = level[1];
309147
- if (price === undefined || size === undefined || !Number.isFinite(price) || !Number.isFinite(size)) {
309148
- return null;
309644
+ function signUserDataStreamParams(exchange, params) {
309645
+ const signParams = exchange.signParams;
309646
+ if (typeof signParams === "function") {
309647
+ return signParams.call(exchange, params);
309149
309648
  }
309150
- return { price, size };
309649
+ const secret = getExchangeString(exchange, "secret");
309650
+ return {
309651
+ ...params,
309652
+ signature: createHmac("sha256", secret).update(sortedQuery(params)).digest("hex")
309653
+ };
309151
309654
  }
309152
- function computeSpreadBps(bestBid, bestAsk) {
309153
- if (bestBid <= 0 || bestAsk <= 0 || bestAsk < bestBid) {
309154
- return 0;
309155
- }
309156
- const mid = (bestBid + bestAsk) / 2;
309157
- if (mid <= 0) {
309158
- return 0;
309159
- }
309160
- return (bestAsk - bestBid) / mid * 1e4;
309655
+ function getBinanceSpotWsApiUrl(exchange) {
309656
+ const urls = exchange.urls;
309657
+ return urls?.api?.ws?.["ws-api"]?.spot ?? BINANCE_SPOT_WS_API_URL;
309161
309658
  }
309162
- function buildOrderbookArchiveTags(input, receivedTimeMs) {
309163
- return buildCommonArchiveTags({
309164
- deploymentId: input.deploymentId,
309165
- accountSelector: input.accountSelector,
309166
- exchange: input.exchange,
309167
- symbol: input.symbol,
309168
- brokerObservedTimestamp: new Date(receivedTimeMs).toISOString()
309169
- });
309659
+ function getRecord(value) {
309660
+ return typeof value === "object" && value !== null ? value : null;
309170
309661
  }
309171
- function buildOrderbookSnapshotRow(input) {
309172
- const bid = topOfBookLevel(input.snapshot.bids);
309173
- const ask = topOfBookLevel(input.snapshot.asks);
309174
- if (!bid || !ask) {
309175
- return null;
309662
+ function getMessage(value) {
309663
+ if (value instanceof Error) {
309664
+ return value.message;
309176
309665
  }
309177
- const archiveDepthLimit = getOrderbookArchiveDepthLimit();
309178
- const bids = splitOrderBookSide(input.snapshot.bids, archiveDepthLimit);
309179
- const asks = splitOrderBookSide(input.snapshot.asks, archiveDepthLimit);
309180
- if (bids.prices.length === 0 || asks.prices.length === 0) {
309181
- return null;
309666
+ if (typeof value === "string" && value.length > 0) {
309667
+ return value;
309182
309668
  }
309183
- const eventTimeMs = scalarTimestampMs2(input.snapshot.timestamp);
309184
- const receivedTimeMs = input.snapshot.receivedTimestamp;
309185
- const mid = (bid.price + ask.price) / 2;
309186
- const sequence = parseSequence(input.snapshot.sequence);
309187
- return {
309188
- table: "market_data.orderbook_snapshots",
309189
- row: compactUndefined3({
309190
- ...buildOrderbookArchiveTags(input, receivedTimeMs),
309191
- asset_type: input.assetType,
309192
- event_time_ms: eventTimeMs,
309193
- received_time_ms: receivedTimeMs,
309194
- best_bid: bid.price,
309195
- best_ask: ask.price,
309196
- bid_size: bid.size,
309197
- ask_size: ask.size,
309198
- mid,
309199
- spread_bps: computeSpreadBps(bid.price, ask.price),
309200
- depth_limit: archiveDepthLimit,
309201
- bid_levels: bids.prices.length,
309202
- ask_levels: asks.prices.length,
309203
- bids_price: bids.prices,
309204
- bids_size: bids.sizes,
309205
- asks_price: asks.prices,
309206
- asks_size: asks.sizes,
309207
- sequence
309208
- })
309209
- };
309210
- }
309211
- function buildCandleRow(input) {
309212
- const { context: context2, bar, isClosed, brokerVersion, receivedTimestamp } = input;
309213
- const tags = buildCommonArchiveTags({
309214
- deploymentId: context2.deploymentId,
309215
- accountSelector: context2.accountSelector,
309216
- exchange: context2.exchange,
309217
- symbol: context2.symbol,
309218
- brokerObservedTimestamp: new Date(receivedTimestamp).toISOString()
309219
- });
309220
- return {
309221
- table: "market_data.candles",
309222
- row: compactUndefined3({
309223
- ...tags,
309224
- asset_type: context2.assetType,
309225
- timeframe: context2.timeframe ?? "1m",
309226
- open_time_ms: bar.openTimeMs,
309227
- open: bar.open,
309228
- high: bar.high,
309229
- low: bar.low,
309230
- close: bar.close,
309231
- volume: bar.volume,
309232
- quote_volume: bar.quoteVolume,
309233
- is_closed: isClosed ? 1 : 0,
309234
- broker_version: brokerVersion
309235
- })
309236
- };
309669
+ const record2 = getRecord(value);
309670
+ const message = record2?.message;
309671
+ return typeof message === "string" && message.length > 0 ? message : null;
309237
309672
  }
309238
- function buildCexStreamEventRow(input) {
309239
- const receivedTimeMs = input.receivedTimestamp;
309240
- const redactedPayload = redactStreamPayload(input.payload);
309241
- const tags = buildCommonArchiveTags({
309242
- deploymentId: input.deploymentId,
309243
- accountSelector: input.accountSelector,
309244
- exchange: input.exchange,
309245
- symbol: input.symbol,
309246
- brokerObservedTimestamp: new Date(receivedTimeMs).toISOString()
309247
- });
309248
- return {
309249
- table: "market_data.cex_stream_events",
309250
- row: compactUndefined3({
309251
- ...tags,
309252
- asset_type: input.assetType,
309253
- stream_type: input.streamType,
309254
- event_time_ms: input.eventTimeMs ?? receivedTimeMs,
309255
- received_time_ms: receivedTimeMs,
309256
- payload_json: JSON.stringify(redactedPayload)
309257
- })
309258
- };
309673
+ function getOptionalExchangeString(exchange, key) {
309674
+ const value = exchange[key];
309675
+ return typeof value === "string" && value.length > 0 ? value : null;
309259
309676
  }
309260
- function buildCexTickerEventRow(input, ticker) {
309261
- const receivedTimeMs = input.receivedTimestamp;
309262
- const tags = buildCommonArchiveTags({
309263
- deploymentId: input.deploymentId,
309264
- accountSelector: input.accountSelector,
309265
- exchange: input.exchange,
309266
- symbol: input.symbol,
309267
- brokerObservedTimestamp: new Date(receivedTimeMs).toISOString()
309268
- });
309269
- return {
309270
- table: "market_data.cex_ticker_events",
309271
- row: compactUndefined3({
309272
- ...tags,
309273
- asset_type: input.assetType,
309274
- event_time_ms: ticker.eventTimeMs,
309275
- received_time_ms: receivedTimeMs,
309276
- last: ticker.last,
309277
- bid: ticker.bid,
309278
- ask: ticker.ask,
309279
- high: ticker.high,
309280
- low: ticker.low,
309281
- open: ticker.open,
309282
- close: ticker.close,
309283
- base_volume: ticker.baseVolume,
309284
- quote_volume: ticker.quoteVolume,
309285
- change: ticker.change,
309286
- percentage: ticker.percentage,
309287
- payload_json: JSON.stringify(redactStreamPayload(input.payload))
309288
- })
309289
- };
309677
+ function redactDiagnosticMessage(message, secretValues) {
309678
+ let redacted = message;
309679
+ for (const value of secretValues) {
309680
+ if (value.length > 0) {
309681
+ redacted = redacted.split(value).join("[redacted]");
309682
+ }
309683
+ }
309684
+ return redacted.replace(/(\b(?:apiKey|secret|signature)\b\s*=\s*)[^\s&,;)]+/gi, "$1[redacted]").replace(/("(?:apiKey|secret|signature)"\s*:\s*")[^"]*(")/gi, "$1[redacted]$2");
309290
309685
  }
309291
- function buildCexTradeRow(input, trade) {
309292
- const receivedTimeMs = input.receivedTimestamp;
309293
- const tags = buildCommonArchiveTags({
309294
- deploymentId: input.deploymentId,
309295
- accountSelector: input.accountSelector,
309296
- exchange: input.exchange,
309297
- symbol: input.symbol,
309298
- brokerObservedTimestamp: new Date(receivedTimeMs).toISOString()
309299
- });
309300
- return {
309301
- table: "market_data.cex_trades",
309302
- row: compactUndefined3({
309303
- ...tags,
309304
- asset_type: input.assetType,
309305
- trade_id: trade.tradeId,
309306
- event_time_ms: trade.eventTimeMs,
309307
- received_time_ms: receivedTimeMs,
309308
- side: trade.side,
309309
- price: trade.price,
309310
- amount: trade.amount,
309311
- cost: trade.cost,
309312
- taker_or_maker: trade.takerOrMaker
309313
- })
309314
- };
309686
+ function formatBinanceUserDataWebSocketError(event, secretValues) {
309687
+ const record2 = getRecord(event);
309688
+ const message = getMessage(record2?.error) ?? getMessage(record2?.message) ?? getMessage(event);
309689
+ const safeMessage = message === null ? null : redactDiagnosticMessage(message, secretValues);
309690
+ return new Error(safeMessage ? `Binance user-data WebSocket error: ${safeMessage}` : "Binance user-data WebSocket error");
309315
309691
  }
309316
-
309317
- // src/helpers/market-data-archive/capture.ts
309318
- async function recordWatchMetric(otelMetrics, metricName, labels) {
309319
- try {
309320
- await otelMetrics?.recordCounter(metricName, 1, labels);
309321
- } catch {}
309692
+ function getCloseReason(value) {
309693
+ if (typeof value === "string") {
309694
+ return value.length > 0 ? value : null;
309695
+ }
309696
+ if (Buffer2.isBuffer(value)) {
309697
+ const reason = value.toString("utf8");
309698
+ return reason.length > 0 ? reason : null;
309699
+ }
309700
+ if (value instanceof Uint8Array) {
309701
+ const reason = Buffer2.from(value).toString("utf8");
309702
+ return reason.length > 0 ? reason : null;
309703
+ }
309704
+ return null;
309322
309705
  }
309323
- function watchLabels(stream4, input) {
309324
- return {
309325
- stream: stream4,
309326
- exchange: input.exchange,
309327
- symbol: input.symbol
309328
- };
309706
+ function formatBinanceUserDataWebSocketClose(codeOrEvent, reasonOrUndefined, secretValues) {
309707
+ const record2 = getRecord(codeOrEvent);
309708
+ const code = record2 ? record2.code : codeOrEvent;
309709
+ const reason = getCloseReason(record2 ? record2.reason : reasonOrUndefined);
309710
+ const safeReason = reason === null ? null : redactDiagnosticMessage(reason, secretValues);
309711
+ const details = [
309712
+ typeof code === "number" || typeof code === "string" ? `code=${code}` : null,
309713
+ safeReason ? `reason=${safeReason}` : null
309714
+ ].filter((detail) => detail !== null);
309715
+ return new Error(details.length > 0 ? `Binance user-data WebSocket closed unexpectedly (${details.join(", ")})` : "Binance user-data WebSocket closed unexpectedly");
309329
309716
  }
309330
- function archiveOrderbookInBackground(archiver, otelMetrics, input, options) {
309331
- const labels = watchLabels("orderbook", input);
309332
- recordWatchMetric(otelMetrics, "cex_watch_frames_received_total", labels);
309333
- if (!isMarketArchiveEnabled() || !archiver?.isEnabled()) {
309334
- return;
309717
+ function decodeMessageData(data) {
309718
+ if (typeof data === "string") {
309719
+ return data;
309335
309720
  }
309336
- if (options?.sampledOut) {
309337
- recordWatchMetric(otelMetrics, "cex_watch_frames_sampled_out_total", labels);
309338
- return;
309721
+ if (Buffer2.isBuffer(data)) {
309722
+ return data.toString("utf8");
309339
309723
  }
309340
- queueMicrotask(() => {
309341
- try {
309342
- const row = buildOrderbookSnapshotRow(input);
309343
- if (row) {
309344
- archiver.enqueue(row);
309345
- recordWatchMetric(otelMetrics, "cex_watch_frames_archived_total", labels);
309724
+ if (data instanceof ArrayBuffer) {
309725
+ return Buffer2.from(data).toString("utf8");
309726
+ }
309727
+ if (ArrayBuffer.isView(data)) {
309728
+ return Buffer2.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
309729
+ }
309730
+ if (Array.isArray(data) && data.every((item) => Buffer2.isBuffer(item))) {
309731
+ return Buffer2.concat(data).toString("utf8");
309732
+ }
309733
+ return data;
309734
+ }
309735
+
309736
+ class BinanceSpotUserDataStream {
309737
+ exchange;
309738
+ ws;
309739
+ secretValues;
309740
+ requestId = `user-data-${Date.now()}-${userDataRequestCounter++}`;
309741
+ maxBufferedEvents;
309742
+ queue = [];
309743
+ waiters = [];
309744
+ closed = false;
309745
+ closeError = null;
309746
+ subscriptionId = null;
309747
+ constructor(exchange, options = {}) {
309748
+ this.exchange = exchange;
309749
+ this.maxBufferedEvents = options.maxBufferedEvents ?? DEFAULT_BINANCE_USER_DATA_MAX_BUFFERED_EVENTS;
309750
+ this.secretValues = [
309751
+ getOptionalExchangeString(exchange, "apiKey"),
309752
+ getOptionalExchangeString(exchange, "secret")
309753
+ ].filter((value) => value !== null);
309754
+ this.ws = createWebSocket(getBinanceSpotWsApiUrl(exchange));
309755
+ this.ws.on("open", () => this.subscribe());
309756
+ this.ws.on("message", (data) => this.handleMessage(data));
309757
+ this.ws.on("error", (error48) => this.fail(formatBinanceUserDataWebSocketError(error48, this.secretValues)));
309758
+ this.ws.on("close", (code, reason) => this.handleClose(code, reason));
309759
+ }
309760
+ async* [Symbol.asyncIterator]() {
309761
+ while (true) {
309762
+ const event = await this.nextEvent();
309763
+ if (!event) {
309764
+ break;
309346
309765
  }
309347
- } catch (error48) {
309348
- rethrowArchiveDurabilityError(error48);
309349
- log.warn("Failed to archive orderbook snapshot", { error: error48 });
309766
+ yield event;
309350
309767
  }
309351
- });
309352
- }
309353
- function archiveOhlcvInBackground(archiver, otelMetrics, tracker, input) {
309354
- const labels = watchLabels("ohlcv", input);
309355
- recordWatchMetric(otelMetrics, "cex_watch_frames_received_total", labels);
309356
- if (!isMarketArchiveEnabled() || !archiver?.isEnabled()) {
309357
- return;
309358
309768
  }
309359
- queueMicrotask(() => {
309769
+ close() {
309770
+ if (this.closed) {
309771
+ return;
309772
+ }
309773
+ this.closed = true;
309774
+ this.queue.length = 0;
309360
309775
  try {
309361
- const candidates = tracker.process(input.payload, input.receivedTimestamp);
309362
- for (const candidate of candidates) {
309363
- const row = buildCandleRow({
309364
- context: input,
309365
- bar: candidate.bar,
309366
- isClosed: candidate.isClosed,
309367
- brokerVersion: candidate.brokerVersion,
309368
- receivedTimestamp: input.receivedTimestamp
309369
- });
309370
- archiver.enqueue(row);
309371
- }
309372
- if (candidates.length > 0) {
309373
- recordWatchMetric(otelMetrics, "cex_watch_frames_archived_total", labels);
309374
- }
309375
- } catch (error48) {
309376
- rethrowArchiveDurabilityError(error48);
309377
- log.warn("Failed to archive OHLCV candle", { error: error48 });
309776
+ this.ws.close();
309777
+ } catch {}
309778
+ this.flushWaiters();
309779
+ }
309780
+ handleClose(code, reason) {
309781
+ if (this.closed) {
309782
+ return;
309378
309783
  }
309379
- });
309380
- }
309381
- function createOrderbookSampler() {
309382
- return new OrderbookSampler;
309383
- }
309384
- function createOhlcvBarTracker() {
309385
- return new OhlcvBarTracker;
309386
- }
309387
- function archiveMarketRowsInBackground(archiver, otelMetrics, stream4, input, enqueueRows) {
309388
- const labels = watchLabels(stream4, input);
309389
- recordWatchMetric(otelMetrics, "cex_watch_frames_received_total", labels);
309390
- if (!isMarketArchiveEnabled() || !archiver?.isEnabled()) {
309391
- return;
309784
+ this.fail(formatBinanceUserDataWebSocketClose(code, reason, this.secretValues));
309392
309785
  }
309393
- queueMicrotask(() => {
309786
+ subscribe() {
309787
+ const apiKey = getExchangeString(this.exchange, "apiKey");
309788
+ const signedParams = signUserDataStreamParams(this.exchange, {
309789
+ apiKey,
309790
+ timestamp: Date.now()
309791
+ });
309792
+ this.ws.send(JSON.stringify({
309793
+ id: this.requestId,
309794
+ method: "userDataStream.subscribe.signature",
309795
+ params: signedParams
309796
+ }));
309797
+ }
309798
+ handleMessage(data) {
309799
+ if (this.closed) {
309800
+ return;
309801
+ }
309802
+ let message;
309394
309803
  try {
309395
- const rows = enqueueRows();
309396
- for (const row of rows) {
309397
- archiver.enqueue(row);
309804
+ const decodedData = decodeMessageData(data);
309805
+ message = typeof decodedData === "string" ? JSON.parse(decodedData) : decodedData;
309806
+ } catch (error48) {
309807
+ this.fail(error48 instanceof Error ? error48 : new Error("Invalid Binance user-data message"));
309808
+ return;
309809
+ }
309810
+ if ("id" in message && message.id === this.requestId) {
309811
+ if (message.status !== 200) {
309812
+ this.fail(new Error(message.error?.msg ?? message.error?.message ?? `Binance user-data subscription failed with status ${message.status}`));
309813
+ return;
309398
309814
  }
309399
- if (rows.length > 0) {
309400
- recordWatchMetric(otelMetrics, "cex_watch_frames_archived_total", labels);
309815
+ this.subscriptionId = message.result?.subscriptionId ?? null;
309816
+ return;
309817
+ }
309818
+ if ("status" in message && typeof message.status === "number" && message.status !== 200) {
309819
+ const errorMessage = message.error?.msg ?? message.error?.message ?? `Binance user-data request failed with status ${message.status}`;
309820
+ const errorCode2 = message.error?.code;
309821
+ this.fail(new Error(typeof errorCode2 === "number" ? `${errorMessage} (code ${errorCode2})` : errorMessage));
309822
+ return;
309823
+ }
309824
+ if (!("event" in message) || !message.event) {
309825
+ return;
309826
+ }
309827
+ const subscriptionId = message.subscriptionId ?? this.subscriptionId;
309828
+ if (subscriptionId === null || subscriptionId === undefined) {
309829
+ return;
309830
+ }
309831
+ this.push({ subscriptionId, event: message.event });
309832
+ }
309833
+ push(event) {
309834
+ if (this.closed) {
309835
+ return;
309836
+ }
309837
+ const waiter = this.waiters.shift();
309838
+ if (waiter) {
309839
+ waiter.resolve(event);
309840
+ return;
309841
+ }
309842
+ if (this.queue.length >= this.maxBufferedEvents) {
309843
+ this.fail(new Error(`Binance user-data stream buffered event limit exceeded (${this.maxBufferedEvents}); downstream consumer is not keeping up`));
309844
+ return;
309845
+ }
309846
+ this.queue.push(event);
309847
+ }
309848
+ nextEvent() {
309849
+ const event = this.queue.shift();
309850
+ if (event) {
309851
+ return Promise.resolve(event);
309852
+ }
309853
+ if (this.closeError) {
309854
+ return Promise.reject(this.closeError);
309855
+ }
309856
+ if (this.closed) {
309857
+ return Promise.resolve(null);
309858
+ }
309859
+ return new Promise((resolve, reject) => {
309860
+ this.waiters.push({ resolve, reject });
309861
+ });
309862
+ }
309863
+ fail(error48) {
309864
+ if (this.closeError) {
309865
+ return;
309866
+ }
309867
+ this.closeError = error48;
309868
+ this.closed = true;
309869
+ this.queue.length = 0;
309870
+ this.flushWaiters();
309871
+ try {
309872
+ this.ws.close();
309873
+ } catch {}
309874
+ }
309875
+ flushWaiters() {
309876
+ const error48 = this.closeError;
309877
+ for (const waiter of this.waiters.splice(0)) {
309878
+ if (error48) {
309879
+ waiter.reject(error48);
309880
+ } else {
309881
+ waiter.resolve(null);
309401
309882
  }
309402
- } catch (error48) {
309403
- rethrowArchiveDurabilityError(error48);
309404
- log.warn(`Failed to archive ${stream4} market data`, { error: error48 });
309405
309883
  }
309406
- });
309407
- }
309408
- function archiveTradesInBackground(archiver, otelMetrics, input) {
309409
- archiveMarketRowsInBackground(archiver, otelMetrics, "trades", input, () => extractTrades(input.payload, input.receivedTimestamp).map((trade) => buildCexTradeRow(input, trade)));
309884
+ }
309410
309885
  }
309411
- function archiveTickerInBackground(archiver, otelMetrics, input) {
309412
- archiveMarketRowsInBackground(archiver, otelMetrics, "ticker", input, () => {
309413
- const ticker = parseTicker(input.payload, input.receivedTimestamp);
309414
- return ticker ? [buildCexTickerEventRow(input, ticker)] : [];
309415
- });
309886
+ function isBinanceBalanceUserDataEvent(event) {
309887
+ return event.e === "outboundAccountPosition" || event.e === "balanceUpdate" || event.e === "externalLockUpdate";
309416
309888
  }
309417
- function archiveCexStreamEventInBackground(archiver, otelMetrics, input) {
309418
- archiveMarketRowsInBackground(archiver, otelMetrics, "stream", input, () => [
309419
- buildCexStreamEventRow(input)
309420
- ]);
309889
+ function isBinanceOrderUserDataEvent(event) {
309890
+ return event.e === "executionReport" || event.e === "listStatus";
309421
309891
  }
309422
309892
  // src/helpers/market-data-archive/ohlcv-bootstrap.ts
309423
309893
  var DEFAULT_OHLCV_BOOTSTRAP_LIMIT = 100;
@@ -309456,6 +309926,7 @@ async function bootstrapOhlcvHistory(broker, archiver, otelMetrics, tracker, inp
309456
309926
  const receivedTimestamp = Date.now();
309457
309927
  archiveOhlcvInBackground(archiver, otelMetrics, tracker, {
309458
309928
  ...input,
309929
+ sourceMode: "broker_bootstrap_fetch_v1",
309459
309930
  payload,
309460
309931
  receivedTimestamp
309461
309932
  });
@@ -310515,7 +310986,7 @@ class CEXBroker {
310515
310986
  if (this.otelMetrics?.isOtelEnabled()) {
310516
310987
  await this.otelMetrics.initialize();
310517
310988
  }
310518
- this.server = getServer(this.policy, this.brokers, this.whitelistIps, this.useVerity, this.#verityProverUrl, this.otelMetrics, this.brokerArchiver, this.orderActivityTracker, this.withdrawalObservationTracker);
310989
+ this.server = getServer(this.policy, this.brokers, this.whitelistIps, this.useVerity, this.#verityProverUrl, this.otelMetrics, this.brokerArchiver, this.orderActivityTracker, this.withdrawalObservationTracker, undefined);
310519
310990
  this.server.bindAsync(`0.0.0.0:${this.port}`, grpc15.ServerCredentials.createInsecure(), (err2, port) => {
310520
310991
  if (err2) {
310521
310992
  log.error(err2);
@@ -310560,4 +311031,4 @@ export {
310560
311031
  CEXBroker as default
310561
311032
  };
310562
311033
 
310563
- //# debugId=10F44147004B63EC64756E2164756E21
311034
+ //# debugId=89161A06846CFA7064756E2164756E21