@usherlabs/cex-broker 0.2.34 → 0.2.35
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 +242 -7
- package/dist/helpers/deposit-archive-poller.d.ts +32 -0
- package/dist/helpers/passive-order.d.ts +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +243 -8
- package/dist/index.js.map +9 -8
- package/package.json +1 -1
package/dist/commands/cli.js
CHANGED
|
@@ -316534,8 +316534,215 @@ class AccountBalanceArchivePoller {
|
|
|
316534
316534
|
}
|
|
316535
316535
|
}
|
|
316536
316536
|
|
|
316537
|
-
// src/helpers/
|
|
316537
|
+
// src/helpers/deposit-archive-poller.ts
|
|
316538
316538
|
var DEFAULT_CONFIG2 = {
|
|
316539
|
+
pollIntervalMs: 60000,
|
|
316540
|
+
lookbackMs: 24 * 60 * 60 * 1000,
|
|
316541
|
+
depositsLimit: 50
|
|
316542
|
+
};
|
|
316543
|
+
var ALL_CURRENCIES_CODE = "*";
|
|
316544
|
+
function nextDepositCursor(deposits, currentSince, depositsLimit) {
|
|
316545
|
+
if (deposits.length >= depositsLimit) {
|
|
316546
|
+
return currentSince;
|
|
316547
|
+
}
|
|
316548
|
+
let next = currentSince;
|
|
316549
|
+
let oldestPending = Number.POSITIVE_INFINITY;
|
|
316550
|
+
for (const deposit of deposits) {
|
|
316551
|
+
const record = asRecord(deposit);
|
|
316552
|
+
if (!record) {
|
|
316553
|
+
return currentSince;
|
|
316554
|
+
}
|
|
316555
|
+
const observedAt = depositField(record, [
|
|
316556
|
+
"timestamp",
|
|
316557
|
+
"creditedAt",
|
|
316558
|
+
"credited_at",
|
|
316559
|
+
"updated",
|
|
316560
|
+
"updatedAt",
|
|
316561
|
+
"datetime"
|
|
316562
|
+
]);
|
|
316563
|
+
const timestamp = typeof observedAt === "number" ? observedAt : typeof observedAt === "string" ? Date.parse(observedAt) : Number.NaN;
|
|
316564
|
+
const status = normalizeDepositStatus(depositField(record, ["status", "state"]));
|
|
316565
|
+
if (!Number.isFinite(timestamp)) {
|
|
316566
|
+
if (status === "pending") {
|
|
316567
|
+
return currentSince;
|
|
316568
|
+
}
|
|
316569
|
+
continue;
|
|
316570
|
+
}
|
|
316571
|
+
if (status === "pending") {
|
|
316572
|
+
oldestPending = Math.min(oldestPending, timestamp);
|
|
316573
|
+
} else {
|
|
316574
|
+
next = Math.max(next, timestamp + 1);
|
|
316575
|
+
}
|
|
316576
|
+
}
|
|
316577
|
+
return Number.isFinite(oldestPending) ? Math.max(currentSince, oldestPending) : next;
|
|
316578
|
+
}
|
|
316579
|
+
|
|
316580
|
+
class DepositArchivePoller {
|
|
316581
|
+
params;
|
|
316582
|
+
#timer = null;
|
|
316583
|
+
#stopped = false;
|
|
316584
|
+
#running = null;
|
|
316585
|
+
#cursors = new Map;
|
|
316586
|
+
#unsupportedLogged = new Set;
|
|
316587
|
+
#config;
|
|
316588
|
+
constructor(params) {
|
|
316589
|
+
this.params = params;
|
|
316590
|
+
this.#config = { ...DEFAULT_CONFIG2, ...params.config };
|
|
316591
|
+
}
|
|
316592
|
+
start() {
|
|
316593
|
+
if (this.#timer || this.#stopped || !this.params.archiver.isEnabled()) {
|
|
316594
|
+
return;
|
|
316595
|
+
}
|
|
316596
|
+
log.info("\uD83D\uDCE5 Deposit archive poller started");
|
|
316597
|
+
this.#schedule(0);
|
|
316598
|
+
}
|
|
316599
|
+
async stop() {
|
|
316600
|
+
this.#stopped = true;
|
|
316601
|
+
if (this.#timer) {
|
|
316602
|
+
clearTimeout(this.#timer);
|
|
316603
|
+
this.#timer = null;
|
|
316604
|
+
}
|
|
316605
|
+
await this.#running;
|
|
316606
|
+
}
|
|
316607
|
+
async pollAllOnce() {
|
|
316608
|
+
if (this.#stopped || this.#running || !this.params.archiver.isEnabled()) {
|
|
316609
|
+
return false;
|
|
316610
|
+
}
|
|
316611
|
+
this.#running = this.#pollAllSequentially();
|
|
316612
|
+
try {
|
|
316613
|
+
return await this.#running;
|
|
316614
|
+
} finally {
|
|
316615
|
+
this.#running = null;
|
|
316616
|
+
}
|
|
316617
|
+
}
|
|
316618
|
+
#targets() {
|
|
316619
|
+
const targets = [];
|
|
316620
|
+
for (const [exchangeId, pool] of Object.entries(this.params.brokers)) {
|
|
316621
|
+
for (const account of [pool.primary, ...pool.secondaryBrokers]) {
|
|
316622
|
+
targets.push({
|
|
316623
|
+
exchangeId,
|
|
316624
|
+
account,
|
|
316625
|
+
code: ALL_CURRENCIES_CODE
|
|
316626
|
+
});
|
|
316627
|
+
}
|
|
316628
|
+
}
|
|
316629
|
+
return targets;
|
|
316630
|
+
}
|
|
316631
|
+
async#pollAllSequentially() {
|
|
316632
|
+
for (const target of this.#targets()) {
|
|
316633
|
+
if (this.#stopped) {
|
|
316634
|
+
break;
|
|
316635
|
+
}
|
|
316636
|
+
await this.#pollOne(target);
|
|
316637
|
+
}
|
|
316638
|
+
return true;
|
|
316639
|
+
}
|
|
316640
|
+
async#pollOne(target) {
|
|
316641
|
+
const exchange = target.account.exchange;
|
|
316642
|
+
const key = this.#targetKey(target);
|
|
316643
|
+
if (typeof exchange.fetchDeposits !== "function" || exchange.has?.fetchDeposits === false) {
|
|
316644
|
+
if (!this.#unsupportedLogged.has(key)) {
|
|
316645
|
+
this.#unsupportedLogged.add(key);
|
|
316646
|
+
log.info("Deposit archive poll skipped: fetchDeposits unsupported", {
|
|
316647
|
+
exchange: target.exchangeId,
|
|
316648
|
+
account: target.account.label
|
|
316649
|
+
});
|
|
316650
|
+
}
|
|
316651
|
+
return;
|
|
316652
|
+
}
|
|
316653
|
+
const since = this.#cursors.get(key) ?? Date.now() - this.#config.lookbackMs;
|
|
316654
|
+
let deposits;
|
|
316655
|
+
try {
|
|
316656
|
+
deposits = await exchange.fetchDeposits(undefined, since, this.#config.depositsLimit);
|
|
316657
|
+
} catch (error) {
|
|
316658
|
+
this.params.metrics?.recordCounter("cex_deposit_poller_errors_total", 1, { exchange: target.exchangeId });
|
|
316659
|
+
log.warn("Deposit archive poll failed", {
|
|
316660
|
+
exchange: target.exchangeId,
|
|
316661
|
+
account: target.account.label,
|
|
316662
|
+
error
|
|
316663
|
+
});
|
|
316664
|
+
return;
|
|
316665
|
+
}
|
|
316666
|
+
if (!Array.isArray(deposits) || deposits.length === 0) {
|
|
316667
|
+
return;
|
|
316668
|
+
}
|
|
316669
|
+
let archived = 0;
|
|
316670
|
+
for (const deposit of deposits) {
|
|
316671
|
+
const record = asRecord(deposit);
|
|
316672
|
+
if (!record) {
|
|
316673
|
+
continue;
|
|
316674
|
+
}
|
|
316675
|
+
const assetSymbol = depositField(record, ["currency", "code", "asset"]);
|
|
316676
|
+
const amount = depositField(record, ["amount"]);
|
|
316677
|
+
const address = depositField(record, [
|
|
316678
|
+
"address",
|
|
316679
|
+
"recipientAddress",
|
|
316680
|
+
"to",
|
|
316681
|
+
"destination"
|
|
316682
|
+
]);
|
|
316683
|
+
const txid = depositField(record, ["txid", "txId", "tx_hash", "txHash"]);
|
|
316684
|
+
const network = depositField(record, ["network", "chain"]);
|
|
316685
|
+
const archiveStatus = normalizeCcxtTransactionForArchive(record).status;
|
|
316686
|
+
const creditedAt = depositField(record, [
|
|
316687
|
+
"creditedAt",
|
|
316688
|
+
"credited_at",
|
|
316689
|
+
"updated",
|
|
316690
|
+
"updatedAt",
|
|
316691
|
+
"timestamp",
|
|
316692
|
+
"datetime"
|
|
316693
|
+
]);
|
|
316694
|
+
const depositTxid = txid === undefined ? undefined : String(txid);
|
|
316695
|
+
this.params.archiver.enqueue(buildTransferEventArchiveRow({
|
|
316696
|
+
tags: buildCommonArchiveTags({
|
|
316697
|
+
deploymentId: this.params.archiver.getDeploymentId(),
|
|
316698
|
+
accountSelector: target.account.label,
|
|
316699
|
+
exchange: target.exchangeId,
|
|
316700
|
+
symbol: assetSymbol === undefined ? undefined : String(assetSymbol)
|
|
316701
|
+
}),
|
|
316702
|
+
transfer: {
|
|
316703
|
+
eventKind: "deposit",
|
|
316704
|
+
lifecycleAction: "observe_deposit",
|
|
316705
|
+
status: archiveStatus,
|
|
316706
|
+
amount: amount === undefined ? undefined : String(amount),
|
|
316707
|
+
address: address === undefined ? undefined : String(address),
|
|
316708
|
+
network: network === undefined ? undefined : String(network),
|
|
316709
|
+
externalId: depositTxid,
|
|
316710
|
+
txid: depositTxid,
|
|
316711
|
+
exchangeTimestamp: typeof creditedAt === "string" ? creditedAt : undefined,
|
|
316712
|
+
payload: record
|
|
316713
|
+
}
|
|
316714
|
+
}));
|
|
316715
|
+
archived += 1;
|
|
316716
|
+
}
|
|
316717
|
+
if (archived > 0) {
|
|
316718
|
+
this.params.metrics?.recordCounter("cex_deposit_poller_deposits_archived_total", archived, { exchange: target.exchangeId });
|
|
316719
|
+
}
|
|
316720
|
+
this.#cursors.set(key, nextDepositCursor(deposits, since, this.#config.depositsLimit));
|
|
316721
|
+
}
|
|
316722
|
+
#targetKey(target) {
|
|
316723
|
+
return `${target.exchangeId}|${target.account.label}|${target.code}`;
|
|
316724
|
+
}
|
|
316725
|
+
#schedule(delayMs) {
|
|
316726
|
+
this.#timer = setTimeout(() => void this.#tick(), delayMs);
|
|
316727
|
+
this.#timer.unref?.();
|
|
316728
|
+
}
|
|
316729
|
+
async#tick() {
|
|
316730
|
+
this.#timer = null;
|
|
316731
|
+
try {
|
|
316732
|
+
await this.pollAllOnce();
|
|
316733
|
+
} catch (error) {
|
|
316734
|
+
rethrowArchiveDurabilityError(error);
|
|
316735
|
+
log.error("Deposit archive poller tick failed", error);
|
|
316736
|
+
} finally {
|
|
316737
|
+
if (!this.#stopped) {
|
|
316738
|
+
this.#schedule(this.#config.pollIntervalMs);
|
|
316739
|
+
}
|
|
316740
|
+
}
|
|
316741
|
+
}
|
|
316742
|
+
}
|
|
316743
|
+
|
|
316744
|
+
// src/helpers/fill-archive-poller.ts
|
|
316745
|
+
var DEFAULT_CONFIG3 = {
|
|
316539
316746
|
pollIntervalMs: 60000,
|
|
316540
316747
|
lookbackMs: 24 * 60 * 60 * 1000,
|
|
316541
316748
|
tradesLimit: 500
|
|
@@ -316560,7 +316767,7 @@ class FillArchivePoller {
|
|
|
316560
316767
|
#config;
|
|
316561
316768
|
constructor(params) {
|
|
316562
316769
|
this.params = params;
|
|
316563
|
-
this.#config = { ...
|
|
316770
|
+
this.#config = { ...DEFAULT_CONFIG3, ...params.config };
|
|
316564
316771
|
}
|
|
316565
316772
|
start() {
|
|
316566
316773
|
if (this.#timer || this.#stopped) {
|
|
@@ -330786,6 +330993,12 @@ function parseActionPayload(schema, rawPayload, callback) {
|
|
|
330786
330993
|
// src/helpers/grpc/status.ts
|
|
330787
330994
|
var grpc2 = __toESM(require_src3(), 1);
|
|
330788
330995
|
function stableGrpcErrorCode(message) {
|
|
330996
|
+
if (message.startsWith("AuthenticationError:")) {
|
|
330997
|
+
return grpc2.status.UNAUTHENTICATED;
|
|
330998
|
+
}
|
|
330999
|
+
if (message.startsWith("InsufficientFunds:")) {
|
|
331000
|
+
return grpc2.status.FAILED_PRECONDITION;
|
|
331001
|
+
}
|
|
330789
331002
|
if (message.startsWith("venue_discovery_unavailable:")) {
|
|
330790
331003
|
return grpc2.status.UNIMPLEMENTED;
|
|
330791
331004
|
}
|
|
@@ -330936,7 +331149,8 @@ async function handleDeposit(ctx) {
|
|
|
330936
331149
|
message: `deposit_address_mismatch: expected address ${value.recipientAddress}, observed ${String(observedAddress)}`
|
|
330937
331150
|
}, null);
|
|
330938
331151
|
}
|
|
330939
|
-
const
|
|
331152
|
+
const responseStatus = normalizeDepositStatus(depositField(deposit, ["status", "state"]));
|
|
331153
|
+
const archiveStatus = normalizeCcxtTransactionForArchive(deposit).status;
|
|
330940
331154
|
const depositTxid = String(depositField(deposit, ["txid", "txId", "tx_hash", "txHash"]) ?? value.transactionHash);
|
|
330941
331155
|
const creditedAt = depositField(deposit, [
|
|
330942
331156
|
"creditedAt",
|
|
@@ -330953,7 +331167,7 @@ async function handleDeposit(ctx) {
|
|
|
330953
331167
|
transfer: {
|
|
330954
331168
|
eventKind: "deposit",
|
|
330955
331169
|
lifecycleAction: "observe_deposit",
|
|
330956
|
-
status:
|
|
331170
|
+
status: archiveStatus,
|
|
330957
331171
|
amount: observedAmount !== undefined ? String(observedAmount) : undefined,
|
|
330958
331172
|
address: String(observedAddress ?? value.recipientAddress),
|
|
330959
331173
|
network: depositNetwork?.exchangeNetworkId,
|
|
@@ -330967,7 +331181,7 @@ async function handleDeposit(ctx) {
|
|
|
330967
331181
|
return ctx.wrappedCallback(null, {
|
|
330968
331182
|
proof: ctx.verity.proof,
|
|
330969
331183
|
result: JSON.stringify({
|
|
330970
|
-
status:
|
|
331184
|
+
status: responseStatus,
|
|
330971
331185
|
exchange: normalizedCex,
|
|
330972
331186
|
accountSelector: selectedBrokerAccount?.label,
|
|
330973
331187
|
asset: symbol2,
|
|
@@ -331486,6 +331700,12 @@ function identifiesUnsupported(message) {
|
|
|
331486
331700
|
return /post[\s-]?only\b.*\b(?:not supported|unsupported|does not support)\b/.test(normalized) || /\b(?:not supported|unsupported|does not support)\b.*\bpost[\s-]?only\b/.test(normalized);
|
|
331487
331701
|
}
|
|
331488
331702
|
function classifyPassiveOrderError(error48) {
|
|
331703
|
+
if (error48 instanceof ccxt_default.InsufficientFunds) {
|
|
331704
|
+
return "InsufficientFunds";
|
|
331705
|
+
}
|
|
331706
|
+
if (error48 instanceof ccxt_default.AuthenticationError) {
|
|
331707
|
+
return "AuthenticationError";
|
|
331708
|
+
}
|
|
331489
331709
|
const message = getErrorMessage(error48);
|
|
331490
331710
|
if (error48 instanceof ccxt_default.OrderImmediatelyFillable || identifiesWouldCross(message)) {
|
|
331491
331711
|
return PASSIVE_ORDER_ERROR_CODES.wouldCross;
|
|
@@ -331614,9 +331834,9 @@ async function handleCreateOrder(ctx) {
|
|
|
331614
331834
|
emitOrderExecutionTelemetryInBackground(otelMetrics, failedCreateContext, undefined, error48);
|
|
331615
331835
|
archiveOrderExecutionInBackground(brokerArchiver, failedCreateContext, undefined, error48, { marketMetadataHash });
|
|
331616
331836
|
if (isPassiveOrder && submission === "in_flight") {
|
|
331617
|
-
const
|
|
331837
|
+
const stableErrorCode = classifyPassiveOrderError(error48);
|
|
331618
331838
|
return rejectWithGrpcError(ctx, error48, {
|
|
331619
|
-
message: `${
|
|
331839
|
+
message: `${stableErrorCode}: ${sanitizeErrorDetail(error48)}`,
|
|
331620
331840
|
preferStableMessageOnly: true
|
|
331621
331841
|
});
|
|
331622
331842
|
}
|
|
@@ -334516,6 +334736,7 @@ class CEXBroker {
|
|
|
334516
334736
|
orderActivityTracker = new OrderActivityTracker;
|
|
334517
334737
|
withdrawalObservationTracker = new WithdrawalObservationTracker;
|
|
334518
334738
|
fillArchivePoller;
|
|
334739
|
+
depositArchivePoller;
|
|
334519
334740
|
accountBalanceArchivePoller;
|
|
334520
334741
|
loadEnvConfig() {
|
|
334521
334742
|
log.info("\uD83D\uDD27 Loading CEX_BROKER_ environment variables:");
|
|
@@ -334661,6 +334882,10 @@ class CEXBroker {
|
|
|
334661
334882
|
this.fillArchivePoller.stop();
|
|
334662
334883
|
this.fillArchivePoller = undefined;
|
|
334663
334884
|
}
|
|
334885
|
+
if (this.depositArchivePoller) {
|
|
334886
|
+
await this.depositArchivePoller.stop();
|
|
334887
|
+
this.depositArchivePoller = undefined;
|
|
334888
|
+
}
|
|
334664
334889
|
if (this.accountBalanceArchivePoller) {
|
|
334665
334890
|
await this.accountBalanceArchivePoller.stop();
|
|
334666
334891
|
this.accountBalanceArchivePoller = undefined;
|
|
@@ -334690,6 +334915,10 @@ class CEXBroker {
|
|
|
334690
334915
|
this.fillArchivePoller.stop();
|
|
334691
334916
|
this.fillArchivePoller = undefined;
|
|
334692
334917
|
}
|
|
334918
|
+
if (this.depositArchivePoller) {
|
|
334919
|
+
await this.depositArchivePoller.stop();
|
|
334920
|
+
this.depositArchivePoller = undefined;
|
|
334921
|
+
}
|
|
334693
334922
|
if (this.accountBalanceArchivePoller) {
|
|
334694
334923
|
await this.accountBalanceArchivePoller.stop();
|
|
334695
334924
|
this.accountBalanceArchivePoller = undefined;
|
|
@@ -334721,6 +334950,12 @@ class CEXBroker {
|
|
|
334721
334950
|
metrics: this.otelMetrics
|
|
334722
334951
|
});
|
|
334723
334952
|
this.fillArchivePoller.start();
|
|
334953
|
+
this.depositArchivePoller = new DepositArchivePoller({
|
|
334954
|
+
brokers: this.brokers,
|
|
334955
|
+
archiver: this.brokerArchiver,
|
|
334956
|
+
metrics: this.otelMetrics
|
|
334957
|
+
});
|
|
334958
|
+
this.depositArchivePoller.start();
|
|
334724
334959
|
}
|
|
334725
334960
|
if (this.brokerArchiver?.canPersistAccountBalanceSnapshots()) {
|
|
334726
334961
|
this.accountBalanceArchivePoller = new AccountBalanceArchivePoller({
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { BrokerPoolEntry } from "./broker";
|
|
2
|
+
import { type BrokerExecutionArchiver } from "./broker-execution-archive";
|
|
3
|
+
import type { OtelMetrics } from "./otel";
|
|
4
|
+
export type DepositArchivePollerConfig = {
|
|
5
|
+
pollIntervalMs: number;
|
|
6
|
+
lookbackMs: number;
|
|
7
|
+
depositsLimit: number;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Advances the inclusive since-watermark only across a fully observed,
|
|
11
|
+
* terminal prefix of deposit history.
|
|
12
|
+
*/
|
|
13
|
+
export declare function nextDepositCursor(deposits: unknown[], currentSince: number, depositsLimit: number): number;
|
|
14
|
+
/**
|
|
15
|
+
* Broker-internal periodic capture of venue deposit history. It polls every
|
|
16
|
+
* configured account sequentially, using ccxt's unfiltered deposit-history
|
|
17
|
+
* surface so newly funded currencies do not depend on balance or market
|
|
18
|
+
* discovery.
|
|
19
|
+
*/
|
|
20
|
+
export declare class DepositArchivePoller {
|
|
21
|
+
#private;
|
|
22
|
+
private readonly params;
|
|
23
|
+
constructor(params: {
|
|
24
|
+
brokers: Record<string, BrokerPoolEntry>;
|
|
25
|
+
archiver: BrokerExecutionArchiver;
|
|
26
|
+
metrics?: OtelMetrics;
|
|
27
|
+
config?: Partial<DepositArchivePollerConfig>;
|
|
28
|
+
});
|
|
29
|
+
start(): void;
|
|
30
|
+
stop(): Promise<void>;
|
|
31
|
+
pollAllOnce(): Promise<boolean>;
|
|
32
|
+
}
|
|
@@ -4,4 +4,5 @@ export declare const PASSIVE_ORDER_ERROR_CODES: {
|
|
|
4
4
|
readonly wouldCross: "passive_order_would_cross";
|
|
5
5
|
};
|
|
6
6
|
export type PassiveOrderErrorCode = (typeof PASSIVE_ORDER_ERROR_CODES)[keyof typeof PASSIVE_ORDER_ERROR_CODES];
|
|
7
|
-
export
|
|
7
|
+
export type PassiveOrderSubmissionErrorCode = PassiveOrderErrorCode | "AuthenticationError" | "InsufficientFunds";
|
|
8
|
+
export declare function classifyPassiveOrderError(error: unknown): PassiveOrderSubmissionErrorCode;
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export default class CEXBroker {
|
|
|
16
16
|
private readonly orderActivityTracker;
|
|
17
17
|
private readonly withdrawalObservationTracker;
|
|
18
18
|
private fillArchivePoller?;
|
|
19
|
+
private depositArchivePoller?;
|
|
19
20
|
private accountBalanceArchivePoller?;
|
|
20
21
|
/**
|
|
21
22
|
* Loads environment variables prefixed with CEX_BROKER_
|