@usherlabs/cex-broker 0.2.16 → 0.2.18

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.
@@ -3,7 +3,8 @@ import type { PolicyConfig } from "../types";
3
3
  import { type BrokerAccount } from "./broker";
4
4
  export { authenticateRequest } from "./auth";
5
5
  export { applyCommonExchangeConfig, type BrokerAccount, BrokerAccountPreconditionError, type BrokerPoolEntry, createBroker, createBrokerPool, createPublicBroker, getCurrentBrokerSelector, resolveBrokerAccount, selectBroker, selectBrokerAccount, } from "./broker";
6
- export { australiaQuestionnaireSchema, registerBinanceTravelRuleWithdrawEndpoint, resolveTravelRuleDecision, type TravelRuleDecision, withdrawViaLocalEntity, } from "./travel-rule";
6
+ export { australiaDepositQuestionnaireSchema, australiaQuestionnaireSchema, getEnabledTravelRuleDepositConfig, registerBinanceTravelRuleDepositEndpoints, registerBinanceTravelRuleWithdrawEndpoint, resolveDepositOriginatorQuestionnaire, resolveTravelRuleDecision, type TravelRuleDecision, withdrawViaLocalEntity, } from "./travel-rule";
7
+ export { loadTravelRuleDepositReconcilerConfigFromEnv, resolveOnChainSender, TravelRuleDepositReconciler, } from "./travel-rule-deposit-reconciler";
7
8
  export { buildHttpClientOverrideFromMetadata, createVerityHttpClientOverride, verityHttpClientOverridePredicate, } from "./verity";
