@usherlabs/cex-broker 0.2.16 → 0.2.17

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.
@@ -119803,7 +119803,7 @@ class binance extends binance_default {
119803
119803
  }
119804
119804
  if (api === "sapi" && path === "asset/dust") {
119805
119805
  query = this.urlencodeWithArrayRepeat(extendedParams);
119806
- } else if (path === "batchOrders" || path.indexOf("sub-account") >= 0 || path === "capital/withdraw/apply" || path === "localentity/withdraw/apply" || path.indexOf("staking") >= 0 || path.indexOf("simple-earn") >= 0) {
119806
+ } else if (path === "batchOrders" || path.indexOf("sub-account") >= 0 || path === "capital/withdraw/apply" || path === "localentity/withdraw/apply" || path === "localentity/deposit/provide-info" || path.indexOf("staking") >= 0 || path.indexOf("simple-earn") >= 0) {
119807
119807
  if (method === "DELETE" && path === "batchOrders") {
119808
119808
  const orderidlist = this.safeList(extendedParams, "orderidlist", []);
119809
119809
  const origclientorderidlist = this.safeList(extendedParams, "origclientorderidlist", []);
@@ -313583,6 +313583,40 @@ function registerBinanceTravelRuleWithdrawEndpoint(exchange) {
313583
313583
  }
313584
313584
  exchange.defineRestApi({ sapi: { post: { "localentity/withdraw/apply": 4.0002 } } }, "request");
313585
313585
  }
313586
+ var australiaDepositQuestionnaireSchema = import_joi.default.object({
313587
+ depositOriginator: import_joi.default.number().valid(1).required(),
313588
+ receiveFrom: import_joi.default.number().valid(1).required(),
313589
+ declaration: import_joi.default.boolean().valid(true).required()
313590
+ });
313591
+ function getEnabledTravelRuleDepositConfig(policy, exchange) {
313592
+ const rules = policy.travelRule?.rule ?? [];
313593
+ const exchangeNorm = exchange.trim().toUpperCase();
313594
+ const entry = rules.find((rule) => rule.exchange.trim().toUpperCase() === exchangeNorm);
313595
+ const deposits = entry?.deposits;
313596
+ if (!deposits || !deposits.enabled) {
313597
+ return null;
313598
+ }
313599
+ return deposits;
313600
+ }
313601
+ function resolveDepositOriginatorQuestionnaire(config, senderAddress) {
313602
+ const senderNorm = senderAddress.trim().toLowerCase();
313603
+ const match = Object.entries(config.originators).find(([address]) => address.trim().toLowerCase() === senderNorm);
313604
+ return match ? match[1].questionnaire : null;
313605
+ }
313606
+ function registerBinanceTravelRuleDepositEndpoints(exchange) {
313607
+ if (exchange.id !== "binance") {
313608
+ return;
313609
+ }
313610
+ exchange.defineRestApi({
313611
+ sapi: {
313612
+ get: {
313613
+ "localentity/deposit/history": 1,
313614
+ "localentity/questionnaire-requirements": 1
313615
+ },
313616
+ put: { "localentity/deposit/provide-info": 4.0002 }
313617
+ }
313618
+ }, "request");
313619
+ }
313586
313620
  async function withdrawViaLocalEntity(broker, args) {
313587
313621
  const exchange = broker;
313588
313622
  if (typeof exchange.sapiPostLocalentityWithdrawApply !== "function") {
@@ -313631,6 +313665,7 @@ function applyCommonExchangeConfig(exchange) {
313631
313665
  adjustForTimeDifference: true
313632
313666
  });
313633
313667
  registerBinanceTravelRuleWithdrawEndpoint(exchange);
313668
+ registerBinanceTravelRuleDepositEndpoints(exchange);
313634
313669
  }
313635
313670
  function createBroker(cex3, credsOrMetadata) {
313636
313671
  let apiKey;
@@ -313936,6 +313971,538 @@ function authenticateRequest(call, whitelistIps) {
313936
313971
  }
313937
313972
  return true;
313938
313973
  }
313974
+ // src/helpers/deposit.ts
313975
+ function depositField(deposit, fields) {
313976
+ for (const field of fields) {
313977
+ const value = deposit[field];
313978
+ if (value !== undefined && value !== null && String(value).length > 0) {
313979
+ return value;
313980
+ }
313981
+ }
313982
+ return;
313983
+ }
313984
+ function normalizeDepositStatus(status) {
313985
+ const normalized = String(status ?? "").trim().toLowerCase();
313986
+ if (["ok", "credited", "complete", "completed", "success"].includes(normalized)) {
313987
+ return "credited";
313988
+ }
313989
+ if (["failed", "failure", "canceled", "cancelled", "rejected"].includes(normalized)) {
313990
+ return "failed";
313991
+ }
313992
+ if (["timeout", "timed_out", "timedout", "expired"].includes(normalized)) {
313993
+ return "timed_out";
313994
+ }
313995
+ if (["pending", "processing", "confirming", "waiting"].includes(normalized)) {
313996
+ return "pending";
313997
+ }
313998
+ return "pending";
313999
+ }
314000
+ function depositMatchesTransaction(deposit, transactionHash) {
314001
+ const candidates = [
314002
+ depositField(deposit, [
314003
+ "txid",
314004
+ "txId",
314005
+ "tx_hash",
314006
+ "txHash",
314007
+ "transactionHash"
314008
+ ]),
314009
+ depositField(deposit, ["id"])
314010
+ ];
314011
+ return candidates.some((candidate) => String(candidate) === transactionHash);
314012
+ }
314013
+ function stringAmountEquals(left, right) {
314014
+ const leftNum = Number(left);
314015
+ const rightNum = Number(right);
314016
+ if (Number.isFinite(leftNum) && Number.isFinite(rightNum)) {
314017
+ return Math.abs(leftNum - rightNum) < 0.000000000001;
314018
+ }
314019
+ return String(left) === String(right);
314020
+ }
314021
+ function normalizeAddress(value) {
314022
+ if (value === undefined || value === null) {
314023
+ return;
314024
+ }
314025
+ return String(value).trim().toLowerCase();
314026
+ }
314027
+
314028
+ // src/helpers/travel-rule-deposit-reconciler.ts
314029
+ function toBool(value) {
314030
+ if (typeof value === "boolean")
314031
+ return value;
314032
+ if (typeof value === "number")
314033
+ return value !== 0;
314034
+ if (typeof value === "string")
314035
+ return value.trim().toLowerCase() === "true";
314036
+ return false;
314037
+ }
314038
+ function parseLocalEntityDeposit(raw) {
314039
+ const tranId = depositField(raw, ["tranId"]);
314040
+ if (tranId === undefined)
314041
+ return null;
314042
+ return {
314043
+ tranId: String(tranId),
314044
+ coin: String(depositField(raw, ["coin", "asset"]) ?? ""),
314045
+ amount: String(depositField(raw, ["amount"]) ?? ""),
314046
+ network: String(depositField(raw, ["network"]) ?? ""),
314047
+ txId: String(depositField(raw, ["txId", "txid", "tx_hash", "txHash"]) ?? ""),
314048
+ travelRuleStatus: String(depositField(raw, ["travelRuleStatusV2", "travelRuleStatus"]) ?? "").trim().toUpperCase(),
314049
+ requireQuestionnaire: toBool(raw.requireQuestionnaire ?? raw.requireQuestionnaireV2),
314050
+ raw
314051
+ };
314052
+ }
314053
+ var EVM_TX_HASH = /^0x[0-9a-fA-F]{64}$/;
314054
+ async function resolveOnChainSender(rpcUrl, txHash, timeoutMs = 1e4) {
314055
+ if (!EVM_TX_HASH.test(txHash))
314056
+ return null;
314057
+ const controller = new AbortController;
314058
+ const timeout2 = setTimeout(() => controller.abort(), timeoutMs);
314059
+ let response;
314060
+ try {
314061
+ response = await fetch(rpcUrl, {
314062
+ method: "POST",
314063
+ headers: { "content-type": "application/json" },
314064
+ signal: controller.signal,
314065
+ body: JSON.stringify({
314066
+ jsonrpc: "2.0",
314067
+ id: 1,
314068
+ method: "eth_getTransactionByHash",
314069
+ params: [txHash]
314070
+ })
314071
+ });
314072
+ } finally {
314073
+ clearTimeout(timeout2);
314074
+ }
314075
+ if (!response.ok) {
314076
+ throw new Error(`travel_rule_rpc_http_${response.status}`);
314077
+ }
314078
+ const json3 = await response.json();
314079
+ if (json3.error) {
314080
+ throw new Error(`travel_rule_rpc_error: ${JSON.stringify(json3.error)}`);
314081
+ }
314082
+ const from = json3.result?.from;
314083
+ return typeof from === "string" && from.length > 0 ? from.toLowerCase() : null;
314084
+ }
314085
+ function errorText(error) {
314086
+ if (error instanceof Error)
314087
+ return error.message;
314088
+ if (typeof error === "string")
314089
+ return error;
314090
+ try {
314091
+ return JSON.stringify(error);
314092
+ } catch {
314093
+ return String(error);
314094
+ }
314095
+ }
314096
+ function isRateLimitError(error) {
314097
+ const text = errorText(error).toLowerCase();
314098
+ return text.includes("-1003") || text.includes("too many requests") || text.includes("429") || text.includes("ddosprotection") || text.includes("ratelimitexceeded");
314099
+ }
314100
+ function isAlreadyProvidedError(error) {
314101
+ const text = errorText(error).toLowerCase();
314102
+ return text.includes("already") && (text.includes("provided") || text.includes("processed") || text.includes("passed") || text.includes("submit"));
314103
+ }
314104
+ function isAcceptedResponse(response) {
314105
+ return response.accepted !== false;
314106
+ }
314107
+ function createAccountState() {
314108
+ return {
314109
+ submittedTranIds: new Set,
314110
+ surfacedFailed: new Set,
314111
+ backoffUntil: new Map,
314112
+ rateLimitedUntil: 0
314113
+ };
314114
+ }
314115
+ function isInBackoff(state, tranId, now3) {
314116
+ const until = state.backoffUntil.get(tranId);
314117
+ return until !== undefined && now3 < until;
314118
+ }
314119
+ async function reconcileAccountOnce(deps) {
314120
+ const { state, now: now3, accountLabel } = deps;
314121
+ const outcomes = [];
314122
+ if (now3 < state.rateLimitedUntil) {
314123
+ return {
314124
+ accountLabel,
314125
+ frozenDeposits: [],
314126
+ outcomes,
314127
+ hadActionableWork: false
314128
+ };
314129
+ }
314130
+ let rawDeposits;
314131
+ try {
314132
+ rawDeposits = await deps.fetchDepositHistory();
314133
+ } catch (error) {
314134
+ if (isRateLimitError(error)) {
314135
+ state.rateLimitedUntil = now3 + deps.rateLimitCooldownMs;
314136
+ }
314137
+ return {
314138
+ accountLabel,
314139
+ frozenDeposits: [],
314140
+ outcomes: [{ kind: "poll-error", error: errorText(error) }],
314141
+ hadActionableWork: false
314142
+ };
314143
+ }
314144
+ const deposits = rawDeposits.map(parseLocalEntityDeposit).filter((d) => d !== null);
314145
+ const frozen = deposits.filter((d) => d.requireQuestionnaire && d.travelRuleStatus === "PENDING");
314146
+ for (const deposit of deposits) {
314147
+ if (deposit.travelRuleStatus === "FAILED" && !state.surfacedFailed.has(deposit.tranId)) {
314148
+ state.surfacedFailed.add(deposit.tranId);
314149
+ outcomes.push({ kind: "failed-terminal", deposit });
314150
+ }
314151
+ }
314152
+ const candidates = frozen.filter((d) => !state.submittedTranIds.has(d.tranId) && !isInBackoff(state, d.tranId, now3));
314153
+ if (candidates.length === 0) {
314154
+ return {
314155
+ accountLabel,
314156
+ frozenDeposits: frozen,
314157
+ outcomes,
314158
+ hadActionableWork: false
314159
+ };
314160
+ }
314161
+ let country = null;
314162
+ try {
314163
+ country = await deps.fetchQuestionnaireCountry();
314164
+ } catch (error) {
314165
+ if (isRateLimitError(error)) {
314166
+ state.rateLimitedUntil = now3 + deps.rateLimitCooldownMs;
314167
+ }
314168
+ country = null;
314169
+ }
314170
+ if (country !== deps.expectedCountry) {
314171
+ for (const deposit of candidates) {
314172
+ outcomes.push({ kind: "entity-drift", deposit, country });
314173
+ }
314174
+ return {
314175
+ accountLabel,
314176
+ frozenDeposits: frozen,
314177
+ outcomes,
314178
+ hadActionableWork: true
314179
+ };
314180
+ }
314181
+ for (const deposit of candidates) {
314182
+ if (now3 < state.rateLimitedUntil)
314183
+ break;
314184
+ let sender = null;
314185
+ try {
314186
+ sender = await deps.resolveSender(deposit.network, deposit.txId);
314187
+ } catch (error) {
314188
+ sender = null;
314189
+ if (isRateLimitError(error)) {
314190
+ state.rateLimitedUntil = now3 + deps.rateLimitCooldownMs;
314191
+ }
314192
+ }
314193
+ if (!sender) {
314194
+ state.backoffUntil.set(deposit.tranId, now3 + deps.failureBackoffMs);
314195
+ outcomes.push({ kind: "unproven-origin", deposit });
314196
+ continue;
314197
+ }
314198
+ const questionnaire = deps.resolveQuestionnaire(deps.depositConfig, sender);
314199
+ if (!questionnaire) {
314200
+ state.backoffUntil.set(deposit.tranId, now3 + deps.failureBackoffMs);
314201
+ outcomes.push({ kind: "undeclared-origin", deposit, sender });
314202
+ continue;
314203
+ }
314204
+ try {
314205
+ const response = await deps.submitProvideInfo(deposit.tranId, questionnaire);
314206
+ if (isAcceptedResponse(response)) {
314207
+ state.submittedTranIds.add(deposit.tranId);
314208
+ state.backoffUntil.delete(deposit.tranId);
314209
+ outcomes.push({ kind: "submitted", deposit, sender });
314210
+ } else {
314211
+ state.backoffUntil.set(deposit.tranId, now3 + deps.failureBackoffMs);
314212
+ outcomes.push({
314213
+ kind: "submit-error",
314214
+ deposit,
314215
+ error: JSON.stringify(response)
314216
+ });
314217
+ }
314218
+ } catch (error) {
314219
+ if (isAlreadyProvidedError(error)) {
314220
+ state.submittedTranIds.add(deposit.tranId);
314221
+ state.backoffUntil.delete(deposit.tranId);
314222
+ outcomes.push({ kind: "already-provided", deposit, sender });
314223
+ } else {
314224
+ if (isRateLimitError(error)) {
314225
+ state.rateLimitedUntil = now3 + deps.rateLimitCooldownMs;
314226
+ }
314227
+ state.backoffUntil.set(deposit.tranId, now3 + deps.failureBackoffMs);
314228
+ outcomes.push({
314229
+ kind: "submit-error",
314230
+ deposit,
314231
+ error: errorText(error)
314232
+ });
314233
+ }
314234
+ }
314235
+ }
314236
+ return {
314237
+ accountLabel,
314238
+ frozenDeposits: frozen,
314239
+ outcomes,
314240
+ hadActionableWork: true
314241
+ };
314242
+ }
314243
+ var DEFAULTS = {
314244
+ pollIntervalActiveMs: 60000,
314245
+ pollIntervalIdleMs: 600000,
314246
+ expectedQuestionnaireCountry: "AU",
314247
+ failureBackoffMs: 15 * 60000,
314248
+ rateLimitCooldownMs: 5 * 60000
314249
+ };
314250
+ function envSecondsToMs(env, key, fallbackMs) {
314251
+ const raw = env[key];
314252
+ if (raw === undefined)
314253
+ return fallbackMs;
314254
+ const secs = Number(raw);
314255
+ return Number.isFinite(secs) && secs > 0 ? secs * 1000 : fallbackMs;
314256
+ }
314257
+ function loadTravelRuleDepositReconcilerConfigFromEnv(env) {
314258
+ const rpcUrlsByNetwork = {};
314259
+ const prefix = "TRAVEL_RULE_RPC_URL_";
314260
+ for (const [key, value] of Object.entries(env)) {
314261
+ if (key.startsWith(prefix) && value) {
314262
+ rpcUrlsByNetwork[key.slice(prefix.length).toUpperCase()] = value;
314263
+ }
314264
+ }
314265
+ return {
314266
+ rpcUrlsByNetwork,
314267
+ pollIntervalActiveMs: envSecondsToMs(env, "TRAVEL_RULE_DEPOSIT_POLL_ACTIVE_SECS", DEFAULTS.pollIntervalActiveMs),
314268
+ pollIntervalIdleMs: envSecondsToMs(env, "TRAVEL_RULE_DEPOSIT_POLL_IDLE_SECS", DEFAULTS.pollIntervalIdleMs),
314269
+ expectedQuestionnaireCountry: env.TRAVEL_RULE_QUESTIONNAIRE_COUNTRY?.trim().toUpperCase() || DEFAULTS.expectedQuestionnaireCountry,
314270
+ failureBackoffMs: envSecondsToMs(env, "TRAVEL_RULE_DEPOSIT_FAILURE_BACKOFF_SECS", DEFAULTS.failureBackoffMs),
314271
+ rateLimitCooldownMs: envSecondsToMs(env, "TRAVEL_RULE_DEPOSIT_RATE_LIMIT_COOLDOWN_SECS", DEFAULTS.rateLimitCooldownMs)
314272
+ };
314273
+ }
314274
+ function parseQuestionnaireCountry(response) {
314275
+ if (!response || typeof response !== "object")
314276
+ return null;
314277
+ const code = response.questionnaireCountryCode;
314278
+ return typeof code === "string" ? code.trim().toUpperCase() : null;
314279
+ }
314280
+
314281
+ class TravelRuleDepositReconciler {
314282
+ params;
314283
+ #timer = null;
314284
+ #stopped = false;
314285
+ #running = false;
314286
+ #states = new Map;
314287
+ constructor(params) {
314288
+ this.params = params;
314289
+ }
314290
+ static hasEnabledExchange(policy) {
314291
+ return (policy.travelRule?.rule ?? []).some((rule) => getEnabledTravelRuleDepositConfig(policy, rule.exchange));
314292
+ }
314293
+ start() {
314294
+ if (this.#timer || this.#stopped)
314295
+ return;
314296
+ if (!TravelRuleDepositReconciler.hasEnabledExchange(this.params.policy)) {
314297
+ return;
314298
+ }
314299
+ if (Object.keys(this.params.config.rpcUrlsByNetwork).length === 0) {
314300
+ log.warn("⚠️ Travel-rule deposit auto-clear is enabled but no TRAVEL_RULE_RPC_URL_<NETWORK> is configured; every frozen deposit will be treated as unproven (left frozen).");
314301
+ }
314302
+ log.info("\uD83D\uDEC2 Travel-rule deposit auto-clear reconciler started");
314303
+ this.#scheduleImmediate();
314304
+ }
314305
+ stop() {
314306
+ this.#stopped = true;
314307
+ if (this.#timer) {
314308
+ clearTimeout(this.#timer);
314309
+ this.#timer = null;
314310
+ }
314311
+ }
314312
+ #scheduleImmediate() {
314313
+ this.#timer = setTimeout(() => void this.#tick(), 0);
314314
+ }
314315
+ #stateFor(key) {
314316
+ let state = this.#states.get(key);
314317
+ if (!state) {
314318
+ state = createAccountState();
314319
+ this.#states.set(key, state);
314320
+ }
314321
+ return state;
314322
+ }
314323
+ #targets() {
314324
+ const targets = [];
314325
+ for (const rule of this.params.policy.travelRule?.rule ?? []) {
314326
+ const depositConfig = getEnabledTravelRuleDepositConfig(this.params.policy, rule.exchange);
314327
+ if (!depositConfig)
314328
+ continue;
314329
+ const exchangeId = rule.exchange.trim().toLowerCase();
314330
+ const pool = this.params.brokers[exchangeId];
314331
+ if (!pool)
314332
+ continue;
314333
+ for (const account of [pool.primary, ...pool.secondaryBrokers]) {
314334
+ targets.push({ exchangeId, account, depositConfig });
314335
+ }
314336
+ }
314337
+ return targets;
314338
+ }
314339
+ async#tick() {
314340
+ if (this.#stopped || this.#running)
314341
+ return;
314342
+ this.#running = true;
314343
+ let anyActionable = false;
314344
+ try {
314345
+ for (const target of this.#targets()) {
314346
+ if (this.#stopped)
314347
+ break;
314348
+ const report = await this.#reconcileTarget(target);
314349
+ this.#emit(target, report);
314350
+ if (report.hadActionableWork)
314351
+ anyActionable = true;
314352
+ }
314353
+ } catch (error) {
314354
+ log.error("Travel-rule deposit reconciler tick failed", error);
314355
+ } finally {
314356
+ this.#running = false;
314357
+ if (!this.#stopped) {
314358
+ const delay = anyActionable ? this.params.config.pollIntervalActiveMs : this.params.config.pollIntervalIdleMs;
314359
+ this.#timer = setTimeout(() => void this.#tick(), delay);
314360
+ }
314361
+ }
314362
+ }
314363
+ #reconcileTarget(target) {
314364
+ const exchange = target.account.exchange;
314365
+ const accountLabel = `${target.exchangeId}:${target.account.label}`;
314366
+ const { config } = this.params;
314367
+ return reconcileAccountOnce({
314368
+ accountLabel,
314369
+ depositConfig: target.depositConfig,
314370
+ expectedCountry: config.expectedQuestionnaireCountry,
314371
+ failureBackoffMs: config.failureBackoffMs,
314372
+ rateLimitCooldownMs: config.rateLimitCooldownMs,
314373
+ now: Date.now(),
314374
+ state: this.#stateFor(accountLabel),
314375
+ fetchDepositHistory: async () => {
314376
+ if (typeof exchange.sapiGetLocalentityDepositHistory !== "function") {
314377
+ throw new Error("binance_localentity_deposit_history_unavailable: endpoint not registered");
314378
+ }
314379
+ const result = await exchange.sapiGetLocalentityDepositHistory({});
314380
+ return Array.isArray(result) ? result : [];
314381
+ },
314382
+ fetchQuestionnaireCountry: async () => {
314383
+ if (typeof exchange.sapiGetLocalentityQuestionnaireRequirements !== "function") {
314384
+ return null;
314385
+ }
314386
+ const result = await exchange.sapiGetLocalentityQuestionnaireRequirements({});
314387
+ return parseQuestionnaireCountry(result);
314388
+ },
314389
+ resolveSender: (network, txId) => {
314390
+ const url2 = config.rpcUrlsByNetwork[network.trim().toUpperCase()];
314391
+ if (!url2)
314392
+ return Promise.resolve(null);
314393
+ return resolveOnChainSender(url2, txId);
314394
+ },
314395
+ submitProvideInfo: (tranId, questionnaire) => {
314396
+ if (typeof exchange.sapiPutLocalentityDepositProvideInfo !== "function") {
314397
+ throw new Error("binance_localentity_provide_info_unavailable: endpoint not registered");
314398
+ }
314399
+ return exchange.sapiPutLocalentityDepositProvideInfo({
314400
+ tranId,
314401
+ questionnaire: JSON.stringify(questionnaire)
314402
+ });
314403
+ },
314404
+ resolveQuestionnaire: resolveDepositOriginatorQuestionnaire
314405
+ });
314406
+ }
314407
+ #emit(target, report) {
314408
+ const { metrics } = this.params;
314409
+ const base2 = { exchange: target.exchangeId, account: target.account.label };
314410
+ metrics?.recordGauge("travel_rule_frozen_deposits", report.frozenDeposits.length, base2);
314411
+ for (const outcome of report.outcomes) {
314412
+ switch (outcome.kind) {
314413
+ case "submitted":
314414
+ case "already-provided": {
314415
+ log.info("\uD83D\uDEC2 Travel-rule deposit auto-declared", {
314416
+ result: outcome.kind,
314417
+ account: report.accountLabel,
314418
+ tranId: outcome.deposit.tranId,
314419
+ coin: outcome.deposit.coin,
314420
+ amount: outcome.deposit.amount,
314421
+ network: outcome.deposit.network,
314422
+ txId: outcome.deposit.txId,
314423
+ originator: outcome.sender
314424
+ });
314425
+ metrics?.recordCounter("travel_rule_deposit_submissions_total", 1, {
314426
+ ...base2,
314427
+ coin: outcome.deposit.coin,
314428
+ result: outcome.kind
314429
+ });
314430
+ break;
314431
+ }
314432
+ case "undeclared-origin":
314433
+ log.warn("\uD83D\uDEC2 Travel-rule deposit from UNDECLARED originator — left frozen", {
314434
+ account: report.accountLabel,
314435
+ tranId: outcome.deposit.tranId,
314436
+ coin: outcome.deposit.coin,
314437
+ amount: outcome.deposit.amount,
314438
+ sender: outcome.sender
314439
+ });
314440
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
314441
+ ...base2,
314442
+ reason: "undeclared_origin"
314443
+ });
314444
+ break;
314445
+ case "unproven-origin":
314446
+ log.warn("\uD83D\uDEC2 Travel-rule deposit origin UNPROVEN (tx sender unresolved) — left frozen", {
314447
+ account: report.accountLabel,
314448
+ tranId: outcome.deposit.tranId,
314449
+ coin: outcome.deposit.coin,
314450
+ network: outcome.deposit.network,
314451
+ txId: outcome.deposit.txId
314452
+ });
314453
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
314454
+ ...base2,
314455
+ reason: "unproven_origin"
314456
+ });
314457
+ break;
314458
+ case "entity-drift":
314459
+ log.warn("\uD83D\uDEC2 Travel-rule deposit skipped — questionnaire entity is not the expected country", {
314460
+ account: report.accountLabel,
314461
+ tranId: outcome.deposit.tranId,
314462
+ observedCountry: outcome.country,
314463
+ expectedCountry: this.params.config.expectedQuestionnaireCountry
314464
+ });
314465
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
314466
+ ...base2,
314467
+ reason: "entity_drift"
314468
+ });
314469
+ break;
314470
+ case "failed-terminal":
314471
+ log.warn("\uD83D\uDEC2 Travel-rule deposit is FAILED (terminal) — needs manual handling", {
314472
+ account: report.accountLabel,
314473
+ tranId: outcome.deposit.tranId,
314474
+ coin: outcome.deposit.coin
314475
+ });
314476
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
314477
+ ...base2,
314478
+ reason: "failed_status"
314479
+ });
314480
+ break;
314481
+ case "submit-error":
314482
+ log.error("\uD83D\uDEC2 Travel-rule deposit provide-info failed", {
314483
+ account: report.accountLabel,
314484
+ tranId: outcome.deposit.tranId,
314485
+ error: outcome.error
314486
+ });
314487
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
314488
+ ...base2,
314489
+ reason: "submit_error"
314490
+ });
314491
+ break;
314492
+ case "poll-error":
314493
+ log.error("\uD83D\uDEC2 Travel-rule deposit history poll failed", {
314494
+ account: report.accountLabel,
314495
+ error: outcome.error
314496
+ });
314497
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
314498
+ ...base2,
314499
+ reason: "poll_error"
314500
+ });
314501
+ break;
314502
+ }
314503
+ }
314504
+ }
314505
+ }
313939
314506
  // src/helpers/verity.ts
