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