@usherlabs/cex-broker 0.2.23 → 0.2.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/cli.js +484 -8
- package/dist/handlers/execute-action/context.d.ts +2 -1
- package/dist/handlers/execute-action/handler.d.ts +2 -1
- package/dist/helpers/account-balance-archive-poller.d.ts +19 -0
- package/dist/helpers/broker-execution-archive/capture.d.ts +6 -0
- package/dist/helpers/broker-execution-archive/index.d.ts +5 -4
- package/dist/helpers/broker-execution-archive/rows.d.ts +18 -0
- package/dist/helpers/broker-execution-archive/types.d.ts +4 -2
- package/dist/helpers/broker-execution-archive/withdrawal-observation-tracker.d.ts +23 -0
- package/dist/helpers/broker-execution-archive/writer.d.ts +3 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +485 -9
- package/dist/index.js.map +15 -13
- package/dist/server.d.ts +2 -2
- package/package.json +1 -1
package/dist/commands/cli.js
CHANGED
|
@@ -315208,6 +315208,9 @@ function compactUndefined(record) {
|
|
|
315208
315208
|
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined));
|
|
315209
315209
|
}
|
|
315210
315210
|
|
|
315211
|
+
// src/helpers/broker-execution-archive/rows.ts
|
|
315212
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
315213
|
+
|
|
315211
315214
|
// src/helpers/broker-execution-archive/redact.ts
|
|
315212
315215
|
import { createHash } from "node:crypto";
|
|
315213
315216
|
var SECRET_KEY_PATTERN = /\b(api[_-]?key|api[_-]?secret|secret|signature|passphrase|password|token|credential)\b/i;
|
|
@@ -315280,6 +315283,8 @@ function hashMarketMetadata(payload) {
|
|
|
315280
315283
|
var BROKER_WRITE_SOURCE = "broker_write";
|
|
315281
315284
|
var ARCHIVE_SCHEMA_VERSION = "1";
|
|
315282
315285
|
var FILL_EVENT_KIND = "trade_history_fill";
|
|
315286
|
+
var ACCOUNT_BALANCE_SCOPE = "spot";
|
|
315287
|
+
var ACCOUNT_BALANCE_PRECISION_BASIS = "ccxt_normalized_number";
|
|
315283
315288
|
|
|
315284
315289
|
// src/helpers/broker-execution-archive/rows.ts
|
|
315285
315290
|
function firstString2(...values2) {
|
|
@@ -315313,6 +315318,151 @@ function normalizeTimestamp2(value) {
|
|
|
315313
315318
|
const date = new Date(ms);
|
|
315314
315319
|
return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
|
|
315315
315320
|
}
|
|
315321
|
+
function decimalString(value) {
|
|
315322
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
315323
|
+
return;
|
|
315324
|
+
}
|
|
315325
|
+
if (Object.is(value, -0)) {
|
|
315326
|
+
return "0";
|
|
315327
|
+
}
|
|
315328
|
+
const rendered = String(value).toLowerCase();
|
|
315329
|
+
if (!rendered.includes("e")) {
|
|
315330
|
+
return rendered;
|
|
315331
|
+
}
|
|
315332
|
+
const [coefficient = "", rawExponent = "0"] = rendered.split("e");
|
|
315333
|
+
const exponent = Number.parseInt(rawExponent, 10);
|
|
315334
|
+
const negative = coefficient.startsWith("-");
|
|
315335
|
+
const unsigned = negative ? coefficient.slice(1) : coefficient;
|
|
315336
|
+
const [integer = "0", fraction = ""] = unsigned.split(".");
|
|
315337
|
+
const digits = `${integer}${fraction}`;
|
|
315338
|
+
const decimalIndex = integer.length + exponent;
|
|
315339
|
+
let expanded;
|
|
315340
|
+
if (decimalIndex <= 0) {
|
|
315341
|
+
expanded = `0.${"0".repeat(-decimalIndex)}${digits}`;
|
|
315342
|
+
} else if (decimalIndex >= digits.length) {
|
|
315343
|
+
expanded = `${digits}${"0".repeat(decimalIndex - digits.length)}`;
|
|
315344
|
+
} else {
|
|
315345
|
+
expanded = `${digits.slice(0, decimalIndex)}.${digits.slice(decimalIndex)}`;
|
|
315346
|
+
}
|
|
315347
|
+
return negative ? `-${expanded}` : expanded;
|
|
315348
|
+
}
|
|
315349
|
+
function normalizedBalanceMap(value) {
|
|
315350
|
+
const record = asRecord(value);
|
|
315351
|
+
if (!record) {
|
|
315352
|
+
return {};
|
|
315353
|
+
}
|
|
315354
|
+
const entries = [];
|
|
315355
|
+
for (const [rawAsset, quantity] of Object.entries(record)) {
|
|
315356
|
+
const asset = rawAsset.trim();
|
|
315357
|
+
const normalized = decimalString(quantity);
|
|
315358
|
+
if (asset && normalized !== undefined) {
|
|
315359
|
+
entries.push([asset, normalized]);
|
|
315360
|
+
}
|
|
315361
|
+
}
|
|
315362
|
+
return Object.fromEntries(entries.sort(([left], [right]) => left.localeCompare(right)));
|
|
315363
|
+
}
|
|
315364
|
+
function sortedBalanceMap(map) {
|
|
315365
|
+
return Object.fromEntries(Object.entries(map).sort(([left], [right]) => left.localeCompare(right)));
|
|
315366
|
+
}
|
|
315367
|
+
var BALANCE_METADATA_KEYS = new Set([
|
|
315368
|
+
"info",
|
|
315369
|
+
"timestamp",
|
|
315370
|
+
"datetime",
|
|
315371
|
+
"free",
|
|
315372
|
+
"used",
|
|
315373
|
+
"total",
|
|
315374
|
+
"debt"
|
|
315375
|
+
]);
|
|
315376
|
+
function normalizeCcxtBalanceForArchive(balance) {
|
|
315377
|
+
const record = asRecord(balance) ?? {};
|
|
315378
|
+
const aggregateRecords = {
|
|
315379
|
+
free: asRecord(record.free),
|
|
315380
|
+
used: asRecord(record.used),
|
|
315381
|
+
total: asRecord(record.total)
|
|
315382
|
+
};
|
|
315383
|
+
const maps = {
|
|
315384
|
+
free: normalizedBalanceMap(record.free),
|
|
315385
|
+
used: normalizedBalanceMap(record.used),
|
|
315386
|
+
total: normalizedBalanceMap(record.total)
|
|
315387
|
+
};
|
|
315388
|
+
const assetEntryAssets = new Set;
|
|
315389
|
+
for (const [rawAsset, value] of Object.entries(record)) {
|
|
315390
|
+
if (BALANCE_METADATA_KEYS.has(rawAsset)) {
|
|
315391
|
+
continue;
|
|
315392
|
+
}
|
|
315393
|
+
const asset = rawAsset.trim();
|
|
315394
|
+
const assetEntry = asRecord(value);
|
|
315395
|
+
if (!asset || !assetEntry || !(("free" in assetEntry) || ("used" in assetEntry) || ("total" in assetEntry))) {
|
|
315396
|
+
continue;
|
|
315397
|
+
}
|
|
315398
|
+
assetEntryAssets.add(asset);
|
|
315399
|
+
for (const field of ["free", "used", "total"]) {
|
|
315400
|
+
const normalized = decimalString(assetEntry[field]);
|
|
315401
|
+
if (normalized !== undefined && maps[field][asset] === undefined) {
|
|
315402
|
+
maps[field][asset] = normalized;
|
|
315403
|
+
}
|
|
315404
|
+
}
|
|
315405
|
+
}
|
|
315406
|
+
const reportedAssets = new Set(assetEntryAssets);
|
|
315407
|
+
for (const aggregateRecord of Object.values(aggregateRecords)) {
|
|
315408
|
+
for (const rawAsset of Object.keys(aggregateRecord ?? {})) {
|
|
315409
|
+
const asset = rawAsset.trim();
|
|
315410
|
+
if (asset) {
|
|
315411
|
+
reportedAssets.add(asset);
|
|
315412
|
+
}
|
|
315413
|
+
}
|
|
315414
|
+
}
|
|
315415
|
+
for (const map of Object.values(maps)) {
|
|
315416
|
+
for (const asset of Object.keys(map)) {
|
|
315417
|
+
reportedAssets.add(asset);
|
|
315418
|
+
}
|
|
315419
|
+
}
|
|
315420
|
+
return {
|
|
315421
|
+
exchangeTimestamp: normalizeTimestamp2(record.timestamp ?? record.datetime),
|
|
315422
|
+
reportedAssets: [...reportedAssets].sort(),
|
|
315423
|
+
assetEntryAssets: [...assetEntryAssets].sort(),
|
|
315424
|
+
freeBalances: sortedBalanceMap(maps.free),
|
|
315425
|
+
usedBalances: sortedBalanceMap(maps.used),
|
|
315426
|
+
totalBalances: sortedBalanceMap(maps.total),
|
|
315427
|
+
freeMapPresent: aggregateRecords.free !== undefined,
|
|
315428
|
+
usedMapPresent: aggregateRecords.used !== undefined,
|
|
315429
|
+
totalMapPresent: aggregateRecords.total !== undefined
|
|
315430
|
+
};
|
|
315431
|
+
}
|
|
315432
|
+
function buildAccountBalanceSnapshotRow(input) {
|
|
315433
|
+
const { tags, balance } = input;
|
|
315434
|
+
const observationId = createHash2("sha256").update(JSON.stringify({
|
|
315435
|
+
deployment_id: tags.deployment_id,
|
|
315436
|
+
exchange: tags.exchange,
|
|
315437
|
+
account_selector: tags.account_selector,
|
|
315438
|
+
balance_scope: ACCOUNT_BALANCE_SCOPE,
|
|
315439
|
+
broker_observed_timestamp: tags.broker_observed_timestamp,
|
|
315440
|
+
balance
|
|
315441
|
+
})).digest("hex");
|
|
315442
|
+
return {
|
|
315443
|
+
table: "broker_account.balance_snapshots",
|
|
315444
|
+
row: compactUndefined2({
|
|
315445
|
+
broker_observed_timestamp: tags.broker_observed_timestamp,
|
|
315446
|
+
exchange_timestamp: balance.exchangeTimestamp,
|
|
315447
|
+
source: tags.source,
|
|
315448
|
+
deployment_id: tags.deployment_id,
|
|
315449
|
+
schema_version: ARCHIVE_SCHEMA_VERSION,
|
|
315450
|
+
exchange: tags.exchange,
|
|
315451
|
+
account_selector: tags.account_selector,
|
|
315452
|
+
balance_scope: ACCOUNT_BALANCE_SCOPE,
|
|
315453
|
+
observation_id: observationId,
|
|
315454
|
+
reported_assets: balance.reportedAssets,
|
|
315455
|
+
asset_entry_assets: balance.assetEntryAssets,
|
|
315456
|
+
free_balances: balance.freeBalances,
|
|
315457
|
+
used_balances: balance.usedBalances,
|
|
315458
|
+
total_balances: balance.totalBalances,
|
|
315459
|
+
aggregate_free_map_present: Number(balance.freeMapPresent),
|
|
315460
|
+
aggregate_used_map_present: Number(balance.usedMapPresent),
|
|
315461
|
+
aggregate_total_map_present: Number(balance.totalMapPresent),
|
|
315462
|
+
precision_basis: ACCOUNT_BALANCE_PRECISION_BASIS
|
|
315463
|
+
})
|
|
315464
|
+
};
|
|
315465
|
+
}
|
|
315316
315466
|
function compactUndefined2(record) {
|
|
315317
315467
|
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined));
|
|
315318
315468
|
}
|
|
@@ -315557,6 +315707,52 @@ function archiveTransferEventInBackground(archiver, input) {
|
|
|
315557
315707
|
}
|
|
315558
315708
|
});
|
|
315559
315709
|
}
|
|
315710
|
+
function archiveWithdrawalObservationsInBackground(archiver, tracker, input) {
|
|
315711
|
+
try {
|
|
315712
|
+
if (!archiver?.isEnabled() || !Array.isArray(input.transactions)) {
|
|
315713
|
+
return;
|
|
315714
|
+
}
|
|
315715
|
+
const brokerObservedTimestamp = new Date().toISOString();
|
|
315716
|
+
for (const [resultIndex, transaction] of input.transactions.entries()) {
|
|
315717
|
+
const normalized = normalizeCcxtTransactionForArchive(transaction);
|
|
315718
|
+
const assetSymbol = normalized.assetSymbol;
|
|
315719
|
+
if (!tracker.shouldArchive({
|
|
315720
|
+
exchange: input.exchange,
|
|
315721
|
+
accountSelector: input.accountSelector,
|
|
315722
|
+
assetSymbol,
|
|
315723
|
+
transaction,
|
|
315724
|
+
normalized
|
|
315725
|
+
})) {
|
|
315726
|
+
continue;
|
|
315727
|
+
}
|
|
315728
|
+
archiveTransferEventInBackground(archiver, {
|
|
315729
|
+
exchange: input.exchange,
|
|
315730
|
+
accountSelector: input.accountSelector,
|
|
315731
|
+
assetSymbol,
|
|
315732
|
+
brokerObservedTimestamp,
|
|
315733
|
+
transfer: {
|
|
315734
|
+
eventKind: "withdrawal",
|
|
315735
|
+
lifecycleAction: "observe_withdrawal",
|
|
315736
|
+
status: normalized.status,
|
|
315737
|
+
amount: normalized.amount,
|
|
315738
|
+
address: normalized.address,
|
|
315739
|
+
network: normalized.network,
|
|
315740
|
+
externalId: normalized.externalId,
|
|
315741
|
+
txid: normalized.txid,
|
|
315742
|
+
resultIndex,
|
|
315743
|
+
feeAmount: normalized.feeAmount,
|
|
315744
|
+
feeCurrency: normalized.feeCurrency,
|
|
315745
|
+
exchangeTimestamp: normalized.exchangeTimestamp,
|
|
315746
|
+
payload: transaction
|
|
315747
|
+
}
|
|
315748
|
+
});
|
|
315749
|
+
}
|
|
315750
|
+
} catch (archiveError) {
|
|
315751
|
+
log.warn("Failed to archive withdrawal observations", {
|
|
315752
|
+
error: archiveError
|
|
315753
|
+
});
|
|
315754
|
+
}
|
|
315755
|
+
}
|
|
315560
315756
|
async function captureMarketMetadataSnapshot(archiver, broker, input) {
|
|
315561
315757
|
if (!archiver?.canPersistMarketMetadataSnapshot()) {
|
|
315562
315758
|
return;
|
|
@@ -315602,6 +315798,106 @@ async function captureMarketMetadataSnapshot(archiver, broker, input) {
|
|
|
315602
315798
|
return;
|
|
315603
315799
|
}
|
|
315604
315800
|
}
|
|
315801
|
+
// src/helpers/broker-execution-archive/withdrawal-observation-tracker.ts
|
|
315802
|
+
var DEFAULT_WITHDRAWAL_OBSERVATION_TRACKER_MAX_ENTRIES = 1e4;
|
|
315803
|
+
var VENUE_LIFECYCLE_EVIDENCE_FIELDS = [
|
|
315804
|
+
"updated",
|
|
315805
|
+
"updatedAt",
|
|
315806
|
+
"updated_at",
|
|
315807
|
+
"updateTime",
|
|
315808
|
+
"update_time",
|
|
315809
|
+
"updateTimestamp",
|
|
315810
|
+
"lastUpdateTimestamp",
|
|
315811
|
+
"completed",
|
|
315812
|
+
"completedAt",
|
|
315813
|
+
"completed_at",
|
|
315814
|
+
"completeTime",
|
|
315815
|
+
"complete_time",
|
|
315816
|
+
"completionTime",
|
|
315817
|
+
"successTime"
|
|
315818
|
+
];
|
|
315819
|
+
function fingerprintEvidenceValue(value) {
|
|
315820
|
+
if (value === undefined || value === null) {
|
|
315821
|
+
return value;
|
|
315822
|
+
}
|
|
315823
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
315824
|
+
return value;
|
|
315825
|
+
}
|
|
315826
|
+
if (typeof value === "bigint") {
|
|
315827
|
+
return value.toString();
|
|
315828
|
+
}
|
|
315829
|
+
try {
|
|
315830
|
+
return JSON.stringify(value);
|
|
315831
|
+
} catch {
|
|
315832
|
+
return String(value);
|
|
315833
|
+
}
|
|
315834
|
+
}
|
|
315835
|
+
function venueLifecycleEvidence(transaction) {
|
|
315836
|
+
const record = asRecord(transaction);
|
|
315837
|
+
const info = asRecord(record?.info);
|
|
315838
|
+
const evidence = [];
|
|
315839
|
+
for (const [scope, source] of [
|
|
315840
|
+
["transaction", record],
|
|
315841
|
+
["info", info]
|
|
315842
|
+
]) {
|
|
315843
|
+
for (const field of VENUE_LIFECYCLE_EVIDENCE_FIELDS) {
|
|
315844
|
+
const value = source?.[field];
|
|
315845
|
+
if (value !== undefined && value !== null) {
|
|
315846
|
+
evidence.push([scope, field, fingerprintEvidenceValue(value)]);
|
|
315847
|
+
}
|
|
315848
|
+
}
|
|
315849
|
+
}
|
|
315850
|
+
return evidence;
|
|
315851
|
+
}
|
|
315852
|
+
function observationFingerprint(observation) {
|
|
315853
|
+
const { normalized, transaction } = observation;
|
|
315854
|
+
return JSON.stringify({
|
|
315855
|
+
status: normalized.status ?? null,
|
|
315856
|
+
txid: normalized.txid ?? null,
|
|
315857
|
+
amount: normalized.amount ?? null,
|
|
315858
|
+
feeAmount: normalized.feeAmount ?? null,
|
|
315859
|
+
feeCurrency: normalized.feeCurrency ?? null,
|
|
315860
|
+
address: normalized.address ?? null,
|
|
315861
|
+
network: normalized.network ?? null,
|
|
315862
|
+
exchangeTimestamp: normalized.exchangeTimestamp ?? null,
|
|
315863
|
+
venueLifecycleEvidence: venueLifecycleEvidence(transaction)
|
|
315864
|
+
});
|
|
315865
|
+
}
|
|
315866
|
+
|
|
315867
|
+
class WithdrawalObservationTracker {
|
|
315868
|
+
#fingerprints = new Map;
|
|
315869
|
+
#maxEntries;
|
|
315870
|
+
#missingIdSequence = 0n;
|
|
315871
|
+
constructor(options) {
|
|
315872
|
+
const maxEntries = options?.maxEntries ?? DEFAULT_WITHDRAWAL_OBSERVATION_TRACKER_MAX_ENTRIES;
|
|
315873
|
+
this.#maxEntries = Number.isFinite(maxEntries) ? Math.max(1, Math.floor(maxEntries)) : DEFAULT_WITHDRAWAL_OBSERVATION_TRACKER_MAX_ENTRIES;
|
|
315874
|
+
}
|
|
315875
|
+
shouldArchive(observation) {
|
|
315876
|
+
const externalId = observation.normalized.externalId?.trim();
|
|
315877
|
+
const identity = JSON.stringify([
|
|
315878
|
+
observation.exchange.trim().toLowerCase() || "unknown",
|
|
315879
|
+
observation.accountSelector?.trim() || "unknown",
|
|
315880
|
+
observation.assetSymbol?.trim().toUpperCase() || "unknown",
|
|
315881
|
+
externalId || `missing:${++this.#missingIdSequence}`
|
|
315882
|
+
]);
|
|
315883
|
+
const fingerprint = observationFingerprint(observation);
|
|
315884
|
+
if (this.#fingerprints.get(identity) === fingerprint) {
|
|
315885
|
+
return false;
|
|
315886
|
+
}
|
|
315887
|
+
this.#fingerprints.delete(identity);
|
|
315888
|
+
this.#fingerprints.set(identity, fingerprint);
|
|
315889
|
+
while (this.#fingerprints.size > this.#maxEntries) {
|
|
315890
|
+
const oldest = this.#fingerprints.keys().next().value;
|
|
315891
|
+
if (oldest === undefined)
|
|
315892
|
+
break;
|
|
315893
|
+
this.#fingerprints.delete(oldest);
|
|
315894
|
+
}
|
|
315895
|
+
return true;
|
|
315896
|
+
}
|
|
315897
|
+
getSize() {
|
|
315898
|
+
return this.#fingerprints.size;
|
|
315899
|
+
}
|
|
315900
|
+
}
|
|
315605
315901
|
// src/helpers/broker-execution-archive/writer.ts
|
|
315606
315902
|
var import_api_logs2 = __toESM(require_src7(), 1);
|
|
315607
315903
|
import { request as httpRequest2 } from "node:http";
|
|
@@ -315620,6 +315916,15 @@ function isMarketArchiveTable(table) {
|
|
|
315620
315916
|
}
|
|
315621
315917
|
|
|
315622
315918
|
// src/helpers/broker-execution-archive/writer.ts
|
|
315919
|
+
var BROKER_EXECUTION_ARCHIVE_TABLES = new Set([
|
|
315920
|
+
"broker_execution.order_events",
|
|
315921
|
+
"broker_execution.market_metadata_snapshots",
|
|
315922
|
+
"broker_execution.transfer_events",
|
|
315923
|
+
"broker_execution.fill_events"
|
|
315924
|
+
]);
|
|
315925
|
+
function isBrokerExecutionArchiveTable(table) {
|
|
315926
|
+
return BROKER_EXECUTION_ARCHIVE_TABLES.has(table);
|
|
315927
|
+
}
|
|
315623
315928
|
var DEFAULT_MAX_QUEUE_SIZE = 1e4;
|
|
315624
315929
|
var DEFAULT_BATCH_SIZE = 10;
|
|
315625
315930
|
var DEFAULT_FLUSH_INTERVAL_MS = 1000;
|
|
@@ -315704,6 +316009,9 @@ class BrokerExecutionArchiver {
|
|
|
315704
316009
|
canPersistMarketMetadataSnapshot() {
|
|
315705
316010
|
return this.isEnabled() && (Boolean(this.otelLogs?.isOtelEnabled()) || Boolean(this.forwarderUrl));
|
|
315706
316011
|
}
|
|
316012
|
+
canPersistAccountBalanceSnapshots() {
|
|
316013
|
+
return this.isEnabled() && Boolean(this.forwarderUrl);
|
|
316014
|
+
}
|
|
315707
316015
|
enqueue(row) {
|
|
315708
316016
|
if (!this.enabled || this.closed || !this.otelLogs && !this.forwarderUrl) {
|
|
315709
316017
|
return;
|
|
@@ -315715,6 +316023,9 @@ class BrokerExecutionArchiver {
|
|
|
315715
316023
|
}
|
|
315716
316024
|
return;
|
|
315717
316025
|
}
|
|
316026
|
+
if (row.table === "broker_account.balance_snapshots" && !this.forwarderUrl) {
|
|
316027
|
+
return;
|
|
316028
|
+
}
|
|
315718
316029
|
if (this.queue.length >= this.maxQueueSize) {
|
|
315719
316030
|
this.queue.shift();
|
|
315720
316031
|
this.stats.shed += 1;
|
|
@@ -315793,7 +316104,7 @@ class BrokerExecutionArchiver {
|
|
|
315793
316104
|
return true;
|
|
315794
316105
|
}
|
|
315795
316106
|
for (const entry of batch) {
|
|
315796
|
-
if (
|
|
316107
|
+
if (isBrokerExecutionArchiveTable(entry.table)) {
|
|
315797
316108
|
this.emitOtelLog(entry);
|
|
315798
316109
|
}
|
|
315799
316110
|
}
|
|
@@ -315929,8 +316240,138 @@ function parsePositiveInt(value, fallback) {
|
|
|
315929
316240
|
const parsed = Number.parseInt(value, 10);
|
|
315930
316241
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
315931
316242
|
}
|
|
315932
|
-
// src/helpers/
|
|
316243
|
+
// src/helpers/account-balance-archive-poller.ts
|
|
315933
316244
|
var DEFAULT_CONFIG = {
|
|
316245
|
+
pollIntervalMs: 60000
|
|
316246
|
+
};
|
|
316247
|
+
var metricLabels = (target) => ({
|
|
316248
|
+
exchange: target.exchangeId,
|
|
316249
|
+
account_selector: target.account.label,
|
|
316250
|
+
balance_scope: ACCOUNT_BALANCE_SCOPE
|
|
316251
|
+
});
|
|
316252
|
+
|
|
316253
|
+
class AccountBalanceArchivePoller {
|
|
316254
|
+
params;
|
|
316255
|
+
#timer = null;
|
|
316256
|
+
#stopped = false;
|
|
316257
|
+
#running = null;
|
|
316258
|
+
#lastSuccessMs = new Map;
|
|
316259
|
+
#config;
|
|
316260
|
+
constructor(params) {
|
|
316261
|
+
this.params = params;
|
|
316262
|
+
this.#config = { ...DEFAULT_CONFIG, ...params.config };
|
|
316263
|
+
}
|
|
316264
|
+
start() {
|
|
316265
|
+
if (this.#timer || this.#stopped || !this.params.archiver.canPersistAccountBalanceSnapshots()) {
|
|
316266
|
+
return;
|
|
316267
|
+
}
|
|
316268
|
+
log.info("\uD83D\uDCB0 Account balance archive poller started", {
|
|
316269
|
+
balanceScope: ACCOUNT_BALANCE_SCOPE
|
|
316270
|
+
});
|
|
316271
|
+
this.#schedule(0);
|
|
316272
|
+
}
|
|
316273
|
+
async stop() {
|
|
316274
|
+
this.#stopped = true;
|
|
316275
|
+
if (this.#timer) {
|
|
316276
|
+
clearTimeout(this.#timer);
|
|
316277
|
+
this.#timer = null;
|
|
316278
|
+
}
|
|
316279
|
+
await this.#running;
|
|
316280
|
+
}
|
|
316281
|
+
async pollAllOnce() {
|
|
316282
|
+
if (this.#stopped || this.#running || !this.params.archiver.canPersistAccountBalanceSnapshots()) {
|
|
316283
|
+
return false;
|
|
316284
|
+
}
|
|
316285
|
+
this.#running = this.#pollAllSequentially();
|
|
316286
|
+
try {
|
|
316287
|
+
return await this.#running;
|
|
316288
|
+
} finally {
|
|
316289
|
+
this.#running = null;
|
|
316290
|
+
}
|
|
316291
|
+
}
|
|
316292
|
+
#targets() {
|
|
316293
|
+
const targets = [];
|
|
316294
|
+
for (const [exchangeId, pool] of Object.entries(this.params.brokers)) {
|
|
316295
|
+
for (const account of [pool.primary, ...pool.secondaryBrokers]) {
|
|
316296
|
+
targets.push({ exchangeId, account });
|
|
316297
|
+
}
|
|
316298
|
+
}
|
|
316299
|
+
return targets;
|
|
316300
|
+
}
|
|
316301
|
+
async#pollAllSequentially() {
|
|
316302
|
+
for (const target of this.#targets()) {
|
|
316303
|
+
if (this.#stopped) {
|
|
316304
|
+
break;
|
|
316305
|
+
}
|
|
316306
|
+
await this.#pollOne(target);
|
|
316307
|
+
}
|
|
316308
|
+
return true;
|
|
316309
|
+
}
|
|
316310
|
+
async#pollOne(target) {
|
|
316311
|
+
const labels = metricLabels(target);
|
|
316312
|
+
this.params.metrics?.recordCounter("cex_account_balance_poll_attempts_total", 1, labels);
|
|
316313
|
+
try {
|
|
316314
|
+
const exchange = target.account.exchange;
|
|
316315
|
+
const balance = await exchange.fetchBalance({
|
|
316316
|
+
type: ACCOUNT_BALANCE_SCOPE
|
|
316317
|
+
});
|
|
316318
|
+
const observedAt = new Date;
|
|
316319
|
+
const normalized = normalizeCcxtBalanceForArchive(balance);
|
|
316320
|
+
this.params.archiver.enqueue(buildAccountBalanceSnapshotRow({
|
|
316321
|
+
tags: buildCommonArchiveTags({
|
|
316322
|
+
deploymentId: this.params.archiver.getDeploymentId(),
|
|
316323
|
+
accountSelector: target.account.label,
|
|
316324
|
+
exchange: target.exchangeId,
|
|
316325
|
+
brokerObservedTimestamp: observedAt.toISOString()
|
|
316326
|
+
}),
|
|
316327
|
+
balance: normalized
|
|
316328
|
+
}));
|
|
316329
|
+
const successMs = Date.now();
|
|
316330
|
+
this.#lastSuccessMs.set(this.#targetKey(target), successMs);
|
|
316331
|
+
this.params.metrics?.recordCounter("cex_account_balance_poll_successes_total", 1, labels);
|
|
316332
|
+
this.params.metrics?.recordGauge("cex_account_balance_poll_last_success_timestamp_seconds", Math.floor(successMs / 1000), labels);
|
|
316333
|
+
this.#recordFreshness(labels, successMs, successMs);
|
|
316334
|
+
} catch (error) {
|
|
316335
|
+
this.params.metrics?.recordCounter("cex_account_balance_poll_failures_total", 1, labels);
|
|
316336
|
+
const now3 = Date.now();
|
|
316337
|
+
const lastSuccess = this.#lastSuccessMs.get(this.#targetKey(target));
|
|
316338
|
+
if (lastSuccess !== undefined) {
|
|
316339
|
+
this.#recordFreshness(labels, lastSuccess, now3);
|
|
316340
|
+
}
|
|
316341
|
+
log.warn("Account balance archive poll failed", {
|
|
316342
|
+
exchange: target.exchangeId,
|
|
316343
|
+
account: target.account.label,
|
|
316344
|
+
balanceScope: ACCOUNT_BALANCE_SCOPE,
|
|
316345
|
+
errorType: error instanceof Error ? error.name : "unknown"
|
|
316346
|
+
});
|
|
316347
|
+
}
|
|
316348
|
+
}
|
|
316349
|
+
#recordFreshness(labels, lastSuccessMs, nowMs) {
|
|
316350
|
+
this.params.metrics?.recordGauge("cex_account_balance_poll_freshness_seconds", Math.max(0, (nowMs - lastSuccessMs) / 1000), labels);
|
|
316351
|
+
}
|
|
316352
|
+
#targetKey(target) {
|
|
316353
|
+
return `${target.exchangeId}|${target.account.label}|${ACCOUNT_BALANCE_SCOPE}`;
|
|
316354
|
+
}
|
|
316355
|
+
#schedule(delayMs) {
|
|
316356
|
+
this.#timer = setTimeout(() => void this.#tick(), delayMs);
|
|
316357
|
+
this.#timer.unref?.();
|
|
316358
|
+
}
|
|
316359
|
+
async#tick() {
|
|
316360
|
+
this.#timer = null;
|
|
316361
|
+
try {
|
|
316362
|
+
await this.pollAllOnce();
|
|
316363
|
+
} catch (error) {
|
|
316364
|
+
log.error("Account balance archive poller tick failed", error);
|
|
316365
|
+
} finally {
|
|
316366
|
+
if (!this.#stopped) {
|
|
316367
|
+
this.#schedule(this.#config.pollIntervalMs);
|
|
316368
|
+
}
|
|
316369
|
+
}
|
|
316370
|
+
}
|
|
316371
|
+
}
|
|
316372
|
+
|
|
316373
|
+
// src/helpers/fill-archive-poller.ts
|
|
316374
|
+
var DEFAULT_CONFIG2 = {
|
|
315934
316375
|
pollIntervalMs: 60000,
|
|
315935
316376
|
lookbackMs: 24 * 60 * 60 * 1000,
|
|
315936
316377
|
tradesLimit: 500
|
|
@@ -315955,7 +316396,7 @@ class FillArchivePoller {
|
|
|
315955
316396
|
#config;
|
|
315956
316397
|
constructor(params) {
|
|
315957
316398
|
this.params = params;
|
|
315958
|
-
this.#config = { ...
|
|
316399
|
+
this.#config = { ...DEFAULT_CONFIG2, ...params.config };
|
|
315959
316400
|
}
|
|
315960
316401
|
start() {
|
|
315961
316402
|
if (this.#timer || this.#stopped) {
|
|
@@ -331688,6 +332129,13 @@ async function handleTreasuryCall(ctx) {
|
|
|
331688
332129
|
}, null);
|
|
331689
332130
|
}
|
|
331690
332131
|
const result = await fn.apply(broker, argsArray);
|
|
332132
|
+
if (callValue.functionName === "fetchWithdrawals") {
|
|
332133
|
+
archiveWithdrawalObservationsInBackground(ctx.brokerArchiver, ctx.withdrawalObservationTracker, {
|
|
332134
|
+
exchange: ctx.normalizedCex,
|
|
332135
|
+
accountSelector: ctx.selectedBrokerAccount?.label,
|
|
332136
|
+
transactions: result
|
|
332137
|
+
});
|
|
332138
|
+
}
|
|
331691
332139
|
ctx.wrappedCallback(null, {
|
|
331692
332140
|
proof: ctx.verity.proof,
|
|
331693
332141
|
result: JSON.stringify(result)
|
|
@@ -331866,6 +332314,7 @@ function createExecuteActionHandler(deps) {
|
|
|
331866
332314
|
brokerArchiver,
|
|
331867
332315
|
orderActivityTracker
|
|
331868
332316
|
} = deps;
|
|
332317
|
+
const withdrawalObservationTracker = deps.withdrawalObservationTracker ?? new WithdrawalObservationTracker;
|
|
331869
332318
|
return async (call, callback) => {
|
|
331870
332319
|
const startTime = Date.now();
|
|
331871
332320
|
const { action: rawAction, cex: cex3, symbol: symbol2 } = call.request;
|
|
@@ -331944,7 +332393,8 @@ function createExecuteActionHandler(deps) {
|
|
|
331944
332393
|
verityProverUrl,
|
|
331945
332394
|
otelMetrics,
|
|
331946
332395
|
brokerArchiver,
|
|
331947
|
-
orderActivityTracker
|
|
332396
|
+
orderActivityTracker,
|
|
332397
|
+
withdrawalObservationTracker
|
|
331948
332398
|
};
|
|
331949
332399
|
if (action === Action.Call) {
|
|
331950
332400
|
const handled = await handleOrderBookCall(preludeCtx);
|
|
@@ -331979,6 +332429,7 @@ import { createHmac } from "node:crypto";
|
|
|
331979
332429
|
var BINANCE_SPOT_WS_API_URL = "wss://ws-api.binance.com:443/ws-api/v3";
|
|
331980
332430
|
var DEFAULT_BINANCE_USER_DATA_MAX_BUFFERED_EVENTS = 16;
|
|
331981
332431
|
var createWebSocket = (url3) => new wrapper_default(url3);
|
|
332432
|
+
var userDataRequestCounter = 0;
|
|
331982
332433
|
function getExchangeString(exchange, key) {
|
|
331983
332434
|
const value = exchange[key];
|
|
331984
332435
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -332085,7 +332536,7 @@ class BinanceSpotUserDataStream {
|
|
|
332085
332536
|
exchange;
|
|
332086
332537
|
ws;
|
|
332087
332538
|
secretValues;
|
|
332088
|
-
requestId = `user-data-${Date.now()}-${
|
|
332539
|
+
requestId = `user-data-${Date.now()}-${userDataRequestCounter++}`;
|
|
332089
332540
|
maxBufferedEvents;
|
|
332090
332541
|
queue = [];
|
|
332091
332542
|
waiters = [];
|
|
@@ -332163,6 +332614,12 @@ class BinanceSpotUserDataStream {
|
|
|
332163
332614
|
this.subscriptionId = message.result?.subscriptionId ?? null;
|
|
332164
332615
|
return;
|
|
332165
332616
|
}
|
|
332617
|
+
if ("status" in message && typeof message.status === "number" && message.status !== 200) {
|
|
332618
|
+
const errorMessage = message.error?.msg ?? message.error?.message ?? `Binance user-data request failed with status ${message.status}`;
|
|
332619
|
+
const errorCode = message.error?.code;
|
|
332620
|
+
this.fail(new Error(typeof errorCode === "number" ? `${errorMessage} (code ${errorCode})` : errorMessage));
|
|
332621
|
+
return;
|
|
332622
|
+
}
|
|
332166
332623
|
if (!("event" in message) || !message.event) {
|
|
332167
332624
|
return;
|
|
332168
332625
|
}
|
|
@@ -333533,7 +333990,7 @@ var CEX_BROKER_PACKAGE_DEFINITION = protoLoader.fromJSON(node_descriptor_default
|
|
|
333533
333990
|
// src/server.ts
|
|
333534
333991
|
var grpcObj = grpc14.loadPackageDefinition(CEX_BROKER_PACKAGE_DEFINITION);
|
|
333535
333992
|
var cexNode = grpcObj.cex_broker;
|
|
333536
|
-
function getServer(policy, brokers, whitelistIps, useVerity, verityProverUrl, otelMetrics, brokerArchiver, orderActivityTracker) {
|
|
333993
|
+
function getServer(policy, brokers, whitelistIps, useVerity, verityProverUrl, otelMetrics, brokerArchiver, orderActivityTracker, withdrawalObservationTracker) {
|
|
333537
333994
|
const server = new grpc14.Server;
|
|
333538
333995
|
server.addService(cexNode.cex_service.service, {
|
|
333539
333996
|
ExecuteAction: createExecuteActionHandler({
|
|
@@ -333544,7 +334001,8 @@ function getServer(policy, brokers, whitelistIps, useVerity, verityProverUrl, ot
|
|
|
333544
334001
|
verityProverUrl,
|
|
333545
334002
|
otelMetrics,
|
|
333546
334003
|
brokerArchiver,
|
|
333547
|
-
orderActivityTracker
|
|
334004
|
+
orderActivityTracker,
|
|
334005
|
+
withdrawalObservationTracker
|
|
333548
334006
|
}),
|
|
333549
334007
|
Subscribe: createSubscribeHandler({
|
|
333550
334008
|
brokers,
|
|
@@ -333690,7 +334148,9 @@ class CEXBroker {
|
|
|
333690
334148
|
brokerArchiver;
|
|
333691
334149
|
depositReconciler;
|
|
333692
334150
|
orderActivityTracker = new OrderActivityTracker;
|
|
334151
|
+
withdrawalObservationTracker = new WithdrawalObservationTracker;
|
|
333693
334152
|
fillArchivePoller;
|
|
334153
|
+
accountBalanceArchivePoller;
|
|
333694
334154
|
loadEnvConfig() {
|
|
333695
334155
|
log.info("\uD83D\uDD27 Loading CEX_BROKER_ environment variables:");
|
|
333696
334156
|
const configMap = {};
|
|
@@ -333835,6 +334295,10 @@ class CEXBroker {
|
|
|
333835
334295
|
this.fillArchivePoller.stop();
|
|
333836
334296
|
this.fillArchivePoller = undefined;
|
|
333837
334297
|
}
|
|
334298
|
+
if (this.accountBalanceArchivePoller) {
|
|
334299
|
+
await this.accountBalanceArchivePoller.stop();
|
|
334300
|
+
this.accountBalanceArchivePoller = undefined;
|
|
334301
|
+
}
|
|
333838
334302
|
if (this.server) {
|
|
333839
334303
|
await this.server.forceShutdown();
|
|
333840
334304
|
}
|
|
@@ -333860,11 +334324,15 @@ class CEXBroker {
|
|
|
333860
334324
|
this.fillArchivePoller.stop();
|
|
333861
334325
|
this.fillArchivePoller = undefined;
|
|
333862
334326
|
}
|
|
334327
|
+
if (this.accountBalanceArchivePoller) {
|
|
334328
|
+
await this.accountBalanceArchivePoller.stop();
|
|
334329
|
+
this.accountBalanceArchivePoller = undefined;
|
|
334330
|
+
}
|
|
333863
334331
|
log.info(`Running CEXBroker at ${new Date().toISOString()}`);
|
|
333864
334332
|
if (this.otelMetrics?.isOtelEnabled()) {
|
|
333865
334333
|
await this.otelMetrics.initialize();
|
|
333866
334334
|
}
|
|
333867
|
-
this.server = getServer(this.policy, this.brokers, this.whitelistIps, this.useVerity, this.#verityProverUrl, this.otelMetrics, this.brokerArchiver, this.orderActivityTracker);
|
|
334335
|
+
this.server = getServer(this.policy, this.brokers, this.whitelistIps, this.useVerity, this.#verityProverUrl, this.otelMetrics, this.brokerArchiver, this.orderActivityTracker, this.withdrawalObservationTracker);
|
|
333868
334336
|
this.server.bindAsync(`0.0.0.0:${this.port}`, grpc15.ServerCredentials.createInsecure(), (err2, port) => {
|
|
333869
334337
|
if (err2) {
|
|
333870
334338
|
log.error(err2);
|
|
@@ -333888,6 +334356,14 @@ class CEXBroker {
|
|
|
333888
334356
|
});
|
|
333889
334357
|
this.fillArchivePoller.start();
|
|
333890
334358
|
}
|
|
334359
|
+
if (this.brokerArchiver?.canPersistAccountBalanceSnapshots()) {
|
|
334360
|
+
this.accountBalanceArchivePoller = new AccountBalanceArchivePoller({
|
|
334361
|
+
brokers: this.brokers,
|
|
334362
|
+
archiver: this.brokerArchiver,
|
|
334363
|
+
metrics: this.otelMetrics
|
|
334364
|
+
});
|
|
334365
|
+
this.accountBalanceArchivePoller.start();
|
|
334366
|
+
}
|
|
333891
334367
|
return this;
|
|
333892
334368
|
}
|
|
333893
334369
|
}
|
|
@@ -3,7 +3,7 @@ import type { Metadata } from "@grpc/grpc-js";
|
|
|
3
3
|
import type { Exchange } from "@usherlabs/ccxt";
|
|
4
4
|
import type { z } from "zod";
|
|
5
5
|
import type { BrokerAccount, BrokerPoolEntry } from "../../helpers";
|
|
6
|
-
import type { BrokerExecutionArchiver } from "../../helpers/broker-execution-archive";
|
|
6
|
+
import type { BrokerExecutionArchiver, WithdrawalObservationTracker } from "../../helpers/broker-execution-archive";
|
|
7
7
|
import type { Action as ActionType } from "../../helpers/constants";
|
|
8
8
|
import type { OrderActivityTracker } from "../../helpers/order-activity-tracker";
|
|
9
9
|
import type { OtelMetrics } from "../../helpers/otel";
|
|
@@ -31,6 +31,7 @@ export type ExecuteActionContext = {
|
|
|
31
31
|
otelMetrics?: OtelMetrics;
|
|
32
32
|
brokerArchiver?: BrokerExecutionArchiver;
|
|
33
33
|
orderActivityTracker?: OrderActivityTracker;
|
|
34
|
+
withdrawalObservationTracker: WithdrawalObservationTracker;
|
|
34
35
|
};
|
|
35
36
|
export declare function requireSymbol(ctx: ExecuteActionContext, message?: string): ctx is ExecuteActionContext & {
|
|
36
37
|
symbol: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as grpc from "@grpc/grpc-js";
|
|
2
2
|
import { type BrokerPoolEntry } from "../../helpers/broker";
|
|
3
|
-
import type
|
|
3
|
+
import { type BrokerExecutionArchiver, WithdrawalObservationTracker } from "../../helpers/broker-execution-archive";
|
|
4
4
|
import type { OrderActivityTracker } from "../../helpers/order-activity-tracker";
|
|
5
5
|
import type { OtelMetrics } from "../../helpers/otel";
|
|
6
6
|
import type { PolicyConfig } from "../../types";
|
|
@@ -14,5 +14,6 @@ export type ExecuteActionDeps = {
|
|
|
14
14
|
otelMetrics?: OtelMetrics;
|
|
15
15
|
brokerArchiver?: BrokerExecutionArchiver;
|
|
16
16
|
orderActivityTracker?: OrderActivityTracker;
|
|
17
|
+
withdrawalObservationTracker?: WithdrawalObservationTracker;
|
|
17
18
|
};
|
|
18
19
|
export declare function createExecuteActionHandler(deps: ExecuteActionDeps): (call: grpc.ServerUnaryCall<ActionRequest, ActionResponse>, callback: grpc.sendUnaryData<ActionResponse>) => Promise<void>;
|