@usherlabs/cex-broker 0.2.15 → 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.
package/dist/index.js CHANGED
@@ -78926,7 +78926,7 @@ class binance extends binance_default {
78926
78926
  }
78927
78927
  if (api === "sapi" && path === "asset/dust") {
78928
78928
  query = this.urlencodeWithArrayRepeat(extendedParams);
78929
- } else if (path === "batchOrders" || path.indexOf("sub-account") >= 0 || path === "capital/withdraw/apply" || path.indexOf("staking") >= 0 || path.indexOf("simple-earn") >= 0) {
78929
+ } 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) {
78930
78930
  if (method === "DELETE" && path === "batchOrders") {
78931
78931
  const orderidlist = this.safeList(extendedParams, "orderidlist", []);
78932
78932
  const origclientorderidlist = this.safeList(extendedParams, "origclientorderidlist", []);
@@ -272836,6 +272836,40 @@ function registerBinanceTravelRuleWithdrawEndpoint(exchange) {
272836
272836
  }
272837
272837
  exchange.defineRestApi({ sapi: { post: { "localentity/withdraw/apply": 4.0002 } } }, "request");
272838
272838
  }
272839
+ var australiaDepositQuestionnaireSchema = import_joi.default.object({
272840
+ depositOriginator: import_joi.default.number().valid(1).required(),
272841
+ receiveFrom: import_joi.default.number().valid(1).required(),
272842
+ declaration: import_joi.default.boolean().valid(true).required()
272843
+ });
272844
+ function getEnabledTravelRuleDepositConfig(policy, exchange) {
272845
+ const rules = policy.travelRule?.rule ?? [];
272846
+ const exchangeNorm = exchange.trim().toUpperCase();
272847
+ const entry = rules.find((rule) => rule.exchange.trim().toUpperCase() === exchangeNorm);
272848
+ const deposits = entry?.deposits;
272849
+ if (!deposits || !deposits.enabled) {
272850
+ return null;
272851
+ }
272852
+ return deposits;
272853
+ }
272854
+ function resolveDepositOriginatorQuestionnaire(config, senderAddress) {
272855
+ const senderNorm = senderAddress.trim().toLowerCase();
272856
+ const match = Object.entries(config.originators).find(([address]) => address.trim().toLowerCase() === senderNorm);
272857
+ return match ? match[1].questionnaire : null;
272858
+ }
272859
+ function registerBinanceTravelRuleDepositEndpoints(exchange) {
272860
+ if (exchange.id !== "binance") {
272861
+ return;
272862
+ }
272863
+ exchange.defineRestApi({
272864
+ sapi: {
272865
+ get: {
272866
+ "localentity/deposit/history": 1,
272867
+ "localentity/questionnaire-requirements": 1
272868
+ },
272869
+ put: { "localentity/deposit/provide-info": 4.0002 }
272870
+ }
272871
+ }, "request");
272872
+ }
272839
272873
  async function withdrawViaLocalEntity(broker, args) {
272840
272874
  const exchange = broker;
272841
272875
  if (typeof exchange.sapiPostLocalentityWithdrawApply !== "function") {
@@ -272884,6 +272918,7 @@ function applyCommonExchangeConfig(exchange) {
272884
272918
  adjustForTimeDifference: true
272885
272919
  });
272886
272920
  registerBinanceTravelRuleWithdrawEndpoint(exchange);
272921
+ registerBinanceTravelRuleDepositEndpoints(exchange);
272887
272922
  }
272888
272923
  function createBroker(cex3, credsOrMetadata) {
272889
272924
  let apiKey;
@@ -273189,6 +273224,538 @@ function authenticateRequest(call, whitelistIps) {
273189
273224
  }
273190
273225
  return true;
273191
273226
  }
273227
+ // src/helpers/deposit.ts
273228
+ function depositField(deposit, fields) {
273229
+ for (const field of fields) {
273230
+ const value = deposit[field];
273231
+ if (value !== undefined && value !== null && String(value).length > 0) {
273232
+ return value;
273233
+ }
273234
+ }
273235
+ return;
273236
+ }
273237
+ function normalizeDepositStatus(status) {
273238
+ const normalized = String(status ?? "").trim().toLowerCase();
273239
+ if (["ok", "credited", "complete", "completed", "success"].includes(normalized)) {
273240
+ return "credited";
273241
+ }
273242
+ if (["failed", "failure", "canceled", "cancelled", "rejected"].includes(normalized)) {
273243
+ return "failed";
273244
+ }
273245
+ if (["timeout", "timed_out", "timedout", "expired"].includes(normalized)) {
273246
+ return "timed_out";
273247
+ }
273248
+ if (["pending", "processing", "confirming", "waiting"].includes(normalized)) {
273249
+ return "pending";
273250
+ }
273251
+ return "pending";
273252
+ }
273253
+ function depositMatchesTransaction(deposit, transactionHash) {
273254
+ const candidates = [
273255
+ depositField(deposit, [
273256
+ "txid",
273257
+ "txId",
273258
+ "tx_hash",
273259
+ "txHash",
273260
+ "transactionHash"
273261
+ ]),
273262
+ depositField(deposit, ["id"])
273263
+ ];
273264
+ return candidates.some((candidate) => String(candidate) === transactionHash);
273265
+ }
273266
+ function stringAmountEquals(left, right) {
273267
+ const leftNum = Number(left);
273268
+ const rightNum = Number(right);
273269
+ if (Number.isFinite(leftNum) && Number.isFinite(rightNum)) {
273270
+ return Math.abs(leftNum - rightNum) < 0.000000000001;
273271
+ }
273272
+ return String(left) === String(right);
273273
+ }
273274
+ function normalizeAddress(value) {
273275
+ if (value === undefined || value === null) {
273276
+ return;
273277
+ }
273278
+ return String(value).trim().toLowerCase();
273279
+ }
273280
+
273281
+ // src/helpers/travel-rule-deposit-reconciler.ts
273282
+ function toBool(value) {
273283
+ if (typeof value === "boolean")
273284
+ return value;
273285
+ if (typeof value === "number")
273286
+ return value !== 0;
273287
+ if (typeof value === "string")
273288
+ return value.trim().toLowerCase() === "true";
273289
+ return false;
273290
+ }
273291
+ function parseLocalEntityDeposit(raw) {
273292
+ const tranId = depositField(raw, ["tranId"]);
273293
+ if (tranId === undefined)
273294
+ return null;
273295
+ return {
273296
+ tranId: String(tranId),
273297
+ coin: String(depositField(raw, ["coin", "asset"]) ?? ""),
273298
+ amount: String(depositField(raw, ["amount"]) ?? ""),
273299
+ network: String(depositField(raw, ["network"]) ?? ""),
273300
+ txId: String(depositField(raw, ["txId", "txid", "tx_hash", "txHash"]) ?? ""),
273301
+ travelRuleStatus: String(depositField(raw, ["travelRuleStatusV2", "travelRuleStatus"]) ?? "").trim().toUpperCase(),
273302
+ requireQuestionnaire: toBool(raw.requireQuestionnaire ?? raw.requireQuestionnaireV2),
273303
+ raw
273304
+ };
273305
+ }
273306
+ var EVM_TX_HASH = /^0x[0-9a-fA-F]{64}$/;
273307
+ async function resolveOnChainSender(rpcUrl, txHash, timeoutMs = 1e4) {
273308
+ if (!EVM_TX_HASH.test(txHash))
273309
+ return null;
273310
+ const controller = new AbortController;
273311
+ const timeout2 = setTimeout(() => controller.abort(), timeoutMs);
273312
+ let response;
273313
+ try {
273314
+ response = await fetch(rpcUrl, {
273315
+ method: "POST",
273316
+ headers: { "content-type": "application/json" },
273317
+ signal: controller.signal,
273318
+ body: JSON.stringify({
273319
+ jsonrpc: "2.0",
273320
+ id: 1,
273321
+ method: "eth_getTransactionByHash",
273322
+ params: [txHash]
273323
+ })
273324
+ });
273325
+ } finally {
273326
+ clearTimeout(timeout2);
273327
+ }
273328
+ if (!response.ok) {
273329
+ throw new Error(`travel_rule_rpc_http_${response.status}`);
273330
+ }
273331
+ const json3 = await response.json();
273332
+ if (json3.error) {
273333
+ throw new Error(`travel_rule_rpc_error: ${JSON.stringify(json3.error)}`);
273334
+ }
273335
+ const from = json3.result?.from;
273336
+ return typeof from === "string" && from.length > 0 ? from.toLowerCase() : null;
273337
+ }
273338
+ function errorText(error) {
273339
+ if (error instanceof Error)
273340
+ return error.message;
273341
+ if (typeof error === "string")
273342
+ return error;
273343
+ try {
273344
+ return JSON.stringify(error);
273345
+ } catch {
273346
+ return String(error);
273347
+ }
273348
+ }
273349
+ function isRateLimitError(error) {
273350
+ const text = errorText(error).toLowerCase();
273351
+ return text.includes("-1003") || text.includes("too many requests") || text.includes("429") || text.includes("ddosprotection") || text.includes("ratelimitexceeded");
273352
+ }
273353
+ function isAlreadyProvidedError(error) {
273354
+ const text = errorText(error).toLowerCase();
273355
+ return text.includes("already") && (text.includes("provided") || text.includes("processed") || text.includes("passed") || text.includes("submit"));
273356
+ }
273357
+ function isAcceptedResponse(response) {
273358
+ return response.accepted !== false;
273359
+ }
273360
+ function createAccountState() {
273361
+ return {
273362
+ submittedTranIds: new Set,
273363
+ surfacedFailed: new Set,
273364
+ backoffUntil: new Map,
273365
+ rateLimitedUntil: 0
273366
+ };
273367
+ }
273368
+ function isInBackoff(state, tranId, now3) {
273369
+ const until = state.backoffUntil.get(tranId);
273370
+ return until !== undefined && now3 < until;
273371
+ }
273372
+ async function reconcileAccountOnce(deps) {
273373
+ const { state, now: now3, accountLabel } = deps;
273374
+ const outcomes = [];
273375
+ if (now3 < state.rateLimitedUntil) {
273376
+ return {
273377
+ accountLabel,
273378
+ frozenDeposits: [],
273379
+ outcomes,
273380
+ hadActionableWork: false
273381
+ };
273382
+ }
273383
+ let rawDeposits;
273384
+ try {
273385
+ rawDeposits = await deps.fetchDepositHistory();
273386
+ } catch (error) {
273387
+ if (isRateLimitError(error)) {
273388
+ state.rateLimitedUntil = now3 + deps.rateLimitCooldownMs;
273389
+ }
273390
+ return {
273391
+ accountLabel,
273392
+ frozenDeposits: [],
273393
+ outcomes: [{ kind: "poll-error", error: errorText(error) }],
273394
+ hadActionableWork: false
273395
+ };
273396
+ }
273397
+ const deposits = rawDeposits.map(parseLocalEntityDeposit).filter((d) => d !== null);
273398
+ const frozen = deposits.filter((d) => d.requireQuestionnaire && d.travelRuleStatus === "PENDING");
273399
+ for (const deposit of deposits) {
273400
+ if (deposit.travelRuleStatus === "FAILED" && !state.surfacedFailed.has(deposit.tranId)) {
273401
+ state.surfacedFailed.add(deposit.tranId);
273402
+ outcomes.push({ kind: "failed-terminal", deposit });
273403
+ }
273404
+ }
273405
+ const candidates = frozen.filter((d) => !state.submittedTranIds.has(d.tranId) && !isInBackoff(state, d.tranId, now3));
273406
+ if (candidates.length === 0) {
273407
+ return {
273408
+ accountLabel,
273409
+ frozenDeposits: frozen,
273410
+ outcomes,
273411
+ hadActionableWork: false
273412
+ };
273413
+ }
273414
+ let country = null;
273415
+ try {
273416
+ country = await deps.fetchQuestionnaireCountry();
273417
+ } catch (error) {
273418
+ if (isRateLimitError(error)) {
273419
+ state.rateLimitedUntil = now3 + deps.rateLimitCooldownMs;
273420
+ }
273421
+ country = null;
273422
+ }
273423
+ if (country !== deps.expectedCountry) {
273424
+ for (const deposit of candidates) {
273425
+ outcomes.push({ kind: "entity-drift", deposit, country });
273426
+ }
273427
+ return {
273428
+ accountLabel,
273429
+ frozenDeposits: frozen,
273430
+ outcomes,
273431
+ hadActionableWork: true
273432
+ };
273433
+ }
273434
+ for (const deposit of candidates) {
273435
+ if (now3 < state.rateLimitedUntil)
273436
+ break;
273437
+ let sender = null;
273438
+ try {
273439
+ sender = await deps.resolveSender(deposit.network, deposit.txId);
273440
+ } catch (error) {
273441
+ sender = null;
273442
+ if (isRateLimitError(error)) {
273443
+ state.rateLimitedUntil = now3 + deps.rateLimitCooldownMs;
273444
+ }
273445
+ }
273446
+ if (!sender) {
273447
+ state.backoffUntil.set(deposit.tranId, now3 + deps.failureBackoffMs);
273448
+ outcomes.push({ kind: "unproven-origin", deposit });
273449
+ continue;
273450
+ }
273451
+ const questionnaire = deps.resolveQuestionnaire(deps.depositConfig, sender);
273452
+ if (!questionnaire) {
273453
+ state.backoffUntil.set(deposit.tranId, now3 + deps.failureBackoffMs);
273454
+ outcomes.push({ kind: "undeclared-origin", deposit, sender });
273455
+ continue;
273456
+ }
273457
+ try {
273458
+ const response = await deps.submitProvideInfo(deposit.tranId, questionnaire);
273459
+ if (isAcceptedResponse(response)) {
273460
+ state.submittedTranIds.add(deposit.tranId);
273461
+ state.backoffUntil.delete(deposit.tranId);
273462
+ outcomes.push({ kind: "submitted", deposit, sender });
273463
+ } else {
273464
+ state.backoffUntil.set(deposit.tranId, now3 + deps.failureBackoffMs);
273465
+ outcomes.push({
273466
+ kind: "submit-error",
273467
+ deposit,
273468
+ error: JSON.stringify(response)
273469
+ });
273470
+ }
273471
+ } catch (error) {
273472
+ if (isAlreadyProvidedError(error)) {
273473
+ state.submittedTranIds.add(deposit.tranId);
273474
+ state.backoffUntil.delete(deposit.tranId);
273475
+ outcomes.push({ kind: "already-provided", deposit, sender });
273476
+ } else {
273477
+ if (isRateLimitError(error)) {
273478
+ state.rateLimitedUntil = now3 + deps.rateLimitCooldownMs;
273479
+ }
273480
+ state.backoffUntil.set(deposit.tranId, now3 + deps.failureBackoffMs);
273481
+ outcomes.push({
273482
+ kind: "submit-error",
273483
+ deposit,
273484
+ error: errorText(error)
273485
+ });
273486
+ }
273487
+ }
273488
+ }
273489
+ return {
273490
+ accountLabel,
273491
+ frozenDeposits: frozen,
273492
+ outcomes,
273493
+ hadActionableWork: true
273494
+ };
273495
+ }
273496
+ var DEFAULTS = {
273497
+ pollIntervalActiveMs: 60000,
273498
+ pollIntervalIdleMs: 600000,
273499
+ expectedQuestionnaireCountry: "AU",
273500
+ failureBackoffMs: 15 * 60000,
273501
+ rateLimitCooldownMs: 5 * 60000
273502
+ };
273503
+ function envSecondsToMs(env, key, fallbackMs) {
273504
+ const raw = env[key];
273505
+ if (raw === undefined)
273506
+ return fallbackMs;
273507
+ const secs = Number(raw);
273508
+ return Number.isFinite(secs) && secs > 0 ? secs * 1000 : fallbackMs;
273509
+ }
273510
+ function loadTravelRuleDepositReconcilerConfigFromEnv(env) {
273511
+ const rpcUrlsByNetwork = {};
273512
+ const prefix = "TRAVEL_RULE_RPC_URL_";
273513
+ for (const [key, value] of Object.entries(env)) {
273514
+ if (key.startsWith(prefix) && value) {
273515
+ rpcUrlsByNetwork[key.slice(prefix.length).toUpperCase()] = value;
273516
+ }
273517
+ }
273518
+ return {
273519
+ rpcUrlsByNetwork,
273520
+ pollIntervalActiveMs: envSecondsToMs(env, "TRAVEL_RULE_DEPOSIT_POLL_ACTIVE_SECS", DEFAULTS.pollIntervalActiveMs),
273521
+ pollIntervalIdleMs: envSecondsToMs(env, "TRAVEL_RULE_DEPOSIT_POLL_IDLE_SECS", DEFAULTS.pollIntervalIdleMs),
273522
+ expectedQuestionnaireCountry: env.TRAVEL_RULE_QUESTIONNAIRE_COUNTRY?.trim().toUpperCase() || DEFAULTS.expectedQuestionnaireCountry,
273523
+ failureBackoffMs: envSecondsToMs(env, "TRAVEL_RULE_DEPOSIT_FAILURE_BACKOFF_SECS", DEFAULTS.failureBackoffMs),
273524
+ rateLimitCooldownMs: envSecondsToMs(env, "TRAVEL_RULE_DEPOSIT_RATE_LIMIT_COOLDOWN_SECS", DEFAULTS.rateLimitCooldownMs)
273525
+ };
273526
+ }
273527
+ function parseQuestionnaireCountry(response) {
273528
+ if (!response || typeof response !== "object")
273529
+ return null;
273530
+ const code = response.questionnaireCountryCode;
273531
+ return typeof code === "string" ? code.trim().toUpperCase() : null;
273532
+ }
273533
+
273534
+ class TravelRuleDepositReconciler {
273535
+ params;
273536
+ #timer = null;
273537
+ #stopped = false;
273538
+ #running = false;
273539
+ #states = new Map;
273540
+ constructor(params) {
273541
+ this.params = params;
273542
+ }
273543
+ static hasEnabledExchange(policy) {
273544
+ return (policy.travelRule?.rule ?? []).some((rule) => getEnabledTravelRuleDepositConfig(policy, rule.exchange));
273545
+ }
273546
+ start() {
273547
+ if (this.#timer || this.#stopped)
273548
+ return;
273549
+ if (!TravelRuleDepositReconciler.hasEnabledExchange(this.params.policy)) {
273550
+ return;
273551
+ }
273552
+ if (Object.keys(this.params.config.rpcUrlsByNetwork).length === 0) {
273553
+ log.warn("\u26A0\uFE0F 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).");
273554
+ }
273555
+ log.info("\uD83D\uDEC2 Travel-rule deposit auto-clear reconciler started");
273556
+ this.#scheduleImmediate();
273557
+ }
273558
+ stop() {
273559
+ this.#stopped = true;
273560
+ if (this.#timer) {
273561
+ clearTimeout(this.#timer);
273562
+ this.#timer = null;
273563
+ }
273564
+ }
273565
+ #scheduleImmediate() {
273566
+ this.#timer = setTimeout(() => void this.#tick(), 0);
273567
+ }
273568
+ #stateFor(key) {
273569
+ let state = this.#states.get(key);
273570
+ if (!state) {
273571
+ state = createAccountState();
273572
+ this.#states.set(key, state);
273573
+ }
273574
+ return state;
273575
+ }
273576
+ #targets() {
273577
+ const targets = [];
273578
+ for (const rule of this.params.policy.travelRule?.rule ?? []) {
273579
+ const depositConfig = getEnabledTravelRuleDepositConfig(this.params.policy, rule.exchange);
273580
+ if (!depositConfig)
273581
+ continue;
273582
+ const exchangeId = rule.exchange.trim().toLowerCase();
273583
+ const pool = this.params.brokers[exchangeId];
273584
+ if (!pool)
273585
+ continue;
273586
+ for (const account of [pool.primary, ...pool.secondaryBrokers]) {
273587
+ targets.push({ exchangeId, account, depositConfig });
273588
+ }
273589
+ }
273590
+ return targets;
273591
+ }
273592
+ async#tick() {
273593
+ if (this.#stopped || this.#running)
273594
+ return;
273595
+ this.#running = true;
273596
+ let anyActionable = false;
273597
+ try {
273598
+ for (const target of this.#targets()) {
273599
+ if (this.#stopped)
273600
+ break;
273601
+ const report = await this.#reconcileTarget(target);
273602
+ this.#emit(target, report);
273603
+ if (report.hadActionableWork)
273604
+ anyActionable = true;
273605
+ }
273606
+ } catch (error) {
273607
+ log.error("Travel-rule deposit reconciler tick failed", error);
273608
+ } finally {
273609
+ this.#running = false;
273610
+ if (!this.#stopped) {
273611
+ const delay = anyActionable ? this.params.config.pollIntervalActiveMs : this.params.config.pollIntervalIdleMs;
273612
+ this.#timer = setTimeout(() => void this.#tick(), delay);
273613
+ }
273614
+ }
273615
+ }
273616
+ #reconcileTarget(target) {
273617
+ const exchange = target.account.exchange;
273618
+ const accountLabel = `${target.exchangeId}:${target.account.label}`;
273619
+ const { config } = this.params;
273620
+ return reconcileAccountOnce({
273621
+ accountLabel,
273622
+ depositConfig: target.depositConfig,
273623
+ expectedCountry: config.expectedQuestionnaireCountry,
273624
+ failureBackoffMs: config.failureBackoffMs,
273625
+ rateLimitCooldownMs: config.rateLimitCooldownMs,
273626
+ now: Date.now(),
273627
+ state: this.#stateFor(accountLabel),
273628
+ fetchDepositHistory: async () => {
273629
+ if (typeof exchange.sapiGetLocalentityDepositHistory !== "function") {
273630
+ throw new Error("binance_localentity_deposit_history_unavailable: endpoint not registered");
273631
+ }
273632
+ const result = await exchange.sapiGetLocalentityDepositHistory({});
273633
+ return Array.isArray(result) ? result : [];
273634
+ },
273635
+ fetchQuestionnaireCountry: async () => {
273636
+ if (typeof exchange.sapiGetLocalentityQuestionnaireRequirements !== "function") {
273637
+ return null;
273638
+ }
273639
+ const result = await exchange.sapiGetLocalentityQuestionnaireRequirements({});
273640
+ return parseQuestionnaireCountry(result);
273641
+ },
273642
+ resolveSender: (network, txId) => {
273643
+ const url2 = config.rpcUrlsByNetwork[network.trim().toUpperCase()];
273644
+ if (!url2)
273645
+ return Promise.resolve(null);
273646
+ return resolveOnChainSender(url2, txId);
273647
+ },
273648
+ submitProvideInfo: (tranId, questionnaire) => {
273649
+ if (typeof exchange.sapiPutLocalentityDepositProvideInfo !== "function") {
273650
+ throw new Error("binance_localentity_provide_info_unavailable: endpoint not registered");
273651
+ }
273652
+ return exchange.sapiPutLocalentityDepositProvideInfo({
273653
+ tranId,
273654
+ questionnaire: JSON.stringify(questionnaire)
273655
+ });
273656
+ },
273657
+ resolveQuestionnaire: resolveDepositOriginatorQuestionnaire
273658
+ });
273659
+ }
273660
+ #emit(target, report) {
273661
+ const { metrics } = this.params;
273662
+ const base2 = { exchange: target.exchangeId, account: target.account.label };
273663
+ metrics?.recordGauge("travel_rule_frozen_deposits", report.frozenDeposits.length, base2);
273664
+ for (const outcome of report.outcomes) {
273665
+ switch (outcome.kind) {
273666
+ case "submitted":
273667
+ case "already-provided": {
273668
+ log.info("\uD83D\uDEC2 Travel-rule deposit auto-declared", {
273669
+ result: outcome.kind,
273670
+ account: report.accountLabel,
273671
+ tranId: outcome.deposit.tranId,
273672
+ coin: outcome.deposit.coin,
273673
+ amount: outcome.deposit.amount,
273674
+ network: outcome.deposit.network,
273675
+ txId: outcome.deposit.txId,
273676
+ originator: outcome.sender
273677
+ });
273678
+ metrics?.recordCounter("travel_rule_deposit_submissions_total", 1, {
273679
+ ...base2,
273680
+ coin: outcome.deposit.coin,
273681
+ result: outcome.kind
273682
+ });
273683
+ break;
273684
+ }
273685
+ case "undeclared-origin":
273686
+ log.warn("\uD83D\uDEC2 Travel-rule deposit from UNDECLARED originator \u2014 left frozen", {
273687
+ account: report.accountLabel,
273688
+ tranId: outcome.deposit.tranId,
273689
+ coin: outcome.deposit.coin,
273690
+ amount: outcome.deposit.amount,
273691
+ sender: outcome.sender
273692
+ });
273693
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
273694
+ ...base2,
273695
+ reason: "undeclared_origin"
273696
+ });
273697
+ break;
273698
+ case "unproven-origin":
273699
+ log.warn("\uD83D\uDEC2 Travel-rule deposit origin UNPROVEN (tx sender unresolved) \u2014 left frozen", {
273700
+ account: report.accountLabel,
273701
+ tranId: outcome.deposit.tranId,
273702
+ coin: outcome.deposit.coin,
273703
+ network: outcome.deposit.network,
273704
+ txId: outcome.deposit.txId
273705
+ });
273706
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
273707
+ ...base2,
273708
+ reason: "unproven_origin"
273709
+ });
273710
+ break;
273711
+ case "entity-drift":
273712
+ log.warn("\uD83D\uDEC2 Travel-rule deposit skipped \u2014 questionnaire entity is not the expected country", {
273713
+ account: report.accountLabel,
273714
+ tranId: outcome.deposit.tranId,
273715
+ observedCountry: outcome.country,
273716
+ expectedCountry: this.params.config.expectedQuestionnaireCountry
273717
+ });
273718
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
273719
+ ...base2,
273720
+ reason: "entity_drift"
273721
+ });
273722
+ break;
273723
+ case "failed-terminal":
273724
+ log.warn("\uD83D\uDEC2 Travel-rule deposit is FAILED (terminal) \u2014 needs manual handling", {
273725
+ account: report.accountLabel,
273726
+ tranId: outcome.deposit.tranId,
273727
+ coin: outcome.deposit.coin
273728
+ });
273729
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
273730
+ ...base2,
273731
+ reason: "failed_status"
273732
+ });
273733
+ break;
273734
+ case "submit-error":
273735
+ log.error("\uD83D\uDEC2 Travel-rule deposit provide-info failed", {
273736
+ account: report.accountLabel,
273737
+ tranId: outcome.deposit.tranId,
273738
+ error: outcome.error
273739
+ });
273740
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
273741
+ ...base2,
273742
+ reason: "submit_error"
273743
+ });
273744
+ break;
273745
+ case "poll-error":
273746
+ log.error("\uD83D\uDEC2 Travel-rule deposit history poll failed", {
273747
+ account: report.accountLabel,
273748
+ error: outcome.error
273749
+ });
273750
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
273751
+ ...base2,
273752
+ reason: "poll_error"
273753
+ });
273754
+ break;
273755
+ }
273756
+ }
273757
+ }
273758
+ }
273192
273759
  // src/helpers/verity.ts
