@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.
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 === "localentity/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,563 @@ function authenticateRequest(call, whitelistIps) {
273189
273224
  }
273190
273225
  return true;
273191
273226
  }
273227
+ // src/helpers/travel-rule-deposit-reconciler.ts
273228
+ import { request as httpRequest } from "http";
273229
+ import { request as httpsRequest } from "https";
273230
+
273231
+ // src/helpers/deposit.ts
273232
+ function depositField(deposit, fields) {
273233
+ for (const field of fields) {
273234
+ const value = deposit[field];
273235
+ if (value !== undefined && value !== null && String(value).length > 0) {
273236
+ return value;
273237
+ }
273238
+ }
273239
+ return;
273240
+ }
273241
+ function normalizeDepositStatus(status) {
273242
+ const normalized = String(status ?? "").trim().toLowerCase();
273243
+ if (["ok", "credited", "complete", "completed", "success"].includes(normalized)) {
273244
+ return "credited";
273245
+ }
273246
+ if (["failed", "failure", "canceled", "cancelled", "rejected"].includes(normalized)) {
273247
+ return "failed";
273248
+ }
273249
+ if (["timeout", "timed_out", "timedout", "expired"].includes(normalized)) {
273250
+ return "timed_out";
273251
+ }
273252
+ if (["pending", "processing", "confirming", "waiting"].includes(normalized)) {
273253
+ return "pending";
273254
+ }
273255
+ return "pending";
273256
+ }
273257
+ function depositMatchesTransaction(deposit, transactionHash) {
273258
+ const candidates = [
273259
+ depositField(deposit, [
273260
+ "txid",
273261
+ "txId",
273262
+ "tx_hash",
273263
+ "txHash",
273264
+ "transactionHash"
273265
+ ]),
273266
+ depositField(deposit, ["id"])
273267
+ ];
273268
+ return candidates.some((candidate) => String(candidate) === transactionHash);
273269
+ }
273270
+ function stringAmountEquals(left, right) {
273271
+ const leftNum = Number(left);
273272
+ const rightNum = Number(right);
273273
+ if (Number.isFinite(leftNum) && Number.isFinite(rightNum)) {
273274
+ return Math.abs(leftNum - rightNum) < 0.000000000001;
273275
+ }
273276
+ return String(left) === String(right);
273277
+ }
273278
+ function normalizeAddress(value) {
273279
+ if (value === undefined || value === null) {
273280
+ return;
273281
+ }
273282
+ return String(value).trim().toLowerCase();
273283
+ }
273284
+
273285
+ // src/helpers/travel-rule-deposit-reconciler.ts
273286
+ function toBool(value) {
273287
+ if (typeof value === "boolean")
273288
+ return value;
273289
+ if (typeof value === "number")
273290
+ return value !== 0;
273291
+ if (typeof value === "string")
273292
+ return value.trim().toLowerCase() === "true";
273293
+ return false;
273294
+ }
273295
+ function parseLocalEntityDeposit(raw) {
273296
+ const tranId = depositField(raw, ["tranId"]);
273297
+ if (tranId === undefined)
273298
+ return null;
273299
+ return {
273300
+ tranId: String(tranId),
273301
+ coin: String(depositField(raw, ["coin", "asset"]) ?? ""),
273302
+ amount: String(depositField(raw, ["amount"]) ?? ""),
273303
+ network: String(depositField(raw, ["network"]) ?? ""),
273304
+ txId: String(depositField(raw, ["txId", "txid", "tx_hash", "txHash"]) ?? ""),
273305
+ travelRuleStatus: String(depositField(raw, ["travelRuleStatusV2", "travelRuleStatus"]) ?? "").trim().toUpperCase(),
273306
+ requireQuestionnaire: toBool(raw.requireQuestionnaire ?? raw.requireQuestionnaireV2),
273307
+ raw
273308
+ };
273309
+ }
273310
+ var EVM_TX_HASH = /^0x[0-9a-fA-F]{64}$/;
273311
+ function resolveOnChainSender(rpcUrl, txHash, timeoutMs = 1e4) {
273312
+ if (!EVM_TX_HASH.test(txHash))
273313
+ return Promise.resolve(null);
273314
+ const body = JSON.stringify({
273315
+ jsonrpc: "2.0",
273316
+ id: 1,
273317
+ method: "eth_getTransactionByHash",
273318
+ params: [txHash]
273319
+ });
273320
+ const url2 = new URL(rpcUrl);
273321
+ const doRequest = url2.protocol === "http:" ? httpRequest : httpsRequest;
273322
+ return new Promise((resolve, reject) => {
273323
+ const req = doRequest(url2, {
273324
+ method: "POST",
273325
+ headers: {
273326
+ "content-type": "application/json",
273327
+ "content-length": Buffer.byteLength(body)
273328
+ },
273329
+ timeout: timeoutMs
273330
+ }, (res) => {
273331
+ const chunks = [];
273332
+ res.on("data", (chunk) => chunks.push(chunk));
273333
+ res.on("end", () => {
273334
+ const status = res.statusCode ?? 0;
273335
+ if (status < 200 || status >= 300) {
273336
+ reject(new Error(`travel_rule_rpc_http_${status}`));
273337
+ return;
273338
+ }
273339
+ try {
273340
+ const json3 = JSON.parse(Buffer.concat(chunks).toString("utf8"));
273341
+ if (json3.error) {
273342
+ reject(new Error(`travel_rule_rpc_error: ${JSON.stringify(json3.error)}`));
273343
+ return;
273344
+ }
273345
+ const from = json3.result?.from;
273346
+ resolve(typeof from === "string" && from.length > 0 ? from.toLowerCase() : null);
273347
+ } catch (parseError) {
273348
+ reject(new Error(`travel_rule_rpc_parse_error: ${String(parseError)}`));
273349
+ }
273350
+ });
273351
+ });
273352
+ req.on("error", reject);
273353
+ req.on("timeout", () => {
273354
+ req.destroy(new Error("travel_rule_rpc_timeout"));
273355
+ });
273356
+ req.write(body);
273357
+ req.end();
273358
+ });
273359
+ }
273360
+ function errorText(error) {
273361
+ if (error instanceof Error)
273362
+ return error.message;
273363
+ if (typeof error === "string")
273364
+ return error;
273365
+ try {
273366
+ return JSON.stringify(error);
273367
+ } catch {
273368
+ return String(error);
273369
+ }
273370
+ }
273371
+ function isRateLimitError(error) {
273372
+ const text = errorText(error).toLowerCase();
273373
+ return text.includes("-1003") || text.includes("too many requests") || text.includes("429") || text.includes("ddosprotection") || text.includes("ratelimitexceeded");
273374
+ }
273375
+ function isAlreadyProvidedError(error) {
273376
+ const text = errorText(error).toLowerCase();
273377
+ return text.includes("already") && (text.includes("provided") || text.includes("processed") || text.includes("passed") || text.includes("submit"));
273378
+ }
273379
+ function isAcceptedResponse(response) {
273380
+ return response.accepted !== false;
273381
+ }
273382
+ function createAccountState() {
273383
+ return {
273384
+ submittedTranIds: new Set,
273385
+ surfacedFailed: new Set,
273386
+ backoffUntil: new Map,
273387
+ rateLimitedUntil: 0
273388
+ };
273389
+ }
273390
+ function isInBackoff(state, tranId, now3) {
273391
+ const until = state.backoffUntil.get(tranId);
273392
+ return until !== undefined && now3 < until;
273393
+ }
273394
+ async function reconcileAccountOnce(deps) {
273395
+ const { state, now: now3, accountLabel } = deps;
273396
+ const outcomes = [];
273397
+ if (now3 < state.rateLimitedUntil) {
273398
+ return {
273399
+ accountLabel,
273400
+ frozenDeposits: [],
273401
+ outcomes,
273402
+ hadActionableWork: false
273403
+ };
273404
+ }
273405
+ let rawDeposits;
273406
+ try {
273407
+ rawDeposits = await deps.fetchDepositHistory();
273408
+ } catch (error) {
273409
+ if (isRateLimitError(error)) {
273410
+ state.rateLimitedUntil = now3 + deps.rateLimitCooldownMs;
273411
+ }
273412
+ return {
273413
+ accountLabel,
273414
+ frozenDeposits: [],
273415
+ outcomes: [{ kind: "poll-error", error: errorText(error) }],
273416
+ hadActionableWork: false
273417
+ };
273418
+ }
273419
+ const deposits = rawDeposits.map(parseLocalEntityDeposit).filter((d) => d !== null);
273420
+ const frozen = deposits.filter((d) => d.requireQuestionnaire && d.travelRuleStatus === "PENDING");
273421
+ for (const deposit of deposits) {
273422
+ if (deposit.travelRuleStatus === "FAILED" && !state.surfacedFailed.has(deposit.tranId)) {
273423
+ state.surfacedFailed.add(deposit.tranId);
273424
+ outcomes.push({ kind: "failed-terminal", deposit });
273425
+ }
273426
+ }
273427
+ const candidates = frozen.filter((d) => !state.submittedTranIds.has(d.tranId) && !isInBackoff(state, d.tranId, now3));
273428
+ if (candidates.length === 0) {
273429
+ return {
273430
+ accountLabel,
273431
+ frozenDeposits: frozen,
273432
+ outcomes,
273433
+ hadActionableWork: false
273434
+ };
273435
+ }
273436
+ let country = null;
273437
+ try {
273438
+ country = await deps.fetchQuestionnaireCountry();
273439
+ } catch (error) {
273440
+ if (isRateLimitError(error)) {
273441
+ state.rateLimitedUntil = now3 + deps.rateLimitCooldownMs;
273442
+ }
273443
+ country = null;
273444
+ }
273445
+ if (country !== deps.expectedCountry) {
273446
+ for (const deposit of candidates) {
273447
+ outcomes.push({ kind: "entity-drift", deposit, country });
273448
+ }
273449
+ return {
273450
+ accountLabel,
273451
+ frozenDeposits: frozen,
273452
+ outcomes,
273453
+ hadActionableWork: true
273454
+ };
273455
+ }
273456
+ for (const deposit of candidates) {
273457
+ if (now3 < state.rateLimitedUntil)
273458
+ break;
273459
+ let sender = null;
273460
+ let resolveError;
273461
+ try {
273462
+ sender = await deps.resolveSender(deposit.network, deposit.txId);
273463
+ } catch (error) {
273464
+ sender = null;
273465
+ resolveError = errorText(error);
273466
+ if (isRateLimitError(error)) {
273467
+ state.rateLimitedUntil = now3 + deps.rateLimitCooldownMs;
273468
+ }
273469
+ }
273470
+ if (!sender) {
273471
+ state.backoffUntil.set(deposit.tranId, now3 + deps.failureBackoffMs);
273472
+ outcomes.push({ kind: "unproven-origin", deposit, error: resolveError });
273473
+ continue;
273474
+ }
273475
+ const questionnaire = deps.resolveQuestionnaire(deps.depositConfig, sender);
273476
+ if (!questionnaire) {
273477
+ state.backoffUntil.set(deposit.tranId, now3 + deps.failureBackoffMs);
273478
+ outcomes.push({ kind: "undeclared-origin", deposit, sender });
273479
+ continue;
273480
+ }
273481
+ try {
273482
+ const response = await deps.submitProvideInfo(deposit.tranId, questionnaire);
273483
+ if (isAcceptedResponse(response)) {
273484
+ state.submittedTranIds.add(deposit.tranId);
273485
+ state.backoffUntil.delete(deposit.tranId);
273486
+ outcomes.push({ kind: "submitted", deposit, sender });
273487
+ } else {
273488
+ state.backoffUntil.set(deposit.tranId, now3 + deps.failureBackoffMs);
273489
+ outcomes.push({
273490
+ kind: "submit-error",
273491
+ deposit,
273492
+ error: JSON.stringify(response)
273493
+ });
273494
+ }
273495
+ } catch (error) {
273496
+ if (isAlreadyProvidedError(error)) {
273497
+ state.submittedTranIds.add(deposit.tranId);
273498
+ state.backoffUntil.delete(deposit.tranId);
273499
+ outcomes.push({ kind: "already-provided", deposit, sender });
273500
+ } else {
273501
+ if (isRateLimitError(error)) {
273502
+ state.rateLimitedUntil = now3 + deps.rateLimitCooldownMs;
273503
+ }
273504
+ state.backoffUntil.set(deposit.tranId, now3 + deps.failureBackoffMs);
273505
+ outcomes.push({
273506
+ kind: "submit-error",
273507
+ deposit,
273508
+ error: errorText(error)
273509
+ });
273510
+ }
273511
+ }
273512
+ }
273513
+ return {
273514
+ accountLabel,
273515
+ frozenDeposits: frozen,
273516
+ outcomes,
273517
+ hadActionableWork: true
273518
+ };
273519
+ }
273520
+ var DEFAULTS = {
273521
+ pollIntervalActiveMs: 60000,
273522
+ pollIntervalIdleMs: 600000,
273523
+ expectedQuestionnaireCountry: "AU",
273524
+ failureBackoffMs: 15 * 60000,
273525
+ rateLimitCooldownMs: 5 * 60000
273526
+ };
273527
+ function envSecondsToMs(env, key, fallbackMs) {
273528
+ const raw = env[key];
273529
+ if (raw === undefined)
273530
+ return fallbackMs;
273531
+ const secs = Number(raw);
273532
+ return Number.isFinite(secs) && secs > 0 ? secs * 1000 : fallbackMs;
273533
+ }
273534
+ function loadTravelRuleDepositReconcilerConfigFromEnv(env) {
273535
+ const rpcUrlsByNetwork = {};
273536
+ const prefix = "TRAVEL_RULE_RPC_URL_";
273537
+ for (const [key, value] of Object.entries(env)) {
273538
+ if (key.startsWith(prefix) && value) {
273539
+ rpcUrlsByNetwork[key.slice(prefix.length).toUpperCase()] = value;
273540
+ }
273541
+ }
273542
+ return {
273543
+ rpcUrlsByNetwork,
273544
+ pollIntervalActiveMs: envSecondsToMs(env, "TRAVEL_RULE_DEPOSIT_POLL_ACTIVE_SECS", DEFAULTS.pollIntervalActiveMs),
273545
+ pollIntervalIdleMs: envSecondsToMs(env, "TRAVEL_RULE_DEPOSIT_POLL_IDLE_SECS", DEFAULTS.pollIntervalIdleMs),
273546
+ expectedQuestionnaireCountry: env.TRAVEL_RULE_QUESTIONNAIRE_COUNTRY?.trim().toUpperCase() || DEFAULTS.expectedQuestionnaireCountry,
273547
+ failureBackoffMs: envSecondsToMs(env, "TRAVEL_RULE_DEPOSIT_FAILURE_BACKOFF_SECS", DEFAULTS.failureBackoffMs),
273548
+ rateLimitCooldownMs: envSecondsToMs(env, "TRAVEL_RULE_DEPOSIT_RATE_LIMIT_COOLDOWN_SECS", DEFAULTS.rateLimitCooldownMs)
273549
+ };
273550
+ }
273551
+ function parseQuestionnaireCountry(response) {
273552
+ if (!response || typeof response !== "object")
273553
+ return null;
273554
+ const code = response.questionnaireCountryCode;
273555
+ return typeof code === "string" ? code.trim().toUpperCase() : null;
273556
+ }
273557
+
273558
+ class TravelRuleDepositReconciler {
273559
+ params;
273560
+ #timer = null;
273561
+ #stopped = false;
273562
+ #running = false;
273563
+ #states = new Map;
273564
+ constructor(params) {
273565
+ this.params = params;
273566
+ }
273567
+ static hasEnabledExchange(policy) {
273568
+ return (policy.travelRule?.rule ?? []).some((rule) => getEnabledTravelRuleDepositConfig(policy, rule.exchange));
273569
+ }
273570
+ start() {
273571
+ if (this.#timer || this.#stopped)
273572
+ return;
273573
+ if (!TravelRuleDepositReconciler.hasEnabledExchange(this.params.policy)) {
273574
+ return;
273575
+ }
273576
+ if (Object.keys(this.params.config.rpcUrlsByNetwork).length === 0) {
273577
+ 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).");
273578
+ }
273579
+ log.info("\uD83D\uDEC2 Travel-rule deposit auto-clear reconciler started");
273580
+ this.#scheduleImmediate();
273581
+ }
273582
+ stop() {
273583
+ this.#stopped = true;
273584
+ if (this.#timer) {
273585
+ clearTimeout(this.#timer);
273586
+ this.#timer = null;
273587
+ }
273588
+ }
273589
+ #scheduleImmediate() {
273590
+ this.#timer = setTimeout(() => void this.#tick(), 0);
273591
+ }
273592
+ #stateFor(key) {
273593
+ let state = this.#states.get(key);
273594
+ if (!state) {
273595
+ state = createAccountState();
273596
+ this.#states.set(key, state);
273597
+ }
273598
+ return state;
273599
+ }
273600
+ #targets() {
273601
+ const targets = [];
273602
+ for (const rule of this.params.policy.travelRule?.rule ?? []) {
273603
+ const depositConfig = getEnabledTravelRuleDepositConfig(this.params.policy, rule.exchange);
273604
+ if (!depositConfig)
273605
+ continue;
273606
+ const exchangeId = rule.exchange.trim().toLowerCase();
273607
+ const pool = this.params.brokers[exchangeId];
273608
+ if (!pool)
273609
+ continue;
273610
+ for (const account of [pool.primary, ...pool.secondaryBrokers]) {
273611
+ targets.push({ exchangeId, account, depositConfig });
273612
+ }
273613
+ }
273614
+ return targets;
273615
+ }
273616
+ async#tick() {
273617
+ if (this.#stopped || this.#running)
273618
+ return;
273619
+ this.#running = true;
273620
+ let anyActionable = false;
273621
+ try {
273622
+ for (const target of this.#targets()) {
273623
+ if (this.#stopped)
273624
+ break;
273625
+ const report = await this.#reconcileTarget(target);
273626
+ this.#emit(target, report);
273627
+ if (report.hadActionableWork)
273628
+ anyActionable = true;
273629
+ }
273630
+ } catch (error) {
273631
+ log.error("Travel-rule deposit reconciler tick failed", error);
273632
+ } finally {
273633
+ this.#running = false;
273634
+ if (!this.#stopped) {
273635
+ const delay = anyActionable ? this.params.config.pollIntervalActiveMs : this.params.config.pollIntervalIdleMs;
273636
+ this.#timer = setTimeout(() => void this.#tick(), delay);
273637
+ }
273638
+ }
273639
+ }
273640
+ #reconcileTarget(target) {
273641
+ const exchange = target.account.exchange;
273642
+ const accountLabel = `${target.exchangeId}:${target.account.label}`;
273643
+ const { config } = this.params;
273644
+ return reconcileAccountOnce({
273645
+ accountLabel,
273646
+ depositConfig: target.depositConfig,
273647
+ expectedCountry: config.expectedQuestionnaireCountry,
273648
+ failureBackoffMs: config.failureBackoffMs,
273649
+ rateLimitCooldownMs: config.rateLimitCooldownMs,
273650
+ now: Date.now(),
273651
+ state: this.#stateFor(accountLabel),
273652
+ fetchDepositHistory: async () => {
273653
+ if (typeof exchange.sapiGetLocalentityDepositHistory !== "function") {
273654
+ throw new Error("binance_localentity_deposit_history_unavailable: endpoint not registered");
273655
+ }
273656
+ const result = await exchange.sapiGetLocalentityDepositHistory({});
273657
+ return Array.isArray(result) ? result : [];
273658
+ },
273659
+ fetchQuestionnaireCountry: async () => {
273660
+ if (typeof exchange.sapiGetLocalentityQuestionnaireRequirements !== "function") {
273661
+ return null;
273662
+ }
273663
+ const result = await exchange.sapiGetLocalentityQuestionnaireRequirements({});
273664
+ return parseQuestionnaireCountry(result);
273665
+ },
273666
+ resolveSender: (network, txId) => {
273667
+ const url2 = config.rpcUrlsByNetwork[network.trim().toUpperCase()];
273668
+ if (!url2)
273669
+ return Promise.resolve(null);
273670
+ return resolveOnChainSender(url2, txId);
273671
+ },
273672
+ submitProvideInfo: (tranId, questionnaire) => {
273673
+ if (typeof exchange.sapiPutLocalentityDepositProvideInfo !== "function") {
273674
+ throw new Error("binance_localentity_provide_info_unavailable: endpoint not registered");
273675
+ }
273676
+ return exchange.sapiPutLocalentityDepositProvideInfo({
273677
+ tranId,
273678
+ questionnaire: JSON.stringify(questionnaire)
273679
+ });
273680
+ },
273681
+ resolveQuestionnaire: resolveDepositOriginatorQuestionnaire
273682
+ });
273683
+ }
273684
+ #emit(target, report) {
273685
+ const { metrics } = this.params;
273686
+ const base2 = { exchange: target.exchangeId, account: target.account.label };
273687
+ metrics?.recordGauge("travel_rule_frozen_deposits", report.frozenDeposits.length, base2);
273688
+ for (const outcome of report.outcomes) {
273689
+ switch (outcome.kind) {
273690
+ case "submitted":
273691
+ case "already-provided": {
273692
+ log.info("\uD83D\uDEC2 Travel-rule deposit auto-declared", {
273693
+ result: outcome.kind,
273694
+ account: report.accountLabel,
273695
+ tranId: outcome.deposit.tranId,
273696
+ coin: outcome.deposit.coin,
273697
+ amount: outcome.deposit.amount,
273698
+ network: outcome.deposit.network,
273699
+ txId: outcome.deposit.txId,
273700
+ originator: outcome.sender
273701
+ });
273702
+ metrics?.recordCounter("travel_rule_deposit_submissions_total", 1, {
273703
+ ...base2,
273704
+ coin: outcome.deposit.coin,
273705
+ result: outcome.kind
273706
+ });
273707
+ break;
273708
+ }
273709
+ case "undeclared-origin":
273710
+ log.warn("\uD83D\uDEC2 Travel-rule deposit from UNDECLARED originator \u2014 left frozen", {
273711
+ account: report.accountLabel,
273712
+ tranId: outcome.deposit.tranId,
273713
+ coin: outcome.deposit.coin,
273714
+ amount: outcome.deposit.amount,
273715
+ sender: outcome.sender
273716
+ });
273717
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
273718
+ ...base2,
273719
+ reason: "undeclared_origin"
273720
+ });
273721
+ break;
273722
+ case "unproven-origin":
273723
+ log.warn("\uD83D\uDEC2 Travel-rule deposit origin UNPROVEN (tx sender unresolved) \u2014 left frozen", {
273724
+ account: report.accountLabel,
273725
+ tranId: outcome.deposit.tranId,
273726
+ coin: outcome.deposit.coin,
273727
+ network: outcome.deposit.network,
273728
+ txId: outcome.deposit.txId,
273729
+ rpcError: outcome.error ?? null
273730
+ });
273731
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
273732
+ ...base2,
273733
+ reason: "unproven_origin"
273734
+ });
273735
+ break;
273736
+ case "entity-drift":
273737
+ log.warn("\uD83D\uDEC2 Travel-rule deposit skipped \u2014 questionnaire entity is not the expected country", {
273738
+ account: report.accountLabel,
273739
+ tranId: outcome.deposit.tranId,
273740
+ observedCountry: outcome.country,
273741
+ expectedCountry: this.params.config.expectedQuestionnaireCountry
273742
+ });
273743
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
273744
+ ...base2,
273745
+ reason: "entity_drift"
273746
+ });
273747
+ break;
273748
+ case "failed-terminal":
273749
+ log.warn("\uD83D\uDEC2 Travel-rule deposit is FAILED (terminal) \u2014 needs manual handling", {
273750
+ account: report.accountLabel,
273751
+ tranId: outcome.deposit.tranId,
273752
+ coin: outcome.deposit.coin
273753
+ });
273754
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
273755
+ ...base2,
273756
+ reason: "failed_status"
273757
+ });
273758
+ break;
273759
+ case "submit-error":
273760
+ log.error("\uD83D\uDEC2 Travel-rule deposit provide-info failed", {
273761
+ account: report.accountLabel,
273762
+ tranId: outcome.deposit.tranId,
273763
+ error: outcome.error
273764
+ });
273765
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
273766
+ ...base2,
273767
+ reason: "submit_error"
273768
+ });
273769
+ break;
273770
+ case "poll-error":
273771
+ log.error("\uD83D\uDEC2 Travel-rule deposit history poll failed", {
273772
+ account: report.accountLabel,
273773
+ error: outcome.error
273774
+ });
273775
+ metrics?.recordCounter("travel_rule_deposit_anomalies_total", 1, {
273776
+ ...base2,
273777
+ reason: "poll_error"
273778
+ });
273779
+ break;
273780
+ }
273781
+ }
273782
+ }
273783
+ }
273192
273784
  // src/helpers/verity.ts
