@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.
@@ -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,563 @@ function authenticateRequest(call, whitelistIps) {
313936
313971
  }
313937
313972
  return true;
313938
313973
  }
313974
+ // src/helpers/travel-rule-deposit-reconciler.ts
313975
+ import { request as httpRequest } from "node:http";
313976
+ import { request as httpsRequest } from "node:https";
313977
+
313978
+ // src/helpers/deposit.ts
313979
+ function depositField(deposit, fields) {
313980
+ for (const field of fields) {
313981
+ const value = deposit[field];
313982
+ if (value !== undefined && value !== null && String(value).length > 0) {
313983
+ return value;
313984
+ }
313985
+ }
313986
+ return;
313987
+ }
313988
+ function normalizeDepositStatus(status) {
313989
+ const normalized = String(status ?? "").trim().toLowerCase();
313990
+ if (["ok", "credited", "complete", "completed", "success"].includes(normalized)) {
313991
+ return "credited";
313992
+ }
313993
+ if (["failed", "failure", "canceled", "cancelled", "rejected"].includes(normalized)) {
313994
+ return "failed";
313995
+ }
313996
+ if (["timeout", "timed_out", "timedout", "expired"].includes(normalized)) {
313997
+ return "timed_out";
313998
+ }
313999
+ if (["pending", "processing", "confirming", "waiting"].includes(normalized)) {
314000
+ return "pending";
314001
+ }
314002
+ return "pending";
314003
+ }
314004
+ function depositMatchesTransaction(deposit, transactionHash) {
314005
+ const candidates = [
314006
+ depositField(deposit, [
314007
+ "txid",
314008
+ "txId",
314009
+ "tx_hash",
314010
+ "txHash",
314011
+ "transactionHash"
314012
+ ]),
314013
+ depositField(deposit, ["id"])
314014
+ ];
314015
+ return candidates.some((candidate) => String(candidate) === transactionHash);
314016
+ }
314017
+ function stringAmountEquals(left, right) {
314018
+ const leftNum = Number(left);
314019
+ const rightNum = Number(right);
314020
+ if (Number.isFinite(leftNum) && Number.isFinite(rightNum)) {
314021
+ return Math.abs(leftNum - rightNum) < 0.000000000001;
314022
+ }
314023
+ return String(left) === String(right);
314024
+ }
314025
+ function normalizeAddress(value) {
314026
+ if (value === undefined || value === null) {
314027
+ return;
314028
+ }
314029
+ return String(value).trim().toLowerCase();
314030
+ }
314031
+
314032
+ // src/helpers/travel-rule-deposit-reconciler.ts
314033
+ function toBool(value) {
314034
+ if (typeof value === "boolean")
314035
+ return value;
314036
+ if (typeof value === "number")
314037
+ return value !== 0;
314038
+ if (typeof value === "string")
314039
+ return value.trim().toLowerCase() === "true";
314040
+ return false;
314041
+ }
314042
+ function parseLocalEntityDeposit(raw) {
314043
+ const tranId = depositField(raw, ["tranId"]);
314044
+ if (tranId === undefined)
314045
+ return null;
314046
+ return {
314047
+ tranId: String(tranId),
314048
+ coin: String(depositField(raw, ["coin", "asset"]) ?? ""),
314049
+ amount: String(depositField(raw, ["amount"]) ?? ""),
314050
+ network: String(depositField(raw, ["network"]) ?? ""),
314051
+ txId: String(depositField(raw, ["txId", "txid", "tx_hash", "txHash"]) ?? ""),
314052
+ travelRuleStatus: String(depositField(raw, ["travelRuleStatusV2", "travelRuleStatus"]) ?? "").trim().toUpperCase(),
314053
+ requireQuestionnaire: toBool(raw.requireQuestionnaire ?? raw.requireQuestionnaireV2),
314054
+ raw
314055
+ };
314056
+ }
314057
+ var EVM_TX_HASH = /^0x[0-9a-fA-F]{64}$/;
314058
+ function resolveOnChainSender(rpcUrl, txHash, timeoutMs = 1e4) {
314059
+ if (!EVM_TX_HASH.test(txHash))
314060
+ return Promise.resolve(null);
314061
+ const body = JSON.stringify({
314062
+ jsonrpc: "2.0",
314063
+ id: 1,
314064
+ method: "eth_getTransactionByHash",
314065
+ params: [txHash]
314066
+ });
314067
+ const url2 = new URL(rpcUrl);
314068
+ const doRequest = url2.protocol === "http:" ? httpRequest : httpsRequest;
314069
+ return new Promise((resolve, reject) => {
314070
+ const req = doRequest(url2, {
314071
+ method: "POST",
314072
+ headers: {
314073
+ "content-type": "application/json",
314074
+ "content-length": Buffer.byteLength(body)
314075
+ },
314076
+ timeout: timeoutMs
314077
+ }, (res) => {
314078
+ const chunks = [];
314079
+ res.on("data", (chunk) => chunks.push(chunk));
314080
+ res.on("end", () => {
314081
+ const status = res.statusCode ?? 0;
314082
+ if (status < 200 || status >= 300) {
314083
+ reject(new Error(`travel_rule_rpc_http_${status}`));
314084
+ return;
314085
+ }
314086
+ try {
314087
+ const json3 = JSON.parse(Buffer.concat(chunks).toString("utf8"));
314088
+ if (json3.error) {
314089
+ reject(new Error(`travel_rule_rpc_error: ${JSON.stringify(json3.error)}`));
314090
+ return;
314091
+ }
314092
+ const from = json3.result?.from;
314093
+ resolve(typeof from === "string" && from.length > 0 ? from.toLowerCase() : null);
314094
+ } catch (parseError) {
314095
+ reject(new Error(`travel_rule_rpc_parse_error: ${String(parseError)}`));
314096
+ }
314097
+ });
314098
+ });
314099
+ req.on("error", reject);
314100
+ req.on("timeout", () => {
314101
+ req.destroy(new Error("travel_rule_rpc_timeout"));
314102
+ });
314103
+ req.write(body);
314104
+ req.end();
314105
+ });
314106
+ }
314107
+ function errorText(error) {
314108
+ if (error instanceof Error)
314109
+ return error.message;
314110
+ if (typeof error === "string")
314111
+ return error;
314112
+ try {
314113
+ return JSON.stringify(error);
314114
+ } catch {
314115
+ return String(error);
314116
+ }
314117
+ }
314118
+ function isRateLimitError(error) {
314119
+ const text = errorText(error).toLowerCase();
314120
+ return text.includes("-1003") || text.includes("too many requests") || text.includes("429") || text.includes("ddosprotection") || text.includes("ratelimitexceeded");
314121
+ }
314122
+ function isAlreadyProvidedError(error) {
314123
+ const text = errorText(error).toLowerCase();
314124
+ return text.includes("already") && (text.includes("provided") || text.includes("processed") || text.includes("passed") || text.includes("submit"));
314125
+ }
314126
+ function isAcceptedResponse(response) {
314127
+ return response.accepted !== false;
314128
+ }
314129
+ function createAccountState() {
314130
+ return {
314131
+ submittedTranIds: new Set,
314132
+ surfacedFailed: new Set,
314133
+ backoffUntil: new Map,
314134
+ rateLimitedUntil: 0
314135
+ };
314136
+ }
314137
+ function isInBackoff(state, tranId, now3) {
314138
+ const until = state.backoffUntil.get(tranId);
314139
+ return until !== undefined && now3 < until;
314140
+ }
314141
+ async function reconcileAccountOnce(deps) {
314142
+ const { state, now: now3, accountLabel } = deps;
314143
+ const outcomes = [];
314144
+ if (now3 < state.rateLimitedUntil) {
314145
+ return {
314146
+ accountLabel,
314147
+ frozenDeposits: [],
314148
+ outcomes,
314149
+ hadActionableWork: false
314150
+ };
314151
+ }
314152
+ let rawDeposits;
314153
+ try {
314154
+ rawDeposits = await deps.fetchDepositHistory();
314155
+ } catch (error) {
314156
+ if (isRateLimitError(error)) {
314157
+ state.rateLimitedUntil = now3 + deps.rateLimitCooldownMs;
314158
+ }
314159
+ return {
314160
+ accountLabel,
314161
+ frozenDeposits: [],
314162
+ outcomes: [{ kind: "poll-error", error: errorText(error) }],
314163
+ hadActionableWork: false
314164
+ };
314165
+ }
314166
+ const deposits = rawDeposits.map(parseLocalEntityDeposit).filter((d) => d !== null);
314167
+ const frozen = deposits.filter((d) => d.requireQuestionnaire && d.travelRuleStatus === "PENDING");
314168
+ for (const deposit of deposits) {
314169
+ if (deposit.travelRuleStatus === "FAILED" && !state.surfacedFailed.has(deposit.tranId)) {
314170
+ state.surfacedFailed.add(deposit.tranId);
314171
+ outcomes.push({ kind: "failed-terminal", deposit });
314172
+ }
314173
+ }
314174
+ const candidates = frozen.filter((d) => !state.submittedTranIds.has(d.tranId) && !isInBackoff(state, d.tranId, now3));
314175
+ if (candidates.length === 0) {
314176
+ return {
314177
+ accountLabel,
314178
+ frozenDeposits: frozen,
314179
+ outcomes,
314180
+ hadActionableWork: false
314181
+ };
314182
+ }
314183
+ let country = null;
314184
+ try {
314185
+ country = await deps.fetchQuestionnaireCountry();
314186
+ } catch (error) {
314187
+ if (isRateLimitError(error)) {
314188
+ state.rateLimitedUntil = now3 + deps.rateLimitCooldownMs;
314189
+ }
314190
+ country = null;
314191
+ }
314192
+ if (country !== deps.expectedCountry) {
314193
+ for (const deposit of candidates) {
314194
+ outcomes.push({ kind: "entity-drift", deposit, country });
314195
+ }
314196
+ return {
314197
+ accountLabel,
314198
+ frozenDeposits: frozen,
314199
+ outcomes,
314200
+ hadActionableWork: true
314201
+ };
314202
+ }
314203
+ for (const deposit of candidates) {
314204
+ if (now3 < state.rateLimitedUntil)
314205
+ break;
314206
+ let sender = null;
314207
+ let resolveError;
314208
+ try {
314209
+ sender = await deps.resolveSender(deposit.network, deposit.txId);
314210
+ } catch (error) {
314211
+ sender = null;
314212
+ resolveError = errorText(error);
314213
+ if (isRateLimitError(error)) {
314214
+ state.rateLimitedUntil = now3 + deps.rateLimitCooldownMs;
314215
+ }
314216
+ }
314217
+ if (!sender) {
314218
+ state.backoffUntil.set(deposit.tranId, now3 + deps.failureBackoffMs);
314219
+ outcomes.push({ kind: "unproven-origin", deposit, error: resolveError });
314220
+ continue;
314221
+ }
314222
+ const questionnaire = deps.resolveQuestionnaire(deps.depositConfig, sender);
314223
+ if (!questionnaire) {
314224
+ state.backoffUntil.set(deposit.tranId, now3 + deps.failureBackoffMs);
314225
+ outcomes.push({ kind: "undeclared-origin", deposit, sender });
314226
+ continue;
314227
+ }
314228
+ try {
314229
+ const response = await deps.submitProvideInfo(deposit.tranId, questionnaire);
314230
+ if (isAcceptedResponse(response)) {
314231
+ state.submittedTranIds.add(deposit.tranId);
314232
+ state.backoffUntil.delete(deposit.tranId);
314233
+ outcomes.push({ kind: "submitted", deposit, sender });
314234
+ } else {
314235
+ state.backoffUntil.set(deposit.tranId, now3 + deps.failureBackoffMs);
314236
+ outcomes.push({
314237
+ kind: "submit-error",
314238
+ deposit,
314239
+ error: JSON.stringify(response)
314240
+ });
314241
+ }
314242
+ } catch (error) {
314243
+ if (isAlreadyProvidedError(error)) {
314244
+ state.submittedTranIds.add(deposit.tranId);
314245
+ state.backoffUntil.delete(deposit.tranId);
314246
+ outcomes.push({ kind: "already-provided", deposit, sender });
314247
+ } else {
314248
+ if (isRateLimitError(error)) {
314249
+ state.rateLimitedUntil = now3 + deps.rateLimitCooldownMs;
314250
+ }
314251
+ state.backoffUntil.set(deposit.tranId, now3 + deps.failureBackoffMs);
314252
+ outcomes.push({
314253
+ kind: "submit-error",
314254
+ deposit,
314255
+ error: errorText(error)
314256
+ });
314257
+ }
314258
+ }
314259
+ }
314260
+ return {
314261
+ accountLabel,
314262
+ frozenDeposits: frozen,
314263
+ outcomes,
314264
+ hadActionableWork: true
314265
+ };
314266
+ }
314267
+ var DEFAULTS = {
314268
+ pollIntervalActiveMs: 60000,
314269
+ pollIntervalIdleMs: 600000,
314270
+ expectedQuestionnaireCountry: "AU",
314271
+ failureBackoffMs: 15 * 60000,
314272
+ rateLimitCooldownMs: 5 * 60000
314273
+ };
314274
+ function envSecondsToMs(env, key, fallbackMs) {
314275
+ const raw = env[key];
314276
+ if (raw === undefined)
314277
+ return fallbackMs;
314278
+ const secs = Number(raw);
314279
+ return Number.isFinite(secs) && secs > 0 ? secs * 1000 : fallbackMs;
314280
+ }
314281
+ function loadTravelRuleDepositReconcilerConfigFromEnv(env) {
314282
+ const rpcUrlsByNetwork = {};
314283
+ const prefix = "TRAVEL_RULE_RPC_URL_";
314284
+ for (const [key, value] of Object.entries(env)) {
314285
+ if (key.startsWith(prefix) && value) {
314286
+ rpcUrlsByNetwork[key.slice(prefix.length).toUpperCase()] = value;
314287
+ }
314288
+ }
314289
+ return {
314290
+ rpcUrlsByNetwork,
314291
+ pollIntervalActiveMs: envSecondsToMs(env, "TRAVEL_RULE_DEPOSIT_POLL_ACTIVE_SECS", DEFAULTS.pollIntervalActiveMs),
314292
+ pollIntervalIdleMs: envSecondsToMs(env, "TRAVEL_RULE_DEPOSIT_POLL_IDLE_SECS", DEFAULTS.pollIntervalIdleMs),
314293
+ expectedQuestionnaireCountry: env.TRAVEL_RULE_QUESTIONNAIRE_COUNTRY?.trim().toUpperCase() || DEFAULTS.expectedQuestionnaireCountry,
314294
+ failureBackoffMs: envSecondsToMs(env, "TRAVEL_RULE_DEPOSIT_FAILURE_BACKOFF_SECS", DEFAULTS.failureBackoffMs),
314295
+ rateLimitCooldownMs: envSecondsToMs(env, "TRAVEL_RULE_DEPOSIT_RATE_LIMIT_COOLDOWN_SECS", DEFAULTS.rateLimitCooldownMs)
314296
+ };
314297
+ }
314298
+ function parseQuestionnaireCountry(response) {
314299
+ if (!response || typeof response !== "object")
314300
+ return null;
314301
+ const code = response.questionnaireCountryCode;
314302
+ return typeof code === "string" ? code.trim().toUpperCase() : null;
314303
+ }
314304
+
314305
+ class TravelRuleDepositReconciler {
314306
+ params;
314307
+ #timer = null;
314308
+ #stopped = false;
314309
+ #running = false;
314310
+ #states = new Map;
314311
+ constructor(params) {
314312
+ this.params = params;
314313
+ }
314314
+ static hasEnabledExchange(policy) {
314315
+ return (policy.travelRule?.rule ?? []).some((rule) => getEnabledTravelRuleDepositConfig(policy, rule.exchange));
314316
+ }
314317
+ start() {
314318
+ if (this.#timer || this.#stopped)
314319
+ return;
314320
+ if (!TravelRuleDepositReconciler.hasEnabledExchange(this.params.policy)) {
314321
+ return;
314322
+ }
314323
+ if (Object.keys(this.params.config.rpcUrlsByNetwork).length === 0) {
314324
+ 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).");
314325
+ }
314326
+ log.info("\uD83D\uDEC2 Travel-rule deposit auto-clear reconciler started");
314327
+ this.#scheduleImmediate();
314328
+ }
314329
+ stop() {
314330
+ this.#stopped = true;
314331
+ if (this.#timer) {
314332
+ clearTimeout(this.#timer);
314333
+ this.#timer = null;
314334
+ }
314335
+ }
314336
+ #scheduleImmediate() {
314337
+ this.#timer = setTimeout(() => void this.#tick(), 0);
314338
+ }
314339
+ #stateFor(key) {
314340
+ let state = this.#states.get(key);
314341
+ if (!state) {
314342
+ state = createAccountState();
314343
+ this.#states.set(key, state);
314344
+ }
314345
+ return state;
314346
+ }
314347
+ #targets() {
314348
+ const targets = [];
314349
+ for (const rule of this.params.policy.travelRule?.rule ?? []) {
314350
+ const depositConfig = getEnabledTravelRuleDepositConfig(this.params.policy, rule.exchange);
314351
+ if (!depositConfig)
314352
+ continue;
314353
+ const exchangeId = rule.exchange.trim().toLowerCase();
314354
+ const pool = this.params.brokers[exchangeId];
314355
+ if (!pool)
314356
+ continue;
314357
+ for (const account of [pool.primary, ...pool.secondaryBrokers]) {
314358
+ targets.push({ exchangeId, account, depositConfig });
314359
+ }
314360
+ }
314361
+ return targets;
314362
+ }
314363
+ async#tick() {
314364
+ if (this.#stopped || this.#running)
314365
+ return;
314366
+ this.#running = true;
314367
+ let anyActionable = false;
314368
+ try {
314369
+ for (const target of this.#targets()) {
314370
+ if (this.#stopped)
314371
+ break;
314372
+ const report = await this.#reconcileTarget(target);
314373
+ this.#emit(target, report);
314374
+ if (report.hadActionableWork)
314375
+ anyActionable = true;
314376
+ }
314377
+ } catch (error) {
314378
+ log.error("Travel-rule deposit reconciler tick failed", error);
314379
+ } finally {
314380
+ this.#running = false;
314381
+ if (!this.#stopped) {
314382
+ const delay = anyActionable ? this.params.config.pollIntervalActiveMs : this.params.config.pollIntervalIdleMs;
314383
+ this.#timer = setTimeout(() => void this.#tick(), delay);
314384
+ }
314385
+ }
314386
+ }
314387
+ #reconcileTarget(target) {
314388
+ const exchange = target.account.exchange;
314389
+ const accountLabel = `${target.exchangeId}:${target.account.label}`;
314390
+ const { config } = this.params;
314391
+ return reconcileAccountOnce({
314392
+ accountLabel,
314393
+ depositConfig: target.depositConfig,
314394
+ expectedCountry: config.expectedQuestionnaireCountry,
314395
+ failureBackoffMs: config.failureBackoffMs,
314396
+ rateLimitCooldownMs: config.rateLimitCooldownMs,
314397
+ now: Date.now(),
314398
+ state: this.#stateFor(accountLabel),
314399
+ fetchDepositHistory: async () => {
314400
+ if (typeof exchange.sapiGetLocalentityDepositHistory !== "function") {
314401
+ throw new Error("binance_localentity_deposit_history_unavailable: endpoint not registered");
314402
+ }
314403
+ const result = await exchange.sapiGetLocalentityDepositHistory({});
314404
+ return Array.isArray(result) ? result : [];
314405
+ },
314406
+ fetchQuestionnaireCountry: async () => {
314407
+ if (typeof exchange.sapiGetLocalentityQuestionnaireRequirements !== "function") {
314408
+ return null;
314409
+ }
314410
+ const result = await exchange.sapiGetLocalentityQuestionnaireRequirements({});
314411
+ return parseQuestionnaireCountry(result);
314412
+ },
314413
+ resolveSender: (network, txId) => {
314414
+ const url2 = config.rpcUrlsByNetwork[network.trim().toUpperCase()];
314415
+ if (!url2)
314416
+ return Promise.resolve(null);
314417
+ return resolveOnChainSender(url2, txId);
314418
+ },
314419
+ submitProvideInfo: (tranId, questionnaire) => {
314420
+ if (typeof exchange.sapiPutLocalentityDepositProvideInfo !== "function") {
314421
+ throw new Error("binance_localentity_provide_info_unavailable: endpoint not registered");
314422
+ }
314423
+ return exchange.sapiPutLocalentityDepositProvideInfo({
314424
+ tranId,
314425
+ questionnaire: JSON.stringify(questionnaire)
314426
+ });
314427
+ },
314428
+ resolveQuestionnaire: resolveDepositOriginatorQuestionnaire
314429
+ });
314430
+ }
314431
+ #emit(target, report) {
314432
+ const { metrics } = this.params;
314433
+ const base2 = { exchange: target.exchangeId, account: target.account.label };
314434
+ metrics?.recordGauge("travel_rule_frozen_deposits", report.frozenDeposits.length, base2);
314435
+ for (const outcome of report.outcomes) {
314436
+ switch (outcome.kind) {
314437
+ case "submitted":
314438
+ case "already-provided": {
314439
+ log.info("\uD83D\uDEC2 Travel-rule deposit auto-declared", {
314440
+ result: outcome.kind,
314441
+ account: report.accountLabel,
314442
+ tranId: outcome.deposit.tranId,
314443
+ coin: outcome.deposit.coin,
314444
+ amount: outcome.deposit.amount,
314445
+ network: outcome.deposit.network,
314446
+ txId: outcome.deposit.txId,
314447
+ originator: outcome.sender
314448
+ });
314449
+ metrics?.recordCounter("travel_rule_deposit_submissions_total", 1, {
314450
+ ...base2,
314451
+ coin: outcome.deposit.coin,
314452
+ result: outcome.kind
314453
+ });
314454
+ break;
314455
+ }
314456
+ case "undeclared-origin":
314457
+ log.warn("\uD83D\uDEC2 Travel-rule deposit from UNDECLARED originator — left frozen", {
314458
+ account: report.accountLabel,
314459
+ tranId: outcome.deposit.tranId,
314460
+ coin: outcome.deposit.coin,
314461
+ amount: outcome.deposit.amount,
314462
+ sender: outcome.sender
314463
+ });
314464
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
314465
+ ...base2,
314466
+ reason: "undeclared_origin"
314467
+ });
314468
+ break;
314469
+ case "unproven-origin":
314470
+ log.warn("\uD83D\uDEC2 Travel-rule deposit origin UNPROVEN (tx sender unresolved) — left frozen", {
314471
+ account: report.accountLabel,
314472
+ tranId: outcome.deposit.tranId,
314473
+ coin: outcome.deposit.coin,
314474
+ network: outcome.deposit.network,
314475
+ txId: outcome.deposit.txId,
314476
+ rpcError: outcome.error ?? null
314477
+ });
314478
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
314479
+ ...base2,
314480
+ reason: "unproven_origin"
314481
+ });
314482
+ break;
314483
+ case "entity-drift":
314484
+ log.warn("\uD83D\uDEC2 Travel-rule deposit skipped — questionnaire entity is not the expected country", {
314485
+ account: report.accountLabel,
314486
+ tranId: outcome.deposit.tranId,
314487
+ observedCountry: outcome.country,
314488
+ expectedCountry: this.params.config.expectedQuestionnaireCountry
314489
+ });
314490
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
314491
+ ...base2,
314492
+ reason: "entity_drift"
314493
+ });
314494
+ break;
314495
+ case "failed-terminal":
314496
+ log.warn("\uD83D\uDEC2 Travel-rule deposit is FAILED (terminal) — needs manual handling", {
314497
+ account: report.accountLabel,
314498
+ tranId: outcome.deposit.tranId,
314499
+ coin: outcome.deposit.coin
314500
+ });
314501
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
314502
+ ...base2,
314503
+ reason: "failed_status"
314504
+ });
314505
+ break;
314506
+ case "submit-error":
314507
+ log.error("\uD83D\uDEC2 Travel-rule deposit provide-info failed", {
314508
+ account: report.accountLabel,
314509
+ tranId: outcome.deposit.tranId,
314510
+ error: outcome.error
314511
+ });
314512
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
314513
+ ...base2,
314514
+ reason: "submit_error"
314515
+ });
314516
+ break;
314517
+ case "poll-error":
314518
+ log.error("\uD83D\uDEC2 Travel-rule deposit history poll failed", {
314519
+ account: report.accountLabel,
314520
+ error: outcome.error
314521
+ });
314522
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
314523
+ ...base2,
314524
+ reason: "poll_error"
314525
+ });
314526
+ break;
314527
+ }
314528
+ }
314529
+ }
314530
+ }
313939
314531
  // src/helpers/verity.ts