313940
314507
  var import_verity_client = __toESM(require_dist(), 1);
313941
314508
 
@@ -314073,7 +314640,14 @@ function loadPolicy(policyPath) {
314073
314640
  description: import_joi2.default.string().optional(),
314074
314641
  addresses: import_joi2.default.object().pattern(import_joi2.default.string(), import_joi2.default.object({
314075
314642
  questionnaire: australiaQuestionnaireSchema.required()
314076
- })).required()
314643
+ })).required(),
314644
+ deposits: import_joi2.default.object({
314645
+ enabled: import_joi2.default.boolean().required(),
314646
+ description: import_joi2.default.string().optional(),
314647
+ originators: import_joi2.default.object().pattern(import_joi2.default.string(), import_joi2.default.object({
314648
+ questionnaire: australiaDepositQuestionnaireSchema.required()
314649
+ })).required()
314650
+ }).optional()
314077
314651
  });
314078
314652
  const policyConfigSchema = import_joi2.default.object({
314079
314653
  withdraw: import_joi2.default.object({
@@ -314717,60 +315291,6 @@ var grpc14 = __toESM(require_src3(), 1);
314717
315291
  // src/handlers/execute-action/deposit.ts
314718
315292
  var grpc3 = __toESM(require_src3(), 1);
314719
315293
 
314720
- // src/helpers/deposit.ts
314721
- function depositField(deposit, fields) {
314722
- for (const field of fields) {
314723
- const value = deposit[field];
314724
- if (value !== undefined && value !== null && String(value).length > 0) {
314725
- return value;
314726
- }
314727
- }
314728
- return;
314729
- }
314730
- function normalizeDepositStatus(status) {
314731
- const normalized = String(status ?? "").trim().toLowerCase();
314732
- if (["ok", "credited", "complete", "completed", "success"].includes(normalized)) {
314733
- return "credited";
314734
- }
314735
- if (["failed", "failure", "canceled", "cancelled", "rejected"].includes(normalized)) {
314736
- return "failed";
314737
- }
314738
- if (["timeout", "timed_out", "timedout", "expired"].includes(normalized)) {
314739
- return "timed_out";
314740
- }
314741
- if (["pending", "processing", "confirming", "waiting"].includes(normalized)) {
314742
- return "pending";
314743
- }
314744
- return "pending";
314745
- }
314746
- function depositMatchesTransaction(deposit, transactionHash) {
314747
- const candidates = [
314748
- depositField(deposit, [
314749
- "txid",
314750
- "txId",
314751
- "tx_hash",
314752
- "txHash",
314753
- "transactionHash"
314754
- ]),
314755
- depositField(deposit, ["id"])
314756
- ];
314757
- return candidates.some((candidate) => String(candidate) === transactionHash);
314758
- }
314759
- function stringAmountEquals(left, right) {
314760
- const leftNum = Number(left);
314761
- const rightNum = Number(right);
314762
- if (Number.isFinite(leftNum) && Number.isFinite(rightNum)) {
314763
- return Math.abs(leftNum - rightNum) < 0.000000000001;
314764
- }
314765
- return String(left) === String(right);
314766
- }
314767
- function normalizeAddress(value) {
314768
- if (value === undefined || value === null) {
314769
- return;
314770
- }
314771
- return String(value).trim().toLowerCase();
314772
- }
314773
-
314774
315294
  // src/helpers/shared/errors.ts
314775
315295
  function getErrorMessage(error) {
314776
315296
  return error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
@@ -331295,6 +331815,7 @@ class CEXBroker {
331295
331815
  useVerity = false;
331296
331816
  otelMetrics;
331297
331817
  otelLogs;
331818
+ depositReconciler;
331298
331819
  loadEnvConfig() {
331299
331820
  log.info("\uD83D\uDD27 Loading CEX_BROKER_ environment variables:");
331300
331821
  const configMap = {};
@@ -331430,6 +331951,10 @@ class CEXBroker {
331430
331951
  unwatchFile(this.#policyFilePath);
331431
331952
  log.info(`Stopped watching policy file: ${this.#policyFilePath}`);
331432
331953
  }
331954
+ if (this.depositReconciler) {
331955
+ this.depositReconciler.stop();
331956
+ this.depositReconciler = undefined;
331957
+ }
331433
331958
  if (this.server) {
331434
331959
  await this.server.forceShutdown();
331435
331960
  }
@@ -331444,6 +331969,10 @@ class CEXBroker {
331444
331969
  if (this.server) {
331445
331970
  await this.server.forceShutdown();
331446
331971
  }
331972
+ if (this.depositReconciler) {
331973
+ this.depositReconciler.stop();
331974
+ this.depositReconciler = undefined;
331975
+ }
331447
331976
  log.info(`Running CEXBroker at ${new Date().toISOString()}`);
331448
331977
  if (this.otelMetrics?.isOtelEnabled()) {
331449
331978
  await this.otelMetrics.initialize();
@@ -331456,6 +331985,13 @@ class CEXBroker {
331456
331985
  }
331457
331986
  log.info(`Your server as started on port ${port}`);
331458
331987
  });
331988
+ this.depositReconciler = new TravelRuleDepositReconciler({
331989
+ policy: this.policy,
331990
+ brokers: this.brokers,
331991
+ config: loadTravelRuleDepositReconcilerConfigFromEnv(process.env),
331992
+ metrics: this.otelMetrics
331993
+ });
331994
+ this.depositReconciler.start();
331459
331995
  return this;
331460
331996
  }
331461
331997
  }