@usherlabs/cex-broker 0.2.37 → 0.2.40

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.
@@ -316623,9 +316623,18 @@ class AccountBalanceArchivePoller {
316623
316623
  // src/helpers/deposit-archive-poller.ts
316624
316624
  var DEFAULT_CONFIG2 = {
316625
316625
  pollIntervalMs: 60000,
316626
+ fetchTimeoutMs: 30000,
316626
316627
  lookbackMs: 24 * 60 * 60 * 1000,
316627
316628
  depositsLimit: 50
316628
316629
  };
316630
+ function withTimeout(promise, timeoutMs, label) {
316631
+ let timer;
316632
+ const expiry = new Promise((_resolve, reject) => {
316633
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
316634
+ timer.unref?.();
316635
+ });
316636
+ return Promise.race([promise, expiry]).finally(() => clearTimeout(timer));
316637
+ }
316629
316638
  var ALL_CURRENCIES_CODE = "*";
316630
316639
  function depositTimestamp(record) {
316631
316640
  const observedAt = depositField(record, [
@@ -316738,6 +316747,14 @@ class DepositArchivePoller {
316738
316747
  return true;
316739
316748
  }
316740
316749
  async#pollOne(target) {
316750
+ let outcome = "error";
316751
+ try {
316752
+ outcome = await this.#pollTarget(target);
316753
+ } finally {
316754
+ this.params.metrics?.recordCounter("cex_deposit_poller_polls_total", 1, { exchange: target.exchangeId, outcome });
316755
+ }
316756
+ }
316757
+ async#pollTarget(target) {
316741
316758
  const exchange = target.account.exchange;
316742
316759
  const key = this.#targetKey(target);
316743
316760
  if (typeof exchange.fetchDeposits !== "function" || exchange.has?.fetchDeposits === false) {
@@ -316748,12 +316765,12 @@ class DepositArchivePoller {
316748
316765
  account: target.account.label
316749
316766
  });
316750
316767
  }
316751
- return;
316768
+ return "unsupported";
316752
316769
  }
316753
316770
  const since = this.#cursors.get(key) ?? Date.now() - this.#config.lookbackMs;
316754
316771
  let deposits;
316755
316772
  try {
316756
- deposits = await exchange.fetchDeposits(undefined, since, this.#config.depositsLimit);
316773
+ deposits = await withTimeout(exchange.fetchDeposits(undefined, since, this.#config.depositsLimit), this.#config.fetchTimeoutMs, "fetchDeposits");
316757
316774
  } catch (error) {
316758
316775
  this.params.metrics?.recordCounter("cex_deposit_poller_errors_total", 1, { exchange: target.exchangeId });
316759
316776
  log.warn("Deposit archive poll failed", {
@@ -316761,10 +316778,10 @@ class DepositArchivePoller {
316761
316778
  account: target.account.label,
316762
316779
  error
316763
316780
  });
316764
- return;
316781
+ return "error";
316765
316782
  }
316766
316783
  if (!Array.isArray(deposits) || deposits.length === 0) {
316767
- return;
316784
+ return "ok";
316768
316785
  }
316769
316786
  let archived = 0;
316770
316787
  for (const deposit of deposits) {
@@ -316812,7 +316829,7 @@ class DepositArchivePoller {
316812
316829
  network: network === undefined ? undefined : String(network),
316813
316830
  externalId: depositTxid,
316814
316831
  txid: depositTxid,
316815
- exchangeTimestamp: typeof creditedAt === "string" ? creditedAt : undefined,
316832
+ exchangeTimestamp: normalizeTimestamp2(creditedAt),
316816
316833
  payload: record
316817
316834
  }
316818
316835
  }));
@@ -316845,6 +316862,7 @@ class DepositArchivePoller {
316845
316862
  this.#lastArchivedByTarget.delete(key);
316846
316863
  }
316847
316864
  }
316865
+ return "ok";
316848
316866
  }
316849
316867
  #targetKey(target) {
316850
316868
  return `${target.exchangeId}|${target.account.label}|${target.code}`;
@@ -316986,387 +317004,1609 @@ class FillArchivePoller {
316986
317004
  }
316987
317005
  }
316988
317006
 