273193
273760
  var import_verity_client = __toESM(require_dist(), 1);
273194
273761
 
@@ -273326,7 +273893,14 @@ function loadPolicy(policyPath) {
273326
273893
  description: import_joi2.default.string().optional(),
273327
273894
  addresses: import_joi2.default.object().pattern(import_joi2.default.string(), import_joi2.default.object({
273328
273895
  questionnaire: australiaQuestionnaireSchema.required()
273329
- })).required()
273896
+ })).required(),
273897
+ deposits: import_joi2.default.object({
273898
+ enabled: import_joi2.default.boolean().required(),
273899
+ description: import_joi2.default.string().optional(),
273900
+ originators: import_joi2.default.object().pattern(import_joi2.default.string(), import_joi2.default.object({
273901
+ questionnaire: australiaDepositQuestionnaireSchema.required()
273902
+ })).required()
273903
+ }).optional()
273330
273904
  });
273331
273905
  const policyConfigSchema = import_joi2.default.object({
273332
273906
  withdraw: import_joi2.default.object({
@@ -277670,60 +278244,6 @@ import * as grpc14 from "@grpc/grpc-js";
277670
278244
  // src/handlers/execute-action/deposit.ts
277671
278245
  import * as grpc3 from "@grpc/grpc-js";
277672
278246
 
277673
- // src/helpers/deposit.ts
277674
- function depositField(deposit, fields) {
277675
- for (const field of fields) {
277676
- const value = deposit[field];
277677
- if (value !== undefined && value !== null && String(value).length > 0) {
277678
- return value;
277679
- }
277680
- }
277681
- return;
277682
- }
277683
- function normalizeDepositStatus(status) {
277684
- const normalized = String(status ?? "").trim().toLowerCase();
277685
- if (["ok", "credited", "complete", "completed", "success"].includes(normalized)) {
277686
- return "credited";
277687
- }
277688
- if (["failed", "failure", "canceled", "cancelled", "rejected"].includes(normalized)) {
277689
- return "failed";
277690
- }
277691
- if (["timeout", "timed_out", "timedout", "expired"].includes(normalized)) {
277692
- return "timed_out";
277693
- }
277694
- if (["pending", "processing", "confirming", "waiting"].includes(normalized)) {
277695
- return "pending";
277696
- }
277697
- return "pending";
277698
- }
277699
- function depositMatchesTransaction(deposit, transactionHash) {
277700
- const candidates = [
277701
- depositField(deposit, [
277702
- "txid",
277703
- "txId",
277704
- "tx_hash",
277705
- "txHash",
277706
- "transactionHash"
277707
- ]),
277708
- depositField(deposit, ["id"])
277709
- ];
277710
- return candidates.some((candidate) => String(candidate) === transactionHash);
277711
- }
277712
- function stringAmountEquals(left, right) {
277713
- const leftNum = Number(left);
277714
- const rightNum = Number(right);
277715
- if (Number.isFinite(leftNum) && Number.isFinite(rightNum)) {
277716
- return Math.abs(leftNum - rightNum) < 0.000000000001;
277717
- }
277718
- return String(left) === String(right);
277719
- }
277720
- function normalizeAddress(value) {
277721
- if (value === undefined || value === null) {
277722
- return;
277723
- }
277724
- return String(value).trim().toLowerCase();
277725
- }
277726
-
277727
278247
  // src/helpers/shared/errors.ts
277728
278248
  function getErrorMessage(error) {
277729
278249
  return error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
@@ -294249,6 +294769,7 @@ class CEXBroker {
294249
294769
  useVerity = false;
294250
294770
  otelMetrics;
294251
294771
  otelLogs;
294772
+ depositReconciler;
294252
294773
  loadEnvConfig() {
294253
294774
  log.info("\uD83D\uDD27 Loading CEX_BROKER_ environment variables:");
294254
294775
  const configMap = {};
@@ -294384,6 +294905,10 @@ class CEXBroker {
294384
294905
  unwatchFile(this.#policyFilePath);
294385
294906
  log.info(`Stopped watching policy file: ${this.#policyFilePath}`);
294386
294907
  }
294908
+ if (this.depositReconciler) {
294909
+ this.depositReconciler.stop();
294910
+ this.depositReconciler = undefined;
294911
+ }
294387
294912
  if (this.server) {
294388
294913
  await this.server.forceShutdown();
294389
294914
  }
@@ -294398,6 +294923,10 @@ class CEXBroker {
294398
294923
  if (this.server) {
294399
294924
  await this.server.forceShutdown();
294400
294925
  }
294926
+ if (this.depositReconciler) {
294927
+ this.depositReconciler.stop();
294928
+ this.depositReconciler = undefined;
294929
+ }
294401
294930
  log.info(`Running CEXBroker at ${new Date().toISOString()}`);
294402
294931
  if (this.otelMetrics?.isOtelEnabled()) {
294403
294932
  await this.otelMetrics.initialize();
@@ -294410,6 +294939,13 @@ class CEXBroker {
294410
294939
  }
294411
294940
  log.info(`Your server as started on port ${port}`);
294412
294941
  });
294942
+ this.depositReconciler = new TravelRuleDepositReconciler({
294943
+ policy: this.policy,
294944
+ brokers: this.brokers,
294945
+ config: loadTravelRuleDepositReconcilerConfigFromEnv(process.env),
294946
+ metrics: this.otelMetrics
294947
+ });
294948
+ this.depositReconciler.start();
294413
294949
  return this;
294414
294950
  }
294415
294951
  }
@@ -294417,4 +294953,4 @@ export {
294417
294953
  CEXBroker as default
294418
294954
  };
294419
294955
 
294420
- //# debugId=8F76D79C41F9E70764756E2164756E21
294956
+ //# debugId=FEB8CDC1C50AA70B64756E2164756E21