@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.
- package/README.md +6 -0
- package/dist/commands/cli.js +2088 -1259
- package/dist/handlers/subscribe/handler.d.ts +2 -0
- package/dist/helpers/binance-user-data-normalization.d.ts +3 -0
- package/dist/helpers/binance-user-data-stream.d.ts +12 -0
- package/dist/helpers/broker-execution-archive/index.d.ts +1 -1
- package/dist/helpers/broker-execution-archive/rows.d.ts +1 -0
- package/dist/helpers/deposit-archive-poller.d.ts +1 -0
- package/dist/helpers/market-data-archive/capture-context.d.ts +24 -3
- package/dist/helpers/market-data-archive/index.d.ts +1 -1
- package/dist/helpers/stream-health-publisher.d.ts +41 -0
- package/dist/helpers/user-data-stream-supervisor.d.ts +27 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2090 -1261
- package/dist/index.js.map +18 -15
- package/dist/server.d.ts +2 -1
- package/package.json +10 -2
package/dist/commands/cli.js
CHANGED
|
@@ -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:
|
|
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/
|
|
316990
|
-
|
|
316991
|
-
|
|
316992
|
-
|
|
316993
|
-
|
|
316994
|
-
|
|
316995
|
-
|
|
316996
|
-
|
|
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
|
-
|
|
316999
|
-
|
|
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
|
-
|
|
317013
|
-
|
|
317014
|
-
|
|
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
|
-
|
|
317026
|
-
|
|
317027
|
-
|
|
317028
|
-
|
|
317029
|
-
|
|
317030
|
-
|
|
317031
|
-
|
|
317032
|
-
|
|
317033
|
-
|
|
317034
|
-
|
|
317035
|
-
|
|
317036
|
-
|
|
317037
|
-
|
|
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
|
-
|
|
317062
|
-
|
|
317063
|
-
|
|
317064
|
-
|
|
317065
|
-
|
|
317066
|
-
|
|
317067
|
-
return
|
|
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
|
-
|
|
317073
|
-
if (
|
|
317074
|
-
|
|
317075
|
-
|
|
317076
|
-
|
|
317077
|
-
|
|
317078
|
-
|
|
317079
|
-
|
|
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
|
|
317088
|
-
|
|
317089
|
-
|
|
317090
|
-
|
|
317091
|
-
|
|
317092
|
-
|
|
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
|
|
317110
|
+
return value;
|
|
317096
317111
|
}
|
|
317097
|
-
|
|
317098
|
-
|
|
317099
|
-
|
|
317100
|
-
|
|
317101
|
-
|
|
317102
|
-
|
|
317103
|
-
|
|
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
|
-
|
|
317106
|
-
|
|
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
|
-
|
|
317122
|
-
|
|
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
|
-
|
|
317125
|
-
|
|
317137
|
+
if (!CAPTURE_FEEDS.includes(context2.feed)) {
|
|
317138
|
+
throw new Error(`Unsupported capture feed: ${context2.feed}`);
|
|
317126
317139
|
}
|
|
317127
|
-
|
|
317128
|
-
|
|
317129
|
-
|
|
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
|
-
|
|
317133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
317138
|
-
|
|
317139
|
-
|
|
317140
|
-
|
|
317141
|
-
|
|
317142
|
-
|
|
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
|
-
|
|
317147
|
-
if (
|
|
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
|
-
|
|
317159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
317174
|
-
|
|
317175
|
-
if (
|
|
317176
|
-
|
|
317177
|
-
|
|
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
|
-
|
|
317183
|
-
|
|
317184
|
-
|
|
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
|
-
|
|
317188
|
-
|
|
317189
|
-
|
|
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
|
-
|
|
317214
|
-
const
|
|
317215
|
-
if (
|
|
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
|
-
|
|
317219
|
-
|
|
317220
|
-
|
|
317221
|
-
|
|
317222
|
-
|
|
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
|
-
|
|
317232
|
-
|
|
317233
|
-
|
|
317234
|
-
|
|
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
|
-
|
|
317237
|
-
|
|
317238
|
-
|
|
317239
|
-
|
|
317240
|
-
|
|
317241
|
-
|
|
317242
|
-
|
|
317243
|
-
}
|
|
317244
|
-
|
|
317245
|
-
|
|
317246
|
-
|
|
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
|
-
|
|
317250
|
-
|
|
317251
|
-
|
|
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
|
-
|
|
317254
|
-
|
|
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
|
-
|
|
317257
|
-
this.
|
|
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
|
-
|
|
317260
|
-
|
|
317261
|
-
|
|
317262
|
-
|
|
317263
|
-
this
|
|
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
|
-
|
|
317267
|
-
|
|
317268
|
-
|
|
317269
|
-
|
|
317270
|
-
|
|
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
|
-
|
|
317274
|
-
|
|
317275
|
-
|
|
317276
|
-
|
|
317277
|
-
|
|
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
|
-
|
|
317281
|
-
|
|
317282
|
-
|
|
317283
|
-
|
|
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
|
-
|
|
317287
|
-
|
|
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
|
-
|
|
317295
|
-
|
|
317296
|
-
|
|
317297
|
-
|
|
317298
|
-
|
|
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
|
-
|
|
317301
|
-
|
|
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
|
-
|
|
317327
|
-
return new
|
|
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
|
-
|
|
317342
|
-
|
|
317343
|
-
|
|
317344
|
-
|
|
317345
|
-
|
|
317346
|
-
|
|
317347
|
-
|
|
317348
|
-
|
|
317349
|
-
|
|
317350
|
-
|
|
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
|
-
|
|
317354
|
-
|
|
317355
|
-
|
|
317356
|
-
|
|
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
|
-
|
|
317360
|
-
|
|
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,
|
|
319452
|
+
function defineLazy(object, key2, getter) {
|
|
318213
319453
|
let value = undefined;
|
|
318214
|
-
Object.defineProperty(object,
|
|
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,
|
|
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,
|
|
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((
|
|
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
|
|
318327
|
-
if (Object.prototype.hasOwnProperty.call(data,
|
|
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
|
|
318471
|
-
if (!(
|
|
318472
|
-
throw new Error(`Unrecognized 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[
|
|
319714
|
+
if (!mask2[key2])
|
|
318475
319715
|
continue;
|
|
318476
|
-
newShape[
|
|
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
|
|
318496
|
-
if (!(
|
|
318497
|
-
throw new Error(`Unrecognized 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[
|
|
319739
|
+
if (!mask2[key2])
|
|
318500
319740
|
continue;
|
|
318501
|
-
delete newShape[
|
|
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
|
|
318519
|
-
if (Object.getOwnPropertyDescriptor(existingShape,
|
|
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
|
|
318573
|
-
if (!(
|
|
318574
|
-
throw new Error(`Unrecognized key: "${
|
|
319812
|
+
for (const key2 in mask2) {
|
|
319813
|
+
if (!(key2 in oldShape)) {
|
|
319814
|
+
throw new Error(`Unrecognized key: "${key2}"`);
|
|
318575
319815
|
}
|
|
318576
|
-
if (!mask2[
|
|
319816
|
+
if (!mask2[key2])
|
|
318577
319817
|
continue;
|
|
318578
|
-
shape[
|
|
319818
|
+
shape[key2] = Class ? new Class({
|
|
318579
319819
|
type: "optional",
|
|
318580
|
-
innerType: oldShape[
|
|
318581
|
-
}) : oldShape[
|
|
319820
|
+
innerType: oldShape[key2]
|
|
319821
|
+
}) : oldShape[key2];
|
|
318582
319822
|
}
|
|
318583
319823
|
} else {
|
|
318584
|
-
for (const
|
|
318585
|
-
shape[
|
|
319824
|
+
for (const key2 in oldShape) {
|
|
319825
|
+
shape[key2] = Class ? new Class({
|
|
318586
319826
|
type: "optional",
|
|
318587
|
-
innerType: oldShape[
|
|
318588
|
-
}) : oldShape[
|
|
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
|
|
318605
|
-
if (!(
|
|
318606
|
-
throw new Error(`Unrecognized key: "${
|
|
319844
|
+
for (const key2 in mask2) {
|
|
319845
|
+
if (!(key2 in shape)) {
|
|
319846
|
+
throw new Error(`Unrecognized key: "${key2}"`);
|
|
318607
319847
|
}
|
|
318608
|
-
if (!mask2[
|
|
319848
|
+
if (!mask2[key2])
|
|
318609
319849
|
continue;
|
|
318610
|
-
shape[
|
|
319850
|
+
shape[key2] = new Class({
|
|
318611
319851
|
type: "nonoptional",
|
|
318612
|
-
innerType: oldShape[
|
|
319852
|
+
innerType: oldShape[key2]
|
|
318613
319853
|
});
|
|
318614
319854
|
}
|
|
318615
319855
|
} else {
|
|
318616
|
-
for (const
|
|
318617
|
-
shape[
|
|
319856
|
+
for (const key2 in oldShape) {
|
|
319857
|
+
shape[key2] = new Class({
|
|
318618
319858
|
type: "nonoptional",
|
|
318619
|
-
innerType: oldShape[
|
|
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,
|
|
321609
|
+
function handlePropertyResult(result, final, key2, input, isOptionalOut) {
|
|
320370
321610
|
if (result.issues.length) {
|
|
320371
|
-
if (isOptionalOut && !(
|
|
321611
|
+
if (isOptionalOut && !(key2 in input)) {
|
|
320372
321612
|
return;
|
|
320373
321613
|
}
|
|
320374
|
-
final.issues.push(...prefixIssues(
|
|
321614
|
+
final.issues.push(...prefixIssues(key2, result.issues));
|
|
320375
321615
|
}
|
|
320376
321616
|
if (result.value === undefined) {
|
|
320377
|
-
if (
|
|
320378
|
-
final.value[
|
|
321617
|
+
if (key2 in input) {
|
|
321618
|
+
final.value[key2] = undefined;
|
|
320379
321619
|
}
|
|
320380
321620
|
} else {
|
|
320381
|
-
final.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
|
|
320407
|
-
if (keySet.has(
|
|
321646
|
+
for (const key2 in input) {
|
|
321647
|
+
if (keySet.has(key2))
|
|
320408
321648
|
continue;
|
|
320409
321649
|
if (t === "never") {
|
|
320410
|
-
unrecognized.push(
|
|
321650
|
+
unrecognized.push(key2);
|
|
320411
321651
|
continue;
|
|
320412
321652
|
}
|
|
320413
|
-
const r = _catchall.run({ value: input[
|
|
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,
|
|
321655
|
+
proms.push(r.then((r2) => handlePropertyResult(r2, payload, key2, input, isOptionalOut)));
|
|
320416
321656
|
} else {
|
|
320417
|
-
handlePropertyResult(r, payload,
|
|
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
|
|
320454
|
-
const field = shape[
|
|
321693
|
+
for (const key2 in shape) {
|
|
321694
|
+
const field = shape[key2]._zod;
|
|
320455
321695
|
if (field.values) {
|
|
320456
|
-
propValues[
|
|
321696
|
+
propValues[key2] ?? (propValues[key2] = new Set);
|
|
320457
321697
|
for (const v of field.values)
|
|
320458
|
-
propValues[
|
|
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
|
|
320482
|
-
const el = shape[
|
|
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[
|
|
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,
|
|
321726
|
+
proms.push(r.then((r2) => handlePropertyResult(r2, payload, key2, input, isOptionalOut)));
|
|
320487
321727
|
} else {
|
|
320488
|
-
handlePropertyResult(r, payload,
|
|
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 = (
|
|
320505
|
-
const k = esc(
|
|
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
|
|
320511
|
-
for (const
|
|
320512
|
-
ids[
|
|
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
|
|
320516
|
-
const id2 = ids[
|
|
320517
|
-
const k = esc(
|
|
320518
|
-
const schema = shape[
|
|
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(
|
|
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((
|
|
322046
|
+
const sharedKeys = Object.keys(a).filter((key2) => bKeys.indexOf(key2) !== -1);
|
|
320807
322047
|
const newObj = { ...a, ...b2 };
|
|
320808
|
-
for (const
|
|
320809
|
-
const sharedValue = mergeValues(a[
|
|
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: [
|
|
322053
|
+
mergeErrorPath: [key2, ...sharedValue.mergeErrorPath]
|
|
320814
322054
|
};
|
|
320815
322055
|
}
|
|
320816
|
-
newObj[
|
|
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
|
|
320973
|
-
if (typeof
|
|
320974
|
-
recordKeys.add(typeof
|
|
320975
|
-
const result = def.valueType._zod.run({ value: input[
|
|
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(
|
|
322219
|
+
payload.issues.push(...prefixIssues(key2, result2.issues));
|
|
320980
322220
|
}
|
|
320981
|
-
payload.value[
|
|
322221
|
+
payload.value[key2] = result2.value;
|
|
320982
322222
|
}));
|
|
320983
322223
|
} else {
|
|
320984
322224
|
if (result.issues.length) {
|
|
320985
|
-
payload.issues.push(...prefixIssues(
|
|
322225
|
+
payload.issues.push(...prefixIssues(key2, result.issues));
|
|
320986
322226
|
}
|
|
320987
|
-
payload.value[
|
|
322227
|
+
payload.value[key2] = result.value;
|
|
320988
322228
|
}
|
|
320989
322229
|
}
|
|
320990
322230
|
}
|
|
320991
322231
|
let unrecognized;
|
|
320992
|
-
for (const
|
|
320993
|
-
if (!recordKeys.has(
|
|
322232
|
+
for (const key2 in input) {
|
|
322233
|
+
if (!recordKeys.has(key2)) {
|
|
320994
322234
|
unrecognized = unrecognized ?? [];
|
|
320995
|
-
unrecognized.push(
|
|
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
|
|
321009
|
-
if (
|
|
322248
|
+
for (const key2 of Reflect.ownKeys(input)) {
|
|
322249
|
+
if (key2 === "__proto__")
|
|
321010
322250
|
continue;
|
|
321011
|
-
let keyResult = def.keyType._zod.run({ value:
|
|
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
|
|
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(
|
|
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[
|
|
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:
|
|
321034
|
-
path: [
|
|
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[
|
|
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(
|
|
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(
|
|
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 [
|
|
321078
|
-
const keyResult = def.keyType._zod.run({ value:
|
|
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,
|
|
322322
|
+
handleMapResult(keyResult2, valueResult2, payload, key2, input, inst, ctx);
|
|
321083
322323
|
}));
|
|
321084
322324
|
} else {
|
|
321085
|
-
handleMapResult(keyResult, valueResult, payload,
|
|
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,
|
|
322333
|
+
function handleMapResult(keyResult, valueResult, final, key2, input, inst, ctx) {
|
|
321094
322334
|
if (keyResult.issues.length) {
|
|
321095
|
-
if (propertyKeyTypes.has(typeof
|
|
321096
|
-
final.issues.push(...prefixIssues(
|
|
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
|
|
321109
|
-
final.issues.push(...prefixIssues(
|
|
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,
|
|
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
|
|
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
|
|
328311
|
-
delete schema2[
|
|
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
|
|
328379
|
-
if (
|
|
329618
|
+
for (const key2 in schema2) {
|
|
329619
|
+
if (key2 === "$ref" || key2 === "allOf")
|
|
328380
329620
|
continue;
|
|
328381
|
-
if (!(
|
|
328382
|
-
delete schema2[
|
|
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
|
|
328388
|
-
if (
|
|
329627
|
+
for (const key2 in schema2) {
|
|
329628
|
+
if (key2 === "$ref" || key2 === "allOf")
|
|
328389
329629
|
continue;
|
|
328390
|
-
if (
|
|
328391
|
-
delete schema2[
|
|
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
|
|
328404
|
-
if (
|
|
329643
|
+
for (const key2 in schema2) {
|
|
329644
|
+
if (key2 === "$ref" || key2 === "allOf")
|
|
328405
329645
|
continue;
|
|
328406
|
-
if (
|
|
328407
|
-
delete schema2[
|
|
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
|
|
328499
|
-
if (isTransforming(def.shape[
|
|
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
|
|
328791
|
-
json3.properties[
|
|
330030
|
+
for (const key2 in shape) {
|
|
330031
|
+
json3.properties[key2] = process2(shape[key2], ctx, {
|
|
328792
330032
|
...params,
|
|
328793
|
-
path: [...params.path, "properties",
|
|
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((
|
|
328798
|
-
const v = def.shape[
|
|
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 [
|
|
330303
|
+
const [key2, schema] = entry;
|
|
329064
330304
|
extractDefs(ctx2, schema);
|
|
329065
|
-
schemas[
|
|
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
|
|
330623
|
-
if (!
|
|
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[
|
|
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 [
|
|
332052
|
+
for (const [key2, propSchema] of Object.entries(properties)) {
|
|
330813
332053
|
const propZodSchema = convertSchema(propSchema, ctx);
|
|
330814
|
-
shape[
|
|
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
|
|
330959
|
-
if (
|
|
330960
|
-
extraMeta[
|
|
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
|
|
330965
|
-
if (
|
|
330966
|
-
extraMeta[
|
|
332204
|
+
for (const key2 of contentMetadataKeys) {
|
|
332205
|
+
if (key2 in schema) {
|
|
332206
|
+
extraMeta[key2] = schema[key2];
|
|
330967
332207
|
}
|
|
330968
332208
|
}
|
|
330969
|
-
for (const
|
|
330970
|
-
if (!RECOGNIZED_KEYS.has(
|
|
330971
|
-
extraMeta[
|
|
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:
|
|
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
|
-
|
|
331762
|
-
|
|
331763
|
-
|
|
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
|
|
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
|
|
331775
|
-
|
|
332815
|
+
function parseOptionalDepthLimit(value) {
|
|
332816
|
+
const parsed = parsePositiveInteger(nonEmptyString(value), "depthLimit");
|
|
332817
|
+
return parsed.ok ? parsed.value : undefined;
|
|
331776
332818
|
}
|
|
331777
|
-
function
|
|
331778
|
-
|
|
331779
|
-
|
|
331780
|
-
|
|
331781
|
-
|
|
331782
|
-
|
|
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
|
|
332826
|
+
return;
|
|
331794
332827
|
}
|
|
331795
|
-
function
|
|
331796
|
-
|
|
331797
|
-
|
|
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
|
-
|
|
331803
|
-
|
|
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
|
-
|
|
331806
|
-
|
|
331807
|
-
|
|
331808
|
-
|
|
331809
|
-
|
|
331810
|
-
|
|
331811
|
-
|
|
331812
|
-
|
|
331813
|
-
|
|
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
|
|
331817
|
-
|
|
331818
|
-
|
|
331819
|
-
|
|
331820
|
-
|
|
331821
|
-
|
|
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
|
-
|
|
331838
|
-
|
|
331839
|
-
|
|
331840
|
-
|
|
331841
|
-
|
|
331842
|
-
|
|
331843
|
-
|
|
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
|
|
331847
|
-
assertCaptureContext(context2);
|
|
332891
|
+
function buildHistoricalOrderBookUnsupported(payload) {
|
|
331848
332892
|
return {
|
|
331849
|
-
|
|
331850
|
-
|
|
331851
|
-
|
|
331852
|
-
|
|
331853
|
-
|
|
331854
|
-
|
|
331855
|
-
|
|
331856
|
-
|
|
331857
|
-
|
|
331858
|
-
|
|
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
|
-
|
|
332233
|
-
|
|
332234
|
-
|
|
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 [
|
|
333304
|
+
for (const [key2, rawValue] of fields) {
|
|
332340
333305
|
const numeric = toNumber2(rawValue);
|
|
332341
333306
|
if (numeric !== undefined) {
|
|
332342
|
-
parsed[
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
334253
|
-
|
|
334254
|
-
|
|
334255
|
-
|
|
334256
|
-
|
|
334257
|
-
|
|
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
|
-
|
|
334260
|
-
|
|
334261
|
-
|
|
334262
|
-
|
|
334263
|
-
|
|
334264
|
-
|
|
334265
|
-
|
|
334266
|
-
|
|
334267
|
-
|
|
334268
|
-
|
|
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
|
-
|
|
334273
|
-
const
|
|
334274
|
-
|
|
334275
|
-
|
|
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
|
-
|
|
334297
|
-
|
|
334298
|
-
|
|
334299
|
-
|
|
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
|
-
|
|
334302
|
-
|
|
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
|
-
|
|
334320
|
-
|
|
334321
|
-
|
|
334322
|
-
|
|
334323
|
-
|
|
334324
|
-
|
|
334325
|
-
|
|
334326
|
-
|
|
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 (
|
|
334329
|
-
|
|
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
|
-
|
|
334335
|
-
|
|
334336
|
-
|
|
334337
|
-
|
|
334338
|
-
|
|
334339
|
-
|
|
334340
|
-
|
|
334341
|
-
|
|
334342
|
-
|
|
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
|
-
|
|
334350
|
-
|
|
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
|
-
|
|
334362
|
-
|
|
334363
|
-
|
|
334364
|
-
|
|
334365
|
-
|
|
334366
|
-
|
|
334367
|
-
|
|
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
|
|
334373
|
-
|
|
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
|
|
334376
|
-
|
|
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
|
-
|
|
334501
|
-
|
|
334502
|
-
|
|
334503
|
-
|
|
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 {
|
|
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
|
|
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 [
|
|
335290
|
-
if (!
|
|
336098
|
+
for (const [key2, value] of Object.entries(process.env)) {
|
|
336099
|
+
if (!key2.startsWith("CEX_BROKER_"))
|
|
335291
336100
|
continue;
|
|
335292
|
-
let match =
|
|
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 =
|
|
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: ${
|
|
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
|
-
|
|
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);
|