@anywalletkit/protocol-swap2-thorchain 0.4.0 → 0.4.2
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/{chunk-AYP7O2NT.js → chunk-EIAXGWNG.js} +158 -89
- package/dist/chunk-EIAXGWNG.js.map +1 -0
- package/dist/{chunk-LP5BU2JS.js → chunk-YKA6IYKX.js} +2 -2
- package/dist/index.cjs +157 -88
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/protocol.cjs +121 -64
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.js +2 -2
- package/dist/provider.cjs +157 -88
- package/dist/provider.cjs.map +1 -1
- package/dist/provider.d.cts +1 -0
- package/dist/provider.d.ts +1 -0
- package/dist/provider.js +1 -1
- package/package.json +3 -3
- package/dist/chunk-AYP7O2NT.js.map +0 -1
- /package/dist/{chunk-LP5BU2JS.js.map → chunk-YKA6IYKX.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -77,10 +77,12 @@ function describeThorchainActivity(tx, ctx) {
|
|
|
77
77
|
// src/provider.ts
|
|
78
78
|
var import_viem = require("viem");
|
|
79
79
|
var import_util2 = require("@anywalletkit/protocol-swap2-core/util");
|
|
80
|
+
var import_errors2 = require("@anywalletkit/protocol-swap2-core/errors");
|
|
80
81
|
var import_swap2_assets = require("@anywalletkit/swap2-assets");
|
|
81
82
|
var import_core = require("@anywalletkit/core");
|
|
82
83
|
|
|
83
84
|
// src/confirm.ts
|
|
85
|
+
var import_errors = require("@anywalletkit/protocol-swap2-core/errors");
|
|
84
86
|
var THORNODE_CONFIRM_PATHS = Object.freeze({
|
|
85
87
|
lastblock: "/thorchain/lastblock",
|
|
86
88
|
inboundAddresses: "/thorchain/inbound_addresses",
|
|
@@ -103,10 +105,18 @@ function confirmOptions(o) {
|
|
|
103
105
|
return { maxLagBlocks, timeoutMs };
|
|
104
106
|
}
|
|
105
107
|
var HostUnavailable = class extends Error {
|
|
106
|
-
|
|
108
|
+
/**
|
|
109
|
+
* `info` classifies the failure (`refusalOf` reads it back): what the HTTP status says (429 `rate_limited`, 401 / 403
|
|
110
|
+
* `key_rejected`), else `upstream_unavailable` — unreachable, timed out, a 5xx, redirected or another shape.
|
|
111
|
+
*/
|
|
112
|
+
constructor(host, reason, info) {
|
|
107
113
|
super(`thorchain: ${host} unavailable (${reason})`);
|
|
114
|
+
(0, import_errors.withReason)(this, info ?? "upstream_unavailable");
|
|
108
115
|
}
|
|
109
116
|
};
|
|
117
|
+
function statusRefusal(status, retryAfter) {
|
|
118
|
+
return (0, import_errors.httpRefusal)(status, retryAfter) ?? { reason: "upstream_unavailable", httpStatus: status };
|
|
119
|
+
}
|
|
110
120
|
function paused(entry) {
|
|
111
121
|
return !entry || entry.halted === true || entry.global_trading_paused === true || entry.chain_trading_paused === true;
|
|
112
122
|
}
|
|
@@ -152,7 +162,7 @@ async function nodeRead(host, path, clientId, signal, outer) {
|
|
|
152
162
|
throw new HostUnavailable(host, error instanceof Error ? error.message : "fetch failed");
|
|
153
163
|
}
|
|
154
164
|
if (res.redirected === true || !sameRequestUrl(res.url, url)) throw new HostUnavailable(host, `${path} redirected`);
|
|
155
|
-
if (!res.ok) throw new HostUnavailable(host, `${path} HTTP ${res.status}
|
|
165
|
+
if (!res.ok) throw new HostUnavailable(host, `${path} HTTP ${res.status}`, statusRefusal(res.status, res.headers?.get?.("retry-after")));
|
|
156
166
|
try {
|
|
157
167
|
return await res.json();
|
|
158
168
|
} catch (error) {
|
|
@@ -384,7 +394,7 @@ async function hostGet(host, path, clientId, signal) {
|
|
|
384
394
|
throw new HostUnavailable(host, error instanceof Error ? error.message : "fetch failed");
|
|
385
395
|
}
|
|
386
396
|
if (res.status >= 500 || res.status === 429 || res.status === 403) {
|
|
387
|
-
throw new HostUnavailable(host, `HTTP ${res.status}
|
|
397
|
+
throw new HostUnavailable(host, `HTTP ${res.status}`, statusRefusal(res.status, res.headers?.get?.("retry-after")));
|
|
388
398
|
}
|
|
389
399
|
const body = await res.json().catch(() => null);
|
|
390
400
|
return { ok: res.ok, status: res.status, body };
|
|
@@ -409,30 +419,53 @@ function belowThorMinimums(amount1e8, inbound, quote) {
|
|
|
409
419
|
if (min !== void 0 && (!/^\d+$/.test(String(min)) || amount1e8 < BigInt(min))) return "amount below THORChain recommended minimum";
|
|
410
420
|
return null;
|
|
411
421
|
}
|
|
422
|
+
function nodesFailed(failures) {
|
|
423
|
+
const known = failures.filter((f) => f !== void 0);
|
|
424
|
+
if (!known.length || known.length !== failures.length) return { reason: "upstream_unavailable" };
|
|
425
|
+
const combined = (0, import_errors2.combinedRefusal)(known.map((f) => ({ message: "", ...f })));
|
|
426
|
+
return combined.reason === "no_route" ? { reason: "upstream_unavailable" } : combined;
|
|
427
|
+
}
|
|
428
|
+
function quoteRejection(text, status, bodyless) {
|
|
429
|
+
if (/emit asset 0 less than price limit/.test(text)) return { reason: "amount_too_small" };
|
|
430
|
+
if (/emit asset \d+ less than price limit/.test(text)) return { reason: "price_check_failed" };
|
|
431
|
+
if (/halt|paused/i.test(text)) return { reason: "chain_halted" };
|
|
432
|
+
if (/dust|too small|not enough [a-z ]*(?:to pay|for) (?:the )?fee/i.test(text)) return { reason: "amount_too_small" };
|
|
433
|
+
if (/insufficient liquidity|not enough liquidity/i.test(text)) return { reason: "insufficient_liquidity" };
|
|
434
|
+
if (/pool [^ ]* ?(?:does not exist|doesn't exist|not exist|not found|not available|is not available)|no pool/i.test(text)) return { reason: "unsupported_pair" };
|
|
435
|
+
if (/rate limit|too many requests/i.test(text)) return { reason: "rate_limited" };
|
|
436
|
+
if (/api key|unauthori[sz]ed/i.test(text)) return { reason: "key_rejected" };
|
|
437
|
+
const byStatus = status >= 400 ? (0, import_errors2.httpRefusal)(status) : void 0;
|
|
438
|
+
if (byStatus) return byStatus;
|
|
439
|
+
return bodyless ? { reason: "upstream_unavailable", httpStatus: status } : { reason: "no_route" };
|
|
440
|
+
}
|
|
412
441
|
async function inboundAddressesOn(host, clientId) {
|
|
413
442
|
const res = await hostGet(host, "/thorchain/inbound_addresses", clientId);
|
|
414
|
-
if (!res.ok || !Array.isArray(res.body)) throw new HostUnavailable(host, `inbound_addresses HTTP ${res.status}
|
|
443
|
+
if (!res.ok || !Array.isArray(res.body)) throw new HostUnavailable(host, `inbound_addresses HTTP ${res.status}`, res.ok ? void 0 : statusRefusal(res.status));
|
|
415
444
|
const entries = inboundAddresses(res.body);
|
|
416
445
|
if (!entries) throw new HostUnavailable(host, "inbound_addresses is not a THORNode inbound address list");
|
|
417
446
|
return entries;
|
|
418
447
|
}
|
|
419
|
-
function verifiedMemo(memo, expectedAsset, expectedDestination, expectedRefund, expectedAffiliate, expectedAffiliateBps) {
|
|
420
|
-
|
|
448
|
+
function verifiedMemo(memo, expectedAsset, expectedDestination, expectedRefund, expectedAffiliate, expectedAffiliateBps, why) {
|
|
449
|
+
const refused = (reason) => {
|
|
450
|
+
why?.(reason);
|
|
451
|
+
return null;
|
|
452
|
+
};
|
|
453
|
+
if (memo.includes("|")) return refused("recipient_unverifiable");
|
|
421
454
|
const parts = memo.split(":");
|
|
422
|
-
if (parts.length !== 4 && parts.length !== 6) return
|
|
455
|
+
if (parts.length !== 4 && parts.length !== 6) return refused("policy_refused");
|
|
423
456
|
const [op, asset, destination, limit, affiliate, bps] = parts;
|
|
424
|
-
if (op !== "=" || !asset || !/^[A-Za-z0-9._~\-]+$/.test(asset)) return
|
|
457
|
+
if (op !== "=" || !asset || !/^[A-Za-z0-9._~\-]+$/.test(asset)) return refused("policy_refused");
|
|
425
458
|
const expectedTarget = expectedRefund ? `${expectedDestination}/${expectedRefund}` : expectedDestination;
|
|
426
|
-
if (!sameTarget(destination, expectedTarget)) return
|
|
459
|
+
if (!sameTarget(destination, expectedTarget)) return refused("recipient_mismatch");
|
|
427
460
|
if (parts.length === 6) {
|
|
428
|
-
if (!expectedAffiliate || affiliate !== expectedAffiliate || bps !== expectedAffiliateBps) return
|
|
461
|
+
if (!expectedAffiliate || affiliate !== expectedAffiliate || bps !== expectedAffiliateBps) return refused("policy_refused");
|
|
429
462
|
} else if (expectedAffiliate || expectedAffiliateBps) {
|
|
430
|
-
return
|
|
463
|
+
return refused("policy_refused");
|
|
431
464
|
}
|
|
432
465
|
const stream = (limit ?? "").split("/");
|
|
433
|
-
if (stream.length !== 1 || stream.some((v) => !/^\d+$/.test(v))) return
|
|
466
|
+
if (stream.length !== 1 || stream.some((v) => !/^\d+$/.test(v))) return refused("price_check_failed");
|
|
434
467
|
const floor = BigInt(stream[0] ?? "0");
|
|
435
|
-
if (floor <= 0n) return
|
|
468
|
+
if (floor <= 0n) return refused("price_check_failed");
|
|
436
469
|
const canonical = `=:${expectedAsset}:${expectedTarget}:${stream.join("/")}${expectedAffiliate ? `:${expectedAffiliate}:${expectedAffiliateBps}` : ""}`;
|
|
437
470
|
return { limit: floor, canonical };
|
|
438
471
|
}
|
|
@@ -445,7 +478,7 @@ function thorchainPins(opts = {}) {
|
|
|
445
478
|
for (const n of nodes) if (!/^https:\/\//i.test(n)) throw new Error("thorchain: thorchainPins nodeUrl must use https");
|
|
446
479
|
const clientId = opts.clientId?.trim() || DEFAULT_CLIENT_ID;
|
|
447
480
|
const { maxLagBlocks, timeoutMs } = confirmOptions(opts);
|
|
448
|
-
const askNode = async (host, claim) => {
|
|
481
|
+
const askNode = async (host, claim, failures) => {
|
|
449
482
|
const node = nodeDeadline(timeoutMs);
|
|
450
483
|
const read = (path) => nodeRead(host, path, clientId, node.signal);
|
|
451
484
|
try {
|
|
@@ -453,59 +486,71 @@ function thorchainPins(opts = {}) {
|
|
|
453
486
|
try {
|
|
454
487
|
return { height: head.height, verdict: await vaultVerdict(host, read, head.inbound, claim) };
|
|
455
488
|
} catch (error) {
|
|
456
|
-
if (error instanceof HostUnavailable)
|
|
489
|
+
if (error instanceof HostUnavailable) {
|
|
490
|
+
failures.push((0, import_errors2.refusalOf)(error));
|
|
491
|
+
return { height: head.height, verdict: null };
|
|
492
|
+
}
|
|
457
493
|
throw error;
|
|
458
494
|
}
|
|
459
495
|
} catch (error) {
|
|
460
|
-
if (error instanceof HostUnavailable)
|
|
496
|
+
if (error instanceof HostUnavailable) {
|
|
497
|
+
failures.push((0, import_errors2.refusalOf)(error));
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
461
500
|
throw error;
|
|
462
501
|
} finally {
|
|
463
502
|
node.clear();
|
|
464
503
|
}
|
|
465
504
|
};
|
|
466
505
|
const routersFor = (chain) => [...THORCHAIN_ROUTERS[chain] ?? [], ...opts.trustedRouters?.[chain] ?? []];
|
|
506
|
+
const { note, reasonOf } = (0, import_errors2.refusalNotes)();
|
|
467
507
|
return Object.freeze({
|
|
468
508
|
callTargets: routersFor,
|
|
469
509
|
spenders: routersFor,
|
|
510
|
+
reasonOf,
|
|
470
511
|
// A Tron source is a send to the inbound vault; verifyBuild confirms that vault with two THORNodes (scan 2026-10-28 F9).
|
|
471
512
|
depositAddressRoutes: true,
|
|
472
513
|
async verifyBuild(quote, tx, params) {
|
|
473
|
-
if (!params) return "the request this quote answered is unknown";
|
|
514
|
+
if (!params) return note("the request this quote answered is unknown", "policy_refused");
|
|
474
515
|
const src = (0, import_swap2_assets.ccAsset)(params.sourceChain, params.sourceToken);
|
|
475
516
|
const dst = destAsset(params.destChain, params.destToken);
|
|
476
|
-
if (!src || !dst) return "unknown source or destination asset";
|
|
517
|
+
if (!src || !dst) return note("unknown source or destination asset", "unsupported_pair");
|
|
477
518
|
const chainId = CHAIN_ID[params.sourceChain];
|
|
478
519
|
let vault, memo, router;
|
|
479
520
|
if (chainId) {
|
|
480
|
-
if (!tx.data) return "no router calldata";
|
|
521
|
+
if (!tx.data) return note("no router calldata", "policy_refused");
|
|
481
522
|
let args;
|
|
482
523
|
try {
|
|
483
524
|
const d = (0, import_viem.decodeFunctionData)({ abi: ROUTER_ABI, data: tx.data });
|
|
484
|
-
if (d.functionName !== "depositWithExpiry") return `${d.functionName} is not depositWithExpiry
|
|
525
|
+
if (d.functionName !== "depositWithExpiry") return note(`${d.functionName} is not depositWithExpiry`, "policy_refused");
|
|
485
526
|
args = d.args;
|
|
486
527
|
} catch {
|
|
487
|
-
return "the router calldata does not decode";
|
|
528
|
+
return note("the router calldata does not decode", "policy_refused");
|
|
488
529
|
}
|
|
489
|
-
if (!args || args.length !== 5) return "the router calldata does not decode";
|
|
530
|
+
if (!args || args.length !== 5) return note("the router calldata does not decode", "policy_refused");
|
|
490
531
|
router = tx.to;
|
|
491
|
-
if (!routersFor(params.sourceChain).some((r) => sameTarget(r, router))) return `router ${tx.to} is not pinned
|
|
532
|
+
if (!routersFor(params.sourceChain).some((r) => sameTarget(r, router))) return note(`router ${tx.to} is not pinned`, "vault_mismatch");
|
|
492
533
|
vault = String(args[0]);
|
|
493
534
|
const asset = String(args[1]);
|
|
494
|
-
if (!sameTarget(asset, src.contract ?? NATIVE)) return `the router call moves ${asset}, not the source asset
|
|
495
|
-
if (BigInt(args[2]) !== BigInt(quote.sourceAmount)) return `the router call moves ${String(args[2])}, not the ${quote.sourceAmount} input
|
|
535
|
+
if (!sameTarget(asset, src.contract ?? NATIVE)) return note(`the router call moves ${asset}, not the source asset`, "policy_refused");
|
|
536
|
+
if (BigInt(args[2]) !== BigInt(quote.sourceAmount)) return note(`the router call moves ${String(args[2])}, not the ${quote.sourceAmount} input`, "policy_refused");
|
|
496
537
|
memo = String(args[3]);
|
|
497
538
|
} else {
|
|
498
|
-
if (!tx.memo || tx.value !== quote.sourceAmount) return "the deposit is not a send of the input with a memo";
|
|
539
|
+
if (!tx.memo || tx.value !== quote.sourceAmount) return note("the deposit is not a send of the input with a memo", "policy_refused");
|
|
499
540
|
vault = tx.to;
|
|
500
541
|
memo = tx.memo;
|
|
501
542
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
543
|
+
let memoRefusal = "recipient_mismatch";
|
|
544
|
+
const checked = verifiedMemo(memo, dst.asset, params.destAddress, params.refundAddress ?? "", opts.affiliate ?? "", opts.affiliateBps ?? "", (why) => {
|
|
545
|
+
memoRefusal = why;
|
|
546
|
+
});
|
|
547
|
+
if (!checked) return note(`the memo does not pay ${params.destAddress} in ${dst.asset} with this app's refund and affiliate`, memoRefusal);
|
|
548
|
+
if (memo !== checked.canonical) return note(`the memo is not the canonical ${checked.canonical}`, "policy_refused");
|
|
549
|
+
if (checked.limit * 10n ** BigInt(dst.decimals) / 100000000n < BigInt(quote.minDestAmount)) return note(`the memo's limit ${checked.limit} is below the quoted minimum`, "price_check_failed");
|
|
506
550
|
const sourceChain = THOR_CHAIN[params.sourceChain] ?? params.sourceChain;
|
|
507
551
|
const claim = { chain: sourceChain, destChain: THOR_CHAIN[params.destChain] ?? params.destChain, vault, ...router !== void 0 ? { router } : {} };
|
|
508
|
-
const
|
|
552
|
+
const failures = [];
|
|
553
|
+
const views = await Promise.all(nodes.map((host) => askNode(host, claim, failures)));
|
|
509
554
|
const top = views.reduce((max, v) => v && v.height > max ? v.height : max, -1);
|
|
510
555
|
let agreeing = 0;
|
|
511
556
|
let lagging = false;
|
|
@@ -519,20 +564,20 @@ function thorchainPins(opts = {}) {
|
|
|
519
564
|
if (!v.ok) {
|
|
520
565
|
switch (v.why) {
|
|
521
566
|
case "halted":
|
|
522
|
-
return `${v.chain} is halted / paused
|
|
567
|
+
return note(`${v.chain} is halted / paused`, "chain_halted");
|
|
523
568
|
case "another-vault":
|
|
524
|
-
return `a THORNode reports another inbound vault than ${vault}
|
|
569
|
+
return note(`a THORNode reports another inbound vault than ${vault}`, "vault_mismatch");
|
|
525
570
|
case "not-active":
|
|
526
|
-
return `a THORNode lists the inbound vault as ${v.status}, not ActiveVault
|
|
571
|
+
return note(`a THORNode lists the inbound vault as ${v.status}, not ActiveVault`, "vault_mismatch");
|
|
527
572
|
case "flagged":
|
|
528
|
-
return `a THORNode's mimir flags the inbound vault as ${v.flag}
|
|
573
|
+
return note(`a THORNode's mimir flags the inbound vault as ${v.flag}`, "vault_mismatch");
|
|
529
574
|
case "another-router":
|
|
530
|
-
return "a THORNode reports another router";
|
|
575
|
+
return note("a THORNode reports another router", "vault_mismatch");
|
|
531
576
|
}
|
|
532
577
|
}
|
|
533
578
|
if (++agreeing >= 2) return null;
|
|
534
579
|
}
|
|
535
|
-
return lagging ? `two THORNodes within ${maxLagBlocks} blocks of the highest THORChain height they report could not confirm the inbound vault (behind or unreachable)
|
|
580
|
+
return lagging ? note(`two THORNodes within ${maxLagBlocks} blocks of the highest THORChain height they report could not confirm the inbound vault (behind or unreachable)`, "stale_node") : note("two THORNodes could not confirm the inbound vault", nodesFailed(failures));
|
|
536
581
|
}
|
|
537
582
|
});
|
|
538
583
|
}
|
|
@@ -613,15 +658,15 @@ function thorchainProvider(config = {}) {
|
|
|
613
658
|
const confirmText = (v) => {
|
|
614
659
|
switch (v.why) {
|
|
615
660
|
case "halted":
|
|
616
|
-
return `${v.chain} is halted / paused on the confirming node
|
|
661
|
+
return { text: `${v.chain} is halted / paused on the confirming node`, info: { reason: "chain_halted" } };
|
|
617
662
|
case "another-vault":
|
|
618
|
-
return "a second node reports another inbound vault \u2014 refused until they agree";
|
|
663
|
+
return { text: "a second node reports another inbound vault \u2014 refused until they agree", info: { reason: "vault_mismatch" } };
|
|
619
664
|
case "not-active":
|
|
620
|
-
return `a second node lists the inbound vault as ${v.status}, not ActiveVault \u2014 refused
|
|
665
|
+
return { text: `a second node lists the inbound vault as ${v.status}, not ActiveVault \u2014 refused`, info: { reason: "vault_mismatch" } };
|
|
621
666
|
case "flagged":
|
|
622
|
-
return `a second node's mimir flags the inbound vault as ${v.flag} \u2014 refused
|
|
667
|
+
return { text: `a second node's mimir flags the inbound vault as ${v.flag} \u2014 refused`, info: { reason: "vault_mismatch" } };
|
|
623
668
|
case "another-router":
|
|
624
|
-
return "a second node reports another router \u2014 refused until they agree";
|
|
669
|
+
return { text: "a second node reports another router \u2014 refused until they agree", info: { reason: "vault_mismatch" } };
|
|
625
670
|
}
|
|
626
671
|
};
|
|
627
672
|
const confirmOn = async (host, signal, outer, quoteHeight, claim) => {
|
|
@@ -640,10 +685,14 @@ function thorchainProvider(config = {}) {
|
|
|
640
685
|
};
|
|
641
686
|
let lagging = false;
|
|
642
687
|
let asked = 0;
|
|
688
|
+
const failures = [];
|
|
643
689
|
for (const host of confirmHosts) {
|
|
644
690
|
if (host === quoteHost) continue;
|
|
645
691
|
const budget = Math.min(confirmTimeoutMs, deadlineAt - Date.now());
|
|
646
|
-
if (budget < MIN_CONFIRM_TIMEOUT_MS)
|
|
692
|
+
if (budget < MIN_CONFIRM_TIMEOUT_MS) {
|
|
693
|
+
failures.push({ reason: "upstream_unavailable" });
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
647
696
|
asked++;
|
|
648
697
|
const node = nodeDeadline(budget, outer);
|
|
649
698
|
try {
|
|
@@ -655,19 +704,22 @@ function thorchainProvider(config = {}) {
|
|
|
655
704
|
return verdict;
|
|
656
705
|
} catch (error) {
|
|
657
706
|
if (outer?.aborted) throw error;
|
|
658
|
-
if (error instanceof HostUnavailable)
|
|
707
|
+
if (error instanceof HostUnavailable) {
|
|
708
|
+
failures.push((0, import_errors2.refusalOf)(error));
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
659
711
|
throw error;
|
|
660
712
|
} finally {
|
|
661
713
|
node.clear();
|
|
662
714
|
}
|
|
663
715
|
}
|
|
664
716
|
if (lagging) {
|
|
665
|
-
return `no confirming node is within ${confirmMaxLagBlocks} blocks of the quoting node's THORChain height (behind or unreachable) \u2014 refused until one catches up
|
|
717
|
+
return { text: `no confirming node is within ${confirmMaxLagBlocks} blocks of the quoting node's THORChain height (behind or unreachable) \u2014 refused until one catches up`, info: { reason: "stale_node" } };
|
|
666
718
|
}
|
|
667
719
|
if (asked === 0) {
|
|
668
|
-
return `the ${CONFIRM_BUDGET_MS / 1e3} s confirmation budget ran out before a second node could be asked to confirm the inbound vault \u2014 retry
|
|
720
|
+
return { text: `the ${CONFIRM_BUDGET_MS / 1e3} s confirmation budget ran out before a second node could be asked to confirm the inbound vault \u2014 retry`, info: { reason: "upstream_unavailable" } };
|
|
669
721
|
}
|
|
670
|
-
return "no second node answered to confirm the inbound vault (configure confirmNodeUrl)";
|
|
722
|
+
return { text: "no second node answered to confirm the inbound vault (configure confirmNodeUrl)", info: nodesFailed(failures) };
|
|
671
723
|
};
|
|
672
724
|
const configuredAffiliate = config.affiliate ?? "";
|
|
673
725
|
const configuredAffiliateBps = config.affiliateBps ?? "";
|
|
@@ -713,24 +765,27 @@ function thorchainProvider(config = {}) {
|
|
|
713
765
|
const fromAsset = thorAsset(params.sourceChain, params.sourceToken);
|
|
714
766
|
const dst = destAsset(params.destChain, params.destToken);
|
|
715
767
|
if (!src || !fromAsset || !dst || fromAsset === dst.asset) {
|
|
716
|
-
params.onRefusal?.(`thorchain: no pool for ${params.sourceToken} on ${params.sourceChain} \u2192 ${params.destToken} on ${params.destChain}
|
|
768
|
+
params.onRefusal?.(`thorchain: no pool for ${params.sourceToken} on ${params.sourceChain} \u2192 ${params.destToken} on ${params.destChain}`, { reason: "unsupported_pair" });
|
|
717
769
|
return null;
|
|
718
770
|
}
|
|
719
771
|
const amountIn = BigInt(params.amountIn);
|
|
720
772
|
const amount1e8 = amountIn * 100000000n / 10n ** BigInt(src.decimals);
|
|
721
773
|
if (amount1e8 <= 0n) return null;
|
|
722
774
|
if (amount1e8 * 10n ** BigInt(src.decimals) !== amountIn * 100000000n) {
|
|
723
|
-
params.onRefusal?.(`thorchain: amount has more precision than THORChain's 8 decimals \u2014 round it to 8 decimals
|
|
775
|
+
params.onRefusal?.(`thorchain: amount has more precision than THORChain's 8 decimals \u2014 round it to 8 decimals`, { reason: "policy_refused" });
|
|
724
776
|
return null;
|
|
725
777
|
}
|
|
726
778
|
const requestedTolerance = params.slippageBps ?? Number(TOLERANCE_BPS);
|
|
727
779
|
if (!Number.isInteger(requestedTolerance) || requestedTolerance < 0 || requestedTolerance > (0, import_core.bigintToNumber)(MAX_TOLERANCE_BPS)) {
|
|
728
|
-
params.onRefusal?.(`thorchain: slippage ${String(params.slippageBps)} bps is outside 0\u2013${MAX_TOLERANCE_BPS} bps
|
|
780
|
+
params.onRefusal?.(`thorchain: slippage ${String(params.slippageBps)} bps is outside 0\u2013${MAX_TOLERANCE_BPS} bps`, { reason: "policy_refused" });
|
|
729
781
|
return null;
|
|
730
782
|
}
|
|
731
783
|
const explicitSlippage = params.slippageBps !== void 0;
|
|
732
784
|
const liquidityBps = explicitSlippage ? dst.decimals > 8 ? requestedTolerance - 1 : requestedTolerance : 0;
|
|
733
785
|
const limitMode = liquidityBps > 0 ? "liquidity_tolerance_bps" : "tolerance_bps";
|
|
786
|
+
let quoteStatus = 0;
|
|
787
|
+
let quoteBodyless = false;
|
|
788
|
+
let quoteBodyAborted = false;
|
|
734
789
|
const quoteOn = async (host, bps, limitMode2 = "tolerance_bps") => {
|
|
735
790
|
const qs = new URLSearchParams({
|
|
736
791
|
from_asset: fromAsset,
|
|
@@ -751,6 +806,9 @@ function thorchainProvider(config = {}) {
|
|
|
751
806
|
}
|
|
752
807
|
if (params.refundAddress) qs.set("refund_address", params.refundAddress);
|
|
753
808
|
const res = await hostGet(host, `/thorchain/quote/swap?${qs.toString()}`, clientId, signal);
|
|
809
|
+
quoteStatus = res.status;
|
|
810
|
+
quoteBodyless = res.body === null && !res.ok;
|
|
811
|
+
quoteBodyAborted = res.body === null && signal?.aborted === true;
|
|
754
812
|
if (Array.isArray(res.body)) return null;
|
|
755
813
|
return res.body ?? (res.ok ? null : { error: `HTTP ${res.status}` });
|
|
756
814
|
};
|
|
@@ -761,13 +819,14 @@ function thorchainProvider(config = {}) {
|
|
|
761
819
|
let quoteHeight = 0;
|
|
762
820
|
let confirmDeadlineAt = 0;
|
|
763
821
|
let heightUnavailable = false;
|
|
822
|
+
const hostFailures = [];
|
|
764
823
|
for (const host of hosts) {
|
|
765
824
|
try {
|
|
766
825
|
if (limitMode === "liquidity_tolerance_bps") {
|
|
767
826
|
q = await quoteOn(host, String(liquidityBps), "liquidity_tolerance_bps");
|
|
768
827
|
const feeBps = Number(q?.fees?.total_bps);
|
|
769
828
|
if (Number.isFinite(feeBps) && feeBps > (0, import_core.bigintToNumber)(MAX_TOLERANCE_BPS)) {
|
|
770
|
-
params.onRefusal?.(`thorchain: THORChain fees ${(feeBps / 100).toFixed(2)}% \u2014 too expensive at this amount (cap ${(0, import_core.bigintToNumber)(MAX_TOLERANCE_BPS) / 100}%)
|
|
829
|
+
params.onRefusal?.(`thorchain: THORChain fees ${(feeBps / 100).toFixed(2)}% \u2014 too expensive at this amount (cap ${(0, import_core.bigintToNumber)(MAX_TOLERANCE_BPS) / 100}%)`, { reason: "policy_refused" });
|
|
771
830
|
return null;
|
|
772
831
|
}
|
|
773
832
|
} else {
|
|
@@ -784,25 +843,28 @@ function thorchainProvider(config = {}) {
|
|
|
784
843
|
}
|
|
785
844
|
}
|
|
786
845
|
if (q?.error || q?.message) {
|
|
787
|
-
params.onRefusal?.(`thorchain: ${String(q.error ?? q.message).slice(0, 200)}
|
|
846
|
+
params.onRefusal?.(`thorchain: ${String(q.error ?? q.message).slice(0, 200)}`, quoteBodyAborted ? (0, import_errors2.httpRefusal)(quoteStatus) : quoteRejection(String(q.error ?? q.message), quoteStatus, quoteBodyless));
|
|
788
847
|
return null;
|
|
789
848
|
}
|
|
790
849
|
if (!q || !q.expected_amount_out || !q.inbound_address || !q.memo || !q.expiry) {
|
|
791
|
-
params.onRefusal?.(`thorchain: incomplete quote from ${host} (expected_amount_out / inbound_address / memo / expiry missing)
|
|
850
|
+
params.onRefusal?.(`thorchain: incomplete quote from ${host} (expected_amount_out / inbound_address / memo / expiry missing)`, quoteBodyAborted ? void 0 : { reason: "upstream_unavailable", ...quoteStatus >= 300 ? { httpStatus: quoteStatus } : {} });
|
|
792
851
|
return null;
|
|
793
852
|
}
|
|
794
853
|
if (chainId ? !q.router : !!q.router) {
|
|
795
|
-
params.onRefusal?.(chainId ? "thorchain: quote has no router for an EVM source" : "thorchain: quote names a router for a deposit source \u2014 execution model changed");
|
|
854
|
+
params.onRefusal?.(chainId ? "thorchain: quote has no router for an EVM source" : "thorchain: quote names a router for a deposit source \u2014 execution model changed", { reason: chainId ? "upstream_unavailable" : "policy_refused" });
|
|
796
855
|
return null;
|
|
797
856
|
}
|
|
798
|
-
|
|
857
|
+
let memoRefusal = "recipient_mismatch";
|
|
858
|
+
checkedMemo = verifiedMemo(q.memo, dst.asset, params.destAddress, params.refundAddress ?? "", configuredAffiliate, configuredAffiliateBps, (why) => {
|
|
859
|
+
memoRefusal = why;
|
|
860
|
+
});
|
|
799
861
|
if (!checkedMemo) {
|
|
800
|
-
params.onRefusal?.(`thorchain: memo failed verification (${String(q.memo).slice(0, 80)})
|
|
862
|
+
params.onRefusal?.(`thorchain: memo failed verification (${String(q.memo).slice(0, 80)})`, { reason: memoRefusal });
|
|
801
863
|
return null;
|
|
802
864
|
}
|
|
803
865
|
const floor1e8 = params.minAmountOut !== void 0 ? toThorUnitsCeil(BigInt(params.minAmountOut), dst.decimals) : void 0;
|
|
804
866
|
if (floor1e8 !== void 0 && checkedMemo.limit < floor1e8) {
|
|
805
|
-
params.onRefusal?.(`thorchain: floor breach (limit=${checkedMemo.limit} < floor=${floor1e8}, 1e8 units)
|
|
867
|
+
params.onRefusal?.(`thorchain: floor breach (limit=${checkedMemo.limit} < floor=${floor1e8}, 1e8 units)`, { reason: "price_check_failed" });
|
|
806
868
|
return null;
|
|
807
869
|
}
|
|
808
870
|
const deadlineAt = Date.now() + CONFIRM_BUDGET_MS;
|
|
@@ -818,6 +880,7 @@ function thorchainProvider(config = {}) {
|
|
|
818
880
|
break;
|
|
819
881
|
} catch (error) {
|
|
820
882
|
if (error instanceof HostUnavailable) {
|
|
883
|
+
hostFailures.push((0, import_errors2.refusalOf)(error));
|
|
821
884
|
q = null;
|
|
822
885
|
checkedMemo = null;
|
|
823
886
|
inbounds = null;
|
|
@@ -828,7 +891,8 @@ function thorchainProvider(config = {}) {
|
|
|
828
891
|
}
|
|
829
892
|
if (!q?.expected_amount_out || !q.inbound_address || !q.expiry || !checkedMemo || !inbounds) {
|
|
830
893
|
params.onRefusal?.(
|
|
831
|
-
inbounds ? "thorchain: no quote from any node" : heightUnavailable ? "thorchain: THORChain height (/thorchain/lastblock) unavailable from every node that answered the inbound addresses" : "thorchain: inbound addresses unavailable from every node"
|
|
894
|
+
inbounds ? "thorchain: no quote from any node" : heightUnavailable ? "thorchain: THORChain height (/thorchain/lastblock) unavailable from every node that answered the inbound addresses" : "thorchain: inbound addresses unavailable from every node",
|
|
895
|
+
inbounds ? { reason: "no_route" } : nodesFailed(hostFailures)
|
|
832
896
|
);
|
|
833
897
|
return null;
|
|
834
898
|
}
|
|
@@ -836,19 +900,19 @@ function thorchainProvider(config = {}) {
|
|
|
836
900
|
const srcInbound = inbounds.find((entry) => entry.chain === THOR_CHAIN[params.sourceChain]);
|
|
837
901
|
const dstInbound = inbounds.find((entry) => entry.chain === THOR_CHAIN[params.destChain]);
|
|
838
902
|
if (paused(srcInbound) || paused(dstInbound)) {
|
|
839
|
-
params.onRefusal?.(`thorchain: ${paused(srcInbound) ? THOR_CHAIN[params.sourceChain] : THOR_CHAIN[params.destChain]} chain is halted / paused on THORChain
|
|
903
|
+
params.onRefusal?.(`thorchain: ${paused(srcInbound) ? THOR_CHAIN[params.sourceChain] : THOR_CHAIN[params.destChain]} chain is halted / paused on THORChain`, { reason: "chain_halted" });
|
|
840
904
|
return null;
|
|
841
905
|
}
|
|
842
906
|
if (!sameTarget(srcInbound?.address, q.inbound_address)) {
|
|
843
|
-
params.onRefusal?.("thorchain: quote inbound address differs from the node's current inbound (vault rotated) \u2014 retry");
|
|
907
|
+
params.onRefusal?.("thorchain: quote inbound address differs from the node's current inbound (vault rotated) \u2014 retry", { reason: "vault_mismatch" });
|
|
844
908
|
return null;
|
|
845
909
|
}
|
|
846
910
|
if (chainId && !sameTarget(srcInbound?.router, q.router)) {
|
|
847
|
-
params.onRefusal?.("thorchain: quote router differs from the node's current router \u2014 retry");
|
|
911
|
+
params.onRefusal?.("thorchain: quote router differs from the node's current router \u2014 retry", { reason: "vault_mismatch" });
|
|
848
912
|
return null;
|
|
849
913
|
}
|
|
850
914
|
if (chainId && !pinnedRouter(params.sourceChain, q.router)) {
|
|
851
|
-
params.onRefusal?.(`thorchain: router ${String(q.router)} is not a THORChain router this module trusts on ${params.sourceChain} (THORCHAIN_ROUTERS / trustedRouters)
|
|
915
|
+
params.onRefusal?.(`thorchain: router ${String(q.router)} is not a THORChain router this module trusts on ${params.sourceChain} (THORCHAIN_ROUTERS / trustedRouters)`, { reason: "vault_mismatch" });
|
|
852
916
|
return null;
|
|
853
917
|
}
|
|
854
918
|
const unconfirmed = await confirmedElsewhere(
|
|
@@ -862,36 +926,36 @@ function thorchainProvider(config = {}) {
|
|
|
862
926
|
signal
|
|
863
927
|
);
|
|
864
928
|
if (unconfirmed) {
|
|
865
|
-
params.onRefusal?.(`thorchain: ${unconfirmed}
|
|
929
|
+
params.onRefusal?.(`thorchain: ${unconfirmed.text}`, unconfirmed.info);
|
|
866
930
|
return null;
|
|
867
931
|
}
|
|
868
932
|
const tooSmall = belowThorMinimums(amount1e8, srcInbound, q);
|
|
869
933
|
if (tooSmall) {
|
|
870
|
-
params.onRefusal?.(`thorchain: ${tooSmall}
|
|
934
|
+
params.onRefusal?.(`thorchain: ${tooSmall}`, { reason: "amount_too_small" });
|
|
871
935
|
return null;
|
|
872
936
|
}
|
|
873
937
|
const outUnits = BigInt(q.expected_amount_out) * 10n ** BigInt(dst.decimals) / 100000000n;
|
|
874
938
|
if (outUnits <= 0n) {
|
|
875
|
-
params.onRefusal?.("thorchain: expected output rounds to zero");
|
|
939
|
+
params.onRefusal?.("thorchain: expected output rounds to zero", { reason: "amount_too_small" });
|
|
876
940
|
return null;
|
|
877
941
|
}
|
|
878
942
|
const memoMinUnits = limit1e8 * 10n ** BigInt(dst.decimals) / 100000000n;
|
|
879
943
|
const minUnits = memoMinUnits;
|
|
880
944
|
if (minUnits <= 0n || minUnits > outUnits) {
|
|
881
|
-
params.onRefusal?.(`thorchain: memo limit ${limit1e8} inconsistent with expected output ${q.expected_amount_out}
|
|
945
|
+
params.onRefusal?.(`thorchain: memo limit ${limit1e8} inconsistent with expected output ${q.expected_amount_out}`, { reason: "price_check_failed" });
|
|
882
946
|
return null;
|
|
883
947
|
}
|
|
884
948
|
if (params.slippageBps !== void 0) {
|
|
885
949
|
const slippageFloor = outUnits * BigInt(1e4 - params.slippageBps) / 10000n;
|
|
886
950
|
if (minUnits < slippageFloor) {
|
|
887
|
-
params.onRefusal?.(`thorchain: memo limit ${limit1e8} is below expected ${q.expected_amount_out} minus ${params.slippageBps} bps slippage (1e8 units)
|
|
951
|
+
params.onRefusal?.(`thorchain: memo limit ${limit1e8} is below expected ${q.expected_amount_out} minus ${params.slippageBps} bps slippage (1e8 units)`, { reason: "price_check_failed" });
|
|
888
952
|
return null;
|
|
889
953
|
}
|
|
890
954
|
}
|
|
891
955
|
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
892
956
|
const executionExpiry = Math.min(q.expiry, nowSeconds + 15 * 60);
|
|
893
957
|
if (!Number.isInteger(executionExpiry) || executionExpiry <= nowSeconds + 30) {
|
|
894
|
-
params.onRefusal?.(`thorchain: quote expires in ${Number.isInteger(executionExpiry) ? executionExpiry - nowSeconds : "?"} s \u2014 too soon to execute
|
|
958
|
+
params.onRefusal?.(`thorchain: quote expires in ${Number.isInteger(executionExpiry) ? executionExpiry - nowSeconds : "?"} s \u2014 too soon to execute`, { reason: "quote_expired" });
|
|
895
959
|
return null;
|
|
896
960
|
}
|
|
897
961
|
const assetAddr = src.contract ? src.contract : NATIVE;
|
|
@@ -947,7 +1011,7 @@ function thorchainProvider(config = {}) {
|
|
|
947
1011
|
if (!quote.txRequest) {
|
|
948
1012
|
const sourceChain = quote.providerData?.sourceChain ?? "";
|
|
949
1013
|
if (!isDepositSource(sourceChain) || !quote.depositAddress || !quote.depositMemo) {
|
|
950
|
-
throw
|
|
1014
|
+
throw (0, import_errors2.refusal)("unsupported_pair", "thorchain: deposit-address execution is only supported for an opted-in Tron source");
|
|
951
1015
|
}
|
|
952
1016
|
return { chain: sourceChain, to: quote.depositAddress, value: quote.sourceAmount, memo: quote.depositMemo };
|
|
953
1017
|
}
|
|
@@ -964,26 +1028,28 @@ function thorchainProvider(config = {}) {
|
|
|
964
1028
|
async preflightDeposit(quote, tx) {
|
|
965
1029
|
const data = quote.providerData;
|
|
966
1030
|
if (!data?.sourceChain || !data.destChain || !data.inboundAddress || !data.assetAddress || !data.expiry || !data.memo) {
|
|
967
|
-
throw
|
|
1031
|
+
throw (0, import_errors2.refusal)("policy_refused", "thorchain: quote is missing execution-bound metadata");
|
|
968
1032
|
}
|
|
969
1033
|
const evmSource = CHAIN_ID[data.sourceChain] !== void 0;
|
|
970
|
-
if (evmSource && !data.router) throw
|
|
971
|
-
if (!evmSource && !isDepositSource(data.sourceChain)) throw
|
|
1034
|
+
if (evmSource && !data.router) throw (0, import_errors2.refusal)("policy_refused", "thorchain: quote is missing execution-bound metadata");
|
|
1035
|
+
if (!evmSource && !isDepositSource(data.sourceChain)) throw (0, import_errors2.refusal)("unsupported_pair", "thorchain: source chain has no executor");
|
|
972
1036
|
if (!/^\d+$/.test(data.expiry) || BigInt(data.expiry) <= BigInt(Math.floor(Date.now() / 1e3))) {
|
|
973
|
-
throw
|
|
1037
|
+
throw (0, import_errors2.refusal)("quote_expired", "thorchain: quote expired before broadcast");
|
|
974
1038
|
}
|
|
975
|
-
if (evmSource && !pinnedRouter(data.sourceChain, data.router)) throw
|
|
1039
|
+
if (evmSource && !pinnedRouter(data.sourceChain, data.router)) throw (0, import_errors2.refusal)("vault_mismatch", "thorchain: the quote's router is not one this module trusts");
|
|
976
1040
|
let inbounds = null;
|
|
977
1041
|
let snapshotHost = "";
|
|
978
1042
|
let snapshotHeight = 0;
|
|
979
1043
|
let confirmDeadlineAt = 0;
|
|
980
1044
|
let heightUnavailable = false;
|
|
1045
|
+
const hostFailures = [];
|
|
981
1046
|
for (const host of hosts) {
|
|
982
1047
|
const deadlineAt = Date.now() + CONFIRM_BUDGET_MS;
|
|
983
1048
|
try {
|
|
984
1049
|
const snapshot = await nodeSnapshot(host);
|
|
985
1050
|
if ("heightError" in snapshot) {
|
|
986
1051
|
heightUnavailable = true;
|
|
1052
|
+
hostFailures.push((0, import_errors2.refusalOf)(snapshot.heightError));
|
|
987
1053
|
continue;
|
|
988
1054
|
}
|
|
989
1055
|
inbounds = snapshot.inbounds;
|
|
@@ -994,12 +1060,13 @@ function thorchainProvider(config = {}) {
|
|
|
994
1060
|
} catch (error) {
|
|
995
1061
|
inbounds = null;
|
|
996
1062
|
if (!(error instanceof HostUnavailable)) throw error;
|
|
1063
|
+
hostFailures.push((0, import_errors2.refusalOf)(error));
|
|
997
1064
|
}
|
|
998
1065
|
}
|
|
999
1066
|
if (!inbounds) {
|
|
1000
|
-
throw new Error(
|
|
1067
|
+
throw (0, import_errors2.withReason)(new Error(
|
|
1001
1068
|
heightUnavailable ? "thorchain: cannot read THORChain height (/thorchain/lastblock) before broadcast from any node that answered the inbound addresses" : "thorchain: cannot refresh inbound addresses before broadcast"
|
|
1002
|
-
);
|
|
1069
|
+
), nodesFailed(hostFailures));
|
|
1003
1070
|
}
|
|
1004
1071
|
const confirm = async () => {
|
|
1005
1072
|
const unconfirmed = await confirmedElsewhere(
|
|
@@ -1011,44 +1078,46 @@ function thorchainProvider(config = {}) {
|
|
|
1011
1078
|
evmSource ? data.router : void 0,
|
|
1012
1079
|
confirmDeadlineAt
|
|
1013
1080
|
);
|
|
1014
|
-
if (unconfirmed) throw new Error(`thorchain: ${unconfirmed}`);
|
|
1081
|
+
if (unconfirmed) throw (0, import_errors2.withReason)(new Error(`thorchain: ${unconfirmed.text}`), unconfirmed.info);
|
|
1015
1082
|
};
|
|
1016
1083
|
const src = inbounds.find((entry) => entry.chain === THOR_CHAIN[data.sourceChain]);
|
|
1017
1084
|
const dst = inbounds.find((entry) => entry.chain === THOR_CHAIN[data.destChain]);
|
|
1018
|
-
if (paused(src) || paused(dst)) throw
|
|
1019
|
-
if (!sameTarget(src?.address, data.inboundAddress)) throw
|
|
1085
|
+
if (paused(src) || paused(dst)) throw (0, import_errors2.refusal)("chain_halted", "thorchain: source or destination chain trading is paused");
|
|
1086
|
+
if (!sameTarget(src?.address, data.inboundAddress)) throw (0, import_errors2.refusal)("vault_mismatch", "thorchain: inbound vault rotated after quote");
|
|
1020
1087
|
if (!evmSource) {
|
|
1021
1088
|
if (tx.data !== void 0 || tx.approvalAddress !== void 0 || tx.bodyBoc !== void 0 || tx.typedData !== void 0 || tx.serializedTx !== void 0 || tx.stateInitBoc !== void 0) {
|
|
1022
|
-
throw
|
|
1089
|
+
throw (0, import_errors2.refusal)("policy_refused", "thorchain: deposit-address execution must be a plain send with memo");
|
|
1023
1090
|
}
|
|
1024
1091
|
if (!sameTarget(tx.to, data.inboundAddress) || tx.memo !== data.memo || tx.value !== quote.sourceAmount || tx.chain && tx.chain !== data.sourceChain) {
|
|
1025
|
-
throw
|
|
1092
|
+
throw (0, import_errors2.refusal)(!sameTarget(tx.to, data.inboundAddress) ? "vault_mismatch" : "policy_refused", "thorchain: deposit does not match the validated quote");
|
|
1026
1093
|
}
|
|
1027
1094
|
const srcAsset = (0, import_swap2_assets.ccAssetByToken)(data.sourceChain, sameTarget(data.assetAddress, NATIVE) ? "" : data.assetAddress);
|
|
1028
|
-
if (!srcAsset) throw
|
|
1095
|
+
if (!srcAsset) throw (0, import_errors2.refusal)("unsupported_pair", "thorchain: unknown source asset");
|
|
1029
1096
|
const amount1e8 = BigInt(quote.sourceAmount) * 100000000n / 10n ** BigInt(srcAsset.decimals);
|
|
1030
1097
|
const tooSmall = belowThorMinimums(amount1e8, src, null);
|
|
1031
|
-
if (tooSmall) throw
|
|
1098
|
+
if (tooSmall) throw (0, import_errors2.refusal)("amount_too_small", `thorchain: ${tooSmall}`);
|
|
1032
1099
|
await confirm();
|
|
1033
1100
|
return;
|
|
1034
1101
|
}
|
|
1035
1102
|
if (!sameTarget(src?.router, data.router) || !sameTarget(tx.to, data.router)) {
|
|
1036
|
-
throw
|
|
1103
|
+
throw (0, import_errors2.refusal)("vault_mismatch", "thorchain: router rotated after quote");
|
|
1037
1104
|
}
|
|
1038
|
-
if (!tx.data) throw
|
|
1105
|
+
if (!tx.data) throw (0, import_errors2.refusal)("policy_refused", "thorchain: missing depositWithExpiry calldata");
|
|
1039
1106
|
let decoded;
|
|
1040
1107
|
try {
|
|
1041
1108
|
decoded = (0, import_viem.decodeFunctionData)({ abi: ROUTER_ABI, data: tx.data });
|
|
1042
1109
|
} catch {
|
|
1043
|
-
throw
|
|
1110
|
+
throw (0, import_errors2.refusal)("policy_refused", "thorchain: malformed depositWithExpiry calldata");
|
|
1044
1111
|
}
|
|
1045
1112
|
const args = decoded.args;
|
|
1046
1113
|
if (decoded.functionName !== "depositWithExpiry" || !args || args.length !== 5 || !sameTarget(String(args[0]), data.inboundAddress) || !sameTarget(String(args[1]), data.assetAddress) || BigInt(args[2]) !== BigInt(quote.sourceAmount) || String(args[3]) !== data.memo || BigInt(args[4]) !== BigInt(data.expiry) || tx.chainId !== CHAIN_ID[data.sourceChain]) {
|
|
1047
|
-
|
|
1114
|
+
const vaultChanged = decoded.functionName === "depositWithExpiry" && !!args && args.length === 5 && !sameTarget(String(args[0]), data.inboundAddress);
|
|
1115
|
+
throw (0, import_errors2.refusal)(vaultChanged ? "vault_mismatch" : "policy_refused", "thorchain: calldata does not match the validated quote");
|
|
1048
1116
|
}
|
|
1049
1117
|
const tokenSource = !sameTarget(data.assetAddress, NATIVE);
|
|
1050
1118
|
if (tx.value !== (tokenSource ? "0" : quote.sourceAmount) || (tokenSource ? !sameTarget(tx.approvalAddress, data.router) : tx.approvalAddress !== void 0)) {
|
|
1051
|
-
|
|
1119
|
+
const spenderChanged = tokenSource && tx.approvalAddress !== void 0 && !sameTarget(tx.approvalAddress, data.router);
|
|
1120
|
+
throw (0, import_errors2.refusal)(spenderChanged ? "vault_mismatch" : "policy_refused", "thorchain: value/approval does not match the validated source asset");
|
|
1052
1121
|
}
|
|
1053
1122
|
await confirm();
|
|
1054
1123
|
},
|