@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/protocol.cjs
CHANGED
|
@@ -69,6 +69,200 @@ var import_viem = require("viem");
|
|
|
69
69
|
var import_util2 = require("@anywalletkit/protocol-swap2-core/util");
|
|
70
70
|
var import_swap2_assets = require("@anywalletkit/swap2-assets");
|
|
71
71
|
var import_core = require("@anywalletkit/core");
|
|
72
|
+
|
|
73
|
+
// src/confirm.ts
|
|
74
|
+
var THORNODE_CONFIRM_PATHS = Object.freeze({
|
|
75
|
+
lastblock: "/thorchain/lastblock",
|
|
76
|
+
inboundAddresses: "/thorchain/inbound_addresses",
|
|
77
|
+
asgardVaults: "/thorchain/vaults/asgard",
|
|
78
|
+
mimir: "/thorchain/mimir"
|
|
79
|
+
});
|
|
80
|
+
var DEFAULT_CONFIRM_MAX_LAG_BLOCKS = 50;
|
|
81
|
+
var DEFAULT_CONFIRM_TIMEOUT_MS = 2500;
|
|
82
|
+
var MIN_CONFIRM_TIMEOUT_MS = 100;
|
|
83
|
+
var MAX_CONFIRM_TIMEOUT_MS = 8e3;
|
|
84
|
+
function confirmOptions(o) {
|
|
85
|
+
const maxLagBlocks = o.confirmMaxLagBlocks ?? DEFAULT_CONFIRM_MAX_LAG_BLOCKS;
|
|
86
|
+
if (!Number.isInteger(maxLagBlocks) || maxLagBlocks < 0 || maxLagBlocks > 1e5) {
|
|
87
|
+
throw new Error("thorchain: confirmMaxLagBlocks must be an integer between 0 and 100000");
|
|
88
|
+
}
|
|
89
|
+
const timeoutMs = o.confirmTimeoutMs ?? DEFAULT_CONFIRM_TIMEOUT_MS;
|
|
90
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < MIN_CONFIRM_TIMEOUT_MS || timeoutMs > MAX_CONFIRM_TIMEOUT_MS) {
|
|
91
|
+
throw new Error(`thorchain: confirmTimeoutMs must be an integer between ${MIN_CONFIRM_TIMEOUT_MS} and ${MAX_CONFIRM_TIMEOUT_MS}`);
|
|
92
|
+
}
|
|
93
|
+
return { maxLagBlocks, timeoutMs };
|
|
94
|
+
}
|
|
95
|
+
var HostUnavailable = class extends Error {
|
|
96
|
+
constructor(host, reason) {
|
|
97
|
+
super(`thorchain: ${host} unavailable (${reason})`);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
function paused(entry) {
|
|
101
|
+
return !entry || entry.halted === true || entry.global_trading_paused === true || entry.chain_trading_paused === true;
|
|
102
|
+
}
|
|
103
|
+
function sameTarget(a, b) {
|
|
104
|
+
if (typeof a !== "string" || typeof b !== "string" || !a || !b) return false;
|
|
105
|
+
return a.startsWith("0x") || b.startsWith("0x") ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
106
|
+
}
|
|
107
|
+
function nodeDeadline(ms, outer) {
|
|
108
|
+
const ctrl = new AbortController();
|
|
109
|
+
const timer = setTimeout(() => ctrl.abort(), Math.max(0, ms));
|
|
110
|
+
const onAbort = () => ctrl.abort();
|
|
111
|
+
if (outer) {
|
|
112
|
+
if (outer.aborted) ctrl.abort();
|
|
113
|
+
else outer.addEventListener("abort", onAbort);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
signal: ctrl.signal,
|
|
117
|
+
clear() {
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
outer?.removeEventListener("abort", onAbort);
|
|
120
|
+
if (!ctrl.signal.aborted) ctrl.abort();
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function sameRequestUrl(actual, expected) {
|
|
125
|
+
if (actual === void 0 || actual === null || actual === "") return true;
|
|
126
|
+
if (typeof actual !== "string") return false;
|
|
127
|
+
try {
|
|
128
|
+
const a = new URL(actual);
|
|
129
|
+
const e = new URL(expected);
|
|
130
|
+
return a.origin === e.origin && a.pathname === e.pathname && a.search === "" && a.hash === "";
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function nodeRead(host, path, clientId, signal, outer) {
|
|
136
|
+
const url = `${host}${path}`;
|
|
137
|
+
let res;
|
|
138
|
+
try {
|
|
139
|
+
res = await fetch(url, { method: "GET", headers: { accept: "application/json", "x-client-id": clientId }, redirect: "error", signal });
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (outer?.aborted) throw error;
|
|
142
|
+
throw new HostUnavailable(host, error instanceof Error ? error.message : "fetch failed");
|
|
143
|
+
}
|
|
144
|
+
if (res.redirected === true || !sameRequestUrl(res.url, url)) throw new HostUnavailable(host, `${path} redirected`);
|
|
145
|
+
if (!res.ok) throw new HostUnavailable(host, `${path} HTTP ${res.status}`);
|
|
146
|
+
try {
|
|
147
|
+
return await res.json();
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (outer?.aborted) throw error;
|
|
150
|
+
throw new HostUnavailable(host, `${path} is not JSON`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function int64(v) {
|
|
154
|
+
if (typeof v === "number") return Number.isSafeInteger(v) ? v : null;
|
|
155
|
+
if (typeof v === "string" && /^-?\d{1,15}$/.test(v)) return Number(v);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
159
|
+
var INBOUND_STRINGS = ["address", "router", "dust_threshold"];
|
|
160
|
+
var INBOUND_FLAGS = ["halted", "global_trading_paused", "chain_trading_paused"];
|
|
161
|
+
function inboundAddresses(body) {
|
|
162
|
+
if (!Array.isArray(body)) return null;
|
|
163
|
+
const out = [];
|
|
164
|
+
for (const e of body) {
|
|
165
|
+
if (!isObject(e) || typeof e.chain !== "string") return null;
|
|
166
|
+
const entry = { chain: e.chain };
|
|
167
|
+
for (const k of INBOUND_STRINGS) {
|
|
168
|
+
const v = e[k];
|
|
169
|
+
if (v === void 0) continue;
|
|
170
|
+
if (typeof v !== "string") return null;
|
|
171
|
+
entry[k] = v;
|
|
172
|
+
}
|
|
173
|
+
for (const k of INBOUND_FLAGS) {
|
|
174
|
+
const v = e[k];
|
|
175
|
+
if (v === void 0) continue;
|
|
176
|
+
if (typeof v !== "boolean") return null;
|
|
177
|
+
entry[k] = v;
|
|
178
|
+
}
|
|
179
|
+
out.push(entry);
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
function lastblockHeight(body) {
|
|
184
|
+
if (!Array.isArray(body) || body.length === 0) return null;
|
|
185
|
+
let height = -1;
|
|
186
|
+
for (const entry of body) {
|
|
187
|
+
if (!isObject(entry)) return null;
|
|
188
|
+
const h = int64(entry.thorchain);
|
|
189
|
+
if (h === null || h < 0) return null;
|
|
190
|
+
if (h > height) height = h;
|
|
191
|
+
}
|
|
192
|
+
return height;
|
|
193
|
+
}
|
|
194
|
+
function asgardVaults(body) {
|
|
195
|
+
if (!Array.isArray(body)) return null;
|
|
196
|
+
const out = [];
|
|
197
|
+
for (const v of body) {
|
|
198
|
+
if (!isObject(v)) return null;
|
|
199
|
+
if (typeof v.status !== "string" || typeof v.pub_key !== "string" || !/^thorpub1[0-9a-z]{20,}$/.test(v.pub_key)) return null;
|
|
200
|
+
if (v.type !== void 0 && v.type !== "AsgardVault") return null;
|
|
201
|
+
if (!Array.isArray(v.addresses)) return null;
|
|
202
|
+
const addresses = [];
|
|
203
|
+
for (const a of v.addresses) {
|
|
204
|
+
if (!isObject(a) || typeof a.chain !== "string" || typeof a.address !== "string") return null;
|
|
205
|
+
addresses.push({ chain: a.chain, address: a.address });
|
|
206
|
+
}
|
|
207
|
+
out.push({ pubKey: v.pub_key, status: v.status, addresses });
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
function mimirFlags(body) {
|
|
212
|
+
if (!isObject(body) || Object.keys(body).length === 0) return null;
|
|
213
|
+
const keys = Object.keys(body).map((k) => [k.toUpperCase(), body[k]]);
|
|
214
|
+
if (keys.some(([, value]) => typeof value !== "number" && typeof value !== "string")) return null;
|
|
215
|
+
return (pubKey) => {
|
|
216
|
+
const pub = pubKey.toUpperCase();
|
|
217
|
+
const compromisedKey = `COMPROMISEDVAULT-${pub}`;
|
|
218
|
+
const insolventPrefix = `SOLVENCYHALTVAULT-${pub}-`;
|
|
219
|
+
let flag = null;
|
|
220
|
+
for (const [key, value] of keys) {
|
|
221
|
+
const kind = key === compromisedKey ? "compromised" : key.startsWith(insolventPrefix) ? "insolvent" : null;
|
|
222
|
+
if (!kind) continue;
|
|
223
|
+
const n = int64(value);
|
|
224
|
+
if (n === null) return "malformed";
|
|
225
|
+
if (n > 0 && flag !== "compromised") flag = kind;
|
|
226
|
+
}
|
|
227
|
+
return flag;
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
async function nodeHead(host, read) {
|
|
231
|
+
const [lastblock, inbound] = await Promise.all([read(THORNODE_CONFIRM_PATHS.lastblock), read(THORNODE_CONFIRM_PATHS.inboundAddresses)]);
|
|
232
|
+
const height = lastblockHeight(lastblock);
|
|
233
|
+
if (height === null) throw new HostUnavailable(host, "lastblock is not a THORNode lastblock list");
|
|
234
|
+
return { height, inbound };
|
|
235
|
+
}
|
|
236
|
+
async function vaultVerdict(host, read, inbound, claim) {
|
|
237
|
+
if (!Array.isArray(inbound)) throw new HostUnavailable(host, "inbound_addresses is not a list");
|
|
238
|
+
const entries = inboundAddresses(inbound);
|
|
239
|
+
if (!entries) throw new HostUnavailable(host, "inbound_addresses is not a THORNode inbound address list");
|
|
240
|
+
const entry = entries.find((e) => e.chain === claim.chain);
|
|
241
|
+
if (paused(entry)) return { ok: false, why: "halted", chain: claim.chain };
|
|
242
|
+
if (claim.destChain !== void 0 && paused(entries.find((e) => e.chain === claim.destChain))) {
|
|
243
|
+
return { ok: false, why: "halted", chain: claim.destChain };
|
|
244
|
+
}
|
|
245
|
+
if (!sameTarget(entry?.address, claim.vault)) {
|
|
246
|
+
const [asgardBody, mimirBody] = await Promise.all([read(THORNODE_CONFIRM_PATHS.asgardVaults), read(THORNODE_CONFIRM_PATHS.mimir)]);
|
|
247
|
+
const vaults = asgardVaults(asgardBody);
|
|
248
|
+
if (!vaults) throw new HostUnavailable(host, "vaults/asgard is not a THORNode vault list");
|
|
249
|
+
const flags = mimirFlags(mimirBody);
|
|
250
|
+
if (!flags) throw new HostUnavailable(host, "mimir is not a THORNode mimir map");
|
|
251
|
+
const holders = vaults.filter((v) => v.addresses.some((a) => a.chain === claim.chain && sameTarget(a.address, claim.vault)));
|
|
252
|
+
const active = holders.find((v) => v.status === "ActiveVault");
|
|
253
|
+
if (!active) {
|
|
254
|
+
if (!holders.length) return { ok: false, why: "another-vault" };
|
|
255
|
+
return { ok: false, why: "not-active", status: /^[A-Za-z]{1,32}$/.test(holders[0].status) ? holders[0].status : "an unknown status" };
|
|
256
|
+
}
|
|
257
|
+
const flag = flags(active.pubKey);
|
|
258
|
+
if (flag === "malformed") throw new HostUnavailable(host, "mimir holds a non-integer vault flag");
|
|
259
|
+
if (flag) return { ok: false, why: "flagged", flag };
|
|
260
|
+
}
|
|
261
|
+
if (claim.router !== void 0 && !sameTarget(entry?.router, claim.router)) return { ok: false, why: "another-router" };
|
|
262
|
+
return { ok: true };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/provider.ts
|
|
72
266
|
var HOSTS = [
|
|
73
267
|
"https://gateway.liquify.com/chain/thorchain_api",
|
|
74
268
|
"https://thorchain.ibs.team/api"
|
|
@@ -84,6 +278,9 @@ var NATIVE = "0x0000000000000000000000000000000000000000";
|
|
|
84
278
|
var TOLERANCE_BPS = "300";
|
|
85
279
|
var MAX_TOLERANCE_BPS = 500n;
|
|
86
280
|
var QUOTE_VALIDITY_MS = 5 * 6e4;
|
|
281
|
+
var NODE_READ_TIMEOUT_MS = 8e3;
|
|
282
|
+
var CONFIRM_BUDGET_MS = 8e3;
|
|
283
|
+
var LAGGING = /* @__PURE__ */ Symbol("lagging");
|
|
87
284
|
var THOR_CHAIN = {
|
|
88
285
|
"eip155:1": "ETH",
|
|
89
286
|
"eip155:8453": "BASE",
|
|
@@ -164,18 +361,13 @@ function destAsset(destChain, destToken) {
|
|
|
164
361
|
const asset = thorAsset(destChain, a.symbol);
|
|
165
362
|
return asset ? { asset, decimals: a.decimals } : void 0;
|
|
166
363
|
}
|
|
167
|
-
var HostUnavailable = class extends Error {
|
|
168
|
-
constructor(host, reason) {
|
|
169
|
-
super(`thorchain: ${host} unavailable (${reason})`);
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
364
|
async function hostGet(host, path, clientId, signal) {
|
|
173
365
|
let res;
|
|
174
366
|
try {
|
|
175
367
|
res = await fetch(`${host}${path}`, {
|
|
176
368
|
headers: { accept: "application/json", "x-client-id": clientId },
|
|
177
369
|
cache: "no-store",
|
|
178
|
-
signal: signal ?? (0, import_util2.timeoutSignal)(
|
|
370
|
+
signal: signal ?? (0, import_util2.timeoutSignal)(NODE_READ_TIMEOUT_MS)
|
|
179
371
|
});
|
|
180
372
|
} catch (error) {
|
|
181
373
|
if (signal?.aborted) throw error;
|
|
@@ -210,14 +402,9 @@ function belowThorMinimums(amount1e8, inbound, quote) {
|
|
|
210
402
|
async function inboundAddressesOn(host, clientId) {
|
|
211
403
|
const res = await hostGet(host, "/thorchain/inbound_addresses", clientId);
|
|
212
404
|
if (!res.ok || !Array.isArray(res.body)) throw new HostUnavailable(host, `inbound_addresses HTTP ${res.status}`);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return !entry || entry.halted === true || entry.global_trading_paused === true || entry.chain_trading_paused === true;
|
|
217
|
-
}
|
|
218
|
-
function sameTarget(a, b) {
|
|
219
|
-
if (!a || !b) return false;
|
|
220
|
-
return a.startsWith("0x") || b.startsWith("0x") ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
405
|
+
const entries = inboundAddresses(res.body);
|
|
406
|
+
if (!entries) throw new HostUnavailable(host, "inbound_addresses is not a THORNode inbound address list");
|
|
407
|
+
return entries;
|
|
221
408
|
}
|
|
222
409
|
function verifiedMemo(memo, expectedAsset, expectedDestination, expectedRefund, expectedAffiliate, expectedAffiliateBps) {
|
|
223
410
|
if (memo.includes("|")) return null;
|
|
@@ -279,21 +466,100 @@ function thorchainProvider(config = {}) {
|
|
|
279
466
|
routers.set(chain, set);
|
|
280
467
|
}
|
|
281
468
|
const pinnedRouter = (chain, router) => !!router && routers.get(chain)?.has(router.toLowerCase()) === true;
|
|
282
|
-
const
|
|
469
|
+
const { maxLagBlocks: confirmMaxLagBlocks, timeoutMs: confirmTimeoutMs } = confirmOptions(config);
|
|
470
|
+
const nodeSnapshot = (host, outer) => new Promise((resolve, reject) => {
|
|
471
|
+
let inbounds;
|
|
472
|
+
let height;
|
|
473
|
+
let heightError;
|
|
474
|
+
let done = false;
|
|
475
|
+
const tip = nodeDeadline(NODE_READ_TIMEOUT_MS, outer);
|
|
476
|
+
const settle = () => {
|
|
477
|
+
if (done || inbounds === void 0 || height === void 0 && heightError === void 0) return;
|
|
478
|
+
done = true;
|
|
479
|
+
resolve(heightError ? { inbounds, heightError } : { inbounds, height });
|
|
480
|
+
};
|
|
481
|
+
const fail = (error) => {
|
|
482
|
+
if (done) return;
|
|
483
|
+
done = true;
|
|
484
|
+
tip.clear();
|
|
485
|
+
reject(error);
|
|
486
|
+
};
|
|
487
|
+
inboundAddressesOn(host, clientId).then((v) => {
|
|
488
|
+
inbounds = v;
|
|
489
|
+
settle();
|
|
490
|
+
}, fail);
|
|
491
|
+
nodeRead(host, THORNODE_CONFIRM_PATHS.lastblock, clientId, tip.signal, outer).then(
|
|
492
|
+
(body) => {
|
|
493
|
+
tip.clear();
|
|
494
|
+
const h = lastblockHeight(body);
|
|
495
|
+
if (h === null) heightError = new HostUnavailable(host, "lastblock is not a THORNode lastblock list");
|
|
496
|
+
else height = h;
|
|
497
|
+
settle();
|
|
498
|
+
},
|
|
499
|
+
(error) => {
|
|
500
|
+
tip.clear();
|
|
501
|
+
if (!(error instanceof HostUnavailable)) return fail(error);
|
|
502
|
+
heightError = error;
|
|
503
|
+
settle();
|
|
504
|
+
}
|
|
505
|
+
);
|
|
506
|
+
});
|
|
507
|
+
const confirmText = (v) => {
|
|
508
|
+
switch (v.why) {
|
|
509
|
+
case "halted":
|
|
510
|
+
return `${v.chain} is halted / paused on the confirming node`;
|
|
511
|
+
case "another-vault":
|
|
512
|
+
return "a second node reports another inbound vault \u2014 refused until they agree";
|
|
513
|
+
case "not-active":
|
|
514
|
+
return `a second node lists the inbound vault as ${v.status}, not ActiveVault \u2014 refused`;
|
|
515
|
+
case "flagged":
|
|
516
|
+
return `a second node's mimir flags the inbound vault as ${v.flag} \u2014 refused`;
|
|
517
|
+
case "another-router":
|
|
518
|
+
return "a second node reports another router \u2014 refused until they agree";
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
const confirmOn = async (host, signal, outer, quoteHeight, claim) => {
|
|
522
|
+
const read = (path) => nodeRead(host, path, clientId, signal, outer);
|
|
523
|
+
const head = await nodeHead(host, read);
|
|
524
|
+
if (quoteHeight - head.height > confirmMaxLagBlocks) return LAGGING;
|
|
525
|
+
const verdict = await vaultVerdict(host, read, head.inbound, claim);
|
|
526
|
+
return verdict.ok ? null : confirmText(verdict);
|
|
527
|
+
};
|
|
528
|
+
const confirmedElsewhere = async (quoteHost, quoteHeight, chain, destChain, vault, router, deadlineAt, outer) => {
|
|
529
|
+
const claim = {
|
|
530
|
+
chain: THOR_CHAIN[chain] ?? chain,
|
|
531
|
+
...destChain !== void 0 ? { destChain: THOR_CHAIN[destChain] ?? destChain } : {},
|
|
532
|
+
vault,
|
|
533
|
+
...router !== void 0 ? { router } : {}
|
|
534
|
+
};
|
|
535
|
+
let lagging = false;
|
|
536
|
+
let asked = 0;
|
|
283
537
|
for (const host of confirmHosts) {
|
|
284
538
|
if (host === quoteHost) continue;
|
|
285
|
-
|
|
539
|
+
const budget = Math.min(confirmTimeoutMs, deadlineAt - Date.now());
|
|
540
|
+
if (budget < MIN_CONFIRM_TIMEOUT_MS) break;
|
|
541
|
+
asked++;
|
|
542
|
+
const node = nodeDeadline(budget, outer);
|
|
286
543
|
try {
|
|
287
|
-
|
|
544
|
+
const verdict = await confirmOn(host, node.signal, outer, quoteHeight, claim);
|
|
545
|
+
if (verdict === LAGGING) {
|
|
546
|
+
lagging = true;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
return verdict;
|
|
288
550
|
} catch (error) {
|
|
551
|
+
if (outer?.aborted) throw error;
|
|
289
552
|
if (error instanceof HostUnavailable) continue;
|
|
290
553
|
throw error;
|
|
554
|
+
} finally {
|
|
555
|
+
node.clear();
|
|
291
556
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
557
|
+
}
|
|
558
|
+
if (lagging) {
|
|
559
|
+
return `no confirming node is within ${confirmMaxLagBlocks} blocks of the quoting node's THORChain height (behind or unreachable) \u2014 refused until one catches up`;
|
|
560
|
+
}
|
|
561
|
+
if (asked === 0) {
|
|
562
|
+
return `the ${CONFIRM_BUDGET_MS / 1e3} s confirmation budget ran out before a second node could be asked to confirm the inbound vault \u2014 retry`;
|
|
297
563
|
}
|
|
298
564
|
return "no second node answered to confirm the inbound vault (configure confirmNodeUrl)";
|
|
299
565
|
};
|
|
@@ -386,6 +652,9 @@ function thorchainProvider(config = {}) {
|
|
|
386
652
|
let checkedMemo = null;
|
|
387
653
|
let inbounds = null;
|
|
388
654
|
let quoteHost = "";
|
|
655
|
+
let quoteHeight = 0;
|
|
656
|
+
let confirmDeadlineAt = 0;
|
|
657
|
+
let heightUnavailable = false;
|
|
389
658
|
for (const host of hosts) {
|
|
390
659
|
try {
|
|
391
660
|
if (limitMode === "liquidity_tolerance_bps") {
|
|
@@ -430,8 +699,16 @@ function thorchainProvider(config = {}) {
|
|
|
430
699
|
params.onRefusal?.(`thorchain: floor breach (limit=${checkedMemo.limit} < floor=${floor1e8}, 1e8 units)`);
|
|
431
700
|
return null;
|
|
432
701
|
}
|
|
433
|
-
|
|
702
|
+
const deadlineAt = Date.now() + CONFIRM_BUDGET_MS;
|
|
703
|
+
const snapshot = await nodeSnapshot(host, signal);
|
|
704
|
+
if ("heightError" in snapshot) {
|
|
705
|
+
heightUnavailable = true;
|
|
706
|
+
throw snapshot.heightError;
|
|
707
|
+
}
|
|
708
|
+
inbounds = snapshot.inbounds;
|
|
709
|
+
quoteHeight = snapshot.height;
|
|
434
710
|
quoteHost = host;
|
|
711
|
+
confirmDeadlineAt = deadlineAt;
|
|
435
712
|
break;
|
|
436
713
|
} catch (error) {
|
|
437
714
|
if (error instanceof HostUnavailable) {
|
|
@@ -444,7 +721,9 @@ function thorchainProvider(config = {}) {
|
|
|
444
721
|
}
|
|
445
722
|
}
|
|
446
723
|
if (!q?.expected_amount_out || !q.inbound_address || !q.expiry || !checkedMemo || !inbounds) {
|
|
447
|
-
params.onRefusal?.(
|
|
724
|
+
params.onRefusal?.(
|
|
725
|
+
inbounds ? "thorchain: no quote from any node" : heightUnavailable ? "thorchain: THORChain height (/thorchain/lastblock) unavailable from every node that answered the inbound addresses" : "thorchain: inbound addresses unavailable from every node"
|
|
726
|
+
);
|
|
448
727
|
return null;
|
|
449
728
|
}
|
|
450
729
|
const limit1e8 = checkedMemo.limit;
|
|
@@ -466,7 +745,16 @@ function thorchainProvider(config = {}) {
|
|
|
466
745
|
params.onRefusal?.(`thorchain: router ${String(q.router)} is not a THORChain router this module trusts on ${params.sourceChain} (THORCHAIN_ROUTERS / trustedRouters)`);
|
|
467
746
|
return null;
|
|
468
747
|
}
|
|
469
|
-
const unconfirmed = await confirmedElsewhere(
|
|
748
|
+
const unconfirmed = await confirmedElsewhere(
|
|
749
|
+
quoteHost,
|
|
750
|
+
quoteHeight,
|
|
751
|
+
params.sourceChain,
|
|
752
|
+
params.destChain,
|
|
753
|
+
q.inbound_address,
|
|
754
|
+
chainId ? q.router : void 0,
|
|
755
|
+
confirmDeadlineAt,
|
|
756
|
+
signal
|
|
757
|
+
);
|
|
470
758
|
if (unconfirmed) {
|
|
471
759
|
params.onRefusal?.(`thorchain: ${unconfirmed}`);
|
|
472
760
|
return null;
|
|
@@ -581,18 +869,42 @@ function thorchainProvider(config = {}) {
|
|
|
581
869
|
if (evmSource && !pinnedRouter(data.sourceChain, data.router)) throw new Error("thorchain: the quote's router is not one this module trusts");
|
|
582
870
|
let inbounds = null;
|
|
583
871
|
let snapshotHost = "";
|
|
872
|
+
let snapshotHeight = 0;
|
|
873
|
+
let confirmDeadlineAt = 0;
|
|
874
|
+
let heightUnavailable = false;
|
|
584
875
|
for (const host of hosts) {
|
|
876
|
+
const deadlineAt = Date.now() + CONFIRM_BUDGET_MS;
|
|
585
877
|
try {
|
|
586
|
-
|
|
878
|
+
const snapshot = await nodeSnapshot(host);
|
|
879
|
+
if ("heightError" in snapshot) {
|
|
880
|
+
heightUnavailable = true;
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
inbounds = snapshot.inbounds;
|
|
884
|
+
snapshotHeight = snapshot.height;
|
|
587
885
|
snapshotHost = host;
|
|
886
|
+
confirmDeadlineAt = deadlineAt;
|
|
588
887
|
break;
|
|
589
888
|
} catch (error) {
|
|
889
|
+
inbounds = null;
|
|
590
890
|
if (!(error instanceof HostUnavailable)) throw error;
|
|
591
891
|
}
|
|
592
892
|
}
|
|
593
|
-
if (!inbounds)
|
|
893
|
+
if (!inbounds) {
|
|
894
|
+
throw new Error(
|
|
895
|
+
heightUnavailable ? "thorchain: cannot read THORChain height (/thorchain/lastblock) before broadcast from any node that answered the inbound addresses" : "thorchain: cannot refresh inbound addresses before broadcast"
|
|
896
|
+
);
|
|
897
|
+
}
|
|
594
898
|
const confirm = async () => {
|
|
595
|
-
const unconfirmed = await confirmedElsewhere(
|
|
899
|
+
const unconfirmed = await confirmedElsewhere(
|
|
900
|
+
snapshotHost,
|
|
901
|
+
snapshotHeight,
|
|
902
|
+
data.sourceChain,
|
|
903
|
+
data.destChain,
|
|
904
|
+
data.inboundAddress,
|
|
905
|
+
evmSource ? data.router : void 0,
|
|
906
|
+
confirmDeadlineAt
|
|
907
|
+
);
|
|
596
908
|
if (unconfirmed) throw new Error(`thorchain: ${unconfirmed}`);
|
|
597
909
|
};
|
|
598
910
|
const src = inbounds.find((entry) => entry.chain === THOR_CHAIN[data.sourceChain]);
|