@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/commands/cli.js +617 -56
- package/dist/helpers/index.d.ts +2 -1
- package/dist/helpers/travel-rule-deposit-reconciler.d.ts +159 -0
- package/dist/helpers/travel-rule.d.ts +32 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +622 -61
- package/dist/index.js.map +11 -10
- package/dist/types.d.ts +14 -0
- package/package.json +1 -1
package/dist/commands/cli.js
CHANGED
|
@@ -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
|
}
|