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