@usherlabs/cex-broker 0.2.40 → 0.2.42

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
@@ -4,25 +4,43 @@ var __getProtoOf = Object.getPrototypeOf;
4
4
  var __defProp = Object.defineProperty;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ function __accessProp(key) {
8
+ return this[key];
9
+ }
10
+ var __toESMCache_node;
11
+ var __toESMCache_esm;
7
12
  var __toESM = (mod, isNodeMode, target) => {
13
+ var canCache = mod != null && typeof mod === "object";
14
+ if (canCache) {
15
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
16
+ var cached = cache.get(mod);
17
+ if (cached)
18
+ return cached;
19
+ }
8
20
  target = mod != null ? __create(__getProtoOf(mod)) : {};
9
21
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
22
  for (let key of __getOwnPropNames(mod))
11
23
  if (!__hasOwnProp.call(to, key))
12
24
  __defProp(to, key, {
13
- get: () => mod[key],
25
+ get: __accessProp.bind(mod, key),
14
26
  enumerable: true
15
27
  });
28
+ if (canCache)
29
+ cache.set(mod, to);
16
30
  return to;
17
31
  };
18
32
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
33
+ var __returnValue = (v) => v;
34
+ function __exportSetter(name, newValue) {
35
+ this[name] = __returnValue.bind(null, newValue);
36
+ }
19
37
  var __export = (target, all) => {
20
38
  for (var name in all)
21
39
  __defProp(target, name, {
22
40
  get: all[name],
23
41
  enumerable: true,
24
42
  configurable: true,
25
- set: (newValue) => all[name] = () => newValue
43
+ set: __exportSetter.bind(all, name)
26
44
  });
27
45
  };
28
46
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -26363,7 +26381,7 @@ var require_tracestate_impl = __commonJS((exports) => {
26363
26381
  const value = listMember.slice(i2 + 1, part.length);
26364
26382
  if ((0, tracestate_validators_1.validateKey)(key) && (0, tracestate_validators_1.validateValue)(value)) {
26365
26383
  agg.set(key, value);
26366
- } else {}
26384
+ }
26367
26385
  }
26368
26386
  return agg;
26369
26387
  }, new Map);
@@ -35496,7 +35514,7 @@ var require_TraceState = __commonJS((exports) => {
35496
35514
  const value = listMember.slice(i2 + 1, part.length);
35497
35515
  if ((0, validators_1.validateKey)(key) && (0, validators_1.validateValue)(value)) {
35498
35516
  agg.set(key, value);
35499
- } else {}
35517
+ }
35500
35518
  }
35501
35519
  return agg;
35502
35520
  }, new Map);
@@ -290507,6 +290525,20 @@ function validateDeposit(policy, exchange, network, ticker) {
290507
290525
  return { valid: true };
290508
290526
  }
290509
290527
 
290528
+ // src/helpers/broker-execution-archive/rows.ts
290529
+ import { createHash as createHash2 } from "node:crypto";
290530
+
290531
+ // src/helpers/shared/guards.ts
290532
+ function isRecord(value) {
290533
+ return value !== null && typeof value === "object" && !Array.isArray(value);
290534
+ }
290535
+ function asRecord(value) {
290536
+ return isRecord(value) ? value : undefined;
290537
+ }
290538
+
290539
+ // src/helpers/broker-execution-archive/redact.ts
290540
+ import { createHash } from "node:crypto";
290541
+
290510
290542
  // src/helpers/shared/errors.ts
290511
290543
  var MAX_ERROR_DETAIL_LENGTH = 512;
290512
290544
  var REDACTED_ERROR_MESSAGE = "redacted_error";
@@ -290566,226 +290598,7 @@ function safeLogRedactedError(context2, error) {
290566
290598
  }
290567
290599
  }
290568
290600
 
290569
- // src/helpers/shared/guards.ts
290570
- function isRecord(value) {
290571
- return value !== null && typeof value === "object" && !Array.isArray(value);
290572
- }
290573
- function asRecord(value) {
290574
- return isRecord(value) ? value : undefined;
290575
- }
290576
-
290577
- // src/helpers/order-telemetry.ts
290578
- var NUMERIC_METRICS = [
290579
- ["requestedQuantity", "cex_market_action_requested_quantity"],
290580
- ["requestedNotional", "cex_market_action_requested_notional"],
290581
- ["executedBaseQuantity", "cex_market_action_executed_base_quantity"],
290582
- ["executedQuoteQuantity", "cex_market_action_executed_quote_quantity"],
290583
- ["averageExecutionPrice", "cex_market_action_average_execution_price"],
290584
- ["filledAmount", "cex_market_action_filled_amount"],
290585
- ["remainingAmount", "cex_market_action_remaining_amount"],
290586
- ["feeAmount", "cex_market_action_fee_amount"],
290587
- ["feeRate", "cex_market_action_fee_rate"]
290588
- ];
290589
- async function emitOrderExecutionTelemetry(otelMetrics, context2, order, error) {
290590
- try {
290591
- const telemetry = buildOrderExecutionTelemetry(context2, order, error);
290592
- log.info("CEX market action execution telemetry", telemetry);
290593
- const labels = {
290594
- action: telemetry.action,
290595
- cex: telemetry.cex,
290596
- account: telemetry.accountLabel,
290597
- symbol: telemetry.symbol,
290598
- side: telemetry.side,
290599
- order_type: telemetry.orderType,
290600
- status: telemetry.status
290601
- };
290602
- await otelMetrics?.recordCounter("cex_market_action_executions_total", 1, {
290603
- ...labels,
290604
- result: error ? "error" : "ok"
290605
- });
290606
- for (const [key, metricName] of NUMERIC_METRICS) {
290607
- const value = telemetry[key];
290608
- if (typeof value === "number" && Number.isFinite(value)) {
290609
- await otelMetrics?.recordHistogram(metricName, value, labels);
290610
- }
290611
- }
290612
- return telemetry;
290613
- } catch (telemetryError) {
290614
- try {
290615
- log.error("Failed to emit CEX order telemetry", {
290616
- error: telemetryError
290617
- });
290618
- } catch {}
290619
- return;
290620
- }
290621
- }
290622
- function buildOrderExecutionTelemetry(context2, order, error) {
290623
- const record = asRecord(order);
290624
- const info = asRecord(record?.info);
290625
- const fees = getFees(record, info);
290626
- const fee = summarizeFees(fees);
290627
- const executedBaseQuantity = firstNumber(record?.filled, info?.executedQty, info?.cumExecQty) ?? computeFilledFromAmount(record);
290628
- const executedQuoteQuantity = firstNumber(record?.cost, info?.cummulativeQuoteQty, info?.cumQuote, info?.cumExecValue);
290629
- const averageExecutionPrice = firstNumber(record?.average, info?.avgPrice) ?? computeAveragePrice(executedBaseQuantity, executedQuoteQuantity);
290630
- const exchangeTimestamp = normalizeTimestamp(firstValue(record?.timestamp, info?.time, info?.transactTime, record?.datetime));
290631
- const status = firstString(record?.status, info?.status) ?? (error ? "failed" : "unknown");
290632
- const errorRecord = error instanceof Error ? error : undefined;
290633
- return compactUndefined({
290634
- event: "cex_market_action_execution",
290635
- action: context2.action,
290636
- cex: context2.cex.trim().toLowerCase() || "unknown",
290637
- accountLabel: context2.accountLabel ?? "unknown",
290638
- symbol: firstString(record?.symbol, info?.symbol, context2.symbol) ?? "unknown",
290639
- side: firstString(record?.side, info?.side, context2.side) ?? "unknown",
290640
- orderType: firstString(record?.type, info?.type, context2.orderType) ?? "unknown",
290641
- orderId: firstString(record?.id, info?.orderId, info?.orderID),
290642
- orderAuthor: context2.orderAuthor,
290643
- clientOrderId: firstString(context2.clientOrderId, record?.clientOrderId, record?.clientOrderID, record?.clientOid, info?.clientOrderId, info?.clientOrderID, info?.clientOid),
290644
- idempotencyId: context2.idempotencyId,
290645
- makerActionId: context2.makerActionId,
290646
- status: status.toLowerCase(),
290647
- requestedQuantity: context2.requestedQuantity ?? firstNumber(record?.amount),
290648
- requestedNotional: context2.requestedNotional,
290649
- executedBaseQuantity,
290650
- executedQuoteQuantity,
290651
- averageExecutionPrice,
290652
- filledAmount: firstNumber(record?.filled, info?.executedQty),
290653
- remainingAmount: firstNumber(record?.remaining, info?.remainingQty),
290654
- feeAmount: fee.amount,
290655
- feeCurrency: fee.currency,
290656
- feeRate: fee.rate,
290657
- exchangeTimestamp,
290658
- brokerObservedTimestamp: context2.brokerObservedTimestamp ?? new Date().toISOString(),
290659
- errorType: errorRecord?.name,
290660
- errorMessage: errorRecord ? REDACTED_ERROR_MESSAGE : undefined
290661
- });
290662
- }
290663
- function emitOrderExecutionTelemetryInBackground(otelMetrics, context2, order, error) {
290664
- emitOrderExecutionTelemetry(otelMetrics, context2, order, error).catch((telemetryError) => {
290665
- try {
290666
- log.warn("Telemetry emit failed", { error: telemetryError });
290667
- } catch {
290668
- console.warn("Telemetry emit failed", telemetryError);
290669
- }
290670
- });
290671
- }
290672
- function extractOrderTelemetryIds(params) {
290673
- const record = params ?? {};
290674
- return {
290675
- clientOrderId: firstString(record.clientOrderId, record.clientOrderID, record.newClientOrderId, record.clientOid),
290676
- idempotencyId: firstString(record.idempotencyId, record.idempotencyID, record.idempotencyKey, record.requestId, record.requestID),
290677
- makerActionId: firstString(record.makerActionId, record.maker_action_id, record.actionId, record.action_id)
290678
- };
290679
- }
290680
- function firstValue(...values2) {
290681
- return values2.find((value) => value !== undefined && value !== null);
290682
- }
290683
- function firstString(...values2) {
290684
- for (const value of values2) {
290685
- if (typeof value === "string" && value.trim()) {
290686
- return value.trim();
290687
- }
290688
- if (typeof value === "number" && Number.isFinite(value)) {
290689
- return String(value);
290690
- }
290691
- }
290692
- return;
290693
- }
290694
- function firstNumber(...values2) {
290695
- for (const value of values2) {
290696
- const numberValue = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
290697
- if (Number.isFinite(numberValue)) {
290698
- return numberValue;
290699
- }
290700
- }
290701
- return;
290702
- }
290703
- function computeFilledFromAmount(record) {
290704
- const amount = firstNumber(record?.amount);
290705
- const remaining = firstNumber(record?.remaining);
290706
- if (amount === undefined || remaining === undefined) {
290707
- return;
290708
- }
290709
- return amount - remaining;
290710
- }
290711
- function computeAveragePrice(executedBaseQuantity, executedQuoteQuantity) {
290712
- if (executedBaseQuantity === undefined || executedQuoteQuantity === undefined || executedBaseQuantity === 0) {
290713
- return;
290714
- }
290715
- return executedQuoteQuantity / executedBaseQuantity;
290716
- }
290717
- function normalizeTimestamp(value) {
290718
- if (typeof value === "string" && value.trim()) {
290719
- return value.trim();
290720
- }
290721
- const timestamp = firstNumber(value);
290722
- if (timestamp === undefined) {
290723
- return;
290724
- }
290725
- const date = new Date(timestamp);
290726
- return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
290727
- }
290728
- function getFees(record, info) {
290729
- const fees = [];
290730
- if (record?.fee)
290731
- fees.push(record.fee);
290732
- if (Array.isArray(record?.fees))
290733
- fees.push(...record.fees);
290734
- if (Array.isArray(record?.trades)) {
290735
- for (const trade of record.trades) {
290736
- const tradeRecord = asRecord(trade);
290737
- if (tradeRecord?.fee)
290738
- fees.push(tradeRecord.fee);
290739
- if (Array.isArray(tradeRecord?.fees))
290740
- fees.push(...tradeRecord.fees);
290741
- }
290742
- }
290743
- if (Array.isArray(info?.fills)) {
290744
- for (const fill of info.fills) {
290745
- const fillRecord = asRecord(fill);
290746
- const commission = firstNumber(fillRecord?.commission);
290747
- if (commission !== undefined) {
290748
- fees.push({
290749
- cost: commission,
290750
- currency: firstString(fillRecord?.commissionAsset)
290751
- });
290752
- }
290753
- }
290754
- }
290755
- return fees;
290756
- }
290757
- function summarizeFees(fees) {
290758
- let amount = 0;
290759
- let amountFound = false;
290760
- let currency;
290761
- let rate;
290762
- for (const rawFee of fees) {
290763
- const fee = asRecord(rawFee);
290764
- if (!fee)
290765
- continue;
290766
- const cost = firstNumber(fee.cost, fee.amount);
290767
- if (cost !== undefined) {
290768
- amount += cost;
290769
- amountFound = true;
290770
- }
290771
- currency ??= firstString(fee.currency);
290772
- rate ??= firstNumber(fee.rate);
290773
- }
290774
- return {
290775
- amount: amountFound ? amount : undefined,
290776
- currency,
290777
- rate
290778
- };
290779
- }
290780
- function compactUndefined(record) {
290781
- return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined));
290782
- }
290783
-
290784
- // src/helpers/broker-execution-archive/rows.ts
290785
- import { createHash as createHash2 } from "node:crypto";
290786
-
290787
290601
  // src/helpers/broker-execution-archive/redact.ts
