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