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