273193
273785
  var import_verity_client = __toESM(require_dist(), 1);
273194
273786
 
@@ -273326,7 +273918,14 @@ function loadPolicy(policyPath) {
273326
273918
  description: import_joi2.default.string().optional(),
273327
273919
  addresses: import_joi2.default.object().pattern(import_joi2.default.string(), import_joi2.default.object({
273328
273920
  questionnaire: australiaQuestionnaireSchema.required()
273329
- })).required()
273921
+ })).required(),
273922
+ deposits: import_joi2.default.object({
273923
+ enabled: import_joi2.default.boolean().required(),
273924
+ description: import_joi2.default.string().optional(),
273925
+ originators: import_joi2.default.object().pattern(import_joi2.default.string(), import_joi2.default.object({
273926
+ questionnaire: australiaDepositQuestionnaireSchema.required()
273927
+ })).required()
273928
+ }).optional()
273330
273929
  });
273331
273930
  const policyConfigSchema = import_joi2.default.object({
273332
273931
  withdraw: import_joi2.default.object({
@@ -276745,11 +277344,11 @@ var JsonMetricsSerializer = {
276745
277344
  }
276746
277345
  };
276747
277346
  // node_modules/@opentelemetry/exporter-logs-otlp-http/build/esm/platform/node/OTLPLogExporter.js
276748
- var import_node_http = __toESM(require_index_node_http(), 1);
277347
+ var import_node_http2 = __toESM(require_index_node_http(), 1);
276749
277348
 
276750
277349
  class OTLPLogExporter extends import_otlp_exporter_base.OTLPExporterBase {
276751
277350
  constructor(config = {}) {
276752
- super(import_node_http.createOtlpHttpExportDelegate(import_node_http.convertLegacyHttpOptions(config, "LOGS", "v1/logs", {
277351
+ super(import_node_http2.createOtlpHttpExportDelegate(import_node_http2.convertLegacyHttpOptions(config, "LOGS", "v1/logs", {
276753
277352
  "Content-Type": "application/json"
276754
277353
  }), JsonLogsSerializer));
276755
277354
  }
@@ -276844,11 +277443,11 @@ class OTLPMetricExporterBase extends import_otlp_exporter_base2.OTLPExporterBase
276844
277443
  }
276845
277444
 
276846
277445
  // node_modules/@opentelemetry/exporter-metrics-otlp-http/build/esm/platform/node/OTLPMetricExporter.js
276847
- var import_node_http2 = __toESM(require_index_node_http(), 1);
277446
+ var import_node_http3 = __toESM(require_index_node_http(), 1);
276848
277447
 
276849
277448
  class OTLPMetricExporter extends OTLPMetricExporterBase {
276850
277449
  constructor(config) {
276851
- super(import_node_http2.createOtlpHttpExportDelegate(import_node_http2.convertLegacyHttpOptions(config ?? {}, "METRICS", "v1/metrics", {
277450
+ super(import_node_http3.createOtlpHttpExportDelegate(import_node_http3.convertLegacyHttpOptions(config ?? {}, "METRICS", "v1/metrics", {
276852
277451
  "Content-Type": "application/json"
276853
277452
  }), JsonMetricsSerializer), config);
276854
277453
  }
@@ -277670,60 +278269,6 @@ import * as grpc14 from "@grpc/grpc-js";
277670
278269
  // src/handlers/execute-action/deposit.ts
277671
278270
  import * as grpc3 from "@grpc/grpc-js";
277672
278271
 
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
278272
  // src/helpers/shared/errors.ts
277728
278273
  function getErrorMessage(error) {
277729
278274
  return error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
@@ -294249,6 +294794,7 @@ class CEXBroker {
294249
294794
  useVerity = false;
294250
294795
  otelMetrics;
294251
294796
  otelLogs;
294797
+ depositReconciler;
294252
294798
  loadEnvConfig() {
294253
294799
  log.info("\uD83D\uDD27 Loading CEX_BROKER_ environment variables:");
294254
294800
  const configMap = {};
@@ -294384,6 +294930,10 @@ class CEXBroker {
294384
294930
  unwatchFile(this.#policyFilePath);
294385
294931
  log.info(`Stopped watching policy file: ${this.#policyFilePath}`);
294386
294932
  }
294933
+ if (this.depositReconciler) {
294934
+ this.depositReconciler.stop();
294935
+ this.depositReconciler = undefined;
294936
+ }
294387
294937
  if (this.server) {
294388
294938
  await this.server.forceShutdown();
294389
294939
  }
@@ -294398,6 +294948,10 @@ class CEXBroker {
294398
294948
  if (this.server) {
294399
294949
  await this.server.forceShutdown();
294400
294950
  }
294951
+ if (this.depositReconciler) {
294952
+ this.depositReconciler.stop();
294953
+ this.depositReconciler = undefined;
294954
+ }
294401
294955
  log.info(`Running CEXBroker at ${new Date().toISOString()}`);
294402
294956
  if (this.otelMetrics?.isOtelEnabled()) {
294403
294957
  await this.otelMetrics.initialize();
@@ -294410,6 +294964,13 @@ class CEXBroker {
294410
294964
  }
294411
294965
  log.info(`Your server as started on port ${port}`);
294412
294966
  });
294967
+ this.depositReconciler = new TravelRuleDepositReconciler({
294968
+ policy: this.policy,
294969
+ brokers: this.brokers,
294970
+ config: loadTravelRuleDepositReconcilerConfigFromEnv(process.env),
294971
+ metrics: this.otelMetrics
294972
+ });
294973
+ this.depositReconciler.start();
294413
294974
  return this;
294414
294975
  }
294415
294976
  }
@@ -294417,4 +294978,4 @@ export {
294417
294978
  CEXBroker as default
294418
294979
  };
294419
294980
 
294420
- //# debugId=5C43863FE4632C1564756E2164756E21
294981
+ //# debugId=75EE3175FC68761464756E2164756E21