313940
314532
  var import_verity_client = __toESM(require_dist(), 1);
313941
314533
 
@@ -314073,7 +314665,14 @@ function loadPolicy(policyPath) {
314073
314665
  description: import_joi2.default.string().optional(),
314074
314666
  addresses: import_joi2.default.object().pattern(import_joi2.default.string(), import_joi2.default.object({
314075
314667
  questionnaire: australiaQuestionnaireSchema.required()
314076
- })).required()
314668
+ })).required(),
314669
+ deposits: import_joi2.default.object({
314670
+ enabled: import_joi2.default.boolean().required(),
314671
+ description: import_joi2.default.string().optional(),
314672
+ originators: import_joi2.default.object().pattern(import_joi2.default.string(), import_joi2.default.object({
314673
+ questionnaire: australiaDepositQuestionnaireSchema.required()
314674
+ })).required()
314675
+ }).optional()
314077
314676
  });
314078
314677
  const policyConfigSchema = import_joi2.default.object({
314079
314678
  withdraw: import_joi2.default.object({
@@ -314717,60 +315316,6 @@ var grpc14 = __toESM(require_src3(), 1);
314717
315316
  // src/handlers/execute-action/deposit.ts
314718
315317
  var grpc3 = __toESM(require_src3(), 1);
314719
315318
 
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
315319
  // src/helpers/shared/errors.ts
314775
315320
  function getErrorMessage(error) {
314776
315321
  return error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
@@ -331295,6 +331840,7 @@ class CEXBroker {
331295
331840
  useVerity = false;
331296
331841
  otelMetrics;
331297
331842
  otelLogs;
331843
+ depositReconciler;
331298
331844
  loadEnvConfig() {
331299
331845
  log.info("\uD83D\uDD27 Loading CEX_BROKER_ environment variables:");
331300
331846
  const configMap = {};
@@ -331430,6 +331976,10 @@ class CEXBroker {
331430
331976
  unwatchFile(this.#policyFilePath);
331431
331977
  log.info(`Stopped watching policy file: ${this.#policyFilePath}`);
331432
331978
  }
331979
+ if (this.depositReconciler) {
331980
+ this.depositReconciler.stop();
331981
+ this.depositReconciler = undefined;
331982
+ }
331433
331983
  if (this.server) {
331434
331984
  await this.server.forceShutdown();
331435
331985
  }
@@ -331444,6 +331994,10 @@ class CEXBroker {
331444
331994
  if (this.server) {
331445
331995
  await this.server.forceShutdown();
331446
331996
  }
331997
+ if (this.depositReconciler) {
331998
+ this.depositReconciler.stop();
331999
+ this.depositReconciler = undefined;
332000
+ }
331447
332001
  log.info(`Running CEXBroker at ${new Date().toISOString()}`);
331448
332002
  if (this.otelMetrics?.isOtelEnabled()) {
331449
332003
  await this.otelMetrics.initialize();
@@ -331456,6 +332010,13 @@ class CEXBroker {
331456
332010
  }
331457
332011
  log.info(`Your server as started on port ${port}`);
331458
332012
  });
332013
+ this.depositReconciler = new TravelRuleDepositReconciler({
332014
+ policy: this.policy,
332015
+ brokers: this.brokers,
332016
+ config: loadTravelRuleDepositReconcilerConfigFromEnv(process.env),
332017
+ metrics: this.otelMetrics
332018
+ });
332019
+ this.depositReconciler.start();
331459
332020
  return this;
331460
332021
  }
331461
332022
  }