@anywalletkit/protocol-swap2-thorchain 0.3.5 → 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/README.md +84 -0
- package/dist/{chunk-LYJOENYE.js → chunk-EIAXGWNG.js} +521 -106
- package/dist/chunk-EIAXGWNG.js.map +1 -0
- package/dist/{chunk-QLDZS2KO.js → chunk-YKA6IYKX.js} +2 -2
- package/dist/index.cjs +520 -105
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -2
- package/dist/protocol.cjs +448 -79
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.js +2 -2
- package/dist/provider.cjs +520 -105
- package/dist/provider.cjs.map +1 -1
- package/dist/provider.d.cts +59 -2
- package/dist/provider.d.ts +59 -2
- package/dist/provider.js +3 -1
- package/package.json +6 -6
- package/dist/chunk-LYJOENYE.js.map +0 -1
- /package/dist/{chunk-QLDZS2KO.js.map → chunk-YKA6IYKX.js.map} +0 -0
package/dist/protocol.cjs
CHANGED
|
@@ -67,8 +67,212 @@ 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");
|
|
73
|
+
|
|
74
|
+
// src/confirm.ts
|
|
75
|
+
var import_errors = require("@anywalletkit/protocol-swap2-core/errors");
|
|
76
|
+
var THORNODE_CONFIRM_PATHS = Object.freeze({
|
|
77
|
+
lastblock: "/thorchain/lastblock",
|
|
78
|
+
inboundAddresses: "/thorchain/inbound_addresses",
|
|
79
|
+
asgardVaults: "/thorchain/vaults/asgard",
|
|
80
|
+
mimir: "/thorchain/mimir"
|
|
81
|
+
});
|
|
82
|
+
var DEFAULT_CONFIRM_MAX_LAG_BLOCKS = 50;
|
|
83
|
+
var DEFAULT_CONFIRM_TIMEOUT_MS = 2500;
|
|
84
|
+
var MIN_CONFIRM_TIMEOUT_MS = 100;
|
|
85
|
+
var MAX_CONFIRM_TIMEOUT_MS = 8e3;
|
|
86
|
+
function confirmOptions(o) {
|
|
87
|
+
const maxLagBlocks = o.confirmMaxLagBlocks ?? DEFAULT_CONFIRM_MAX_LAG_BLOCKS;
|
|
88
|
+
if (!Number.isInteger(maxLagBlocks) || maxLagBlocks < 0 || maxLagBlocks > 1e5) {
|
|
89
|
+
throw new Error("thorchain: confirmMaxLagBlocks must be an integer between 0 and 100000");
|
|
90
|
+
}
|
|
91
|
+
const timeoutMs = o.confirmTimeoutMs ?? DEFAULT_CONFIRM_TIMEOUT_MS;
|
|
92
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < MIN_CONFIRM_TIMEOUT_MS || timeoutMs > MAX_CONFIRM_TIMEOUT_MS) {
|
|
93
|
+
throw new Error(`thorchain: confirmTimeoutMs must be an integer between ${MIN_CONFIRM_TIMEOUT_MS} and ${MAX_CONFIRM_TIMEOUT_MS}`);
|
|
94
|
+
}
|
|
95
|
+
return { maxLagBlocks, timeoutMs };
|
|
96
|
+
}
|
|
97
|
+
var HostUnavailable = class extends Error {
|
|
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) {
|
|
103
|
+
super(`thorchain: ${host} unavailable (${reason})`);
|
|
104
|
+
(0, import_errors.withReason)(this, info ?? "upstream_unavailable");
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
function statusRefusal(status, retryAfter) {
|
|
108
|
+
return (0, import_errors.httpRefusal)(status, retryAfter) ?? { reason: "upstream_unavailable", httpStatus: status };
|
|
109
|
+
}
|
|
110
|
+
function paused(entry) {
|
|
111
|
+
return !entry || entry.halted === true || entry.global_trading_paused === true || entry.chain_trading_paused === true;
|
|
112
|
+
}
|
|
113
|
+
function sameTarget(a, b) {
|
|
114
|
+
if (typeof a !== "string" || typeof b !== "string" || !a || !b) return false;
|
|
115
|
+
return a.startsWith("0x") || b.startsWith("0x") ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
116
|
+
}
|
|
117
|
+
function nodeDeadline(ms, outer) {
|
|
118
|
+
const ctrl = new AbortController();
|
|
119
|
+
const timer = setTimeout(() => ctrl.abort(), Math.max(0, ms));
|
|
120
|
+
const onAbort = () => ctrl.abort();
|
|
121
|
+
if (outer) {
|
|
122
|
+
if (outer.aborted) ctrl.abort();
|
|
123
|
+
else outer.addEventListener("abort", onAbort);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
signal: ctrl.signal,
|
|
127
|
+
clear() {
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
outer?.removeEventListener("abort", onAbort);
|
|
130
|
+
if (!ctrl.signal.aborted) ctrl.abort();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function sameRequestUrl(actual, expected) {
|
|
135
|
+
if (actual === void 0 || actual === null || actual === "") return true;
|
|
136
|
+
if (typeof actual !== "string") return false;
|
|
137
|
+
try {
|
|
138
|
+
const a = new URL(actual);
|
|
139
|
+
const e = new URL(expected);
|
|
140
|
+
return a.origin === e.origin && a.pathname === e.pathname && a.search === "" && a.hash === "";
|
|
141
|
+
} catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function nodeRead(host, path, clientId, signal, outer) {
|
|
146
|
+
const url = `${host}${path}`;
|
|
147
|
+
let res;
|
|
148
|
+
try {
|
|
149
|
+
res = await fetch(url, { method: "GET", headers: { accept: "application/json", "x-client-id": clientId }, redirect: "error", signal });
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (outer?.aborted) throw error;
|
|
152
|
+
throw new HostUnavailable(host, error instanceof Error ? error.message : "fetch failed");
|
|
153
|
+
}
|
|
154
|
+
if (res.redirected === true || !sameRequestUrl(res.url, url)) throw new HostUnavailable(host, `${path} redirected`);
|
|
155
|
+
if (!res.ok) throw new HostUnavailable(host, `${path} HTTP ${res.status}`, statusRefusal(res.status, res.headers?.get?.("retry-after")));
|
|
156
|
+
try {
|
|
157
|
+
return await res.json();
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (outer?.aborted) throw error;
|
|
160
|
+
throw new HostUnavailable(host, `${path} is not JSON`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function int64(v) {
|
|
164
|
+
if (typeof v === "number") return Number.isSafeInteger(v) ? v : null;
|
|
165
|
+
if (typeof v === "string" && /^-?\d{1,15}$/.test(v)) return Number(v);
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
169
|
+
var INBOUND_STRINGS = ["address", "router", "dust_threshold"];
|
|
170
|
+
var INBOUND_FLAGS = ["halted", "global_trading_paused", "chain_trading_paused"];
|
|
171
|
+
function inboundAddresses(body) {
|
|
172
|
+
if (!Array.isArray(body)) return null;
|
|
173
|
+
const out = [];
|
|
174
|
+
for (const e of body) {
|
|
175
|
+
if (!isObject(e) || typeof e.chain !== "string") return null;
|
|
176
|
+
const entry = { chain: e.chain };
|
|
177
|
+
for (const k of INBOUND_STRINGS) {
|
|
178
|
+
const v = e[k];
|
|
179
|
+
if (v === void 0) continue;
|
|
180
|
+
if (typeof v !== "string") return null;
|
|
181
|
+
entry[k] = v;
|
|
182
|
+
}
|
|
183
|
+
for (const k of INBOUND_FLAGS) {
|
|
184
|
+
const v = e[k];
|
|
185
|
+
if (v === void 0) continue;
|
|
186
|
+
if (typeof v !== "boolean") return null;
|
|
187
|
+
entry[k] = v;
|
|
188
|
+
}
|
|
189
|
+
out.push(entry);
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
function lastblockHeight(body) {
|
|
194
|
+
if (!Array.isArray(body) || body.length === 0) return null;
|
|
195
|
+
let height = -1;
|
|
196
|
+
for (const entry of body) {
|
|
197
|
+
if (!isObject(entry)) return null;
|
|
198
|
+
const h = int64(entry.thorchain);
|
|
199
|
+
if (h === null || h < 0) return null;
|
|
200
|
+
if (h > height) height = h;
|
|
201
|
+
}
|
|
202
|
+
return height;
|
|
203
|
+
}
|
|
204
|
+
function asgardVaults(body) {
|
|
205
|
+
if (!Array.isArray(body)) return null;
|
|
206
|
+
const out = [];
|
|
207
|
+
for (const v of body) {
|
|
208
|
+
if (!isObject(v)) return null;
|
|
209
|
+
if (typeof v.status !== "string" || typeof v.pub_key !== "string" || !/^thorpub1[0-9a-z]{20,}$/.test(v.pub_key)) return null;
|
|
210
|
+
if (v.type !== void 0 && v.type !== "AsgardVault") return null;
|
|
211
|
+
if (!Array.isArray(v.addresses)) return null;
|
|
212
|
+
const addresses = [];
|
|
213
|
+
for (const a of v.addresses) {
|
|
214
|
+
if (!isObject(a) || typeof a.chain !== "string" || typeof a.address !== "string") return null;
|
|
215
|
+
addresses.push({ chain: a.chain, address: a.address });
|
|
216
|
+
}
|
|
217
|
+
out.push({ pubKey: v.pub_key, status: v.status, addresses });
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
function mimirFlags(body) {
|
|
222
|
+
if (!isObject(body) || Object.keys(body).length === 0) return null;
|
|
223
|
+
const keys = Object.keys(body).map((k) => [k.toUpperCase(), body[k]]);
|
|
224
|
+
if (keys.some(([, value]) => typeof value !== "number" && typeof value !== "string")) return null;
|
|
225
|
+
return (pubKey) => {
|
|
226
|
+
const pub = pubKey.toUpperCase();
|
|
227
|
+
const compromisedKey = `COMPROMISEDVAULT-${pub}`;
|
|
228
|
+
const insolventPrefix = `SOLVENCYHALTVAULT-${pub}-`;
|
|
229
|
+
let flag = null;
|
|
230
|
+
for (const [key, value] of keys) {
|
|
231
|
+
const kind = key === compromisedKey ? "compromised" : key.startsWith(insolventPrefix) ? "insolvent" : null;
|
|
232
|
+
if (!kind) continue;
|
|
233
|
+
const n = int64(value);
|
|
234
|
+
if (n === null) return "malformed";
|
|
235
|
+
if (n > 0 && flag !== "compromised") flag = kind;
|
|
236
|
+
}
|
|
237
|
+
return flag;
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
async function nodeHead(host, read) {
|
|
241
|
+
const [lastblock, inbound] = await Promise.all([read(THORNODE_CONFIRM_PATHS.lastblock), read(THORNODE_CONFIRM_PATHS.inboundAddresses)]);
|
|
242
|
+
const height = lastblockHeight(lastblock);
|
|
243
|
+
if (height === null) throw new HostUnavailable(host, "lastblock is not a THORNode lastblock list");
|
|
244
|
+
return { height, inbound };
|
|
245
|
+
}
|
|
246
|
+
async function vaultVerdict(host, read, inbound, claim) {
|
|
247
|
+
if (!Array.isArray(inbound)) throw new HostUnavailable(host, "inbound_addresses is not a list");
|
|
248
|
+
const entries = inboundAddresses(inbound);
|
|
249
|
+
if (!entries) throw new HostUnavailable(host, "inbound_addresses is not a THORNode inbound address list");
|
|
250
|
+
const entry = entries.find((e) => e.chain === claim.chain);
|
|
251
|
+
if (paused(entry)) return { ok: false, why: "halted", chain: claim.chain };
|
|
252
|
+
if (claim.destChain !== void 0 && paused(entries.find((e) => e.chain === claim.destChain))) {
|
|
253
|
+
return { ok: false, why: "halted", chain: claim.destChain };
|
|
254
|
+
}
|
|
255
|
+
if (!sameTarget(entry?.address, claim.vault)) {
|
|
256
|
+
const [asgardBody, mimirBody] = await Promise.all([read(THORNODE_CONFIRM_PATHS.asgardVaults), read(THORNODE_CONFIRM_PATHS.mimir)]);
|
|
257
|
+
const vaults = asgardVaults(asgardBody);
|
|
258
|
+
if (!vaults) throw new HostUnavailable(host, "vaults/asgard is not a THORNode vault list");
|
|
259
|
+
const flags = mimirFlags(mimirBody);
|
|
260
|
+
if (!flags) throw new HostUnavailable(host, "mimir is not a THORNode mimir map");
|
|
261
|
+
const holders = vaults.filter((v) => v.addresses.some((a) => a.chain === claim.chain && sameTarget(a.address, claim.vault)));
|
|
262
|
+
const active = holders.find((v) => v.status === "ActiveVault");
|
|
263
|
+
if (!active) {
|
|
264
|
+
if (!holders.length) return { ok: false, why: "another-vault" };
|
|
265
|
+
return { ok: false, why: "not-active", status: /^[A-Za-z]{1,32}$/.test(holders[0].status) ? holders[0].status : "an unknown status" };
|
|
266
|
+
}
|
|
267
|
+
const flag = flags(active.pubKey);
|
|
268
|
+
if (flag === "malformed") throw new HostUnavailable(host, "mimir holds a non-integer vault flag");
|
|
269
|
+
if (flag) return { ok: false, why: "flagged", flag };
|
|
270
|
+
}
|
|
271
|
+
if (claim.router !== void 0 && !sameTarget(entry?.router, claim.router)) return { ok: false, why: "another-router" };
|
|
272
|
+
return { ok: true };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/provider.ts
|
|
72
276
|
var HOSTS = [
|
|
73
277
|
"https://gateway.liquify.com/chain/thorchain_api",
|
|
74
278
|
"https://thorchain.ibs.team/api"
|
|
@@ -84,6 +288,9 @@ var NATIVE = "0x0000000000000000000000000000000000000000";
|
|
|
84
288
|
var TOLERANCE_BPS = "300";
|
|
85
289
|
var MAX_TOLERANCE_BPS = 500n;
|
|
86
290
|
var QUOTE_VALIDITY_MS = 5 * 6e4;
|
|
291
|
+
var NODE_READ_TIMEOUT_MS = 8e3;
|
|
292
|
+
var CONFIRM_BUDGET_MS = 8e3;
|
|
293
|
+
var LAGGING = /* @__PURE__ */ Symbol("lagging");
|
|
87
294
|
var THOR_CHAIN = {
|
|
88
295
|
"eip155:1": "ETH",
|
|
89
296
|
"eip155:8453": "BASE",
|
|
@@ -164,25 +371,20 @@ function destAsset(destChain, destToken) {
|
|
|
164
371
|
const asset = thorAsset(destChain, a.symbol);
|
|
165
372
|
return asset ? { asset, decimals: a.decimals } : void 0;
|
|
166
373
|
}
|
|
167
|
-
var HostUnavailable = class extends Error {
|
|
168
|
-
constructor(host, reason) {
|
|
169
|
-
super(`thorchain: ${host} unavailable (${reason})`);
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
374
|
async function hostGet(host, path, clientId, signal) {
|
|
173
375
|
let res;
|
|
174
376
|
try {
|
|
175
377
|
res = await fetch(`${host}${path}`, {
|
|
176
378
|
headers: { accept: "application/json", "x-client-id": clientId },
|
|
177
379
|
cache: "no-store",
|
|
178
|
-
signal: signal ?? (0, import_util2.timeoutSignal)(
|
|
380
|
+
signal: signal ?? (0, import_util2.timeoutSignal)(NODE_READ_TIMEOUT_MS)
|
|
179
381
|
});
|
|
180
382
|
} catch (error) {
|
|
181
383
|
if (signal?.aborted) throw error;
|
|
182
384
|
throw new HostUnavailable(host, error instanceof Error ? error.message : "fetch failed");
|
|
183
385
|
}
|
|
184
386
|
if (res.status >= 500 || res.status === 429 || res.status === 403) {
|
|
185
|
-
throw new HostUnavailable(host, `HTTP ${res.status}
|
|
387
|
+
throw new HostUnavailable(host, `HTTP ${res.status}`, statusRefusal(res.status, res.headers?.get?.("retry-after")));
|
|
186
388
|
}
|
|
187
389
|
const body = await res.json().catch(() => null);
|
|
188
390
|
return { ok: res.ok, status: res.status, body };
|
|
@@ -207,35 +409,53 @@ function belowThorMinimums(amount1e8, inbound, quote) {
|
|
|
207
409
|
if (min !== void 0 && (!/^\d+$/.test(String(min)) || amount1e8 < BigInt(min))) return "amount below THORChain recommended minimum";
|
|
208
410
|
return null;
|
|
209
411
|
}
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
if (!
|
|
213
|
-
|
|
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;
|
|
214
417
|
}
|
|
215
|
-
function
|
|
216
|
-
|
|
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" };
|
|
217
430
|
}
|
|
218
|
-
function
|
|
219
|
-
|
|
220
|
-
|
|
431
|
+
async function inboundAddressesOn(host, clientId) {
|
|
432
|
+
const res = await hostGet(host, "/thorchain/inbound_addresses", clientId);
|
|
433
|
+
if (!res.ok || !Array.isArray(res.body)) throw new HostUnavailable(host, `inbound_addresses HTTP ${res.status}`, res.ok ? void 0 : statusRefusal(res.status));
|
|
434
|
+
const entries = inboundAddresses(res.body);
|
|
435
|
+
if (!entries) throw new HostUnavailable(host, "inbound_addresses is not a THORNode inbound address list");
|
|
436
|
+
return entries;
|
|
221
437
|
}
|
|
222
|
-
function verifiedMemo(memo, expectedAsset, expectedDestination, expectedRefund, expectedAffiliate, expectedAffiliateBps) {
|
|
223
|
-
|
|
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");
|
|
224
444
|
const parts = memo.split(":");
|
|
225
|
-
if (parts.length !== 4 && parts.length !== 6) return
|
|
445
|
+
if (parts.length !== 4 && parts.length !== 6) return refused("policy_refused");
|
|
226
446
|
const [op, asset, destination, limit, affiliate, bps] = parts;
|
|
227
|
-
if (op !== "=" || !asset || !/^[A-Za-z0-9._~\-]+$/.test(asset)) return
|
|
447
|
+
if (op !== "=" || !asset || !/^[A-Za-z0-9._~\-]+$/.test(asset)) return refused("policy_refused");
|
|
228
448
|
const expectedTarget = expectedRefund ? `${expectedDestination}/${expectedRefund}` : expectedDestination;
|
|
229
|
-
if (!sameTarget(destination, expectedTarget)) return
|
|
449
|
+
if (!sameTarget(destination, expectedTarget)) return refused("recipient_mismatch");
|
|
230
450
|
if (parts.length === 6) {
|
|
231
|
-
if (!expectedAffiliate || affiliate !== expectedAffiliate || bps !== expectedAffiliateBps) return
|
|
451
|
+
if (!expectedAffiliate || affiliate !== expectedAffiliate || bps !== expectedAffiliateBps) return refused("policy_refused");
|
|
232
452
|
} else if (expectedAffiliate || expectedAffiliateBps) {
|
|
233
|
-
return
|
|
453
|
+
return refused("policy_refused");
|
|
234
454
|
}
|
|
235
455
|
const stream = (limit ?? "").split("/");
|
|
236
|
-
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");
|
|
237
457
|
const floor = BigInt(stream[0] ?? "0");
|
|
238
|
-
if (floor <= 0n) return
|
|
458
|
+
if (floor <= 0n) return refused("price_check_failed");
|
|
239
459
|
const canonical = `=:${expectedAsset}:${expectedTarget}:${stream.join("/")}${expectedAffiliate ? `:${expectedAffiliate}:${expectedAffiliateBps}` : ""}`;
|
|
240
460
|
return { limit: floor, canonical };
|
|
241
461
|
}
|
|
@@ -279,23 +499,109 @@ function thorchainProvider(config = {}) {
|
|
|
279
499
|
routers.set(chain, set);
|
|
280
500
|
}
|
|
281
501
|
const pinnedRouter = (chain, router) => !!router && routers.get(chain)?.has(router.toLowerCase()) === true;
|
|
282
|
-
const
|
|
502
|
+
const { maxLagBlocks: confirmMaxLagBlocks, timeoutMs: confirmTimeoutMs } = confirmOptions(config);
|
|
503
|
+
const nodeSnapshot = (host, outer) => new Promise((resolve, reject) => {
|
|
504
|
+
let inbounds;
|
|
505
|
+
let height;
|
|
506
|
+
let heightError;
|
|
507
|
+
let done = false;
|
|
508
|
+
const tip = nodeDeadline(NODE_READ_TIMEOUT_MS, outer);
|
|
509
|
+
const settle = () => {
|
|
510
|
+
if (done || inbounds === void 0 || height === void 0 && heightError === void 0) return;
|
|
511
|
+
done = true;
|
|
512
|
+
resolve(heightError ? { inbounds, heightError } : { inbounds, height });
|
|
513
|
+
};
|
|
514
|
+
const fail = (error) => {
|
|
515
|
+
if (done) return;
|
|
516
|
+
done = true;
|
|
517
|
+
tip.clear();
|
|
518
|
+
reject(error);
|
|
519
|
+
};
|
|
520
|
+
inboundAddressesOn(host, clientId).then((v) => {
|
|
521
|
+
inbounds = v;
|
|
522
|
+
settle();
|
|
523
|
+
}, fail);
|
|
524
|
+
nodeRead(host, THORNODE_CONFIRM_PATHS.lastblock, clientId, tip.signal, outer).then(
|
|
525
|
+
(body) => {
|
|
526
|
+
tip.clear();
|
|
527
|
+
const h = lastblockHeight(body);
|
|
528
|
+
if (h === null) heightError = new HostUnavailable(host, "lastblock is not a THORNode lastblock list");
|
|
529
|
+
else height = h;
|
|
530
|
+
settle();
|
|
531
|
+
},
|
|
532
|
+
(error) => {
|
|
533
|
+
tip.clear();
|
|
534
|
+
if (!(error instanceof HostUnavailable)) return fail(error);
|
|
535
|
+
heightError = error;
|
|
536
|
+
settle();
|
|
537
|
+
}
|
|
538
|
+
);
|
|
539
|
+
});
|
|
540
|
+
const confirmText = (v) => {
|
|
541
|
+
switch (v.why) {
|
|
542
|
+
case "halted":
|
|
543
|
+
return { text: `${v.chain} is halted / paused on the confirming node`, info: { reason: "chain_halted" } };
|
|
544
|
+
case "another-vault":
|
|
545
|
+
return { text: "a second node reports another inbound vault \u2014 refused until they agree", info: { reason: "vault_mismatch" } };
|
|
546
|
+
case "not-active":
|
|
547
|
+
return { text: `a second node lists the inbound vault as ${v.status}, not ActiveVault \u2014 refused`, info: { reason: "vault_mismatch" } };
|
|
548
|
+
case "flagged":
|
|
549
|
+
return { text: `a second node's mimir flags the inbound vault as ${v.flag} \u2014 refused`, info: { reason: "vault_mismatch" } };
|
|
550
|
+
case "another-router":
|
|
551
|
+
return { text: "a second node reports another router \u2014 refused until they agree", info: { reason: "vault_mismatch" } };
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
const confirmOn = async (host, signal, outer, quoteHeight, claim) => {
|
|
555
|
+
const read = (path) => nodeRead(host, path, clientId, signal, outer);
|
|
556
|
+
const head = await nodeHead(host, read);
|
|
557
|
+
if (quoteHeight - head.height > confirmMaxLagBlocks) return LAGGING;
|
|
558
|
+
const verdict = await vaultVerdict(host, read, head.inbound, claim);
|
|
559
|
+
return verdict.ok ? null : confirmText(verdict);
|
|
560
|
+
};
|
|
561
|
+
const confirmedElsewhere = async (quoteHost, quoteHeight, chain, destChain, vault, router, deadlineAt, outer) => {
|
|
562
|
+
const claim = {
|
|
563
|
+
chain: THOR_CHAIN[chain] ?? chain,
|
|
564
|
+
...destChain !== void 0 ? { destChain: THOR_CHAIN[destChain] ?? destChain } : {},
|
|
565
|
+
vault,
|
|
566
|
+
...router !== void 0 ? { router } : {}
|
|
567
|
+
};
|
|
568
|
+
let lagging = false;
|
|
569
|
+
let asked = 0;
|
|
570
|
+
const failures = [];
|
|
283
571
|
for (const host of confirmHosts) {
|
|
284
572
|
if (host === quoteHost) continue;
|
|
285
|
-
|
|
573
|
+
const budget = Math.min(confirmTimeoutMs, deadlineAt - Date.now());
|
|
574
|
+
if (budget < MIN_CONFIRM_TIMEOUT_MS) {
|
|
575
|
+
failures.push({ reason: "upstream_unavailable" });
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
asked++;
|
|
579
|
+
const node = nodeDeadline(budget, outer);
|
|
286
580
|
try {
|
|
287
|
-
|
|
581
|
+
const verdict = await confirmOn(host, node.signal, outer, quoteHeight, claim);
|
|
582
|
+
if (verdict === LAGGING) {
|
|
583
|
+
lagging = true;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
return verdict;
|
|
288
587
|
} catch (error) {
|
|
289
|
-
if (
|
|
588
|
+
if (outer?.aborted) throw error;
|
|
589
|
+
if (error instanceof HostUnavailable) {
|
|
590
|
+
failures.push((0, import_errors2.refusalOf)(error));
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
290
593
|
throw error;
|
|
594
|
+
} finally {
|
|
595
|
+
node.clear();
|
|
291
596
|
}
|
|
292
|
-
const entry = entries.find((e) => e.chain === THOR_CHAIN[chain]);
|
|
293
|
-
if (paused(entry)) return `${THOR_CHAIN[chain]} is halted / paused on the confirming node`;
|
|
294
|
-
if (!sameTarget(entry?.address, vault)) return "a second node reports another inbound vault \u2014 refused until they agree";
|
|
295
|
-
if (router !== void 0 && !sameTarget(entry?.router, router)) return "a second node reports another router \u2014 refused until they agree";
|
|
296
|
-
return null;
|
|
297
597
|
}
|
|
298
|
-
|
|
598
|
+
if (lagging) {
|
|
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" } };
|
|
600
|
+
}
|
|
601
|
+
if (asked === 0) {
|
|
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" } };
|
|
603
|
+
}
|
|
604
|
+
return { text: "no second node answered to confirm the inbound vault (configure confirmNodeUrl)", info: nodesFailed(failures) };
|
|
299
605
|
};
|
|
300
606
|
const configuredAffiliate = config.affiliate ?? "";
|
|
301
607
|
const configuredAffiliateBps = config.affiliateBps ?? "";
|
|
@@ -341,24 +647,27 @@ function thorchainProvider(config = {}) {
|
|
|
341
647
|
const fromAsset = thorAsset(params.sourceChain, params.sourceToken);
|
|
342
648
|
const dst = destAsset(params.destChain, params.destToken);
|
|
343
649
|
if (!src || !fromAsset || !dst || fromAsset === dst.asset) {
|
|
344
|
-
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" });
|
|
345
651
|
return null;
|
|
346
652
|
}
|
|
347
653
|
const amountIn = BigInt(params.amountIn);
|
|
348
654
|
const amount1e8 = amountIn * 100000000n / 10n ** BigInt(src.decimals);
|
|
349
655
|
if (amount1e8 <= 0n) return null;
|
|
350
656
|
if (amount1e8 * 10n ** BigInt(src.decimals) !== amountIn * 100000000n) {
|
|
351
|
-
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" });
|
|
352
658
|
return null;
|
|
353
659
|
}
|
|
354
660
|
const requestedTolerance = params.slippageBps ?? Number(TOLERANCE_BPS);
|
|
355
661
|
if (!Number.isInteger(requestedTolerance) || requestedTolerance < 0 || requestedTolerance > (0, import_core.bigintToNumber)(MAX_TOLERANCE_BPS)) {
|
|
356
|
-
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" });
|
|
357
663
|
return null;
|
|
358
664
|
}
|
|
359
665
|
const explicitSlippage = params.slippageBps !== void 0;
|
|
360
666
|
const liquidityBps = explicitSlippage ? dst.decimals > 8 ? requestedTolerance - 1 : requestedTolerance : 0;
|
|
361
667
|
const limitMode = liquidityBps > 0 ? "liquidity_tolerance_bps" : "tolerance_bps";
|
|
668
|
+
let quoteStatus = 0;
|
|
669
|
+
let quoteBodyless = false;
|
|
670
|
+
let quoteBodyAborted = false;
|
|
362
671
|
const quoteOn = async (host, bps, limitMode2 = "tolerance_bps") => {
|
|
363
672
|
const qs = new URLSearchParams({
|
|
364
673
|
from_asset: fromAsset,
|
|
@@ -379,6 +688,9 @@ function thorchainProvider(config = {}) {
|
|
|
379
688
|
}
|
|
380
689
|
if (params.refundAddress) qs.set("refund_address", params.refundAddress);
|
|
381
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;
|
|
382
694
|
if (Array.isArray(res.body)) return null;
|
|
383
695
|
return res.body ?? (res.ok ? null : { error: `HTTP ${res.status}` });
|
|
384
696
|
};
|
|
@@ -386,13 +698,17 @@ function thorchainProvider(config = {}) {
|
|
|
386
698
|
let checkedMemo = null;
|
|
387
699
|
let inbounds = null;
|
|
388
700
|
let quoteHost = "";
|
|
701
|
+
let quoteHeight = 0;
|
|
702
|
+
let confirmDeadlineAt = 0;
|
|
703
|
+
let heightUnavailable = false;
|
|
704
|
+
const hostFailures = [];
|
|
389
705
|
for (const host of hosts) {
|
|
390
706
|
try {
|
|
391
707
|
if (limitMode === "liquidity_tolerance_bps") {
|
|
392
708
|
q = await quoteOn(host, String(liquidityBps), "liquidity_tolerance_bps");
|
|
393
709
|
const feeBps = Number(q?.fees?.total_bps);
|
|
394
710
|
if (Number.isFinite(feeBps) && feeBps > (0, import_core.bigintToNumber)(MAX_TOLERANCE_BPS)) {
|
|
395
|
-
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" });
|
|
396
712
|
return null;
|
|
397
713
|
}
|
|
398
714
|
} else {
|
|
@@ -409,32 +725,44 @@ function thorchainProvider(config = {}) {
|
|
|
409
725
|
}
|
|
410
726
|
}
|
|
411
727
|
if (q?.error || q?.message) {
|
|
412
|
-
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));
|
|
413
729
|
return null;
|
|
414
730
|
}
|
|
415
731
|
if (!q || !q.expected_amount_out || !q.inbound_address || !q.memo || !q.expiry) {
|
|
416
|
-
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 } : {} });
|
|
417
733
|
return null;
|
|
418
734
|
}
|
|
419
735
|
if (chainId ? !q.router : !!q.router) {
|
|
420
|
-
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" });
|
|
421
737
|
return null;
|
|
422
738
|
}
|
|
423
|
-
|
|
739
|
+
let memoRefusal = "recipient_mismatch";
|
|
740
|
+
checkedMemo = verifiedMemo(q.memo, dst.asset, params.destAddress, params.refundAddress ?? "", configuredAffiliate, configuredAffiliateBps, (why) => {
|
|
741
|
+
memoRefusal = why;
|
|
742
|
+
});
|
|
424
743
|
if (!checkedMemo) {
|
|
425
|
-
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 });
|
|
426
745
|
return null;
|
|
427
746
|
}
|
|
428
747
|
const floor1e8 = params.minAmountOut !== void 0 ? toThorUnitsCeil(BigInt(params.minAmountOut), dst.decimals) : void 0;
|
|
429
748
|
if (floor1e8 !== void 0 && checkedMemo.limit < floor1e8) {
|
|
430
|
-
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" });
|
|
431
750
|
return null;
|
|
432
751
|
}
|
|
433
|
-
|
|
752
|
+
const deadlineAt = Date.now() + CONFIRM_BUDGET_MS;
|
|
753
|
+
const snapshot = await nodeSnapshot(host, signal);
|
|
754
|
+
if ("heightError" in snapshot) {
|
|
755
|
+
heightUnavailable = true;
|
|
756
|
+
throw snapshot.heightError;
|
|
757
|
+
}
|
|
758
|
+
inbounds = snapshot.inbounds;
|
|
759
|
+
quoteHeight = snapshot.height;
|
|
434
760
|
quoteHost = host;
|
|
761
|
+
confirmDeadlineAt = deadlineAt;
|
|
435
762
|
break;
|
|
436
763
|
} catch (error) {
|
|
437
764
|
if (error instanceof HostUnavailable) {
|
|
765
|
+
hostFailures.push((0, import_errors2.refusalOf)(error));
|
|
438
766
|
q = null;
|
|
439
767
|
checkedMemo = null;
|
|
440
768
|
inbounds = null;
|
|
@@ -444,60 +772,72 @@ function thorchainProvider(config = {}) {
|
|
|
444
772
|
}
|
|
445
773
|
}
|
|
446
774
|
if (!q?.expected_amount_out || !q.inbound_address || !q.expiry || !checkedMemo || !inbounds) {
|
|
447
|
-
params.onRefusal?.(
|
|
775
|
+
params.onRefusal?.(
|
|
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)
|
|
778
|
+
);
|
|
448
779
|
return null;
|
|
449
780
|
}
|
|
450
781
|
const limit1e8 = checkedMemo.limit;
|
|
451
782
|
const srcInbound = inbounds.find((entry) => entry.chain === THOR_CHAIN[params.sourceChain]);
|
|
452
783
|
const dstInbound = inbounds.find((entry) => entry.chain === THOR_CHAIN[params.destChain]);
|
|
453
784
|
if (paused(srcInbound) || paused(dstInbound)) {
|
|
454
|
-
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" });
|
|
455
786
|
return null;
|
|
456
787
|
}
|
|
457
788
|
if (!sameTarget(srcInbound?.address, q.inbound_address)) {
|
|
458
|
-
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" });
|
|
459
790
|
return null;
|
|
460
791
|
}
|
|
461
792
|
if (chainId && !sameTarget(srcInbound?.router, q.router)) {
|
|
462
|
-
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" });
|
|
463
794
|
return null;
|
|
464
795
|
}
|
|
465
796
|
if (chainId && !pinnedRouter(params.sourceChain, q.router)) {
|
|
466
|
-
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" });
|
|
467
798
|
return null;
|
|
468
799
|
}
|
|
469
|
-
const unconfirmed = await confirmedElsewhere(
|
|
800
|
+
const unconfirmed = await confirmedElsewhere(
|
|
801
|
+
quoteHost,
|
|
802
|
+
quoteHeight,
|
|
803
|
+
params.sourceChain,
|
|
804
|
+
params.destChain,
|
|
805
|
+
q.inbound_address,
|
|
806
|
+
chainId ? q.router : void 0,
|
|
807
|
+
confirmDeadlineAt,
|
|
808
|
+
signal
|
|
809
|
+
);
|
|
470
810
|
if (unconfirmed) {
|
|
471
|
-
params.onRefusal?.(`thorchain: ${unconfirmed}
|
|
811
|
+
params.onRefusal?.(`thorchain: ${unconfirmed.text}`, unconfirmed.info);
|
|
472
812
|
return null;
|
|
473
813
|
}
|
|
474
814
|
const tooSmall = belowThorMinimums(amount1e8, srcInbound, q);
|
|
475
815
|
if (tooSmall) {
|
|
476
|
-
params.onRefusal?.(`thorchain: ${tooSmall}
|
|
816
|
+
params.onRefusal?.(`thorchain: ${tooSmall}`, { reason: "amount_too_small" });
|
|
477
817
|
return null;
|
|
478
818
|
}
|
|
479
819
|
const outUnits = BigInt(q.expected_amount_out) * 10n ** BigInt(dst.decimals) / 100000000n;
|
|
480
820
|
if (outUnits <= 0n) {
|
|
481
|
-
params.onRefusal?.("thorchain: expected output rounds to zero");
|
|
821
|
+
params.onRefusal?.("thorchain: expected output rounds to zero", { reason: "amount_too_small" });
|
|
482
822
|
return null;
|
|
483
823
|
}
|
|
484
824
|
const memoMinUnits = limit1e8 * 10n ** BigInt(dst.decimals) / 100000000n;
|
|
485
825
|
const minUnits = memoMinUnits;
|
|
486
826
|
if (minUnits <= 0n || minUnits > outUnits) {
|
|
487
|
-
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" });
|
|
488
828
|
return null;
|
|
489
829
|
}
|
|
490
830
|
if (params.slippageBps !== void 0) {
|
|
491
831
|
const slippageFloor = outUnits * BigInt(1e4 - params.slippageBps) / 10000n;
|
|
492
832
|
if (minUnits < slippageFloor) {
|
|
493
|
-
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" });
|
|
494
834
|
return null;
|
|
495
835
|
}
|
|
496
836
|
}
|
|
497
837
|
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
498
838
|
const executionExpiry = Math.min(q.expiry, nowSeconds + 15 * 60);
|
|
499
839
|
if (!Number.isInteger(executionExpiry) || executionExpiry <= nowSeconds + 30) {
|
|
500
|
-
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" });
|
|
501
841
|
return null;
|
|
502
842
|
}
|
|
503
843
|
const assetAddr = src.contract ? src.contract : NATIVE;
|
|
@@ -553,7 +893,7 @@ function thorchainProvider(config = {}) {
|
|
|
553
893
|
if (!quote.txRequest) {
|
|
554
894
|
const sourceChain = quote.providerData?.sourceChain ?? "";
|
|
555
895
|
if (!isDepositSource(sourceChain) || !quote.depositAddress || !quote.depositMemo) {
|
|
556
|
-
throw
|
|
896
|
+
throw (0, import_errors2.refusal)("unsupported_pair", "thorchain: deposit-address execution is only supported for an opted-in Tron source");
|
|
557
897
|
}
|
|
558
898
|
return { chain: sourceChain, to: quote.depositAddress, value: quote.sourceAmount, memo: quote.depositMemo };
|
|
559
899
|
}
|
|
@@ -570,67 +910,96 @@ function thorchainProvider(config = {}) {
|
|
|
570
910
|
async preflightDeposit(quote, tx) {
|
|
571
911
|
const data = quote.providerData;
|
|
572
912
|
if (!data?.sourceChain || !data.destChain || !data.inboundAddress || !data.assetAddress || !data.expiry || !data.memo) {
|
|
573
|
-
throw
|
|
913
|
+
throw (0, import_errors2.refusal)("policy_refused", "thorchain: quote is missing execution-bound metadata");
|
|
574
914
|
}
|
|
575
915
|
const evmSource = CHAIN_ID[data.sourceChain] !== void 0;
|
|
576
|
-
if (evmSource && !data.router) throw
|
|
577
|
-
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");
|
|
578
918
|
if (!/^\d+$/.test(data.expiry) || BigInt(data.expiry) <= BigInt(Math.floor(Date.now() / 1e3))) {
|
|
579
|
-
throw
|
|
919
|
+
throw (0, import_errors2.refusal)("quote_expired", "thorchain: quote expired before broadcast");
|
|
580
920
|
}
|
|
581
|
-
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");
|
|
582
922
|
let inbounds = null;
|
|
583
923
|
let snapshotHost = "";
|
|
924
|
+
let snapshotHeight = 0;
|
|
925
|
+
let confirmDeadlineAt = 0;
|
|
926
|
+
let heightUnavailable = false;
|
|
927
|
+
const hostFailures = [];
|
|
584
928
|
for (const host of hosts) {
|
|
929
|
+
const deadlineAt = Date.now() + CONFIRM_BUDGET_MS;
|
|
585
930
|
try {
|
|
586
|
-
|
|
931
|
+
const snapshot = await nodeSnapshot(host);
|
|
932
|
+
if ("heightError" in snapshot) {
|
|
933
|
+
heightUnavailable = true;
|
|
934
|
+
hostFailures.push((0, import_errors2.refusalOf)(snapshot.heightError));
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
inbounds = snapshot.inbounds;
|
|
938
|
+
snapshotHeight = snapshot.height;
|
|
587
939
|
snapshotHost = host;
|
|
940
|
+
confirmDeadlineAt = deadlineAt;
|
|
588
941
|
break;
|
|
589
942
|
} catch (error) {
|
|
943
|
+
inbounds = null;
|
|
590
944
|
if (!(error instanceof HostUnavailable)) throw error;
|
|
945
|
+
hostFailures.push((0, import_errors2.refusalOf)(error));
|
|
591
946
|
}
|
|
592
947
|
}
|
|
593
|
-
if (!inbounds)
|
|
948
|
+
if (!inbounds) {
|
|
949
|
+
throw (0, import_errors2.withReason)(new Error(
|
|
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"
|
|
951
|
+
), nodesFailed(hostFailures));
|
|
952
|
+
}
|
|
594
953
|
const confirm = async () => {
|
|
595
|
-
const unconfirmed = await confirmedElsewhere(
|
|
596
|
-
|
|
954
|
+
const unconfirmed = await confirmedElsewhere(
|
|
955
|
+
snapshotHost,
|
|
956
|
+
snapshotHeight,
|
|
957
|
+
data.sourceChain,
|
|
958
|
+
data.destChain,
|
|
959
|
+
data.inboundAddress,
|
|
960
|
+
evmSource ? data.router : void 0,
|
|
961
|
+
confirmDeadlineAt
|
|
962
|
+
);
|
|
963
|
+
if (unconfirmed) throw (0, import_errors2.withReason)(new Error(`thorchain: ${unconfirmed.text}`), unconfirmed.info);
|
|
597
964
|
};
|
|
598
965
|
const src = inbounds.find((entry) => entry.chain === THOR_CHAIN[data.sourceChain]);
|
|
599
966
|
const dst = inbounds.find((entry) => entry.chain === THOR_CHAIN[data.destChain]);
|
|
600
|
-
if (paused(src) || paused(dst)) throw
|
|
601
|
-
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");
|
|
602
969
|
if (!evmSource) {
|
|
603
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) {
|
|
604
|
-
throw
|
|
971
|
+
throw (0, import_errors2.refusal)("policy_refused", "thorchain: deposit-address execution must be a plain send with memo");
|
|
605
972
|
}
|
|
606
973
|
if (!sameTarget(tx.to, data.inboundAddress) || tx.memo !== data.memo || tx.value !== quote.sourceAmount || tx.chain && tx.chain !== data.sourceChain) {
|
|
607
|
-
throw
|
|
974
|
+
throw (0, import_errors2.refusal)(!sameTarget(tx.to, data.inboundAddress) ? "vault_mismatch" : "policy_refused", "thorchain: deposit does not match the validated quote");
|
|
608
975
|
}
|
|
609
976
|
const srcAsset = (0, import_swap2_assets.ccAssetByToken)(data.sourceChain, sameTarget(data.assetAddress, NATIVE) ? "" : data.assetAddress);
|
|
610
|
-
if (!srcAsset) throw
|
|
977
|
+
if (!srcAsset) throw (0, import_errors2.refusal)("unsupported_pair", "thorchain: unknown source asset");
|
|
611
978
|
const amount1e8 = BigInt(quote.sourceAmount) * 100000000n / 10n ** BigInt(srcAsset.decimals);
|
|
612
979
|
const tooSmall = belowThorMinimums(amount1e8, src, null);
|
|
613
|
-
if (tooSmall) throw
|
|
980
|
+
if (tooSmall) throw (0, import_errors2.refusal)("amount_too_small", `thorchain: ${tooSmall}`);
|
|
614
981
|
await confirm();
|
|
615
982
|
return;
|
|
616
983
|
}
|
|
617
984
|
if (!sameTarget(src?.router, data.router) || !sameTarget(tx.to, data.router)) {
|
|
618
|
-
throw
|
|
985
|
+
throw (0, import_errors2.refusal)("vault_mismatch", "thorchain: router rotated after quote");
|
|
619
986
|
}
|
|
620
|
-
if (!tx.data) throw
|
|
987
|
+
if (!tx.data) throw (0, import_errors2.refusal)("policy_refused", "thorchain: missing depositWithExpiry calldata");
|
|
621
988
|
let decoded;
|
|
622
989
|
try {
|
|
623
990
|
decoded = (0, import_viem.decodeFunctionData)({ abi: ROUTER_ABI, data: tx.data });
|
|
624
991
|
} catch {
|
|
625
|
-
throw
|
|
992
|
+
throw (0, import_errors2.refusal)("policy_refused", "thorchain: malformed depositWithExpiry calldata");
|
|
626
993
|
}
|
|
627
994
|
const args = decoded.args;
|
|
628
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]) {
|
|
629
|
-
|
|
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");
|
|
630
998
|
}
|
|
631
999
|
const tokenSource = !sameTarget(data.assetAddress, NATIVE);
|
|
632
1000
|
if (tx.value !== (tokenSource ? "0" : quote.sourceAmount) || (tokenSource ? !sameTarget(tx.approvalAddress, data.router) : tx.approvalAddress !== void 0)) {
|
|
633
|
-
|
|
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");
|
|
634
1003
|
}
|
|
635
1004
|
await confirm();
|
|
636
1005
|
},
|