@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.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ThorchainSwap2Protocol
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-YKA6IYKX.js";
|
|
4
4
|
import {
|
|
5
5
|
THORCHAIN_ROUTERS,
|
|
6
6
|
THORNODE_CONFIRM_PATHS,
|
|
7
7
|
thorchainPins,
|
|
8
8
|
thorchainProvider
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-EIAXGWNG.js";
|
|
10
10
|
import {
|
|
11
11
|
THORCHAIN_DEPOSIT_WITH_EXPIRY,
|
|
12
12
|
THORCHAIN_MODULE,
|
package/dist/protocol.cjs
CHANGED
|
@@ -67,10 +67,12 @@ function describeThorchainActivity(tx, ctx) {
|
|
|
67
67
|
// src/provider.ts
|
|
68
68
|
var import_viem = require("viem");
|
|
69
69
|
var import_util2 = require("@anywalletkit/protocol-swap2-core/util");
|
|
70
|
+
var import_errors2 = require("@anywalletkit/protocol-swap2-core/errors");
|
|
70
71
|
var import_swap2_assets = require("@anywalletkit/swap2-assets");
|
|
71
72
|
var import_core = require("@anywalletkit/core");
|
|
72
73
|
|
|
73
74
|
// src/confirm.ts
|
|
75
|
+
var import_errors = require("@anywalletkit/protocol-swap2-core/errors");
|
|
74
76
|
var THORNODE_CONFIRM_PATHS = Object.freeze({
|
|
75
77
|
lastblock: "/thorchain/lastblock",
|
|
76
78
|
inboundAddresses: "/thorchain/inbound_addresses",
|
|
@@ -93,10 +95,18 @@ function confirmOptions(o) {
|
|
|
93
95
|
return { maxLagBlocks, timeoutMs };
|
|
94
96
|
}
|
|
95
97
|
var HostUnavailable = class extends Error {
|
|
96
|
-
|
|
98
|
+
/**
|
|
99
|
+
* `info` classifies the failure (`refusalOf` reads it back): what the HTTP status says (429 `rate_limited`, 401 / 403
|
|
100
|
+
* `key_rejected`), else `upstream_unavailable` — unreachable, timed out, a 5xx, redirected or another shape.
|
|
101
|
+
*/
|
|
102
|
+
constructor(host, reason, info) {
|
|
97
103
|
super(`thorchain: ${host} unavailable (${reason})`);
|
|
104
|
+
(0, import_errors.withReason)(this, info ?? "upstream_unavailable");
|
|
98
105
|
}
|
|
99
106
|
};
|
|
107
|
+
function statusRefusal(status, retryAfter) {
|
|
108
|
+
return (0, import_errors.httpRefusal)(status, retryAfter) ?? { reason: "upstream_unavailable", httpStatus: status };
|
|
109
|
+
}
|
|
100
110
|
function paused(entry) {
|
|
101
111
|
return !entry || entry.halted === true || entry.global_trading_paused === true || entry.chain_trading_paused === true;
|
|
102
112
|
}
|
|
@@ -142,7 +152,7 @@ async function nodeRead(host, path, clientId, signal, outer) {
|
|
|
142
152
|
throw new HostUnavailable(host, error instanceof Error ? error.message : "fetch failed");
|
|
143
153
|
}
|
|
144
154
|
if (res.redirected === true || !sameRequestUrl(res.url, url)) throw new HostUnavailable(host, `${path} redirected`);
|
|
145
|
-
if (!res.ok) throw new HostUnavailable(host, `${path} HTTP ${res.status}
|
|
155
|
+
if (!res.ok) throw new HostUnavailable(host, `${path} HTTP ${res.status}`, statusRefusal(res.status, res.headers?.get?.("retry-after")));
|
|
146
156
|
try {
|
|
147
157
|
return await res.json();
|
|
148
158
|
} catch (error) {
|
|
@@ -374,7 +384,7 @@ async function hostGet(host, path, clientId, signal) {
|
|
|
374
384
|
throw new HostUnavailable(host, error instanceof Error ? error.message : "fetch failed");
|
|
375
385
|
}
|
|
376
386
|
if (res.status >= 500 || res.status === 429 || res.status === 403) {
|
|
377
|
-
throw new HostUnavailable(host, `HTTP ${res.status}
|
|
387
|
+
throw new HostUnavailable(host, `HTTP ${res.status}`, statusRefusal(res.status, res.headers?.get?.("retry-after")));
|
|
378
388
|
}
|
|
379
389
|
const body = await res.json().catch(() => null);
|
|
380
390
|
return { ok: res.ok, status: res.status, body };
|
|
@@ -399,30 +409,53 @@ function belowThorMinimums(amount1e8, inbound, quote) {
|
|
|
399
409
|
if (min !== void 0 && (!/^\d+$/.test(String(min)) || amount1e8 < BigInt(min))) return "amount below THORChain recommended minimum";
|
|
400
410
|
return null;
|
|
401
411
|
}
|
|
412
|
+
function nodesFailed(failures) {
|
|
413
|
+
const known = failures.filter((f) => f !== void 0);
|
|
414
|
+
if (!known.length || known.length !== failures.length) return { reason: "upstream_unavailable" };
|
|
415
|
+
const combined = (0, import_errors2.combinedRefusal)(known.map((f) => ({ message: "", ...f })));
|
|
416
|
+
return combined.reason === "no_route" ? { reason: "upstream_unavailable" } : combined;
|
|
417
|
+
}
|
|
418
|
+
function quoteRejection(text, status, bodyless) {
|
|
419
|
+
if (/emit asset 0 less than price limit/.test(text)) return { reason: "amount_too_small" };
|
|
420
|
+
if (/emit asset \d+ less than price limit/.test(text)) return { reason: "price_check_failed" };
|
|
421
|
+
if (/halt|paused/i.test(text)) return { reason: "chain_halted" };
|
|
422
|
+
if (/dust|too small|not enough [a-z ]*(?:to pay|for) (?:the )?fee/i.test(text)) return { reason: "amount_too_small" };
|
|
423
|
+
if (/insufficient liquidity|not enough liquidity/i.test(text)) return { reason: "insufficient_liquidity" };
|
|
424
|
+
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" };
|
|
425
|
+
if (/rate limit|too many requests/i.test(text)) return { reason: "rate_limited" };
|
|
426
|
+
if (/api key|unauthori[sz]ed/i.test(text)) return { reason: "key_rejected" };
|
|
427
|
+
const byStatus = status >= 400 ? (0, import_errors2.httpRefusal)(status) : void 0;
|
|
428
|
+
if (byStatus) return byStatus;
|
|
429
|
+
return bodyless ? { reason: "upstream_unavailable", httpStatus: status } : { reason: "no_route" };
|
|
430
|
+
}
|
|
402
431
|
async function inboundAddressesOn(host, clientId) {
|
|
403
432
|
const res = await hostGet(host, "/thorchain/inbound_addresses", clientId);
|
|
404
|
-
if (!res.ok || !Array.isArray(res.body)) throw new HostUnavailable(host, `inbound_addresses HTTP ${res.status}
|
|
433
|
+
if (!res.ok || !Array.isArray(res.body)) throw new HostUnavailable(host, `inbound_addresses HTTP ${res.status}`, res.ok ? void 0 : statusRefusal(res.status));
|
|
405
434
|
const entries = inboundAddresses(res.body);
|
|
406
435
|
if (!entries) throw new HostUnavailable(host, "inbound_addresses is not a THORNode inbound address list");
|
|
407
436
|
return entries;
|
|
408
437
|
}
|
|
409
|
-
function verifiedMemo(memo, expectedAsset, expectedDestination, expectedRefund, expectedAffiliate, expectedAffiliateBps) {
|
|
410
|
-
|
|
438
|
+
function verifiedMemo(memo, expectedAsset, expectedDestination, expectedRefund, expectedAffiliate, expectedAffiliateBps, why) {
|
|
439
|
+
const refused = (reason) => {
|
|
440
|
+
why?.(reason);
|
|
441
|
+
return null;
|
|
442
|
+
};
|
|
443
|
+
if (memo.includes("|")) return refused("recipient_unverifiable");
|
|
411
444
|
const parts = memo.split(":");
|
|
412
|
-
if (parts.length !== 4 && parts.length !== 6) return
|
|
445
|
+
if (parts.length !== 4 && parts.length !== 6) return refused("policy_refused");
|
|
413
446
|
const [op, asset, destination, limit, affiliate, bps] = parts;
|
|
414
|
-
if (op !== "=" || !asset || !/^[A-Za-z0-9._~\-]+$/.test(asset)) return
|
|
447
|
+
if (op !== "=" || !asset || !/^[A-Za-z0-9._~\-]+$/.test(asset)) return refused("policy_refused");
|
|
415
448
|
const expectedTarget = expectedRefund ? `${expectedDestination}/${expectedRefund}` : expectedDestination;
|
|
416
|
-
if (!sameTarget(destination, expectedTarget)) return
|
|
449
|
+
if (!sameTarget(destination, expectedTarget)) return refused("recipient_mismatch");
|
|
417
450
|
if (parts.length === 6) {
|
|
418
|
-
if (!expectedAffiliate || affiliate !== expectedAffiliate || bps !== expectedAffiliateBps) return
|
|
451
|
+
if (!expectedAffiliate || affiliate !== expectedAffiliate || bps !== expectedAffiliateBps) return refused("policy_refused");
|
|
419
452
|
} else if (expectedAffiliate || expectedAffiliateBps) {
|
|
420
|
-
return
|
|
453
|
+
return refused("policy_refused");
|
|
421
454
|
}
|
|
422
455
|
const stream = (limit ?? "").split("/");
|
|
423
|
-
if (stream.length !== 1 || stream.some((v) => !/^\d+$/.test(v))) return
|
|
456
|
+
if (stream.length !== 1 || stream.some((v) => !/^\d+$/.test(v))) return refused("price_check_failed");
|
|
424
457
|
const floor = BigInt(stream[0] ?? "0");
|
|
425
|
-
if (floor <= 0n) return
|
|
458
|
+
if (floor <= 0n) return refused("price_check_failed");
|
|
426
459
|
const canonical = `=:${expectedAsset}:${expectedTarget}:${stream.join("/")}${expectedAffiliate ? `:${expectedAffiliate}:${expectedAffiliateBps}` : ""}`;
|
|
427
460
|
return { limit: floor, canonical };
|
|
428
461
|
}
|
|
@@ -507,15 +540,15 @@ function thorchainProvider(config = {}) {
|
|
|
507
540
|
const confirmText = (v) => {
|
|
508
541
|
switch (v.why) {
|
|
509
542
|
case "halted":
|
|
510
|
-
return `${v.chain} is halted / paused on the confirming node
|
|
543
|
+
return { text: `${v.chain} is halted / paused on the confirming node`, info: { reason: "chain_halted" } };
|
|
511
544
|
case "another-vault":
|
|
512
|
-
return "a second node reports another inbound vault \u2014 refused until they agree";
|
|
545
|
+
return { text: "a second node reports another inbound vault \u2014 refused until they agree", info: { reason: "vault_mismatch" } };
|
|
513
546
|
case "not-active":
|
|
514
|
-
return `a second node lists the inbound vault as ${v.status}, not ActiveVault \u2014 refused
|
|
547
|
+
return { text: `a second node lists the inbound vault as ${v.status}, not ActiveVault \u2014 refused`, info: { reason: "vault_mismatch" } };
|
|
515
548
|
case "flagged":
|
|
516
|
-
return `a second node's mimir flags the inbound vault as ${v.flag} \u2014 refused
|
|
549
|
+
return { text: `a second node's mimir flags the inbound vault as ${v.flag} \u2014 refused`, info: { reason: "vault_mismatch" } };
|
|
517
550
|
case "another-router":
|
|
518
|
-
return "a second node reports another router \u2014 refused until they agree";
|
|
551
|
+
return { text: "a second node reports another router \u2014 refused until they agree", info: { reason: "vault_mismatch" } };
|
|
519
552
|
}
|
|
520
553
|
};
|
|
521
554
|
const confirmOn = async (host, signal, outer, quoteHeight, claim) => {
|
|
@@ -534,10 +567,14 @@ function thorchainProvider(config = {}) {
|
|
|
534
567
|
};
|
|
535
568
|
let lagging = false;
|
|
536
569
|
let asked = 0;
|
|
570
|
+
const failures = [];
|
|
537
571
|
for (const host of confirmHosts) {
|
|
538
572
|
if (host === quoteHost) continue;
|
|
539
573
|
const budget = Math.min(confirmTimeoutMs, deadlineAt - Date.now());
|
|
540
|
-
if (budget < MIN_CONFIRM_TIMEOUT_MS)
|
|
574
|
+
if (budget < MIN_CONFIRM_TIMEOUT_MS) {
|
|
575
|
+
failures.push({ reason: "upstream_unavailable" });
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
541
578
|
asked++;
|
|
542
579
|
const node = nodeDeadline(budget, outer);
|
|
543
580
|
try {
|
|
@@ -549,19 +586,22 @@ function thorchainProvider(config = {}) {
|
|
|
549
586
|
return verdict;
|
|
550
587
|
} catch (error) {
|
|
551
588
|
if (outer?.aborted) throw error;
|
|
552
|
-
if (error instanceof HostUnavailable)
|
|
589
|
+
if (error instanceof HostUnavailable) {
|
|
590
|
+
failures.push((0, import_errors2.refusalOf)(error));
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
553
593
|
throw error;
|
|
554
594
|
} finally {
|
|
555
595
|
node.clear();
|
|
556
596
|
}
|
|
557
597
|
}
|
|
558
598
|
if (lagging) {
|
|
559
|
-
return `no confirming node is within ${confirmMaxLagBlocks} blocks of the quoting node's THORChain height (behind or unreachable) \u2014 refused until one catches up
|
|
599
|
+
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" } };
|
|
560
600
|
}
|
|
561
601
|
if (asked === 0) {
|
|
562
|
-
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
|
|
602
|
+
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" } };
|
|
563
603
|
}
|
|
564
|
-
return "no second node answered to confirm the inbound vault (configure confirmNodeUrl)";
|
|
604
|
+
return { text: "no second node answered to confirm the inbound vault (configure confirmNodeUrl)", info: nodesFailed(failures) };
|
|
565
605
|
};
|
|
566
606
|
const configuredAffiliate = config.affiliate ?? "";
|
|
567
607
|
const configuredAffiliateBps = config.affiliateBps ?? "";
|
|
@@ -607,24 +647,27 @@ function thorchainProvider(config = {}) {
|
|
|
607
647
|
const fromAsset = thorAsset(params.sourceChain, params.sourceToken);
|
|
608
648
|
const dst = destAsset(params.destChain, params.destToken);
|
|
609
649
|
if (!src || !fromAsset || !dst || fromAsset === dst.asset) {
|
|
610
|
-
params.onRefusal?.(`thorchain: no pool for ${params.sourceToken} on ${params.sourceChain} \u2192 ${params.destToken} on ${params.destChain}
|
|
650
|
+
params.onRefusal?.(`thorchain: no pool for ${params.sourceToken} on ${params.sourceChain} \u2192 ${params.destToken} on ${params.destChain}`, { reason: "unsupported_pair" });
|
|
611
651
|
return null;
|
|
612
652
|
}
|
|
613
653
|
const amountIn = BigInt(params.amountIn);
|
|
614
654
|
const amount1e8 = amountIn * 100000000n / 10n ** BigInt(src.decimals);
|
|
615
655
|
if (amount1e8 <= 0n) return null;
|
|
616
656
|
if (amount1e8 * 10n ** BigInt(src.decimals) !== amountIn * 100000000n) {
|
|
617
|
-
params.onRefusal?.(`thorchain: amount has more precision than THORChain's 8 decimals \u2014 round it to 8 decimals
|
|
657
|
+
params.onRefusal?.(`thorchain: amount has more precision than THORChain's 8 decimals \u2014 round it to 8 decimals`, { reason: "policy_refused" });
|
|
618
658
|
return null;
|
|
619
659
|
}
|
|
620
660
|
const requestedTolerance = params.slippageBps ?? Number(TOLERANCE_BPS);
|
|
621
661
|
if (!Number.isInteger(requestedTolerance) || requestedTolerance < 0 || requestedTolerance > (0, import_core.bigintToNumber)(MAX_TOLERANCE_BPS)) {
|
|
622
|
-
params.onRefusal?.(`thorchain: slippage ${String(params.slippageBps)} bps is outside 0\u2013${MAX_TOLERANCE_BPS} bps
|
|
662
|
+
params.onRefusal?.(`thorchain: slippage ${String(params.slippageBps)} bps is outside 0\u2013${MAX_TOLERANCE_BPS} bps`, { reason: "policy_refused" });
|
|
623
663
|
return null;
|
|
624
664
|
}
|
|
625
665
|
const explicitSlippage = params.slippageBps !== void 0;
|
|
626
666
|
const liquidityBps = explicitSlippage ? dst.decimals > 8 ? requestedTolerance - 1 : requestedTolerance : 0;
|
|
627
667
|
const limitMode = liquidityBps > 0 ? "liquidity_tolerance_bps" : "tolerance_bps";
|
|
668
|
+
let quoteStatus = 0;
|
|
669
|
+
let quoteBodyless = false;
|
|
670
|
+
let quoteBodyAborted = false;
|
|
628
671
|
const quoteOn = async (host, bps, limitMode2 = "tolerance_bps") => {
|
|
629
672
|
const qs = new URLSearchParams({
|
|
630
673
|
from_asset: fromAsset,
|
|
@@ -645,6 +688,9 @@ function thorchainProvider(config = {}) {
|
|
|
645
688
|
}
|
|
646
689
|
if (params.refundAddress) qs.set("refund_address", params.refundAddress);
|
|
647
690
|
const res = await hostGet(host, `/thorchain/quote/swap?${qs.toString()}`, clientId, signal);
|
|
691
|
+
quoteStatus = res.status;
|
|
692
|
+
quoteBodyless = res.body === null && !res.ok;
|
|
693
|
+
quoteBodyAborted = res.body === null && signal?.aborted === true;
|
|
648
694
|
if (Array.isArray(res.body)) return null;
|
|
649
695
|
return res.body ?? (res.ok ? null : { error: `HTTP ${res.status}` });
|
|
650
696
|
};
|
|
@@ -655,13 +701,14 @@ function thorchainProvider(config = {}) {
|
|
|
655
701
|
let quoteHeight = 0;
|
|
656
702
|
let confirmDeadlineAt = 0;
|
|
657
703
|
let heightUnavailable = false;
|
|
704
|
+
const hostFailures = [];
|
|
658
705
|
for (const host of hosts) {
|
|
659
706
|
try {
|
|
660
707
|
if (limitMode === "liquidity_tolerance_bps") {
|
|
661
708
|
q = await quoteOn(host, String(liquidityBps), "liquidity_tolerance_bps");
|
|
662
709
|
const feeBps = Number(q?.fees?.total_bps);
|
|
663
710
|
if (Number.isFinite(feeBps) && feeBps > (0, import_core.bigintToNumber)(MAX_TOLERANCE_BPS)) {
|
|
664
|
-
params.onRefusal?.(`thorchain: THORChain fees ${(feeBps / 100).toFixed(2)}% \u2014 too expensive at this amount (cap ${(0, import_core.bigintToNumber)(MAX_TOLERANCE_BPS) / 100}%)
|
|
711
|
+
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" });
|
|
665
712
|
return null;
|
|
666
713
|
}
|
|
667
714
|
} else {
|
|
@@ -678,25 +725,28 @@ function thorchainProvider(config = {}) {
|
|
|
678
725
|
}
|
|
679
726
|
}
|
|
680
727
|
if (q?.error || q?.message) {
|
|
681
|
-
params.onRefusal?.(`thorchain: ${String(q.error ?? q.message).slice(0, 200)}
|
|
728
|
+
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));
|
|
682
729
|
return null;
|
|
683
730
|
}
|
|
684
731
|
if (!q || !q.expected_amount_out || !q.inbound_address || !q.memo || !q.expiry) {
|
|
685
|
-
params.onRefusal?.(`thorchain: incomplete quote from ${host} (expected_amount_out / inbound_address / memo / expiry missing)
|
|
732
|
+
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 } : {} });
|
|
686
733
|
return null;
|
|
687
734
|
}
|
|
688
735
|
if (chainId ? !q.router : !!q.router) {
|
|
689
|
-
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");
|
|
736
|
+
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" });
|
|
690
737
|
return null;
|
|
691
738
|
}
|
|
692
|
-
|
|
739
|
+
let memoRefusal = "recipient_mismatch";
|
|
740
|
+
checkedMemo = verifiedMemo(q.memo, dst.asset, params.destAddress, params.refundAddress ?? "", configuredAffiliate, configuredAffiliateBps, (why) => {
|
|
741
|
+
memoRefusal = why;
|
|
742
|
+
});
|
|
693
743
|
if (!checkedMemo) {
|
|
694
|
-
params.onRefusal?.(`thorchain: memo failed verification (${String(q.memo).slice(0, 80)})
|
|
744
|
+
params.onRefusal?.(`thorchain: memo failed verification (${String(q.memo).slice(0, 80)})`, { reason: memoRefusal });
|
|
695
745
|
return null;
|
|
696
746
|
}
|
|
697
747
|
const floor1e8 = params.minAmountOut !== void 0 ? toThorUnitsCeil(BigInt(params.minAmountOut), dst.decimals) : void 0;
|
|
698
748
|
if (floor1e8 !== void 0 && checkedMemo.limit < floor1e8) {
|
|
699
|
-
params.onRefusal?.(`thorchain: floor breach (limit=${checkedMemo.limit} < floor=${floor1e8}, 1e8 units)
|
|
749
|
+
params.onRefusal?.(`thorchain: floor breach (limit=${checkedMemo.limit} < floor=${floor1e8}, 1e8 units)`, { reason: "price_check_failed" });
|
|
700
750
|
return null;
|
|
701
751
|
}
|
|
702
752
|
const deadlineAt = Date.now() + CONFIRM_BUDGET_MS;
|
|
@@ -712,6 +762,7 @@ function thorchainProvider(config = {}) {
|
|
|
712
762
|
break;
|
|
713
763
|
} catch (error) {
|
|
714
764
|
if (error instanceof HostUnavailable) {
|
|
765
|
+
hostFailures.push((0, import_errors2.refusalOf)(error));
|
|
715
766
|
q = null;
|
|
716
767
|
checkedMemo = null;
|
|
717
768
|
inbounds = null;
|
|
@@ -722,7 +773,8 @@ function thorchainProvider(config = {}) {
|
|
|
722
773
|
}
|
|
723
774
|
if (!q?.expected_amount_out || !q.inbound_address || !q.expiry || !checkedMemo || !inbounds) {
|
|
724
775
|
params.onRefusal?.(
|
|
725
|
-
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"
|
|
776
|
+
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",
|
|
777
|
+
inbounds ? { reason: "no_route" } : nodesFailed(hostFailures)
|
|
726
778
|
);
|
|
727
779
|
return null;
|
|
728
780
|
}
|
|
@@ -730,19 +782,19 @@ function thorchainProvider(config = {}) {
|
|
|
730
782
|
const srcInbound = inbounds.find((entry) => entry.chain === THOR_CHAIN[params.sourceChain]);
|
|
731
783
|
const dstInbound = inbounds.find((entry) => entry.chain === THOR_CHAIN[params.destChain]);
|
|
732
784
|
if (paused(srcInbound) || paused(dstInbound)) {
|
|
733
|
-
params.onRefusal?.(`thorchain: ${paused(srcInbound) ? THOR_CHAIN[params.sourceChain] : THOR_CHAIN[params.destChain]} chain is halted / paused on THORChain
|
|
785
|
+
params.onRefusal?.(`thorchain: ${paused(srcInbound) ? THOR_CHAIN[params.sourceChain] : THOR_CHAIN[params.destChain]} chain is halted / paused on THORChain`, { reason: "chain_halted" });
|
|
734
786
|
return null;
|
|
735
787
|
}
|
|
736
788
|
if (!sameTarget(srcInbound?.address, q.inbound_address)) {
|
|
737
|
-
params.onRefusal?.("thorchain: quote inbound address differs from the node's current inbound (vault rotated) \u2014 retry");
|
|
789
|
+
params.onRefusal?.("thorchain: quote inbound address differs from the node's current inbound (vault rotated) \u2014 retry", { reason: "vault_mismatch" });
|
|
738
790
|
return null;
|
|
739
791
|
}
|
|
740
792
|
if (chainId && !sameTarget(srcInbound?.router, q.router)) {
|
|
741
|
-
params.onRefusal?.("thorchain: quote router differs from the node's current router \u2014 retry");
|
|
793
|
+
params.onRefusal?.("thorchain: quote router differs from the node's current router \u2014 retry", { reason: "vault_mismatch" });
|
|
742
794
|
return null;
|
|
743
795
|
}
|
|
744
796
|
if (chainId && !pinnedRouter(params.sourceChain, q.router)) {
|
|
745
|
-
params.onRefusal?.(`thorchain: router ${String(q.router)} is not a THORChain router this module trusts on ${params.sourceChain} (THORCHAIN_ROUTERS / trustedRouters)
|
|
797
|
+
params.onRefusal?.(`thorchain: router ${String(q.router)} is not a THORChain router this module trusts on ${params.sourceChain} (THORCHAIN_ROUTERS / trustedRouters)`, { reason: "vault_mismatch" });
|
|
746
798
|
return null;
|
|
747
799
|
}
|
|
748
800
|
const unconfirmed = await confirmedElsewhere(
|
|
@@ -756,36 +808,36 @@ function thorchainProvider(config = {}) {
|
|
|
756
808
|
signal
|
|
757
809
|
);
|
|
758
810
|
if (unconfirmed) {
|
|
759
|
-
params.onRefusal?.(`thorchain: ${unconfirmed}
|
|
811
|
+
params.onRefusal?.(`thorchain: ${unconfirmed.text}`, unconfirmed.info);
|
|
760
812
|
return null;
|
|
761
813
|
}
|
|
762
814
|
const tooSmall = belowThorMinimums(amount1e8, srcInbound, q);
|
|
763
815
|
if (tooSmall) {
|
|
764
|
-
params.onRefusal?.(`thorchain: ${tooSmall}
|
|
816
|
+
params.onRefusal?.(`thorchain: ${tooSmall}`, { reason: "amount_too_small" });
|
|
765
817
|
return null;
|
|
766
818
|
}
|
|
767
819
|
const outUnits = BigInt(q.expected_amount_out) * 10n ** BigInt(dst.decimals) / 100000000n;
|
|
768
820
|
if (outUnits <= 0n) {
|
|
769
|
-
params.onRefusal?.("thorchain: expected output rounds to zero");
|
|
821
|
+
params.onRefusal?.("thorchain: expected output rounds to zero", { reason: "amount_too_small" });
|
|
770
822
|
return null;
|
|
771
823
|
}
|
|
772
824
|
const memoMinUnits = limit1e8 * 10n ** BigInt(dst.decimals) / 100000000n;
|
|
773
825
|
const minUnits = memoMinUnits;
|
|
774
826
|
if (minUnits <= 0n || minUnits > outUnits) {
|
|
775
|
-
params.onRefusal?.(`thorchain: memo limit ${limit1e8} inconsistent with expected output ${q.expected_amount_out}
|
|
827
|
+
params.onRefusal?.(`thorchain: memo limit ${limit1e8} inconsistent with expected output ${q.expected_amount_out}`, { reason: "price_check_failed" });
|
|
776
828
|
return null;
|
|
777
829
|
}
|
|
778
830
|
if (params.slippageBps !== void 0) {
|
|
779
831
|
const slippageFloor = outUnits * BigInt(1e4 - params.slippageBps) / 10000n;
|
|
780
832
|
if (minUnits < slippageFloor) {
|
|
781
|
-
params.onRefusal?.(`thorchain: memo limit ${limit1e8} is below expected ${q.expected_amount_out} minus ${params.slippageBps} bps slippage (1e8 units)
|
|
833
|
+
params.onRefusal?.(`thorchain: memo limit ${limit1e8} is below expected ${q.expected_amount_out} minus ${params.slippageBps} bps slippage (1e8 units)`, { reason: "price_check_failed" });
|
|
782
834
|
return null;
|
|
783
835
|
}
|
|
784
836
|
}
|
|
785
837
|
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
786
838
|
const executionExpiry = Math.min(q.expiry, nowSeconds + 15 * 60);
|
|
787
839
|
if (!Number.isInteger(executionExpiry) || executionExpiry <= nowSeconds + 30) {
|
|
788
|
-
params.onRefusal?.(`thorchain: quote expires in ${Number.isInteger(executionExpiry) ? executionExpiry - nowSeconds : "?"} s \u2014 too soon to execute
|
|
840
|
+
params.onRefusal?.(`thorchain: quote expires in ${Number.isInteger(executionExpiry) ? executionExpiry - nowSeconds : "?"} s \u2014 too soon to execute`, { reason: "quote_expired" });
|
|
789
841
|
return null;
|
|
790
842
|
}
|
|
791
843
|
const assetAddr = src.contract ? src.contract : NATIVE;
|
|
@@ -841,7 +893,7 @@ function thorchainProvider(config = {}) {
|
|
|
841
893
|
if (!quote.txRequest) {
|
|
842
894
|
const sourceChain = quote.providerData?.sourceChain ?? "";
|
|
843
895
|
if (!isDepositSource(sourceChain) || !quote.depositAddress || !quote.depositMemo) {
|
|
844
|
-
throw
|
|
896
|
+
throw (0, import_errors2.refusal)("unsupported_pair", "thorchain: deposit-address execution is only supported for an opted-in Tron source");
|
|
845
897
|
}
|
|
846
898
|
return { chain: sourceChain, to: quote.depositAddress, value: quote.sourceAmount, memo: quote.depositMemo };
|
|
847
899
|
}
|
|
@@ -858,26 +910,28 @@ function thorchainProvider(config = {}) {
|
|
|
858
910
|
async preflightDeposit(quote, tx) {
|
|
859
911
|
const data = quote.providerData;
|
|
860
912
|
if (!data?.sourceChain || !data.destChain || !data.inboundAddress || !data.assetAddress || !data.expiry || !data.memo) {
|
|
861
|
-
throw
|
|
913
|
+
throw (0, import_errors2.refusal)("policy_refused", "thorchain: quote is missing execution-bound metadata");
|
|
862
914
|
}
|
|
863
915
|
const evmSource = CHAIN_ID[data.sourceChain] !== void 0;
|
|
864
|
-
if (evmSource && !data.router) throw
|
|
865
|
-
if (!evmSource && !isDepositSource(data.sourceChain)) throw
|
|
916
|
+
if (evmSource && !data.router) throw (0, import_errors2.refusal)("policy_refused", "thorchain: quote is missing execution-bound metadata");
|
|
917
|
+
if (!evmSource && !isDepositSource(data.sourceChain)) throw (0, import_errors2.refusal)("unsupported_pair", "thorchain: source chain has no executor");
|
|
866
918
|
if (!/^\d+$/.test(data.expiry) || BigInt(data.expiry) <= BigInt(Math.floor(Date.now() / 1e3))) {
|
|
867
|
-
throw
|
|
919
|
+
throw (0, import_errors2.refusal)("quote_expired", "thorchain: quote expired before broadcast");
|
|
868
920
|
}
|
|
869
|
-
if (evmSource && !pinnedRouter(data.sourceChain, data.router)) throw
|
|
921
|
+
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");
|
|
870
922
|
let inbounds = null;
|
|
871
923
|
let snapshotHost = "";
|
|
872
924
|
let snapshotHeight = 0;
|
|
873
925
|
let confirmDeadlineAt = 0;
|
|
874
926
|
let heightUnavailable = false;
|
|
927
|
+
const hostFailures = [];
|
|
875
928
|
for (const host of hosts) {
|
|
876
929
|
const deadlineAt = Date.now() + CONFIRM_BUDGET_MS;
|
|
877
930
|
try {
|
|
878
931
|
const snapshot = await nodeSnapshot(host);
|
|
879
932
|
if ("heightError" in snapshot) {
|
|
880
933
|
heightUnavailable = true;
|
|
934
|
+
hostFailures.push((0, import_errors2.refusalOf)(snapshot.heightError));
|
|
881
935
|
continue;
|
|
882
936
|
}
|
|
883
937
|
inbounds = snapshot.inbounds;
|
|
@@ -888,12 +942,13 @@ function thorchainProvider(config = {}) {
|
|
|
888
942
|
} catch (error) {
|
|
889
943
|
inbounds = null;
|
|
890
944
|
if (!(error instanceof HostUnavailable)) throw error;
|
|
945
|
+
hostFailures.push((0, import_errors2.refusalOf)(error));
|
|
891
946
|
}
|
|
892
947
|
}
|
|
893
948
|
if (!inbounds) {
|
|
894
|
-
throw new Error(
|
|
949
|
+
throw (0, import_errors2.withReason)(new Error(
|
|
895
950
|
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"
|
|
896
|
-
);
|
|
951
|
+
), nodesFailed(hostFailures));
|
|
897
952
|
}
|
|
898
953
|
const confirm = async () => {
|
|
899
954
|
const unconfirmed = await confirmedElsewhere(
|
|
@@ -905,44 +960,46 @@ function thorchainProvider(config = {}) {
|
|
|
905
960
|
evmSource ? data.router : void 0,
|
|
906
961
|
confirmDeadlineAt
|
|
907
962
|
);
|
|
908
|
-
if (unconfirmed) throw new Error(`thorchain: ${unconfirmed}`);
|
|
963
|
+
if (unconfirmed) throw (0, import_errors2.withReason)(new Error(`thorchain: ${unconfirmed.text}`), unconfirmed.info);
|
|
909
964
|
};
|
|
910
965
|
const src = inbounds.find((entry) => entry.chain === THOR_CHAIN[data.sourceChain]);
|
|
911
966
|
const dst = inbounds.find((entry) => entry.chain === THOR_CHAIN[data.destChain]);
|
|
912
|
-
if (paused(src) || paused(dst)) throw
|
|
913
|
-
if (!sameTarget(src?.address, data.inboundAddress)) throw
|
|
967
|
+
if (paused(src) || paused(dst)) throw (0, import_errors2.refusal)("chain_halted", "thorchain: source or destination chain trading is paused");
|
|
968
|
+
if (!sameTarget(src?.address, data.inboundAddress)) throw (0, import_errors2.refusal)("vault_mismatch", "thorchain: inbound vault rotated after quote");
|
|
914
969
|
if (!evmSource) {
|
|
915
970
|
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) {
|
|
916
|
-
throw
|
|
971
|
+
throw (0, import_errors2.refusal)("policy_refused", "thorchain: deposit-address execution must be a plain send with memo");
|
|
917
972
|
}
|
|
918
973
|
if (!sameTarget(tx.to, data.inboundAddress) || tx.memo !== data.memo || tx.value !== quote.sourceAmount || tx.chain && tx.chain !== data.sourceChain) {
|
|
919
|
-
throw
|
|
974
|
+
throw (0, import_errors2.refusal)(!sameTarget(tx.to, data.inboundAddress) ? "vault_mismatch" : "policy_refused", "thorchain: deposit does not match the validated quote");
|
|
920
975
|
}
|
|
921
976
|
const srcAsset = (0, import_swap2_assets.ccAssetByToken)(data.sourceChain, sameTarget(data.assetAddress, NATIVE) ? "" : data.assetAddress);
|
|
922
|
-
if (!srcAsset) throw
|
|
977
|
+
if (!srcAsset) throw (0, import_errors2.refusal)("unsupported_pair", "thorchain: unknown source asset");
|
|
923
978
|
const amount1e8 = BigInt(quote.sourceAmount) * 100000000n / 10n ** BigInt(srcAsset.decimals);
|
|
924
979
|
const tooSmall = belowThorMinimums(amount1e8, src, null);
|
|
925
|
-
if (tooSmall) throw
|
|
980
|
+
if (tooSmall) throw (0, import_errors2.refusal)("amount_too_small", `thorchain: ${tooSmall}`);
|
|
926
981
|
await confirm();
|
|
927
982
|
return;
|
|
928
983
|
}
|
|
929
984
|
if (!sameTarget(src?.router, data.router) || !sameTarget(tx.to, data.router)) {
|
|
930
|
-
throw
|
|
985
|
+
throw (0, import_errors2.refusal)("vault_mismatch", "thorchain: router rotated after quote");
|
|
931
986
|
}
|
|
932
|
-
if (!tx.data) throw
|
|
987
|
+
if (!tx.data) throw (0, import_errors2.refusal)("policy_refused", "thorchain: missing depositWithExpiry calldata");
|
|
933
988
|
let decoded;
|
|
934
989
|
try {
|
|
935
990
|
decoded = (0, import_viem.decodeFunctionData)({ abi: ROUTER_ABI, data: tx.data });
|
|
936
991
|
} catch {
|
|
937
|
-
throw
|
|
992
|
+
throw (0, import_errors2.refusal)("policy_refused", "thorchain: malformed depositWithExpiry calldata");
|
|
938
993
|
}
|
|
939
994
|
const args = decoded.args;
|
|
940
995
|
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]) {
|
|
941
|
-
|
|
996
|
+
const vaultChanged = decoded.functionName === "depositWithExpiry" && !!args && args.length === 5 && !sameTarget(String(args[0]), data.inboundAddress);
|
|
997
|
+
throw (0, import_errors2.refusal)(vaultChanged ? "vault_mismatch" : "policy_refused", "thorchain: calldata does not match the validated quote");
|
|
942
998
|
}
|
|
943
999
|
const tokenSource = !sameTarget(data.assetAddress, NATIVE);
|
|
944
1000
|
if (tx.value !== (tokenSource ? "0" : quote.sourceAmount) || (tokenSource ? !sameTarget(tx.approvalAddress, data.router) : tx.approvalAddress !== void 0)) {
|
|
945
|
-
|
|
1001
|
+
const spenderChanged = tokenSource && tx.approvalAddress !== void 0 && !sameTarget(tx.approvalAddress, data.router);
|
|
1002
|
+
throw (0, import_errors2.refusal)(spenderChanged ? "vault_mismatch" : "policy_refused", "thorchain: value/approval does not match the validated source asset");
|
|
946
1003
|
}
|
|
947
1004
|
await confirm();
|
|
948
1005
|
},
|