316989
- // src/helpers/order-activity-tracker.ts
316990
- var DEFAULT_MAX_AGE_MS = 6 * 60 * 60 * 1000;
316991
-
316992
- class OrderActivityTracker {
316993
- #entries = new Map;
316994
- #maxAgeMs;
316995
- constructor(options) {
316996
- this.#maxAgeMs = options?.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
317007
+ // src/helpers/market-data-archive/capture-contract.ts
317008
+ import { createHash as createHash3 } from "node:crypto";
317009
+ var MARKET_CAPTURE_SCHEMA_VERSION = "1.0.0";
317010
+ var CHECKSUM_ALGORITHM = "sha256-canonical-json-v1";
317011
+ var ARCHIVE_SOURCES = ["broker_read", "broker_write"];
317012
+ var CAPTURE_FEEDS = [
317013
+ "ORDERBOOK",
317014
+ "TICKER",
317015
+ "TRADES",
317016
+ "OHLCV"
317017
+ ];
317018
+ var SOURCE_MODES = [
317019
+ "broker_live_stream_v1",
317020
+ "broker_live_sampling_v1",
317021
+ "broker_current_snapshot_v1",
317022
+ "broker_bootstrap_fetch_v1",
317023
+ "external_ccxt_fallback_v1",
317024
+ "external_hummingbot_fallback_v1",
317025
+ "legacy_migration_v1"
317026
+ ];
317027
+ var RAW_CAPTURE_SCOPES = [
317028
+ "ccxt_normalized_object",
317029
+ "broker_visible_payload",
317030
+ "exchange_wire_frame"
317031
+ ];
317032
+ var CHECKSUM_FIELDS = new Set([
317033
+ "normalized_row_checksum",
317034
+ "raw_checksum",
317035
+ "checksum"
317036
+ ]);
317037
+ function canonicalDecimal(value) {
317038
+ if (!Number.isFinite(value)) {
317039
+ throw new Error("Canonical numbers must be finite");
316997
317040
  }
316998
- record(exchangeId, accountLabel, symbol, now3 = Date.now()) {
316999
- const exchange = exchangeId.trim().toLowerCase();
317000
- const trimmedSymbol = symbol.trim();
317001
- if (!exchange || !accountLabel.trim() || !trimmedSymbol) {
317002
- return;
317003
- }
317004
- const key = `${exchange}|${accountLabel}|${trimmedSymbol}`;
317005
- this.#entries.set(key, {
317006
- exchangeId: exchange,
317007
- accountLabel,
317008
- symbol: trimmedSymbol,
317009
- lastActivityAt: now3
317010
- });
317041
+ if (Object.is(value, -0)) {
317042
+ return "0";
317011
317043
  }
317012
- list(now3 = Date.now()) {
317013
- const active = [];
317014
- for (const [key, entry] of this.#entries) {
317015
- if (now3 - entry.lastActivityAt > this.#maxAgeMs) {
317016
- this.#entries.delete(key);
317017
- continue;
317018
- }
317019
- active.push(entry);
317020
- }
317021
- return active;
317044
+ const rendered = String(value).toLowerCase();
317045
+ if (!rendered.includes("e")) {
317046
+ return rendered;
317022
317047
  }
317048
+ const [coefficient = "0", exponentText = "0"] = rendered.split("e");
317049
+ const exponent = Number.parseInt(exponentText, 10);
317050
+ const negative = coefficient.startsWith("-");
317051
+ const unsigned = negative ? coefficient.slice(1) : coefficient;
317052
+ const [integer = "0", fraction = ""] = unsigned.split(".");
317053
+ const digits = `${integer}${fraction}`;
317054
+ const decimalIndex = integer.length + exponent;
317055
+ let result;
317056
+ if (decimalIndex <= 0) {
317057
+ result = `0.${"0".repeat(-decimalIndex)}${digits}`;
317058
+ } else if (decimalIndex >= digits.length) {
317059
+ result = `${digits}${"0".repeat(decimalIndex - digits.length)}`;
317060
+ } else {
317061
+ result = `${digits.slice(0, decimalIndex)}.${digits.slice(decimalIndex)}`;
317062
+ }
317063
+ return negative ? `-${result}` : result;
317023
317064
  }
317024
-
317025
- // src/helpers/otel.ts
317026
- var import_api2 = __toESM(require_src5(), 1);
317027
- var import_api_logs3 = __toESM(require_src7(), 1);
317028
- var import_exporter_logs_otlp_http = __toESM(require_src14(), 1);
317029
- var import_exporter_metrics_otlp_http = __toESM(require_src15(), 1);
317030
- var import_resources = __toESM(require_src11(), 1);
317031
- var import_sdk_logs = __toESM(require_src16(), 1);
317032
- var import_sdk_metrics = __toESM(require_src12(), 1);
317033
- var DEFAULT_SERVICE = "cex-broker";
317034
- var DEFAULT_OTLP_PORT = 4318;
317035
- var EXPORT_INTERVAL_MS = 5000;
317036
-
317037
- class BaseOtelSignal {
317038
- signal;
317039
- provider = null;
317040
- isEnabled = false;
317041
- serviceName;
317042
- constructor(config, signal) {
317043
- this.signal = signal;
317044
- this.serviceName = config?.serviceName ?? DEFAULT_SERVICE;
317045
- const endpointResolution = resolveOtlpEndpoint(this.signal, config);
317046
- if (!endpointResolution) {
317047
- log.info(`OTel ${signal} disabled: no OTLP endpoint or host provided`);
317048
- return;
317049
- }
317050
- try {
317051
- this.provider = this.createProvider(endpointResolution.endpoint, this.serviceName, endpointResolution.appendSignalPath);
317052
- this.onProviderCreated(this.provider);
317053
- this.isEnabled = true;
317054
- log.info(`OTel ${signal} enabled: ${endpointResolution.endpoint}`);
317055
- } catch (error) {
317056
- log.error(`Failed to initialize OTel ${signal}:`, error);
317057
- this.isEnabled = false;
317058
- this.provider = null;
317065
+ function serializeCanonical(value, stack) {
317066
+ if (value === null)
317067
+ return "null";
317068
+ if (typeof value === "string")
317069
+ return JSON.stringify(value);
317070
+ if (typeof value === "boolean")
317071
+ return value ? "true" : "false";
317072
+ if (typeof value === "number")
317073
+ return canonicalDecimal(value);
317074
+ if (typeof value === "bigint")
317075
+ return value.toString(10);
317076
+ if (value instanceof Date) {
317077
+ if (Number.isNaN(value.getTime())) {
317078
+ throw new Error("Canonical timestamps must be valid");
317059
317079
  }
317080
+ return value.getTime().toString(10);
317060
317081
  }
317061
- onProviderCreated(_provider) {}
317062
- onProviderClosed() {}
317063
- getProvider() {
317064
- return this.provider;
317065
- }
317066
- getServiceName() {
317067
- return this.serviceName;
317068
- }
317069
- isOtelEnabled() {
317070
- return this.isEnabled && this.provider !== null;
317082
+ if (Array.isArray(value)) {
317083
+ if (stack.has(value))
317084
+ throw new Error("Canonical values must be acyclic");
317085
+ stack.add(value);
317086
+ const result = `[${value.map((entry) => entry === undefined ? "null" : serializeCanonical(entry, stack)).join(",")}]`;
317087
+ stack.delete(value);
317088
+ return result;
317071
317089
  }
317072
- async close() {
317073
- if (!this.provider) {
317074
- return;
317075
- }
317076
- try {
317077
- await this.shutdownProvider(this.provider);
317078
- log.info(`OTel ${this.signal} provider shut down`);
317079
- } catch (error) {
317080
- log.error(`Error shutting down OTel ${this.signal} provider:`, error);
317081
- }
317082
- this.provider = null;
317083
- this.isEnabled = false;
317084
- this.onProviderClosed();
317090
+ if (typeof value === "object") {
317091
+ if (stack.has(value))
317092
+ throw new Error("Canonical values must be acyclic");
317093
+ stack.add(value);
317094
+ const entries = Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => left.localeCompare(right));
317095
+ const result = `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${serializeCanonical(entry, stack)}`).join(",")}}`;
317096
+ stack.delete(value);
317097
+ return result;
317085
317098
  }
317099
+ throw new Error(`Unsupported canonical value type: ${typeof value}`);
317086
317100
  }
317087
- function toAttributes(labels, service) {
317088
- const attrs = { ...labels, service };
317089
- for (const key of Object.keys(attrs)) {
317090
- const v = attrs[key];
317091
- if (typeof v !== "string" && typeof v !== "number") {
317092
- attrs[key] = String(v);
317093
- }
317101
+ function canonicalSerialize(value) {
317102
+ return serializeCanonical(value, new Set);
317103
+ }
317104
+ function omitChecksumFields(value) {
317105
+ if (Array.isArray(value))
317106
+ return value.map(omitChecksumFields);
317107
+ if (value && typeof value === "object" && !(value instanceof Date)) {
317108
+ return Object.fromEntries(Object.entries(value).filter(([key]) => !CHECKSUM_FIELDS.has(key)).map(([key, entry]) => [key, omitChecksumFields(entry)]));
317094
317109
  }
317095
- return attrs;
317110
+ return value;
317096
317111
  }
317097
-
317098
- class OtelMetrics extends BaseOtelSignal {
317099
- counters = new Map;
317100
- histograms = new Map;
317101
- observableGauges = new Map;
317102
- constructor(config) {
317103
- super(config, "metrics");
317112
+ function sha256Canonical(value) {
317113
+ return createHash3("sha256").update(canonicalSerialize(omitChecksumFields(value))).digest("hex");
317114
+ }
317115
+ function normalizeTimestampMs(value, field) {
317116
+ let timestamp;
317117
+ if (value instanceof Date) {
317118
+ timestamp = value.getTime();
317119
+ } else if (typeof value === "number") {
317120
+ timestamp = value;
317121
+ } else if (typeof value === "string" && /^\d+$/.test(value.trim())) {
317122
+ timestamp = Number(value.trim());
317123
+ } else if (typeof value === "string") {
317124
+ timestamp = Date.parse(value);
317125
+ } else {
317126
+ timestamp = Number.NaN;
317104
317127
  }
317105
- createProvider(endpoint, serviceName, appendSignalPath) {
317106
- const exporter = new import_exporter_metrics_otlp_http.OTLPMetricExporter({
317107
- url: appendOtlpPath(endpoint, "metrics", appendSignalPath)
317108
- });
317109
- const reader = new import_sdk_metrics.PeriodicExportingMetricReader({
317110
- exporter,
317111
- exportIntervalMillis: EXPORT_INTERVAL_MS
317112
- });
317113
- const resource = import_resources.resourceFromAttributes({
317114
- "service.name": serviceName
317115
- });
317116
- return new import_sdk_metrics.MeterProvider({
317117
- resource,
317118
- readers: [reader]
317119
- });
317128
+ if (!Number.isSafeInteger(timestamp) || timestamp < 0) {
317129
+ throw new Error(`${field} must be a non-negative millisecond timestamp`);
317120
317130
  }
317121
- onProviderCreated(provider) {
317122
- import_api2.metrics.setGlobalMeterProvider(provider);
317131
+ return timestamp;
317132
+ }
317133
+ function assertCaptureContext(context2) {
317134
+ if (!ARCHIVE_SOURCES.includes(context2.source)) {
317135
+ throw new Error(`Unsupported archive source: ${context2.source}`);
317123
317136
  }
317124
- shutdownProvider(provider) {
317125
- return provider.shutdown();
317137
+ if (!CAPTURE_FEEDS.includes(context2.feed)) {
317138
+ throw new Error(`Unsupported capture feed: ${context2.feed}`);
317126
317139
  }
317127
- async initialize() {
317128
- if (this.isOtelEnabled()) {
317129
- log.info("OTel metrics initialized (storage is handled by the collector)");
317130
- }
317140
+ if (!SOURCE_MODES.includes(context2.sourceMode)) {
317141
+ throw new Error(`Unsupported source mode: ${context2.sourceMode}`);
317142
+ }
317143
+ for (const [field, value] of [
317144
+ ["deployment_id", context2.deploymentId],
317145
+ ["capture_bundle_id", context2.captureBundleId],
317146
+ ["exchange", context2.exchange],
317147
+ ["symbol", context2.symbol],
317148
+ ["provider", context2.provider]
317149
+ ]) {
317150
+ if (!value.trim())
317151
+ throw new Error(`${field} must not be empty`);
317152
+ }
317153
+ }
317154
+ function createRawCapture(context2, input) {
317155
+ assertCaptureContext(context2);
317156
+ if (!RAW_CAPTURE_SCOPES.includes(input.scope)) {
317157
+ throw new Error(`Unsupported raw capture scope: ${input.scope}`);
317158
+ }
317159
+ const eventTimeMs = normalizeTimestampMs(input.eventTimeMs, "event_time_ms");
317160
+ const receivedTimeMs = normalizeTimestampMs(input.receivedTimeMs, "received_time_ms");
317161
+ const redactedPayload = redactStreamPayload(input.payload);
317162
+ const rawChecksum = sha256Canonical(redactedPayload);
317163
+ const rawCaptureId = sha256Canonical({
317164
+ capture_bundle_id: context2.captureBundleId,
317165
+ exchange: context2.exchange.trim().toLowerCase(),
317166
+ feed: context2.feed,
317167
+ raw_capture_scope: input.scope,
317168
+ raw_payload_sha256: rawChecksum,
317169
+ schema_version: context2.schemaVersion,
317170
+ source_mode: context2.sourceMode,
317171
+ source_symbol: context2.symbol.trim(),
317172
+ source_time_ms: eventTimeMs
317173
+ });
317174
+ return {
317175
+ rawCaptureId,
317176
+ rawCaptureScope: input.scope,
317177
+ rawChecksum,
317178
+ redactedPayload,
317179
+ eventTimeMs,
317180
+ receivedTimeMs,
317181
+ checksumAlgorithm: context2.checksumAlgorithm
317182
+ };
317183
+ }
317184
+ function captureCoreFields(context2, rawCapture) {
317185
+ assertCaptureContext(context2);
317186
+ return {
317187
+ source: context2.source,
317188
+ deployment_id: context2.deploymentId,
317189
+ capture_bundle_id: context2.captureBundleId,
317190
+ exchange: context2.exchange.trim().toLowerCase(),
317191
+ symbol: context2.symbol.trim(),
317192
+ trading_pair: context2.symbol.trim().replace("/", "-"),
317193
+ source_symbol: context2.symbol.trim(),
317194
+ asset_type: context2.assetType,
317195
+ feed: context2.feed,
317196
+ provider: context2.provider,
317197
+ source_mode: context2.sourceMode,
317198
+ source_time_ms: rawCapture.eventTimeMs,
317199
+ received_time_ms: rawCapture.receivedTimeMs,
317200
+ raw_capture_id: rawCapture.rawCaptureId,
317201
+ raw_capture_scope: rawCapture.rawCaptureScope,
317202
+ schema_version: context2.schemaVersion,
317203
+ checksum_algorithm: context2.checksumAlgorithm,
317204
+ raw_checksum: rawCapture.rawChecksum,
317205
+ provenance_complete: context2.provenanceComplete ? 1 : 0
317206
+ };
317207
+ }
317208
+
317209
+ // src/helpers/market-data-archive/capture-context.ts
317210
+ function createMarketCaptureContext(input) {
317211
+ const environment = input.environment ?? "development";
317212
+ const deploymentId = input.deploymentId.trim();
317213
+ if (!deploymentId)
317214
+ throw new Error("deployment_id must not be empty");
317215
+ const configuredBundle = input.captureBundleId?.trim();
317216
+ if (environment === "production" && !configuredBundle) {
317217
+ throw new Error("capture_bundle_id is required for production market capture");
317218
+ }
317219
+ const exchange = input.exchange.trim().toLowerCase();
317220
+ const symbol = input.symbol.trim();
317221
+ if (!exchange || !symbol) {
317222
+ throw new Error("exchange and symbol are required for market capture");
317223
+ }
317224
+ return {
317225
+ source: input.source,
317226
+ deploymentId,
317227
+ captureBundleId: configuredBundle ?? `development:${deploymentId}`,
317228
+ exchange,
317229
+ symbol,
317230
+ assetType: input.assetType,
317231
+ feed: input.feed,
317232
+ provider: input.provider?.trim() || `ccxt:${exchange}`,
317233
+ sourceMode: input.sourceMode,
317234
+ schemaVersion: MARKET_CAPTURE_SCHEMA_VERSION,
317235
+ checksumAlgorithm: CHECKSUM_ALGORITHM,
317236
+ provenanceComplete: true,
317237
+ timeframe: input.timeframe,
317238
+ accountSelector: input.accountSelector
317239
+ };
317240
+ }
317241
+ function resolveMarketCaptureArchiveState(input) {
317242
+ if (!input.archiveEnabled) {
317243
+ return { enabled: false, reason: "archive_disabled" };
317244
+ }
317245
+ if (!input.marketArchiveEnabled) {
317246
+ return { enabled: false, reason: "market_archive_disabled" };
317247
+ }
317248
+ const environment = input.environment?.trim() || "development";
317249
+ if (environment !== "development" && environment !== "production") {
317250
+ return { enabled: false, reason: "invalid_capture_environment" };
317251
+ }
317252
+ if (environment === "production") {
317253
+ const deploymentId = input.deploymentId?.trim();
317254
+ if (!deploymentId || deploymentId === "unknown") {
317255
+ return { enabled: false, reason: "missing_deployment_id" };
317256
+ }
317257
+ if (!input.captureBundleId?.trim()) {
317258
+ return { enabled: false, reason: "missing_capture_bundle_id" };
317259
+ }
317260
+ }
317261
+ return { enabled: true };
317262
+ }
317263
+ function assertMarketCaptureArchiveStartable(state) {
317264
+ if (state.enabled || state.reason === "archive_disabled" || state.reason === "market_archive_disabled") {
317265
+ return;
317266
+ }
317267
+ throw new Error(`Refusing to start: canonical market-data archival was requested but its capture identity is incomplete (${state.reason}). ` + "Set CEX_BROKER_MARKET_CAPTURE_ENVIRONMENT, CEX_BROKER_DEPLOYMENT_ID and CEX_BROKER_CAPTURE_BUNDLE_ID, " + "or disable archival explicitly via CEX_BROKER_MARKET_ARCHIVE_ENABLED.");
317268
+ }
317269
+ function captureEnvironmentFromEnv(value = process.env.CEX_BROKER_MARKET_CAPTURE_ENVIRONMENT) {
317270
+ const environment = value?.trim() || "development";
317271
+ if (environment !== "development" && environment !== "production") {
317272
+ throw new Error("CEX_BROKER_MARKET_CAPTURE_ENVIRONMENT must be development or production");
317273
+ }
317274
+ return environment;
317275
+ }
317276
+
317277
+ // src/helpers/market-data-archive/orderbook-sampler.ts
317278
+ var DEFAULT_ORDERBOOK_INTERVAL_MS = 1000;
317279
+ function getOrderbookIntervalMs() {
317280
+ const raw = process.env.CEX_BROKER_ORDERBOOK_INTERVAL_MS ?? process.env.CEX_BROKER_ORDERBOOK_TOB_INTERVAL_MS;
317281
+ if (!raw) {
317282
+ return DEFAULT_ORDERBOOK_INTERVAL_MS;
317283
+ }
317284
+ const parsed = Number.parseInt(raw, 10);
317285
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_ORDERBOOK_INTERVAL_MS;
317286
+ }
317287
+ function isMarketArchiveEnabled() {
317288
+ return process.env.CEX_BROKER_MARKET_ARCHIVE_ENABLED !== "false";
317289
+ }
317290
+
317291
+ class OrderbookSampler {
317292
+ intervalMs;
317293
+ lastEmitMs = null;
317294
+ constructor(intervalMs = getOrderbookIntervalMs()) {
317295
+ this.intervalMs = intervalMs;
317296
+ }
317297
+ shouldEmit(nowMs = Date.now()) {
317298
+ if (this.lastEmitMs !== null && nowMs < this.lastEmitMs) {
317299
+ this.lastEmitMs = nowMs;
317300
+ return true;
317301
+ }
317302
+ if (this.lastEmitMs !== null && nowMs - this.lastEmitMs < this.intervalMs) {
317303
+ return false;
317304
+ }
317305
+ this.lastEmitMs = nowMs;
317306
+ return true;
317307
+ }
317308
+ }
317309
+
317310
+ // src/helpers/order-activity-tracker.ts
317311
+ var DEFAULT_MAX_AGE_MS = 6 * 60 * 60 * 1000;
317312
+
317313
+ class OrderActivityTracker {
317314
+ #entries = new Map;
317315
+ #maxAgeMs;
317316
+ constructor(options) {
317317
+ this.#maxAgeMs = options?.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
317318
+ }
317319
+ record(exchangeId, accountLabel, symbol, now3 = Date.now()) {
317320
+ const exchange = exchangeId.trim().toLowerCase();
317321
+ const trimmedSymbol = symbol.trim();
317322
+ if (!exchange || !accountLabel.trim() || !trimmedSymbol) {
317323
+ return;
317324
+ }
317325
+ const key = `${exchange}|${accountLabel}|${trimmedSymbol}`;
317326
+ this.#entries.set(key, {
317327
+ exchangeId: exchange,
317328
+ accountLabel,
317329
+ symbol: trimmedSymbol,
317330
+ lastActivityAt: now3
317331
+ });
317332
+ }
317333
+ list(now3 = Date.now()) {
317334
+ const active = [];
317335
+ for (const [key, entry] of this.#entries) {
317336
+ if (now3 - entry.lastActivityAt > this.#maxAgeMs) {
317337
+ this.#entries.delete(key);
317338
+ continue;
317339
+ }
317340
+ active.push(entry);
317341
+ }
317342
+ return active;
317343
+ }
317344
+ }
317345
+
317346
+ // src/helpers/otel.ts
317347
+ var import_api2 = __toESM(require_src5(), 1);
317348
+ var import_api_logs3 = __toESM(require_src7(), 1);
317349
+ var import_exporter_logs_otlp_http = __toESM(require_src14(), 1);
317350
+ var import_exporter_metrics_otlp_http = __toESM(require_src15(), 1);
317351
+ var import_resources = __toESM(require_src11(), 1);
317352
+ var import_sdk_logs = __toESM(require_src16(), 1);
317353
+ var import_sdk_metrics = __toESM(require_src12(), 1);
317354
+ var DEFAULT_SERVICE = "cex-broker";
317355
+ var DEFAULT_OTLP_PORT = 4318;
317356
+ var EXPORT_INTERVAL_MS = 5000;
317357
+
317358
+ class BaseOtelSignal {
317359
+ signal;
317360
+ provider = null;
317361
+ isEnabled = false;
317362
+ serviceName;
317363
+ constructor(config, signal) {
317364
+ this.signal = signal;
317365
+ this.serviceName = config?.serviceName ?? DEFAULT_SERVICE;
317366
+ const endpointResolution = resolveOtlpEndpoint(this.signal, config);
317367
+ if (!endpointResolution) {
317368
+ log.info(`OTel ${signal} disabled: no OTLP endpoint or host provided`);
317369
+ return;
317370
+ }
317371
+ try {
317372
+ this.provider = this.createProvider(endpointResolution.endpoint, this.serviceName, endpointResolution.appendSignalPath);
317373
+ this.onProviderCreated(this.provider);
317374
+ this.isEnabled = true;
317375
+ log.info(`OTel ${signal} enabled: ${endpointResolution.endpoint}`);
317376
+ } catch (error) {
317377
+ log.error(`Failed to initialize OTel ${signal}:`, error);
317378
+ this.isEnabled = false;
317379
+ this.provider = null;
317380
+ }
317381
+ }
317382
+ onProviderCreated(_provider) {}
317383
+ onProviderClosed() {}
317384
+ getProvider() {
317385
+ return this.provider;
317386
+ }
317387
+ getServiceName() {
317388
+ return this.serviceName;
317389
+ }
317390
+ isOtelEnabled() {
317391
+ return this.isEnabled && this.provider !== null;
317392
+ }
317393
+ async close() {
317394
+ if (!this.provider) {
317395
+ return;
317396
+ }
317397
+ try {
317398
+ await this.shutdownProvider(this.provider);
317399
+ log.info(`OTel ${this.signal} provider shut down`);
317400
+ } catch (error) {
317401
+ log.error(`Error shutting down OTel ${this.signal} provider:`, error);
317402
+ }
317403
+ this.provider = null;
317404
+ this.isEnabled = false;
317405
+ this.onProviderClosed();
317406
+ }
317407
+ }
317408
+ function toAttributes(labels, service) {
317409
+ const attrs = { ...labels, service };
317410
+ for (const key of Object.keys(attrs)) {
317411
+ const v = attrs[key];
317412
+ if (typeof v !== "string" && typeof v !== "number") {
317413
+ attrs[key] = String(v);
317414
+ }
317415
+ }
317416
+ return attrs;
317417
+ }
317418
+
317419
+ class OtelMetrics extends BaseOtelSignal {
317420
+ counters = new Map;
317421
+ histograms = new Map;
317422
+ observableGauges = new Map;
317423
+ constructor(config) {
317424
+ super(config, "metrics");
317425
+ }
317426
+ createProvider(endpoint, serviceName, appendSignalPath) {
317427
+ const exporter = new import_exporter_metrics_otlp_http.OTLPMetricExporter({
317428
+ url: appendOtlpPath(endpoint, "metrics", appendSignalPath)
317429
+ });
317430
+ const reader = new import_sdk_metrics.PeriodicExportingMetricReader({
317431
+ exporter,
317432
+ exportIntervalMillis: EXPORT_INTERVAL_MS
317433
+ });
317434
+ const resource = import_resources.resourceFromAttributes({
317435
+ "service.name": serviceName
317436
+ });
317437
+ return new import_sdk_metrics.MeterProvider({
317438
+ resource,
317439
+ readers: [reader]
317440
+ });
317441
+ }
317442
+ onProviderCreated(provider) {
317443
+ import_api2.metrics.setGlobalMeterProvider(provider);
317444
+ }
317445
+ shutdownProvider(provider) {
317446
+ return provider.shutdown();
317447
+ }
317448
+ async initialize() {
317449
+ if (this.isOtelEnabled()) {
317450
+ log.info("OTel metrics initialized (storage is handled by the collector)");
317451
+ }
317452
+ }
317453
+ async insertMetric(metric) {
317454
+ if (!this.isOtelEnabled())
317455
+ return;
317456
+ const labels = metric.labels ? JSON.parse(metric.labels) : {};
317457
+ try {
317458
+ if (metric.metric_type === "counter") {
317459
+ await this.recordCounter(metric.metric_name, metric.value, labels, metric.service);
317460
+ } else if (metric.metric_type === "gauge") {
317461
+ await this.recordGauge(metric.metric_name, metric.value, labels, metric.service);
317462
+ } else {
317463
+ await this.recordHistogram(metric.metric_name, metric.value, labels, metric.service);
317464
+ }
317465
+ } catch {}
317466
+ }
317467
+ async insertMetrics(metricsList) {
317468
+ if (!this.isOtelEnabled() || metricsList.length === 0)
317469
+ return;
317470
+ for (const m of metricsList) {
317471
+ await this.insertMetric(m);
317472
+ }
317473
+ }
317474
+ async recordCounter(metricName, value, labels, service = this.getServiceName()) {
317475
+ const provider = this.getProvider();
317476
+ if (!this.isOtelEnabled() || !provider)
317477
+ return;
317478
+ try {
317479
+ let counter = this.counters.get(metricName);
317480
+ if (!counter) {
317481
+ const meter = provider.getMeter("cex-broker-metrics", "1.0.0");
317482
+ counter = meter.createCounter(metricName, { description: metricName });
317483
+ this.counters.set(metricName, counter);
317484
+ }
317485
+ counter.add(value, toAttributes(labels, service));
317486
+ } catch (error) {
317487
+ log.error("Failed to record counter:", error);
317488
+ }
317489
+ }
317490
+ async recordGauge(metricName, value, labels, service = this.getServiceName()) {
317491
+ const provider = this.getProvider();
317492
+ if (!this.isOtelEnabled() || !provider)
317493
+ return;
317494
+ try {
317495
+ let hist = this.histograms.get(`gauge_${metricName}`);
317496
+ if (!hist) {
317497
+ const meter = provider.getMeter("cex-broker-metrics", "1.0.0");
317498
+ hist = meter.createHistogram(`${metricName}_gauge`, {
317499
+ description: metricName
317500
+ });
317501
+ this.histograms.set(`gauge_${metricName}`, hist);
317502
+ }
317503
+ hist.record(value, toAttributes(labels, service));
317504
+ } catch (error) {
317505
+ log.error("Failed to record gauge:", error);
317506
+ }
317507
+ }
317508
+ async setObservableGauge(metricName, value, labels, service = this.getServiceName()) {
317509
+ const provider = this.getProvider();
317510
+ if (!this.isOtelEnabled() || !provider)
317511
+ return;
317512
+ try {
317513
+ let state = this.observableGauges.get(metricName);
317514
+ if (!state) {
317515
+ const observations = new Map;
317516
+ const instrument = provider.getMeter("cex-broker-metrics", "1.0.0").createObservableGauge(metricName, { description: metricName });
317517
+ instrument.addCallback((result) => {
317518
+ for (const observation of observations.values()) {
317519
+ result.observe(observation.value, observation.attributes);
317520
+ }
317521
+ });
317522
+ state = { instrument, observations };
317523
+ this.observableGauges.set(metricName, state);
317524
+ }
317525
+ const attributes = toAttributes(labels, service);
317526
+ state.observations.set(stableAttributeKey(attributes), {
317527
+ value,
317528
+ attributes
317529
+ });
317530
+ } catch (error) {
317531
+ log.error("Failed to set observable gauge:", error);
317532
+ }
317533
+ }
317534
+ async recordHistogram(metricName, value, labels, service = this.getServiceName()) {
317535
+ const provider = this.getProvider();
317536
+ if (!this.isOtelEnabled() || !provider)
317537
+ return;
317538
+ try {
317539
+ let hist = this.histograms.get(metricName);
317540
+ if (!hist) {
317541
+ const meter = provider.getMeter("cex-broker-metrics", "1.0.0");
317542
+ hist = meter.createHistogram(metricName, { description: metricName });
317543
+ this.histograms.set(metricName, hist);
317544
+ }
317545
+ hist.record(value, toAttributes(labels, service));
317546
+ } catch (error) {
317547
+ log.error("Failed to record histogram:", error);
317548
+ }
317549
+ }
317550
+ }
317551
+
317552
+ class OtelLogs extends BaseOtelSignal {
317553
+ logger = null;
317554
+ constructor(config) {
317555
+ super(config, "logs");
317556
+ }
317557
+ createProvider(endpoint, serviceName, appendSignalPath) {
317558
+ const exporter = new import_exporter_logs_otlp_http.OTLPLogExporter({
317559
+ url: appendOtlpPath(endpoint, "logs", appendSignalPath)
317560
+ });
317561
+ const processor = new import_sdk_logs.BatchLogRecordProcessor(exporter);
317562
+ const resource = import_resources.resourceFromAttributes({
317563
+ "service.name": serviceName
317564
+ });
317565
+ return new import_sdk_logs.LoggerProvider({
317566
+ resource,
317567
+ processors: [processor]
317568
+ });
317569
+ }
317570
+ onProviderCreated(provider) {
317571
+ import_api_logs3.logs.setGlobalLoggerProvider(provider);
317572
+ this.logger = provider.getLogger("cex-broker-logs", "1.0.0");
317573
+ }
317574
+ shutdownProvider(provider) {
317575
+ return provider.forceFlush().then(() => provider.shutdown());
317576
+ }
317577
+ onProviderClosed() {
317578
+ this.logger = null;
317579
+ }
317580
+ emit(record) {
317581
+ if (!this.isOtelEnabled() || !this.logger) {
317582
+ return;
317583
+ }
317584
+ this.logger.emit(record);
317585
+ }
317586
+ }
317587
+ function resolveOtlpEndpoint(signal, config) {
317588
+ if (config?.otlpEndpoint) {
317589
+ return {
317590
+ endpoint: normalizeOtlpEndpoint(config.otlpEndpoint),
317591
+ appendSignalPath: true
317592
+ };
317593
+ }
317594
+ const signalEndpoint = signal === "metrics" ? process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT : process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT;
317595
+ if (signalEndpoint) {
317596
+ return {
317597
+ endpoint: signalEndpoint,
317598
+ appendSignalPath: false
317599
+ };
317600
+ }
317601
+ if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {
317602
+ return {
317603
+ endpoint: normalizeOtlpEndpoint(process.env.OTEL_EXPORTER_OTLP_ENDPOINT),
317604
+ appendSignalPath: true
317605
+ };
317606
+ }
317607
+ if (config?.host) {
317608
+ const protocol = config.protocol || "http";
317609
+ const port = config.port ?? DEFAULT_OTLP_PORT;
317610
+ return {
317611
+ endpoint: `${protocol}://${config.host}:${port}`,
317612
+ appendSignalPath: true
317613
+ };
317614
+ }
317615
+ return null;
317616
+ }
317617
+ function appendOtlpPath(endpoint, signal, appendSignalPath) {
317618
+ if (!appendSignalPath) {
317619
+ return endpoint;
317620
+ }
317621
+ const baseEndpoint = normalizeOtlpEndpoint(endpoint);
317622
+ return `${baseEndpoint}/v1/${signal}`;
317623
+ }
317624
+ function normalizeOtlpEndpoint(endpoint) {
317625
+ return endpoint.replace(/\/v1\/(metrics|logs)\/?$/, "").replace(/\/+$/, "");
317626
+ }
317627
+ function getOtelHostFromEnv() {
317628
+ return process.env.CEX_BROKER_OTEL_HOST ?? process.env.CEX_BROKER_CLICKHOUSE_HOST;
317629
+ }
317630
+ function getOtelPortFromEnv() {
317631
+ const port = process.env.CEX_BROKER_OTEL_PORT ?? process.env.CEX_BROKER_CLICKHOUSE_PORT;
317632
+ return port ? Number.parseInt(port, 10) : undefined;
317633
+ }
317634
+ function getOtelProtocolFromEnv() {
317635
+ const protocol = process.env.CEX_BROKER_OTEL_PROTOCOL ?? process.env.CEX_BROKER_CLICKHOUSE_PROTOCOL;
317636
+ return protocol || "http";
317637
+ }
317638
+ function createOtelMetricsFromEnv(options = {}) {
317639
+ const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
317640
+ const serviceName = process.env.OTEL_SERVICE_NAME || options.defaultServiceName || DEFAULT_SERVICE;
317641
+ if (otlpEndpoint) {
317642
+ return new OtelMetrics({
317643
+ otlpEndpoint,
317644
+ serviceName
317645
+ });
317646
+ }
317647
+ if (options.allowLegacyBrokerConfig === false) {
317648
+ return new OtelMetrics({ serviceName });
317649
+ }
317650
+ const host = getOtelHostFromEnv();
317651
+ if (!host)
317652
+ return new OtelMetrics({ serviceName });
317653
+ const port = getOtelPortFromEnv();
317654
+ const config = {
317655
+ host,
317656
+ port: port ?? DEFAULT_OTLP_PORT,
317657
+ protocol: getOtelProtocolFromEnv(),
317658
+ serviceName
317659
+ };
317660
+ return new OtelMetrics(config);
317661
+ }
317662
+ function stableAttributeKey(attributes) {
317663
+ return JSON.stringify(Object.entries(attributes).sort(([left], [right]) => left.localeCompare(right)));
317664
+ }
317665
+ function createOtelLogsFromEnv() {
317666
+ const logsEndpoint = process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT;
317667
+ const genericEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
317668
+ const host = getOtelHostFromEnv();
317669
+ if (logsEndpoint) {
317670
+ return new OtelLogs({
317671
+ serviceName: process.env.OTEL_SERVICE_NAME || DEFAULT_SERVICE
317672
+ });
317673
+ }
317674
+ if (genericEndpoint) {
317675
+ return new OtelLogs({
317676
+ otlpEndpoint: genericEndpoint,
317677
+ serviceName: process.env.OTEL_SERVICE_NAME || DEFAULT_SERVICE
317678
+ });
317131
317679
  }
317132
- async insertMetric(metric) {
317133
- if (!this.isOtelEnabled())
317680
+ if (!host) {
317681
+ return new OtelLogs;
317682
+ }
317683
+ const port = getOtelPortFromEnv();
317684
+ const config = {
317685
+ host,
317686
+ port: port ?? DEFAULT_OTLP_PORT,
317687
+ protocol: getOtelProtocolFromEnv(),
317688
+ serviceName: process.env.OTEL_SERVICE_NAME || DEFAULT_SERVICE
317689
+ };
317690
+ return new OtelLogs(config);
317691
+ }
317692
+
317693
+ // src/helpers/stream-health-publisher.ts
317694
+ import { createHash as createHash4, randomUUID } from "node:crypto";
317695
+ import {
317696
+ closeSync as closeSync2,
317697
+ fsyncSync as fsyncSync2,
317698
+ openSync as openSync2,
317699
+ readFileSync,
317700
+ renameSync,
317701
+ statSync,
317702
+ unlinkSync,
317703
+ writeFileSync
317704
+ } from "node:fs";
317705
+ import { request as httpRequest3 } from "node:http";
317706
+ import { request as httpsRequest3 } from "node:https";
317707
+ import { dirname } from "node:path";
317708
+ var SOURCE = "broker_write";
317709
+ var TABLE = "broker_stream_health.snapshots";
317710
+ var PRODUCER_ID = "cex-broker-user-data";
317711
+ var STATE_VERSION = 1;
317712
+ var HEARTBEAT_MS = 30000;
317713
+ var FORWARDER_TIMEOUT_MS = 3000;
317714
+ var IDENTIFIER = /^[a-z0-9][a-z0-9:_-]{0,127}$/;
317715
+ function identifier(value, name) {
317716
+ const normalized = value.trim().toLowerCase();
317717
+ if (!IDENTIFIER.test(normalized)) {
317718
+ throw new Error(`${name} must be a lower-case stream-health identifier`);
317719
+ }
317720
+ return normalized;
317721
+ }
317722
+ function counter(value) {
317723
+ if (!/^(0|[1-9]\d*)$/.test(value)) {
317724
+ throw new Error("Invalid persisted stream-health counter");
317725
+ }
317726
+ return BigInt(value);
317727
+ }
317728
+ function next(value) {
317729
+ return (counter(value) + 1n).toString();
317730
+ }
317731
+ function key(snapshot) {
317732
+ return `exchange:${snapshot.exchange}|account:${snapshot.accountSelector}|stream:${snapshot.streamKind}|scope:${snapshot.accountScope}`;
317733
+ }
317734
+ function registryRevision(snapshots) {
317735
+ const rows = snapshots.map((snapshot) => ({
317736
+ exchange: snapshot.exchange,
317737
+ account_selector: snapshot.accountSelector,
317738
+ account_role: snapshot.accountRole ?? null,
317739
+ stream_kind: snapshot.streamKind,
317740
+ account_scope: snapshot.accountScope,
317741
+ registry_status: snapshot.registryStatus
317742
+ })).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
317743
+ return createHash4("sha256").update(JSON.stringify(rows)).digest("hex");
317744
+ }
317745
+ function validState(value) {
317746
+ if (!value || typeof value !== "object" || Array.isArray(value))
317747
+ return false;
317748
+ const state = value;
317749
+ return state.version === STATE_VERSION && state.producerId === PRODUCER_ID && typeof state.producerEpoch === "string" && typeof state.runId === "string" && typeof state.nextBatchSequence === "string" && state.nextStreamSequences !== null && typeof state.nextStreamSequences === "object" && (state.pendingBody === undefined || typeof state.pendingBody === "string");
317750
+ }
317751
+ function forwarderPost(url2, body, authToken, timeoutMs) {
317752
+ const request = url2.protocol === "http:" ? httpRequest3 : httpsRequest3;
317753
+ const headers = {
317754
+ "content-type": "application/json",
317755
+ "content-length": Buffer.byteLength(body)
317756
+ };
317757
+ if (authToken)
317758
+ headers.authorization = `Bearer ${authToken}`;
317759
+ return new Promise((resolve, reject) => {
317760
+ const req = request(url2, { method: "POST", headers, timeout: timeoutMs }, (res) => {
317761
+ res.on("data", () => {});
317762
+ res.on("end", () => {
317763
+ const status = res.statusCode ?? 0;
317764
+ if (status < 200 || status >= 300) {
317765
+ reject(new Error(`Stream health forwarder returned ${status}`));
317766
+ return;
317767
+ }
317768
+ resolve();
317769
+ });
317770
+ });
317771
+ req.on("error", reject);
317772
+ req.on("timeout", () => req.destroy(new Error("Stream health forwarder timed out")));
317773
+ req.write(body);
317774
+ req.end();
317775
+ });
317776
+ }
317777
+
317778
+ class StreamHealthPublisher {
317779
+ #deploymentId;
317780
+ #statePath;
317781
+ #heartbeatMs;
317782
+ #post;
317783
+ #state;
317784
+ #advanceRun;
317785
+ #snapshots = [];
317786
+ #dirty = false;
317787
+ #closed = false;
317788
+ #pumping = null;
317789
+ #heartbeat = null;
317790
+ #retry = null;
317791
+ #retryAttempt = 0;
317792
+ constructor(options) {
317793
+ this.#deploymentId = identifier(options.deploymentId, "deployment_id");
317794
+ this.#statePath = options.statePath.trim();
317795
+ if (!this.#statePath) {
317796
+ throw new Error("CEX_BROKER_STREAM_HEALTH_STATE_PATH is required");
317797
+ }
317798
+ this.#heartbeatMs = options.heartbeatIntervalMs ?? HEARTBEAT_MS;
317799
+ if (!Number.isInteger(this.#heartbeatMs) || this.#heartbeatMs < 1 || this.#heartbeatMs > 60000) {
317800
+ throw new Error("Stream health heartbeat interval must be between 1 and 60000ms");
317801
+ }
317802
+ let url2;
317803
+ try {
317804
+ url2 = new URL(options.forwarderUrl.trim());
317805
+ } catch (error) {
317806
+ throw new Error("CEX_BROKER_ARCHIVE_FORWARDER_URL must be valid", {
317807
+ cause: error
317808
+ });
317809
+ }
317810
+ if (url2.protocol !== "http:" && url2.protocol !== "https:") {
317811
+ throw new Error("CEX_BROKER_ARCHIVE_FORWARDER_URL must use HTTP(S)");
317812
+ }
317813
+ this.#post = options.post ?? ((body) => forwarderPost(url2, body, options.forwarderAuthToken, options.forwarderTimeoutMs ?? FORWARDER_TIMEOUT_MS));
317814
+ const loaded = this.#read();
317815
+ this.#state = loaded ?? {
317816
+ version: STATE_VERSION,
317817
+ producerId: PRODUCER_ID,
317818
+ producerEpoch: "1",
317819
+ runId: randomUUID(),
317820
+ nextBatchSequence: "1",
317821
+ nextStreamSequences: {}
317822
+ };
317823
+ counter(this.#state.producerEpoch);
317824
+ counter(this.#state.nextBatchSequence);
317825
+ for (const value of Object.values(this.#state.nextStreamSequences))
317826
+ counter(value);
317827
+ this.#advanceRun = loaded !== null;
317828
+ if (!loaded)
317829
+ this.#persist();
317830
+ }
317831
+ start() {
317832
+ if (this.#closed || this.#heartbeat)
317134
317833
  return;
317135
- const labels = metric.labels ? JSON.parse(metric.labels) : {};
317834
+ this.#heartbeat = setInterval(() => {
317835
+ if (this.#snapshots.length > 0) {
317836
+ this.#dirty = true;
317837
+ this.#schedule();
317838
+ }
317839
+ }, this.#heartbeatMs);
317840
+ this.#heartbeat.unref?.();
317841
+ if (this.#state.pendingBody || this.#dirty)
317842
+ this.#schedule();
317843
+ }
317844
+ publish(snapshots) {
317845
+ if (this.#closed)
317846
+ return;
317847
+ this.#snapshots = snapshots.map((snapshot) => ({ ...snapshot }));
317848
+ this.#dirty = true;
317849
+ this.#schedule();
317850
+ }
317851
+ async close(snapshots, timeoutMs = FORWARDER_TIMEOUT_MS) {
317852
+ if (this.#closed)
317853
+ return;
317854
+ if (this.#heartbeat)
317855
+ clearInterval(this.#heartbeat);
317856
+ this.#heartbeat = null;
317857
+ if (this.#retry)
317858
+ clearTimeout(this.#retry);
317859
+ this.#retry = null;
317860
+ this.#snapshots = snapshots.map((snapshot) => ({ ...snapshot }));
317861
+ this.#dirty = this.#snapshots.length > 0;
317862
+ this.#schedule();
317863
+ await Promise.race([
317864
+ this.#waitForIdle(),
317865
+ new Promise((resolve) => setTimeout(resolve, timeoutMs))
317866
+ ]);
317867
+ this.#closed = true;
317868
+ if (this.#retry)
317869
+ clearTimeout(this.#retry);
317870
+ this.#retry = null;
317871
+ }
317872
+ #schedule() {
317873
+ if (this.#closed || this.#pumping)
317874
+ return;
317875
+ this.#pumping = this.#pump().finally(() => {
317876
+ this.#pumping = null;
317877
+ if (!this.#closed && !this.#retry && (this.#state.pendingBody || this.#dirty))
317878
+ this.#schedule();
317879
+ });
317880
+ }
317881
+ async#pump() {
317882
+ if (this.#state.pendingBody && !await this.#deliver())
317883
+ return;
317884
+ if (!this.#dirty || this.#snapshots.length === 0)
317885
+ return;
317886
+ if (this.#advanceRun) {
317887
+ this.#state.producerEpoch = next(this.#state.producerEpoch);
317888
+ this.#state.runId = randomUUID();
317889
+ this.#state.nextBatchSequence = "1";
317890
+ this.#state.nextStreamSequences = {};
317891
+ this.#advanceRun = false;
317892
+ this.#persist();
317893
+ }
317894
+ this.#dirty = false;
317895
+ this.#state.pendingBody = this.#body(this.#snapshots);
317896
+ this.#persist();
317897
+ await this.#deliver();
317898
+ }
317899
+ async#deliver() {
317900
+ const body = this.#state.pendingBody;
317901
+ if (!body)
317902
+ return true;
317136
317903
  try {
317137
- if (metric.metric_type === "counter") {
317138
- await this.recordCounter(metric.metric_name, metric.value, labels, metric.service);
317139
- } else if (metric.metric_type === "gauge") {
317140
- await this.recordGauge(metric.metric_name, metric.value, labels, metric.service);
317141
- } else {
317142
- await this.recordHistogram(metric.metric_name, metric.value, labels, metric.service);
317904
+ await this.#post(body);
317905
+ this.#state.pendingBody = undefined;
317906
+ this.#persist();
317907
+ this.#retryAttempt = 0;
317908
+ return true;
317909
+ } catch {
317910
+ this.#retryLater();
317911
+ return false;
317912
+ }
317913
+ }
317914
+ #body(snapshots) {
317915
+ const ordered3 = [...snapshots].sort((left, right) => key(left).localeCompare(key(right)));
317916
+ if (ordered3.length === 0 || ordered3.length > 1000) {
317917
+ throw new Error("Stream health requires between one and 1000 registry rows");
317918
+ }
317919
+ const batchSequence = this.#state.nextBatchSequence;
317920
+ this.#state.nextBatchSequence = next(batchSequence);
317921
+ const heartbeatAt = new Date().toISOString();
317922
+ const active = ordered3.filter((snapshot) => snapshot.registryStatus === "active").length;
317923
+ const rows = ordered3.map((snapshot) => {
317924
+ const streamKey = key(snapshot);
317925
+ const sequence = this.#state.nextStreamSequences[streamKey] ?? "1";
317926
+ this.#state.nextStreamSequences[streamKey] = next(sequence);
317927
+ return {
317928
+ table: TABLE,
317929
+ row: {
317930
+ producer_id: PRODUCER_ID,
317931
+ producer_epoch: this.#state.producerEpoch,
317932
+ run_id: this.#state.runId,
317933
+ batch_sequence: batchSequence,
317934
+ batch_snapshot_count: String(ordered3.length),
317935
+ batch_active_stream_count: String(active),
317936
+ registry_revision: registryRevision(ordered3),
317937
+ registry_status: snapshot.registryStatus,
317938
+ retired_at: snapshot.retiredAt,
317939
+ exchange: snapshot.exchange,
317940
+ account_selector: snapshot.accountSelector,
317941
+ account_role: snapshot.accountRole ?? null,
317942
+ stream_kind: snapshot.streamKind,
317943
+ account_scope: snapshot.accountScope,
317944
+ sequence,
317945
+ state: snapshot.state,
317946
+ state_changed_at: snapshot.stateChangedAt,
317947
+ last_connected_at: snapshot.lastConnectedAt,
317948
+ last_authenticated_at: snapshot.lastAuthenticatedAt,
317949
+ last_received_at: snapshot.lastReceivedAt,
317950
+ heartbeat_at: heartbeatAt,
317951
+ connect_attempt_count: snapshot.connectAttemptCount,
317952
+ reconnect_count: snapshot.reconnectCount,
317953
+ error_count: snapshot.errorCount,
317954
+ last_failure_kind: snapshot.lastFailureKind,
317955
+ last_failure_reason: snapshot.lastFailureReason,
317956
+ traffic_mode: snapshot.trafficMode,
317957
+ source_watermark: snapshot.sourceWatermark
317958
+ }
317959
+ };
317960
+ });
317961
+ return JSON.stringify({
317962
+ source: SOURCE,
317963
+ deployment_id: this.#deploymentId,
317964
+ rows
317965
+ });
317966
+ }
317967
+ #retryLater() {
317968
+ if (this.#closed || this.#retry)
317969
+ return;
317970
+ const delay = Math.min(1000 * 2 ** this.#retryAttempt, 30000);
317971
+ this.#retryAttempt += 1;
317972
+ this.#retry = setTimeout(() => {
317973
+ this.#retry = null;
317974
+ this.#schedule();
317975
+ }, delay);
317976
+ this.#retry.unref?.();
317977
+ }
317978
+ async#waitForIdle() {
317979
+ while (this.#pumping)
317980
+ await this.#pumping;
317981
+ }
317982
+ #read() {
317983
+ try {
317984
+ const parsed = JSON.parse(readFileSync(this.#statePath, "utf8"));
317985
+ if (!validState(parsed))
317986
+ throw new Error("invalid state shape");
317987
+ return parsed;
317988
+ } catch (error) {
317989
+ if (error.code === "ENOENT")
317990
+ return null;
317991
+ throw new Error("Stream health state cannot be read", { cause: error });
317992
+ }
317993
+ }
317994
+ #persist() {
317995
+ const parent = dirname(this.#statePath);
317996
+ try {
317997
+ if (!statSync(parent).isDirectory())
317998
+ throw new Error("state parent is not a directory");
317999
+ } catch (error) {
318000
+ throw new Error("Stream health state directory is unavailable", {
318001
+ cause: error
318002
+ });
318003
+ }
318004
+ const temporary = `${this.#statePath}.${process.pid}.${randomUUID()}.tmp`;
318005
+ let fd2;
318006
+ try {
318007
+ fd2 = openSync2(temporary, "wx", 384);
318008
+ writeFileSync(fd2, JSON.stringify(this.#state));
318009
+ fsyncSync2(fd2);
318010
+ closeSync2(fd2);
318011
+ fd2 = undefined;
318012
+ renameSync(temporary, this.#statePath);
318013
+ const parentFd = openSync2(parent, "r");
318014
+ try {
318015
+ fsyncSync2(parentFd);
318016
+ } finally {
318017
+ closeSync2(parentFd);
318018
+ }
318019
+ } catch (error) {
318020
+ if (fd2 !== undefined)
318021
+ closeSync2(fd2);
318022
+ try {
318023
+ unlinkSync(temporary);
318024
+ } catch {}
318025
+ throw new Error("Stream health state cannot be persisted", {
318026
+ cause: error
318027
+ });
318028
+ }
318029
+ }
318030
+ }
318031
+ function streamHealthPublisherConfigFromEnv(env = process.env) {
318032
+ if (env.CEX_BROKER_ARCHIVE_ENABLED !== "true") {
318033
+ throw new Error("Configured account user streams require CEX_BROKER_ARCHIVE_ENABLED=true");
318034
+ }
318035
+ const deploymentId = env.CEX_BROKER_DEPLOYMENT_ID?.trim();
318036
+ const forwarderUrl = env.CEX_BROKER_ARCHIVE_FORWARDER_URL?.trim();
318037
+ const statePath = env.CEX_BROKER_STREAM_HEALTH_STATE_PATH?.trim();
318038
+ if (!deploymentId || !forwarderUrl || !statePath) {
318039
+ throw new Error("Configured account user streams require deployment, forwarder, and persistent state configuration");
318040
+ }
318041
+ return {
318042
+ deploymentId,
318043
+ forwarderUrl,
318044
+ statePath,
318045
+ forwarderAuthToken: env.CEX_BROKER_ARCHIVE_FORWARDER_TOKEN?.trim() || undefined
318046
+ };
318047
+ }
318048
+
318049
+ // src/helpers/binance-user-data-stream.ts
318050
+ import { Buffer as Buffer2 } from "node:buffer";
318051
+ import { createHmac } from "node:crypto";
318052
+ var BINANCE_SPOT_WS_API_URL = "wss://ws-api.binance.com:443/ws-api/v3";
318053
+ var DEFAULT_BINANCE_USER_DATA_MAX_BUFFERED_EVENTS = 16;
318054
+ var createWebSocket = (url2) => new wrapper_default(url2);
318055
+ var userDataRequestCounter = 0;
318056
+ function getExchangeString(exchange, key2) {
318057
+ const value = exchange[key2];
318058
+ if (typeof value !== "string" || value.length === 0) {
318059
+ throw new Error(`Binance user-data stream requires exchange.${key2}`);
318060
+ }
318061
+ return value;
318062
+ }
318063
+ function sortedQuery(params) {
318064
+ return Object.entries(params).sort(([left], [right]) => left.localeCompare(right)).map(([key2, value]) => `${encodeURIComponent(key2)}=${encodeURIComponent(String(value))}`).join("&");
318065
+ }
318066
+ function signUserDataStreamParams(exchange, params) {
318067
+ const signParams = exchange.signParams;
318068
+ if (typeof signParams === "function") {
318069
+ return signParams.call(exchange, params);
318070
+ }
318071
+ const secret = getExchangeString(exchange, "secret");
318072
+ return {
318073
+ ...params,
318074
+ signature: createHmac("sha256", secret).update(sortedQuery(params)).digest("hex")
318075
+ };
318076
+ }
318077
+ function getBinanceSpotWsApiUrl(exchange) {
318078
+ const urls = exchange.urls;
318079
+ return urls?.api?.ws?.["ws-api"]?.spot ?? BINANCE_SPOT_WS_API_URL;
318080
+ }
318081
+ function getRecord(value) {
318082
+ return typeof value === "object" && value !== null ? value : null;
318083
+ }
318084
+ function getMessage(value) {
318085
+ if (value instanceof Error) {
318086
+ return value.message;
318087
+ }
318088
+ if (typeof value === "string" && value.length > 0) {
318089
+ return value;
318090
+ }
318091
+ const record = getRecord(value);
318092
+ const message = record?.message;
318093
+ return typeof message === "string" && message.length > 0 ? message : null;
318094
+ }
318095
+ function getOptionalExchangeString(exchange, key2) {
318096
+ const value = exchange[key2];
318097
+ return typeof value === "string" && value.length > 0 ? value : null;
318098
+ }
318099
+ function redactDiagnosticMessage(message, secretValues) {
318100
+ let redacted = message;
318101
+ for (const value of secretValues) {
318102
+ if (value.length > 0) {
318103
+ redacted = redacted.split(value).join("[redacted]");
318104
+ }
318105
+ }
318106
+ return redacted.replace(/(\b(?:apiKey|secret|signature)\b\s*=\s*)[^\s&,;)]+/gi, "$1[redacted]").replace(/("(?:apiKey|secret|signature)"\s*:\s*")[^"]*(")/gi, "$1[redacted]$2");
318107
+ }
318108
+ function formatBinanceUserDataWebSocketError(event, secretValues) {
318109
+ const record = getRecord(event);
318110
+ const message = getMessage(record?.error) ?? getMessage(record?.message) ?? getMessage(event);
318111
+ const safeMessage = message === null ? null : redactDiagnosticMessage(message, secretValues);
318112
+ return new Error(safeMessage ? `Binance user-data WebSocket error: ${safeMessage}` : "Binance user-data WebSocket error");
318113
+ }
318114
+ function getCloseReason(value) {
318115
+ if (typeof value === "string") {
318116
+ return value.length > 0 ? value : null;
318117
+ }
318118
+ if (Buffer2.isBuffer(value)) {
318119
+ const reason = value.toString("utf8");
318120
+ return reason.length > 0 ? reason : null;
318121
+ }
318122
+ if (value instanceof Uint8Array) {
318123
+ const reason = Buffer2.from(value).toString("utf8");
318124
+ return reason.length > 0 ? reason : null;
318125
+ }
318126
+ return null;
318127
+ }
318128
+ function formatBinanceUserDataWebSocketClose(codeOrEvent, reasonOrUndefined, secretValues) {
318129
+ const record = getRecord(codeOrEvent);
318130
+ const code = record ? record.code : codeOrEvent;
318131
+ const reason = getCloseReason(record ? record.reason : reasonOrUndefined);
318132
+ const safeReason = reason === null ? null : redactDiagnosticMessage(reason, secretValues);
318133
+ const details = [
318134
+ typeof code === "number" || typeof code === "string" ? `code=${code}` : null,
318135
+ safeReason ? `reason=${safeReason}` : null
318136
+ ].filter((detail) => detail !== null);
318137
+ return new Error(details.length > 0 ? `Binance user-data WebSocket closed unexpectedly (${details.join(", ")})` : "Binance user-data WebSocket closed unexpectedly");
318138
+ }
318139
+ function decodeMessageData(data) {
318140
+ if (typeof data === "string") {
318141
+ return data;
318142
+ }
318143
+ if (Buffer2.isBuffer(data)) {
318144
+ return data.toString("utf8");
318145
+ }
318146
+ if (data instanceof ArrayBuffer) {
318147
+ return Buffer2.from(data).toString("utf8");
318148
+ }
318149
+ if (ArrayBuffer.isView(data)) {
318150
+ return Buffer2.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
318151
+ }
318152
+ if (Array.isArray(data) && data.every((item) => Buffer2.isBuffer(item))) {
318153
+ return Buffer2.concat(data).toString("utf8");
318154
+ }
318155
+ return data;
318156
+ }
318157
+
318158
+ class BinanceSpotUserDataStream {
318159
+ exchange;
318160
+ ws;
318161
+ secretValues;
318162
+ requestId = `user-data-${Date.now()}-${userDataRequestCounter++}`;
318163
+ maxBufferedEvents;
318164
+ observer;
318165
+ queue = [];
318166
+ waiters = [];
318167
+ closed = false;
318168
+ closeError = null;
318169
+ subscriptionId = null;
318170
+ constructor(exchange, options = {}) {
318171
+ this.exchange = exchange;
318172
+ this.maxBufferedEvents = options.maxBufferedEvents ?? DEFAULT_BINANCE_USER_DATA_MAX_BUFFERED_EVENTS;
318173
+ this.observer = options.observer;
318174
+ this.secretValues = [
318175
+ getOptionalExchangeString(exchange, "apiKey"),
318176
+ getOptionalExchangeString(exchange, "secret")
318177
+ ].filter((value) => value !== null);
318178
+ this.ws = createWebSocket(getBinanceSpotWsApiUrl(exchange));
318179
+ this.ws.on("open", () => {
318180
+ this.observer?.onConnected?.();
318181
+ this.subscribe();
318182
+ });
318183
+ this.ws.on("message", (data) => this.handleMessage(data));
318184
+ this.ws.on("error", (error) => this.fail(formatBinanceUserDataWebSocketError(error, this.secretValues), "transport_error"));
318185
+ this.ws.on("close", (code, reason) => this.handleClose(code, reason));
318186
+ }
318187
+ async* [Symbol.asyncIterator]() {
318188
+ while (true) {
318189
+ const event = await this.nextEvent();
318190
+ if (!event) {
318191
+ break;
317143
318192
  }
318193
+ yield event;
318194
+ }
318195
+ }
318196
+ close() {
318197
+ if (this.closed) {
318198
+ return;
318199
+ }
318200
+ this.closed = true;
318201
+ this.queue.length = 0;
318202
+ try {
318203
+ this.ws.close();
317144
318204
  } catch {}
318205
+ this.flushWaiters();
318206
+ }
318207
+ handleClose(code, reason) {
318208
+ if (this.closed) {
318209
+ return;
318210
+ }
318211
+ this.fail(formatBinanceUserDataWebSocketClose(code, reason, this.secretValues), "remote_closed");
318212
+ }
318213
+ subscribe() {
318214
+ const apiKey = getExchangeString(this.exchange, "apiKey");
318215
+ const signedParams = signUserDataStreamParams(this.exchange, {
318216
+ apiKey,
318217
+ timestamp: Date.now()
318218
+ });
318219
+ this.ws.send(JSON.stringify({
318220
+ id: this.requestId,
318221
+ method: "userDataStream.subscribe.signature",
318222
+ params: signedParams
318223
+ }));
317145
318224
  }
317146
- async insertMetrics(metricsList) {
317147
- if (!this.isOtelEnabled() || metricsList.length === 0)
318225
+ handleMessage(data) {
318226
+ if (this.closed) {
317148
318227
  return;
317149
- for (const m of metricsList) {
317150
- await this.insertMetric(m);
317151
318228
  }
317152
- }
317153
- async recordCounter(metricName, value, labels, service = this.getServiceName()) {
317154
- const provider = this.getProvider();
317155
- if (!this.isOtelEnabled() || !provider)
317156
- return;
318229
+ let message;
317157
318230
  try {
317158
- let counter = this.counters.get(metricName);
317159
- if (!counter) {
317160
- const meter = provider.getMeter("cex-broker-metrics", "1.0.0");
317161
- counter = meter.createCounter(metricName, { description: metricName });
317162
- this.counters.set(metricName, counter);
317163
- }
317164
- counter.add(value, toAttributes(labels, service));
318231
+ const decodedData = decodeMessageData(data);
318232
+ message = typeof decodedData === "string" ? JSON.parse(decodedData) : decodedData;
317165
318233
  } catch (error) {
317166
- log.error("Failed to record counter:", error);
317167
- }
317168
- }
317169
- async recordGauge(metricName, value, labels, service = this.getServiceName()) {
317170
- const provider = this.getProvider();
317171
- if (!this.isOtelEnabled() || !provider)
318234
+ this.fail(error instanceof Error ? error : new Error("Invalid Binance user-data message"), "protocol_error");
317172
318235
  return;
317173
- try {
317174
- let hist = this.histograms.get(`gauge_${metricName}`);
317175
- if (!hist) {
317176
- const meter = provider.getMeter("cex-broker-metrics", "1.0.0");
317177
- hist = meter.createHistogram(`${metricName}_gauge`, {
317178
- description: metricName
317179
- });
317180
- this.histograms.set(`gauge_${metricName}`, hist);
318236
+ }
318237
+ if ("id" in message && message.id === this.requestId) {
318238
+ if (message.status !== 200) {
318239
+ this.fail(new Error(message.error?.msg ?? message.error?.message ?? `Binance user-data subscription failed with status ${message.status}`), "auth_failed");
318240
+ return;
317181
318241
  }
317182
- hist.record(value, toAttributes(labels, service));
317183
- } catch (error) {
317184
- log.error("Failed to record gauge:", error);
318242
+ this.subscriptionId = message.result?.subscriptionId ?? null;
318243
+ this.observer?.onAuthenticated?.();
318244
+ return;
317185
318245
  }
318246
+ if ("status" in message && typeof message.status === "number" && message.status !== 200) {
318247
+ const errorMessage = message.error?.msg ?? message.error?.message ?? `Binance user-data request failed with status ${message.status}`;
318248
+ const errorCode2 = message.error?.code;
318249
+ this.fail(new Error(typeof errorCode2 === "number" ? `${errorMessage} (code ${errorCode2})` : errorMessage), "protocol_error");
318250
+ return;
318251
+ }
318252
+ if (!("event" in message) || !message.event) {
318253
+ return;
318254
+ }
318255
+ const subscriptionId = message.subscriptionId ?? this.subscriptionId;
318256
+ if (subscriptionId === null || subscriptionId === undefined) {
318257
+ return;
318258
+ }
318259
+ this.push({ subscriptionId, event: message.event });
317186
318260
  }
317187
- async setObservableGauge(metricName, value, labels, service = this.getServiceName()) {
317188
- const provider = this.getProvider();
317189
- if (!this.isOtelEnabled() || !provider)
318261
+ push(event) {
318262
+ if (this.closed) {
318263
+ return;
318264
+ }
318265
+ this.observer?.onEvent?.(event);
318266
+ const waiter = this.waiters.shift();
318267
+ if (waiter) {
318268
+ waiter.resolve(event);
318269
+ return;
318270
+ }
318271
+ if (this.queue.length >= this.maxBufferedEvents) {
318272
+ this.fail(new Error(`Binance user-data stream buffered event limit exceeded (${this.maxBufferedEvents}); downstream consumer is not keeping up`), "backpressure");
317190
318273
  return;
317191
- try {
317192
- let state = this.observableGauges.get(metricName);
317193
- if (!state) {
317194
- const observations = new Map;
317195
- const instrument = provider.getMeter("cex-broker-metrics", "1.0.0").createObservableGauge(metricName, { description: metricName });
317196
- instrument.addCallback((result) => {
317197
- for (const observation of observations.values()) {
317198
- result.observe(observation.value, observation.attributes);
317199
- }
317200
- });
317201
- state = { instrument, observations };
317202
- this.observableGauges.set(metricName, state);
317203
- }
317204
- const attributes = toAttributes(labels, service);
317205
- state.observations.set(stableAttributeKey(attributes), {
317206
- value,
317207
- attributes
317208
- });
317209
- } catch (error) {
317210
- log.error("Failed to set observable gauge:", error);
317211
318274
  }
318275
+ this.queue.push(event);
317212
318276
  }
317213
- async recordHistogram(metricName, value, labels, service = this.getServiceName()) {
317214
- const provider = this.getProvider();
317215
- if (!this.isOtelEnabled() || !provider)
318277
+ nextEvent() {
318278
+ const event = this.queue.shift();
318279
+ if (event) {
318280
+ return Promise.resolve(event);
318281
+ }
318282
+ if (this.closeError) {
318283
+ return Promise.reject(this.closeError);
318284
+ }
318285
+ if (this.closed) {
318286
+ return Promise.resolve(null);
318287
+ }
318288
+ return new Promise((resolve, reject) => {
318289
+ this.waiters.push({ resolve, reject });
318290
+ });
318291
+ }
318292
+ fail(error, kind) {
318293
+ if (this.closeError) {
317216
318294
  return;
318295
+ }
318296
+ this.closeError = error;
318297
+ this.observer?.onFailure?.({ kind, reason: error.message });
318298
+ this.closed = true;
318299
+ this.queue.length = 0;
318300
+ this.flushWaiters();
317217
318301
  try {
317218
- let hist = this.histograms.get(metricName);
317219
- if (!hist) {
317220
- const meter = provider.getMeter("cex-broker-metrics", "1.0.0");
317221
- hist = meter.createHistogram(metricName, { description: metricName });
317222
- this.histograms.set(metricName, hist);
318302
+ this.ws.close();
318303
+ } catch {}
318304
+ }
318305
+ flushWaiters() {
318306
+ const error = this.closeError;
318307
+ for (const waiter of this.waiters.splice(0)) {
318308
+ if (error) {
318309
+ waiter.reject(error);
318310
+ } else {
318311
+ waiter.resolve(null);
317223
318312
  }
317224
- hist.record(value, toAttributes(labels, service));
317225
- } catch (error) {
317226
- log.error("Failed to record histogram:", error);
317227
318313
  }
317228
318314
  }
317229
318315
  }
318316
+ function isBinanceBalanceUserDataEvent(event) {
318317
+ return event.e === "outboundAccountPosition" || event.e === "balanceUpdate" || event.e === "externalLockUpdate";
318318
+ }
318319
+ function isBinanceOrderUserDataEvent(event) {
318320
+ return event.e === "executionReport" || event.e === "listStatus";
318321
+ }
317230
318322
 
317231
- class OtelLogs extends BaseOtelSignal {
317232
- logger = null;
317233
- constructor(config) {
317234
- super(config, "logs");
318323
+ // src/helpers/user-data-stream-supervisor.ts
318324
+ var MAX_SUBSCRIBER_EVENTS = 16;
318325
+ function now3() {
318326
+ return new Date().toISOString();
318327
+ }
318328
+ function retryDelay(attempt) {
318329
+ return Math.min(1000 * 2 ** attempt, 30000);
318330
+ }
318331
+ function safeFailureReason(exchange, reason) {
318332
+ const secrets = [exchange.apiKey, exchange.secret].filter((value) => typeof value === "string" && value.length > 0);
318333
+ return redactSecretLiterals(reason, secrets).replace(/\s+/g, " ").trim().slice(0, 256);
318334
+ }
318335
+
318336
+ class Subscriber {
318337
+ kind;
318338
+ marketId;
318339
+ onClose;
318340
+ #queue = [];
318341
+ #waiters = [];
318342
+ #closed = false;
318343
+ #error = null;
318344
+ constructor(kind, marketId, onClose) {
318345
+ this.kind = kind;
318346
+ this.marketId = marketId;
318347
+ this.onClose = onClose;
317235
318348
  }
317236
- createProvider(endpoint, serviceName, appendSignalPath) {
317237
- const exporter = new import_exporter_logs_otlp_http.OTLPLogExporter({
317238
- url: appendOtlpPath(endpoint, "logs", appendSignalPath)
317239
- });
317240
- const processor = new import_sdk_logs.BatchLogRecordProcessor(exporter);
317241
- const resource = import_resources.resourceFromAttributes({
317242
- "service.name": serviceName
317243
- });
317244
- return new import_sdk_logs.LoggerProvider({
317245
- resource,
317246
- processors: [processor]
317247
- });
318349
+ push(message) {
318350
+ if (this.#closed || !this.#matches(message.event))
318351
+ return;
318352
+ const waiter = this.#waiters.shift();
318353
+ if (waiter) {
318354
+ waiter.resolve(message);
318355
+ return;
318356
+ }
318357
+ if (this.#queue.length >= MAX_SUBSCRIBER_EVENTS) {
318358
+ this.#fail(new Error("Configured account user-data subscriber fell behind"));
318359
+ return;
318360
+ }
318361
+ this.#queue.push(message);
317248
318362
  }
317249
- onProviderCreated(provider) {
317250
- import_api_logs3.logs.setGlobalLoggerProvider(provider);
317251
- this.logger = provider.getLogger("cex-broker-logs", "1.0.0");
318363
+ close() {
318364
+ if (this.#closed)
318365
+ return;
318366
+ this.#closed = true;
318367
+ this.#queue.length = 0;
318368
+ this.onClose();
318369
+ for (const waiter of this.#waiters.splice(0))
318370
+ waiter.resolve(null);
317252
318371
  }
317253
- shutdownProvider(provider) {
317254
- return provider.forceFlush().then(() => provider.shutdown());
318372
+ async* [Symbol.asyncIterator]() {
318373
+ while (true) {
318374
+ const event = await this.#next();
318375
+ if (!event)
318376
+ return;
318377
+ yield event;
318378
+ }
317255
318379
  }
317256
- onProviderClosed() {
317257
- this.logger = null;
318380
+ #matches(event) {
318381
+ if (this.kind === "balance")
318382
+ return isBinanceBalanceUserDataEvent(event);
318383
+ if (!isBinanceOrderUserDataEvent(event))
318384
+ return false;
318385
+ return !this.marketId || event.s === this.marketId;
317258
318386
  }
317259
- emit(record) {
317260
- if (!this.isOtelEnabled() || !this.logger) {
317261
- return;
317262
- }
317263
- this.logger.emit(record);
318387
+ #next() {
318388
+ const event = this.#queue.shift();
318389
+ if (event)
318390
+ return Promise.resolve(event);
318391
+ if (this.#error)
318392
+ return Promise.reject(this.#error);
318393
+ if (this.#closed)
318394
+ return Promise.resolve(null);
318395
+ return new Promise((resolve, reject) => this.#waiters.push({ resolve, reject }));
317264
318396
  }
317265
- }
317266
- function resolveOtlpEndpoint(signal, config) {
317267
- if (config?.otlpEndpoint) {
317268
- return {
317269
- endpoint: normalizeOtlpEndpoint(config.otlpEndpoint),
317270
- appendSignalPath: true
318397
+ #fail(error) {
318398
+ if (this.#closed)
318399
+ return;
318400
+ this.#closed = true;
318401
+ this.#error = error;
318402
+ this.#queue.length = 0;
318403
+ this.onClose();
318404
+ for (const waiter of this.#waiters.splice(0))
318405
+ waiter.reject(error);
318406
+ }
318407
+ }
318408
+
318409
+ class AccountWorker {
318410
+ exchangeName;
318411
+ account;
318412
+ onChange;
318413
+ #subscribers = new Set;
318414
+ #snapshot;
318415
+ #stopping = false;
318416
+ #stream = null;
318417
+ #retryTimer = null;
318418
+ #retryResolve = null;
318419
+ #run = null;
318420
+ #failureObserved = false;
318421
+ #attempts = 0;
318422
+ constructor(exchangeName, account, onChange) {
318423
+ this.exchangeName = exchangeName;
318424
+ this.account = account;
318425
+ this.onChange = onChange;
318426
+ const timestamp = now3();
318427
+ this.#snapshot = {
318428
+ exchange: exchangeName,
318429
+ accountSelector: account.label,
318430
+ accountRole: account.role,
318431
+ streamKind: "user_data",
318432
+ accountScope: "spot",
318433
+ registryStatus: "active",
318434
+ retiredAt: null,
318435
+ state: "connecting",
318436
+ stateChangedAt: timestamp,
318437
+ lastConnectedAt: null,
318438
+ lastAuthenticatedAt: null,
318439
+ lastReceivedAt: null,
318440
+ connectAttemptCount: "0",
318441
+ reconnectCount: "0",
318442
+ errorCount: "0",
318443
+ lastFailureKind: "none",
318444
+ lastFailureReason: "",
318445
+ trafficMode: "event_driven",
318446
+ sourceWatermark: null
317271
318447
  };
317272
318448
  }
317273
- const signalEndpoint = signal === "metrics" ? process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT : process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT;
317274
- if (signalEndpoint) {
317275
- return {
317276
- endpoint: signalEndpoint,
317277
- appendSignalPath: false
317278
- };
318449
+ start() {
318450
+ if (this.exchangeName !== "binance") {
318451
+ this.#fail("unsupported_connector", "Configured exchange has no user-data supervisor");
318452
+ return;
318453
+ }
318454
+ this.#run = this.#connectLoop();
317279
318455
  }
317280
- if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {
317281
- return {
317282
- endpoint: normalizeOtlpEndpoint(process.env.OTEL_EXPORTER_OTLP_ENDPOINT),
317283
- appendSignalPath: true
317284
- };
318456
+ subscribe(options) {
318457
+ if (this.#stopping)
318458
+ throw new Error("Configured account user-data supervisor is stopping");
318459
+ const subscriber = new Subscriber(options.kind, options.marketId, () => {
318460
+ this.#subscribers.delete(subscriber);
318461
+ });
318462
+ this.#subscribers.add(subscriber);
318463
+ return subscriber;
317285
318464
  }
317286
- if (config?.host) {
317287
- const protocol = config.protocol || "http";
317288
- const port = config.port ?? DEFAULT_OTLP_PORT;
317289
- return {
317290
- endpoint: `${protocol}://${config.host}:${port}`,
317291
- appendSignalPath: true
317292
- };
318465
+ snapshot() {
318466
+ return { ...this.#snapshot };
317293
318467
  }
317294
- return null;
317295
- }
317296
- function appendOtlpPath(endpoint, signal, appendSignalPath) {
317297
- if (!appendSignalPath) {
317298
- return endpoint;
318468
+ async stop() {
318469
+ this.#stopping = true;
318470
+ if (this.#retryTimer)
318471
+ clearTimeout(this.#retryTimer);
318472
+ this.#retryTimer = null;
318473
+ this.#retryResolve?.();
318474
+ this.#retryResolve = null;
318475
+ this.#stream?.close();
318476
+ await this.#run;
318477
+ this.#transition("disconnected", "shutdown", "Broker shutdown");
318478
+ for (const subscriber of [...this.#subscribers])
318479
+ subscriber.close();
318480
+ }
318481
+ #transition(state, failureKind, failureReason) {
318482
+ const timestamp = now3();
318483
+ if (this.#snapshot.state !== state) {
318484
+ this.#snapshot.state = state;
318485
+ this.#snapshot.stateChangedAt = timestamp;
318486
+ }
318487
+ if (failureKind) {
318488
+ this.#snapshot.lastFailureKind = failureKind;
318489
+ this.#snapshot.lastFailureReason = failureReason ?? "";
318490
+ }
318491
+ this.onChange();
318492
+ }
318493
+ #connected() {
318494
+ this.#snapshot.lastConnectedAt = now3();
318495
+ this.#transition("connected");
318496
+ }
318497
+ #authenticated() {
318498
+ this.#snapshot.lastAuthenticatedAt = now3();
318499
+ this.onChange();
318500
+ }
318501
+ #received(message) {
318502
+ this.#snapshot.lastReceivedAt = now3();
318503
+ const eventTimestamp = message.event.E;
318504
+ this.#snapshot.sourceWatermark = typeof eventTimestamp === "number" || typeof eventTimestamp === "string" ? String(eventTimestamp).slice(0, 512) : null;
318505
+ for (const subscriber of this.#subscribers)
318506
+ subscriber.push(message);
318507
+ this.onChange();
318508
+ }
318509
+ #fail(kind, reason) {
318510
+ this.#failureObserved = true;
318511
+ this.#snapshot.errorCount = (BigInt(this.#snapshot.errorCount) + 1n).toString();
318512
+ this.#transition("error", kind, safeFailureReason(this.account.exchange, reason));
318513
+ }
318514
+ async#connectLoop() {
318515
+ while (!this.#stopping) {
318516
+ if (this.#attempts > 0) {
318517
+ this.#snapshot.reconnectCount = (BigInt(this.#snapshot.reconnectCount) + 1n).toString();
318518
+ }
318519
+ this.#attempts += 1;
318520
+ this.#snapshot.connectAttemptCount = String(this.#attempts);
318521
+ this.#failureObserved = false;
318522
+ this.#transition("connecting");
318523
+ const stream4 = new BinanceSpotUserDataStream(this.account.exchange, {
318524
+ observer: {
318525
+ onConnected: () => this.#connected(),
318526
+ onAuthenticated: () => this.#authenticated(),
318527
+ onEvent: (message) => this.#received(message),
318528
+ onFailure: (failure) => this.#handleStreamFailure(failure)
318529
+ }
318530
+ });
318531
+ this.#stream = stream4;
318532
+ try {
318533
+ for await (const _event of stream4) {}
318534
+ } catch (error) {
318535
+ if (!this.#stopping && !this.#failureObserved) {
318536
+ this.#fail("transport_error", error instanceof Error ? error.message : "User-data stream failed");
318537
+ }
318538
+ } finally {
318539
+ stream4.close();
318540
+ if (this.#stream === stream4)
318541
+ this.#stream = null;
318542
+ }
318543
+ if (!this.#stopping)
318544
+ await this.#waitForRetry();
318545
+ }
317299
318546
  }
317300
- const baseEndpoint = normalizeOtlpEndpoint(endpoint);
317301
- return `${baseEndpoint}/v1/${signal}`;
317302
- }
317303
- function normalizeOtlpEndpoint(endpoint) {
317304
- return endpoint.replace(/\/v1\/(metrics|logs)\/?$/, "").replace(/\/+$/, "");
317305
- }
317306
- function getOtelHostFromEnv() {
317307
- return process.env.CEX_BROKER_OTEL_HOST ?? process.env.CEX_BROKER_CLICKHOUSE_HOST;
317308
- }
317309
- function getOtelPortFromEnv() {
317310
- const port = process.env.CEX_BROKER_OTEL_PORT ?? process.env.CEX_BROKER_CLICKHOUSE_PORT;
317311
- return port ? Number.parseInt(port, 10) : undefined;
317312
- }
317313
- function getOtelProtocolFromEnv() {
317314
- const protocol = process.env.CEX_BROKER_OTEL_PROTOCOL ?? process.env.CEX_BROKER_CLICKHOUSE_PROTOCOL;
317315
- return protocol || "http";
317316
- }
317317
- function createOtelMetricsFromEnv(options = {}) {
317318
- const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
317319
- const serviceName = process.env.OTEL_SERVICE_NAME || options.defaultServiceName || DEFAULT_SERVICE;
317320
- if (otlpEndpoint) {
317321
- return new OtelMetrics({
317322
- otlpEndpoint,
317323
- serviceName
317324
- });
318547
+ #handleStreamFailure(failure) {
318548
+ this.#fail(failure.kind, failure.reason);
317325
318549
  }
317326
- if (options.allowLegacyBrokerConfig === false) {
317327
- return new OtelMetrics({ serviceName });
318550
+ #waitForRetry() {
318551
+ return new Promise((resolve) => {
318552
+ const delay = retryDelay(Math.max(this.#attempts - 1, 0));
318553
+ this.#retryResolve = resolve;
318554
+ this.#retryTimer = setTimeout(() => {
318555
+ this.#retryTimer = null;
318556
+ this.#retryResolve = null;
318557
+ resolve();
318558
+ }, delay);
318559
+ this.#retryTimer.unref?.();
318560
+ });
317328
318561
  }
317329
- const host = getOtelHostFromEnv();
317330
- if (!host)
317331
- return new OtelMetrics({ serviceName });
317332
- const port = getOtelPortFromEnv();
317333
- const config = {
317334
- host,
317335
- port: port ?? DEFAULT_OTLP_PORT,
317336
- protocol: getOtelProtocolFromEnv(),
317337
- serviceName
317338
- };
317339
- return new OtelMetrics(config);
317340
318562
  }
317341
- function stableAttributeKey(attributes) {
317342
- return JSON.stringify(Object.entries(attributes).sort(([left], [right]) => left.localeCompare(right)));
317343
- }
317344
- function createOtelLogsFromEnv() {
317345
- const logsEndpoint = process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT;
317346
- const genericEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
317347
- const host = getOtelHostFromEnv();
317348
- if (logsEndpoint) {
317349
- return new OtelLogs({
317350
- serviceName: process.env.OTEL_SERVICE_NAME || DEFAULT_SERVICE
317351
- });
318563
+
318564
+ class UserDataStreamSupervisor {
318565
+ options;
318566
+ #workers = new Map;
318567
+ #started = false;
318568
+ constructor(options) {
318569
+ this.options = options;
318570
+ for (const [exchange, pool] of Object.entries(options.brokers)) {
318571
+ for (const account of [pool.primary, ...pool.secondaryBrokers]) {
318572
+ const normalizedExchange = exchange.trim().toLowerCase();
318573
+ const worker = new AccountWorker(normalizedExchange, account, () => this.#publish());
318574
+ this.#workers.set(`${normalizedExchange}|${account.label}`, worker);
318575
+ }
318576
+ }
318577
+ if (this.#workers.size === 0) {
318578
+ throw new Error("User-data supervisor requires at least one configured account");
318579
+ }
317352
318580
  }
317353
- if (genericEndpoint) {
317354
- return new OtelLogs({
317355
- otlpEndpoint: genericEndpoint,
317356
- serviceName: process.env.OTEL_SERVICE_NAME || DEFAULT_SERVICE
317357
- });
318581
+ start() {
318582
+ if (this.#started)
318583
+ return;
318584
+ this.#started = true;
318585
+ this.options.publisher.start();
318586
+ for (const worker of this.#workers.values())
318587
+ worker.start();
318588
+ this.#publish();
318589
+ }
318590
+ subscribe(options) {
318591
+ const exchange = options.exchange.trim().toLowerCase();
318592
+ const worker = this.#workers.get(`${exchange}|${options.accountSelector}`);
318593
+ if (!worker)
318594
+ throw new Error("Configured account user-data stream is unavailable");
318595
+ return worker.subscribe({ kind: options.kind, marketId: options.marketId });
317358
318596
  }
317359
- if (!host) {
317360
- return new OtelLogs;
318597
+ async close() {
318598
+ for (const worker of this.#workers.values())
318599
+ await worker.stop();
318600
+ await this.options.publisher.close(this.#snapshots());
318601
+ }
318602
+ #snapshots() {
318603
+ return [...this.#workers.values()].map((worker) => worker.snapshot());
318604
+ }
318605
+ #publish() {
318606
+ if (!this.#started)
318607
+ return;
318608
+ this.options.publisher.publish(this.#snapshots());
317361
318609
  }
317362
- const port = getOtelPortFromEnv();
317363
- const config = {
317364
- host,
317365
- port: port ?? DEFAULT_OTLP_PORT,
317366
- protocol: getOtelProtocolFromEnv(),
317367
- serviceName: process.env.OTEL_SERVICE_NAME || DEFAULT_SERVICE
317368
- };
317369
- return new OtelLogs(config);
317370
318610
  }
317371
318611
 
317372
318612
  // src/server.ts
@@ -318209,9 +319449,9 @@ function floatSafeRemainder(val, step) {
318209
319449
  return valInt % stepInt / 10 ** decCount;
318210
319450
  }
318211
319451
  var EVALUATING = Symbol("evaluating");
318212
- function defineLazy(object, key, getter) {
319452
+ function defineLazy(object, key2, getter) {
318213
319453
  let value = undefined;
318214
- Object.defineProperty(object, key, {
319454
+ Object.defineProperty(object, key2, {
318215
319455
  get() {
318216
319456
  if (value === EVALUATING) {
318217
319457
  return;
@@ -318223,7 +319463,7 @@ function defineLazy(object, key, getter) {
318223
319463
  return value;
318224
319464
  },
318225
319465
  set(v) {
318226
- Object.defineProperty(object, key, {
319466
+ Object.defineProperty(object, key2, {
318227
319467
  value: v
318228
319468
  });
318229
319469
  },
@@ -318255,11 +319495,11 @@ function cloneDef(schema) {
318255
319495
  function getElementAtPath(obj, path) {
318256
319496
  if (!path)
318257
319497
  return obj;
318258
- return path.reduce((acc, key) => acc?.[key], obj);
319498
+ return path.reduce((acc, key2) => acc?.[key2], obj);
318259
319499
  }
318260
319500
  function promiseAllObject(promisesObj) {
318261
319501
  const keys2 = Object.keys(promisesObj);
318262
- const promises = keys2.map((key) => promisesObj[key]);
319502
+ const promises = keys2.map((key2) => promisesObj[key2]);
318263
319503
  return Promise.all(promises).then((results) => {
318264
319504
  const resolvedObj = {};
318265
319505
  for (let i2 = 0;i2 < keys2.length; i2++) {
@@ -318323,8 +319563,8 @@ function shallowClone(o) {
318323
319563
  }
318324
319564
  function numKeys(data) {
318325
319565
  let keyCount = 0;
318326
- for (const key in data) {
318327
- if (Object.prototype.hasOwnProperty.call(data, key)) {
319566
+ for (const key2 in data) {
319567
+ if (Object.prototype.hasOwnProperty.call(data, key2)) {
318328
319568
  keyCount++;
318329
319569
  }
318330
319570
  }
@@ -318467,13 +319707,13 @@ function pick(schema, mask2) {
318467
319707
  const def = mergeDefs(schema._zod.def, {
318468
319708
  get shape() {
318469
319709
  const newShape = {};
318470
- for (const key in mask2) {
318471
- if (!(key in currDef.shape)) {
318472
- throw new Error(`Unrecognized key: "${key}"`);
319710
+ for (const key2 in mask2) {
319711
+ if (!(key2 in currDef.shape)) {
319712
+ throw new Error(`Unrecognized key: "${key2}"`);
318473
319713
  }
318474
- if (!mask2[key])
319714
+ if (!mask2[key2])
318475
319715
  continue;
318476
- newShape[key] = currDef.shape[key];
319716
+ newShape[key2] = currDef.shape[key2];
318477
319717
  }
318478
319718
  assignProp(this, "shape", newShape);
318479
319719
  return newShape;
@@ -318492,13 +319732,13 @@ function omit5(schema, mask2) {
318492
319732
  const def = mergeDefs(schema._zod.def, {
318493
319733
  get shape() {
318494
319734
  const newShape = { ...schema._zod.def.shape };
318495
- for (const key in mask2) {
318496
- if (!(key in currDef.shape)) {
318497
- throw new Error(`Unrecognized key: "${key}"`);
319735
+ for (const key2 in mask2) {
319736
+ if (!(key2 in currDef.shape)) {
319737
+ throw new Error(`Unrecognized key: "${key2}"`);
318498
319738
  }
318499
- if (!mask2[key])
319739
+ if (!mask2[key2])
318500
319740
  continue;
318501
- delete newShape[key];
319741
+ delete newShape[key2];
318502
319742
  }
318503
319743
  assignProp(this, "shape", newShape);
318504
319744
  return newShape;
@@ -318515,8 +319755,8 @@ function extend4(schema, shape) {
318515
319755
  const hasChecks = checks && checks.length > 0;
318516
319756
  if (hasChecks) {
318517
319757
  const existingShape = schema._zod.def.shape;
318518
- for (const key in shape) {
318519
- if (Object.getOwnPropertyDescriptor(existingShape, key) !== undefined) {
319758
+ for (const key2 in shape) {
319759
+ if (Object.getOwnPropertyDescriptor(existingShape, key2) !== undefined) {
318520
319760
  throw new Error("Cannot overwrite keys on object schemas containing refinements. Use `.safeExtend()` instead.");
318521
319761
  }
318522
319762
  }
@@ -318569,23 +319809,23 @@ function partial(Class, schema, mask2) {
318569
319809
  const oldShape = schema._zod.def.shape;
318570
319810
  const shape = { ...oldShape };
318571
319811
  if (mask2) {
318572
- for (const key in mask2) {
318573
- if (!(key in oldShape)) {
318574
- throw new Error(`Unrecognized key: "${key}"`);
319812
+ for (const key2 in mask2) {
319813
+ if (!(key2 in oldShape)) {
319814
+ throw new Error(`Unrecognized key: "${key2}"`);
318575
319815
  }
318576
- if (!mask2[key])
319816
+ if (!mask2[key2])
318577
319817
  continue;
318578
- shape[key] = Class ? new Class({
319818
+ shape[key2] = Class ? new Class({
318579
319819
  type: "optional",
318580
- innerType: oldShape[key]
318581
- }) : oldShape[key];
319820
+ innerType: oldShape[key2]
319821
+ }) : oldShape[key2];
318582
319822
  }
318583
319823
  } else {
318584
- for (const key in oldShape) {
318585
- shape[key] = Class ? new Class({
319824
+ for (const key2 in oldShape) {
319825
+ shape[key2] = Class ? new Class({
318586
319826
  type: "optional",
318587
- innerType: oldShape[key]
318588
- }) : oldShape[key];
319827
+ innerType: oldShape[key2]
319828
+ }) : oldShape[key2];
318589
319829
  }
318590
319830
  }
318591
319831
  assignProp(this, "shape", shape);
@@ -318601,22 +319841,22 @@ function required(Class, schema, mask2) {
318601
319841
  const oldShape = schema._zod.def.shape;
318602
319842
  const shape = { ...oldShape };
318603
319843
  if (mask2) {
318604
- for (const key in mask2) {
318605
- if (!(key in shape)) {
318606
- throw new Error(`Unrecognized key: "${key}"`);
319844
+ for (const key2 in mask2) {
319845
+ if (!(key2 in shape)) {
319846
+ throw new Error(`Unrecognized key: "${key2}"`);
318607
319847
  }
318608
- if (!mask2[key])
319848
+ if (!mask2[key2])
318609
319849
  continue;
318610
- shape[key] = new Class({
319850
+ shape[key2] = new Class({
318611
319851
  type: "nonoptional",
318612
- innerType: oldShape[key]
319852
+ innerType: oldShape[key2]
318613
319853
  });
318614
319854
  }
318615
319855
  } else {
318616
- for (const key in oldShape) {
318617
- shape[key] = new Class({
319856
+ for (const key2 in oldShape) {
319857
+ shape[key2] = new Class({
318618
319858
  type: "nonoptional",
318619
- innerType: oldShape[key]
319859
+ innerType: oldShape[key2]
318620
319860
  });
318621
319861
  }
318622
319862
  }
@@ -320366,19 +321606,19 @@ var $ZodArray = /* @__PURE__ */ $constructor("$ZodArray", (inst, def) => {
320366
321606
  return payload;
320367
321607
  };
320368
321608
  });
320369
- function handlePropertyResult(result, final, key, input, isOptionalOut) {
321609
+ function handlePropertyResult(result, final, key2, input, isOptionalOut) {
320370
321610
  if (result.issues.length) {
320371
- if (isOptionalOut && !(key in input)) {
321611
+ if (isOptionalOut && !(key2 in input)) {
320372
321612
  return;
320373
321613
  }
320374
- final.issues.push(...prefixIssues(key, result.issues));
321614
+ final.issues.push(...prefixIssues(key2, result.issues));
320375
321615
  }
320376
321616
  if (result.value === undefined) {
320377
- if (key in input) {
320378
- final.value[key] = undefined;
321617
+ if (key2 in input) {
321618
+ final.value[key2] = undefined;
320379
321619
  }
320380
321620
  } else {
320381
- final.value[key] = result.value;
321621
+ final.value[key2] = result.value;
320382
321622
  }
320383
321623
  }
320384
321624
  function normalizeDef(def) {
@@ -320403,18 +321643,18 @@ function handleCatchall(proms, input, payload, ctx, def, inst) {
320403
321643
  const _catchall = def.catchall._zod;
320404
321644
  const t = _catchall.def.type;
320405
321645
  const isOptionalOut = _catchall.optout === "optional";
320406
- for (const key in input) {
320407
- if (keySet.has(key))
321646
+ for (const key2 in input) {
321647
+ if (keySet.has(key2))
320408
321648
  continue;
320409
321649
  if (t === "never") {
320410
- unrecognized.push(key);
321650
+ unrecognized.push(key2);
320411
321651
  continue;
320412
321652
  }
320413
- const r = _catchall.run({ value: input[key], issues: [] }, ctx);
321653
+ const r = _catchall.run({ value: input[key2], issues: [] }, ctx);
320414
321654
  if (r instanceof Promise) {
320415
- proms.push(r.then((r2) => handlePropertyResult(r2, payload, key, input, isOptionalOut)));
321655
+ proms.push(r.then((r2) => handlePropertyResult(r2, payload, key2, input, isOptionalOut)));
320416
321656
  } else {
320417
- handlePropertyResult(r, payload, key, input, isOptionalOut);
321657
+ handlePropertyResult(r, payload, key2, input, isOptionalOut);
320418
321658
  }
320419
321659
  }
320420
321660
  if (unrecognized.length) {
@@ -320450,12 +321690,12 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
320450
321690
  defineLazy(inst._zod, "propValues", () => {
320451
321691
  const shape = def.shape;
320452
321692
  const propValues = {};
320453
- for (const key in shape) {
320454
- const field = shape[key]._zod;
321693
+ for (const key2 in shape) {
321694
+ const field = shape[key2]._zod;
320455
321695
  if (field.values) {
320456
- propValues[key] ?? (propValues[key] = new Set);
321696
+ propValues[key2] ?? (propValues[key2] = new Set);
320457
321697
  for (const v of field.values)
320458
- propValues[key].add(v);
321698
+ propValues[key2].add(v);
320459
321699
  }
320460
321700
  }
320461
321701
  return propValues;
@@ -320478,14 +321718,14 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
320478
321718
  payload.value = {};
320479
321719
  const proms = [];
320480
321720
  const shape = value.shape;
320481
- for (const key of value.keys) {
320482
- const el = shape[key];
321721
+ for (const key2 of value.keys) {
321722
+ const el = shape[key2];
320483
321723
  const isOptionalOut = el._zod.optout === "optional";
320484
- const r = el._zod.run({ value: input[key], issues: [] }, ctx);
321724
+ const r = el._zod.run({ value: input[key2], issues: [] }, ctx);
320485
321725
  if (r instanceof Promise) {
320486
- proms.push(r.then((r2) => handlePropertyResult(r2, payload, key, input, isOptionalOut)));
321726
+ proms.push(r.then((r2) => handlePropertyResult(r2, payload, key2, input, isOptionalOut)));
320487
321727
  } else {
320488
- handlePropertyResult(r, payload, key, input, isOptionalOut);
321728
+ handlePropertyResult(r, payload, key2, input, isOptionalOut);
320489
321729
  }
320490
321730
  }
320491
321731
  if (!catchall) {
@@ -320501,23 +321741,23 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
320501
321741
  const generateFastpass = (shape) => {
320502
321742
  const doc = new Doc(["shape", "payload", "ctx"]);
320503
321743
  const normalized = _normalized.value;
320504
- const parseStr = (key) => {
320505
- const k = esc(key);
321744
+ const parseStr = (key2) => {
321745
+ const k = esc(key2);
320506
321746
  return `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`;
320507
321747
  };
320508
321748
  doc.write(`const input = payload.value;`);
320509
321749
  const ids = Object.create(null);
320510
- let counter = 0;
320511
- for (const key of normalized.keys) {
320512
- ids[key] = `key_${counter++}`;
321750
+ let counter2 = 0;
321751
+ for (const key2 of normalized.keys) {
321752
+ ids[key2] = `key_${counter2++}`;
320513
321753
  }
320514
321754
  doc.write(`const newResult = {};`);
320515
- for (const key of normalized.keys) {
320516
- const id2 = ids[key];
320517
- const k = esc(key);
320518
- const schema = shape[key];
321755
+ for (const key2 of normalized.keys) {
321756
+ const id2 = ids[key2];
321757
+ const k = esc(key2);
321758
+ const schema = shape[key2];
320519
321759
  const isOptionalOut = schema?._zod?.optout === "optional";
320520
- doc.write(`const ${id2} = ${parseStr(key)};`);
321760
+ doc.write(`const ${id2} = ${parseStr(key2)};`);
320521
321761
  if (isOptionalOut) {
320522
321762
  doc.write(`
320523
321763
  if (${id2}.issues.length) {
@@ -320803,17 +322043,17 @@ function mergeValues(a, b2) {
320803
322043
  }
320804
322044
  if (isPlainObject2(a) && isPlainObject2(b2)) {
320805
322045
  const bKeys = Object.keys(b2);
320806
- const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
322046
+ const sharedKeys = Object.keys(a).filter((key2) => bKeys.indexOf(key2) !== -1);
320807
322047
  const newObj = { ...a, ...b2 };
320808
- for (const key of sharedKeys) {
320809
- const sharedValue = mergeValues(a[key], b2[key]);
322048
+ for (const key2 of sharedKeys) {
322049
+ const sharedValue = mergeValues(a[key2], b2[key2]);
320810
322050
  if (!sharedValue.valid) {
320811
322051
  return {
320812
322052
  valid: false,
320813
- mergeErrorPath: [key, ...sharedValue.mergeErrorPath]
322053
+ mergeErrorPath: [key2, ...sharedValue.mergeErrorPath]
320814
322054
  };
320815
322055
  }
320816
- newObj[key] = sharedValue.data;
322056
+ newObj[key2] = sharedValue.data;
320817
322057
  }
320818
322058
  return { valid: true, data: newObj };
320819
322059
  }
@@ -320969,30 +322209,30 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
320969
322209
  if (values2) {
320970
322210
  payload.value = {};
320971
322211
  const recordKeys = new Set;
320972
- for (const key of values2) {
320973
- if (typeof key === "string" || typeof key === "number" || typeof key === "symbol") {
320974
- recordKeys.add(typeof key === "number" ? key.toString() : key);
320975
- const result = def.valueType._zod.run({ value: input[key], issues: [] }, ctx);
322212
+ for (const key2 of values2) {
322213
+ if (typeof key2 === "string" || typeof key2 === "number" || typeof key2 === "symbol") {
322214
+ recordKeys.add(typeof key2 === "number" ? key2.toString() : key2);
322215
+ const result = def.valueType._zod.run({ value: input[key2], issues: [] }, ctx);
320976
322216
  if (result instanceof Promise) {
320977
322217
  proms.push(result.then((result2) => {
320978
322218
  if (result2.issues.length) {
320979
- payload.issues.push(...prefixIssues(key, result2.issues));
322219
+ payload.issues.push(...prefixIssues(key2, result2.issues));
320980
322220
  }
320981
- payload.value[key] = result2.value;
322221
+ payload.value[key2] = result2.value;
320982
322222
  }));
320983
322223
  } else {
320984
322224
  if (result.issues.length) {
320985
- payload.issues.push(...prefixIssues(key, result.issues));
322225
+ payload.issues.push(...prefixIssues(key2, result.issues));
320986
322226
  }
320987
- payload.value[key] = result.value;
322227
+ payload.value[key2] = result.value;
320988
322228
  }
320989
322229
  }
320990
322230
  }
320991
322231
  let unrecognized;
320992
- for (const key in input) {
320993
- if (!recordKeys.has(key)) {
322232
+ for (const key2 in input) {
322233
+ if (!recordKeys.has(key2)) {
320994
322234
  unrecognized = unrecognized ?? [];
320995
- unrecognized.push(key);
322235
+ unrecognized.push(key2);
320996
322236
  }
320997
322237
  }
320998
322238
  if (unrecognized && unrecognized.length > 0) {
@@ -321005,16 +322245,16 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
321005
322245
  }
321006
322246
  } else {
321007
322247
  payload.value = {};
321008
- for (const key of Reflect.ownKeys(input)) {
321009
- if (key === "__proto__")
322248
+ for (const key2 of Reflect.ownKeys(input)) {
322249
+ if (key2 === "__proto__")
321010
322250
  continue;
321011
- let keyResult = def.keyType._zod.run({ value: key, issues: [] }, ctx);
322251
+ let keyResult = def.keyType._zod.run({ value: key2, issues: [] }, ctx);
321012
322252
  if (keyResult instanceof Promise) {
321013
322253
  throw new Error("Async schemas not supported in object keys currently");
321014
322254
  }
321015
- const checkNumericKey = typeof key === "string" && number3.test(key) && keyResult.issues.length;
322255
+ const checkNumericKey = typeof key2 === "string" && number3.test(key2) && keyResult.issues.length;
321016
322256
  if (checkNumericKey) {
321017
- const retryResult = def.keyType._zod.run({ value: Number(key), issues: [] }, ctx);
322257
+ const retryResult = def.keyType._zod.run({ value: Number(key2), issues: [] }, ctx);
321018
322258
  if (retryResult instanceof Promise) {
321019
322259
  throw new Error("Async schemas not supported in object keys currently");
321020
322260
  }
@@ -321024,30 +322264,30 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
321024
322264
  }
321025
322265
  if (keyResult.issues.length) {
321026
322266
  if (def.mode === "loose") {
321027
- payload.value[key] = input[key];
322267
+ payload.value[key2] = input[key2];
321028
322268
  } else {
321029
322269
  payload.issues.push({
321030
322270
  code: "invalid_key",
321031
322271
  origin: "record",
321032
322272
  issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),
321033
- input: key,
321034
- path: [key],
322273
+ input: key2,
322274
+ path: [key2],
321035
322275
  inst
321036
322276
  });
321037
322277
  }
321038
322278
  continue;
321039
322279
  }
321040
- const result = def.valueType._zod.run({ value: input[key], issues: [] }, ctx);
322280
+ const result = def.valueType._zod.run({ value: input[key2], issues: [] }, ctx);
321041
322281
  if (result instanceof Promise) {
321042
322282
  proms.push(result.then((result2) => {
321043
322283
  if (result2.issues.length) {
321044
- payload.issues.push(...prefixIssues(key, result2.issues));
322284
+ payload.issues.push(...prefixIssues(key2, result2.issues));
321045
322285
  }
321046
322286
  payload.value[keyResult.value] = result2.value;
321047
322287
  }));
321048
322288
  } else {
321049
322289
  if (result.issues.length) {
321050
- payload.issues.push(...prefixIssues(key, result.issues));
322290
+ payload.issues.push(...prefixIssues(key2, result.issues));
321051
322291
  }
321052
322292
  payload.value[keyResult.value] = result.value;
321053
322293
  }
@@ -321074,15 +322314,15 @@ var $ZodMap = /* @__PURE__ */ $constructor("$ZodMap", (inst, def) => {
321074
322314
  }
321075
322315
  const proms = [];
321076
322316
  payload.value = new Map;
321077
- for (const [key, value] of input) {
321078
- const keyResult = def.keyType._zod.run({ value: key, issues: [] }, ctx);
322317
+ for (const [key2, value] of input) {
322318
+ const keyResult = def.keyType._zod.run({ value: key2, issues: [] }, ctx);
321079
322319
  const valueResult = def.valueType._zod.run({ value, issues: [] }, ctx);
321080
322320
  if (keyResult instanceof Promise || valueResult instanceof Promise) {
321081
322321
  proms.push(Promise.all([keyResult, valueResult]).then(([keyResult2, valueResult2]) => {
321082
- handleMapResult(keyResult2, valueResult2, payload, key, input, inst, ctx);
322322
+ handleMapResult(keyResult2, valueResult2, payload, key2, input, inst, ctx);
321083
322323
  }));
321084
322324
  } else {
321085
- handleMapResult(keyResult, valueResult, payload, key, input, inst, ctx);
322325
+ handleMapResult(keyResult, valueResult, payload, key2, input, inst, ctx);
321086
322326
  }
321087
322327
  }
321088
322328
  if (proms.length)
@@ -321090,10 +322330,10 @@ var $ZodMap = /* @__PURE__ */ $constructor("$ZodMap", (inst, def) => {
321090
322330
  return payload;
321091
322331
  };
321092
322332
  });
321093
- function handleMapResult(keyResult, valueResult, final, key, input, inst, ctx) {
322333
+ function handleMapResult(keyResult, valueResult, final, key2, input, inst, ctx) {
321094
322334
  if (keyResult.issues.length) {
321095
- if (propertyKeyTypes.has(typeof key)) {
321096
- final.issues.push(...prefixIssues(key, keyResult.issues));
322335
+ if (propertyKeyTypes.has(typeof key2)) {
322336
+ final.issues.push(...prefixIssues(key2, keyResult.issues));
321097
322337
  } else {
321098
322338
  final.issues.push({
321099
322339
  code: "invalid_key",
@@ -321105,15 +322345,15 @@ function handleMapResult(keyResult, valueResult, final, key, input, inst, ctx) {
321105
322345
  }
321106
322346
  }
321107
322347
  if (valueResult.issues.length) {
321108
- if (propertyKeyTypes.has(typeof key)) {
321109
- final.issues.push(...prefixIssues(key, valueResult.issues));
322348
+ if (propertyKeyTypes.has(typeof key2)) {
322349
+ final.issues.push(...prefixIssues(key2, valueResult.issues));
321110
322350
  } else {
321111
322351
  final.issues.push({
321112
322352
  origin: "map",
321113
322353
  code: "invalid_element",
321114
322354
  input,
321115
322355
  inst,
321116
- key,
322356
+ key: key2,
321117
322357
  issues: valueResult.issues.map((iss) => finalizeIssue(iss, ctx, config()))
321118
322358
  });
321119
322359
  }
@@ -321443,12 +322683,12 @@ var $ZodPipe = /* @__PURE__ */ $constructor("$ZodPipe", (inst, def) => {
321443
322683
  return handlePipeResult(left, def.out, ctx);
321444
322684
  };
321445
322685
  });
321446
- function handlePipeResult(left, next, ctx) {
322686
+ function handlePipeResult(left, next2, ctx) {
321447
322687
  if (left.issues.length) {
321448
322688
  left.aborted = true;
321449
322689
  return left;
321450
322690
  }
321451
- return next._zod.run({ value: left.value, issues: left.issues }, ctx);
322691
+ return next2._zod.run({ value: left.value, issues: left.issues }, ctx);
321452
322692
  }
321453
322693
  var $ZodCodec = /* @__PURE__ */ $constructor("$ZodCodec", (inst, def) => {
321454
322694
  $ZodType.init(inst, def);
@@ -328307,8 +329547,8 @@ function extractDefs(ctx, schema) {
328307
329547
  if (defId)
328308
329548
  seen.defId = defId;
328309
329549
  const schema2 = seen.schema;
328310
- for (const key in schema2) {
328311
- delete schema2[key];
329550
+ for (const key2 in schema2) {
329551
+ delete schema2[key2];
328312
329552
  }
328313
329553
  schema2.$ref = ref;
328314
329554
  };
@@ -328375,20 +329615,20 @@ function finalize(ctx, schema) {
328375
329615
  Object.assign(schema2, _cached);
328376
329616
  const isParentRef = zodSchema._zod.parent === ref;
328377
329617
  if (isParentRef) {
328378
- for (const key in schema2) {
328379
- if (key === "$ref" || key === "allOf")
329618
+ for (const key2 in schema2) {
329619
+ if (key2 === "$ref" || key2 === "allOf")
328380
329620
  continue;
328381
- if (!(key in _cached)) {
328382
- delete schema2[key];
329621
+ if (!(key2 in _cached)) {
329622
+ delete schema2[key2];
328383
329623
  }
328384
329624
  }
328385
329625
  }
328386
329626
  if (refSchema.$ref && refSeen.def) {
328387
- for (const key in schema2) {
328388
- if (key === "$ref" || key === "allOf")
329627
+ for (const key2 in schema2) {
329628
+ if (key2 === "$ref" || key2 === "allOf")
328389
329629
  continue;
328390
- if (key in refSeen.def && JSON.stringify(schema2[key]) === JSON.stringify(refSeen.def[key])) {
328391
- delete schema2[key];
329630
+ if (key2 in refSeen.def && JSON.stringify(schema2[key2]) === JSON.stringify(refSeen.def[key2])) {
329631
+ delete schema2[key2];
328392
329632
  }
328393
329633
  }
328394
329634
  }
@@ -328400,11 +329640,11 @@ function finalize(ctx, schema) {
328400
329640
  if (parentSeen?.schema.$ref) {
328401
329641
  schema2.$ref = parentSeen.schema.$ref;
328402
329642
  if (parentSeen.def) {
328403
- for (const key in schema2) {
328404
- if (key === "$ref" || key === "allOf")
329643
+ for (const key2 in schema2) {
329644
+ if (key2 === "$ref" || key2 === "allOf")
328405
329645
  continue;
328406
- if (key in parentSeen.def && JSON.stringify(schema2[key]) === JSON.stringify(parentSeen.def[key])) {
328407
- delete schema2[key];
329646
+ if (key2 in parentSeen.def && JSON.stringify(schema2[key2]) === JSON.stringify(parentSeen.def[key2])) {
329647
+ delete schema2[key2];
328408
329648
  }
328409
329649
  }
328410
329650
  }
@@ -328495,8 +329735,8 @@ function isTransforming(_schema, _ctx) {
328495
329735
  return isTransforming(def.in, ctx) || isTransforming(def.out, ctx);
328496
329736
  }
328497
329737
  if (def.type === "object") {
328498
- for (const key in def.shape) {
328499
- if (isTransforming(def.shape[key], ctx))
329738
+ for (const key2 in def.shape) {
329739
+ if (isTransforming(def.shape[key2], ctx))
328500
329740
  return true;
328501
329741
  }
328502
329742
  return false;
@@ -328787,15 +330027,15 @@ var objectProcessor = (schema, ctx, _json, params) => {
328787
330027
  json3.type = "object";
328788
330028
  json3.properties = {};
328789
330029
  const shape = def.shape;
328790
- for (const key in shape) {
328791
- json3.properties[key] = process2(shape[key], ctx, {
330030
+ for (const key2 in shape) {
330031
+ json3.properties[key2] = process2(shape[key2], ctx, {
328792
330032
  ...params,
328793
- path: [...params.path, "properties", key]
330033
+ path: [...params.path, "properties", key2]
328794
330034
  });
328795
330035
  }
328796
330036
  const allKeys = new Set(Object.keys(shape));
328797
- const requiredKeys = new Set([...allKeys].filter((key) => {
328798
- const v = def.shape[key]._zod;
330037
+ const requiredKeys = new Set([...allKeys].filter((key2) => {
330038
+ const v = def.shape[key2]._zod;
328799
330039
  if (ctx.io === "input") {
328800
330040
  return v.optin === undefined;
328801
330041
  } else {
@@ -329060,9 +330300,9 @@ function toJSONSchema(input, params) {
329060
330300
  };
329061
330301
  ctx2.external = external;
329062
330302
  for (const entry of registry2._idmap.entries()) {
329063
- const [key, schema] = entry;
330303
+ const [key2, schema] = entry;
329064
330304
  extractDefs(ctx2, schema);
329065
- schemas[key] = finalize(ctx2, schema);
330305
+ schemas[key2] = finalize(ctx2, schema);
329066
330306
  }
329067
330307
  if (Object.keys(defs).length > 0) {
329068
330308
  const defsSegment = ctx2.target === "draft-2020-12" ? "$defs" : "definitions";
@@ -330619,11 +331859,11 @@ function resolveRef(ref, ctx) {
330619
331859
  }
330620
331860
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
330621
331861
  if (path[0] === defsKey) {
330622
- const key = path[1];
330623
- if (!key || !ctx.defs[key]) {
331862
+ const key2 = path[1];
331863
+ if (!key2 || !ctx.defs[key2]) {
330624
331864
  throw new Error(`Reference not found: ${ref}`);
330625
331865
  }
330626
- return ctx.defs[key];
331866
+ return ctx.defs[key2];
330627
331867
  }
330628
331868
  throw new Error(`Reference not found: ${ref}`);
330629
331869
  }
@@ -330809,9 +332049,9 @@ function convertBaseSchema(schema, ctx) {
330809
332049
  const shape = {};
330810
332050
  const properties = schema.properties || {};
330811
332051
  const requiredSet = new Set(schema.required || []);
330812
- for (const [key, propSchema] of Object.entries(properties)) {
332052
+ for (const [key2, propSchema] of Object.entries(properties)) {
330813
332053
  const propZodSchema = convertSchema(propSchema, ctx);
330814
- shape[key] = requiredSet.has(key) ? propZodSchema : propZodSchema.optional();
332054
+ shape[key2] = requiredSet.has(key2) ? propZodSchema : propZodSchema.optional();
330815
332055
  }
330816
332056
  if (schema.propertyNames) {
330817
332057
  const keySchema = convertSchema(schema.propertyNames, ctx);
@@ -330955,20 +332195,20 @@ function convertSchema(schema, ctx) {
330955
332195
  }
330956
332196
  const extraMeta = {};
330957
332197
  const coreMetadataKeys = ["$id", "id", "$comment", "$anchor", "$vocabulary", "$dynamicRef", "$dynamicAnchor"];
330958
- for (const key of coreMetadataKeys) {
330959
- if (key in schema) {
330960
- extraMeta[key] = schema[key];
332198
+ for (const key2 of coreMetadataKeys) {
332199
+ if (key2 in schema) {
332200
+ extraMeta[key2] = schema[key2];
330961
332201
  }
330962
332202
  }
330963
332203
  const contentMetadataKeys = ["contentEncoding", "contentMediaType", "contentSchema"];
330964
- for (const key of contentMetadataKeys) {
330965
- if (key in schema) {
330966
- extraMeta[key] = schema[key];
332204
+ for (const key2 of contentMetadataKeys) {
332205
+ if (key2 in schema) {
332206
+ extraMeta[key2] = schema[key2];
330967
332207
  }
330968
332208
  }
330969
- for (const key of Object.keys(schema)) {
330970
- if (!RECOGNIZED_KEYS.has(key)) {
330971
- extraMeta[key] = schema[key];
332209
+ for (const key2 of Object.keys(schema)) {
332210
+ if (!RECOGNIZED_KEYS.has(key2)) {
332211
+ extraMeta[key2] = schema[key2];
330972
332212
  }
330973
332213
  }
330974
332214
  if (Object.keys(extraMeta).length > 0) {
@@ -331333,7 +332573,7 @@ async function handleDeposit(ctx) {
331333
332573
  network: depositNetwork?.exchangeNetworkId,
331334
332574
  externalId: depositTxid,
331335
332575
  txid: depositTxid,
331336
- exchangeTimestamp: typeof creditedAt === "string" ? creditedAt : undefined,
332576
+ exchangeTimestamp: normalizeTimestamp2(creditedAt),
331337
332577
  payload: deposit
331338
332578
  }
331339
332579
  });
@@ -331554,320 +332794,118 @@ function parseOrderBookCallPayload(payload, request) {
331554
332794
  }
331555
332795
  if (parsedStart.timestamp >= parsedEnd.timestamp) {
331556
332796
  return {
331557
- kind: "error",
331558
- message: "ValidationError: start must be before end"
331559
- };
331560
- }
331561
- const parsedCadence = parseCadence(payloadValue(payload, "cadence"));
331562
- if (!parsedCadence.ok) {
331563
- return { kind: "error", message: parsedCadence.message };
331564
- }
331565
- return {
331566
- kind: "order_book",
331567
- payload: {
331568
- ...parsed,
331569
- start: parsedStart.value,
331570
- end: parsedEnd.value,
331571
- cadence: parsedCadence.value
331572
- }
331573
- };
331574
- }
331575
- function parseOptionalDepthLimit(value) {
331576
- const parsed = parsePositiveInteger(nonEmptyString(value), "depthLimit");
331577
- return parsed.ok ? parsed.value : undefined;
331578
- }
331579
- function scalarByAlias(payload, aliases) {
331580
- for (const alias of aliases) {
331581
- const value = payload[alias];
331582
- if (isScalar(value)) {
331583
- return value;
331584
- }
331585
- }
331586
- return;
331587
- }
331588
- function normalizeSide(payload, side, depthLimit) {
331589
- const rawLevels = payload[side];
331590
- if (!Array.isArray(rawLevels)) {
331591
- throw new Error(`Malformed order book: ${side} must be an array`);
331592
- }
331593
- return rawLevels.slice(0, depthLimit).map((level, index2) => {
331594
- if (!Array.isArray(level) || level.length < 2) {
331595
- throw new Error(`Malformed order book: ${side}[${index2}] must be [price, amount]`);
331596
- }
331597
- const price = Number(level[0]);
331598
- const amount = Number(level[1]);
331599
- if (!Number.isFinite(price) || !Number.isFinite(amount)) {
331600
- throw new Error(`Malformed order book: ${side}[${index2}] must be numeric`);
331601
- }
331602
- return [price, amount];
331603
- });
331604
- }
331605
- function normalizeOrderBookSnapshot(orderBook, options) {
331606
- if (!isRecord(orderBook)) {
331607
- throw new Error("Malformed order book: expected object");
331608
- }
331609
- const receivedTimestamp = options.receivedTimestamp ?? Date.now();
331610
- const timestamp = scalarByAlias(orderBook, ["timestamp"]) ?? receivedTimestamp;
331611
- const sequence = scalarByAlias(orderBook, [
331612
- "sequence",
331613
- "updateId",
331614
- "lastUpdateId",
331615
- "nonce"
331616
- ]);
331617
- const normalized = {
331618
- bids: normalizeSide(orderBook, "bids", options.depthLimit),
331619
- asks: normalizeSide(orderBook, "asks", options.depthLimit),
331620
- timestamp,
331621
- receivedTimestamp,
331622
- exchange: options.exchange,
331623
- symbol: options.symbol,
331624
- depthLimit: options.depthLimit
331625
- };
331626
- if (sequence !== undefined) {
331627
- normalized.sequence = sequence;
331628
- }
331629
- return normalized;
331630
- }
331631
- function supportsBrokerMethod(broker, method) {
331632
- const fn = broker[method];
331633
- const hasValue = broker.has?.[method];
331634
- return typeof fn === "function" && hasValue !== false;
331635
- }
331636
- function buildOrderBookCapability(broker, payload) {
331637
- return {
331638
- exchange: payload.exchange,
331639
- symbol: payload.symbol,
331640
- provider: "ccxt_order_book",
331641
- maxDepth: payload.depthLimit,
331642
- timestampPrecision: "milliseconds",
331643
- constructionMode: payload.constructionMode,
331644
- supportsCurrentSnapshot: supportsBrokerMethod(broker, "fetchOrderBook"),
331645
- supportsLiveStream: supportsBrokerMethod(broker, "watchOrderBook"),
331646
- supportsHistoricalSnapshots: false,
331647
- supportsSampledTopN: false,
331648
- supportsExactL2Reconstruction: false
331649
- };
331650
- }
331651
- function buildHistoricalOrderBookUnsupported(payload) {
331652
- return {
331653
- exchange: payload.exchange,
331654
- symbol: payload.symbol,
331655
- provider: "ccxt_order_book",
331656
- constructionMode: payload.constructionMode,
331657
- depthLimit: payload.depthLimit,
331658
- start: payload.start,
331659
- end: payload.end,
331660
- cadence: payload.cadence,
331661
- unsupported: true,
331662
- unsupportedReason: HISTORICAL_ORDER_BOOK_PROVIDER_UNSUPPORTED
331663
- };
331664
- }
331665
-
331666
- // src/handlers/execute-action/order-book-call.ts
331667
- var grpc4 = __toESM(require_src3(), 1);
331668
-
331669
- // src/helpers/market-data-archive/capture-contract.ts
331670
- import { createHash as createHash3 } from "node:crypto";
331671
- var MARKET_CAPTURE_SCHEMA_VERSION = "1.0.0";
331672
- var CHECKSUM_ALGORITHM = "sha256-canonical-json-v1";
331673
- var ARCHIVE_SOURCES = ["broker_read", "broker_write"];
331674
- var CAPTURE_FEEDS = [
331675
- "ORDERBOOK",
331676
- "TICKER",
331677
- "TRADES",
331678
- "OHLCV"
331679
- ];
331680
- var SOURCE_MODES = [
331681
- "broker_live_stream_v1",
331682
- "broker_live_sampling_v1",
331683
- "broker_current_snapshot_v1",
331684
- "broker_bootstrap_fetch_v1",
331685
- "external_ccxt_fallback_v1",
331686
- "external_hummingbot_fallback_v1",
331687
- "legacy_migration_v1"
331688
- ];
331689
- var RAW_CAPTURE_SCOPES = [
331690
- "ccxt_normalized_object",
331691
- "broker_visible_payload",
331692
- "exchange_wire_frame"
331693
- ];
331694
- var CHECKSUM_FIELDS = new Set([
331695
- "normalized_row_checksum",
331696
- "raw_checksum",
331697
- "checksum"
331698
- ]);
331699
- function canonicalDecimal(value) {
331700
- if (!Number.isFinite(value)) {
331701
- throw new Error("Canonical numbers must be finite");
331702
- }
331703
- if (Object.is(value, -0)) {
331704
- return "0";
331705
- }
331706
- const rendered = String(value).toLowerCase();
331707
- if (!rendered.includes("e")) {
331708
- return rendered;
331709
- }
331710
- const [coefficient = "0", exponentText = "0"] = rendered.split("e");
331711
- const exponent = Number.parseInt(exponentText, 10);
331712
- const negative = coefficient.startsWith("-");
331713
- const unsigned = negative ? coefficient.slice(1) : coefficient;
331714
- const [integer2 = "0", fraction = ""] = unsigned.split(".");
331715
- const digits = `${integer2}${fraction}`;
331716
- const decimalIndex = integer2.length + exponent;
331717
- let result;
331718
- if (decimalIndex <= 0) {
331719
- result = `0.${"0".repeat(-decimalIndex)}${digits}`;
331720
- } else if (decimalIndex >= digits.length) {
331721
- result = `${digits}${"0".repeat(decimalIndex - digits.length)}`;
331722
- } else {
331723
- result = `${digits.slice(0, decimalIndex)}.${digits.slice(decimalIndex)}`;
331724
- }
331725
- return negative ? `-${result}` : result;
331726
- }
331727
- function serializeCanonical(value, stack) {
331728
- if (value === null)
331729
- return "null";
331730
- if (typeof value === "string")
331731
- return JSON.stringify(value);
331732
- if (typeof value === "boolean")
331733
- return value ? "true" : "false";
331734
- if (typeof value === "number")
331735
- return canonicalDecimal(value);
331736
- if (typeof value === "bigint")
331737
- return value.toString(10);
331738
- if (value instanceof Date) {
331739
- if (Number.isNaN(value.getTime())) {
331740
- throw new Error("Canonical timestamps must be valid");
331741
- }
331742
- return value.getTime().toString(10);
331743
- }
331744
- if (Array.isArray(value)) {
331745
- if (stack.has(value))
331746
- throw new Error("Canonical values must be acyclic");
331747
- stack.add(value);
331748
- const result = `[${value.map((entry) => entry === undefined ? "null" : serializeCanonical(entry, stack)).join(",")}]`;
331749
- stack.delete(value);
331750
- return result;
331751
- }
331752
- if (typeof value === "object") {
331753
- if (stack.has(value))
331754
- throw new Error("Canonical values must be acyclic");
331755
- stack.add(value);
331756
- const entries = Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => left.localeCompare(right));
331757
- const result = `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${serializeCanonical(entry, stack)}`).join(",")}}`;
331758
- stack.delete(value);
331759
- return result;
332797
+ kind: "error",
332798
+ message: "ValidationError: start must be before end"
332799
+ };
331760
332800
  }
331761
- throw new Error(`Unsupported canonical value type: ${typeof value}`);
331762
- }
331763
- function canonicalSerialize(value) {
331764
- return serializeCanonical(value, new Set);
331765
- }
331766
- function omitChecksumFields(value) {
331767
- if (Array.isArray(value))
331768
- return value.map(omitChecksumFields);
331769
- if (value && typeof value === "object" && !(value instanceof Date)) {
331770
- return Object.fromEntries(Object.entries(value).filter(([key]) => !CHECKSUM_FIELDS.has(key)).map(([key, entry]) => [key, omitChecksumFields(entry)]));
332801
+ const parsedCadence = parseCadence(payloadValue(payload, "cadence"));
332802
+ if (!parsedCadence.ok) {
332803
+ return { kind: "error", message: parsedCadence.message };
331771
332804
  }
331772
- return value;
332805
+ return {
332806
+ kind: "order_book",
332807
+ payload: {
332808
+ ...parsed,
332809
+ start: parsedStart.value,
332810
+ end: parsedEnd.value,
332811
+ cadence: parsedCadence.value
332812
+ }
332813
+ };
331773
332814
  }
331774
- function sha256Canonical(value) {
331775
- return createHash3("sha256").update(canonicalSerialize(omitChecksumFields(value))).digest("hex");
332815
+ function parseOptionalDepthLimit(value) {
332816
+ const parsed = parsePositiveInteger(nonEmptyString(value), "depthLimit");
332817
+ return parsed.ok ? parsed.value : undefined;
331776
332818
  }
331777
- function normalizeTimestampMs(value, field) {
331778
- let timestamp;
331779
- if (value instanceof Date) {
331780
- timestamp = value.getTime();
331781
- } else if (typeof value === "number") {
331782
- timestamp = value;
331783
- } else if (typeof value === "string" && /^\d+$/.test(value.trim())) {
331784
- timestamp = Number(value.trim());
331785
- } else if (typeof value === "string") {
331786
- timestamp = Date.parse(value);
331787
- } else {
331788
- timestamp = Number.NaN;
331789
- }
331790
- if (!Number.isSafeInteger(timestamp) || timestamp < 0) {
331791
- throw new Error(`${field} must be a non-negative millisecond timestamp`);
332819
+ function scalarByAlias(payload, aliases) {
332820
+ for (const alias of aliases) {
332821
+ const value = payload[alias];
332822
+ if (isScalar(value)) {
332823
+ return value;
332824
+ }
331792
332825
  }
331793
- return timestamp;
332826
+ return;
331794
332827
  }
331795
- function assertCaptureContext(context2) {
331796
- if (!ARCHIVE_SOURCES.includes(context2.source)) {
331797
- throw new Error(`Unsupported archive source: ${context2.source}`);
331798
- }
331799
- if (!CAPTURE_FEEDS.includes(context2.feed)) {
331800
- throw new Error(`Unsupported capture feed: ${context2.feed}`);
332828
+ function normalizeSide(payload, side, depthLimit) {
332829
+ const rawLevels = payload[side];
332830
+ if (!Array.isArray(rawLevels)) {
332831
+ throw new Error(`Malformed order book: ${side} must be an array`);
331801
332832
  }
331802
- if (!SOURCE_MODES.includes(context2.sourceMode)) {
331803
- throw new Error(`Unsupported source mode: ${context2.sourceMode}`);
332833
+ return rawLevels.slice(0, depthLimit).map((level, index2) => {
332834
+ if (!Array.isArray(level) || level.length < 2) {
332835
+ throw new Error(`Malformed order book: ${side}[${index2}] must be [price, amount]`);
332836
+ }
332837
+ const price = Number(level[0]);
332838
+ const amount = Number(level[1]);
332839
+ if (!Number.isFinite(price) || !Number.isFinite(amount)) {
332840
+ throw new Error(`Malformed order book: ${side}[${index2}] must be numeric`);
332841
+ }
332842
+ return [price, amount];
332843
+ });
332844
+ }
332845
+ function normalizeOrderBookSnapshot(orderBook, options) {
332846
+ if (!isRecord(orderBook)) {
332847
+ throw new Error("Malformed order book: expected object");
331804
332848
  }
331805
- for (const [field, value] of [
331806
- ["deployment_id", context2.deploymentId],
331807
- ["capture_bundle_id", context2.captureBundleId],
331808
- ["exchange", context2.exchange],
331809
- ["symbol", context2.symbol],
331810
- ["provider", context2.provider]
331811
- ]) {
331812
- if (!value.trim())
331813
- throw new Error(`${field} must not be empty`);
332849
+ const receivedTimestamp = options.receivedTimestamp ?? Date.now();
332850
+ const timestamp = scalarByAlias(orderBook, ["timestamp"]) ?? receivedTimestamp;
332851
+ const sequence = scalarByAlias(orderBook, [
332852
+ "sequence",
332853
+ "updateId",
332854
+ "lastUpdateId",
332855
+ "nonce"
332856
+ ]);
332857
+ const normalized = {
332858
+ bids: normalizeSide(orderBook, "bids", options.depthLimit),
332859
+ asks: normalizeSide(orderBook, "asks", options.depthLimit),
332860
+ timestamp,
332861
+ receivedTimestamp,
332862
+ exchange: options.exchange,
332863
+ symbol: options.symbol,
332864
+ depthLimit: options.depthLimit
332865
+ };
332866
+ if (sequence !== undefined) {
332867
+ normalized.sequence = sequence;
331814
332868
  }
332869
+ return normalized;
331815
332870
  }
331816
- function createRawCapture(context2, input) {
331817
- assertCaptureContext(context2);
331818
- if (!RAW_CAPTURE_SCOPES.includes(input.scope)) {
331819
- throw new Error(`Unsupported raw capture scope: ${input.scope}`);
331820
- }
331821
- const eventTimeMs = normalizeTimestampMs(input.eventTimeMs, "event_time_ms");
331822
- const receivedTimeMs = normalizeTimestampMs(input.receivedTimeMs, "received_time_ms");
331823
- const redactedPayload = redactStreamPayload(input.payload);
331824
- const rawChecksum = sha256Canonical(redactedPayload);
331825
- const rawCaptureId = sha256Canonical({
331826
- capture_bundle_id: context2.captureBundleId,
331827
- exchange: context2.exchange.trim().toLowerCase(),
331828
- feed: context2.feed,
331829
- raw_capture_scope: input.scope,
331830
- raw_payload_sha256: rawChecksum,
331831
- schema_version: context2.schemaVersion,
331832
- source_mode: context2.sourceMode,
331833
- source_symbol: context2.symbol.trim(),
331834
- source_time_ms: eventTimeMs
331835
- });
332871
+ function supportsBrokerMethod(broker, method) {
332872
+ const fn = broker[method];
332873
+ const hasValue = broker.has?.[method];
332874
+ return typeof fn === "function" && hasValue !== false;
332875
+ }
332876
+ function buildOrderBookCapability(broker, payload) {
331836
332877
  return {
331837
- rawCaptureId,
331838
- rawCaptureScope: input.scope,
331839
- rawChecksum,
331840
- redactedPayload,
331841
- eventTimeMs,
331842
- receivedTimeMs,
331843
- checksumAlgorithm: context2.checksumAlgorithm
332878
+ exchange: payload.exchange,
332879
+ symbol: payload.symbol,
332880
+ provider: "ccxt_order_book",
332881
+ maxDepth: payload.depthLimit,
332882
+ timestampPrecision: "milliseconds",
332883
+ constructionMode: payload.constructionMode,
332884
+ supportsCurrentSnapshot: supportsBrokerMethod(broker, "fetchOrderBook"),
332885
+ supportsLiveStream: supportsBrokerMethod(broker, "watchOrderBook"),
332886
+ supportsHistoricalSnapshots: false,
332887
+ supportsSampledTopN: false,
332888
+ supportsExactL2Reconstruction: false
331844
332889
  };
331845
332890
  }
331846
- function captureCoreFields(context2, rawCapture) {
331847
- assertCaptureContext(context2);
332891
+ function buildHistoricalOrderBookUnsupported(payload) {
331848
332892
  return {
331849
- source: context2.source,
331850
- deployment_id: context2.deploymentId,
331851
- capture_bundle_id: context2.captureBundleId,
331852
- exchange: context2.exchange.trim().toLowerCase(),
331853
- symbol: context2.symbol.trim(),
331854
- trading_pair: context2.symbol.trim().replace("/", "-"),
331855
- source_symbol: context2.symbol.trim(),
331856
- asset_type: context2.assetType,
331857
- feed: context2.feed,
331858
- provider: context2.provider,
331859
- source_mode: context2.sourceMode,
331860
- source_time_ms: rawCapture.eventTimeMs,
331861
- received_time_ms: rawCapture.receivedTimeMs,
331862
- raw_capture_id: rawCapture.rawCaptureId,
331863
- raw_capture_scope: rawCapture.rawCaptureScope,
331864
- schema_version: context2.schemaVersion,
331865
- checksum_algorithm: context2.checksumAlgorithm,
331866
- raw_checksum: rawCapture.rawChecksum,
331867
- provenance_complete: context2.provenanceComplete ? 1 : 0
332893
+ exchange: payload.exchange,
332894
+ symbol: payload.symbol,
332895
+ provider: "ccxt_order_book",
332896
+ constructionMode: payload.constructionMode,
332897
+ depthLimit: payload.depthLimit,
332898
+ start: payload.start,
332899
+ end: payload.end,
332900
+ cadence: payload.cadence,
332901
+ unsupported: true,
332902
+ unsupportedReason: HISTORICAL_ORDER_BOOK_PROVIDER_UNSUPPORTED
331868
332903
  };
331869
332904
  }
331870
332905
 
332906
+ // src/handlers/execute-action/order-book-call.ts
332907
+ var grpc4 = __toESM(require_src3(), 1);
332908
+
331871
332909
  // src/helpers/market-data-archive/canonical-orderbook.ts
331872
332910
  class OrderBookValidationError extends Error {
331873
332911
  reason;
@@ -332035,46 +333073,6 @@ function buildCanonicalOrderBookRows(input) {
332035
333073
  };
332036
333074
  }
332037
333075
 
332038
- // src/helpers/market-data-archive/capture-context.ts
332039
- function createMarketCaptureContext(input) {
332040
- const environment = input.environment ?? "development";
332041
- const deploymentId = input.deploymentId.trim();
332042
- if (!deploymentId)
332043
- throw new Error("deployment_id must not be empty");
332044
- const configuredBundle = input.captureBundleId?.trim();
332045
- if (environment === "production" && !configuredBundle) {
332046
- throw new Error("capture_bundle_id is required for production market capture");
332047
- }
332048
- const exchange = input.exchange.trim().toLowerCase();
332049
- const symbol2 = input.symbol.trim();
332050
- if (!exchange || !symbol2) {
332051
- throw new Error("exchange and symbol are required for market capture");
332052
- }
332053
- return {
332054
- source: input.source,
332055
- deploymentId,
332056
- captureBundleId: configuredBundle ?? `development:${deploymentId}`,
332057
- exchange,
332058
- symbol: symbol2,
332059
- assetType: input.assetType,
332060
- feed: input.feed,
332061
- provider: input.provider?.trim() || `ccxt:${exchange}`,
332062
- sourceMode: input.sourceMode,
332063
- schemaVersion: MARKET_CAPTURE_SCHEMA_VERSION,
332064
- checksumAlgorithm: CHECKSUM_ALGORITHM,
332065
- provenanceComplete: true,
332066
- timeframe: input.timeframe,
332067
- accountSelector: input.accountSelector
332068
- };
332069
- }
332070
- function captureEnvironmentFromEnv(value = process.env.CEX_BROKER_MARKET_CAPTURE_ENVIRONMENT) {
332071
- const environment = value?.trim() || "development";
332072
- if (environment !== "development" && environment !== "production") {
332073
- throw new Error("CEX_BROKER_MARKET_CAPTURE_ENVIRONMENT must be development or production");
332074
- }
332075
- return environment;
332076
- }
332077
-
332078
333076
  // src/helpers/market-data-archive/ohlcv-bar-tracker.ts
332079
333077
  function isFiniteNumber(value) {
332080
333078
  return typeof value === "number" && Number.isFinite(value);
@@ -332199,47 +333197,14 @@ var DEFAULT_ORDERBOOK_ARCHIVE_DEPTH_LIMIT = 25;
332199
333197
  var MAX_ORDERBOOK_ARCHIVE_DEPTH_LIMIT = 500;
332200
333198
  function getOrderbookArchiveDepthLimit() {
332201
333199
  const raw = process.env.CEX_BROKER_ORDERBOOK_ARCHIVE_DEPTH_LIMIT;
332202
- if (!raw) {
332203
- return DEFAULT_ORDERBOOK_ARCHIVE_DEPTH_LIMIT;
332204
- }
332205
- const parsed = Number.parseInt(raw, 10);
332206
- if (!Number.isFinite(parsed) || parsed <= 0) {
332207
- return DEFAULT_ORDERBOOK_ARCHIVE_DEPTH_LIMIT;
332208
- }
332209
- return Math.min(parsed, MAX_ORDERBOOK_ARCHIVE_DEPTH_LIMIT);
332210
- }
332211
-
332212
- // src/helpers/market-data-archive/orderbook-sampler.ts
332213
- var DEFAULT_ORDERBOOK_INTERVAL_MS = 1000;
332214
- function getOrderbookIntervalMs() {
332215
- const raw = process.env.CEX_BROKER_ORDERBOOK_INTERVAL_MS ?? process.env.CEX_BROKER_ORDERBOOK_TOB_INTERVAL_MS;
332216
- if (!raw) {
332217
- return DEFAULT_ORDERBOOK_INTERVAL_MS;
332218
- }
332219
- const parsed = Number.parseInt(raw, 10);
332220
- return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_ORDERBOOK_INTERVAL_MS;
332221
- }
332222
- function isMarketArchiveEnabled() {
332223
- return process.env.CEX_BROKER_MARKET_ARCHIVE_ENABLED !== "false";
332224
- }
332225
-
332226
- class OrderbookSampler {
332227
- intervalMs;
332228
- lastEmitMs = null;
332229
- constructor(intervalMs = getOrderbookIntervalMs()) {
332230
- this.intervalMs = intervalMs;
333200
+ if (!raw) {
333201
+ return DEFAULT_ORDERBOOK_ARCHIVE_DEPTH_LIMIT;
332231
333202
  }
332232
- shouldEmit(nowMs = Date.now()) {
332233
- if (this.lastEmitMs !== null && nowMs < this.lastEmitMs) {
332234
- this.lastEmitMs = nowMs;
332235
- return true;
332236
- }
332237
- if (this.lastEmitMs !== null && nowMs - this.lastEmitMs < this.intervalMs) {
332238
- return false;
332239
- }
332240
- this.lastEmitMs = nowMs;
332241
- return true;
333203
+ const parsed = Number.parseInt(raw, 10);
333204
+ if (!Number.isFinite(parsed) || parsed <= 0) {
333205
+ return DEFAULT_ORDERBOOK_ARCHIVE_DEPTH_LIMIT;
332242
333206
  }
333207
+ return Math.min(parsed, MAX_ORDERBOOK_ARCHIVE_DEPTH_LIMIT);
332243
333208
  }
332244
333209
 
332245
333210
  // src/helpers/market-data-archive/parse-stream.ts
@@ -332336,10 +333301,10 @@ function parseTicker(value, fallbackMs) {
332336
333301
  ["change", record2.change],
332337
333302
  ["percentage", record2.percentage]
332338
333303
  ];
332339
- for (const [key, rawValue] of fields) {
333304
+ for (const [key2, rawValue] of fields) {
332340
333305
  const numeric = toNumber2(rawValue);
332341
333306
  if (numeric !== undefined) {
332342
- parsed[key] = numeric;
333307
+ parsed[key2] = numeric;
332343
333308
  }
332344
333309
  }
332345
333310
  return parsed;
@@ -332356,9 +333321,19 @@ function withNormalizedChecksum(record2) {
332356
333321
  normalized_row_checksum: sha256Canonical(compact)
332357
333322
  };
332358
333323
  }
333324
+ function legacyMarketFields(context2, rawCapture) {
333325
+ return {
333326
+ account_selector: context2.accountSelector,
333327
+ broker_observed_timestamp: new Date(rawCapture.receivedTimeMs).toISOString()
333328
+ };
333329
+ }
333330
+ function legacyDecimal8(value) {
333331
+ return value === undefined ? undefined : Number(value.toFixed(8));
333332
+ }
332359
333333
  function buildCanonicalCexStreamEventRow(context2, rawCapture) {
332360
333334
  const row = withNormalizedChecksum({
332361
333335
  ...captureCoreFields(context2, rawCapture),
333336
+ ...legacyMarketFields(context2, rawCapture),
332362
333337
  stream_type: context2.feed,
332363
333338
  event_time_ms: rawCapture.eventTimeMs,
332364
333339
  payload_encoding: "canonical_json_v1",
@@ -332372,19 +333347,21 @@ function buildCanonicalTickerEventRow(context2, rawCapture, ticker) {
332372
333347
  }
332373
333348
  const row = withNormalizedChecksum({
332374
333349
  ...captureCoreFields(context2, rawCapture),
333350
+ ...legacyMarketFields(context2, rawCapture),
332375
333351
  source_time_ms: ticker.eventTimeMs,
332376
333352
  event_time_ms: ticker.eventTimeMs,
332377
- last: ticker.last,
332378
- bid: ticker.bid,
332379
- ask: ticker.ask,
332380
- high: ticker.high,
332381
- low: ticker.low,
332382
- open: ticker.open,
332383
- close: ticker.close,
332384
- base_volume: ticker.baseVolume,
332385
- quote_volume: ticker.quoteVolume,
332386
- change: ticker.change,
332387
- percentage: ticker.percentage
333353
+ last: legacyDecimal8(ticker.last),
333354
+ bid: legacyDecimal8(ticker.bid),
333355
+ ask: legacyDecimal8(ticker.ask),
333356
+ high: legacyDecimal8(ticker.high),
333357
+ low: legacyDecimal8(ticker.low),
333358
+ open: legacyDecimal8(ticker.open),
333359
+ close: legacyDecimal8(ticker.close),
333360
+ base_volume: legacyDecimal8(ticker.baseVolume),
333361
+ quote_volume: legacyDecimal8(ticker.quoteVolume),
333362
+ change: legacyDecimal8(ticker.change),
333363
+ percentage: legacyDecimal8(ticker.percentage),
333364
+ payload_json: JSON.stringify(rawCapture.redactedPayload)
332388
333365
  });
332389
333366
  return { table: "market_data.cex_ticker_events", row };
332390
333367
  }
@@ -332394,13 +333371,14 @@ function buildCanonicalTradeRow(context2, rawCapture, trade) {
332394
333371
  }
332395
333372
  const row = withNormalizedChecksum({
332396
333373
  ...captureCoreFields(context2, rawCapture),
333374
+ ...legacyMarketFields(context2, rawCapture),
332397
333375
  source_time_ms: trade.eventTimeMs,
332398
333376
  trade_id: trade.tradeId,
332399
333377
  event_time_ms: trade.eventTimeMs,
332400
333378
  side: trade.side,
332401
- price: trade.price,
332402
- amount: trade.amount,
332403
- cost: trade.cost,
333379
+ price: legacyDecimal8(trade.price),
333380
+ amount: legacyDecimal8(trade.amount),
333381
+ cost: legacyDecimal8(trade.cost),
332404
333382
  taker_or_maker: trade.takerOrMaker
332405
333383
  });
332406
333384
  return { table: "market_data.cex_trades", row };
@@ -332480,10 +333458,19 @@ function resolveCaptureContext(archiver, input, feed, sourceMode) {
332480
333458
  environment: captureEnvironmentFromEnv()
332481
333459
  });
332482
333460
  }
333461
+ function canArchiveMarketData(archiver) {
333462
+ return resolveMarketCaptureArchiveState({
333463
+ archiveEnabled: archiver?.isEnabled() ?? false,
333464
+ marketArchiveEnabled: isMarketArchiveEnabled(),
333465
+ environment: process.env.CEX_BROKER_MARKET_CAPTURE_ENVIRONMENT,
333466
+ deploymentId: archiver?.getDeploymentId(),
333467
+ captureBundleId: process.env.CEX_BROKER_CAPTURE_BUNDLE_ID
333468
+ }).enabled;
333469
+ }
332483
333470
  function archiveOrderbookInBackground(archiver, otelMetrics, input, options) {
332484
333471
  const labels = watchLabels("orderbook", input, archiver, "ORDERBOOK");
332485
333472
  recordWatchMetric(otelMetrics, "cex_watch_frames_received_total", labels);
332486
- if (!isMarketArchiveEnabled() || !archiver?.isEnabled()) {
333473
+ if (!canArchiveMarketData(archiver)) {
332487
333474
  return;
332488
333475
  }
332489
333476
  if (options?.sampledOut) {
@@ -332525,7 +333512,7 @@ function archiveOrderbookInBackground(archiver, otelMetrics, input, options) {
332525
333512
  function archiveOhlcvInBackground(archiver, otelMetrics, tracker, input) {
332526
333513
  const labels = watchLabels("ohlcv", input, archiver, "OHLCV");
332527
333514
  recordWatchMetric(otelMetrics, "cex_watch_frames_received_total", labels);
332528
- if (!isMarketArchiveEnabled() || !archiver?.isEnabled()) {
333515
+ if (!canArchiveMarketData(archiver)) {
332529
333516
  return;
332530
333517
  }
332531
333518
  queueMicrotask(() => {
@@ -332570,7 +333557,7 @@ function createOhlcvBarTracker() {
332570
333557
  function archiveMarketRowsInBackground(archiver, otelMetrics, stream4, input, feed, enqueueRows) {
332571
333558
  const labels = watchLabels(stream4, input, archiver, feed);
332572
333559
  recordWatchMetric(otelMetrics, "cex_watch_frames_received_total", labels);
332573
- if (!isMarketArchiveEnabled() || !archiver?.isEnabled()) {
333560
+ if (!canArchiveMarketData(archiver)) {
332574
333561
  return;
332575
333562
  }
332576
333563
  queueMicrotask(() => {
@@ -333997,383 +334984,173 @@ function createExecuteActionHandler(deps) {
333997
334984
  }, null);
333998
334985
  }
333999
334986
  const normalizedCex = cex3.trim().toLowerCase();
334000
- const metadata = call.metadata;
334001
- const selectedBrokerAccount = selectBrokerAccountForCex(normalizedCex, brokers, metadata);
334002
- const broker = selectedBrokerAccount?.exchange ?? createBroker(normalizedCex, call.metadata) ?? (isPublicMarketDataAction(action, call.request.payload) ? createPublicBroker(normalizedCex) : null);
334003
- if (!broker) {
334004
- return wrappedCallback({
334005
- code: grpc12.status.UNAUTHENTICATED,
334006
- message: `This Exchange is not registered and No API metadata was found`
334007
- }, null);
334008
- }
334009
- const verity = { proof: "" };
334010
- const applyVerityToBroker = (targetBroker) => {
334011
- if (!useVerity)
334012
- return;
334013
- const override = buildHttpClientOverrideFromMetadata(metadata, verityProverUrl, (proof, notaryPubKey) => {
334014
- verity.proof = proof;
334015
- log.debug(`Verity proof:`, { proof, notaryPubKey });
334016
- });
334017
- targetBroker.setHttpClientOverride(override, verityHttpClientOverridePredicate);
334018
- };
334019
- const preludeCtx = {
334020
- call,
334021
- wrappedCallback,
334022
- action,
334023
- policy,
334024
- brokers,
334025
- metadata,
334026
- normalizedCex,
334027
- cex: cex3,
334028
- symbol: symbol2,
334029
- selectedBrokerAccount,
334030
- broker,
334031
- verity,
334032
- applyVerityToBroker,
334033
- useVerity,
334034
- verityProverUrl,
334035
- otelMetrics,
334036
- brokerArchiver,
334037
- orderActivityTracker,
334038
- withdrawalObservationTracker
334039
- };
334040
- if (action === Action.Call) {
334041
- const handled = await handleOrderBookCall(preludeCtx);
334042
- if (handled)
334043
- return;
334044
- }
334045
- applyVerityToBroker(broker);
334046
- const ctx = { ...preludeCtx, broker };
334047
- await dispatchExecuteAction(ctx);
334048
- } catch (error48) {
334049
- safeLogError("ExecuteAction unhandled error", error48);
334050
- return wrappedCallback({
334051
- code: grpc12.status.INTERNAL,
334052
- message: "ExecuteAction failed unexpectedly"
334053
- }, null);
334054
- }
334055
- };
334056
- }
334057
- // src/handlers/subscribe/broker-lifecycle.ts
334058
- class SubscribeBrokerLifecycle {
334059
- #brokers = new Map;
334060
- #closing = new Map;
334061
- #shuttingDown = false;
334062
- register(broker, context2) {
334063
- this.#brokers.set(broker, context2);
334064
- if (this.#shuttingDown) {
334065
- this.close(broker);
334066
- }
334067
- }
334068
- close(broker) {
334069
- const existing = this.#closing.get(broker);
334070
- if (existing) {
334071
- return existing;
334072
- }
334073
- const context2 = this.#brokers.get(broker) ?? {
334074
- cex: "unknown",
334075
- symbol: "unknown"
334076
- };
334077
- this.#brokers.delete(broker);
334078
- const closing = (async () => {
334079
- try {
334080
- await broker.close();
334081
- log.debug("Request-scoped Subscribe broker closed", context2);
334082
- return "closed";
334083
- } catch (error48) {
334084
- log.warn("Failed to close request-scoped Subscribe broker", {
334085
- ...context2,
334086
- error: error48
334087
- });
334088
- return "failed";
334089
- } finally {
334090
- this.#closing.delete(broker);
334091
- }
334092
- })();
334093
- this.#closing.set(broker, closing);
334094
- return closing;
334095
- }
334096
- async closeAll() {
334097
- this.#shuttingDown = true;
334098
- let failed = 0;
334099
- while (this.#brokers.size > 0 || this.#closing.size > 0) {
334100
- const inFlight = [...this.#closing.values()];
334101
- const fresh = [...this.#brokers.keys()].map((broker) => this.close(broker));
334102
- const outcomes = await Promise.all([...fresh, ...inFlight]);
334103
- failed += outcomes.filter((outcome) => outcome === "failed").length;
334104
- }
334105
- if (failed > 0) {
334106
- throw new Error(`${failed} request-scoped Subscribe broker(s) failed to close`);
334107
- }
334108
- }
334109
- }
334110
- // src/handlers/subscribe/handler.ts
334111
- var grpc13 = __toESM(require_src3(), 1);
334112
-
334113
- // src/helpers/binance-user-data-stream.ts
334114
- import { Buffer as Buffer2 } from "node:buffer";
334115
- import { createHmac } from "node:crypto";
334116
- var BINANCE_SPOT_WS_API_URL = "wss://ws-api.binance.com:443/ws-api/v3";
334117
- var DEFAULT_BINANCE_USER_DATA_MAX_BUFFERED_EVENTS = 16;
334118
- var createWebSocket = (url3) => new wrapper_default(url3);
334119
- var userDataRequestCounter = 0;
334120
- function getExchangeString(exchange, key) {
334121
- const value = exchange[key];
334122
- if (typeof value !== "string" || value.length === 0) {
334123
- throw new Error(`Binance user-data stream requires exchange.${key}`);
334124
- }
334125
- return value;
334126
- }
334127
- function sortedQuery(params) {
334128
- return Object.entries(params).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`).join("&");
334129
- }
334130
- function signUserDataStreamParams(exchange, params) {
334131
- const signParams = exchange.signParams;
334132
- if (typeof signParams === "function") {
334133
- return signParams.call(exchange, params);
334134
- }
334135
- const secret = getExchangeString(exchange, "secret");
334136
- return {
334137
- ...params,
334138
- signature: createHmac("sha256", secret).update(sortedQuery(params)).digest("hex")
334139
- };
334140
- }
334141
- function getBinanceSpotWsApiUrl(exchange) {
334142
- const urls = exchange.urls;
334143
- return urls?.api?.ws?.["ws-api"]?.spot ?? BINANCE_SPOT_WS_API_URL;
334144
- }
334145
- function getRecord(value) {
334146
- return typeof value === "object" && value !== null ? value : null;
334147
- }
334148
- function getMessage(value) {
334149
- if (value instanceof Error) {
334150
- return value.message;
334151
- }
334152
- if (typeof value === "string" && value.length > 0) {
334153
- return value;
334154
- }
334155
- const record2 = getRecord(value);
334156
- const message = record2?.message;
334157
- return typeof message === "string" && message.length > 0 ? message : null;
334158
- }
334159
- function getOptionalExchangeString(exchange, key) {
334160
- const value = exchange[key];
334161
- return typeof value === "string" && value.length > 0 ? value : null;
334162
- }
334163
- function redactDiagnosticMessage(message, secretValues) {
334164
- let redacted = message;
334165
- for (const value of secretValues) {
334166
- if (value.length > 0) {
334167
- redacted = redacted.split(value).join("[redacted]");
334168
- }
334169
- }
334170
- return redacted.replace(/(\b(?:apiKey|secret|signature)\b\s*=\s*)[^\s&,;)]+/gi, "$1[redacted]").replace(/("(?:apiKey|secret|signature)"\s*:\s*")[^"]*(")/gi, "$1[redacted]$2");
334171
- }
334172
- function formatBinanceUserDataWebSocketError(event, secretValues) {
334173
- const record2 = getRecord(event);
334174
- const message = getMessage(record2?.error) ?? getMessage(record2?.message) ?? getMessage(event);
334175
- const safeMessage = message === null ? null : redactDiagnosticMessage(message, secretValues);
334176
- return new Error(safeMessage ? `Binance user-data WebSocket error: ${safeMessage}` : "Binance user-data WebSocket error");
334177
- }
334178
- function getCloseReason(value) {
334179
- if (typeof value === "string") {
334180
- return value.length > 0 ? value : null;
334181
- }
334182
- if (Buffer2.isBuffer(value)) {
334183
- const reason = value.toString("utf8");
334184
- return reason.length > 0 ? reason : null;
334185
- }
334186
- if (value instanceof Uint8Array) {
334187
- const reason = Buffer2.from(value).toString("utf8");
334188
- return reason.length > 0 ? reason : null;
334189
- }
334190
- return null;
334191
- }
334192
- function formatBinanceUserDataWebSocketClose(codeOrEvent, reasonOrUndefined, secretValues) {
334193
- const record2 = getRecord(codeOrEvent);
334194
- const code = record2 ? record2.code : codeOrEvent;
334195
- const reason = getCloseReason(record2 ? record2.reason : reasonOrUndefined);
334196
- const safeReason = reason === null ? null : redactDiagnosticMessage(reason, secretValues);
334197
- const details = [
334198
- typeof code === "number" || typeof code === "string" ? `code=${code}` : null,
334199
- safeReason ? `reason=${safeReason}` : null
334200
- ].filter((detail) => detail !== null);
334201
- return new Error(details.length > 0 ? `Binance user-data WebSocket closed unexpectedly (${details.join(", ")})` : "Binance user-data WebSocket closed unexpectedly");
334202
- }
334203
- function decodeMessageData(data) {
334204
- if (typeof data === "string") {
334205
- return data;
334206
- }
334207
- if (Buffer2.isBuffer(data)) {
334208
- return data.toString("utf8");
334209
- }
334210
- if (data instanceof ArrayBuffer) {
334211
- return Buffer2.from(data).toString("utf8");
334212
- }
334213
- if (ArrayBuffer.isView(data)) {
334214
- return Buffer2.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
334215
- }
334216
- if (Array.isArray(data) && data.every((item) => Buffer2.isBuffer(item))) {
334217
- return Buffer2.concat(data).toString("utf8");
334218
- }
334219
- return data;
334220
- }
334221
-
334222
- class BinanceSpotUserDataStream {
334223
- exchange;
334224
- ws;
334225
- secretValues;
334226
- requestId = `user-data-${Date.now()}-${userDataRequestCounter++}`;
334227
- maxBufferedEvents;
334228
- queue = [];
334229
- waiters = [];
334230
- closed = false;
334231
- closeError = null;
334232
- subscriptionId = null;
334233
- constructor(exchange, options = {}) {
334234
- this.exchange = exchange;
334235
- this.maxBufferedEvents = options.maxBufferedEvents ?? DEFAULT_BINANCE_USER_DATA_MAX_BUFFERED_EVENTS;
334236
- this.secretValues = [
334237
- getOptionalExchangeString(exchange, "apiKey"),
334238
- getOptionalExchangeString(exchange, "secret")
334239
- ].filter((value) => value !== null);
334240
- this.ws = createWebSocket(getBinanceSpotWsApiUrl(exchange));
334241
- this.ws.on("open", () => this.subscribe());
334242
- this.ws.on("message", (data) => this.handleMessage(data));
334243
- this.ws.on("error", (error48) => this.fail(formatBinanceUserDataWebSocketError(error48, this.secretValues)));
334244
- this.ws.on("close", (code, reason) => this.handleClose(code, reason));
334245
- }
334246
- async* [Symbol.asyncIterator]() {
334247
- while (true) {
334248
- const event = await this.nextEvent();
334249
- if (!event) {
334250
- break;
334987
+ const metadata = call.metadata;
334988
+ const selectedBrokerAccount = selectBrokerAccountForCex(normalizedCex, brokers, metadata);
334989
+ const broker = selectedBrokerAccount?.exchange ?? createBroker(normalizedCex, call.metadata) ?? (isPublicMarketDataAction(action, call.request.payload) ? createPublicBroker(normalizedCex) : null);
334990
+ if (!broker) {
334991
+ return wrappedCallback({
334992
+ code: grpc12.status.UNAUTHENTICATED,
334993
+ message: `This Exchange is not registered and No API metadata was found`
334994
+ }, null);
334251
334995
  }
334252
- yield event;
334253
- }
334254
- }
334255
- close() {
334256
- if (this.closed) {
334257
- return;
334996
+ const verity = { proof: "" };
334997
+ const applyVerityToBroker = (targetBroker) => {
334998
+ if (!useVerity)
334999
+ return;
335000
+ const override = buildHttpClientOverrideFromMetadata(metadata, verityProverUrl, (proof, notaryPubKey) => {
335001
+ verity.proof = proof;
335002
+ log.debug(`Verity proof:`, { proof, notaryPubKey });
335003
+ });
335004
+ targetBroker.setHttpClientOverride(override, verityHttpClientOverridePredicate);
335005
+ };
335006
+ const preludeCtx = {
335007
+ call,
335008
+ wrappedCallback,
335009
+ action,
335010
+ policy,
335011
+ brokers,
335012
+ metadata,
335013
+ normalizedCex,
335014
+ cex: cex3,
335015
+ symbol: symbol2,
335016
+ selectedBrokerAccount,
335017
+ broker,
335018
+ verity,
335019
+ applyVerityToBroker,
335020
+ useVerity,
335021
+ verityProverUrl,
335022
+ otelMetrics,
335023
+ brokerArchiver,
335024
+ orderActivityTracker,
335025
+ withdrawalObservationTracker
335026
+ };
335027
+ if (action === Action.Call) {
335028
+ const handled = await handleOrderBookCall(preludeCtx);
335029
+ if (handled)
335030
+ return;
335031
+ }
335032
+ applyVerityToBroker(broker);
335033
+ const ctx = { ...preludeCtx, broker };
335034
+ await dispatchExecuteAction(ctx);
335035
+ } catch (error48) {
335036
+ safeLogError("ExecuteAction unhandled error", error48);
335037
+ return wrappedCallback({
335038
+ code: grpc12.status.INTERNAL,
335039
+ message: "ExecuteAction failed unexpectedly"
335040
+ }, null);
334258
335041
  }
334259
- this.closed = true;
334260
- this.queue.length = 0;
334261
- try {
334262
- this.ws.close();
334263
- } catch {}
334264
- this.flushWaiters();
334265
- }
334266
- handleClose(code, reason) {
334267
- if (this.closed) {
334268
- return;
335042
+ };
335043
+ }
335044
+ // src/handlers/subscribe/broker-lifecycle.ts
335045
+ class SubscribeBrokerLifecycle {
335046
+ #brokers = new Map;
335047
+ #closing = new Map;
335048
+ #shuttingDown = false;
335049
+ register(broker, context2) {
335050
+ this.#brokers.set(broker, context2);
335051
+ if (this.#shuttingDown) {
335052
+ this.close(broker);
334269
335053
  }
334270
- this.fail(formatBinanceUserDataWebSocketClose(code, reason, this.secretValues));
334271
335054
  }
334272
- subscribe() {
334273
- const apiKey = getExchangeString(this.exchange, "apiKey");
334274
- const signedParams = signUserDataStreamParams(this.exchange, {
334275
- apiKey,
334276
- timestamp: Date.now()
334277
- });
334278
- this.ws.send(JSON.stringify({
334279
- id: this.requestId,
334280
- method: "userDataStream.subscribe.signature",
334281
- params: signedParams
334282
- }));
334283
- }
334284
- handleMessage(data) {
334285
- if (this.closed) {
334286
- return;
334287
- }
334288
- let message;
334289
- try {
334290
- const decodedData = decodeMessageData(data);
334291
- message = typeof decodedData === "string" ? JSON.parse(decodedData) : decodedData;
334292
- } catch (error48) {
334293
- this.fail(error48 instanceof Error ? error48 : new Error("Invalid Binance user-data message"));
334294
- return;
335055
+ close(broker) {
335056
+ const existing = this.#closing.get(broker);
335057
+ if (existing) {
335058
+ return existing;
334295
335059
  }
334296
- if ("id" in message && message.id === this.requestId) {
334297
- if (message.status !== 200) {
334298
- this.fail(new Error(message.error?.msg ?? message.error?.message ?? `Binance user-data subscription failed with status ${message.status}`));
334299
- return;
335060
+ const context2 = this.#brokers.get(broker) ?? {
335061
+ cex: "unknown",
335062
+ symbol: "unknown"
335063
+ };
335064
+ this.#brokers.delete(broker);
335065
+ const closing = (async () => {
335066
+ try {
335067
+ await broker.close();
335068
+ log.debug("Request-scoped Subscribe broker closed", context2);
335069
+ return "closed";
335070
+ } catch (error48) {
335071
+ log.warn("Failed to close request-scoped Subscribe broker", {
335072
+ ...context2,
335073
+ error: error48
335074
+ });
335075
+ return "failed";
335076
+ } finally {
335077
+ this.#closing.delete(broker);
334300
335078
  }
334301
- this.subscriptionId = message.result?.subscriptionId ?? null;
334302
- return;
334303
- }
334304
- if ("status" in message && typeof message.status === "number" && message.status !== 200) {
334305
- const errorMessage = message.error?.msg ?? message.error?.message ?? `Binance user-data request failed with status ${message.status}`;
334306
- const errorCode2 = message.error?.code;
334307
- this.fail(new Error(typeof errorCode2 === "number" ? `${errorMessage} (code ${errorCode2})` : errorMessage));
334308
- return;
334309
- }
334310
- if (!("event" in message) || !message.event) {
334311
- return;
334312
- }
334313
- const subscriptionId = message.subscriptionId ?? this.subscriptionId;
334314
- if (subscriptionId === null || subscriptionId === undefined) {
334315
- return;
334316
- }
334317
- this.push({ subscriptionId, event: message.event });
335079
+ })();
335080
+ this.#closing.set(broker, closing);
335081
+ return closing;
334318
335082
  }
334319
- push(event) {
334320
- if (this.closed) {
334321
- return;
334322
- }
334323
- const waiter = this.waiters.shift();
334324
- if (waiter) {
334325
- waiter.resolve(event);
334326
- return;
335083
+ async closeAll() {
335084
+ this.#shuttingDown = true;
335085
+ let failed = 0;
335086
+ while (this.#brokers.size > 0 || this.#closing.size > 0) {
335087
+ const inFlight = [...this.#closing.values()];
335088
+ const fresh = [...this.#brokers.keys()].map((broker) => this.close(broker));
335089
+ const outcomes = await Promise.all([...fresh, ...inFlight]);
335090
+ failed += outcomes.filter((outcome) => outcome === "failed").length;
334327
335091
  }
334328
- if (this.queue.length >= this.maxBufferedEvents) {
334329
- this.fail(new Error(`Binance user-data stream buffered event limit exceeded (${this.maxBufferedEvents}); downstream consumer is not keeping up`));
334330
- return;
335092
+ if (failed > 0) {
335093
+ throw new Error(`${failed} request-scoped Subscribe broker(s) failed to close`);
334331
335094
  }
334332
- this.queue.push(event);
334333
335095
  }
334334
- nextEvent() {
334335
- const event = this.queue.shift();
334336
- if (event) {
334337
- return Promise.resolve(event);
334338
- }
334339
- if (this.closeError) {
334340
- return Promise.reject(this.closeError);
334341
- }
334342
- if (this.closed) {
334343
- return Promise.resolve(null);
334344
- }
334345
- return new Promise((resolve, reject) => {
334346
- this.waiters.push({ resolve, reject });
334347
- });
335096
+ }
335097
+ // src/handlers/subscribe/handler.ts
335098
+ var grpc13 = __toESM(require_src3(), 1);
335099
+
335100
+ // src/helpers/binance-user-data-normalization.ts
335101
+ function requireQuantity(entry, key2) {
335102
+ const value = entry[key2];
335103
+ if (typeof value === "string" && value.trim().length > 0) {
335104
+ return value;
334348
335105
  }
334349
- fail(error48) {
334350
- if (this.closeError) {
334351
- return;
334352
- }
334353
- this.closeError = error48;
334354
- this.closed = true;
334355
- this.queue.length = 0;
334356
- this.flushWaiters();
334357
- try {
334358
- this.ws.close();
334359
- } catch {}
335106
+ if (typeof value === "number" && Number.isFinite(value)) {
335107
+ return String(value);
334360
335108
  }
334361
- flushWaiters() {
334362
- const error48 = this.closeError;
334363
- for (const waiter of this.waiters.splice(0)) {
334364
- if (error48) {
334365
- waiter.reject(error48);
334366
- } else {
334367
- waiter.resolve(null);
334368
- }
335109
+ throw new Error(`Invalid Binance balance quantity: ${key2}`);
335110
+ }
335111
+ async function normalizeBinanceSpotBalanceEvent(exchange, event) {
335112
+ if (event.e !== "outboundAccountPosition") {
335113
+ return exchange.fetchBalance({ type: "spot" });
335114
+ }
335115
+ if (!Array.isArray(event.B)) {
335116
+ throw new Error("Invalid Binance outboundAccountPosition balances");
335117
+ }
335118
+ const timestamp = typeof event.E === "number" && Number.isFinite(event.E) ? event.E : undefined;
335119
+ const balance = {
335120
+ info: event,
335121
+ ...timestamp !== undefined && {
335122
+ timestamp,
335123
+ datetime: new Date(timestamp).toISOString()
335124
+ }
335125
+ };
335126
+ for (const rawEntry of event.B) {
335127
+ const entry = asRecord(rawEntry);
335128
+ const asset = entry?.a;
335129
+ if (!entry || typeof asset !== "string" || asset.length === 0) {
335130
+ throw new Error("Invalid Binance outboundAccountPosition asset");
334369
335131
  }
335132
+ balance[asset] = {
335133
+ free: requireQuantity(entry, "f"),
335134
+ used: requireQuantity(entry, "l")
335135
+ };
334370
335136
  }
335137
+ return exchange.safeBalance(balance);
334371
335138
  }
334372
- function isBinanceBalanceUserDataEvent(event) {
334373
- return event.e === "outboundAccountPosition" || event.e === "balanceUpdate" || event.e === "externalLockUpdate";
335139
+ function getTradeId(value) {
335140
+ if (typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.length > 0) {
335141
+ const tradeId = String(value);
335142
+ return tradeId === "-1" ? undefined : tradeId;
335143
+ }
335144
+ return;
334374
335145
  }
334375
- function isBinanceOrderUserDataEvent(event) {
334376
- return event.e === "executionReport" || event.e === "listStatus";
335146
+ function normalizeBinanceExecutionReport(exchange, event) {
335147
+ const parsed = asRecord(exchange.parseWsOrder(event));
335148
+ if (!parsed) {
335149
+ throw new Error("Binance executionReport did not parse as an order");
335150
+ }
335151
+ const { fee: _fee, fees: _fees, ...order } = parsed;
335152
+ const tradeId = getTradeId(event.t);
335153
+ return tradeId === undefined ? order : { ...order, tradeId };
334377
335154
  }
334378
335155
  // src/helpers/market-data-archive/ohlcv-bootstrap.ts
334379
335156
  var DEFAULT_OHLCV_BOOTSTRAP_LIMIT = 100;
@@ -334495,12 +335272,14 @@ async function getBinanceMarketId(broker, symbol2) {
334495
335272
  }
334496
335273
  return symbol2.replace("/", "").toUpperCase();
334497
335274
  }
334498
- async function streamBinanceUserData(call, broker, symbol2, subscriptionType, isClosed, archiveContext) {
334499
- const userDataStream = new BinanceSpotUserDataStream(broker);
334500
- call.once("close", () => userDataStream.close());
334501
- call.once("cancelled", () => userDataStream.close());
334502
- call.once("error", () => userDataStream.close());
334503
- const marketId = subscriptionType === SubscriptionType.ORDERS ? await getBinanceMarketId(broker, symbol2) : null;
335275
+ async function streamBinanceUserData(call, broker, symbol2, subscriptionType, isClosed, archiveContext, userDataSource, knownMarketId) {
335276
+ const userDataStream = userDataSource ?? new BinanceSpotUserDataStream(broker);
335277
+ if (!userDataSource) {
335278
+ call.once("close", () => userDataStream.close());
335279
+ call.once("cancelled", () => userDataStream.close());
335280
+ call.once("error", () => userDataStream.close());
335281
+ }
335282
+ const marketId = knownMarketId ?? (subscriptionType === SubscriptionType.ORDERS ? await getBinanceMarketId(broker, symbol2) : null);
334504
335283
  try {
334505
335284
  for await (const message of userDataStream) {
334506
335285
  if (isClosed()) {
@@ -334521,17 +335300,6 @@ async function streamBinanceUserData(call, broker, symbol2, subscriptionType, is
334521
335300
  }
334522
335301
  }
334523
335302
  const receivedTimestamp = Date.now();
334524
- if (!await writeSubscribeFrame(call, isClosed, {
334525
- data: JSON.stringify({
334526
- subscriptionId: message.subscriptionId,
334527
- event
334528
- }),
334529
- timestamp: receivedTimestamp,
334530
- symbol: symbol2,
334531
- type: subscriptionType
334532
- })) {
334533
- break;
334534
- }
334535
335303
  const archiveSubscriptionType = subscriptionType === SubscriptionType.BALANCE ? "BALANCE" : "ORDERS";
334536
335304
  archiveSubscribeStreamInBackground(archiveContext?.archiver, {
334537
335305
  exchange: archiveContext?.exchange ?? "binance",
@@ -334552,6 +335320,18 @@ async function streamBinanceUserData(call, broker, symbol2, subscriptionType, is
334552
335320
  receivedTimestamp
334553
335321
  });
334554
335322
  }
335323
+ if (subscriptionType === SubscriptionType.ORDERS && event.e === "listStatus") {
335324
+ continue;
335325
+ }
335326
+ const data = subscriptionType === SubscriptionType.BALANCE ? await normalizeBinanceSpotBalanceEvent(broker, event) : normalizeBinanceExecutionReport(broker, event);
335327
+ if (!await writeSubscribeFrame(call, isClosed, {
335328
+ data: JSON.stringify(data),
335329
+ timestamp: receivedTimestamp,
335330
+ symbol: symbol2,
335331
+ type: subscriptionType
335332
+ })) {
335333
+ break;
335334
+ }
334555
335335
  }
334556
335336
  } finally {
334557
335337
  userDataStream.close();
@@ -334591,7 +335371,13 @@ async function runCcxtSubscribeLoop(call, isClosed, symbol2, subscriptionType, w
334591
335371
  }
334592
335372
  }
334593
335373
  function createSubscribeHandler(deps) {
334594
- const { brokers, whitelistIps, otelMetrics, brokerArchiver } = deps;
335374
+ const {
335375
+ brokers,
335376
+ whitelistIps,
335377
+ otelMetrics,
335378
+ brokerArchiver,
335379
+ userDataStreamSupervisor
335380
+ } = deps;
334595
335381
  const brokerLifecycle = deps.brokerLifecycle ?? new SubscribeBrokerLifecycle;
334596
335382
  return async (call) => {
334597
335383
  const subscribeStartTime = Date.now();
@@ -334726,7 +335512,28 @@ function createSubscribeHandler(deps) {
334726
335512
  });
334727
335513
  return;
334728
335514
  }
334729
- await streamBinanceUserData(call, accountBroker, resolvedSymbol, subscriptionType, isStreamClosed, streamArchiveContext);
335515
+ const marketId = subscriptionType === SubscriptionType.ORDERS ? await getBinanceMarketId(accountBroker, resolvedSymbol) : undefined;
335516
+ let userDataSource;
335517
+ if (selectedBrokerAccount) {
335518
+ if (!userDataStreamSupervisor) {
335519
+ await writeSubscribeError(call, isStreamClosed, {
335520
+ data: JSON.stringify({
335521
+ error: "Configured account user-data supervisor is unavailable"
335522
+ }),
335523
+ timestamp: Date.now(),
335524
+ symbol: resolvedSymbol,
335525
+ type: subscriptionType
335526
+ });
335527
+ return;
335528
+ }
335529
+ userDataSource = userDataStreamSupervisor.subscribe({
335530
+ exchange: normalizedCex,
335531
+ accountSelector: selectedBrokerAccount.label,
335532
+ kind: subscriptionType === SubscriptionType.BALANCE ? "balance" : "orders",
335533
+ marketId
335534
+ });
335535
+ }
335536
+ await streamBinanceUserData(call, accountBroker, resolvedSymbol, subscriptionType, isStreamClosed, streamArchiveContext, userDataSource, marketId);
334730
335537
  return;
334731
335538
  }
334732
335539
  switch (subscriptionType) {
@@ -335120,7 +335927,7 @@ var CEX_BROKER_PACKAGE_DEFINITION = protoLoader.fromJSON(node_descriptor_default
335120
335927
  // src/server.ts
335121
335928
  var grpcObj = grpc14.loadPackageDefinition(CEX_BROKER_PACKAGE_DEFINITION);
335122
335929
  var cexNode = grpcObj.cex_broker;
335123
- function getServer(policy, brokers, whitelistIps, useVerity, verityProverUrl, otelMetrics, brokerArchiver, orderActivityTracker, withdrawalObservationTracker, subscribeBrokerLifecycle) {
335930
+ function getServer(policy, brokers, whitelistIps, useVerity, verityProverUrl, otelMetrics, brokerArchiver, orderActivityTracker, withdrawalObservationTracker, subscribeBrokerLifecycle, userDataStreamSupervisor) {
335124
335931
  const server = new grpc14.Server;
335125
335932
  server.addService(cexNode.cex_service.service, {
335126
335933
  ExecuteAction: createExecuteActionHandler({
@@ -335139,7 +335946,8 @@ function getServer(policy, brokers, whitelistIps, useVerity, verityProverUrl, ot
335139
335946
  whitelistIps,
335140
335947
  otelMetrics,
335141
335948
  brokerArchiver,
335142
- brokerLifecycle: subscribeBrokerLifecycle
335949
+ brokerLifecycle: subscribeBrokerLifecycle,
335950
+ userDataStreamSupervisor
335143
335951
  })
335144
335952
  });
335145
335953
  return server;
@@ -335283,13 +336091,14 @@ class CEXBroker {
335283
336091
  fillArchivePoller;
335284
336092
  depositArchivePoller;
335285
336093
  accountBalanceArchivePoller;
336094
+ userDataStreamSupervisor;
335286
336095
  loadEnvConfig() {
335287
336096
  log.info("\uD83D\uDD27 Loading CEX_BROKER_ environment variables:");
335288
336097
  const configMap = {};
335289
- for (const [key, value] of Object.entries(process.env)) {
335290
- if (!key.startsWith("CEX_BROKER_"))
336098
+ for (const [key2, value] of Object.entries(process.env)) {
336099
+ if (!key2.startsWith("CEX_BROKER_"))
335291
336100
  continue;
335292
- let match = key.match(/^CEX_BROKER_(\w+)_(API_(KEY|SECRET)|ROLE|EMAIL|SUBACCOUNTID|UID)_(\d+)$/);
336101
+ let match = key2.match(/^CEX_BROKER_(\w+)_(API_(KEY|SECRET)|ROLE|EMAIL|SUBACCOUNTID|UID)_(\d+)$/);
335293
336102
  if (match) {
335294
336103
  const broker2 = match[1]?.toLowerCase() ?? "";
335295
336104
  const type3 = match[2]?.toLowerCase() ?? "";
@@ -335318,9 +336127,9 @@ class CEXBroker {
335318
336127
  }
335319
336128
  continue;
335320
336129
  }
335321
- match = key.match(/^CEX_BROKER_(\w+)_(API_(KEY|SECRET)|ROLE|EMAIL|SUBACCOUNTID|UID)$/);
336130
+ match = key2.match(/^CEX_BROKER_(\w+)_(API_(KEY|SECRET)|ROLE|EMAIL|SUBACCOUNTID|UID)$/);
335322
336131
  if (!match) {
335323
- log.warn(`⚠️ Skipping unrecognized env var: ${key}`);
336132
+ log.warn(`⚠️ Skipping unrecognized env var: ${key2}`);
335324
336133
  continue;
335325
336134
  }
335326
336135
  const broker = match[1]?.toLowerCase() ?? "";
@@ -335438,6 +336247,10 @@ class CEXBroker {
335438
336247
  if (this.server) {
335439
336248
  await this.server.forceShutdown();
335440
336249
  }
336250
+ if (this.userDataStreamSupervisor) {
336251
+ await this.userDataStreamSupervisor.close();
336252
+ this.userDataStreamSupervisor = undefined;
336253
+ }
335441
336254
  if (this.brokerArchiver) {
335442
336255
  await this.brokerArchiver.close();
335443
336256
  }
@@ -335449,6 +336262,14 @@ class CEXBroker {
335449
336262
  }
335450
336263
  }
335451
336264
  async run() {
336265
+ const marketArchiveState = resolveMarketCaptureArchiveState({
336266
+ archiveEnabled: this.brokerArchiver?.isEnabled() ?? false,
336267
+ marketArchiveEnabled: isMarketArchiveEnabled(),
336268
+ environment: process.env.CEX_BROKER_MARKET_CAPTURE_ENVIRONMENT,
336269
+ deploymentId: this.brokerArchiver?.getDeploymentId(),
336270
+ captureBundleId: process.env.CEX_BROKER_CAPTURE_BUNDLE_ID
336271
+ });
336272
+ assertMarketCaptureArchiveStartable(marketArchiveState);
335452
336273
  if (this.server) {
335453
336274
  await this.server.forceShutdown();
335454
336275
  }
@@ -335472,7 +336293,15 @@ class CEXBroker {
335472
336293
  if (this.otelMetrics?.isOtelEnabled()) {
335473
336294
  await this.otelMetrics.initialize();
335474
336295
  }
335475
- this.server = getServer(this.policy, this.brokers, this.whitelistIps, this.useVerity, this.#verityProverUrl, this.otelMetrics, this.brokerArchiver, this.orderActivityTracker, this.withdrawalObservationTracker, undefined);
336296
+ if (!this.userDataStreamSupervisor && Object.keys(this.brokers).length > 0) {
336297
+ const publisher = new StreamHealthPublisher(streamHealthPublisherConfigFromEnv());
336298
+ this.userDataStreamSupervisor = new UserDataStreamSupervisor({
336299
+ brokers: this.brokers,
336300
+ publisher
336301
+ });
336302
+ this.userDataStreamSupervisor.start();
336303
+ }
336304
+ this.server = getServer(this.policy, this.brokers, this.whitelistIps, this.useVerity, this.#verityProverUrl, this.otelMetrics, this.brokerArchiver, this.orderActivityTracker, this.withdrawalObservationTracker, undefined, this.userDataStreamSupervisor);
335476
336305
  this.server.bindAsync(`0.0.0.0:${this.port}`, grpc15.ServerCredentials.createInsecure(), (err2, port) => {
335477
336306
  if (err2) {
335478
336307
  log.error(err2);