@anywalletkit/protocol-swap2-thorchain 0.3.5 → 0.4.0
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-AYP7O2NT.js} +387 -41
- package/dist/chunk-AYP7O2NT.js.map +1 -0
- package/dist/{chunk-QLDZS2KO.js → chunk-LP5BU2JS.js} +2 -2
- package/dist/index.cjs +386 -40
- 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 +340 -28
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.js +2 -2
- package/dist/provider.cjs +386 -40
- package/dist/provider.cjs.map +1 -1
- package/dist/provider.d.cts +58 -2
- package/dist/provider.d.ts +58 -2
- package/dist/provider.js +3 -1
- package/package.json +3 -3
- package/dist/chunk-LYJOENYE.js.map +0 -1
- /package/dist/{chunk-QLDZS2KO.js.map → chunk-LP5BU2JS.js.map} +0 -0
package/dist/provider.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var provider_exports = {};
|
|
22
22
|
__export(provider_exports, {
|
|
23
23
|
THORCHAIN_ROUTERS: () => THORCHAIN_ROUTERS,
|
|
24
|
+
THORNODE_CONFIRM_PATHS: () => THORNODE_CONFIRM_PATHS,
|
|
24
25
|
thorchainPins: () => thorchainPins,
|
|
25
26
|
thorchainProvider: () => thorchainProvider
|
|
26
27
|
});
|
|
@@ -70,6 +71,200 @@ var import_viem = require("viem");
|
|
|
70
71
|
var import_util2 = require("@anywalletkit/protocol-swap2-core/util");
|
|
71
72
|
var import_swap2_assets = require("@anywalletkit/swap2-assets");
|
|
72
73
|
var import_core = require("@anywalletkit/core");
|
|
74
|
+
|
|
75
|
+
// src/confirm.ts
|
|
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
|
+
constructor(host, reason) {
|
|
99
|
+
super(`thorchain: ${host} unavailable (${reason})`);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
function paused(entry) {
|
|
103
|
+
return !entry || entry.halted === true || entry.global_trading_paused === true || entry.chain_trading_paused === true;
|
|
104
|
+
}
|
|
105
|
+
function sameTarget(a, b) {
|
|
106
|
+
if (typeof a !== "string" || typeof b !== "string" || !a || !b) return false;
|
|
107
|
+
return a.startsWith("0x") || b.startsWith("0x") ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
108
|
+
}
|
|
109
|
+
function nodeDeadline(ms, outer) {
|
|
110
|
+
const ctrl = new AbortController();
|
|
111
|
+
const timer = setTimeout(() => ctrl.abort(), Math.max(0, ms));
|
|
112
|
+
const onAbort = () => ctrl.abort();
|
|
113
|
+
if (outer) {
|
|
114
|
+
if (outer.aborted) ctrl.abort();
|
|
115
|
+
else outer.addEventListener("abort", onAbort);
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
signal: ctrl.signal,
|
|
119
|
+
clear() {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
outer?.removeEventListener("abort", onAbort);
|
|
122
|
+
if (!ctrl.signal.aborted) ctrl.abort();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function sameRequestUrl(actual, expected) {
|
|
127
|
+
if (actual === void 0 || actual === null || actual === "") return true;
|
|
128
|
+
if (typeof actual !== "string") return false;
|
|
129
|
+
try {
|
|
130
|
+
const a = new URL(actual);
|
|
131
|
+
const e = new URL(expected);
|
|
132
|
+
return a.origin === e.origin && a.pathname === e.pathname && a.search === "" && a.hash === "";
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function nodeRead(host, path, clientId, signal, outer) {
|
|
138
|
+
const url = `${host}${path}`;
|
|
139
|
+
let res;
|
|
140
|
+
try {
|
|
141
|
+
res = await fetch(url, { method: "GET", headers: { accept: "application/json", "x-client-id": clientId }, redirect: "error", signal });
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (outer?.aborted) throw error;
|
|
144
|
+
throw new HostUnavailable(host, error instanceof Error ? error.message : "fetch failed");
|
|
145
|
+
}
|
|
146
|
+
if (res.redirected === true || !sameRequestUrl(res.url, url)) throw new HostUnavailable(host, `${path} redirected`);
|
|
147
|
+
if (!res.ok) throw new HostUnavailable(host, `${path} HTTP ${res.status}`);
|
|
148
|
+
try {
|
|
149
|
+
return await res.json();
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (outer?.aborted) throw error;
|
|
152
|
+
throw new HostUnavailable(host, `${path} is not JSON`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function int64(v) {
|
|
156
|
+
if (typeof v === "number") return Number.isSafeInteger(v) ? v : null;
|
|
157
|
+
if (typeof v === "string" && /^-?\d{1,15}$/.test(v)) return Number(v);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
161
|
+
var INBOUND_STRINGS = ["address", "router", "dust_threshold"];
|
|
162
|
+
var INBOUND_FLAGS = ["halted", "global_trading_paused", "chain_trading_paused"];
|
|
163
|
+
function inboundAddresses(body) {
|
|
164
|
+
if (!Array.isArray(body)) return null;
|
|
165
|
+
const out = [];
|
|
166
|
+
for (const e of body) {
|
|
167
|
+
if (!isObject(e) || typeof e.chain !== "string") return null;
|
|
168
|
+
const entry = { chain: e.chain };
|
|
169
|
+
for (const k of INBOUND_STRINGS) {
|
|
170
|
+
const v = e[k];
|
|
171
|
+
if (v === void 0) continue;
|
|
172
|
+
if (typeof v !== "string") return null;
|
|
173
|
+
entry[k] = v;
|
|
174
|
+
}
|
|
175
|
+
for (const k of INBOUND_FLAGS) {
|
|
176
|
+
const v = e[k];
|
|
177
|
+
if (v === void 0) continue;
|
|
178
|
+
if (typeof v !== "boolean") return null;
|
|
179
|
+
entry[k] = v;
|
|
180
|
+
}
|
|
181
|
+
out.push(entry);
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
function lastblockHeight(body) {
|
|
186
|
+
if (!Array.isArray(body) || body.length === 0) return null;
|
|
187
|
+
let height = -1;
|
|
188
|
+
for (const entry of body) {
|
|
189
|
+
if (!isObject(entry)) return null;
|
|
190
|
+
const h = int64(entry.thorchain);
|
|
191
|
+
if (h === null || h < 0) return null;
|
|
192
|
+
if (h > height) height = h;
|
|
193
|
+
}
|
|
194
|
+
return height;
|
|
195
|
+
}
|
|
196
|
+
function asgardVaults(body) {
|
|
197
|
+
if (!Array.isArray(body)) return null;
|
|
198
|
+
const out = [];
|
|
199
|
+
for (const v of body) {
|
|
200
|
+
if (!isObject(v)) return null;
|
|
201
|
+
if (typeof v.status !== "string" || typeof v.pub_key !== "string" || !/^thorpub1[0-9a-z]{20,}$/.test(v.pub_key)) return null;
|
|
202
|
+
if (v.type !== void 0 && v.type !== "AsgardVault") return null;
|
|
203
|
+
if (!Array.isArray(v.addresses)) return null;
|
|
204
|
+
const addresses = [];
|
|
205
|
+
for (const a of v.addresses) {
|
|
206
|
+
if (!isObject(a) || typeof a.chain !== "string" || typeof a.address !== "string") return null;
|
|
207
|
+
addresses.push({ chain: a.chain, address: a.address });
|
|
208
|
+
}
|
|
209
|
+
out.push({ pubKey: v.pub_key, status: v.status, addresses });
|
|
210
|
+
}
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
function mimirFlags(body) {
|
|
214
|
+
if (!isObject(body) || Object.keys(body).length === 0) return null;
|
|
215
|
+
const keys = Object.keys(body).map((k) => [k.toUpperCase(), body[k]]);
|
|
216
|
+
if (keys.some(([, value]) => typeof value !== "number" && typeof value !== "string")) return null;
|
|
217
|
+
return (pubKey) => {
|
|
218
|
+
const pub = pubKey.toUpperCase();
|
|
219
|
+
const compromisedKey = `COMPROMISEDVAULT-${pub}`;
|
|
220
|
+
const insolventPrefix = `SOLVENCYHALTVAULT-${pub}-`;
|
|
221
|
+
let flag = null;
|
|
222
|
+
for (const [key, value] of keys) {
|
|
223
|
+
const kind = key === compromisedKey ? "compromised" : key.startsWith(insolventPrefix) ? "insolvent" : null;
|
|
224
|
+
if (!kind) continue;
|
|
225
|
+
const n = int64(value);
|
|
226
|
+
if (n === null) return "malformed";
|
|
227
|
+
if (n > 0 && flag !== "compromised") flag = kind;
|
|
228
|
+
}
|
|
229
|
+
return flag;
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
async function nodeHead(host, read) {
|
|
233
|
+
const [lastblock, inbound] = await Promise.all([read(THORNODE_CONFIRM_PATHS.lastblock), read(THORNODE_CONFIRM_PATHS.inboundAddresses)]);
|
|
234
|
+
const height = lastblockHeight(lastblock);
|
|
235
|
+
if (height === null) throw new HostUnavailable(host, "lastblock is not a THORNode lastblock list");
|
|
236
|
+
return { height, inbound };
|
|
237
|
+
}
|
|
238
|
+
async function vaultVerdict(host, read, inbound, claim) {
|
|
239
|
+
if (!Array.isArray(inbound)) throw new HostUnavailable(host, "inbound_addresses is not a list");
|
|
240
|
+
const entries = inboundAddresses(inbound);
|
|
241
|
+
if (!entries) throw new HostUnavailable(host, "inbound_addresses is not a THORNode inbound address list");
|
|
242
|
+
const entry = entries.find((e) => e.chain === claim.chain);
|
|
243
|
+
if (paused(entry)) return { ok: false, why: "halted", chain: claim.chain };
|
|
244
|
+
if (claim.destChain !== void 0 && paused(entries.find((e) => e.chain === claim.destChain))) {
|
|
245
|
+
return { ok: false, why: "halted", chain: claim.destChain };
|
|
246
|
+
}
|
|
247
|
+
if (!sameTarget(entry?.address, claim.vault)) {
|
|
248
|
+
const [asgardBody, mimirBody] = await Promise.all([read(THORNODE_CONFIRM_PATHS.asgardVaults), read(THORNODE_CONFIRM_PATHS.mimir)]);
|
|
249
|
+
const vaults = asgardVaults(asgardBody);
|
|
250
|
+
if (!vaults) throw new HostUnavailable(host, "vaults/asgard is not a THORNode vault list");
|
|
251
|
+
const flags = mimirFlags(mimirBody);
|
|
252
|
+
if (!flags) throw new HostUnavailable(host, "mimir is not a THORNode mimir map");
|
|
253
|
+
const holders = vaults.filter((v) => v.addresses.some((a) => a.chain === claim.chain && sameTarget(a.address, claim.vault)));
|
|
254
|
+
const active = holders.find((v) => v.status === "ActiveVault");
|
|
255
|
+
if (!active) {
|
|
256
|
+
if (!holders.length) return { ok: false, why: "another-vault" };
|
|
257
|
+
return { ok: false, why: "not-active", status: /^[A-Za-z]{1,32}$/.test(holders[0].status) ? holders[0].status : "an unknown status" };
|
|
258
|
+
}
|
|
259
|
+
const flag = flags(active.pubKey);
|
|
260
|
+
if (flag === "malformed") throw new HostUnavailable(host, "mimir holds a non-integer vault flag");
|
|
261
|
+
if (flag) return { ok: false, why: "flagged", flag };
|
|
262
|
+
}
|
|
263
|
+
if (claim.router !== void 0 && !sameTarget(entry?.router, claim.router)) return { ok: false, why: "another-router" };
|
|
264
|
+
return { ok: true };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/provider.ts
|
|
73
268
|
var HOSTS = [
|
|
74
269
|
"https://gateway.liquify.com/chain/thorchain_api",
|
|
75
270
|
"https://thorchain.ibs.team/api"
|
|
@@ -85,6 +280,9 @@ var NATIVE = "0x0000000000000000000000000000000000000000";
|
|
|
85
280
|
var TOLERANCE_BPS = "300";
|
|
86
281
|
var MAX_TOLERANCE_BPS = 500n;
|
|
87
282
|
var QUOTE_VALIDITY_MS = 5 * 6e4;
|
|
283
|
+
var NODE_READ_TIMEOUT_MS = 8e3;
|
|
284
|
+
var CONFIRM_BUDGET_MS = 8e3;
|
|
285
|
+
var LAGGING = /* @__PURE__ */ Symbol("lagging");
|
|
88
286
|
var THOR_CHAIN = {
|
|
89
287
|
"eip155:1": "ETH",
|
|
90
288
|
"eip155:8453": "BASE",
|
|
@@ -165,18 +363,13 @@ function destAsset(destChain, destToken) {
|
|
|
165
363
|
const asset = thorAsset(destChain, a.symbol);
|
|
166
364
|
return asset ? { asset, decimals: a.decimals } : void 0;
|
|
167
365
|
}
|
|
168
|
-
var HostUnavailable = class extends Error {
|
|
169
|
-
constructor(host, reason) {
|
|
170
|
-
super(`thorchain: ${host} unavailable (${reason})`);
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
366
|
async function hostGet(host, path, clientId, signal) {
|
|
174
367
|
let res;
|
|
175
368
|
try {
|
|
176
369
|
res = await fetch(`${host}${path}`, {
|
|
177
370
|
headers: { accept: "application/json", "x-client-id": clientId },
|
|
178
371
|
cache: "no-store",
|
|
179
|
-
signal: signal ?? (0, import_util2.timeoutSignal)(
|
|
372
|
+
signal: signal ?? (0, import_util2.timeoutSignal)(NODE_READ_TIMEOUT_MS)
|
|
180
373
|
});
|
|
181
374
|
} catch (error) {
|
|
182
375
|
if (signal?.aborted) throw error;
|
|
@@ -211,14 +404,9 @@ function belowThorMinimums(amount1e8, inbound, quote) {
|
|
|
211
404
|
async function inboundAddressesOn(host, clientId) {
|
|
212
405
|
const res = await hostGet(host, "/thorchain/inbound_addresses", clientId);
|
|
213
406
|
if (!res.ok || !Array.isArray(res.body)) throw new HostUnavailable(host, `inbound_addresses HTTP ${res.status}`);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return !entry || entry.halted === true || entry.global_trading_paused === true || entry.chain_trading_paused === true;
|
|
218
|
-
}
|
|
219
|
-
function sameTarget(a, b) {
|
|
220
|
-
if (!a || !b) return false;
|
|
221
|
-
return a.startsWith("0x") || b.startsWith("0x") ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
407
|
+
const entries = inboundAddresses(res.body);
|
|
408
|
+
if (!entries) throw new HostUnavailable(host, "inbound_addresses is not a THORNode inbound address list");
|
|
409
|
+
return entries;
|
|
222
410
|
}
|
|
223
411
|
function verifiedMemo(memo, expectedAsset, expectedDestination, expectedRefund, expectedAffiliate, expectedAffiliateBps) {
|
|
224
412
|
if (memo.includes("|")) return null;
|
|
@@ -248,6 +436,25 @@ function thorchainPins(opts = {}) {
|
|
|
248
436
|
const nodes = [...new Set((Array.isArray(opts.nodeUrl) ? opts.nodeUrl : opts.nodeUrl ? opts.nodeUrl.split(",") : HOSTS).map((h) => h.trim().replace(/\/$/, "")).filter(Boolean))];
|
|
249
437
|
for (const n of nodes) if (!/^https:\/\//i.test(n)) throw new Error("thorchain: thorchainPins nodeUrl must use https");
|
|
250
438
|
const clientId = opts.clientId?.trim() || DEFAULT_CLIENT_ID;
|
|
439
|
+
const { maxLagBlocks, timeoutMs } = confirmOptions(opts);
|
|
440
|
+
const askNode = async (host, claim) => {
|
|
441
|
+
const node = nodeDeadline(timeoutMs);
|
|
442
|
+
const read = (path) => nodeRead(host, path, clientId, node.signal);
|
|
443
|
+
try {
|
|
444
|
+
const head = await nodeHead(host, read);
|
|
445
|
+
try {
|
|
446
|
+
return { height: head.height, verdict: await vaultVerdict(host, read, head.inbound, claim) };
|
|
447
|
+
} catch (error) {
|
|
448
|
+
if (error instanceof HostUnavailable) return { height: head.height, verdict: null };
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
} catch (error) {
|
|
452
|
+
if (error instanceof HostUnavailable) return null;
|
|
453
|
+
throw error;
|
|
454
|
+
} finally {
|
|
455
|
+
node.clear();
|
|
456
|
+
}
|
|
457
|
+
};
|
|
251
458
|
const routersFor = (chain) => [...THORCHAIN_ROUTERS[chain] ?? [], ...opts.trustedRouters?.[chain] ?? []];
|
|
252
459
|
return Object.freeze({
|
|
253
460
|
callTargets: routersFor,
|
|
@@ -288,22 +495,36 @@ function thorchainPins(opts = {}) {
|
|
|
288
495
|
if (!checked) return `the memo does not pay ${params.destAddress} in ${dst.asset} with this app's refund and affiliate`;
|
|
289
496
|
if (memo !== checked.canonical) return `the memo is not the canonical ${checked.canonical}`;
|
|
290
497
|
if (checked.limit * 10n ** BigInt(dst.decimals) / 100000000n < BigInt(quote.minDestAmount)) return `the memo's limit ${checked.limit} is below the quoted minimum`;
|
|
498
|
+
const sourceChain = THOR_CHAIN[params.sourceChain] ?? params.sourceChain;
|
|
499
|
+
const claim = { chain: sourceChain, destChain: THOR_CHAIN[params.destChain] ?? params.destChain, vault, ...router !== void 0 ? { router } : {} };
|
|
500
|
+
const views = await Promise.all(nodes.map((host) => askNode(host, claim)));
|
|
501
|
+
const top = views.reduce((max, v) => v && v.height > max ? v.height : max, -1);
|
|
291
502
|
let agreeing = 0;
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
503
|
+
let lagging = false;
|
|
504
|
+
for (const view of views) {
|
|
505
|
+
if (!view?.verdict) continue;
|
|
506
|
+
if (top - view.height > maxLagBlocks) {
|
|
507
|
+
lagging = true;
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const v = view.verdict;
|
|
511
|
+
if (!v.ok) {
|
|
512
|
+
switch (v.why) {
|
|
513
|
+
case "halted":
|
|
514
|
+
return `${v.chain} is halted / paused`;
|
|
515
|
+
case "another-vault":
|
|
516
|
+
return `a THORNode reports another inbound vault than ${vault}`;
|
|
517
|
+
case "not-active":
|
|
518
|
+
return `a THORNode lists the inbound vault as ${v.status}, not ActiveVault`;
|
|
519
|
+
case "flagged":
|
|
520
|
+
return `a THORNode's mimir flags the inbound vault as ${v.flag}`;
|
|
521
|
+
case "another-router":
|
|
522
|
+
return "a THORNode reports another router";
|
|
523
|
+
}
|
|
299
524
|
}
|
|
300
|
-
const entry = entries.find((e) => e.chain === THOR_CHAIN[params.sourceChain]);
|
|
301
|
-
if (paused(entry)) return `${THOR_CHAIN[params.sourceChain]} is halted / paused`;
|
|
302
|
-
if (!sameTarget(entry?.address, vault)) return `a THORNode reports another inbound vault than ${vault}`;
|
|
303
|
-
if (router !== void 0 && !sameTarget(entry?.router, router)) return "a THORNode reports another router";
|
|
304
525
|
if (++agreeing >= 2) return null;
|
|
305
526
|
}
|
|
306
|
-
return "two THORNodes could not confirm the inbound vault";
|
|
527
|
+
return lagging ? `two THORNodes within ${maxLagBlocks} blocks of the highest THORChain height they report could not confirm the inbound vault (behind or unreachable)` : "two THORNodes could not confirm the inbound vault";
|
|
307
528
|
}
|
|
308
529
|
});
|
|
309
530
|
}
|
|
@@ -343,21 +564,100 @@ function thorchainProvider(config = {}) {
|
|
|
343
564
|
routers.set(chain, set);
|
|
344
565
|
}
|
|
345
566
|
const pinnedRouter = (chain, router) => !!router && routers.get(chain)?.has(router.toLowerCase()) === true;
|
|
346
|
-
const
|
|
567
|
+
const { maxLagBlocks: confirmMaxLagBlocks, timeoutMs: confirmTimeoutMs } = confirmOptions(config);
|
|
568
|
+
const nodeSnapshot = (host, outer) => new Promise((resolve, reject) => {
|
|
569
|
+
let inbounds;
|
|
570
|
+
let height;
|
|
571
|
+
let heightError;
|
|
572
|
+
let done = false;
|
|
573
|
+
const tip = nodeDeadline(NODE_READ_TIMEOUT_MS, outer);
|
|
574
|
+
const settle = () => {
|
|
575
|
+
if (done || inbounds === void 0 || height === void 0 && heightError === void 0) return;
|
|
576
|
+
done = true;
|
|
577
|
+
resolve(heightError ? { inbounds, heightError } : { inbounds, height });
|
|
578
|
+
};
|
|
579
|
+
const fail = (error) => {
|
|
580
|
+
if (done) return;
|
|
581
|
+
done = true;
|
|
582
|
+
tip.clear();
|
|
583
|
+
reject(error);
|
|
584
|
+
};
|
|
585
|
+
inboundAddressesOn(host, clientId).then((v) => {
|
|
586
|
+
inbounds = v;
|
|
587
|
+
settle();
|
|
588
|
+
}, fail);
|
|
589
|
+
nodeRead(host, THORNODE_CONFIRM_PATHS.lastblock, clientId, tip.signal, outer).then(
|
|
590
|
+
(body) => {
|
|
591
|
+
tip.clear();
|
|
592
|
+
const h = lastblockHeight(body);
|
|
593
|
+
if (h === null) heightError = new HostUnavailable(host, "lastblock is not a THORNode lastblock list");
|
|
594
|
+
else height = h;
|
|
595
|
+
settle();
|
|
596
|
+
},
|
|
597
|
+
(error) => {
|
|
598
|
+
tip.clear();
|
|
599
|
+
if (!(error instanceof HostUnavailable)) return fail(error);
|
|
600
|
+
heightError = error;
|
|
601
|
+
settle();
|
|
602
|
+
}
|
|
603
|
+
);
|
|
604
|
+
});
|
|
605
|
+
const confirmText = (v) => {
|
|
606
|
+
switch (v.why) {
|
|
607
|
+
case "halted":
|
|
608
|
+
return `${v.chain} is halted / paused on the confirming node`;
|
|
609
|
+
case "another-vault":
|
|
610
|
+
return "a second node reports another inbound vault \u2014 refused until they agree";
|
|
611
|
+
case "not-active":
|
|
612
|
+
return `a second node lists the inbound vault as ${v.status}, not ActiveVault \u2014 refused`;
|
|
613
|
+
case "flagged":
|
|
614
|
+
return `a second node's mimir flags the inbound vault as ${v.flag} \u2014 refused`;
|
|
615
|
+
case "another-router":
|
|
616
|
+
return "a second node reports another router \u2014 refused until they agree";
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
const confirmOn = async (host, signal, outer, quoteHeight, claim) => {
|
|
620
|
+
const read = (path) => nodeRead(host, path, clientId, signal, outer);
|
|
621
|
+
const head = await nodeHead(host, read);
|
|
622
|
+
if (quoteHeight - head.height > confirmMaxLagBlocks) return LAGGING;
|
|
623
|
+
const verdict = await vaultVerdict(host, read, head.inbound, claim);
|
|
624
|
+
return verdict.ok ? null : confirmText(verdict);
|
|
625
|
+
};
|
|
626
|
+
const confirmedElsewhere = async (quoteHost, quoteHeight, chain, destChain, vault, router, deadlineAt, outer) => {
|
|
627
|
+
const claim = {
|
|
628
|
+
chain: THOR_CHAIN[chain] ?? chain,
|
|
629
|
+
...destChain !== void 0 ? { destChain: THOR_CHAIN[destChain] ?? destChain } : {},
|
|
630
|
+
vault,
|
|
631
|
+
...router !== void 0 ? { router } : {}
|
|
632
|
+
};
|
|
633
|
+
let lagging = false;
|
|
634
|
+
let asked = 0;
|
|
347
635
|
for (const host of confirmHosts) {
|
|
348
636
|
if (host === quoteHost) continue;
|
|
349
|
-
|
|
637
|
+
const budget = Math.min(confirmTimeoutMs, deadlineAt - Date.now());
|
|
638
|
+
if (budget < MIN_CONFIRM_TIMEOUT_MS) break;
|
|
639
|
+
asked++;
|
|
640
|
+
const node = nodeDeadline(budget, outer);
|
|
350
641
|
try {
|
|
351
|
-
|
|
642
|
+
const verdict = await confirmOn(host, node.signal, outer, quoteHeight, claim);
|
|
643
|
+
if (verdict === LAGGING) {
|
|
644
|
+
lagging = true;
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
return verdict;
|
|
352
648
|
} catch (error) {
|
|
649
|
+
if (outer?.aborted) throw error;
|
|
353
650
|
if (error instanceof HostUnavailable) continue;
|
|
354
651
|
throw error;
|
|
652
|
+
} finally {
|
|
653
|
+
node.clear();
|
|
355
654
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
655
|
+
}
|
|
656
|
+
if (lagging) {
|
|
657
|
+
return `no confirming node is within ${confirmMaxLagBlocks} blocks of the quoting node's THORChain height (behind or unreachable) \u2014 refused until one catches up`;
|
|
658
|
+
}
|
|
659
|
+
if (asked === 0) {
|
|
660
|
+
return `the ${CONFIRM_BUDGET_MS / 1e3} s confirmation budget ran out before a second node could be asked to confirm the inbound vault \u2014 retry`;
|
|
361
661
|
}
|
|
362
662
|
return "no second node answered to confirm the inbound vault (configure confirmNodeUrl)";
|
|
363
663
|
};
|
|
@@ -450,6 +750,9 @@ function thorchainProvider(config = {}) {
|
|
|
450
750
|
let checkedMemo = null;
|
|
451
751
|
let inbounds = null;
|
|
452
752
|
let quoteHost = "";
|
|
753
|
+
let quoteHeight = 0;
|
|
754
|
+
let confirmDeadlineAt = 0;
|
|
755
|
+
let heightUnavailable = false;
|
|
453
756
|
for (const host of hosts) {
|
|
454
757
|
try {
|
|
455
758
|
if (limitMode === "liquidity_tolerance_bps") {
|
|
@@ -494,8 +797,16 @@ function thorchainProvider(config = {}) {
|
|
|
494
797
|
params.onRefusal?.(`thorchain: floor breach (limit=${checkedMemo.limit} < floor=${floor1e8}, 1e8 units)`);
|
|
495
798
|
return null;
|
|
496
799
|
}
|
|
497
|
-
|
|
800
|
+
const deadlineAt = Date.now() + CONFIRM_BUDGET_MS;
|
|
801
|
+
const snapshot = await nodeSnapshot(host, signal);
|
|
802
|
+
if ("heightError" in snapshot) {
|
|
803
|
+
heightUnavailable = true;
|
|
804
|
+
throw snapshot.heightError;
|
|
805
|
+
}
|
|
806
|
+
inbounds = snapshot.inbounds;
|
|
807
|
+
quoteHeight = snapshot.height;
|
|
498
808
|
quoteHost = host;
|
|
809
|
+
confirmDeadlineAt = deadlineAt;
|
|
499
810
|
break;
|
|
500
811
|
} catch (error) {
|
|
501
812
|
if (error instanceof HostUnavailable) {
|
|
@@ -508,7 +819,9 @@ function thorchainProvider(config = {}) {
|
|
|
508
819
|
}
|
|
509
820
|
}
|
|
510
821
|
if (!q?.expected_amount_out || !q.inbound_address || !q.expiry || !checkedMemo || !inbounds) {
|
|
511
|
-
params.onRefusal?.(
|
|
822
|
+
params.onRefusal?.(
|
|
823
|
+
inbounds ? "thorchain: no quote from any node" : heightUnavailable ? "thorchain: THORChain height (/thorchain/lastblock) unavailable from every node that answered the inbound addresses" : "thorchain: inbound addresses unavailable from every node"
|
|
824
|
+
);
|
|
512
825
|
return null;
|
|
513
826
|
}
|
|
514
827
|
const limit1e8 = checkedMemo.limit;
|
|
@@ -530,7 +843,16 @@ function thorchainProvider(config = {}) {
|
|
|
530
843
|
params.onRefusal?.(`thorchain: router ${String(q.router)} is not a THORChain router this module trusts on ${params.sourceChain} (THORCHAIN_ROUTERS / trustedRouters)`);
|
|
531
844
|
return null;
|
|
532
845
|
}
|
|
533
|
-
const unconfirmed = await confirmedElsewhere(
|
|
846
|
+
const unconfirmed = await confirmedElsewhere(
|
|
847
|
+
quoteHost,
|
|
848
|
+
quoteHeight,
|
|
849
|
+
params.sourceChain,
|
|
850
|
+
params.destChain,
|
|
851
|
+
q.inbound_address,
|
|
852
|
+
chainId ? q.router : void 0,
|
|
853
|
+
confirmDeadlineAt,
|
|
854
|
+
signal
|
|
855
|
+
);
|
|
534
856
|
if (unconfirmed) {
|
|
535
857
|
params.onRefusal?.(`thorchain: ${unconfirmed}`);
|
|
536
858
|
return null;
|
|
@@ -645,18 +967,42 @@ function thorchainProvider(config = {}) {
|
|
|
645
967
|
if (evmSource && !pinnedRouter(data.sourceChain, data.router)) throw new Error("thorchain: the quote's router is not one this module trusts");
|
|
646
968
|
let inbounds = null;
|
|
647
969
|
let snapshotHost = "";
|
|
970
|
+
let snapshotHeight = 0;
|
|
971
|
+
let confirmDeadlineAt = 0;
|
|
972
|
+
let heightUnavailable = false;
|
|
648
973
|
for (const host of hosts) {
|
|
974
|
+
const deadlineAt = Date.now() + CONFIRM_BUDGET_MS;
|
|
649
975
|
try {
|
|
650
|
-
|
|
976
|
+
const snapshot = await nodeSnapshot(host);
|
|
977
|
+
if ("heightError" in snapshot) {
|
|
978
|
+
heightUnavailable = true;
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
inbounds = snapshot.inbounds;
|
|
982
|
+
snapshotHeight = snapshot.height;
|
|
651
983
|
snapshotHost = host;
|
|
984
|
+
confirmDeadlineAt = deadlineAt;
|
|
652
985
|
break;
|
|
653
986
|
} catch (error) {
|
|
987
|
+
inbounds = null;
|
|
654
988
|
if (!(error instanceof HostUnavailable)) throw error;
|
|
655
989
|
}
|
|
656
990
|
}
|
|
657
|
-
if (!inbounds)
|
|
991
|
+
if (!inbounds) {
|
|
992
|
+
throw new Error(
|
|
993
|
+
heightUnavailable ? "thorchain: cannot read THORChain height (/thorchain/lastblock) before broadcast from any node that answered the inbound addresses" : "thorchain: cannot refresh inbound addresses before broadcast"
|
|
994
|
+
);
|
|
995
|
+
}
|
|
658
996
|
const confirm = async () => {
|
|
659
|
-
const unconfirmed = await confirmedElsewhere(
|
|
997
|
+
const unconfirmed = await confirmedElsewhere(
|
|
998
|
+
snapshotHost,
|
|
999
|
+
snapshotHeight,
|
|
1000
|
+
data.sourceChain,
|
|
1001
|
+
data.destChain,
|
|
1002
|
+
data.inboundAddress,
|
|
1003
|
+
evmSource ? data.router : void 0,
|
|
1004
|
+
confirmDeadlineAt
|
|
1005
|
+
);
|
|
660
1006
|
if (unconfirmed) throw new Error(`thorchain: ${unconfirmed}`);
|
|
661
1007
|
};
|
|
662
1008
|
const src = inbounds.find((entry) => entry.chain === THOR_CHAIN[data.sourceChain]);
|