290788
- import { createHash } from "node:crypto";
290789
290602
  var SECRET_KEY_PATTERN = /\b(api[_-]?key|api[_-]?secret|secret|signature|passphrase|password|token|credential)\b/i;
290790
290603
  var SECRET_VALUE_PATTERN = /(\b(?:apiKey|api_key|apiSecret|api_secret|secret|signature|passphrase|password|token)\b\s*[=:]\s*)[^\s&,;)}\]]+/gi;
290791
290604
  var SECRET_JSON_PATTERN = /("(?:apiKey|api_key|apiSecret|api_secret|secret|signature|passphrase|password|token)"\s*:\s*")[^"]*(")/gi;
@@ -290858,9 +290671,11 @@ var ARCHIVE_SCHEMA_VERSION = "1";
290858
290671
  var FILL_EVENT_KIND = "trade_history_fill";
290859
290672
  var ACCOUNT_BALANCE_SCOPE = "spot";
290860
290673
  var ACCOUNT_BALANCE_PRECISION_BASIS = "ccxt_normalized_number";
290674
+ var USER_ASSET_BALANCE_SCOPE = "user_asset";
290675
+ var USER_ASSET_PRECISION_BASIS = "venue_raw_string";
290861
290676
 
290862
290677
  // src/helpers/broker-execution-archive/rows.ts
290863
- function firstString2(...values2) {
290678
+ function firstString(...values2) {
290864
290679
  for (const value of values2) {
290865
290680
  if (typeof value === "string" && value.trim()) {
290866
290681
  return value.trim();
@@ -290871,7 +290686,7 @@ function firstString2(...values2) {
290871
290686
  }
290872
290687
  return;
290873
290688
  }
290874
- function firstNumber2(...values2) {
290689
+ function firstNumber(...values2) {
290875
290690
  for (const value of values2) {
290876
290691
  const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
290877
290692
  if (Number.isFinite(numeric)) {
@@ -290880,11 +290695,11 @@ function firstNumber2(...values2) {
290880
290695
  }
290881
290696
  return;
290882
290697
  }
290883
- function normalizeTimestamp2(value) {
290698
+ function normalizeTimestamp(value) {
290884
290699
  if (typeof value === "string" && value.trim()) {
290885
290700
  return value.trim();
290886
290701
  }
290887
- const ms = firstNumber2(value);
290702
+ const ms = firstNumber(value);
290888
290703
  if (ms === undefined) {
290889
290704
  return;
290890
290705
  }
@@ -290991,7 +290806,7 @@ function normalizeCcxtBalanceForArchive(balance) {
290991
290806
  }
290992
290807
  }
290993
290808
  return {
290994
- exchangeTimestamp: normalizeTimestamp2(record.timestamp ?? record.datetime),
290809
+ exchangeTimestamp: normalizeTimestamp(record.timestamp ?? record.datetime),
290995
290810
  reportedAssets: [...reportedAssets].sort(),
290996
290811
  assetEntryAssets: [...assetEntryAssets].sort(),
290997
290812
  freeBalances: sortedBalanceMap(maps.free),
@@ -291014,7 +290829,7 @@ function buildAccountBalanceSnapshotRow(input) {
291014
290829
  })).digest("hex");
291015
290830
  return {
291016
290831
  table: "broker_account.balance_snapshots",
291017
- row: compactUndefined2({
290832
+ row: compactUndefined({
291018
290833
  broker_observed_timestamp: tags.broker_observed_timestamp,
291019
290834
  exchange_timestamp: balance.exchangeTimestamp,
291020
290835
  source: tags.source,
@@ -291036,7 +290851,101 @@ function buildAccountBalanceSnapshotRow(input) {
291036
290851
  })
291037
290852
  };
291038
290853
  }
291039
- function compactUndefined2(record) {
290854
+ var USER_ASSET_BUCKETS = {
290855
+ freeBalances: "free",
290856
+ lockedBalances: "locked",
290857
+ freezeBalances: "freeze",
290858
+ withdrawingBalances: "withdrawing"
290859
+ };
290860
+ function userAssetQuantity(value) {
290861
+ if (typeof value === "number") {
290862
+ return decimalString(value);
290863
+ }
290864
+ if (typeof value !== "string") {
290865
+ return;
290866
+ }
290867
+ const trimmed = value.trim();
290868
+ if (!trimmed || !Number.isFinite(Number(trimmed))) {
290869
+ return;
290870
+ }
290871
+ return trimmed;
290872
+ }
290873
+ function normalizeBinanceUserAssetsForArchive(response) {
290874
+ if (!Array.isArray(response)) {
290875
+ throw new Error("binance_user_asset_malformed_response: getUserAsset did not return an array");
290876
+ }
290877
+ const reportedAssets = [];
290878
+ const incompleteAssets = [];
290879
+ const maps = {
290880
+ freeBalances: {},
290881
+ lockedBalances: {},
290882
+ freezeBalances: {},
290883
+ withdrawingBalances: {}
290884
+ };
290885
+ for (const entry of response) {
290886
+ const record = asRecord(entry);
290887
+ const asset = typeof record?.asset === "string" ? record.asset.trim() : undefined;
290888
+ if (!record || !asset) {
290889
+ throw new Error("binance_user_asset_malformed_response: getUserAsset entry has no asset key");
290890
+ }
290891
+ if (reportedAssets.includes(asset)) {
290892
+ throw new Error(`binance_user_asset_malformed_response: getUserAsset returned duplicate entries for ${asset}`);
290893
+ }
290894
+ reportedAssets.push(asset);
290895
+ let complete = true;
290896
+ for (const [field, venueKey] of Object.entries(USER_ASSET_BUCKETS)) {
290897
+ const quantity = userAssetQuantity(record[venueKey]);
290898
+ if (quantity === undefined) {
290899
+ complete = false;
290900
+ continue;
290901
+ }
290902
+ maps[field][asset] = quantity;
290903
+ }
290904
+ if (!complete) {
290905
+ incompleteAssets.push(asset);
290906
+ }
290907
+ }
290908
+ return {
290909
+ reportedAssets: reportedAssets.sort(),
290910
+ incompleteAssets: incompleteAssets.sort(),
290911
+ freeBalances: sortedBalanceMap(maps.freeBalances),
290912
+ lockedBalances: sortedBalanceMap(maps.lockedBalances),
290913
+ freezeBalances: sortedBalanceMap(maps.freezeBalances),
290914
+ withdrawingBalances: sortedBalanceMap(maps.withdrawingBalances)
290915
+ };
290916
+ }
290917
+ function buildUserAssetSnapshotRow(input) {
290918
+ const { tags, userAssets } = input;
290919
+ const observationId = createHash2("sha256").update(JSON.stringify({
290920
+ deployment_id: tags.deployment_id,
290921
+ exchange: tags.exchange,
290922
+ account_selector: tags.account_selector,
290923
+ balance_scope: USER_ASSET_BALANCE_SCOPE,
290924
+ broker_observed_timestamp: tags.broker_observed_timestamp,
290925
+ userAssets
290926
+ })).digest("hex");
290927
+ return {
290928
+ table: "broker_account.user_asset_snapshots",
290929
+ row: {
290930
+ broker_observed_timestamp: tags.broker_observed_timestamp,
290931
+ source: tags.source,
290932
+ deployment_id: tags.deployment_id,
290933
+ schema_version: ARCHIVE_SCHEMA_VERSION,
290934
+ exchange: tags.exchange,
290935
+ account_selector: tags.account_selector,
290936
+ balance_scope: USER_ASSET_BALANCE_SCOPE,
290937
+ observation_id: observationId,
290938
+ reported_assets: userAssets.reportedAssets,
290939
+ incomplete_assets: userAssets.incompleteAssets,
290940
+ free_balances: userAssets.freeBalances,
290941
+ locked_balances: userAssets.lockedBalances,
290942
+ freeze_balances: userAssets.freezeBalances,
290943
+ withdrawing_balances: userAssets.withdrawingBalances,
290944
+ precision_basis: USER_ASSET_PRECISION_BASIS
290945
+ }
290946
+ };
290947
+ }
290948
+ function compactUndefined(record) {
291040
290949
  return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined));
291041
290950
  }
291042
290951
  function buildCommonArchiveTags(input) {
@@ -291053,7 +290962,7 @@ function buildOrderEventArchiveRow(input) {
291053
290962
  const { tags, telemetry, action } = input;
291054
290963
  return {
291055
290964
  table: "broker_execution.order_events",
291056
- row: compactUndefined2({
290965
+ row: compactUndefined({
291057
290966
  ...tags,
291058
290967
  event_kind: input.eventKind ?? "execute_action",
291059
290968
  action,
@@ -291090,35 +290999,35 @@ function buildSubscribeStreamArchiveRow(input) {
291090
290999
  const info = asRecord(record?.info);
291091
291000
  return {
291092
291001
  table: "broker_execution.order_events",
291093
- row: compactUndefined2({
291002
+ row: compactUndefined({
291094
291003
  ...input.tags,
291095
291004
  event_kind: "subscribe_stream",
291096
291005
  subscription_type: input.subscriptionType,
291097
- order_id: firstString2(record?.id, record?.orderId, record?.i, info?.orderId, info?.i),
291098
- client_order_id: firstString2(record?.clientOrderId, record?.clientOrderID, record?.c, info?.clientOrderId, info?.c),
291099
- status: firstString2(record?.status, record?.X, info?.status, info?.X)?.toLowerCase(),
291006
+ order_id: firstString(record?.id, record?.orderId, record?.i, info?.orderId, info?.i),
291007
+ client_order_id: firstString(record?.clientOrderId, record?.clientOrderID, record?.c, info?.clientOrderId, info?.c),
291008
+ status: firstString(record?.status, record?.X, info?.status, info?.X)?.toLowerCase(),
291100
291009
  payload_json: JSON.stringify(redactedPayload)
291101
291010
  })
291102
291011
  };
291103
291012
  }
291104
291013
  function quantityString(...values2) {
291105
- return firstString2(...values2);
291014
+ return firstString(...values2);
291106
291015
  }
291107
291016
  function extractBinanceInternalTransferId(response) {
291108
291017
  const record = asRecord(response);
291109
- return firstString2(record?.txnId, record?.tranId);
291018
+ return firstString(record?.txnId, record?.tranId);
291110
291019
  }
291111
291020
  function buildTransferEventArchiveRow(input) {
291112
291021
  const { tags, transfer } = input;
291113
291022
  return {
291114
291023
  table: "broker_execution.transfer_events",
291115
- row: compactUndefined2({
291024
+ row: compactUndefined({
291116
291025
  ...tags,
291117
291026
  schema_version: ARCHIVE_SCHEMA_VERSION,
291118
291027
  event_kind: transfer.eventKind,
291119
291028
  lifecycle_action: transfer.lifecycleAction,
291120
291029
  status: transfer.status ?? "",
291121
- asset_symbol: tags.symbol,
291030
+ asset_symbol: transfer.assetSymbol ?? tags.symbol,
291122
291031
  amount: transfer.amount,
291123
291032
  address: transfer.address,
291124
291033
  network: transfer.network,
@@ -291138,18 +291047,18 @@ function normalizeCcxtTransactionForArchive(transaction) {
291138
291047
  const record = asRecord(transaction);
291139
291048
  const info = asRecord(record?.info);
291140
291049
  const fee = asRecord(record?.fee);
291141
- return compactUndefined2({
291142
- externalId: firstString2(record?.id, info?.id, record?.txid, info?.txId),
291143
- clientWithdrawalId: firstString2(info?.withdrawOrderId),
291144
- txid: firstString2(record?.txid, info?.txId, info?.txid, info?.tx_hash),
291145
- address: firstString2(record?.address, record?.addressTo, info?.address),
291146
- network: firstString2(record?.network, info?.network),
291050
+ return compactUndefined({
291051
+ externalId: firstString(record?.id, info?.id, record?.txid, info?.txId),
291052
+ clientWithdrawalId: firstString(info?.withdrawOrderId),
291053
+ txid: firstString(record?.txid, info?.txId, info?.txid, info?.tx_hash),
291054
+ address: firstString(record?.address, record?.addressTo, info?.address),
291055
+ network: firstString(record?.network, info?.network),
291147
291056
  amount: quantityString(info?.amount, record?.amount),
291148
- assetSymbol: firstString2(record?.currency, info?.coin, info?.asset),
291149
- status: firstString2(record?.status, info?.status)?.toLowerCase(),
291057
+ assetSymbol: firstString(record?.currency, info?.coin, info?.asset),
291058
+ status: firstString(record?.status, info?.status)?.toLowerCase(),
291150
291059
  feeAmount: quantityString(fee?.cost, record?.feeCost),
291151
- feeCurrency: firstString2(fee?.currency, record?.feeCurrency),
291152
- exchangeTimestamp: normalizeTimestamp2(firstValueForTransfer(record?.timestamp, record?.datetime, info?.applyTime))
291060
+ feeCurrency: firstString(fee?.currency, record?.feeCurrency),
291061
+ exchangeTimestamp: normalizeTimestamp(firstValueForTransfer(record?.timestamp, record?.datetime, info?.applyTime))
291153
291062
  });
291154
291063
  }
291155
291064
  function firstValueForTransfer(...values2) {
@@ -291159,7 +291068,7 @@ function buildFillEventArchiveRow(input) {
291159
291068
  const { tags, fill } = input;
291160
291069
  return {
291161
291070
  table: "broker_execution.fill_events",
291162
- row: compactUndefined2({
291071
+ row: compactUndefined({
291163
291072
  ...tags,
291164
291073
  schema_version: ARCHIVE_SCHEMA_VERSION,
291165
291074
  event_kind: FILL_EVENT_KIND,
@@ -291185,18 +291094,18 @@ function normalizeCcxtTradeForArchive(trade) {
291185
291094
  const info = asRecord(record?.info);
291186
291095
  const fee = asRecord(record?.fee);
291187
291096
  return {
291188
- orderId: firstString2(record?.order, info?.orderId, info?.orderID),
291189
- clientOrderId: firstString2(record?.clientOrderId, info?.clientOrderId, info?.origClientOrderId),
291190
- fillId: firstString2(record?.id, info?.id, info?.tradeId),
291191
- side: firstString2(record?.side, info?.side)?.toLowerCase(),
291192
- orderType: firstString2(record?.type, info?.type)?.toLowerCase(),
291097
+ orderId: firstString(record?.order, info?.orderId, info?.orderID),
291098
+ clientOrderId: firstString(record?.clientOrderId, info?.clientOrderId, info?.origClientOrderId),
291099
+ fillId: firstString(record?.id, info?.id, info?.tradeId),
291100
+ side: firstString(record?.side, info?.side)?.toLowerCase(),
291101
+ orderType: firstString(record?.type, info?.type)?.toLowerCase(),
291193
291102
  price: quantityString(info?.price, record?.price),
291194
291103
  baseQuantity: quantityString(info?.qty, record?.amount),
291195
291104
  quoteQuantity: quantityString(info?.quoteQty, record?.cost),
291196
291105
  feeAmount: quantityString(fee?.cost, info?.commission),
291197
- feeCurrency: firstString2(fee?.currency, info?.commissionAsset),
291106
+ feeCurrency: firstString(fee?.currency, info?.commissionAsset),
291198
291107
  feeRate: quantityString(fee?.rate),
291199
- exchangeTimestamp: normalizeTimestamp2(firstValueForTransfer(record?.timestamp, record?.datetime, info?.time)),
291108
+ exchangeTimestamp: normalizeTimestamp(firstValueForTransfer(record?.timestamp, record?.datetime, info?.time)),
291200
291109
  payload: trade
291201
291110
  };
291202
291111
  }
@@ -291205,7 +291114,7 @@ function buildMarketMetadataSnapshotRow(input) {
291205
291114
  const metadataHash = hashMarketMetadata(redactedSnapshot);
291206
291115
  return {
291207
291116
  table: "broker_execution.market_metadata_snapshots",
291208
- row: compactUndefined2({
291117
+ row: compactUndefined({
291209
291118
  ...input.tags,
291210
291119
  client_order_id: input.clientOrderId,
291211
291120
  order_id: input.orderId,
@@ -291217,10 +291126,209 @@ function buildMarketMetadataSnapshotRow(input) {
291217
291126
  };
291218
291127
  }
291219
291128
 
291129
+ // src/helpers/order-telemetry.ts
291130
+ var NUMERIC_METRICS = [
291131
+ ["requestedQuantity", "cex_market_action_requested_quantity"],
291132
+ ["requestedNotional", "cex_market_action_requested_notional"],
291133
+ ["executedBaseQuantity", "cex_market_action_executed_base_quantity"],
291134
+ ["executedQuoteQuantity", "cex_market_action_executed_quote_quantity"],
291135
+ ["averageExecutionPrice", "cex_market_action_average_execution_price"],
291136
+ ["filledAmount", "cex_market_action_filled_amount"],
291137
+ ["remainingAmount", "cex_market_action_remaining_amount"],
291138
+ ["feeAmount", "cex_market_action_fee_amount"],
291139
+ ["feeRate", "cex_market_action_fee_rate"]
291140
+ ];
291141
+ async function emitOrderExecutionTelemetry(otelMetrics, context2, order, error) {
291142
+ try {
291143
+ const telemetry = buildOrderExecutionTelemetry(context2, order, error);
291144
+ log.info("CEX market action execution telemetry", telemetry);
291145
+ const labels = {
291146
+ action: telemetry.action,
291147
+ cex: telemetry.cex,
291148
+ account: telemetry.accountLabel,
291149
+ symbol: telemetry.symbol,
291150
+ side: telemetry.side,
291151
+ order_type: telemetry.orderType,
291152
+ status: telemetry.status
291153
+ };
291154
+ await otelMetrics?.recordCounter("cex_market_action_executions_total", 1, {
291155
+ ...labels,
291156
+ result: error ? "error" : "ok"
291157
+ });
291158
+ for (const [key, metricName] of NUMERIC_METRICS) {
291159
+ const value = telemetry[key];
291160
+ if (typeof value === "number" && Number.isFinite(value)) {
291161
+ await otelMetrics?.recordHistogram(metricName, value, labels);
291162
+ }
291163
+ }
291164
+ return telemetry;
291165
+ } catch (telemetryError) {
291166
+ try {
291167
+ log.error("Failed to emit CEX order telemetry", {
291168
+ error: telemetryError
291169
+ });
291170
+ } catch {}
291171
+ return;
291172
+ }
291173
+ }
291174
+ function buildOrderExecutionTelemetry(context2, order, error) {
291175
+ const record = asRecord(order);
291176
+ const info = asRecord(record?.info);
291177
+ const fees = getFees(record, info);
291178
+ const fee = summarizeFees(fees);
291179
+ const executedBaseQuantity = firstNumber2(record?.filled, info?.executedQty, info?.cumExecQty) ?? computeFilledFromAmount(record);
291180
+ const executedQuoteQuantity = firstNumber2(record?.cost, info?.cummulativeQuoteQty, info?.cumQuote, info?.cumExecValue);
291181
+ const averageExecutionPrice = firstNumber2(record?.average, info?.avgPrice) ?? computeAveragePrice(executedBaseQuantity, executedQuoteQuantity);
291182
+ const exchangeTimestamp = normalizeTimestamp(firstValue(record?.timestamp, info?.time, info?.transactTime, record?.datetime));
291183
+ const status = firstString2(record?.status, info?.status) ?? (error ? "failed" : "unknown");
291184
+ const errorRecord = error instanceof Error ? error : undefined;
291185
+ return compactUndefined2({
291186
+ event: "cex_market_action_execution",
291187
+ action: context2.action,
291188
+ cex: context2.cex.trim().toLowerCase() || "unknown",
291189
+ accountLabel: context2.accountLabel ?? "unknown",
291190
+ symbol: firstString2(record?.symbol, info?.symbol, context2.symbol) ?? "unknown",
291191
+ side: firstString2(record?.side, info?.side, context2.side) ?? "unknown",
291192
+ orderType: firstString2(record?.type, info?.type, context2.orderType) ?? "unknown",
291193
+ orderId: firstString2(record?.id, info?.orderId, info?.orderID),
291194
+ orderAuthor: context2.orderAuthor,
291195
+ clientOrderId: firstString2(context2.clientOrderId, record?.clientOrderId, record?.clientOrderID, record?.clientOid, info?.clientOrderId, info?.clientOrderID, info?.clientOid),
291196
+ idempotencyId: context2.idempotencyId,
291197
+ makerActionId: context2.makerActionId,
291198
+ status: status.toLowerCase(),
291199
+ requestedQuantity: context2.requestedQuantity ?? firstNumber2(record?.amount),
291200
+ requestedNotional: context2.requestedNotional,
291201
+ executedBaseQuantity,
291202
+ executedQuoteQuantity,
291203
+ averageExecutionPrice,
291204
+ filledAmount: firstNumber2(record?.filled, info?.executedQty),
291205
+ remainingAmount: firstNumber2(record?.remaining, info?.remainingQty),
291206
+ feeAmount: fee.amount,
291207
+ feeCurrency: fee.currency,
291208
+ feeRate: fee.rate,
291209
+ exchangeTimestamp,
291210
+ brokerObservedTimestamp: context2.brokerObservedTimestamp ?? new Date().toISOString(),
291211
+ errorType: errorRecord?.name,
291212
+ errorMessage: errorRecord ? REDACTED_ERROR_MESSAGE : undefined
291213
+ });
291214
+ }
291215
+ function emitOrderExecutionTelemetryInBackground(otelMetrics, context2, order, error) {
291216
+ emitOrderExecutionTelemetry(otelMetrics, context2, order, error).catch((telemetryError) => {
291217
+ try {
291218
+ log.warn("Telemetry emit failed", { error: telemetryError });
291219
+ } catch {
291220
+ console.warn("Telemetry emit failed", telemetryError);
291221
+ }
291222
+ });
291223
+ }
291224
+ function extractOrderTelemetryIds(params) {
291225
+ const record = params ?? {};
291226
+ return {
291227
+ clientOrderId: firstString2(record.clientOrderId, record.clientOrderID, record.newClientOrderId, record.clientOid),
291228
+ idempotencyId: firstString2(record.idempotencyId, record.idempotencyID, record.idempotencyKey, record.requestId, record.requestID),
291229
+ makerActionId: firstString2(record.makerActionId, record.maker_action_id, record.actionId, record.action_id)
291230
+ };
291231
+ }
291232
+ function firstValue(...values2) {
291233
+ return values2.find((value) => value !== undefined && value !== null);
291234
+ }
291235
+ function firstString2(...values2) {
291236
+ for (const value of values2) {
291237
+ if (typeof value === "string" && value.trim()) {
291238
+ return value.trim();
291239
+ }
291240
+ if (typeof value === "number" && Number.isFinite(value)) {
291241
+ return String(value);
291242
+ }
291243
+ }
291244
+ return;
291245
+ }
291246
+ function firstNumber2(...values2) {
291247
+ for (const value of values2) {
291248
+ const numberValue = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
291249
+ if (Number.isFinite(numberValue)) {
291250
+ return numberValue;
291251
+ }
291252
+ }
291253
+ return;
291254
+ }
291255
+ function computeFilledFromAmount(record) {
291256
+ const amount = firstNumber2(record?.amount);
291257
+ const remaining = firstNumber2(record?.remaining);
291258
+ if (amount === undefined || remaining === undefined) {
291259
+ return;
291260
+ }
291261
+ return amount - remaining;
291262
+ }
291263
+ function computeAveragePrice(executedBaseQuantity, executedQuoteQuantity) {
291264
+ if (executedBaseQuantity === undefined || executedQuoteQuantity === undefined || executedBaseQuantity === 0) {
291265
+ return;
291266
+ }
291267
+ return executedQuoteQuantity / executedBaseQuantity;
291268
+ }
291269
+ function getFees(record, info) {
291270
+ const fees = [];
291271
+ if (record?.fee)
291272
+ fees.push(record.fee);
291273
+ if (Array.isArray(record?.fees))
291274
+ fees.push(...record.fees);
291275
+ if (Array.isArray(record?.trades)) {
291276
+ for (const trade of record.trades) {
291277
+ const tradeRecord = asRecord(trade);
291278
+ if (tradeRecord?.fee)
291279
+ fees.push(tradeRecord.fee);
291280
+ if (Array.isArray(tradeRecord?.fees))
291281
+ fees.push(...tradeRecord.fees);
291282
+ }
291283
+ }
291284
+ if (Array.isArray(info?.fills)) {
291285
+ for (const fill of info.fills) {
291286
+ const fillRecord = asRecord(fill);
291287
+ const commission = firstNumber2(fillRecord?.commission);
291288
+ if (commission !== undefined) {
291289
+ fees.push({
291290
+ cost: commission,
291291
+ currency: firstString2(fillRecord?.commissionAsset)
291292
+ });
291293
+ }
291294
+ }
291295
+ }
291296
+ return fees;
291297
+ }
291298
+ function summarizeFees(fees) {
291299
+ let amount = 0;
291300
+ let amountFound = false;
291301
+ let currency;
291302
+ let rate;
291303
+ for (const rawFee of fees) {
291304
+ const fee = asRecord(rawFee);
291305
+ if (!fee)
291306
+ continue;
291307
+ const cost = firstNumber2(fee.cost, fee.amount);
291308
+ if (cost !== undefined) {
291309
+ amount += cost;
291310
+ amountFound = true;
291311
+ }
291312
+ currency ??= firstString2(fee.currency);
291313
+ rate ??= firstNumber2(fee.rate);
291314
+ }
291315
+ return {
291316
+ amount: amountFound ? amount : undefined,
291317
+ currency,
291318
+ rate
291319
+ };
291320
+ }
291321
+ function compactUndefined2(record) {
291322
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined));
291323
+ }
291324
+
291220
291325
  // src/helpers/broker-execution-archive/writer.ts
291221
291326
  var import_api_logs2 = __toESM(require_src4(), 1);
291327
+ import { randomUUID } from "node:crypto";
291222
291328
  import { closeSync, fsyncSync, openSync, writeSync } from "node:fs";
291223
- import { request as httpRequest2 } from "node:http";
291329
+ import {
291330
+ request as httpRequest2
291331
+ } from "node:http";
291224
291332
  import { request as httpsRequest2 } from "node:https";
291225
291333
  var BROKER_EXECUTION_ARCHIVE_TABLES = new Set([
291226
291334
  "broker_execution.order_events",
@@ -291248,6 +291356,7 @@ var DEFAULT_BATCH_SIZE = 10;
291248
291356
  var DEFAULT_FLUSH_INTERVAL_MS = 1000;
291249
291357
  var DEFAULT_FORWARDER_TIMEOUT_MS = 3000;
291250
291358
  var SHED_WARN_INTERVAL_MS = 60000;
291359
+ var MAX_PINNED_BATCH_ATTEMPTS = 10;
291251
291360
  var MARKET_FEEDS = new Set(["ORDERBOOK", "TICKER", "TRADES", "OHLCV"]);
291252
291361
  function archiveFeed(row) {
291253
291362
  const declared = row.row.feed ?? row.row.stream_type;
@@ -291293,14 +291402,26 @@ class BrokerExecutionArchiver {
291293
291402
  flushIntervalMs;
291294
291403
  forwarderTimeoutMs;
291295
291404
  queue = [];
291405
+ enqueueTimes = new WeakMap;
291296
291406
  stats = {
291297
291407
  enqueued: 0,
291298
291408
  shed: 0,
291299
291409
  flushed: 0,
291300
291410
  forwarderFailures: 0
291301
291411
  };
291412
+ pendingRetry = null;
291302
291413
  flushTimer = null;
291303
291414
  flushInFlight = null;
291415
+ inFlightBatch = null;
291416
+ inFlightStartedAtMs = null;
291417
+ lastFailureAtMs = null;
291418
+ lastShedAtMs = null;
291419
+ lastSuccessAtMs = null;
291420
+ lastFailureEventSequence = null;
291421
+ lastShedEventSequence = null;
291422
+ lastSuccessEventSequence = null;
291423
+ lastSinkLatencyMs = null;
291424
+ healthEventSequence = 0;
291304
291425
  lastShedWarnAtMs = 0;
291305
291426
  closing = false;
291306
291427
  closed = false;
@@ -291334,6 +291455,7 @@ class BrokerExecutionArchiver {
291334
291455
  }
291335
291456
  try {
291336
291457
  this.flushTimer = setInterval(() => {
291458
+ this.emitArchiveHealthMetrics();
291337
291459
  this.flush();
291338
291460
  }, this.flushIntervalMs);
291339
291461
  this.flushTimer.unref?.();
@@ -291377,32 +291499,34 @@ class BrokerExecutionArchiver {
291377
291499
  row: { ...row.row, source: this.source }
291378
291500
  };
291379
291501
  if (this.queue.length >= this.maxQueueSize) {
291380
- const shedRow = this.queue[0];
291381
- if (shedRow) {
291382
- this.appendLossRecords([shedRow], "queue_shed");
291502
+ const shedEntry = this.queue[0];
291503
+ if (shedEntry) {
291504
+ this.appendLossRecords([shedEntry], "queue_shed");
291383
291505
  this.queue.shift();
291384
291506
  }
291385
291507
  this.stats.shed += 1;
291508
+ this.recordHealthEvent("shed");
291386
291509
  this.recordArchiveMetric("cex_archive_rows_shed_total", {
291387
- table: shedRow?.table ?? "unknown",
291510
+ table: shedEntry?.table ?? "unknown",
291388
291511
  source: this.source,
291389
- feed: shedRow ? archiveFeed(shedRow) : "NON_MARKET"
291512
+ feed: shedEntry ? archiveFeed(shedEntry) : "NON_MARKET"
291390
291513
  });
291391
291514
  this.recordArchiveMetric("cex_archive_queue_saturated_rows_total", {
291392
- table: shedRow?.table ?? "unknown",
291515
+ table: shedEntry?.table ?? "unknown",
291393
291516
  source: this.source,
291394
- feed: shedRow ? archiveFeed(shedRow) : "NON_MARKET"
291517
+ feed: shedEntry ? archiveFeed(shedEntry) : "NON_MARKET"
291395
291518
  });
291396
291519
  const now3 = Date.now();
291397
291520
  if (now3 - this.lastShedWarnAtMs >= SHED_WARN_INTERVAL_MS) {
291398
291521
  log.warn("Archive queue full: shedding oldest rows", {
291399
291522
  shed_total: this.stats.shed,
291400
291523
  queue_max: this.maxQueueSize,
291401
- table: shedRow?.table ?? "unknown"
291524
+ table: shedEntry?.table ?? "unknown"
291402
291525
  });
291403
291526
  this.lastShedWarnAtMs = now3;
291404
291527
  }
291405
291528
  }
291529
+ this.enqueueTimes.set(archiveRow, Date.now());
291406
291530
  this.queue.push(archiveRow);
291407
291531
  this.stats.enqueued += 1;
291408
291532
  this.recordArchiveMetric("cex_archive_rows_enqueued_total", {
@@ -291410,6 +291534,7 @@ class BrokerExecutionArchiver {
291410
291534
  source: this.source,
291411
291535
  feed: archiveFeed(archiveRow)
291412
291536
  });
291537
+ this.emitArchiveHealthMetrics();
291413
291538
  if (this.queue.length >= this.batchSize) {
291414
291539
  this.flush();
291415
291540
  }
@@ -291418,20 +291543,25 @@ class BrokerExecutionArchiver {
291418
291543
  queueMicrotask(() => this.enqueue(row));
291419
291544
  }
291420
291545
  async flush() {
291421
- if (!this.enabled || this.closed || this.queue.length === 0) {
291546
+ if (!this.enabled || this.closed || this.queue.length === 0 && this.pendingRetry === null) {
291422
291547
  return;
291423
291548
  }
291424
291549
  if (this.flushInFlight) {
291425
291550
  return this.flushInFlight;
291426
291551
  }
291552
+ const inFlightState = {};
291427
291553
  const inFlight = this.flushBatch().then(() => {
291428
291554
  return;
291429
291555
  }).finally(() => {
291556
+ if (this.flushInFlight !== inFlightState.promise) {
291557
+ return;
291558
+ }
291430
291559
  this.flushInFlight = null;
291431
291560
  if (!this.closed && !this.closing && this.enabled && this.queue.length >= this.batchSize) {
291432
291561
  queueMicrotask(() => void this.flush());
291433
291562
  }
291434
291563
  });
291564
+ inFlightState.promise = inFlight;
291435
291565
  this.flushInFlight = inFlight;
291436
291566
  return inFlight;
291437
291567
  }
@@ -291443,16 +291573,18 @@ class BrokerExecutionArchiver {
291443
291573
  }
291444
291574
  let closeError;
291445
291575
  try {
291446
- while (this.queue.length > 0 || this.flushInFlight) {
291576
+ while (this.pendingRetry !== null || this.queue.length > 0 || this.flushInFlight) {
291447
291577
  if (this.flushInFlight) {
291448
291578
  await this.flushInFlight;
291449
291579
  continue;
291450
291580
  }
291451
- const depthBefore = this.queue.length;
291452
- const flushed = await this.flushBatch();
291453
- if (!flushed && this.queue.length >= depthBefore && depthBefore > 0) {
291454
- const undelivered = [...this.queue];
291581
+ if (!await this.flushBatch()) {
291582
+ const undelivered = [
291583
+ ...this.pendingRetry?.rows ?? [],
291584
+ ...this.queue
291585
+ ];
291455
291586
  this.appendLossRecords(undelivered, "shutdown_forwarder_failure");
291587
+ this.pendingRetry = null;
291456
291588
  this.queue.length = 0;
291457
291589
  break;
291458
291590
  }
@@ -291476,7 +291608,87 @@ class BrokerExecutionArchiver {
291476
291608
  return { ...this.stats };
291477
291609
  }
291478
291610
  getQueueDepth() {
291479
- return this.queue.length;
291611
+ return this.queue.length + (this.pendingRetry?.rows.length ?? 0);
291612
+ }
291613
+ getHealthSnapshot() {
291614
+ const now3 = Date.now();
291615
+ const oldestPendingAtMs = this.oldestPendingEnqueueAtMs();
291616
+ const oldestPendingAgeMs = oldestPendingAtMs === null ? 0 : Math.max(0, now3 - oldestPendingAtMs);
291617
+ const inFlightAgeMs = this.inFlightStartedAtMs === null ? 0 : Math.max(0, now3 - this.inFlightStartedAtMs);
291618
+ const lastUnhealthyEventSequence = Math.max(this.lastFailureEventSequence ?? Number.NEGATIVE_INFINITY, this.lastShedEventSequence ?? Number.NEGATIVE_INFINITY);
291619
+ const recoveredFromEvents = lastUnhealthyEventSequence === Number.NEGATIVE_INFINITY || this.lastSuccessEventSequence !== null && this.lastSuccessEventSequence > lastUnhealthyEventSequence;
291620
+ const healthy = this.enabled && !this.closing && !this.closed && this.queue.length < this.maxQueueSize && oldestPendingAgeMs <= this.forwarderTimeoutMs && inFlightAgeMs <= this.forwarderTimeoutMs && recoveredFromEvents;
291621
+ return {
291622
+ healthy,
291623
+ queue_depth: this.getQueueDepth(),
291624
+ oldest_pending_age_ms: oldestPendingAgeMs,
291625
+ shed_total: this.stats.shed,
291626
+ last_failure_at: this.lastFailureAtMs,
291627
+ last_shed_at: this.lastShedAtMs,
291628
+ last_success_at: this.lastSuccessAtMs,
291629
+ sink_latency_ms: this.lastSinkLatencyMs,
291630
+ sink_errors_total: this.stats.forwarderFailures,
291631
+ in_flight: this.inFlightBatch !== null,
291632
+ in_flight_age_ms: inFlightAgeMs
291633
+ };
291634
+ }
291635
+ oldestPendingEnqueueAtMs() {
291636
+ let oldest = null;
291637
+ for (const entry of this.pendingRetry?.rows ?? []) {
291638
+ const enqueuedAtMs = this.enqueueTimes.get(entry);
291639
+ if (enqueuedAtMs === undefined) {
291640
+ continue;
291641
+ }
291642
+ oldest = oldest === null ? enqueuedAtMs : Math.min(oldest, enqueuedAtMs);
291643
+ }
291644
+ for (const entry of this.queue) {
291645
+ const enqueuedAtMs = this.enqueueTimes.get(entry);
291646
+ if (enqueuedAtMs === undefined) {
291647
+ continue;
291648
+ }
291649
+ oldest = oldest === null ? enqueuedAtMs : Math.min(oldest, enqueuedAtMs);
291650
+ }
291651
+ for (const entry of this.inFlightBatch ?? []) {
291652
+ const enqueuedAtMs = this.enqueueTimes.get(entry);
291653
+ if (enqueuedAtMs === undefined) {
291654
+ continue;
291655
+ }
291656
+ oldest = oldest === null ? enqueuedAtMs : Math.min(oldest, enqueuedAtMs);
291657
+ }
291658
+ return oldest;
291659
+ }
291660
+ recordHealthEvent(event) {
291661
+ const eventSequence = ++this.healthEventSequence;
291662
+ const eventTimestampMs = Date.now();
291663
+ switch (event) {
291664
+ case "failure":
291665
+ this.lastFailureAtMs = eventTimestampMs;
291666
+ this.lastFailureEventSequence = eventSequence;
291667
+ return;
291668
+ case "shed":
291669
+ this.lastShedAtMs = eventTimestampMs;
291670
+ this.lastShedEventSequence = eventSequence;
291671
+ return;
291672
+ case "success":
291673
+ this.lastSuccessAtMs = eventTimestampMs;
291674
+ this.lastSuccessEventSequence = eventSequence;
291675
+ return;
291676
+ }
291677
+ }
291678
+ emitArchiveHealthMetrics() {
291679
+ const snapshot = this.getHealthSnapshot();
291680
+ const labels = { source: this.source };
291681
+ this.recordArchiveObservableGauge("cex_archive_health", snapshot.healthy ? 1 : 0, labels);
291682
+ this.recordArchiveGauge("cex_archive_queue_depth", snapshot.queue_depth, labels);
291683
+ this.recordArchiveGauge("cex_archive_oldest_pending_age_ms", snapshot.oldest_pending_age_ms, labels);
291684
+ this.recordArchiveGauge("cex_archive_shed_total", snapshot.shed_total, labels);
291685
+ this.recordArchiveGauge("cex_archive_last_failure_at", snapshot.last_failure_at ?? 0, labels);
291686
+ this.recordArchiveGauge("cex_archive_last_shed_at", snapshot.last_shed_at ?? 0, labels);
291687
+ this.recordArchiveGauge("cex_archive_last_success_at", snapshot.last_success_at ?? 0, labels);
291688
+ this.recordArchiveGauge("cex_archive_sink_latency_ms", snapshot.sink_latency_ms ?? 0, labels);
291689
+ this.recordArchiveGauge("cex_archive_sink_errors_total", snapshot.sink_errors_total, labels);
291690
+ this.recordArchiveGauge("cex_archive_in_flight", snapshot.in_flight ? 1 : 0, labels);
291691
+ this.recordArchiveGauge("cex_archive_in_flight_age_ms", snapshot.in_flight_age_ms, labels);
291480
291692
  }
291481
291693
  closeLossJournal() {
291482
291694
  if (this.deadLetterFd === undefined) {
@@ -291491,27 +291703,6 @@ class BrokerExecutionArchiver {
291491
291703
  this.deadLetterFd = undefined;
291492
291704
  }
291493
291705
  }
291494
- enforceQueueBound() {
291495
- while (this.queue.length > this.maxQueueSize) {
291496
- const dropped = this.queue[0];
291497
- if (!dropped) {
291498
- return;
291499
- }
291500
- this.appendLossRecords([dropped], "queue_shed");
291501
- this.queue.shift();
291502
- this.stats.shed += 1;
291503
- this.recordArchiveMetric("cex_archive_rows_shed_total", {
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)
291512
- });
291513
- }
291514
- }
291515
291706
  appendLossRecords(rows, reason) {
291516
291707
  if (rows.length === 0) {
291517
291708
  return;
@@ -291556,10 +291747,14 @@ class BrokerExecutionArchiver {
291556
291747
  }
291557
291748
  }
291558
291749
  async flushBatch() {
291559
- const batch = this.queue.splice(0, this.batchSize);
291750
+ const pinned = this.pendingRetry;
291751
+ const batch = pinned ? pinned.rows : this.queue.splice(0, this.batchSize);
291560
291752
  if (batch.length === 0) {
291561
291753
  return true;
291562
291754
  }
291755
+ const batchId = pinned?.batchId ?? randomUUID();
291756
+ this.inFlightBatch = batch;
291757
+ this.inFlightStartedAtMs = Date.now();
291563
291758
  for (const entry of batch) {
291564
291759
  if (isBrokerExecutionArchiveTable(entry.table)) {
291565
291760
  this.emitOtelLog(entry);
@@ -291567,11 +291762,27 @@ class BrokerExecutionArchiver {
291567
291762
  }
291568
291763
  if (this.forwarderUrl) {
291569
291764
  try {
291570
- await this.postToForwarder(batch);
291765
+ await this.postToForwarder(batch, batchId);
291571
291766
  } catch (error) {
291572
291767
  this.stats.forwarderFailures += 1;
291573
- this.queue.push(...batch);
291574
- this.enforceQueueBound();
291768
+ this.recordHealthEvent("failure");
291769
+ this.lastSinkLatencyMs = Math.max(0, Date.now() - (this.inFlightStartedAtMs ?? Date.now()));
291770
+ const attempts = (pinned?.attempts ?? 0) + 1;
291771
+ try {
291772
+ if (attempts >= MAX_PINNED_BATCH_ATTEMPTS) {
291773
+ this.pendingRetry = null;
291774
+ this.appendLossRecords(batch, "retry_exhausted");
291775
+ log.warn("Broker execution archive gave up on a batch", {
291776
+ attempts,
291777
+ rows: batch.length
291778
+ });
291779
+ } else {
291780
+ this.pendingRetry = { batchId, rows: batch, attempts };
291781
+ }
291782
+ } finally {
291783
+ this.clearInFlightBatch(batch);
291784
+ this.emitArchiveHealthMetrics();
291785
+ }
291575
291786
  this.recordArchiveMetric("cex_archive_forwarder_failures_total", {
291576
291787
  count: batch.length
291577
291788
  });
@@ -291579,15 +291790,28 @@ class BrokerExecutionArchiver {
291579
291790
  return false;
291580
291791
  }
291581
291792
  }
291793
+ this.pendingRetry = null;
291582
291794
  this.stats.flushed += batch.length;
291795
+ this.recordHealthEvent("success");
291796
+ this.lastSinkLatencyMs = Math.max(0, Date.now() - (this.inFlightStartedAtMs ?? Date.now()));
291797
+ this.clearInFlightBatch(batch);
291583
291798
  this.recordFlushHealth(batch);
291799
+ this.emitArchiveHealthMetrics();
291584
291800
  return true;
291585
291801
  }
291802
+ clearInFlightBatch(batch) {
291803
+ if (this.inFlightBatch !== batch) {
291804
+ return;
291805
+ }
291806
+ this.inFlightBatch = null;
291807
+ this.inFlightStartedAtMs = null;
291808
+ }
291586
291809
  recordFlushHealth(batch) {
291587
291810
  const countByTable = new Map;
291588
291811
  for (const entry of batch) {
291589
- const key = `${entry.table}\x00${archiveFeed(entry)}`;
291590
- const grouped = countByTable.get(key) ?? { row: entry, count: 0 };
291812
+ const row = entry;
291813
+ const key = `${row.table}\x00${archiveFeed(row)}`;
291814
+ const grouped = countByTable.get(key) ?? { row, count: 0 };
291591
291815
  grouped.count += 1;
291592
291816
  countByTable.set(key, grouped);
291593
291817
  }
@@ -291605,9 +291829,14 @@ class BrokerExecutionArchiver {
291605
291829
  await this.otelMetrics?.recordCounter(metricName, value, labels);
291606
291830
  } catch {}
291607
291831
  }
291608
- async recordArchiveGauge(metricName, value) {
291832
+ async recordArchiveGauge(metricName, value, labels = {}) {
291833
+ try {
291834
+ await this.otelMetrics?.recordGauge(metricName, value, labels);
291835
+ } catch {}
291836
+ }
291837
+ async recordArchiveObservableGauge(metricName, value, labels) {
291609
291838
  try {
291610
- await this.otelMetrics?.recordGauge(metricName, value, {});
291839
+ await this.otelMetrics?.setObservableGauge(metricName, value, labels);
291611
291840
  } catch {}
291612
291841
  }
291613
291842
  emitOtelLog(entry) {
@@ -291625,13 +291854,14 @@ class BrokerExecutionArchiver {
291625
291854
  log.warn("Broker execution archive OTLP emit failed", { error });
291626
291855
  }
291627
291856
  }
291628
- postToForwarder(batch) {
291857
+ postToForwarder(batch, batchId) {
291629
291858
  if (!this.forwarderUrl || batch.length === 0) {
291630
291859
  return Promise.resolve();
291631
291860
  }
291632
291861
  const body = JSON.stringify({
291633
291862
  source: this.source,
291634
291863
  deployment_id: this.deploymentId,
291864
+ batch_id: batchId,
291635
291865
  rows: batch
291636
291866
  });
291637
291867
  const url2 = new URL(this.forwarderUrl);
@@ -291644,27 +291874,136 @@ class BrokerExecutionArchiver {
291644
291874
  headers.authorization = `Bearer ${this.forwarderAuthToken}`;
291645
291875
  }
291646
291876
  return new Promise((resolve, reject) => {
291647
- const req = doRequest(url2, {
291648
- method: "POST",
291649
- headers,
291650
- timeout: this.forwarderTimeoutMs
291651
- }, (res) => {
291652
- res.on("data", () => {});
291653
- res.on("end", () => {
291654
- const status = res.statusCode ?? 0;
291655
- if (status < 200 || status >= 300) {
291656
- reject(new Error(`Archive forwarder returned ${status} ${res.statusMessage ?? ""}`));
291657
- return;
291658
- }
291877
+ let req;
291878
+ let response;
291879
+ let settled = false;
291880
+ let requestClosed = false;
291881
+ let requestCloseFailureScheduled = false;
291882
+ let responseClosed = false;
291883
+ let responseEnded = false;
291884
+ let outerTimer;
291885
+ function cleanupRequestListeners() {
291886
+ req?.removeListener("error", onRequestError);
291887
+ req?.removeListener("timeout", onRequestTimeout);
291888
+ req?.removeListener("close", onRequestClose);
291889
+ }
291890
+ function cleanupResponseListeners() {
291891
+ response?.removeListener("data", onResponseData);
291892
+ response?.removeListener("end", onResponseEnd);
291893
+ response?.removeListener("aborted", onResponseAborted);
291894
+ response?.removeListener("error", onResponseError);
291895
+ response?.removeListener("close", onResponseClose);
291896
+ }
291897
+ function onRequestClose() {
291898
+ requestClosed = true;
291899
+ if (!settled && !response && !requestCloseFailureScheduled) {
291900
+ requestCloseFailureScheduled = true;
291901
+ queueMicrotask(() => {
291902
+ if (!settled && !response) {
291903
+ settle2(new Error("Archive forwarder request closed before response headers"), false, true);
291904
+ }
291905
+ });
291906
+ }
291907
+ if (settled) {
291908
+ cleanupRequestListeners();
291909
+ }
291910
+ }
291911
+ function onRequestError(error) {
291912
+ if (!settled) {
291913
+ settle2(error, true);
291914
+ } else {
291915
+ cleanupRequestListeners();
291916
+ }
291917
+ }
291918
+ function onRequestTimeout() {
291919
+ settle2(new Error("Archive forwarder socket timed out"), true);
291920
+ }
291921
+ function onResponseData() {}
291922
+ function onResponseEnd() {
291923
+ responseEnded = true;
291924
+ if (!response?.complete) {
291925
+ settle2(new Error("Archive forwarder response was incomplete"), true);
291926
+ return;
291927
+ }
291928
+ const status = response.statusCode ?? 0;
291929
+ if (status < 200 || status >= 300) {
291930
+ settle2(new Error(`Archive forwarder returned ${status} ${response.statusMessage ?? ""}`), false);
291931
+ return;
291932
+ }
291933
+ settle2(undefined, false);
291934
+ }
291935
+ function onResponseAborted() {
291936
+ settle2(new Error("Archive forwarder response was aborted"), true);
291937
+ }
291938
+ function onResponseError(error) {
291939
+ if (!settled) {
291940
+ settle2(error, true);
291941
+ }
291942
+ }
291943
+ function onResponseClose() {
291944
+ responseClosed = true;
291945
+ if (!settled && !responseEnded) {
291946
+ settle2(new Error("Archive forwarder response closed before completion"), true);
291947
+ return;
291948
+ }
291949
+ cleanupResponseListeners();
291950
+ }
291951
+ function settle2(error, destroyRequest, retainClosedRequestError = false) {
291952
+ if (settled) {
291953
+ return;
291954
+ }
291955
+ settled = true;
291956
+ if (outerTimer) {
291957
+ clearTimeout(outerTimer);
291958
+ outerTimer = undefined;
291959
+ }
291960
+ response?.removeListener("data", onResponseData);
291961
+ response?.removeListener("end", onResponseEnd);
291962
+ response?.removeListener("aborted", onResponseAborted);
291963
+ if (responseClosed) {
291964
+ cleanupResponseListeners();
291965
+ }
291966
+ if (destroyRequest && req && !requestClosed) {
291967
+ req.destroy(error);
291968
+ } else if (retainClosedRequestError && req && requestClosed) {
291969
+ req.removeListener("timeout", onRequestTimeout);
291970
+ req.removeListener("close", onRequestClose);
291971
+ } else if (!req || requestClosed) {
291972
+ cleanupRequestListeners();
291973
+ }
291974
+ if (error) {
291975
+ reject(error);
291976
+ } else {
291659
291977
  resolve();
291978
+ }
291979
+ }
291980
+ outerTimer = setTimeout(() => {
291981
+ settle2(new Error("Archive forwarder request deadline exceeded"), true);
291982
+ }, this.forwarderTimeoutMs);
291983
+ try {
291984
+ req = doRequest(url2, {
291985
+ method: "POST",
291986
+ headers,
291987
+ timeout: this.forwarderTimeoutMs
291988
+ }, (nextResponse) => {
291989
+ response = nextResponse;
291990
+ response.on("data", onResponseData);
291991
+ response.on("end", onResponseEnd);
291992
+ response.on("aborted", onResponseAborted);
291993
+ response.on("error", onResponseError);
291994
+ response.on("close", onResponseClose);
291995
+ if (settled) {
291996
+ response.resume();
291997
+ }
291660
291998
  });
291661
- });
291662
- req.on("error", reject);
291663
- req.on("timeout", () => {
291664
- req.destroy(new Error("Archive forwarder request timed out"));
291665
- });
291666
- req.write(body);
291667
- req.end();
291999
+ req.on("error", onRequestError);
292000
+ req.on("timeout", onRequestTimeout);
292001
+ req.on("close", onRequestClose);
292002
+ req.write(body);
292003
+ req.end();
292004
+ } catch (error) {
292005
+ settle2(error instanceof Error ? error : new Error(String(error)), true);
292006
+ }
291668
292007
  });
291669
292008
  }
291670
292009
  }
@@ -292134,6 +292473,94 @@ class AccountBalanceArchivePoller {
292134
292473
  }
292135
292474
  }
292136
292475
 
292476
+ // src/helpers/balance-update-archive-consumer.ts
292477
+ class BalanceUpdateArchiveConsumer {
292478
+ params;
292479
+ #started = false;
292480
+ #stopping = false;
292481
+ #subscriptions = new Set;
292482
+ #runs = [];
292483
+ constructor(params) {
292484
+ this.params = params;
292485
+ }
292486
+ start() {
292487
+ if (this.#started || this.#stopping)
292488
+ return;
292489
+ this.#started = true;
292490
+ const targets = this.#targets();
292491
+ log.info("\uD83D\uDCB8 Balance-update archive consumer started", {
292492
+ accounts: targets.length
292493
+ });
292494
+ for (const target of targets) {
292495
+ this.#runs.push(this.#consume(target));
292496
+ }
292497
+ }
292498
+ async stop() {
292499
+ this.#stopping = true;
292500
+ for (const subscription of [...this.#subscriptions]) {
292501
+ subscription.close();
292502
+ }
292503
+ await Promise.all(this.#runs);
292504
+ }
292505
+ #targets() {
292506
+ const targets = [];
292507
+ for (const [exchange, pool] of Object.entries(this.params.brokers)) {
292508
+ for (const account of [pool.primary, ...pool.secondaryBrokers]) {
292509
+ targets.push({ exchange, accountSelector: account.label });
292510
+ }
292511
+ }
292512
+ return targets;
292513
+ }
292514
+ async#consume(target) {
292515
+ while (!this.#stopping) {
292516
+ let subscription;
292517
+ try {
292518
+ subscription = this.params.userDataStreamSupervisor.subscribe({
292519
+ exchange: target.exchange,
292520
+ accountSelector: target.accountSelector,
292521
+ kind: "balance"
292522
+ });
292523
+ this.#subscriptions.add(subscription);
292524
+ for await (const message of subscription) {
292525
+ if (this.#stopping)
292526
+ break;
292527
+ const event = message.event;
292528
+ if (event.e !== "balanceUpdate")
292529
+ continue;
292530
+ this.params.archiver.enqueue(buildTransferEventArchiveRow({
292531
+ tags: buildCommonArchiveTags({
292532
+ deploymentId: this.params.archiver.getDeploymentId(),
292533
+ accountSelector: target.accountSelector,
292534
+ exchange: target.exchange
292535
+ }),
292536
+ transfer: {
292537
+ eventKind: "balance_delta",
292538
+ lifecycleAction: "observe_balance_update",
292539
+ amount: typeof event.d === "string" ? event.d : undefined,
292540
+ assetSymbol: typeof event.a === "string" ? event.a : undefined,
292541
+ exchangeTimestamp: normalizeTimestamp(event.T),
292542
+ payload: event
292543
+ }
292544
+ }));
292545
+ }
292546
+ } catch (error) {
292547
+ if (!this.#stopping) {
292548
+ log.warn("Balance-update archive subscription failed", {
292549
+ exchange: target.exchange,
292550
+ accountSelector: target.accountSelector,
292551
+ errorType: error instanceof Error ? error.name : typeof error
292552
+ });
292553
+ }
292554
+ } finally {
292555
+ if (subscription) {
292556
+ this.#subscriptions.delete(subscription);
292557
+ subscription.close();
292558
+ }
292559
+ }
292560
+ }
292561
+ }
292562
+ }
292563
+
292137
292564
  // src/helpers/deposit-archive-poller.ts
292138
292565
  var DEFAULT_CONFIG2 = {
292139
292566
  pollIntervalMs: 60000,
@@ -292343,7 +292770,7 @@ class DepositArchivePoller {
292343
292770
  network: network === undefined ? undefined : String(network),
292344
292771
  externalId: depositTxid,
292345
292772
  txid: depositTxid,
292346
- exchangeTimestamp: normalizeTimestamp2(creditedAt),
292773
+ exchangeTimestamp: normalizeTimestamp(creditedAt),
292347
292774
  payload: record
292348
292775
  }
292349
292776
  }));
@@ -293205,7 +293632,7 @@ function createOtelLogsFromEnv() {
293205
293632
  }
293206
293633
 
293207
293634
  // src/helpers/stream-health-publisher.ts
293208
- import { createHash as createHash4, randomUUID } from "node:crypto";
293635
+ import { createHash as createHash4, randomUUID as randomUUID2 } from "node:crypto";
293209
293636
  import {
293210
293637
  closeSync as closeSync2,
293211
293638
  fsyncSync as fsyncSync2,
@@ -293330,7 +293757,7 @@ class StreamHealthPublisher {
293330
293757
  version: STATE_VERSION,
293331
293758
  producerId: PRODUCER_ID,
293332
293759
  producerEpoch: "1",
293333
- runId: randomUUID(),
293760
+ runId: randomUUID2(),
293334
293761
  nextBatchSequence: "1",
293335
293762
  nextStreamSequences: {}
293336
293763
  };
@@ -293399,7 +293826,7 @@ class StreamHealthPublisher {
293399
293826
  return;
293400
293827
  if (this.#advanceRun) {
293401
293828
  this.#state.producerEpoch = next(this.#state.producerEpoch);
293402
- this.#state.runId = randomUUID();
293829
+ this.#state.runId = randomUUID2();
293403
293830
  this.#state.nextBatchSequence = "1";
293404
293831
  this.#state.nextStreamSequences = {};
293405
293832
  this.#advanceRun = false;
@@ -293515,7 +293942,7 @@ class StreamHealthPublisher {
293515
293942
  cause: error
293516
293943
  });
293517
293944
  }
293518
- const temporary = `${this.#statePath}.${process.pid}.${randomUUID()}.tmp`;
293945
+ const temporary = `${this.#statePath}.${process.pid}.${randomUUID2()}.tmp`;
293519
293946
  let fd2;
293520
293947
  try {
293521
293948
  fd2 = openSync2(temporary, "wx", 384);
@@ -293560,6 +293987,143 @@ function streamHealthPublisherConfigFromEnv(env = process.env) {
293560
293987
  };
293561
293988
  }
293562
293989
 
293990
+ // src/helpers/user-asset-archive-poller.ts
293991
+ var DEFAULT_CONFIG4 = {
293992
+ pollIntervalMs: 60000
293993
+ };
293994
+ var USER_ASSET_EXCHANGE_ID = "binance";
293995
+ var metricLabels2 = (target) => ({
293996
+ exchange: target.exchangeId,
293997
+ account_selector: target.account.label,
293998
+ balance_scope: USER_ASSET_BALANCE_SCOPE
293999
+ });
294000
+
294001
+ class UserAssetArchivePoller {
294002
+ params;
294003
+ #timer = null;
294004
+ #stopped = false;
294005
+ #running = null;
294006
+ #lastSuccessMs = new Map;
294007
+ #config;
294008
+ constructor(params) {
294009
+ this.params = params;
294010
+ this.#config = { ...DEFAULT_CONFIG4, ...params.config };
294011
+ }
294012
+ start() {
294013
+ if (this.#timer || this.#stopped || !this.params.archiver.canPersistAccountBalanceSnapshots() || this.#targets().length === 0) {
294014
+ return;
294015
+ }
294016
+ log.info("\uD83E\uDDCA User asset archive poller started", {
294017
+ balanceScope: USER_ASSET_BALANCE_SCOPE
294018
+ });
294019
+ this.#schedule(0);
294020
+ }
294021
+ async stop() {
294022
+ this.#stopped = true;
294023
+ if (this.#timer) {
294024
+ clearTimeout(this.#timer);
294025
+ this.#timer = null;
294026
+ }
294027
+ await this.#running;
294028
+ }
294029
+ async pollAllOnce() {
294030
+ if (this.#stopped || this.#running || !this.params.archiver.canPersistAccountBalanceSnapshots()) {
294031
+ return false;
294032
+ }
294033
+ this.#running = this.#pollAllSequentially();
294034
+ try {
294035
+ return await this.#running;
294036
+ } finally {
294037
+ this.#running = null;
294038
+ }
294039
+ }
294040
+ #targets() {
294041
+ const targets = [];
294042
+ for (const [exchangeId, pool] of Object.entries(this.params.brokers)) {
294043
+ if (exchangeId.trim().toLowerCase() !== USER_ASSET_EXCHANGE_ID) {
294044
+ continue;
294045
+ }
294046
+ for (const account of [pool.primary, ...pool.secondaryBrokers]) {
294047
+ targets.push({ exchangeId, account });
294048
+ }
294049
+ }
294050
+ return targets;
294051
+ }
294052
+ async#pollAllSequentially() {
294053
+ for (const target of this.#targets()) {
294054
+ if (this.#stopped) {
294055
+ break;
294056
+ }
294057
+ await this.#pollOne(target);
294058
+ }
294059
+ return true;
294060
+ }
294061
+ async#pollOne(target) {
294062
+ const labels = metricLabels2(target);
294063
+ this.params.metrics?.recordCounter("cex_user_asset_poll_attempts_total", 1, labels);
294064
+ try {
294065
+ const exchange = target.account.exchange;
294066
+ if (typeof exchange.sapiV3PostAssetGetUserAsset !== "function") {
294067
+ throw new Error("binance_user_asset_unavailable: getUserAsset is not defined on this exchange instance");
294068
+ }
294069
+ const response = await exchange.sapiV3PostAssetGetUserAsset({});
294070
+ const observedAt = new Date;
294071
+ const normalized = normalizeBinanceUserAssetsForArchive(response);
294072
+ this.params.archiver.enqueue(buildUserAssetSnapshotRow({
294073
+ tags: buildCommonArchiveTags({
294074
+ deploymentId: this.params.archiver.getDeploymentId(),
294075
+ accountSelector: target.account.label,
294076
+ exchange: target.exchangeId,
294077
+ brokerObservedTimestamp: observedAt.toISOString()
294078
+ }),
294079
+ userAssets: normalized
294080
+ }));
294081
+ const successMs = Date.now();
294082
+ this.#lastSuccessMs.set(this.#targetKey(target), successMs);
294083
+ this.params.metrics?.recordCounter("cex_user_asset_poll_successes_total", 1, labels);
294084
+ this.params.metrics?.recordGauge("cex_user_asset_poll_last_success_timestamp_seconds", Math.floor(successMs / 1000), labels);
294085
+ this.#recordFreshness(labels, successMs, successMs);
294086
+ } catch (error) {
294087
+ rethrowArchiveDurabilityError(error);
294088
+ this.params.metrics?.recordCounter("cex_user_asset_poll_failures_total", 1, labels);
294089
+ const now3 = Date.now();
294090
+ const lastSuccess = this.#lastSuccessMs.get(this.#targetKey(target));
294091
+ if (lastSuccess !== undefined) {
294092
+ this.#recordFreshness(labels, lastSuccess, now3);
294093
+ }
294094
+ log.warn("User asset archive poll failed", {
294095
+ exchange: target.exchangeId,
294096
+ account: target.account.label,
294097
+ balanceScope: USER_ASSET_BALANCE_SCOPE,
294098
+ errorType: error instanceof Error ? error.name : "unknown"
294099
+ });
294100
+ }
294101
+ }
294102
+ #recordFreshness(labels, lastSuccessMs, nowMs) {
294103
+ this.params.metrics?.recordGauge("cex_user_asset_poll_freshness_seconds", Math.max(0, (nowMs - lastSuccessMs) / 1000), labels);
294104
+ }
294105
+ #targetKey(target) {
294106
+ return `${target.exchangeId}|${target.account.label}|${USER_ASSET_BALANCE_SCOPE}`;
294107
+ }
294108
+ #schedule(delayMs) {
294109
+ this.#timer = setTimeout(() => void this.#tick(), delayMs);
294110
+ this.#timer.unref?.();
294111
+ }
294112
+ async#tick() {
294113
+ this.#timer = null;
294114
+ try {
294115
+ await this.pollAllOnce();
294116
+ } catch (error) {
294117
+ rethrowArchiveDurabilityError(error);
294118
+ log.error("User asset archive poller tick failed", error);
294119
+ } finally {
294120
+ if (!this.#stopped) {
294121
+ this.#schedule(this.#config.pollIntervalMs);
294122
+ }
294123
+ }
294124
+ }
294125
+ }
294126
+
293563
294127
  // src/helpers/binance-user-data-stream.ts
293564
294128
  import { Buffer as Buffer2 } from "node:buffer";
293565
294129
  import { createHmac } from "node:crypto";
@@ -305180,7 +305744,7 @@ function finalize(ctx, schema) {
305180
305744
  result.$schema = "http://json-schema.org/draft-07/schema#";
305181
305745
  } else if (ctx.target === "draft-04") {
305182
305746
  result.$schema = "http://json-schema.org/draft-04/schema#";
305183
- } else if (ctx.target === "openapi-3.0") {} else {}
305747
+ } else if (ctx.target === "openapi-3.0") {}
305184
305748
  if (ctx.external?.uri) {
305185
305749
  const id2 = ctx.external.registry.get(schema)?.id;
305186
305750
  if (!id2)
@@ -305428,7 +305992,7 @@ var literalProcessor = (schema, ctx, json3, _params) => {
305428
305992
  if (val === undefined) {
305429
305993
  if (ctx.unrepresentable === "throw") {
305430
305994
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
305431
- } else {}
305995
+ }
305432
305996
  } else if (typeof val === "bigint") {
305433
305997
  if (ctx.unrepresentable === "throw") {
305434
305998
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -308087,7 +308651,7 @@ async function handleDeposit(ctx) {
308087
308651
  network: depositNetwork?.exchangeNetworkId,
308088
308652
  externalId: depositTxid,
308089
308653
  txid: depositTxid,
308090
- exchangeTimestamp: normalizeTimestamp2(creditedAt),
308654
+ exchangeTimestamp: normalizeTimestamp(creditedAt),
308091
308655
  payload: deposit
308092
308656
  }
308093
308657
  });
@@ -311605,6 +312169,8 @@ class CEXBroker {
311605
312169
  fillArchivePoller;
311606
312170
  depositArchivePoller;
311607
312171
  accountBalanceArchivePoller;
312172
+ userAssetArchivePoller;
312173
+ balanceUpdateArchiveConsumer;
311608
312174
  userDataStreamSupervisor;
311609
312175
  loadEnvConfig() {
311610
312176
  log.info("\uD83D\uDD27 Loading CEX_BROKER_ environment variables:");
@@ -311758,6 +312324,14 @@ class CEXBroker {
311758
312324
  await this.accountBalanceArchivePoller.stop();
311759
312325
  this.accountBalanceArchivePoller = undefined;
311760
312326
  }
312327
+ if (this.userAssetArchivePoller) {
312328
+ await this.userAssetArchivePoller.stop();
312329
+ this.userAssetArchivePoller = undefined;
312330
+ }
312331
+ if (this.balanceUpdateArchiveConsumer) {
312332
+ await this.balanceUpdateArchiveConsumer.stop();
312333
+ this.balanceUpdateArchiveConsumer = undefined;
312334
+ }
311761
312335
  if (this.server) {
311762
312336
  await this.server.forceShutdown();
311763
312337
  }
@@ -311803,6 +312377,14 @@ class CEXBroker {
311803
312377
  await this.accountBalanceArchivePoller.stop();
311804
312378
  this.accountBalanceArchivePoller = undefined;
311805
312379
  }
312380
+ if (this.userAssetArchivePoller) {
312381
+ await this.userAssetArchivePoller.stop();
312382
+ this.userAssetArchivePoller = undefined;
312383
+ }
312384
+ if (this.balanceUpdateArchiveConsumer) {
312385
+ await this.balanceUpdateArchiveConsumer.stop();
312386
+ this.balanceUpdateArchiveConsumer = undefined;
312387
+ }
311806
312388
  log.info(`Running CEXBroker at ${new Date().toISOString()}`);
311807
312389
  if (this.otelMetrics?.isOtelEnabled()) {
311808
312390
  await this.otelMetrics.initialize();
@@ -311844,6 +312426,14 @@ class CEXBroker {
311844
312426
  metrics: this.otelMetrics
311845
312427
  });
311846
312428
  this.depositArchivePoller.start();
312429
+ if (this.userDataStreamSupervisor) {
312430
+ this.balanceUpdateArchiveConsumer = new BalanceUpdateArchiveConsumer({
312431
+ brokers: this.brokers,
312432
+ archiver: this.brokerArchiver,
312433
+ userDataStreamSupervisor: this.userDataStreamSupervisor
312434
+ });
312435
+ this.balanceUpdateArchiveConsumer.start();
312436
+ }
311847
312437
  }
311848
312438
  if (this.brokerArchiver?.canPersistAccountBalanceSnapshots()) {
311849
312439
  this.accountBalanceArchivePoller = new AccountBalanceArchivePoller({
@@ -311852,6 +312442,12 @@ class CEXBroker {
311852
312442
  metrics: this.otelMetrics
311853
312443
  });
311854
312444
  this.accountBalanceArchivePoller.start();
312445
+ this.userAssetArchivePoller = new UserAssetArchivePoller({
312446
+ brokers: this.brokers,
312447
+ archiver: this.brokerArchiver,
312448
+ metrics: this.otelMetrics
312449
+ });
312450
+ this.userAssetArchivePoller.start();
311855
312451
  }
311856
312452
  return this;
311857
312453
  }
@@ -311860,4 +312456,4 @@ export {
311860
312456
  CEXBroker as default
311861
312457
  };
311862
312458
 
311863
- //# debugId=66D8B9B2EE90476F64756E2164756E21
312459
+ //# debugId=2BB56A1EBD617FCC64756E2164756E21