@anywalletkit/protocol-swap2-lifi 0.7.0 → 0.8.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 +38 -2
- package/dist/{chunk-HJRWHGMX.js → chunk-G26GE46T.js} +410 -115
- package/dist/chunk-G26GE46T.js.map +1 -0
- package/dist/{chunk-D42KNYLU.js → chunk-TCPUWYDL.js} +2 -2
- package/dist/index.cjs +409 -114
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/protocol.cjs +409 -114
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.js +2 -2
- package/dist/provider.cjs +409 -114
- package/dist/provider.cjs.map +1 -1
- package/dist/provider.d.cts +3 -1
- package/dist/provider.d.ts +3 -1
- package/dist/provider.js +1 -1
- package/package.json +9 -7
- package/dist/chunk-HJRWHGMX.js.map +0 -1
- /package/dist/{chunk-D42KNYLU.js.map → chunk-TCPUWYDL.js.map} +0 -0
|
@@ -6,6 +6,76 @@ import {
|
|
|
6
6
|
describeLifiActivity
|
|
7
7
|
} from "./chunk-EU7XQVZQ.js";
|
|
8
8
|
|
|
9
|
+
// src/solana-address.ts
|
|
10
|
+
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
11
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
12
|
+
var ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
13
|
+
var BASE58 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
14
|
+
var SOLANA_TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
15
|
+
var ASSOCIATED_TOKEN_PROGRAM = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
16
|
+
var PDA_MARKER = Uint8Array.from("ProgramDerivedAddress", (c) => c.charCodeAt(0));
|
|
17
|
+
function base58(bytes) {
|
|
18
|
+
let n = 0n;
|
|
19
|
+
for (const b of bytes) n = n << 8n | BigInt(b);
|
|
20
|
+
let out = "";
|
|
21
|
+
while (n > 0n) {
|
|
22
|
+
out = ALPHABET[Number(n % 58n)] + out;
|
|
23
|
+
n /= 58n;
|
|
24
|
+
}
|
|
25
|
+
for (const b of bytes) {
|
|
26
|
+
if (b !== 0) break;
|
|
27
|
+
out = `1${out}`;
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
function solanaKey(address) {
|
|
32
|
+
if (!BASE58.test(address)) return null;
|
|
33
|
+
let n = 0n;
|
|
34
|
+
for (const ch of address) n = n * 58n + BigInt(ALPHABET.indexOf(ch));
|
|
35
|
+
let zeros = 0;
|
|
36
|
+
while (address[zeros] === "1") zeros++;
|
|
37
|
+
const body = [];
|
|
38
|
+
while (n > 0n) {
|
|
39
|
+
body.unshift(Number(n & 0xffn));
|
|
40
|
+
n >>= 8n;
|
|
41
|
+
}
|
|
42
|
+
if (zeros + body.length !== 32) return null;
|
|
43
|
+
const out = new Uint8Array(32);
|
|
44
|
+
out.set(body, zeros);
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
var onCurve = (bytes) => {
|
|
48
|
+
try {
|
|
49
|
+
ed25519.Point.fromBytes(bytes);
|
|
50
|
+
return true;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
function programAddress(seeds, program) {
|
|
56
|
+
for (let bump = 255; bump >= 0; bump--) {
|
|
57
|
+
const parts = [...seeds, Uint8Array.of(bump), program, PDA_MARKER];
|
|
58
|
+
const buf = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
59
|
+
let at = 0;
|
|
60
|
+
for (const p of parts) {
|
|
61
|
+
buf.set(p, at);
|
|
62
|
+
at += p.length;
|
|
63
|
+
}
|
|
64
|
+
const hash = sha256(buf);
|
|
65
|
+
if (!onCurve(hash)) return hash;
|
|
66
|
+
}
|
|
67
|
+
throw new Error("no program address for these seeds");
|
|
68
|
+
}
|
|
69
|
+
function associatedTokenAccount(owner, mint, tokenProgram = solanaKey(SOLANA_TOKEN_PROGRAM)) {
|
|
70
|
+
return programAddress([owner, tokenProgram, mint], solanaKey(ASSOCIATED_TOKEN_PROGRAM));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/refusals.ts
|
|
74
|
+
import { refusalNotes } from "@anywalletkit/protocol-swap2-core/errors";
|
|
75
|
+
var notes = refusalNotes();
|
|
76
|
+
var note = notes.note;
|
|
77
|
+
var reasonOf = notes.reasonOf;
|
|
78
|
+
|
|
9
79
|
// src/calldata.ts
|
|
10
80
|
var GENERIC_SWAPS = {
|
|
11
81
|
"0x4630a0d8": { single: false },
|
|
@@ -225,18 +295,18 @@ function swapsAt(r, base, head) {
|
|
|
225
295
|
function checkSteps(steps, e, finalAsset) {
|
|
226
296
|
const first = steps[0];
|
|
227
297
|
const last = steps[steps.length - 1];
|
|
228
|
-
if (!same(first.sendingAssetId, e.fromToken)) return `spends ${first.sendingAssetId}, not ${e.fromToken}
|
|
229
|
-
if (first.fromAmount === 0n || first.fromAmount > e.amountIn) return `swaps ${first.fromAmount}, not the ${e.amountIn} spent
|
|
298
|
+
if (!same(first.sendingAssetId, e.fromToken)) return note(`spends ${first.sendingAssetId}, not ${e.fromToken}`, "policy_refused");
|
|
299
|
+
if (first.fromAmount === 0n || first.fromAmount > e.amountIn) return note(`swaps ${first.fromAmount}, not the ${e.amountIn} spent`, "policy_refused");
|
|
230
300
|
let deposited = 0n;
|
|
231
301
|
for (let i = 0; i < steps.length; i++) {
|
|
232
302
|
const step = steps[i];
|
|
233
|
-
if (i > 0 && !same(step.sendingAssetId, steps[i - 1].receivingAssetId)) return `step ${i} spends ${step.sendingAssetId}, which step ${i - 1} does not deliver
|
|
303
|
+
if (i > 0 && !same(step.sendingAssetId, steps[i - 1].receivingAssetId)) return note(`step ${i} spends ${step.sendingAssetId}, which step ${i - 1} does not deliver`, "policy_refused");
|
|
234
304
|
if (!step.requiresDeposit) continue;
|
|
235
|
-
if (!same(step.sendingAssetId, e.fromToken)) return `step ${i} pulls ${step.sendingAssetId} from the wallet, not the input ${e.fromToken}
|
|
305
|
+
if (!same(step.sendingAssetId, e.fromToken)) return note(`step ${i} pulls ${step.sendingAssetId} from the wallet, not the input ${e.fromToken}`, "policy_refused");
|
|
236
306
|
deposited += step.fromAmount;
|
|
237
307
|
}
|
|
238
|
-
if (deposited > e.amountIn) return `the steps pull ${deposited} from the wallet, more than the ${e.amountIn} spent
|
|
239
|
-
if (!same(last.receivingAssetId, finalAsset)) return `delivers ${last.receivingAssetId}, not ${finalAsset}
|
|
308
|
+
if (deposited > e.amountIn) return note(`the steps pull ${deposited} from the wallet, more than the ${e.amountIn} spent`, "policy_refused");
|
|
309
|
+
if (!same(last.receivingAssetId, finalAsset)) return note(`delivers ${last.receivingAssetId}, not ${finalAsset}`, "policy_refused");
|
|
240
310
|
return null;
|
|
241
311
|
}
|
|
242
312
|
var MAYAN_REFUND_AT = {
|
|
@@ -277,32 +347,32 @@ function acrossSwapRefunds(r, at) {
|
|
|
277
347
|
}
|
|
278
348
|
var ERC20_TRANSFER = "0xa9059cbb";
|
|
279
349
|
function ecoDelivery(r, at, e) {
|
|
280
|
-
if (kindOf(e.toToken) === "native") return "an Eco route to the native token cannot be checked here";
|
|
350
|
+
if (kindOf(e.toToken) === "native") return note("an Eco route to the native token cannot be checked here", "recipient_unverifiable");
|
|
281
351
|
const destination = r.word(at + 32 * 3);
|
|
282
|
-
if (destination !== e.destChainId) return `the Eco intent executes on chain ${destination}, not the destination ${e.destChainId} \u2014 a route whose delivery cannot be checked here
|
|
352
|
+
if (destination !== e.destChainId) return note(`the Eco intent executes on chain ${destination}, not the destination ${e.destChainId} \u2014 a route whose delivery cannot be checked here`, "recipient_unverifiable");
|
|
283
353
|
const route = r.bytes(at, at + 32 * 4);
|
|
284
354
|
const tuple = route.small(0);
|
|
285
355
|
const calls = tuple + route.small(tuple + 32 * 5);
|
|
286
356
|
const n = route.small(calls);
|
|
287
|
-
if (n === 0) return "the Eco route makes no calls on the destination chain";
|
|
288
|
-
if (n > 16) return `the Eco route makes ${n} calls on the destination chain, more than the 16 read here
|
|
357
|
+
if (n === 0) return note("the Eco route makes no calls on the destination chain", "recipient_unverifiable");
|
|
358
|
+
if (n > 16) return note(`the Eco route makes ${n} calls on the destination chain, more than the 16 read here`, "recipient_unverifiable");
|
|
289
359
|
const last = calls + 32 + route.small(calls + 32 + 32 * (n - 1));
|
|
290
360
|
const target = route.address(last);
|
|
291
361
|
const data = route.bytes(last, last + 32);
|
|
292
|
-
if (route.word(last + 64) !== 0n || data.length !== 68 || data.raw(0, 4) !== ERC20_TRANSFER) return "the Eco route does not end in a token transfer";
|
|
362
|
+
if (route.word(last + 64) !== 0n || data.length !== 68 || data.raw(0, 4) !== ERC20_TRANSFER) return note("the Eco route does not end in a token transfer", "recipient_unverifiable");
|
|
293
363
|
const to = data.address(4);
|
|
294
364
|
const amount = data.word(36);
|
|
295
|
-
if (!same(target, e.toToken)) return `the Eco route delivers ${target}, not ${e.toToken}
|
|
296
|
-
if (!same(to, e.receiver)) return `the Eco route pays ${to}, not ${e.receiver}
|
|
297
|
-
if (amount < e.minOut) return `the Eco route delivers ${amount}, below the ${e.minOut} shown
|
|
365
|
+
if (!same(target, e.toToken)) return note(`the Eco route delivers ${target}, not ${e.toToken}`, "policy_refused");
|
|
366
|
+
if (!same(to, e.receiver)) return note(`the Eco route pays ${to}, not ${e.receiver}`, "recipient_mismatch");
|
|
367
|
+
if (amount < e.minOut) return note(`the Eco route delivers ${amount}, below the ${e.minOut} shown`, "price_check_failed");
|
|
298
368
|
return null;
|
|
299
369
|
}
|
|
300
370
|
function m0Delivery(r, at, e) {
|
|
301
|
-
if (kindOf(e.toToken) === "native") return "an M0 order cannot deliver the native token";
|
|
371
|
+
if (kindOf(e.toToken) === "native") return note("an M0 order cannot deliver the native token", "unsupported_pair");
|
|
302
372
|
const tokenOut = r.address(at + 32 * 3);
|
|
303
373
|
const amountOut = r.word(at + 32 * 5);
|
|
304
|
-
if (!same(tokenOut, e.toToken)) return `the M0 order asks for ${tokenOut}, not ${e.toToken}
|
|
305
|
-
if (amountOut < e.minOut) return `the M0 order asks for ${amountOut}, below the ${e.minOut} shown
|
|
374
|
+
if (!same(tokenOut, e.toToken)) return note(`the M0 order asks for ${tokenOut}, not ${e.toToken}`, "policy_refused");
|
|
375
|
+
if (amountOut < e.minOut) return note(`the M0 order asks for ${amountOut}, below the ${e.minOut} shown`, "price_check_failed");
|
|
306
376
|
return null;
|
|
307
377
|
}
|
|
308
378
|
var FACET_DATA = {
|
|
@@ -316,8 +386,9 @@ var FACET_DATA = {
|
|
|
316
386
|
Frax: { inline: 5, refunds: [["refundRecipient", 3]], zeroRefused: true },
|
|
317
387
|
Glacis: { inline: 4, refunds: [["refundAddress", 1]], zeroRefused: true },
|
|
318
388
|
LayerSwap: { refunds: [["refundRecipient", 2]], zeroRefused: true },
|
|
389
|
+
// The escrow order's user. Not reached while LI.FI Intents is refused on every EVM-source route (INTENTS_UNPINNED); kept for when
|
|
390
|
+
// its oracles and settler are pinned.
|
|
319
391
|
LiFiIntentEscrowV2: { refunds: [["depositAndRefundAddress", 2]], zeroRefused: true },
|
|
320
|
-
// the escrow order's user
|
|
321
392
|
M0: { inline: 7, refunds: [["refundRecipient", 1], ["orderOwner", 2]], zeroRefused: true, delivery: m0Delivery },
|
|
322
393
|
// orderOwner gets a cancelled order back
|
|
323
394
|
Mayan: { refunds: [["refundRecipient", 7]], nested: mayanRefunds, zeroRefused: true },
|
|
@@ -335,79 +406,161 @@ function facetDataAt(r, bridge, spec) {
|
|
|
335
406
|
}
|
|
336
407
|
function refundMismatch(r, bridge, e) {
|
|
337
408
|
const spec = FACET_DATA[bridge.facet];
|
|
338
|
-
if (!spec) return NO_REFUND_FIELDS.has(bridge.facet) ? null : `no known refund layout for ${bridge.facet}
|
|
409
|
+
if (!spec) return NO_REFUND_FIELDS.has(bridge.facet) ? null : note(`no known refund layout for ${bridge.facet}`, "recipient_unverifiable");
|
|
339
410
|
const at = facetDataAt(r, bridge, spec);
|
|
340
411
|
const paid = (spec.refunds ?? []).map(([field, word]) => [`${bridge.facet} ${field}`, r.address(at + 32 * word), spec.zeroRefused === true]);
|
|
341
412
|
paid.push(...spec.nested?.(r, at) ?? []);
|
|
342
413
|
for (const [what, to, zeroRefused] of paid) {
|
|
343
414
|
if (zeroRefused && /^0x0{40}$/.test(to)) continue;
|
|
344
|
-
if (!same(to, e.sender) && !same(to, e.receiver)) return `${what} pays ${to}, neither the sender ${e.sender} nor the recipient
|
|
415
|
+
if (!same(to, e.sender) && !same(to, e.receiver)) return note(`${what} pays ${to}, neither the sender ${e.sender} nor the recipient`, "recipient_mismatch");
|
|
345
416
|
}
|
|
346
417
|
return null;
|
|
347
418
|
}
|
|
419
|
+
var NON_EVM_UNREAD = "the recipient on a non-EVM destination sits in bridge-specific data and cannot be checked here";
|
|
420
|
+
var LIFI_SOLANA_CHAIN_ID = 1151111081099710n;
|
|
421
|
+
var NON_EVM_ADDRESS = "0x11f111f111f111f111f111f111f111f111f111f1";
|
|
422
|
+
var SOLANA_USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
423
|
+
var WORMHOLE_SOLANA = 1n;
|
|
424
|
+
var CCTP_DOMAIN_SOLANA = 5n;
|
|
425
|
+
var CHAINFLIP_SOLANA_ASSETS = {
|
|
426
|
+
"11111111111111111111111111111111": 9n,
|
|
427
|
+
[SOLANA_USDC]: 10n,
|
|
428
|
+
Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB: 16n
|
|
429
|
+
};
|
|
430
|
+
var DEPOSIT_FACETS = /* @__PURE__ */ new Set(["NEARIntents", "LayerSwap", "Unit"]);
|
|
431
|
+
var MAYAN_SOLANA = {
|
|
432
|
+
// Swift v2 createOrderWithToken(tokenIn, amountIn, (payloadType, trader, destAddr, destChainId, referrerAddr, tokenOut, minAmountOut, …), customPayload)
|
|
433
|
+
"0xa3a30834": { destAddr: 4, chain: 5, cctp: false, payloadType: [2, 1n], tokenOut: 7 },
|
|
434
|
+
// MCTP createOrder((tokenIn, amountIn, gasDrop, destAddr, destChain, tokenOut, minAmountOut, deadline, redeemFee, referrerAddr, referrerBps))
|
|
435
|
+
"0x1c59b7fc": { destAddr: 3, chain: 4, cctp: false, tokenOut: 5 },
|
|
436
|
+
// MCTP bridgeWithFee(tokenIn, amountIn, redeemFee, gasDrop, destAddr, destDomain, payloadType, customPayload): delivers USDC
|
|
437
|
+
"0x2072197f": { destAddr: 4, chain: 5, cctp: true, payloadType: [6, 1n] },
|
|
438
|
+
// Fast MCTP bridge(tokenIn, amountIn, redeemFee, circleMaxFee, gasDrop, destAddr, destDomain, referrerAddress, referrerBps, payloadType, minFinalityThreshold, customPayload): delivers USDC
|
|
439
|
+
"0xf58b6de8": { destAddr: 5, chain: 6, cctp: true, payloadType: [9, 1n] },
|
|
440
|
+
// Fast MCTP createOrder(tokenIn, amountIn, circleMaxFee, destDomain, minFinalityThreshold, (payloadType 3, destAddr, tokenOut, amountOutMin, …))
|
|
441
|
+
"0x2337e236": { destAddr: 6, chain: 3, cctp: true, payloadType: [5, 3n], tokenOut: 7 }
|
|
442
|
+
};
|
|
443
|
+
var hexOf = (b) => `0x${Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("")}`;
|
|
444
|
+
var keyOf = (word) => base58(Uint8Array.from(word.slice(2).match(/../g) ?? [], (h) => Number.parseInt(h, 16)));
|
|
445
|
+
function mayanToSolana(r, at, t) {
|
|
446
|
+
const declared = r.raw(at, 32);
|
|
447
|
+
if (declared !== t.ownerHex) return note(`Mayan's nonEVMReceiver is ${keyOf(declared)}, not the recipient ${t.e.receiver}`, "recipient_mismatch");
|
|
448
|
+
const p = r.bytes(at, at + 32 * 2);
|
|
449
|
+
const selector = p.raw(0, 4);
|
|
450
|
+
const o = MAYAN_SOLANA[selector];
|
|
451
|
+
if (!o) return note(`the Mayan call ${selector} is an entry point whose Solana recipient cannot be checked here`, "recipient_unverifiable");
|
|
452
|
+
const word = (i) => 4 + 32 * i;
|
|
453
|
+
if (o.payloadType && p.word(word(o.payloadType[0])) !== o.payloadType[1]) return note(`the Mayan call ${selector} carries payload type ${p.word(word(o.payloadType[0]))}, not a plain delivery`, "recipient_unverifiable");
|
|
454
|
+
const dest = p.raw(word(o.destAddr), 32);
|
|
455
|
+
if (dest !== t.ownerHex) return note(`the Mayan order pays ${keyOf(dest)} on Solana, not the recipient ${t.e.receiver}`, "recipient_mismatch");
|
|
456
|
+
const chain = p.word(word(o.chain));
|
|
457
|
+
if (chain !== (o.cctp ? CCTP_DOMAIN_SOLANA : WORMHOLE_SOLANA)) return note(`the Mayan order goes to ${o.cctp ? "CCTP domain" : "Wormhole chain"} ${chain}, not Solana`, "policy_refused");
|
|
458
|
+
if (o.tokenOut === void 0) return t.e.toToken === SOLANA_USDC ? null : note(`a Mayan CCTP transfer delivers USDC, not ${t.e.toToken}`, "policy_refused");
|
|
459
|
+
const token = p.raw(word(o.tokenOut), 32);
|
|
460
|
+
return token === t.mintHex ? null : note(`the Mayan order delivers ${keyOf(token)}, not ${t.e.toToken}`, "policy_refused");
|
|
461
|
+
}
|
|
462
|
+
function polymerToSolana(r, at, t) {
|
|
463
|
+
const declared = r.raw(at + 32 * 2, 32);
|
|
464
|
+
if (declared !== t.ownerHex) return note(`Polymer CCTP's nonEVMReceiver is ${keyOf(declared)}, not the recipient ${t.e.receiver}`, "recipient_mismatch");
|
|
465
|
+
if (t.e.toToken !== SOLANA_USDC) return note(`Polymer CCTP delivers USDC, not ${t.e.toToken}`, "policy_refused");
|
|
466
|
+
const mintTo = r.raw(at + 32 * 3, 32);
|
|
467
|
+
const account = hexOf(associatedTokenAccount(t.owner, t.mint));
|
|
468
|
+
return mintTo === account ? null : note(`the Polymer CCTP transfer mints to ${keyOf(mintTo)}, not the recipient's USDC account ${keyOf(account)}`, "recipient_mismatch");
|
|
469
|
+
}
|
|
470
|
+
function chainflipToSolana(r, at, t) {
|
|
471
|
+
const dst = r.bytes(at, at);
|
|
472
|
+
if (dst.length !== 32) return note(`Chainflip pays a ${dst.length}-byte address on Solana, not the recipient ${t.e.receiver}`, "recipient_mismatch");
|
|
473
|
+
if (dst.raw(0, 32) !== t.ownerHex) return note(`Chainflip pays ${keyOf(dst.raw(0, 32))} on Solana, not the recipient ${t.e.receiver}`, "recipient_mismatch");
|
|
474
|
+
const asset = r.word(at + 32);
|
|
475
|
+
const want = CHAINFLIP_SOLANA_ASSETS[t.e.toToken];
|
|
476
|
+
return want !== void 0 && asset === want ? null : note(`Chainflip delivers its asset ${asset}, not ${t.e.toToken}`, "policy_refused");
|
|
477
|
+
}
|
|
478
|
+
var SOLANA_DELIVERY = {
|
|
479
|
+
Mayan: mayanToSolana,
|
|
480
|
+
PolymerCCTP: polymerToSolana,
|
|
481
|
+
Chainflip: chainflipToSolana
|
|
482
|
+
};
|
|
483
|
+
var INTENTS_TO_SOLANA = "a LI.FI Intents order to Solana names its own oracles, output settler and output amount, which cannot be checked here \u2014 its escrow could release the input without the delivery";
|
|
484
|
+
var INTENTS_UNPINNED = "a LI.FI Intents order to an EVM chain names its own oracles, output settler and output amount, which cannot be checked here \u2014 its escrow could release the input without the delivery";
|
|
485
|
+
var INTENTS_FACET = "LiFiIntentEscrowV2";
|
|
486
|
+
function solanaRecipientMismatch(r, bridge, receiver, e) {
|
|
487
|
+
if (e.destChainId !== LIFI_SOLANA_CHAIN_ID) return note(NON_EVM_UNREAD, "recipient_unverifiable");
|
|
488
|
+
if (!same(receiver, NON_EVM_ADDRESS)) return note(`BridgeData pays ${receiver}, not LI.FI's non-EVM placeholder ${NON_EVM_ADDRESS}, on a Solana route`, "recipient_mismatch");
|
|
489
|
+
if (DEPOSIT_FACETS.has(bridge.facet)) return note(`${bridge.facet} pays a deposit address whose off-chain order names the Solana recipient \u2014 the transaction does not bind it`, "recipient_unverifiable");
|
|
490
|
+
if (bridge.facet === INTENTS_FACET) return note(INTENTS_TO_SOLANA, "recipient_unverifiable");
|
|
491
|
+
const read = SOLANA_DELIVERY[bridge.facet];
|
|
492
|
+
if (!read) return note(NON_EVM_UNREAD, "recipient_unverifiable");
|
|
493
|
+
const owner = solanaKey(e.receiver);
|
|
494
|
+
if (!owner) return note(`the recipient ${e.receiver} is not a Solana address`, "recipient_mismatch");
|
|
495
|
+
const mint = solanaKey(e.toToken);
|
|
496
|
+
if (!mint) return note(`the destination token ${e.toToken} is not a Solana mint`, "unsupported_pair");
|
|
497
|
+
return read(r, facetDataAt(r, bridge, FACET_DATA[bridge.facet]), { owner, ownerHex: hexOf(owner), mint, mintHex: hexOf(mint), e });
|
|
498
|
+
}
|
|
348
499
|
function diamondCallMismatch(data, e) {
|
|
349
500
|
let r;
|
|
350
501
|
try {
|
|
351
502
|
r = new Reader(data);
|
|
352
503
|
} catch {
|
|
353
|
-
return "the calldata is not hex";
|
|
504
|
+
return note("the calldata is not hex", "upstream_unavailable");
|
|
354
505
|
}
|
|
355
|
-
if (r.length < 4 + 32 * 2) return "the calldata is too short";
|
|
506
|
+
if (r.length < 4 + 32 * 2) return note("the calldata is too short", "upstream_unavailable");
|
|
356
507
|
const selector = data.slice(0, 10).toLowerCase();
|
|
357
508
|
const generic = GENERIC_SWAPS[selector];
|
|
358
509
|
try {
|
|
359
510
|
if (generic) {
|
|
360
|
-
if (!e.sameChain) return "a same-chain swap call on a cross-chain route";
|
|
511
|
+
if (!e.sameChain) return note("a same-chain swap call on a cross-chain route", "policy_refused");
|
|
361
512
|
const receiver2 = r.address(4 + 96);
|
|
362
513
|
const minAmount2 = r.word(4 + 128);
|
|
363
514
|
const steps = generic.single ? [swapAt(r, 4 + r.small(4 + 160))] : swapsAt(r, 4, 4 + 160);
|
|
364
|
-
if (!e.evmDestination || !same(receiver2, e.receiver)) return `pays ${receiver2}, not ${e.receiver}
|
|
365
|
-
if (minAmount2 < e.minOut) return `accepts ${minAmount2}, below the ${e.minOut} shown
|
|
366
|
-
if (generic.from && kindOf(e.fromToken) !== generic.from) return `${selector} swaps from ${generic.from === "native" ? "the native coin" : "a token"}, but the input is ${kindOf(e.fromToken) === "native" ? "the native coin" : "a token"}
|
|
515
|
+
if (!e.evmDestination || !same(receiver2, e.receiver)) return note(`pays ${receiver2}, not ${e.receiver}`, "recipient_mismatch");
|
|
516
|
+
if (minAmount2 < e.minOut) return note(`accepts ${minAmount2}, below the ${e.minOut} shown`, "price_check_failed");
|
|
517
|
+
if (generic.from && kindOf(e.fromToken) !== generic.from) return note(`${selector} swaps from ${generic.from === "native" ? "the native coin" : "a token"}, but the input is ${kindOf(e.fromToken) === "native" ? "the native coin" : "a token"}`, "policy_refused");
|
|
367
518
|
if (generic.to && (kindOf(e.toToken) !== generic.to || kindOf(steps[steps.length - 1].receivingAssetId) !== generic.to)) {
|
|
368
|
-
return `${selector} delivers ${generic.to === "native" ? "the native coin" : "a token"}, but the output is ${kindOf(e.toToken) === "native" ? "the native coin" : "a token"} \u2014 its minimum would be counted in the wrong asset
|
|
519
|
+
return note(`${selector} delivers ${generic.to === "native" ? "the native coin" : "a token"}, but the output is ${kindOf(e.toToken) === "native" ? "the native coin" : "a token"} \u2014 its minimum would be counted in the wrong asset`, "policy_refused");
|
|
369
520
|
}
|
|
370
521
|
return checkSteps(steps, e, e.toToken);
|
|
371
522
|
}
|
|
372
523
|
const unbound = UNBOUND_BRIDGES[selector];
|
|
373
|
-
if (unbound) return `${unbound} delivers to a recipient the transaction does not bind (an off-chain order or unchecked facet data)
|
|
524
|
+
if (unbound) return note(`${unbound} delivers to a recipient the transaction does not bind (an off-chain order or unchecked facet data)`, "recipient_unverifiable");
|
|
374
525
|
const bridge = BRIDGE_CALLS[selector];
|
|
375
|
-
if (!bridge) return `unknown Diamond call ${selector}
|
|
526
|
+
if (!bridge) return note(`unknown Diamond call ${selector}`, "policy_refused");
|
|
527
|
+
if (bridge.facet === INTENTS_FACET && e.evmDestination) return note(INTENTS_UNPINNED, "recipient_unverifiable");
|
|
376
528
|
const head = r.small(4);
|
|
377
|
-
if (head < (bridge.sourceSwaps ? 96 : 64) || head % 32 !== 0 || head > 32 * 32) return `unknown Diamond call ${selector}
|
|
529
|
+
if (head < (bridge.sourceSwaps ? 96 : 64) || head % 32 !== 0 || head > 32 * 32) return note(`unknown Diamond call ${selector}`, "policy_refused");
|
|
378
530
|
const at = 4 + head;
|
|
379
|
-
if (r.small(at + 32) !== 320) return `unknown Diamond call ${selector}
|
|
531
|
+
if (r.small(at + 32) !== 320) return note(`unknown Diamond call ${selector}`, "policy_refused");
|
|
380
532
|
const sendingAssetId = r.address(at + 128);
|
|
381
533
|
const receiver = r.address(at + 160);
|
|
382
534
|
const minAmount = r.word(at + 192);
|
|
383
535
|
const destinationChainId = r.word(at + 224);
|
|
384
536
|
const hasSourceSwaps = r.bool(at + 256);
|
|
385
537
|
const hasDestinationCall = r.bool(at + 288);
|
|
386
|
-
if (e.sameChain) return "a bridge call on a same-chain route";
|
|
387
|
-
if (destinationChainId !== e.destChainId) return `bridges to chain ${destinationChainId}, not ${e.destChainId}
|
|
388
|
-
if (hasDestinationCall) return "the route runs a call on the destination chain, whose recipient cannot be checked here";
|
|
389
|
-
if (!e.evmDestination)
|
|
390
|
-
|
|
391
|
-
|
|
538
|
+
if (e.sameChain) return note("a bridge call on a same-chain route", "policy_refused");
|
|
539
|
+
if (destinationChainId !== e.destChainId) return note(`bridges to chain ${destinationChainId}, not ${e.destChainId}`, "policy_refused");
|
|
540
|
+
if (hasDestinationCall) return note("the route runs a call on the destination chain, whose recipient cannot be checked here", "recipient_unverifiable");
|
|
541
|
+
if (!e.evmDestination) {
|
|
542
|
+
const why = solanaRecipientMismatch(r, bridge, receiver, e);
|
|
543
|
+
if (why) return why;
|
|
544
|
+
} else if (!same(receiver, e.receiver)) return note(`pays ${receiver}, not ${e.receiver}`, "recipient_mismatch");
|
|
545
|
+
if (hasSourceSwaps !== bridge.sourceSwaps) return note(`unknown Diamond call ${selector}`, "policy_refused");
|
|
392
546
|
const refund = refundMismatch(r, bridge, e);
|
|
393
547
|
if (refund) return refund;
|
|
394
548
|
const spec = FACET_DATA[bridge.facet];
|
|
395
549
|
const delivery = spec?.delivery?.(r, facetDataAt(r, bridge, spec), e);
|
|
396
550
|
if (delivery) return delivery;
|
|
397
551
|
if (hasSourceSwaps) {
|
|
398
|
-
if (minAmount === 0n) return "bridges nothing after the source swaps";
|
|
552
|
+
if (minAmount === 0n) return note("bridges nothing after the source swaps", "policy_refused");
|
|
399
553
|
return checkSteps(swapsAt(r, 4, 4 + 32), e, sendingAssetId);
|
|
400
554
|
}
|
|
401
|
-
if (minAmount > e.amountIn || minAmount * 10000n < e.amountIn * (10000n - BRIDGE_INPUT_FEE_MAX_BPS)) return `bridges ${minAmount}, not the ${e.amountIn} spent
|
|
402
|
-
return same(sendingAssetId, e.fromToken) ? null : `bridges ${sendingAssetId}, not ${e.fromToken}
|
|
555
|
+
if (minAmount > e.amountIn || minAmount * 10000n < e.amountIn * (10000n - BRIDGE_INPUT_FEE_MAX_BPS)) return note(`bridges ${minAmount}, not the ${e.amountIn} spent`, "policy_refused");
|
|
556
|
+
return same(sendingAssetId, e.fromToken) ? null : note(`bridges ${sendingAssetId}, not ${e.fromToken}`, "policy_refused");
|
|
403
557
|
} catch (err) {
|
|
404
|
-
return `the Diamond calldata does not decode (${err instanceof Error ? err.message : String(err)})
|
|
558
|
+
return note(`the Diamond calldata does not decode (${err instanceof Error ? err.message : String(err)})`, "policy_refused");
|
|
405
559
|
}
|
|
406
560
|
}
|
|
407
561
|
|
|
408
562
|
// src/solana.ts
|
|
409
563
|
import { base64Bytes } from "@anywalletkit/protocol-swap2-core/util";
|
|
410
|
-
var ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
411
564
|
var MAYAN_ENTRY_PROGRAMS = /* @__PURE__ */ new Set([
|
|
412
565
|
"D8C8iW6zmoKg5TRr8nQ7h14TMWqQX8FiBdj2ju5MF3wa",
|
|
413
566
|
// CPI_PROXY_PROGRAM_ID
|
|
@@ -422,20 +575,6 @@ var MAYAN_ENTRY_PROGRAMS = /* @__PURE__ */ new Set([
|
|
|
422
575
|
]);
|
|
423
576
|
var SYSTEM_PROGRAM = "11111111111111111111111111111111";
|
|
424
577
|
var LIFI_SOLANA_FEE_WALLET = "34FKjAdVcTax2DHqV2XnbXa9J3zmyKcFuFKWbcmgxjgm";
|
|
425
|
-
function base58(bytes) {
|
|
426
|
-
let n = 0n;
|
|
427
|
-
for (const b of bytes) n = n << 8n | BigInt(b);
|
|
428
|
-
let out = "";
|
|
429
|
-
while (n > 0n) {
|
|
430
|
-
out = ALPHABET[Number(n % 58n)] + out;
|
|
431
|
-
n /= 58n;
|
|
432
|
-
}
|
|
433
|
-
for (const b of bytes) {
|
|
434
|
-
if (b !== 0) break;
|
|
435
|
-
out = `1${out}`;
|
|
436
|
-
}
|
|
437
|
-
return out;
|
|
438
|
-
}
|
|
439
578
|
var Cursor = class {
|
|
440
579
|
constructor(b) {
|
|
441
580
|
this.b = b;
|
|
@@ -465,7 +604,7 @@ var Cursor = class {
|
|
|
465
604
|
};
|
|
466
605
|
function solanaBridgeTxMismatch(base64, sender) {
|
|
467
606
|
const bytes = base64Bytes(base64);
|
|
468
|
-
if (!bytes) return "the Solana transaction is not base64";
|
|
607
|
+
if (!bytes) return note("the Solana transaction is not base64", "upstream_unavailable");
|
|
469
608
|
const c = new Cursor(bytes);
|
|
470
609
|
try {
|
|
471
610
|
c.bytes(64 * c.short());
|
|
@@ -473,7 +612,7 @@ function solanaBridgeTxMismatch(base64, sender) {
|
|
|
473
612
|
if ((first & 128) === 0) c.at--;
|
|
474
613
|
c.bytes(3);
|
|
475
614
|
const keys = Array.from({ length: c.short() }, () => base58(c.bytes(32)));
|
|
476
|
-
if (keys[0] !== sender) return `the Solana transaction is paid by ${keys[0] ?? "nobody"}, not the sender ${sender}
|
|
615
|
+
if (keys[0] !== sender) return note(`the Solana transaction is paid by ${keys[0] ?? "nobody"}, not the sender ${sender}`, "policy_refused");
|
|
477
616
|
c.bytes(32);
|
|
478
617
|
let mayan = false;
|
|
479
618
|
const payees = [];
|
|
@@ -488,15 +627,16 @@ function solanaBridgeTxMismatch(base64, sender) {
|
|
|
488
627
|
if (to !== void 0 && to !== sender && to !== LIFI_SOLANA_FEE_WALLET) payees.push(to);
|
|
489
628
|
}
|
|
490
629
|
}
|
|
491
|
-
return mayan && payees.length ? `the Solana transaction transfers SOL to ${payees[0]}, which is neither the sender nor LI.FI's fee wallet
|
|
630
|
+
return mayan && payees.length ? note(`the Solana transaction transfers SOL to ${payees[0]}, which is neither the sender nor LI.FI's fee wallet`, "recipient_mismatch") : null;
|
|
492
631
|
} catch (err) {
|
|
493
|
-
return `the Solana transaction does not decode (${err instanceof Error ? err.message : String(err)})
|
|
632
|
+
return note(`the Solana transaction does not decode (${err instanceof Error ? err.message : String(err)})`, "policy_refused");
|
|
494
633
|
}
|
|
495
634
|
}
|
|
496
635
|
|
|
497
636
|
// src/provider.ts
|
|
498
637
|
import { timeoutSignal, sourceAssetOf } from "@anywalletkit/protocol-swap2-core/util";
|
|
499
638
|
import { slippageFloor } from "@anywalletkit/protocol-swap2-core/validate";
|
|
639
|
+
import { httpRefusal, parseRetryAfter, refusal } from "@anywalletkit/protocol-swap2-core/errors";
|
|
500
640
|
import { bigintToNumber } from "@anywalletkit/core";
|
|
501
641
|
var DEFAULT_LIFI_BASE = "https://li.quest/v1";
|
|
502
642
|
var LIFI_CHAIN = {
|
|
@@ -537,7 +677,8 @@ var LIFI_CHAIN = {
|
|
|
537
677
|
"eip155:2818": "2818",
|
|
538
678
|
"eip155:14": "14",
|
|
539
679
|
// 2026-11-10: Elysium (Kinetiq; li.quest/v1/chains id 99901, key "ely"). LI.FI's bridges there are Relay (refused
|
|
540
|
-
// below: UNBOUND_TOOLS) and LI.FI Intents
|
|
680
|
+
// below: UNBOUND_TOOLS) and LI.FI Intents (refused too: UNPINNED_TOOLS), so it has no cross-chain LI.FI route for now;
|
|
681
|
+
// same-chain swaps go through GenericSwapFacetV3 (KyberSwap, Nordstern).
|
|
541
682
|
"eip155:99901": "99901",
|
|
542
683
|
[LIFI_SOLANA_CAIP2]: "1151111081099710"
|
|
543
684
|
};
|
|
@@ -547,12 +688,12 @@ var SOLANA_NATIVE = "11111111111111111111111111111111";
|
|
|
547
688
|
var EVM_PLACEHOLDER = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
|
|
548
689
|
var SOLANA_PLACEHOLDER = "HAgk14JpMQLgt6rVgv7cBQFJWFto5Dqxi472uT3DKpqk";
|
|
549
690
|
var isSolana = (chain) => chain === LIFI_SOLANA_CAIP2;
|
|
550
|
-
var
|
|
691
|
+
var BASE582 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
551
692
|
var BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
552
693
|
var SAME_CHAIN_MAX_SECONDS = 90;
|
|
553
694
|
var MAX_INPUT_FEE_BPS = 300;
|
|
554
695
|
function sameChainAddress(chain, actual, expected) {
|
|
555
|
-
if (isSolana(chain)) return !!actual &&
|
|
696
|
+
if (isSolana(chain)) return !!actual && BASE582.test(actual) && actual === expected;
|
|
556
697
|
return sameAddress(actual, expected);
|
|
557
698
|
}
|
|
558
699
|
function nativeFor(chain, token) {
|
|
@@ -641,7 +782,16 @@ function headers(key) {
|
|
|
641
782
|
}
|
|
642
783
|
var TOOL_KEY = /^[A-Za-z0-9_-]{1,64}$/;
|
|
643
784
|
var UNBOUND_TOOLS = Object.freeze(["relaydepository", "garden", "squid", "symbiosis"]);
|
|
644
|
-
|
|
785
|
+
var UNPINNED_TOOLS = Object.freeze(["lifiIntents"]);
|
|
786
|
+
var REFUSED_EVM_TOOLS = Object.freeze([...UNBOUND_TOOLS, ...UNPINNED_TOOLS]);
|
|
787
|
+
var SOLANA_DESTINATION_TOOLS = Object.freeze(["mayan", "mayanMCTP", "mayanFastMCTP", "polymer", "polymerStandard", "chainflip"]);
|
|
788
|
+
var SMART_DEPOSITS_TOOL = /^smartdeposits?$/i;
|
|
789
|
+
var dryRecipient = (params, fromAddress) => !isSolana(params.sourceChain) && isSolana(params.destChain) ? SOLANA_PLACEHOLDER : fromAddress;
|
|
790
|
+
function placeholderIn(params) {
|
|
791
|
+
if (!params.refundAddress) return "sender";
|
|
792
|
+
return !params.destAddress && dryRecipient(params, params.refundAddress) !== params.refundAddress ? "recipient" : null;
|
|
793
|
+
}
|
|
794
|
+
function routeOptions(body, excluded, allowed) {
|
|
645
795
|
const routes = body?.routes;
|
|
646
796
|
if (!Array.isArray(routes)) return [];
|
|
647
797
|
const out = [];
|
|
@@ -651,7 +801,7 @@ function routeOptions(body, excluded) {
|
|
|
651
801
|
const cross = Array.isArray(step.includedSteps) ? step.includedSteps.find((i) => i?.type === "cross")?.tool : void 0;
|
|
652
802
|
const tool = typeof cross === "string" ? cross : step.tool;
|
|
653
803
|
const eta = step.estimate?.executionDuration;
|
|
654
|
-
if (typeof tool !== "string" || !TOOL_KEY.test(tool) || excluded.includes(tool) || typeof eta !== "number" || !Number.isFinite(eta) || eta < 0) continue;
|
|
804
|
+
if (typeof tool !== "string" || !TOOL_KEY.test(tool) || excluded.includes(tool) || allowed && !allowed.includes(tool) || typeof eta !== "number" || !Number.isFinite(eta) || eta < 0) continue;
|
|
655
805
|
if (typeof r.toAmount !== "string" || typeof r.toAmountMin !== "string" || !/^\d{1,60}$/.test(r.toAmount) || !/^\d{1,60}$/.test(r.toAmountMin)) continue;
|
|
656
806
|
out.push({
|
|
657
807
|
tool,
|
|
@@ -664,6 +814,92 @@ function routeOptions(body, excluded) {
|
|
|
664
814
|
}
|
|
665
815
|
return out;
|
|
666
816
|
}
|
|
817
|
+
var LIFI_TOOL_REASONS = {
|
|
818
|
+
NO_POSSIBLE_ROUTE: "no_route",
|
|
819
|
+
INSUFFICIENT_LIQUIDITY: "insufficient_liquidity",
|
|
820
|
+
TOOL_TIMEOUT: "upstream_unavailable",
|
|
821
|
+
RPC_ERROR: "upstream_unavailable",
|
|
822
|
+
AMOUNT_TOO_LOW: "amount_too_small",
|
|
823
|
+
AMOUNT_TOO_HIGH: "amount_too_large",
|
|
824
|
+
FEES_HIGHER_THAN_AMOUNT: "amount_too_small",
|
|
825
|
+
FEES_HGHER_THAN_AMOUNT: "amount_too_small",
|
|
826
|
+
// an older spelling of the same code
|
|
827
|
+
DIFFERENT_RECIPIENT_NOT_SUPPORTED: "unsupported_pair",
|
|
828
|
+
CANNOT_GUARANTEE_MIN_AMOUNT: "price_check_failed"
|
|
829
|
+
};
|
|
830
|
+
var LIFI_ERROR_REASONS = {
|
|
831
|
+
1001: "no_route",
|
|
832
|
+
// FailedToBuildTransactionError: "None of the available routes could successfully generate a tx"
|
|
833
|
+
1002: "no_route",
|
|
834
|
+
// NoQuoteError: "No available quotes for the requested transfer"
|
|
835
|
+
1003: "no_route",
|
|
836
|
+
// NotFoundError
|
|
837
|
+
1005: "rate_limited",
|
|
838
|
+
// RateLimitError
|
|
839
|
+
1006: "upstream_unavailable",
|
|
840
|
+
// ServerError
|
|
841
|
+
1007: "price_check_failed",
|
|
842
|
+
// SlippageError
|
|
843
|
+
1008: "upstream_unavailable",
|
|
844
|
+
// ThirdPartyError
|
|
845
|
+
1009: "upstream_unavailable",
|
|
846
|
+
// TimeoutError
|
|
847
|
+
1010: "key_rejected",
|
|
848
|
+
// UnauthorizedError
|
|
849
|
+
1012: "upstream_unavailable"
|
|
850
|
+
// RpcFailure
|
|
851
|
+
};
|
|
852
|
+
var LIFI_MESSAGE_REASONS = [
|
|
853
|
+
[/no available quotes|unable to find a quote|none of the available routes|no route/i, "no_route"],
|
|
854
|
+
[/rate limit|too many requests/i, "rate_limited"],
|
|
855
|
+
[/api[ -]?key|unauthori[sz]ed/i, "key_rejected"],
|
|
856
|
+
[/insufficient liquidity|not enough liquidity/i, "insufficient_liquidity"],
|
|
857
|
+
[/amount (?:is )?too (?:low|small)|below the minimum/i, "amount_too_small"],
|
|
858
|
+
[/amount (?:is )?too (?:high|large)|above the maximum/i, "amount_too_large"],
|
|
859
|
+
[/not supported|unsupported|unknown token|invalid token/i, "unsupported_pair"],
|
|
860
|
+
[/halted|paused|maintenance/i, "chain_halted"]
|
|
861
|
+
];
|
|
862
|
+
function lifiToolCodes(errors) {
|
|
863
|
+
const out = /* @__PURE__ */ new Set();
|
|
864
|
+
const stack = [errors];
|
|
865
|
+
for (let visited = 0; stack.length > 0 && visited < 256; visited++) {
|
|
866
|
+
const node = stack.pop();
|
|
867
|
+
if (Array.isArray(node)) {
|
|
868
|
+
stack.push(...node.slice(0, 64));
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
if (!node || typeof node !== "object") continue;
|
|
872
|
+
const code = node.code;
|
|
873
|
+
if (typeof code === "string" && Object.prototype.hasOwnProperty.call(LIFI_TOOL_REASONS, code)) out.add(code);
|
|
874
|
+
for (const [key, value] of Object.entries(node).slice(0, 64)) if (key !== "action" && value && typeof value === "object") stack.push(value);
|
|
875
|
+
}
|
|
876
|
+
return [...out];
|
|
877
|
+
}
|
|
878
|
+
function lifiBodyReason(body, filteredOut) {
|
|
879
|
+
const b = body && typeof body === "object" ? body : {};
|
|
880
|
+
const tools = [...new Set(lifiToolCodes(b.errors).map((code) => LIFI_TOOL_REASONS[code]))];
|
|
881
|
+
if (tools.length === 1) return tools[0];
|
|
882
|
+
if (filteredOut.length > 0 && filteredOut.every((r) => /^ROUTE_SLIPPAGE_FLOOR_UNREACHABLE/.test(r))) return "price_check_failed";
|
|
883
|
+
const byCode = typeof b.code === "number" ? LIFI_ERROR_REASONS[b.code] : void 0;
|
|
884
|
+
if (byCode) return byCode;
|
|
885
|
+
const message = typeof b.message === "string" ? b.message : "";
|
|
886
|
+
return LIFI_MESSAGE_REASONS.find(([pattern]) => pattern.test(message))?.[1];
|
|
887
|
+
}
|
|
888
|
+
var LIFI_TRANSIENT_REASONS = /* @__PURE__ */ new Set(["rate_limited", "insufficient_liquidity", "chain_halted"]);
|
|
889
|
+
function lifiHttpRefusal(status, retryAfter, body, filteredOut) {
|
|
890
|
+
const http = httpRefusal(status, retryAfter);
|
|
891
|
+
if (http && http.reason !== "upstream_unavailable") return http;
|
|
892
|
+
const info = (reason) => {
|
|
893
|
+
if (reason === "rate_limited") {
|
|
894
|
+
const wait = parseRetryAfter(retryAfter);
|
|
895
|
+
return { reason, httpStatus: status, ...wait !== void 0 ? { retryAfterMs: wait } : {} };
|
|
896
|
+
}
|
|
897
|
+
return reason === "upstream_unavailable" || reason === "key_rejected" ? { reason, httpStatus: status } : { reason };
|
|
898
|
+
};
|
|
899
|
+
const byBody = lifiBodyReason(body, filteredOut);
|
|
900
|
+
if (http) return byBody !== void 0 && LIFI_TRANSIENT_REASONS.has(byBody) ? info(byBody) : http;
|
|
901
|
+
return byBody !== void 0 ? info(byBody) : { reason: "no_route" };
|
|
902
|
+
}
|
|
667
903
|
function cleanChoice(choice) {
|
|
668
904
|
if (!choice) return null;
|
|
669
905
|
const allow = (choice.allowBridges ?? []).filter((t) => TOOL_KEY.test(t));
|
|
@@ -704,8 +940,10 @@ function lifiProvider(config = {}) {
|
|
|
704
940
|
return !!LIFI_CHAIN[from.chain] && !!LIFI_CHAIN[to.chain];
|
|
705
941
|
},
|
|
706
942
|
async getQuote(params, signal) {
|
|
707
|
-
const reject = (why) => {
|
|
708
|
-
|
|
943
|
+
const reject = (why, reason) => {
|
|
944
|
+
const info = typeof reason === "string" ? { reason } : reason;
|
|
945
|
+
if (info) params.onRefusal?.(`lifi: ${why}`, info);
|
|
946
|
+
else params.onRefusal?.(`lifi: ${why}`);
|
|
709
947
|
return null;
|
|
710
948
|
};
|
|
711
949
|
if (params.flex) return null;
|
|
@@ -716,13 +954,15 @@ function lifiProvider(config = {}) {
|
|
|
716
954
|
if (!fromToken) return null;
|
|
717
955
|
const solanaSource = isSolana(params.sourceChain);
|
|
718
956
|
const crossChain = params.sourceChain !== params.destChain;
|
|
957
|
+
const evmToSolana = !solanaSource && isSolana(params.destChain);
|
|
719
958
|
const noDestinationCall = crossChain && !solanaSource;
|
|
720
959
|
let routeScope = config.routeWideSlippage === true && !solanaSource && !isSolana(params.destChain);
|
|
721
960
|
const dry = params.dry !== false;
|
|
722
961
|
const fromAddress = params.refundAddress || (dry ? solanaSource ? SOLANA_PLACEHOLDER : EVM_PLACEHOLDER : "");
|
|
723
|
-
const toAddress = params.destAddress || (dry ? fromAddress : "");
|
|
962
|
+
const toAddress = params.destAddress || (dry ? dryRecipient(params, fromAddress) : "");
|
|
724
963
|
if (!fromAddress) return reject("a live quote needs the sender's address");
|
|
725
964
|
if (!toAddress) return reject("a live quote needs a recipient");
|
|
965
|
+
const placeholder = dry ? placeholderIn(params) : null;
|
|
726
966
|
const slippageBps = params.slippageBps ?? defaultSlippageBps;
|
|
727
967
|
let sentSlippage = slippageBps / 1e4;
|
|
728
968
|
let deadline;
|
|
@@ -743,7 +983,7 @@ function lifiProvider(config = {}) {
|
|
|
743
983
|
options: {
|
|
744
984
|
slippage: slippageBps / 1e4,
|
|
745
985
|
...routeScope ? { slippageScope: "route" } : {},
|
|
746
|
-
...solanaSource ? {} : { bridges: { deny:
|
|
986
|
+
...solanaSource ? {} : { bridges: { ...evmToSolana ? { allow: SOLANA_DESTINATION_TOOLS } : {}, deny: REFUSED_EVM_TOOLS } },
|
|
747
987
|
...noDestinationCall ? { allowDestinationCall: false } : {},
|
|
748
988
|
...integrator().trim() ? { integrator: integrator().trim() } : {}
|
|
749
989
|
}
|
|
@@ -751,7 +991,7 @@ function lifiProvider(config = {}) {
|
|
|
751
991
|
signal: signal ?? timeoutSignal(8e3)
|
|
752
992
|
});
|
|
753
993
|
if (res.ok) {
|
|
754
|
-
const options = routeOptions(await res.json(), solanaSource ? [] :
|
|
994
|
+
const options = routeOptions(await res.json(), solanaSource ? [] : REFUSED_EVM_TOOLS, evmToSolana ? SOLANA_DESTINATION_TOOLS : void 0);
|
|
755
995
|
if (options.length) choice = cleanChoice(config.chooseRoute(options, params));
|
|
756
996
|
}
|
|
757
997
|
} catch {
|
|
@@ -773,8 +1013,10 @@ function lifiProvider(config = {}) {
|
|
|
773
1013
|
}
|
|
774
1014
|
if (routeScope) url.searchParams.set("slippageScope", "route");
|
|
775
1015
|
if (noDestinationCall) url.searchParams.set("allowDestinationCall", "false");
|
|
776
|
-
const unbound = crossChain && !solanaSource ?
|
|
777
|
-
|
|
1016
|
+
const unbound = crossChain && !solanaSource ? REFUSED_EVM_TOOLS : [];
|
|
1017
|
+
const allow = (choice?.allowBridges ?? []).filter((b) => solanaSource || !REFUSED_EVM_TOOLS.includes(b));
|
|
1018
|
+
const solanaAllow = evmToSolana ? allow.filter((b) => SOLANA_DESTINATION_TOOLS.includes(b)) : [];
|
|
1019
|
+
for (const t of evmToSolana ? solanaAllow.length ? solanaAllow : SOLANA_DESTINATION_TOOLS : allow) url.searchParams.append("allowBridges", t);
|
|
778
1020
|
for (const t of /* @__PURE__ */ new Set([...choice?.denyBridges ?? [], ...unbound])) url.searchParams.append("denyBridges", t);
|
|
779
1021
|
if (choice?.order) url.searchParams.set("order", choice.order);
|
|
780
1022
|
deadline = signal ? void 0 : timeoutSignal(12e3);
|
|
@@ -793,18 +1035,28 @@ function lifiProvider(config = {}) {
|
|
|
793
1035
|
continue;
|
|
794
1036
|
}
|
|
795
1037
|
const why = reasons.length === 1 ? ` (${reasons[0].slice(0, 200)})` : "";
|
|
796
|
-
params.onRefusal?.(
|
|
1038
|
+
params.onRefusal?.(
|
|
1039
|
+
`lifi: ${typeof body?.message === "string" ? body.message.slice(0, 200) : `HTTP ${res.status}`}${why}`,
|
|
1040
|
+
lifiHttpRefusal(res.status, res.headers?.get?.("retry-after"), body, reasons)
|
|
1041
|
+
);
|
|
797
1042
|
return null;
|
|
798
1043
|
}
|
|
799
1044
|
const q = await res.json();
|
|
800
1045
|
const receivedAt = Date.now();
|
|
1046
|
+
if (evmToSolana) {
|
|
1047
|
+
const steps = q.includedSteps;
|
|
1048
|
+
const tools = [q.tool, ...Array.isArray(steps) ? steps.map((i) => i?.tool) : []];
|
|
1049
|
+
if (tools.some((t) => typeof t === "string" && SMART_DEPOSITS_TOOL.test(t))) {
|
|
1050
|
+
return reject("LI.FI Smart Deposits pay a deposit address whose recipient the transaction does not bind \u2014 refused for a Solana destination", "recipient_unverifiable");
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
801
1053
|
const action = q.action;
|
|
802
1054
|
const est = q.estimate;
|
|
803
1055
|
const tr = q.transactionRequest;
|
|
804
1056
|
const expectedToToken = nativeFor(params.destChain, params.destToken);
|
|
805
1057
|
const expectedFromChain = Number(fromChain);
|
|
806
1058
|
const expectedToChain = Number(toChain);
|
|
807
|
-
if (q.type !== "lifi" || !q.id || !action || !est?.toAmount || !est.fromAmount || !tr) return reject("incomplete quote (no action, estimate or transaction)");
|
|
1059
|
+
if (q.type !== "lifi" || !q.id || !action || !est?.toAmount || !est.fromAmount || !tr) return reject("incomplete quote (no action, estimate or transaction)", "upstream_unavailable");
|
|
808
1060
|
const mismatch = [
|
|
809
1061
|
action.fromChainId !== expectedFromChain && "source chain",
|
|
810
1062
|
action.toChainId !== expectedToChain && "destination chain",
|
|
@@ -814,41 +1066,53 @@ function lifiProvider(config = {}) {
|
|
|
814
1066
|
!sameChainAddress(params.sourceChain, action.fromAddress, fromAddress) && "sender",
|
|
815
1067
|
!sameChainAddress(params.destChain, action.toAddress, toAddress) && "recipient"
|
|
816
1068
|
].filter(Boolean);
|
|
817
|
-
if (mismatch.length)
|
|
1069
|
+
if (mismatch.length) {
|
|
1070
|
+
return reject(
|
|
1071
|
+
`quote does not match the request (${mismatch.join(", ")})`,
|
|
1072
|
+
// LI.FI refunds to the sender it was asked for (fromAddress): either address differing pays or refunds someone else.
|
|
1073
|
+
mismatch.includes("recipient") || mismatch.includes("sender") ? "recipient_mismatch" : "policy_refused"
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
818
1076
|
const bridged = BigInt(est.fromAmount);
|
|
819
1077
|
const spent = BigInt(params.amountIn);
|
|
820
|
-
if (bridged > spent) return reject(`the estimate bridges ${est.fromAmount}, more than the ${params.amountIn} spent
|
|
1078
|
+
if (bridged > spent) return reject(`the estimate bridges ${est.fromAmount}, more than the ${params.amountIn} spent`, "price_check_failed");
|
|
821
1079
|
if ((spent - bridged) * 10000n > spent * BigInt(MAX_INPUT_FEE_BPS)) {
|
|
822
|
-
return reject(`the route takes ${bigintToNumber((spent - bridged) * 10000n / spent) / 100}% of the input as a fee (over ${MAX_INPUT_FEE_BPS / 100}%)
|
|
1080
|
+
return reject(`the route takes ${bigintToNumber((spent - bridged) * 10000n / spent) / 100}% of the input as a fee (over ${MAX_INPUT_FEE_BPS / 100}%)`, "policy_refused");
|
|
823
1081
|
}
|
|
824
1082
|
if (solanaSource) {
|
|
825
|
-
if (!tr.data || tr.data.length < 64 || tr.data.length % 4 !== 0 || !BASE64.test(tr.data)) return reject("the Solana transaction is not base64");
|
|
826
|
-
if (tr.to !== void 0 || tr.value !== void 0 || tr.gasLimit !== void 0) return reject("the Solana transaction carries EVM fields");
|
|
1083
|
+
if (!tr.data || tr.data.length < 64 || tr.data.length % 4 !== 0 || !BASE64.test(tr.data)) return reject("the Solana transaction is not base64", "upstream_unavailable");
|
|
1084
|
+
if (tr.to !== void 0 || tr.value !== void 0 || tr.gasLimit !== void 0) return reject("the Solana transaction carries EVM fields", "upstream_unavailable");
|
|
827
1085
|
if (fromChain !== toChain) {
|
|
828
1086
|
const why = solanaBridgeTxMismatch(tr.data, fromAddress);
|
|
829
|
-
if (why) return reject(why);
|
|
1087
|
+
if (why) return reject(why, reasonOf(why));
|
|
830
1088
|
}
|
|
831
|
-
} else if (!isHexData(tr.data) || !normalizeAddress(tr.to) || !normalizeAddress(tr.from) || tr.chainId !== expectedFromChain || !tr.value || !tr.gasLimit || !trustedAddress(params.sourceChain, tr.to, trustedTransactionTargets) || !sameAddress(tr.from, fromAddress))
|
|
832
|
-
|
|
1089
|
+
} else if (!isHexData(tr.data) || !normalizeAddress(tr.to) || !normalizeAddress(tr.from) || tr.chainId !== expectedFromChain || !tr.value || !tr.gasLimit || !trustedAddress(params.sourceChain, tr.to, trustedTransactionTargets) || !sameAddress(tr.from, fromAddress)) {
|
|
1090
|
+
return reject(
|
|
1091
|
+
"the transaction is malformed or targets an untrusted contract",
|
|
1092
|
+
!isHexData(tr.data) || !normalizeAddress(tr.to) || !normalizeAddress(tr.from) || !tr.value || !tr.gasLimit ? "upstream_unavailable" : !trustedAddress(params.sourceChain, tr.to, trustedTransactionTargets) ? "vault_mismatch" : "policy_refused"
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
if (sentSlippage !== void 0 && (typeof action.slippage !== "number" || Math.abs(action.slippage - sentSlippage) > 1e-12)) return reject("the quote ignored the requested slippage", "price_check_failed");
|
|
833
1096
|
const sourceIsToken = fromToken !== EVM_ZERO && fromToken !== SOLANA_NATIVE;
|
|
834
1097
|
if (solanaSource) {
|
|
835
1098
|
} else if (sourceIsToken) {
|
|
836
|
-
if (!trustedAddress(params.sourceChain, est.approvalAddress, trustedApprovalAddresses)) return reject("the approval target is not a trusted LI.FI contract");
|
|
1099
|
+
if (!trustedAddress(params.sourceChain, est.approvalAddress, trustedApprovalAddresses)) return reject("the approval target is not a trusted LI.FI contract", "vault_mismatch");
|
|
837
1100
|
} else if (est.approvalAddress !== void 0) {
|
|
838
|
-
if (!trustedAddress(params.sourceChain, est.approvalAddress, trustedApprovalAddresses)) return reject("the approval target is not a trusted LI.FI contract");
|
|
1101
|
+
if (!trustedAddress(params.sourceChain, est.approvalAddress, trustedApprovalAddresses)) return reject("the approval target is not a trusted LI.FI contract", "vault_mismatch");
|
|
839
1102
|
}
|
|
840
1103
|
let txRequest;
|
|
1104
|
+
let nativeFee = 0n;
|
|
841
1105
|
if (solanaSource) {
|
|
842
1106
|
txRequest = { serializedTx: tr.data, value: sourceIsToken ? "0" : params.amountIn };
|
|
843
1107
|
} else {
|
|
844
1108
|
try {
|
|
845
|
-
if (BigInt(tr.value) < 0n || BigInt(tr.gasLimit) <= 0n) return reject("the transaction has a negative value or no gas limit");
|
|
1109
|
+
if (BigInt(tr.value) < 0n || BigInt(tr.gasLimit) <= 0n) return reject("the transaction has a negative value or no gas limit", "upstream_unavailable");
|
|
846
1110
|
} catch {
|
|
847
|
-
return reject("the transaction value or gas limit is not a number");
|
|
1111
|
+
return reject("the transaction value or gas limit is not a number", "upstream_unavailable");
|
|
848
1112
|
}
|
|
849
1113
|
const value = BigInt(tr.value);
|
|
850
|
-
|
|
851
|
-
if (!sourceIsToken && value < BigInt(params.amountIn)) return reject("the transaction sends less native coin than the input");
|
|
1114
|
+
nativeFee = sourceIsToken ? value : value > BigInt(params.amountIn) ? value - BigInt(params.amountIn) : 0n;
|
|
1115
|
+
if (!sourceIsToken && value < BigInt(params.amountIn)) return reject("the transaction sends less native coin than the input", "policy_refused");
|
|
852
1116
|
txRequest = {
|
|
853
1117
|
to: tr.to,
|
|
854
1118
|
data: tr.data,
|
|
@@ -860,7 +1124,7 @@ function lifiProvider(config = {}) {
|
|
|
860
1124
|
};
|
|
861
1125
|
}
|
|
862
1126
|
const feeUsd = (est.feeCosts ?? []).reduce((a, f) => a + Number(f.amountUSD ?? 0), 0);
|
|
863
|
-
if (!est.toAmountMin) return reject("the quote has no minimum output");
|
|
1127
|
+
if (!est.toAmountMin) return reject("the quote has no minimum output", "upstream_unavailable");
|
|
864
1128
|
const minDestAmount = est.toAmountMin;
|
|
865
1129
|
if (sentSlippage !== void 0) {
|
|
866
1130
|
const dest = BigInt(est.toAmount);
|
|
@@ -874,18 +1138,22 @@ function lifiProvider(config = {}) {
|
|
|
874
1138
|
continue;
|
|
875
1139
|
}
|
|
876
1140
|
params.onRefusal?.(
|
|
877
|
-
`lifi: this route's minimum is ${(effective * 100).toFixed(2)}% under the quote (LI.FI applies slippage on every step) \u2014 more than the requested ${slippageBps / 100}
|
|
1141
|
+
`lifi: this route's minimum is ${(effective * 100).toFixed(2)}% under the quote (LI.FI applies slippage on every step) \u2014 more than the requested ${slippageBps / 100}%`,
|
|
1142
|
+
{ reason: "price_check_failed" }
|
|
878
1143
|
);
|
|
879
1144
|
return null;
|
|
880
1145
|
}
|
|
881
1146
|
}
|
|
882
1147
|
if (params.minAmountOut !== void 0 && BigInt(minDestAmount) < BigInt(params.minAmountOut)) {
|
|
883
|
-
params.onRefusal?.(`lifi: floor breach (min=${minDestAmount} < floor=${params.minAmountOut})
|
|
1148
|
+
params.onRefusal?.(`lifi: floor breach (min=${minDestAmount} < floor=${params.minAmountOut})`, { reason: "price_check_failed" });
|
|
884
1149
|
return null;
|
|
885
1150
|
}
|
|
886
1151
|
const duration = est.executionDuration ?? 0;
|
|
887
1152
|
if (fromChain === toChain && duration > SAME_CHAIN_MAX_SECONDS) {
|
|
888
|
-
params.onRefusal?.(
|
|
1153
|
+
params.onRefusal?.(
|
|
1154
|
+
`lifi: same-chain route settles asynchronously (${duration} s via ${q.tool ?? "unknown tool"}) \u2014 a same-chain swap must deliver in the same transaction`,
|
|
1155
|
+
{ reason: "unsupported_pair" }
|
|
1156
|
+
);
|
|
889
1157
|
return null;
|
|
890
1158
|
}
|
|
891
1159
|
if (!solanaSource) {
|
|
@@ -900,7 +1168,7 @@ function lifiProvider(config = {}) {
|
|
|
900
1168
|
sameChain: fromChain === toChain,
|
|
901
1169
|
evmDestination: !isSolana(params.destChain)
|
|
902
1170
|
});
|
|
903
|
-
if (why) return reject(`the transaction does not match the quote: ${why}
|
|
1171
|
+
if (why) return reject(`the transaction does not match the quote: ${why}`, reasonOf(why) ?? "policy_refused");
|
|
904
1172
|
}
|
|
905
1173
|
return {
|
|
906
1174
|
provider: "lifi",
|
|
@@ -908,7 +1176,10 @@ function lifiProvider(config = {}) {
|
|
|
908
1176
|
intentId: `lifi:${fromChain}:${toChain}`,
|
|
909
1177
|
depositAddress: "",
|
|
910
1178
|
depositMemo: null,
|
|
911
|
-
|
|
1179
|
+
// Every check above ran on LI.FI's transaction; one built for a placeholder is not handed out (placeholderIn).
|
|
1180
|
+
// Its bridge fee on top of the input still is, as `providerData.nativeFee`, so the router's value nets it out
|
|
1181
|
+
// (usdValue reads it when there is no txRequest) instead of ranking the route on its gross output.
|
|
1182
|
+
...placeholder ? { providerData: { placeholder, ...nativeFee > 0n ? { nativeFee: nativeFee.toString() } : {} } } : { txRequest },
|
|
912
1183
|
// What the wallet spends (action.fromAmount, = the input); an input-side bridge fee is inside destAmount.
|
|
913
1184
|
sourceAmount: params.amountIn,
|
|
914
1185
|
destAmount: est.toAmount,
|
|
@@ -922,14 +1193,18 @@ function lifiProvider(config = {}) {
|
|
|
922
1193
|
validUntilMs: receivedAt + quoteTtlMs
|
|
923
1194
|
};
|
|
924
1195
|
} catch {
|
|
925
|
-
if (deadline?.aborted) params.onRefusal?.("lifi: no quote within 12 s");
|
|
1196
|
+
if (deadline?.aborted) params.onRefusal?.("lifi: no quote within 12 s", { reason: "upstream_unavailable" });
|
|
926
1197
|
return null;
|
|
927
1198
|
}
|
|
928
1199
|
return null;
|
|
929
1200
|
},
|
|
930
1201
|
async buildDeposit(quote) {
|
|
1202
|
+
const placeholder = quote.providerData?.placeholder;
|
|
1203
|
+
if (placeholder !== void 0) {
|
|
1204
|
+
throw refusal("policy_refused", `swap2: this LI.FI quote was priced for a placeholder ${String(placeholder).slice(0, 20)} (a dry quote without that address) and has no transaction to build \u2014 quote again with the sender and the recipient`);
|
|
1205
|
+
}
|
|
931
1206
|
const tx = quote.txRequest;
|
|
932
|
-
if (!tx) throw
|
|
1207
|
+
if (!tx) throw refusal("policy_refused", "swap2: LI.FI quote has no transaction request");
|
|
933
1208
|
const [, fromChain] = quote.intentId.split(":");
|
|
934
1209
|
return {
|
|
935
1210
|
chain: tx.serializedTx ? CAIP2_BY_LIFI_CHAIN[fromChain ?? ""] ?? "" : "",
|
|
@@ -978,22 +1253,27 @@ function lifiProvider(config = {}) {
|
|
|
978
1253
|
function lifiVerifyQuote(params, quote) {
|
|
979
1254
|
const fromChain = LIFI_CHAIN[params.sourceChain];
|
|
980
1255
|
const toChain = LIFI_CHAIN[params.destChain];
|
|
981
|
-
if (!fromChain || !toChain) return "not a LI.FI chain pair";
|
|
1256
|
+
if (!fromChain || !toChain) return note("not a LI.FI chain pair", "unsupported_pair");
|
|
1257
|
+
const placeholder = params.dry !== false ? placeholderIn(params) : null;
|
|
1258
|
+
if (placeholder) {
|
|
1259
|
+
if (quote.txRequest !== void 0) return note(`a dry quote priced for a placeholder ${placeholder} carries a transaction \u2014 it would pay or refund that placeholder, not the wallet that signs it`, "recipient_mismatch");
|
|
1260
|
+
return priceOnlyMismatch(params, quote, `lifi:${fromChain}:${toChain}`);
|
|
1261
|
+
}
|
|
982
1262
|
const tx = quote.txRequest;
|
|
983
|
-
if (!tx) return "the quote carries no transaction";
|
|
1263
|
+
if (!tx) return note("the quote carries no transaction", "policy_refused");
|
|
984
1264
|
if (isSolana(params.sourceChain)) {
|
|
985
|
-
if (!tx.serializedTx || tx.data !== void 0 || tx.to !== void 0) return "a Solana source carries a serialized transaction only";
|
|
1265
|
+
if (!tx.serializedTx || tx.data !== void 0 || tx.to !== void 0) return note("a Solana source carries a serialized transaction only", "policy_refused");
|
|
986
1266
|
return fromChain === toChain ? null : solanaBridgeTxMismatch(tx.serializedTx, params.refundAddress || (params.dry !== false ? SOLANA_PLACEHOLDER : ""));
|
|
987
1267
|
}
|
|
988
|
-
if (!isHexData(tx.data) || !tx.to || tx.serializedTx !== void 0) return "the quote carries no Diamond call";
|
|
989
|
-
if (!sameAddress(tx.to, diamondFor(params.sourceChain))) return `the call targets ${tx.to}, not the LI.FI Diamond
|
|
990
|
-
if (tx.chainId !== void 0 && String(tx.chainId) !== fromChain) return `the call is for chain ${tx.chainId}, not ${fromChain}
|
|
1268
|
+
if (!isHexData(tx.data) || !tx.to || tx.serializedTx !== void 0) return note("the quote carries no Diamond call", "policy_refused");
|
|
1269
|
+
if (!sameAddress(tx.to, diamondFor(params.sourceChain))) return note(`the call targets ${tx.to}, not the LI.FI Diamond`, "vault_mismatch");
|
|
1270
|
+
if (tx.chainId !== void 0 && String(tx.chainId) !== fromChain) return note(`the call is for chain ${tx.chainId}, not ${fromChain}`, "policy_refused");
|
|
991
1271
|
const fromToken = lifiToken(params);
|
|
992
|
-
if (!fromToken) return `unknown source token ${params.sourceToken}
|
|
1272
|
+
if (!fromToken) return note(`unknown source token ${params.sourceToken}`, "unsupported_pair");
|
|
993
1273
|
const sourceIsToken = fromToken !== EVM_ZERO;
|
|
994
1274
|
const dry = params.dry !== false;
|
|
995
1275
|
const fromAddress = params.refundAddress || (dry ? EVM_PLACEHOLDER : "");
|
|
996
|
-
const toAddress = params.destAddress || (dry ? fromAddress : "");
|
|
1276
|
+
const toAddress = params.destAddress || (dry ? dryRecipient(params, fromAddress) : "");
|
|
997
1277
|
if (!fromAddress || !toAddress) return "a live quote needs the sender and the recipient";
|
|
998
1278
|
let amountIn, minOut, value, nativeFee;
|
|
999
1279
|
try {
|
|
@@ -1002,18 +1282,18 @@ function lifiVerifyQuote(params, quote) {
|
|
|
1002
1282
|
value = BigInt(tx.value ?? "0");
|
|
1003
1283
|
nativeFee = BigInt(tx.nativeFee ?? "0");
|
|
1004
1284
|
} catch {
|
|
1005
|
-
return "the quote's amounts are not integers";
|
|
1285
|
+
return note("the quote's amounts are not integers", "upstream_unavailable");
|
|
1006
1286
|
}
|
|
1007
|
-
if (BigInt(quote.sourceAmount) !== amountIn) return `the quote spends ${quote.sourceAmount}, not the ${amountIn} input
|
|
1008
|
-
if (params.minAmountOut !== void 0 && minOut < BigInt(params.minAmountOut)) return `the quoted minimum ${minOut} is below the caller's floor ${params.minAmountOut}
|
|
1287
|
+
if (BigInt(quote.sourceAmount) !== amountIn) return note(`the quote spends ${quote.sourceAmount}, not the ${amountIn} input`, "policy_refused");
|
|
1288
|
+
if (params.minAmountOut !== void 0 && minOut < BigInt(params.minAmountOut)) return note(`the quoted minimum ${minOut} is below the caller's floor ${params.minAmountOut}`, "price_check_failed");
|
|
1009
1289
|
if (sourceIsToken) {
|
|
1010
|
-
if (value !== nativeFee) return "a token input attaches only its declared bridge fee";
|
|
1011
|
-
if (!tx.approvalAddress || !sameAddress(tx.approvalAddress, diamondFor(params.sourceChain))) return "a token input approves the LI.FI Diamond";
|
|
1290
|
+
if (value !== nativeFee) return note("a token input attaches only its declared bridge fee", "policy_refused");
|
|
1291
|
+
if (!tx.approvalAddress || !sameAddress(tx.approvalAddress, diamondFor(params.sourceChain))) return note("a token input approves the LI.FI Diamond", "vault_mismatch");
|
|
1012
1292
|
} else {
|
|
1013
|
-
if (value < amountIn || value - amountIn !== nativeFee) return "a native input is paid as the input plus its declared bridge fee";
|
|
1014
|
-
if (tx.approvalAddress !== void 0) return "a native input approves nothing";
|
|
1293
|
+
if (value < amountIn || value - amountIn !== nativeFee) return note("a native input is paid as the input plus its declared bridge fee", "policy_refused");
|
|
1294
|
+
if (tx.approvalAddress !== void 0) return note("a native input approves nothing", "vault_mismatch");
|
|
1015
1295
|
}
|
|
1016
|
-
if (quote.intentId.replace(/~[0-9a-f]{32}$/, "") !== `lifi:${fromChain}:${toChain}`) return "the intent id does not name this route";
|
|
1296
|
+
if (quote.intentId.replace(/~[0-9a-f]{32}$/, "") !== `lifi:${fromChain}:${toChain}`) return note("the intent id does not name this route", "policy_refused");
|
|
1017
1297
|
const why = diamondCallMismatch(tx.data, {
|
|
1018
1298
|
fromToken,
|
|
1019
1299
|
toToken: nativeFor(params.destChain, params.destToken),
|
|
@@ -1025,12 +1305,27 @@ function lifiVerifyQuote(params, quote) {
|
|
|
1025
1305
|
sameChain: fromChain === toChain,
|
|
1026
1306
|
evmDestination: !isSolana(params.destChain)
|
|
1027
1307
|
});
|
|
1028
|
-
return why ? `the transaction does not match the request: ${why}
|
|
1308
|
+
return why ? note(`the transaction does not match the request: ${why}`, reasonOf(why) ?? "policy_refused") : null;
|
|
1309
|
+
}
|
|
1310
|
+
function priceOnlyMismatch(params, quote, route) {
|
|
1311
|
+
if (quote.depositAddress !== "") return note(`a LI.FI quote pays no deposit address, but this one names ${String(quote.depositAddress).slice(0, 64)}`, "vault_mismatch");
|
|
1312
|
+
const fee = quote.providerData?.nativeFee;
|
|
1313
|
+
if (fee !== void 0 && !/^\d{1,80}$/.test(String(fee))) return note(`the price-only quote declares a native fee of ${String(fee).slice(0, 40)}, not a base-unit integer`, "upstream_unavailable");
|
|
1314
|
+
try {
|
|
1315
|
+
if (BigInt(quote.sourceAmount) !== BigInt(params.amountIn)) return note(`the quote spends ${quote.sourceAmount}, not the ${params.amountIn} input`, "policy_refused");
|
|
1316
|
+
const minOut = BigInt(quote.minDestAmount);
|
|
1317
|
+
if (params.minAmountOut !== void 0 && minOut < BigInt(params.minAmountOut)) return note(`the quoted minimum ${minOut} is below the caller's floor ${params.minAmountOut}`, "price_check_failed");
|
|
1318
|
+
} catch {
|
|
1319
|
+
return note("the quote's amounts are not integers", "upstream_unavailable");
|
|
1320
|
+
}
|
|
1321
|
+
return quote.intentId.replace(/~[0-9a-f]{32}$/, "") === route ? null : note("the intent id does not name this route", "policy_refused");
|
|
1029
1322
|
}
|
|
1030
1323
|
var lifiPins = Object.freeze({
|
|
1031
1324
|
callTargets: (chain) => /^eip155:[0-9]+$/.test(chain) ? [diamondFor(chain)] : [],
|
|
1032
1325
|
spenders: (chain) => /^eip155:[0-9]+$/.test(chain) ? [diamondFor(chain)] : [],
|
|
1033
|
-
verifyQuote: lifiVerifyQuote
|
|
1326
|
+
verifyQuote: lifiVerifyQuote,
|
|
1327
|
+
/** The reason of a text {@link lifiVerifyQuote} returned (`remoteProvider` puts it on the stub's refusal). */
|
|
1328
|
+
reasonOf: (text) => reasonOf(text)
|
|
1034
1329
|
});
|
|
1035
1330
|
|
|
1036
1331
|
export {
|
|
@@ -1038,4 +1333,4 @@ export {
|
|
|
1038
1333
|
lifiVerifyQuote,
|
|
1039
1334
|
lifiPins
|
|
1040
1335
|
};
|
|
1041
|
-
//# sourceMappingURL=chunk-
|
|
1336
|
+
//# sourceMappingURL=chunk-G26GE46T.js.map
|