8
9
  /**
9
10
  * Loads and validates policy configuration
@@ -0,0 +1,159 @@
1
+ import type { PolicyConfig, TravelRuleDepositConfig, TravelRuleDepositQuestionnaire } from "../types";
2
+ import type { BrokerPoolEntry } from "./broker";
3
+ import type { OtelMetrics } from "./otel";
4
+ /**
5
+ * Auto-clear reconciler for Binance travel-rule-frozen DEPOSITS.
6
+ *
7
+ * Under the AUSTRAC travel rule Binance credits an inbound deposit but holds it
8
+ * in `getUserAsset.freeze` (invisible to free+locked balances) until a per-deposit
9
+ * questionnaire is answered. This reconciler polls `localentity/deposit/history`
10
+ * for such frozen deposits and submits the questionnaire — but only for deposits
11
+ * whose on-chain sender is PROVEN (via `eth_getTransactionByHash`) to be one of
12
+ * our configured originator wallets. Everything else is left frozen and surfaced.
13
+ *
14
+ * Compliance invariant: a deposit is NEVER auto-declared unless its origin is
15
+ * proven ours. Undeclared origin, unresolvable sender, entity drift, or any
16
+ * uncertainty all fail closed (skip + surface), never "declare anyway".
17
+ *
18
+ * This runs entirely broker-internal (not through the gRPC verity path), so
19
+ * provide-info carries no verity proof — acceptable for a compliance attestation
20
+ * that is not a trading action; see the plan's edge-case notes.
21
+ */
22
+ export type LocalEntityDeposit = {
23
+ tranId: string;
24
+ coin: string;
25
+ amount: string;
26
+ network: string;
27
+ txId: string;
28
+ travelRuleStatus: string;
29
+ requireQuestionnaire: boolean;
30
+ raw: Record<string, unknown>;
31
+ };
32
+ /**
33
+ * Parses one raw `localentity/deposit/history` row. Returns null when the row
34
+ * lacks a tranId (the only id valid for provide-info — the `capital/deposit/hisrec`
35
+ * id is a different id space and yields "Deposit request not found").
36
+ */
37
+ export declare function parseLocalEntityDeposit(raw: Record<string, unknown>): LocalEntityDeposit | null;
38
+ /**
39
+ * Resolves the on-chain sender (`tx.from`) of a deposit's transaction hash via a
40
+ * JSON-RPC `eth_getTransactionByHash`. Returns the lowercased sender, or null
41
+ * when the hash is not a well-formed EVM tx hash or the tx is not found (pruned
42
+ * node / reorg / not yet mined) — both of which mean the origin is UNPROVEN and
43
+ * the caller must skip rather than assume.
44
+ *
45
+ * `tx.from` is the EOA that signed the deposit transaction. For our funding
46
+ * churn (owner wallet → Binance via a direct ERC20 transfer) that equals the
47
+ * token originator. A transfer routed through a contract could differ; hardening
48
+ * to the ERC20 Transfer event's `from` is possible later if that case arises.
49
+ *
50
+ * Uses `node:https` rather than the global `fetch`: inside the Gramine SGX
51
+ * enclave undici (which backs global fetch) fails to establish outbound
52
+ * connections — the same failure mode as the enclave's dead Binance user-data
53
+ * WebSocket — while the ccxt HTTP path (node http/https) works. Using node's
54
+ * request keeps this on the transport that is proven to work in the enclave.
55
+ */
56
+ export declare function resolveOnChainSender(rpcUrl: string, txHash: string, timeoutMs?: number): Promise<string | null>;
57
+ /** Binance rate-limit signals (-1003 / HTTP 429 / DDoS guard). */
58
+ export declare function isRateLimitError(error: unknown): boolean;
59
+ /**
60
+ * A provide-info call that Binance treats as already-satisfied. Re-submitting a
61
+ * tranId that was already provided (a manual release, or a reconciler restart)
62
+ * must be idempotent, so these responses/errors count as success.
63
+ */
64
+ export declare function isAlreadyProvidedError(error: unknown): boolean;
65
+ export type ReconcilerAccountState = {
66
+ submittedTranIds: Set<string>;
67
+ surfacedFailed: Set<string>;
68
+ backoffUntil: Map<string, number>;
69
+ rateLimitedUntil: number;
70
+ };
71
+ export declare function createAccountState(): ReconcilerAccountState;
72
+ export type ReconcileOutcome = {
73
+ kind: "submitted";
74
+ deposit: LocalEntityDeposit;
75
+ sender: string;
76
+ } | {
77
+ kind: "already-provided";
78
+ deposit: LocalEntityDeposit;
79
+ sender: string;
80
+ } | {
81
+ kind: "undeclared-origin";
82
+ deposit: LocalEntityDeposit;
83
+ sender: string;
84
+ } | {
85
+ kind: "unproven-origin";
86
+ deposit: LocalEntityDeposit;
87
+ error?: string;
88
+ } | {
89
+ kind: "entity-drift";
90
+ deposit: LocalEntityDeposit;
91
+ country: string | null;
92
+ } | {
93
+ kind: "failed-terminal";
94
+ deposit: LocalEntityDeposit;
95
+ } | {
96
+ kind: "submit-error";
97
+ deposit: LocalEntityDeposit;
98
+ error: string;
99
+ } | {
100
+ kind: "poll-error";
101
+ error: string;
102
+ };
103
+ export type ReconcileAccountReport = {
104
+ accountLabel: string;
105
+ frozenDeposits: LocalEntityDeposit[];
106
+ outcomes: ReconcileOutcome[];
107
+ hadActionableWork: boolean;
108
+ };
109
+ export type ReconcileAccountDeps = {
110
+ accountLabel: string;
111
+ depositConfig: TravelRuleDepositConfig;
112
+ expectedCountry: string;
113
+ failureBackoffMs: number;
114
+ rateLimitCooldownMs: number;
115
+ now: number;
116
+ state: ReconcilerAccountState;
117
+ fetchDepositHistory: () => Promise<Array<Record<string, unknown>>>;
118
+ fetchQuestionnaireCountry: () => Promise<string | null>;
119
+ resolveSender: (network: string, txId: string) => Promise<string | null>;
120
+ submitProvideInfo: (tranId: string, questionnaire: TravelRuleDepositQuestionnaire) => Promise<Record<string, unknown>>;
121
+ resolveQuestionnaire: (config: TravelRuleDepositConfig, sender: string) => TravelRuleDepositQuestionnaire | null;
122
+ };
123
+ /**
124
+ * Reconciles a single account once. Pure orchestration: all I/O is injected so
125
+ * the compliance invariants (never submit an unproven/undeclared/entity-drifted
126
+ * deposit) are unit-testable without a network. Fetch/poll errors are captured
127
+ * as outcomes rather than thrown so one bad account never stalls the loop.
128
+ */
129
+ export declare function reconcileAccountOnce(deps: ReconcileAccountDeps): Promise<ReconcileAccountReport>;
130
+ export type TravelRuleDepositReconcilerConfig = {
131
+ rpcUrlsByNetwork: Record<string, string>;
132
+ pollIntervalActiveMs: number;
133
+ pollIntervalIdleMs: number;
134
+ expectedQuestionnaireCountry: string;
135
+ failureBackoffMs: number;
136
+ rateLimitCooldownMs: number;
137
+ };
138
+ /**
139
+ * Builds reconciler config from the environment. RPC URLs are read from
140
+ * `TRAVEL_RULE_RPC_URL_<NETWORK>` vars (network suffix must match Binance's
141
+ * network code, e.g. `TRAVEL_RULE_RPC_URL_ARBITRUM`). These are intentionally NOT
142
+ * `CEX_BROKER_`-prefixed so the credential env scan ignores them. Cadence and the
143
+ * expected questionnaire country are overridable but default to the AU rollout.
144
+ */
145
+ export declare function loadTravelRuleDepositReconcilerConfigFromEnv(env: Record<string, string | undefined>): TravelRuleDepositReconcilerConfig;
146
+ export declare class TravelRuleDepositReconciler {
147
+ #private;
148
+ private readonly params;
149
+ constructor(params: {
150
+ policy: PolicyConfig;
151
+ brokers: Record<string, BrokerPoolEntry>;
152
+ config: TravelRuleDepositReconcilerConfig;
153
+ metrics?: OtelMetrics;
154
+ });
155
+ /** True when at least one exchange has deposit auto-clear enabled in policy. */
156
+ static hasEnabledExchange(policy: PolicyConfig): boolean;
157
+ start(): void;
158
+ stop(): void;
159
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Exchange } from "@usherlabs/ccxt";
2
2
  import Joi from "joi";
