@anywalletkit/protocol-swap2-thorchain 0.0.0-stage → 0.1.3
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/LICENSE +202 -0
- package/README.md +24 -2
- package/dist/activity.cjs +69 -0
- package/dist/activity.cjs.map +1 -0
- package/dist/activity.d.cts +24 -0
- package/dist/activity.d.ts +24 -0
- package/dist/activity.js +16 -0
- package/dist/activity.js.map +1 -0
- package/dist/chunk-AVFULD4D.js +565 -0
- package/dist/chunk-AVFULD4D.js.map +1 -0
- package/dist/chunk-BN25E4FP.js +27 -0
- package/dist/chunk-BN25E4FP.js.map +1 -0
- package/dist/chunk-DIQDMNP7.js +34 -0
- package/dist/chunk-DIQDMNP7.js.map +1 -0
- package/dist/chunk-X6XXFQ2M.js +21 -0
- package/dist/chunk-X6XXFQ2M.js.map +1 -0
- package/dist/config.cjs +41 -0
- package/dist/config.cjs.map +1 -0
- package/dist/config.d.cts +9 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -0
- package/dist/index.cjs +643 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.cjs +635 -0
- package/dist/protocol.cjs.map +1 -0
- package/dist/protocol.d.cts +15 -0
- package/dist/protocol.d.ts +15 -0
- package/dist/protocol.js +10 -0
- package/dist/protocol.js.map +1 -0
- package/dist/provider.cjs +619 -0
- package/dist/provider.cjs.map +1 -0
- package/dist/provider.d.cts +26 -0
- package/dist/provider.d.ts +26 -0
- package/dist/provider.js +9 -0
- package/dist/provider.js.map +1 -0
- package/module.json +20 -0
- package/package.json +70 -3
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/provider.ts
|
|
21
|
+
var provider_exports = {};
|
|
22
|
+
__export(provider_exports, {
|
|
23
|
+
thorchainProvider: () => thorchainProvider
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(provider_exports);
|
|
26
|
+
|
|
27
|
+
// src/config.ts
|
|
28
|
+
var import_util = require("@anywalletkit/protocol-swap2-core/util");
|
|
29
|
+
var THOR_EVM = /* @__PURE__ */ new Set([1, 8453, 56, 43114]);
|
|
30
|
+
var THOR_NS = /* @__PURE__ */ new Set(["bitcoin", "cosmos", "solana", "tron", "xrp", "litecoin", "doge", "bch"]);
|
|
31
|
+
var thorchain = {
|
|
32
|
+
enabled: () => true,
|
|
33
|
+
affiliate: () => "",
|
|
34
|
+
affiliateBps: () => "",
|
|
35
|
+
supportsChain(chain) {
|
|
36
|
+
if (THOR_NS.has((0, import_util.namespace)(chain))) return true;
|
|
37
|
+
return (0, import_util.isEvm)(chain) && THOR_EVM.has((0, import_util.evmRef)(chain));
|
|
38
|
+
},
|
|
39
|
+
supports(from, to) {
|
|
40
|
+
return this.supportsChain(from) && this.supportsChain(to);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// src/activity.ts
|
|
45
|
+
var import_activity = require("@anywalletkit/protocol-swap2-core/activity");
|
|
46
|
+
var THORCHAIN_MODULE = "@anywalletkit/protocol-swap2-thorchain";
|
|
47
|
+
var THORCHAIN_DEPOSIT_WITH_EXPIRY = "0x44bc937b";
|
|
48
|
+
var THORCHAIN_TRANSFER_OUT = "0x574da717";
|
|
49
|
+
var THORCHAIN_TRANSFER_OUT_AND_CALL = "0xec341444";
|
|
50
|
+
function describeThorchainActivity(tx, ctx) {
|
|
51
|
+
if (!tx.chain.startsWith("eip155:") || !thorchain.supportsChain(tx.chain)) return null;
|
|
52
|
+
const selector = (0, import_activity.callSelector)(tx);
|
|
53
|
+
if (selector === THORCHAIN_DEPOSIT_WITH_EXPIRY) {
|
|
54
|
+
const isOwn = (a) => ctx.own.has(a) || ctx.own.has(a.toLowerCase());
|
|
55
|
+
if (!isOwn(tx.from)) return null;
|
|
56
|
+
return (0, import_activity.describeSwapLike)(tx, ctx, { protocol: THORCHAIN_MODULE, subtitle: "THORChain", kind: "auto" });
|
|
57
|
+
}
|
|
58
|
+
if (selector === THORCHAIN_TRANSFER_OUT || selector === THORCHAIN_TRANSFER_OUT_AND_CALL) {
|
|
59
|
+
const { assetsIn } = (0, import_activity.ownerAssets)(tx, ctx);
|
|
60
|
+
if (assetsIn.length === 0) return null;
|
|
61
|
+
return (0, import_activity.describeSwapLike)(tx, ctx, { protocol: THORCHAIN_MODULE, subtitle: "THORChain", kind: "bridge", inSuffix: "via THORChain" });
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/provider.ts
|
|
67
|
+
var import_viem = require("viem");
|
|
68
|
+
var import_util2 = require("@anywalletkit/protocol-swap2-core/util");
|
|
69
|
+
var import_swap2_assets = require("@anywalletkit/swap2-assets");
|
|
70
|
+
var HOSTS = [
|
|
71
|
+
"https://gateway.liquify.com/chain/thorchain_api",
|
|
72
|
+
"https://thornode.thorchain.network"
|
|
73
|
+
];
|
|
74
|
+
var DEFAULT_CLIENT_ID = "anywalletkit";
|
|
75
|
+
var NATIVE = "0x0000000000000000000000000000000000000000";
|
|
76
|
+
var TOLERANCE_BPS = "300";
|
|
77
|
+
var MAX_TOLERANCE_BPS = 500n;
|
|
78
|
+
var QUOTE_VALIDITY_MS = 5 * 6e4;
|
|
79
|
+
var THOR_CHAIN = {
|
|
80
|
+
"eip155:1": "ETH",
|
|
81
|
+
"eip155:8453": "BASE",
|
|
82
|
+
"eip155:56": "BSC",
|
|
83
|
+
// 2026-09-16: AVAX.AVAX/USDC/USDT pools Available (≈$2.5M/$0.3M/$0.2M),
|
|
84
|
+
// router 0x00dc61…f1d4 live in inbound_addresses; same depositWithExpiry ABI.
|
|
85
|
+
// Polygon has no THORChain pool any more — deliberately absent.
|
|
86
|
+
"eip155:43114": "AVAX",
|
|
87
|
+
"tron:728126428": "TRON",
|
|
88
|
+
"bitcoin:mainnet": "BTC",
|
|
89
|
+
// 2026-08 (pools verified live via the Liquify gateway): THORChain added
|
|
90
|
+
// SOL/XRP, and our catalog now carries LTC/DOGE/BCH/ATOM — destination legs
|
|
91
|
+
// for the step-2 off-ramp (source stays EVM/Tron, see supportsRoute).
|
|
92
|
+
"solana:mainnet": "SOL",
|
|
93
|
+
"xrp:mainnet": "XRP",
|
|
94
|
+
"litecoin:mainnet": "LTC",
|
|
95
|
+
"doge:mainnet": "DOGE",
|
|
96
|
+
"bch:mainnet": "BCH",
|
|
97
|
+
"cosmos:cosmoshub-4": "GAIA"
|
|
98
|
+
};
|
|
99
|
+
var CHAIN_ID = { "eip155:1": 1, "eip155:8453": 8453, "eip155:56": 56, "eip155:43114": 43114 };
|
|
100
|
+
var TRON_CAIP2 = "tron:728126428";
|
|
101
|
+
var DEPOSIT_SOURCES = /* @__PURE__ */ new Set([TRON_CAIP2]);
|
|
102
|
+
var NATIVE_ASSET = {
|
|
103
|
+
ETH: "ETH.ETH",
|
|
104
|
+
BASE: "BASE.ETH",
|
|
105
|
+
BSC: "BSC.BNB",
|
|
106
|
+
AVAX: "AVAX.AVAX",
|
|
107
|
+
TRON: "TRON.TRX",
|
|
108
|
+
BTC: "BTC.BTC",
|
|
109
|
+
SOL: "SOL.SOL",
|
|
110
|
+
XRP: "XRP.XRP",
|
|
111
|
+
LTC: "LTC.LTC",
|
|
112
|
+
DOGE: "DOGE.DOGE",
|
|
113
|
+
BCH: "BCH.BCH",
|
|
114
|
+
GAIA: "GAIA.ATOM"
|
|
115
|
+
};
|
|
116
|
+
var TOKEN_ASSET = {
|
|
117
|
+
"ETH:USDC": "ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48",
|
|
118
|
+
"ETH:USDT": "ETH.USDT-0XDAC17F958D2EE523A2206206994597C13D831EC7",
|
|
119
|
+
"BASE:USDC": "BASE.USDC-0X833589FCD6EDB6E08F4C7C32D4F71B54BDA02913",
|
|
120
|
+
"BSC:USDC": "BSC.USDC-0X8AC76A51CC950D9822D68B83FE1AD97B32CD580D",
|
|
121
|
+
"BSC:USDT": "BSC.USDT-0X55D398326F99059FF775485246999027B3197955",
|
|
122
|
+
"AVAX:USDC": "AVAX.USDC-0XB97EF9EF8734C71904D8002F8B6BC66DD9C48A6E",
|
|
123
|
+
"AVAX:USDT": "AVAX.USDT-0X9702230A8EA53601F5CD2DC00FDBC13D4DF4A8C7",
|
|
124
|
+
"TRON:USDT": "TRON.USDT-TR7NHQJEKQXGTCI8Q8ZY4PL8OTSZGJLJ6T"
|
|
125
|
+
};
|
|
126
|
+
var ROUTER_ABI = [
|
|
127
|
+
{
|
|
128
|
+
type: "function",
|
|
129
|
+
name: "depositWithExpiry",
|
|
130
|
+
stateMutability: "payable",
|
|
131
|
+
inputs: [
|
|
132
|
+
{ name: "vault", type: "address" },
|
|
133
|
+
{ name: "asset", type: "address" },
|
|
134
|
+
{ name: "amount", type: "uint256" },
|
|
135
|
+
{ name: "memo", type: "string" },
|
|
136
|
+
{ name: "expiration", type: "uint256" }
|
|
137
|
+
],
|
|
138
|
+
outputs: []
|
|
139
|
+
}
|
|
140
|
+
];
|
|
141
|
+
function isEvm2(caip2) {
|
|
142
|
+
return caip2.startsWith("eip155:");
|
|
143
|
+
}
|
|
144
|
+
function thorAsset(caip2, symbol) {
|
|
145
|
+
const chain = THOR_CHAIN[caip2];
|
|
146
|
+
if (!chain) return void 0;
|
|
147
|
+
const sym = symbol.toUpperCase();
|
|
148
|
+
const token = TOKEN_ASSET[`${chain}:${sym}`];
|
|
149
|
+
if (token) return token;
|
|
150
|
+
const native = NATIVE_ASSET[chain];
|
|
151
|
+
return native && native.split(".")[1] === sym ? native : void 0;
|
|
152
|
+
}
|
|
153
|
+
function destAsset(destChain, destToken) {
|
|
154
|
+
const a = (0, import_swap2_assets.ccAssetByToken)(destChain, destToken);
|
|
155
|
+
if (!a) return void 0;
|
|
156
|
+
const asset = thorAsset(destChain, a.symbol);
|
|
157
|
+
return asset ? { asset, decimals: a.decimals } : void 0;
|
|
158
|
+
}
|
|
159
|
+
var HostUnavailable = class extends Error {
|
|
160
|
+
constructor(host, reason) {
|
|
161
|
+
super(`thorchain: ${host} unavailable (${reason})`);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
async function hostGet(host, path, clientId, signal) {
|
|
165
|
+
let res;
|
|
166
|
+
try {
|
|
167
|
+
res = await fetch(`${host}${path}`, {
|
|
168
|
+
headers: { accept: "application/json", "x-client-id": clientId },
|
|
169
|
+
cache: "no-store",
|
|
170
|
+
signal: signal ?? (0, import_util2.timeoutSignal)(8e3)
|
|
171
|
+
});
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (signal?.aborted) throw error;
|
|
174
|
+
throw new HostUnavailable(host, error instanceof Error ? error.message : "fetch failed");
|
|
175
|
+
}
|
|
176
|
+
if (res.status >= 500 || res.status === 429 || res.status === 403) {
|
|
177
|
+
throw new HostUnavailable(host, `HTTP ${res.status}`);
|
|
178
|
+
}
|
|
179
|
+
const body = await res.json().catch(() => null);
|
|
180
|
+
return { ok: res.ok, status: res.status, body };
|
|
181
|
+
}
|
|
182
|
+
async function thornodeGet(path, signal, hosts, clientId) {
|
|
183
|
+
for (const host of hosts) {
|
|
184
|
+
try {
|
|
185
|
+
const res = await hostGet(host, path, clientId, signal);
|
|
186
|
+
if (!res.ok) continue;
|
|
187
|
+
return Array.isArray(res.body) ? null : res.body;
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error instanceof HostUnavailable) continue;
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
function belowThorMinimums(amount1e8, inbound, quote) {
|
|
196
|
+
const dust = inbound?.dust_threshold;
|
|
197
|
+
if (dust !== void 0 && (!/^\d+$/.test(String(dust)) || amount1e8 <= BigInt(dust))) return "amount at or below THORChain dust threshold";
|
|
198
|
+
const min = quote?.recommended_min_amount_in;
|
|
199
|
+
if (min !== void 0 && (!/^\d+$/.test(String(min)) || amount1e8 < BigInt(min))) return "amount below THORChain recommended minimum";
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
async function inboundAddressesOn(host, clientId) {
|
|
203
|
+
const res = await hostGet(host, "/thorchain/inbound_addresses", clientId);
|
|
204
|
+
if (!res.ok || !Array.isArray(res.body)) throw new HostUnavailable(host, `inbound_addresses HTTP ${res.status}`);
|
|
205
|
+
return res.body;
|
|
206
|
+
}
|
|
207
|
+
async function inboundAddresses(hosts, clientId) {
|
|
208
|
+
for (const host of hosts) {
|
|
209
|
+
try {
|
|
210
|
+
return await inboundAddressesOn(host, clientId);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (error instanceof HostUnavailable) continue;
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
function paused(entry) {
|
|
219
|
+
return !entry || entry.halted === true || entry.global_trading_paused === true || entry.chain_trading_paused === true;
|
|
220
|
+
}
|
|
221
|
+
function sameTarget(a, b) {
|
|
222
|
+
if (!a || !b) return false;
|
|
223
|
+
return a.startsWith("0x") || b.startsWith("0x") ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
224
|
+
}
|
|
225
|
+
function verifiedMemo(memo, expectedAsset, expectedDestination, expectedRefund, expectedAffiliate, expectedAffiliateBps) {
|
|
226
|
+
if (memo.includes("|")) return null;
|
|
227
|
+
const parts = memo.split(":");
|
|
228
|
+
if (parts.length !== 4 && parts.length !== 6) return null;
|
|
229
|
+
const [op, asset, destination, limit, affiliate, bps] = parts;
|
|
230
|
+
if (op !== "=" || !asset || !/^[A-Za-z0-9._~\-]+$/.test(asset)) return null;
|
|
231
|
+
const expectedTarget = expectedRefund ? `${expectedDestination}/${expectedRefund}` : expectedDestination;
|
|
232
|
+
if (!sameTarget(destination, expectedTarget)) return null;
|
|
233
|
+
if (parts.length === 6) {
|
|
234
|
+
if (!expectedAffiliate || affiliate !== expectedAffiliate || bps !== expectedAffiliateBps) return null;
|
|
235
|
+
} else if (expectedAffiliate || expectedAffiliateBps) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
const stream = (limit ?? "").split("/");
|
|
239
|
+
if (stream.length !== 1 || stream.some((v) => !/^\d+$/.test(v))) return null;
|
|
240
|
+
const floor = BigInt(stream[0] ?? "0");
|
|
241
|
+
if (floor <= 0n) return null;
|
|
242
|
+
const canonical = `=:${expectedAsset}:${expectedTarget}:${stream.join("/")}${expectedAffiliate ? `:${expectedAffiliate}:${expectedAffiliateBps}` : ""}`;
|
|
243
|
+
return { limit: floor, canonical };
|
|
244
|
+
}
|
|
245
|
+
function toThorUnitsCeil(amount, decimals) {
|
|
246
|
+
const scale = 10n ** BigInt(decimals);
|
|
247
|
+
return (amount * 100000000n + scale - 1n) / scale;
|
|
248
|
+
}
|
|
249
|
+
function thorchainProvider(config = {}) {
|
|
250
|
+
config = Object.freeze({ ...config });
|
|
251
|
+
const configuredHosts = (Array.isArray(config.nodeUrl) ? config.nodeUrl : (config.nodeUrl ?? "").split(",")).map((h) => h.trim()).filter(Boolean);
|
|
252
|
+
const hosts = [...new Set((configuredHosts.length ? configuredHosts : HOSTS).map((h) => {
|
|
253
|
+
let url;
|
|
254
|
+
try {
|
|
255
|
+
url = new URL(h);
|
|
256
|
+
} catch {
|
|
257
|
+
throw new Error("thorchain: nodeUrl must be an absolute http(s) URL");
|
|
258
|
+
}
|
|
259
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error("thorchain: nodeUrl must use http or https");
|
|
260
|
+
return url.toString().replace(/\/$/, "");
|
|
261
|
+
}))];
|
|
262
|
+
const configuredAffiliate = config.affiliate ?? "";
|
|
263
|
+
const configuredAffiliateBps = config.affiliateBps ?? "";
|
|
264
|
+
if (!!configuredAffiliate !== !!configuredAffiliateBps) {
|
|
265
|
+
throw new Error("thorchain: affiliate and affiliateBps must be configured together");
|
|
266
|
+
}
|
|
267
|
+
if (configuredAffiliate && !/^[A-Za-z0-9._~-]+$/.test(configuredAffiliate)) {
|
|
268
|
+
throw new Error("thorchain: affiliate contains unsupported memo characters");
|
|
269
|
+
}
|
|
270
|
+
if (configuredAffiliateBps && (!/^\d+$/.test(configuredAffiliateBps) || BigInt(configuredAffiliateBps) > 10000n)) {
|
|
271
|
+
throw new Error("thorchain: affiliateBps must be an integer between 0 and 10000");
|
|
272
|
+
}
|
|
273
|
+
const clientId = config.clientId?.trim() || DEFAULT_CLIENT_ID;
|
|
274
|
+
if (!/^[\x21-\x7e]{1,128}$/.test(clientId)) throw new Error("thorchain: clientId must contain 1-128 visible ASCII characters");
|
|
275
|
+
const tronSource = config.tronSource === true;
|
|
276
|
+
const isDepositSource = (caip2) => tronSource && DEPOSIT_SOURCES.has(caip2);
|
|
277
|
+
return {
|
|
278
|
+
name: "thorchain",
|
|
279
|
+
describeActivity: describeThorchainActivity,
|
|
280
|
+
enabled: () => config.enabled ?? true,
|
|
281
|
+
// Capability discovery comes from the provider itself, so it can run alone
|
|
282
|
+
// or inside the optional aggregator without a central capability table.
|
|
283
|
+
supportsChain: (chain) => thorchain.supportsChain(chain),
|
|
284
|
+
supportsChainPair: (fromChain, toChain) => thorchain.supports(fromChain, toChain),
|
|
285
|
+
// Inbound: EVM/Tron source → the mainnet vault. Outbound (step-2 off-ramp):
|
|
286
|
+
// EVM source (Ethereum) → any THOR-supported chain. The SOURCE must be EVM
|
|
287
|
+
// (router calldata) or, when opted in, Tron (send + memo); both legs must
|
|
288
|
+
// be THOR chains and differ.
|
|
289
|
+
supportsRoute(from, to) {
|
|
290
|
+
const srcExec = isEvm2(from.chain) || isDepositSource(from.chain);
|
|
291
|
+
if (!(srcExec && !!THOR_CHAIN[from.chain] && !!THOR_CHAIN[to.chain] && from.chain !== to.chain)) return false;
|
|
292
|
+
const fromAsset = thorAsset(from.chain, from.token) ?? thorAsset(from.chain, (0, import_swap2_assets.ccAssetByToken)(from.chain, from.token)?.symbol ?? "");
|
|
293
|
+
const toAsset = destAsset(to.chain, to.token) ?? (thorAsset(to.chain, to.token) ? { asset: thorAsset(to.chain, to.token) } : void 0);
|
|
294
|
+
return !!fromAsset && !!toAsset && fromAsset !== toAsset.asset;
|
|
295
|
+
},
|
|
296
|
+
async getQuote(params, signal) {
|
|
297
|
+
if (!(config.enabled ?? true)) return null;
|
|
298
|
+
if (params.flex) return null;
|
|
299
|
+
const chainId = CHAIN_ID[params.sourceChain];
|
|
300
|
+
if (!chainId && !isDepositSource(params.sourceChain)) return null;
|
|
301
|
+
if (params.sourceChain === params.destChain) return null;
|
|
302
|
+
const src = (0, import_swap2_assets.ccAsset)(params.sourceChain, params.sourceToken);
|
|
303
|
+
const fromAsset = thorAsset(params.sourceChain, params.sourceToken);
|
|
304
|
+
const dst = destAsset(params.destChain, params.destToken);
|
|
305
|
+
if (!src || !fromAsset || !dst || fromAsset === dst.asset) {
|
|
306
|
+
params.onRefusal?.(`thorchain: no pool for ${params.sourceToken} on ${params.sourceChain} \u2192 ${params.destToken} on ${params.destChain}`);
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
const amountIn = BigInt(params.amountIn);
|
|
310
|
+
const amount1e8 = amountIn * 100000000n / 10n ** BigInt(src.decimals);
|
|
311
|
+
if (amount1e8 <= 0n) return null;
|
|
312
|
+
if (amount1e8 * 10n ** BigInt(src.decimals) !== amountIn * 100000000n) {
|
|
313
|
+
params.onRefusal?.(`thorchain: amount has more precision than THORChain's 8 decimals \u2014 round it to 8 decimals`);
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
const requestedTolerance = params.slippageBps ?? Number(TOLERANCE_BPS);
|
|
317
|
+
if (!Number.isInteger(requestedTolerance) || requestedTolerance < 0 || requestedTolerance > Number(MAX_TOLERANCE_BPS)) {
|
|
318
|
+
params.onRefusal?.(`thorchain: slippage ${String(params.slippageBps)} bps is outside 0\u2013${MAX_TOLERANCE_BPS} bps`);
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
const explicitSlippage = params.slippageBps !== void 0;
|
|
322
|
+
const liquidityBps = explicitSlippage ? dst.decimals > 8 ? requestedTolerance - 1 : requestedTolerance : 0;
|
|
323
|
+
const limitMode = liquidityBps > 0 ? "liquidity_tolerance_bps" : "tolerance_bps";
|
|
324
|
+
const quoteOn = async (host, bps, limitMode2 = "tolerance_bps") => {
|
|
325
|
+
const qs = new URLSearchParams({
|
|
326
|
+
from_asset: fromAsset,
|
|
327
|
+
to_asset: dst.asset,
|
|
328
|
+
amount: amount1e8.toString(),
|
|
329
|
+
destination: params.destAddress,
|
|
330
|
+
[limitMode2]: bps,
|
|
331
|
+
// Pin a single (non-streaming) swap. Left to itself the node picks
|
|
332
|
+
// streaming for thin pools (max_streaming_quantity > 1) and then
|
|
333
|
+
// simulates the LIM per sub-swap — the adaptive retry could never
|
|
334
|
+
// pass on BASE.ETH / BSC stables (verified 2026-09-16). We reject
|
|
335
|
+
// streaming memos anyway, so the quote must be simulated atomically.
|
|
336
|
+
streaming_quantity: "1"
|
|
337
|
+
});
|
|
338
|
+
if (configuredAffiliate && configuredAffiliateBps) {
|
|
339
|
+
qs.set("affiliate", configuredAffiliate);
|
|
340
|
+
qs.set("affiliate_bps", configuredAffiliateBps);
|
|
341
|
+
}
|
|
342
|
+
if (params.refundAddress) qs.set("refund_address", params.refundAddress);
|
|
343
|
+
const res = await hostGet(host, `/thorchain/quote/swap?${qs.toString()}`, clientId, signal);
|
|
344
|
+
if (Array.isArray(res.body)) return null;
|
|
345
|
+
return res.body ?? (res.ok ? null : { error: `HTTP ${res.status}` });
|
|
346
|
+
};
|
|
347
|
+
let q = null;
|
|
348
|
+
let checkedMemo = null;
|
|
349
|
+
let inbounds = null;
|
|
350
|
+
for (const host of hosts) {
|
|
351
|
+
try {
|
|
352
|
+
if (limitMode === "liquidity_tolerance_bps") {
|
|
353
|
+
q = await quoteOn(host, String(liquidityBps), "liquidity_tolerance_bps");
|
|
354
|
+
const feeBps = Number(q?.fees?.total_bps);
|
|
355
|
+
if (Number.isFinite(feeBps) && feeBps > Number(MAX_TOLERANCE_BPS)) {
|
|
356
|
+
params.onRefusal?.(`thorchain: THORChain fees ${(feeBps / 100).toFixed(2)}% \u2014 too expensive at this amount (cap ${Number(MAX_TOLERANCE_BPS) / 100}%)`);
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
q = await quoteOn(host, String(requestedTolerance));
|
|
361
|
+
const rejected = (q?.error ?? q?.message)?.match(/emit asset (\d+) less than price limit (\d+)/);
|
|
362
|
+
if (rejected && !explicitSlippage) {
|
|
363
|
+
const emit = BigInt(rejected[1] ?? "0");
|
|
364
|
+
const limit = BigInt(rejected[2] ?? "0");
|
|
365
|
+
if (emit > 0n && limit > emit) {
|
|
366
|
+
const spot = limit * 10000n / (10000n - BigInt(requestedTolerance));
|
|
367
|
+
const needed = 10000n - emit * 10000n / spot + 50n;
|
|
368
|
+
if (needed <= MAX_TOLERANCE_BPS) q = await quoteOn(host, needed.toString());
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (q?.error || q?.message) {
|
|
373
|
+
params.onRefusal?.(`thorchain: ${String(q.error ?? q.message).slice(0, 200)}`);
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
if (!q || !q.expected_amount_out || !q.inbound_address || !q.memo || !q.expiry) {
|
|
377
|
+
params.onRefusal?.(`thorchain: incomplete quote from ${host} (expected_amount_out / inbound_address / memo / expiry missing)`);
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
if (chainId ? !q.router : !!q.router) {
|
|
381
|
+
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");
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
checkedMemo = verifiedMemo(q.memo, dst.asset, params.destAddress, params.refundAddress ?? "", configuredAffiliate, configuredAffiliateBps);
|
|
385
|
+
if (!checkedMemo) {
|
|
386
|
+
params.onRefusal?.(`thorchain: memo failed verification (${String(q.memo).slice(0, 80)})`);
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
const floor1e8 = params.minAmountOut !== void 0 ? toThorUnitsCeil(BigInt(params.minAmountOut), dst.decimals) : void 0;
|
|
390
|
+
if (floor1e8 !== void 0 && checkedMemo.limit < floor1e8) {
|
|
391
|
+
params.onRefusal?.(`thorchain: floor breach (limit=${checkedMemo.limit} < floor=${floor1e8}, 1e8 units)`);
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
inbounds = await inboundAddressesOn(host, clientId);
|
|
395
|
+
break;
|
|
396
|
+
} catch (error) {
|
|
397
|
+
if (error instanceof HostUnavailable) {
|
|
398
|
+
q = null;
|
|
399
|
+
checkedMemo = null;
|
|
400
|
+
inbounds = null;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (!q?.expected_amount_out || !q.inbound_address || !q.expiry || !checkedMemo || !inbounds) {
|
|
407
|
+
params.onRefusal?.(!inbounds ? "thorchain: inbound addresses unavailable from every node" : "thorchain: no quote from any node");
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
const limit1e8 = checkedMemo.limit;
|
|
411
|
+
const srcInbound = inbounds.find((entry) => entry.chain === THOR_CHAIN[params.sourceChain]);
|
|
412
|
+
const dstInbound = inbounds.find((entry) => entry.chain === THOR_CHAIN[params.destChain]);
|
|
413
|
+
if (paused(srcInbound) || paused(dstInbound)) {
|
|
414
|
+
params.onRefusal?.(`thorchain: ${paused(srcInbound) ? THOR_CHAIN[params.sourceChain] : THOR_CHAIN[params.destChain]} chain is halted / paused on THORChain`);
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
if (!sameTarget(srcInbound?.address, q.inbound_address)) {
|
|
418
|
+
params.onRefusal?.("thorchain: quote inbound address differs from the node's current inbound (vault rotated) \u2014 retry");
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
if (chainId && !sameTarget(srcInbound?.router, q.router)) {
|
|
422
|
+
params.onRefusal?.("thorchain: quote router differs from the node's current router \u2014 retry");
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
const tooSmall = belowThorMinimums(amount1e8, srcInbound, q);
|
|
426
|
+
if (tooSmall) {
|
|
427
|
+
params.onRefusal?.(`thorchain: ${tooSmall}`);
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
const outUnits = BigInt(q.expected_amount_out) * 10n ** BigInt(dst.decimals) / 100000000n;
|
|
431
|
+
if (outUnits <= 0n) {
|
|
432
|
+
params.onRefusal?.("thorchain: expected output rounds to zero");
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
const memoMinUnits = limit1e8 * 10n ** BigInt(dst.decimals) / 100000000n;
|
|
436
|
+
const minUnits = memoMinUnits;
|
|
437
|
+
if (minUnits <= 0n || minUnits > outUnits) {
|
|
438
|
+
params.onRefusal?.(`thorchain: memo limit ${limit1e8} inconsistent with expected output ${q.expected_amount_out}`);
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
if (params.slippageBps !== void 0) {
|
|
442
|
+
const slippageFloor = outUnits * BigInt(1e4 - params.slippageBps) / 10000n;
|
|
443
|
+
if (minUnits < slippageFloor) {
|
|
444
|
+
params.onRefusal?.(`thorchain: memo limit ${limit1e8} is below expected ${q.expected_amount_out} minus ${params.slippageBps} bps slippage (1e8 units)`);
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
449
|
+
const executionExpiry = Math.min(q.expiry, nowSeconds + 15 * 60);
|
|
450
|
+
if (!Number.isInteger(executionExpiry) || executionExpiry <= nowSeconds + 30) {
|
|
451
|
+
params.onRefusal?.(`thorchain: quote expires in ${Number.isInteger(executionExpiry) ? executionExpiry - nowSeconds : "?"} s \u2014 too soon to execute`);
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
const assetAddr = src.contract ? src.contract : NATIVE;
|
|
455
|
+
const common = {
|
|
456
|
+
provider: "thorchain",
|
|
457
|
+
intentId: `thorchain:${chainId ?? THOR_CHAIN[params.sourceChain]}`,
|
|
458
|
+
sourceAmount: params.amountIn,
|
|
459
|
+
destAmount: outUnits.toString(),
|
|
460
|
+
minDestAmount: minUnits.toString(),
|
|
461
|
+
feeUsd: 0,
|
|
462
|
+
// destAmount is already net of THORChain fees
|
|
463
|
+
etaSeconds: q.total_swap_seconds ?? 120,
|
|
464
|
+
reliability: 0.9,
|
|
465
|
+
// Validity must outlive the ERC-20 approval leg: for USDT that is
|
|
466
|
+
// approve(0) + approve(n), each awaited to inclusion (≈2 blocks), so a
|
|
467
|
+
// 60 s window expired before the deposit and only the revoke went out
|
|
468
|
+
// (2026-09-16). The on-chain LIM bounds price movement and preflight
|
|
469
|
+
// re-checks vault/router/halts right before broadcast, so a longer
|
|
470
|
+
// window costs nothing in safety.
|
|
471
|
+
validUntilMs: Math.min(Date.now() + QUOTE_VALIDITY_MS, executionExpiry * 1e3),
|
|
472
|
+
providerData: {
|
|
473
|
+
sourceChain: params.sourceChain,
|
|
474
|
+
destChain: params.destChain,
|
|
475
|
+
inboundAddress: q.inbound_address,
|
|
476
|
+
router: q.router ?? "",
|
|
477
|
+
assetAddress: assetAddr,
|
|
478
|
+
expiry: String(executionExpiry),
|
|
479
|
+
memo: checkedMemo.canonical
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
if (!chainId) {
|
|
483
|
+
return { ...common, depositAddress: q.inbound_address, depositMemo: checkedMemo.canonical };
|
|
484
|
+
}
|
|
485
|
+
const data = (0, import_viem.encodeFunctionData)({
|
|
486
|
+
abi: ROUTER_ABI,
|
|
487
|
+
functionName: "depositWithExpiry",
|
|
488
|
+
args: [q.inbound_address, assetAddr, amountIn, checkedMemo.canonical, BigInt(executionExpiry)]
|
|
489
|
+
});
|
|
490
|
+
return {
|
|
491
|
+
...common,
|
|
492
|
+
depositAddress: "",
|
|
493
|
+
depositMemo: null,
|
|
494
|
+
txRequest: {
|
|
495
|
+
to: q.router,
|
|
496
|
+
data,
|
|
497
|
+
value: src.contract ? "0" : params.amountIn,
|
|
498
|
+
chainId,
|
|
499
|
+
...src.contract ? { approvalAddress: q.router } : {}
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
},
|
|
503
|
+
async buildDeposit(quote) {
|
|
504
|
+
if (!quote.txRequest) {
|
|
505
|
+
const sourceChain = quote.providerData?.sourceChain ?? "";
|
|
506
|
+
if (!isDepositSource(sourceChain) || !quote.depositAddress || !quote.depositMemo) {
|
|
507
|
+
throw new Error("thorchain: deposit-address execution is only supported for an opted-in Tron source");
|
|
508
|
+
}
|
|
509
|
+
return { chain: sourceChain, to: quote.depositAddress, value: quote.sourceAmount, memo: quote.depositMemo };
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
chain: "",
|
|
513
|
+
to: quote.txRequest?.to ?? "",
|
|
514
|
+
value: quote.txRequest?.value ?? "0",
|
|
515
|
+
...quote.txRequest?.data ? { data: quote.txRequest.data } : {},
|
|
516
|
+
...quote.txRequest?.chainId !== void 0 ? { chainId: quote.txRequest.chainId } : {},
|
|
517
|
+
...quote.txRequest?.gasLimit ? { gasLimit: quote.txRequest.gasLimit } : {},
|
|
518
|
+
...quote.txRequest?.approvalAddress ? { approvalAddress: quote.txRequest.approvalAddress } : {}
|
|
519
|
+
};
|
|
520
|
+
},
|
|
521
|
+
async preflightDeposit(quote, tx) {
|
|
522
|
+
const data = quote.providerData;
|
|
523
|
+
if (!data?.sourceChain || !data.destChain || !data.inboundAddress || !data.assetAddress || !data.expiry || !data.memo) {
|
|
524
|
+
throw new Error("thorchain: quote is missing execution-bound metadata");
|
|
525
|
+
}
|
|
526
|
+
const evmSource = CHAIN_ID[data.sourceChain] !== void 0;
|
|
527
|
+
if (evmSource && !data.router) throw new Error("thorchain: quote is missing execution-bound metadata");
|
|
528
|
+
if (!evmSource && !isDepositSource(data.sourceChain)) throw new Error("thorchain: source chain has no executor");
|
|
529
|
+
if (!/^\d+$/.test(data.expiry) || BigInt(data.expiry) <= BigInt(Math.floor(Date.now() / 1e3))) {
|
|
530
|
+
throw new Error("thorchain: quote expired before broadcast");
|
|
531
|
+
}
|
|
532
|
+
const inbounds = await inboundAddresses(hosts, clientId);
|
|
533
|
+
if (!inbounds) throw new Error("thorchain: cannot refresh inbound addresses before broadcast");
|
|
534
|
+
const src = inbounds.find((entry) => entry.chain === THOR_CHAIN[data.sourceChain]);
|
|
535
|
+
const dst = inbounds.find((entry) => entry.chain === THOR_CHAIN[data.destChain]);
|
|
536
|
+
if (paused(src) || paused(dst)) throw new Error("thorchain: source or destination chain trading is paused");
|
|
537
|
+
if (!sameTarget(src?.address, data.inboundAddress)) throw new Error("thorchain: inbound vault rotated after quote");
|
|
538
|
+
if (!evmSource) {
|
|
539
|
+
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) {
|
|
540
|
+
throw new Error("thorchain: deposit-address execution must be a plain send with memo");
|
|
541
|
+
}
|
|
542
|
+
if (!sameTarget(tx.to, data.inboundAddress) || tx.memo !== data.memo || tx.value !== quote.sourceAmount || tx.chain && tx.chain !== data.sourceChain) {
|
|
543
|
+
throw new Error("thorchain: deposit does not match the validated quote");
|
|
544
|
+
}
|
|
545
|
+
const srcAsset = (0, import_swap2_assets.ccAssetByToken)(data.sourceChain, sameTarget(data.assetAddress, NATIVE) ? "" : data.assetAddress);
|
|
546
|
+
if (!srcAsset) throw new Error("thorchain: unknown source asset");
|
|
547
|
+
const amount1e8 = BigInt(quote.sourceAmount) * 100000000n / 10n ** BigInt(srcAsset.decimals);
|
|
548
|
+
const tooSmall = belowThorMinimums(amount1e8, src, null);
|
|
549
|
+
if (tooSmall) throw new Error(`thorchain: ${tooSmall}`);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if (!sameTarget(src?.router, data.router) || !sameTarget(tx.to, data.router)) {
|
|
553
|
+
throw new Error("thorchain: router rotated after quote");
|
|
554
|
+
}
|
|
555
|
+
if (!tx.data) throw new Error("thorchain: missing depositWithExpiry calldata");
|
|
556
|
+
let decoded;
|
|
557
|
+
try {
|
|
558
|
+
decoded = (0, import_viem.decodeFunctionData)({ abi: ROUTER_ABI, data: tx.data });
|
|
559
|
+
} catch {
|
|
560
|
+
throw new Error("thorchain: malformed depositWithExpiry calldata");
|
|
561
|
+
}
|
|
562
|
+
const args = decoded.args;
|
|
563
|
+
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]) {
|
|
564
|
+
throw new Error("thorchain: calldata does not match the validated quote");
|
|
565
|
+
}
|
|
566
|
+
const tokenSource = !sameTarget(data.assetAddress, NATIVE);
|
|
567
|
+
if (tx.value !== (tokenSource ? "0" : quote.sourceAmount) || (tokenSource ? !sameTarget(tx.approvalAddress, data.router) : tx.approvalAddress !== void 0)) {
|
|
568
|
+
throw new Error("thorchain: value/approval does not match the validated source asset");
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
async trackIntent(ref) {
|
|
572
|
+
if (!ref.sourceTxHash) return { state: "pending" };
|
|
573
|
+
if (!/^(?:0x)?[0-9a-fA-F]{64}$/.test(ref.sourceTxHash)) {
|
|
574
|
+
throw new Error("thorchain: source transaction hash must be 32-byte hex");
|
|
575
|
+
}
|
|
576
|
+
const hash = ref.sourceTxHash.replace(/^0x/i, "").toUpperCase();
|
|
577
|
+
const j = await thornodeGet(`/thorchain/tx/status/${hash}`, void 0, hosts, clientId) ?? await thornodeGet(`/thorchain/tx/${hash}`, void 0, hosts, clientId);
|
|
578
|
+
if (!j) return { state: "pending" };
|
|
579
|
+
const stages = j.stages;
|
|
580
|
+
if (stages) {
|
|
581
|
+
const planned = j.planned_out_txs ?? [];
|
|
582
|
+
const refunded = planned.some((out) => out.refund === true);
|
|
583
|
+
const delivered = planned.some((out) => out.refund === false);
|
|
584
|
+
const outTxs = j.out_txs ?? [];
|
|
585
|
+
const outHash2 = outTxs.find((out) => typeof out.id === "string" && out.id.length > 0 && !/^0+$/.test(out.id))?.id;
|
|
586
|
+
const deliveredHash = outTxs.find(
|
|
587
|
+
(out) => typeof out.id === "string" && out.id.length > 0 && !/^0+$/.test(out.id) && typeof out.memo === "string" && !/^REFUND/i.test(out.memo)
|
|
588
|
+
)?.id;
|
|
589
|
+
if (stages.outbound_signed?.completed) {
|
|
590
|
+
if (!refunded && !delivered || !outHash2) {
|
|
591
|
+
return { state: "pending", depositReceived: true };
|
|
592
|
+
}
|
|
593
|
+
return refunded && delivered ? { state: "partial", depositReceived: true, ...deliveredHash ? { destTxHash: deliveredHash } : {} } : refunded ? { state: "refunded", depositReceived: true } : { state: "completed", depositReceived: true, ...outHash2 ? { destTxHash: outHash2 } : {} };
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
state: "pending",
|
|
597
|
+
depositReceived: !!(stages.inbound_observed?.started || stages.inbound_observed?.completed || stages.inbound_finalised?.completed)
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
const obs = j.observed_tx;
|
|
601
|
+
const outHash = obs?.out_hashes?.find((h) => typeof h === "string" && h.length > 0 && !/^0+$/.test(h));
|
|
602
|
+
if (obs?.status === "done") {
|
|
603
|
+
const planned = j.planned_out_txs ?? j.out_txs ?? [];
|
|
604
|
+
const refunded = planned.some(
|
|
605
|
+
(o) => o?.refund === true || /^REFUND/i.test(o?.memo ?? "")
|
|
606
|
+
);
|
|
607
|
+
const delivered = planned.some(
|
|
608
|
+
(o) => o?.refund === false && !/^REFUND/i.test(o?.memo ?? "")
|
|
609
|
+
);
|
|
610
|
+
if (!refunded && !delivered || !outHash) return { state: "pending", depositReceived: true };
|
|
611
|
+
if (refunded && delivered) return { state: "partial", depositReceived: true };
|
|
612
|
+
if (refunded) return { state: "refunded", depositReceived: true };
|
|
613
|
+
return { state: "completed", ...outHash ? { destTxHash: outHash } : {} };
|
|
614
|
+
}
|
|
615
|
+
return { state: "pending", depositReceived: !!obs };
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
//# sourceMappingURL=provider.cjs.map
|