@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/index.cjs
CHANGED
|
@@ -25,6 +25,7 @@ __export(src_exports, {
|
|
|
25
25
|
THORCHAIN_ROUTERS: () => THORCHAIN_ROUTERS,
|
|
26
26
|
THORCHAIN_TRANSFER_OUT: () => THORCHAIN_TRANSFER_OUT,
|
|
27
27
|
THORCHAIN_TRANSFER_OUT_AND_CALL: () => THORCHAIN_TRANSFER_OUT_AND_CALL,
|
|
28
|
+
THORNODE_CONFIRM_PATHS: () => THORNODE_CONFIRM_PATHS,
|
|
28
29
|
ThorchainSwap2Protocol: () => ThorchainSwap2Protocol,
|
|
29
30
|
default: () => ThorchainSwap2Protocol,
|
|
30
31
|
describeThorchainActivity: () => describeThorchainActivity,
|
|
@@ -78,6 +79,200 @@ var import_viem = require("viem");
|
|
|
78
79
|
var import_util2 = require("@anywalletkit/protocol-swap2-core/util");
|
|
79
80
|
var import_swap2_assets = require("@anywalletkit/swap2-assets");
|
|
80
81
|
var import_core = require("@anywalletkit/core");
|
|
82
|
+
|
|
83
|
+
// src/confirm.ts
|
|
84
|
+
var THORNODE_CONFIRM_PATHS = Object.freeze({
|
|
85
|
+
lastblock: "/thorchain/lastblock",
|
|
86
|
+
inboundAddresses: "/thorchain/inbound_addresses",
|
|
87
|
+
asgardVaults: "/thorchain/vaults/asgard",
|
|
88
|
+
mimir: "/thorchain/mimir"
|
|
89
|
+
});
|
|
90
|
+
var DEFAULT_CONFIRM_MAX_LAG_BLOCKS = 50;
|
|
91
|
+
var DEFAULT_CONFIRM_TIMEOUT_MS = 2500;
|
|
92
|
+
var MIN_CONFIRM_TIMEOUT_MS = 100;
|
|
93
|
+
var MAX_CONFIRM_TIMEOUT_MS = 8e3;
|
|
94
|
+
function confirmOptions(o) {
|
|
95
|
+
const maxLagBlocks = o.confirmMaxLagBlocks ?? DEFAULT_CONFIRM_MAX_LAG_BLOCKS;
|
|
96
|
+
if (!Number.isInteger(maxLagBlocks) || maxLagBlocks < 0 || maxLagBlocks > 1e5) {
|
|
97
|
+
throw new Error("thorchain: confirmMaxLagBlocks must be an integer between 0 and 100000");
|
|
98
|
+
}
|
|
99
|
+
const timeoutMs = o.confirmTimeoutMs ?? DEFAULT_CONFIRM_TIMEOUT_MS;
|
|
100
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < MIN_CONFIRM_TIMEOUT_MS || timeoutMs > MAX_CONFIRM_TIMEOUT_MS) {
|
|
101
|
+
throw new Error(`thorchain: confirmTimeoutMs must be an integer between ${MIN_CONFIRM_TIMEOUT_MS} and ${MAX_CONFIRM_TIMEOUT_MS}`);
|
|
102
|
+
}
|
|
103
|
+
return { maxLagBlocks, timeoutMs };
|
|
104
|
+
}
|
|
105
|
+
var HostUnavailable = class extends Error {
|
|
106
|
+
constructor(host, reason) {
|
|
107
|
+
super(`thorchain: ${host} unavailable (${reason})`);
|
|
108
|
+
}
|
|
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}`);
|
|
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
|
|
81
276
|
var HOSTS = [
|
|
82
277
|
"https://gateway.liquify.com/chain/thorchain_api",
|
|
83
278
|
"https://thorchain.ibs.team/api"
|
|
@@ -93,6 +288,9 @@ var NATIVE = "0x0000000000000000000000000000000000000000";
|
|
|
93
288
|
var TOLERANCE_BPS = "300";
|
|
94
289
|
var MAX_TOLERANCE_BPS = 500n;
|
|
95
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");
|
|
96
294
|
var THOR_CHAIN = {
|
|
97
295
|
"eip155:1": "ETH",
|
|
98
296
|
"eip155:8453": "BASE",
|
|
@@ -173,18 +371,13 @@ function destAsset(destChain, destToken) {
|
|
|
173
371
|
const asset = thorAsset(destChain, a.symbol);
|
|
174
372
|
return asset ? { asset, decimals: a.decimals } : void 0;
|
|
175
373
|
}
|
|
176
|
-
var HostUnavailable = class extends Error {
|
|
177
|
-
constructor(host, reason) {
|
|
178
|
-
super(`thorchain: ${host} unavailable (${reason})`);
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
374
|
async function hostGet(host, path, clientId, signal) {
|
|
182
375
|
let res;
|
|
183
376
|
try {
|
|
184
377
|
res = await fetch(`${host}${path}`, {
|
|
185
378
|
headers: { accept: "application/json", "x-client-id": clientId },
|
|
186
379
|
cache: "no-store",
|
|
187
|
-
signal: signal ?? (0, import_util2.timeoutSignal)(
|
|
380
|
+
signal: signal ?? (0, import_util2.timeoutSignal)(NODE_READ_TIMEOUT_MS)
|
|
188
381
|
});
|
|
189
382
|
} catch (error) {
|
|
190
383
|
if (signal?.aborted) throw error;
|
|
@@ -219,14 +412,9 @@ function belowThorMinimums(amount1e8, inbound, quote) {
|
|
|
219
412
|
async function inboundAddressesOn(host, clientId) {
|
|
220
413
|
const res = await hostGet(host, "/thorchain/inbound_addresses", clientId);
|
|
221
414
|
if (!res.ok || !Array.isArray(res.body)) throw new HostUnavailable(host, `inbound_addresses HTTP ${res.status}`);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
return !entry || entry.halted === true || entry.global_trading_paused === true || entry.chain_trading_paused === true;
|
|
226
|
-
}
|
|
227
|
-
function sameTarget(a, b) {
|
|
228
|
-
if (!a || !b) return false;
|
|
229
|
-
return a.startsWith("0x") || b.startsWith("0x") ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
415
|
+
const entries = inboundAddresses(res.body);
|
|
416
|
+
if (!entries) throw new HostUnavailable(host, "inbound_addresses is not a THORNode inbound address list");
|
|
417
|
+
return entries;
|
|
230
418
|
}
|
|
231
419
|
function verifiedMemo(memo, expectedAsset, expectedDestination, expectedRefund, expectedAffiliate, expectedAffiliateBps) {
|
|
232
420
|
if (memo.includes("|")) return null;
|
|
@@ -256,6 +444,25 @@ function thorchainPins(opts = {}) {
|
|
|
256
444
|
const nodes = [...new Set((Array.isArray(opts.nodeUrl) ? opts.nodeUrl : opts.nodeUrl ? opts.nodeUrl.split(",") : HOSTS).map((h) => h.trim().replace(/\/$/, "")).filter(Boolean))];
|
|
257
445
|
for (const n of nodes) if (!/^https:\/\//i.test(n)) throw new Error("thorchain: thorchainPins nodeUrl must use https");
|
|
258
446
|
const clientId = opts.clientId?.trim() || DEFAULT_CLIENT_ID;
|
|
447
|
+
const { maxLagBlocks, timeoutMs } = confirmOptions(opts);
|
|
448
|
+
const askNode = async (host, claim) => {
|
|
449
|
+
const node = nodeDeadline(timeoutMs);
|
|
450
|
+
const read = (path) => nodeRead(host, path, clientId, node.signal);
|
|
451
|
+
try {
|
|
452
|
+
const head = await nodeHead(host, read);
|
|
453
|
+
try {
|
|
454
|
+
return { height: head.height, verdict: await vaultVerdict(host, read, head.inbound, claim) };
|
|
455
|
+
} catch (error) {
|
|
456
|
+
if (error instanceof HostUnavailable) return { height: head.height, verdict: null };
|
|
457
|
+
throw error;
|
|
458
|
+
}
|
|
459
|
+
} catch (error) {
|
|
460
|
+
if (error instanceof HostUnavailable) return null;
|
|
461
|
+
throw error;
|
|
462
|
+
} finally {
|
|
463
|
+
node.clear();
|
|
464
|
+
}
|
|
465
|
+
};
|
|
259
466
|
const routersFor = (chain) => [...THORCHAIN_ROUTERS[chain] ?? [], ...opts.trustedRouters?.[chain] ?? []];
|
|
260
467
|
return Object.freeze({
|
|
261
468
|
callTargets: routersFor,
|
|
@@ -296,22 +503,36 @@ function thorchainPins(opts = {}) {
|
|
|
296
503
|
if (!checked) return `the memo does not pay ${params.destAddress} in ${dst.asset} with this app's refund and affiliate`;
|
|
297
504
|
if (memo !== checked.canonical) return `the memo is not the canonical ${checked.canonical}`;
|
|
298
505
|
if (checked.limit * 10n ** BigInt(dst.decimals) / 100000000n < BigInt(quote.minDestAmount)) return `the memo's limit ${checked.limit} is below the quoted minimum`;
|
|
506
|
+
const sourceChain = THOR_CHAIN[params.sourceChain] ?? params.sourceChain;
|
|
507
|
+
const claim = { chain: sourceChain, destChain: THOR_CHAIN[params.destChain] ?? params.destChain, vault, ...router !== void 0 ? { router } : {} };
|
|
508
|
+
const views = await Promise.all(nodes.map((host) => askNode(host, claim)));
|
|
509
|
+
const top = views.reduce((max, v) => v && v.height > max ? v.height : max, -1);
|
|
299
510
|
let agreeing = 0;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
511
|
+
let lagging = false;
|
|
512
|
+
for (const view of views) {
|
|
513
|
+
if (!view?.verdict) continue;
|
|
514
|
+
if (top - view.height > maxLagBlocks) {
|
|
515
|
+
lagging = true;
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const v = view.verdict;
|
|
519
|
+
if (!v.ok) {
|
|
520
|
+
switch (v.why) {
|
|
521
|
+
case "halted":
|
|
522
|
+
return `${v.chain} is halted / paused`;
|
|
523
|
+
case "another-vault":
|
|
524
|
+
return `a THORNode reports another inbound vault than ${vault}`;
|
|
525
|
+
case "not-active":
|
|
526
|
+
return `a THORNode lists the inbound vault as ${v.status}, not ActiveVault`;
|
|
527
|
+
case "flagged":
|
|
528
|
+
return `a THORNode's mimir flags the inbound vault as ${v.flag}`;
|
|
529
|
+
case "another-router":
|
|
530
|
+
return "a THORNode reports another router";
|
|
531
|
+
}
|
|
307
532
|
}
|
|
308
|
-
const entry = entries.find((e) => e.chain === THOR_CHAIN[params.sourceChain]);
|
|
309
|
-
if (paused(entry)) return `${THOR_CHAIN[params.sourceChain]} is halted / paused`;
|
|
310
|
-
if (!sameTarget(entry?.address, vault)) return `a THORNode reports another inbound vault than ${vault}`;
|
|
311
|
-
if (router !== void 0 && !sameTarget(entry?.router, router)) return "a THORNode reports another router";
|
|
312
533
|
if (++agreeing >= 2) return null;
|
|
313
534
|
}
|
|
314
|
-
return "two THORNodes could not confirm the inbound vault";
|
|
535
|
+
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";
|
|
315
536
|
}
|
|
316
537
|
});
|
|
317
538
|
}
|
|
@@ -351,21 +572,100 @@ function thorchainProvider(config = {}) {
|
|
|
351
572
|
routers.set(chain, set);
|
|
352
573
|
}
|
|
353
574
|
const pinnedRouter = (chain, router) => !!router && routers.get(chain)?.has(router.toLowerCase()) === true;
|
|
354
|
-
const
|
|
575
|
+
const { maxLagBlocks: confirmMaxLagBlocks, timeoutMs: confirmTimeoutMs } = confirmOptions(config);
|
|
576
|
+
const nodeSnapshot = (host, outer) => new Promise((resolve, reject) => {
|
|
577
|
+
let inbounds;
|
|
578
|
+
let height;
|
|
579
|
+
let heightError;
|
|
580
|
+
let done = false;
|
|
581
|
+
const tip = nodeDeadline(NODE_READ_TIMEOUT_MS, outer);
|
|
582
|
+
const settle = () => {
|
|
583
|
+
if (done || inbounds === void 0 || height === void 0 && heightError === void 0) return;
|
|
584
|
+
done = true;
|
|
585
|
+
resolve(heightError ? { inbounds, heightError } : { inbounds, height });
|
|
586
|
+
};
|
|
587
|
+
const fail = (error) => {
|
|
588
|
+
if (done) return;
|
|
589
|
+
done = true;
|
|
590
|
+
tip.clear();
|
|
591
|
+
reject(error);
|
|
592
|
+
};
|
|
593
|
+
inboundAddressesOn(host, clientId).then((v) => {
|
|
594
|
+
inbounds = v;
|
|
595
|
+
settle();
|
|
596
|
+
}, fail);
|
|
597
|
+
nodeRead(host, THORNODE_CONFIRM_PATHS.lastblock, clientId, tip.signal, outer).then(
|
|
598
|
+
(body) => {
|
|
599
|
+
tip.clear();
|
|
600
|
+
const h = lastblockHeight(body);
|
|
601
|
+
if (h === null) heightError = new HostUnavailable(host, "lastblock is not a THORNode lastblock list");
|
|
602
|
+
else height = h;
|
|
603
|
+
settle();
|
|
604
|
+
},
|
|
605
|
+
(error) => {
|
|
606
|
+
tip.clear();
|
|
607
|
+
if (!(error instanceof HostUnavailable)) return fail(error);
|
|
608
|
+
heightError = error;
|
|
609
|
+
settle();
|
|
610
|
+
}
|
|
611
|
+
);
|
|
612
|
+
});
|
|
613
|
+
const confirmText = (v) => {
|
|
614
|
+
switch (v.why) {
|
|
615
|
+
case "halted":
|
|
616
|
+
return `${v.chain} is halted / paused on the confirming node`;
|
|
617
|
+
case "another-vault":
|
|
618
|
+
return "a second node reports another inbound vault \u2014 refused until they agree";
|
|
619
|
+
case "not-active":
|
|
620
|
+
return `a second node lists the inbound vault as ${v.status}, not ActiveVault \u2014 refused`;
|
|
621
|
+
case "flagged":
|
|
622
|
+
return `a second node's mimir flags the inbound vault as ${v.flag} \u2014 refused`;
|
|
623
|
+
case "another-router":
|
|
624
|
+
return "a second node reports another router \u2014 refused until they agree";
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
const confirmOn = async (host, signal, outer, quoteHeight, claim) => {
|
|
628
|
+
const read = (path) => nodeRead(host, path, clientId, signal, outer);
|
|
629
|
+
const head = await nodeHead(host, read);
|
|
630
|
+
if (quoteHeight - head.height > confirmMaxLagBlocks) return LAGGING;
|
|
631
|
+
const verdict = await vaultVerdict(host, read, head.inbound, claim);
|
|
632
|
+
return verdict.ok ? null : confirmText(verdict);
|
|
633
|
+
};
|
|
634
|
+
const confirmedElsewhere = async (quoteHost, quoteHeight, chain, destChain, vault, router, deadlineAt, outer) => {
|
|
635
|
+
const claim = {
|
|
636
|
+
chain: THOR_CHAIN[chain] ?? chain,
|
|
637
|
+
...destChain !== void 0 ? { destChain: THOR_CHAIN[destChain] ?? destChain } : {},
|
|
638
|
+
vault,
|
|
639
|
+
...router !== void 0 ? { router } : {}
|
|
640
|
+
};
|
|
641
|
+
let lagging = false;
|
|
642
|
+
let asked = 0;
|
|
355
643
|
for (const host of confirmHosts) {
|
|
356
644
|
if (host === quoteHost) continue;
|
|
357
|
-
|
|
645
|
+
const budget = Math.min(confirmTimeoutMs, deadlineAt - Date.now());
|
|
646
|
+
if (budget < MIN_CONFIRM_TIMEOUT_MS) break;
|
|
647
|
+
asked++;
|
|
648
|
+
const node = nodeDeadline(budget, outer);
|
|
358
649
|
try {
|
|
359
|
-
|
|
650
|
+
const verdict = await confirmOn(host, node.signal, outer, quoteHeight, claim);
|
|
651
|
+
if (verdict === LAGGING) {
|
|
652
|
+
lagging = true;
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
return verdict;
|
|
360
656
|
} catch (error) {
|
|
657
|
+
if (outer?.aborted) throw error;
|
|
361
658
|
if (error instanceof HostUnavailable) continue;
|
|
362
659
|
throw error;
|
|
660
|
+
} finally {
|
|
661
|
+
node.clear();
|
|
363
662
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
663
|
+
}
|
|
664
|
+
if (lagging) {
|
|
665
|
+
return `no confirming node is within ${confirmMaxLagBlocks} blocks of the quoting node's THORChain height (behind or unreachable) \u2014 refused until one catches up`;
|
|
666
|
+
}
|
|
667
|
+
if (asked === 0) {
|
|
668
|
+
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`;
|
|
369
669
|
}
|
|
370
670
|
return "no second node answered to confirm the inbound vault (configure confirmNodeUrl)";
|
|
371
671
|
};
|
|
@@ -458,6 +758,9 @@ function thorchainProvider(config = {}) {
|
|
|
458
758
|
let checkedMemo = null;
|
|
459
759
|
let inbounds = null;
|
|
460
760
|
let quoteHost = "";
|
|
761
|
+
let quoteHeight = 0;
|
|
762
|
+
let confirmDeadlineAt = 0;
|
|
763
|
+
let heightUnavailable = false;
|
|
461
764
|
for (const host of hosts) {
|
|
462
765
|
try {
|
|
463
766
|
if (limitMode === "liquidity_tolerance_bps") {
|
|
@@ -502,8 +805,16 @@ function thorchainProvider(config = {}) {
|
|
|
502
805
|
params.onRefusal?.(`thorchain: floor breach (limit=${checkedMemo.limit} < floor=${floor1e8}, 1e8 units)`);
|
|
503
806
|
return null;
|
|
504
807
|
}
|
|
505
|
-
|
|
808
|
+
const deadlineAt = Date.now() + CONFIRM_BUDGET_MS;
|
|
809
|
+
const snapshot = await nodeSnapshot(host, signal);
|
|
810
|
+
if ("heightError" in snapshot) {
|
|
811
|
+
heightUnavailable = true;
|
|
812
|
+
throw snapshot.heightError;
|
|
813
|
+
}
|
|
814
|
+
inbounds = snapshot.inbounds;
|
|
815
|
+
quoteHeight = snapshot.height;
|
|
506
816
|
quoteHost = host;
|
|
817
|
+
confirmDeadlineAt = deadlineAt;
|
|
507
818
|
break;
|
|
508
819
|
} catch (error) {
|
|
509
820
|
if (error instanceof HostUnavailable) {
|
|
@@ -516,7 +827,9 @@ function thorchainProvider(config = {}) {
|
|
|
516
827
|
}
|
|
517
828
|
}
|
|
518
829
|
if (!q?.expected_amount_out || !q.inbound_address || !q.expiry || !checkedMemo || !inbounds) {
|
|
519
|
-
params.onRefusal?.(
|
|
830
|
+
params.onRefusal?.(
|
|
831
|
+
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"
|
|
832
|
+
);
|
|
520
833
|
return null;
|
|
521
834
|
}
|
|
522
835
|
const limit1e8 = checkedMemo.limit;
|
|
@@ -538,7 +851,16 @@ function thorchainProvider(config = {}) {
|
|
|
538
851
|
params.onRefusal?.(`thorchain: router ${String(q.router)} is not a THORChain router this module trusts on ${params.sourceChain} (THORCHAIN_ROUTERS / trustedRouters)`);
|
|
539
852
|
return null;
|
|
540
853
|
}
|
|
541
|
-
const unconfirmed = await confirmedElsewhere(
|
|
854
|
+
const unconfirmed = await confirmedElsewhere(
|
|
855
|
+
quoteHost,
|
|
856
|
+
quoteHeight,
|
|
857
|
+
params.sourceChain,
|
|
858
|
+
params.destChain,
|
|
859
|
+
q.inbound_address,
|
|
860
|
+
chainId ? q.router : void 0,
|
|
861
|
+
confirmDeadlineAt,
|
|
862
|
+
signal
|
|
863
|
+
);
|
|
542
864
|
if (unconfirmed) {
|
|
543
865
|
params.onRefusal?.(`thorchain: ${unconfirmed}`);
|
|
544
866
|
return null;
|
|
@@ -653,18 +975,42 @@ function thorchainProvider(config = {}) {
|
|
|
653
975
|
if (evmSource && !pinnedRouter(data.sourceChain, data.router)) throw new Error("thorchain: the quote's router is not one this module trusts");
|
|
654
976
|
let inbounds = null;
|
|
655
977
|
let snapshotHost = "";
|
|
978
|
+
let snapshotHeight = 0;
|
|
979
|
+
let confirmDeadlineAt = 0;
|
|
980
|
+
let heightUnavailable = false;
|
|
656
981
|
for (const host of hosts) {
|
|
982
|
+
const deadlineAt = Date.now() + CONFIRM_BUDGET_MS;
|
|
657
983
|
try {
|
|
658
|
-
|
|
984
|
+
const snapshot = await nodeSnapshot(host);
|
|
985
|
+
if ("heightError" in snapshot) {
|
|
986
|
+
heightUnavailable = true;
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
inbounds = snapshot.inbounds;
|
|
990
|
+
snapshotHeight = snapshot.height;
|
|
659
991
|
snapshotHost = host;
|
|
992
|
+
confirmDeadlineAt = deadlineAt;
|
|
660
993
|
break;
|
|
661
994
|
} catch (error) {
|
|
995
|
+
inbounds = null;
|
|
662
996
|
if (!(error instanceof HostUnavailable)) throw error;
|
|
663
997
|
}
|
|
664
998
|
}
|
|
665
|
-
if (!inbounds)
|
|
999
|
+
if (!inbounds) {
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
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"
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
666
1004
|
const confirm = async () => {
|
|
667
|
-
const unconfirmed = await confirmedElsewhere(
|
|
1005
|
+
const unconfirmed = await confirmedElsewhere(
|
|
1006
|
+
snapshotHost,
|
|
1007
|
+
snapshotHeight,
|
|
1008
|
+
data.sourceChain,
|
|
1009
|
+
data.destChain,
|
|
1010
|
+
data.inboundAddress,
|
|
1011
|
+
evmSource ? data.router : void 0,
|
|
1012
|
+
confirmDeadlineAt
|
|
1013
|
+
);
|
|
668
1014
|
if (unconfirmed) throw new Error(`thorchain: ${unconfirmed}`);
|
|
669
1015
|
};
|
|
670
1016
|
const src = inbounds.find((entry) => entry.chain === THOR_CHAIN[data.sourceChain]);
|