3
- import type { PolicyConfig, TravelRuleQuestionnaire } from "../types";
3
+ import type { PolicyConfig, TravelRuleDepositConfig, TravelRuleDepositQuestionnaire, TravelRuleQuestionnaire } from "../types";
4
4
  export declare const australiaQuestionnaireSchema: Joi.ObjectSchema<any>;
5
5
  export type TravelRuleDecision = {
6
6
  mode: "standard";
@@ -26,6 +26,37 @@ export declare function resolveTravelRuleDecision(policy: PolicyConfig, exchange
26
26
  * the generated implicit method if called again.
27
27
  */
28
28
  export declare function registerBinanceTravelRuleWithdrawEndpoint(exchange: Exchange): void;
29
+ export declare const australiaDepositQuestionnaireSchema: Joi.ObjectSchema<any>;
30
+ /**
31
+ * Returns the enabled deposit travel-rule config for an exchange, or null when
32
+ * the feature is absent or disabled. Gated solely by `deposits.enabled` (not the
33
+ * entry's withdraw `enabled` flag): when null the reconciler does nothing for the
34
+ * exchange, preserving exact pre-feature behavior.
35
+ */
36
+ export declare function getEnabledTravelRuleDepositConfig(policy: PolicyConfig, exchange: string): TravelRuleDepositConfig | null;
37
+ /**
38
+ * Looks up the questionnaire for a PROVEN on-chain sender. Matching is
39
+ * case-insensitive. Returns null when the sender is not a declared originator —
40
+ * the reconciler must then leave the deposit frozen and surface an anomaly
41
+ * rather than attest an origin it cannot prove is ours.
42
+ */
43
+ export declare function resolveDepositOriginatorQuestionnaire(config: TravelRuleDepositConfig, senderAddress: string): TravelRuleDepositQuestionnaire | null;
44
+ /**
45
+ * Registers Binance's localentity deposit endpoints used by the reconciler:
46
+ * the two read endpoints (deposit history + questionnaire requirements) and the
47
+ * provide-info write. No-op for non-Binance exchanges. Idempotent.
48
+ *
49
+ * `localentity/deposit/provide-info` must be signed with the questionnaire JSON
50
+ * RAW (unencoded) — see the @usherlabs/ccxt patch (`binance.sign` rawencode
51
+ * branch). Signing it url-encoded yields Binance error -1022. The two GETs carry
52
+ * only simple params and sign fine either way.
53
+ *
54
+ * provide-info is a 600-weight UID-limited write (same class as
55
+ * capital/withdraw/apply), so it carries ccxt-binance's `4.0002` cost encoding
56
+ * (`.0002` = charge the UID limiter) rather than a naive `1`, which would
57
+ * under-throttle the write and invite -1003 once attestation is active.
58
+ */
59
+ export declare function registerBinanceTravelRuleDepositEndpoints(exchange: Exchange): void;
29
60
  type LocalEntityWithdrawArgs = {
30
61
  code: string;
31
62
  amount: number;
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export default class CEXBroker {
11
11
  private useVerity;
12
12
  private otelMetrics?;
13
13
  private otelLogs?;
14
+ private depositReconciler?;
14
15
  /**
15
16
  * Loads environment variables prefixed with CEX_BROKER_
16
17
  * Expected format: