@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.
@@ -316534,8 +316534,215 @@ class AccountBalanceArchivePoller {
316534
316534
  }
316535
316535
  }
316536
316536
 
316537
- // src/helpers/fill-archive-poller.ts
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 = { ...DEFAULT_CONFIG2, ...params.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 status4 = normalizeDepositStatus(depositField(deposit, ["status", "state"]));
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: status4,
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: status4,
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 passiveErrorCode = classifyPassiveOrderError(error48);
331837
+ const stableErrorCode = classifyPassiveOrderError(error48);
331618
331838
  return rejectWithGrpcError(ctx, error48, {
331619
- message: `${passiveErrorCode}: ${sanitizeErrorDetail(error48)}`,
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 declare function classifyPassiveOrderError(error: unknown): PassiveOrderErrorCode;
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_