@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
package/dist/protocol.cjs
CHANGED
|
@@ -73,6 +73,76 @@ function describeLifiActivity(tx, ctx, isDiamond) {
|
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// src/solana-address.ts
|
|
77
|
+
var import_ed25519 = require("@noble/curves/ed25519.js");
|
|
78
|
+
var import_sha2 = require("@noble/hashes/sha2.js");
|
|
79
|
+
var ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
80
|
+
var BASE58 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
81
|
+
var SOLANA_TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
82
|
+
var ASSOCIATED_TOKEN_PROGRAM = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
83
|
+
var PDA_MARKER = Uint8Array.from("ProgramDerivedAddress", (c) => c.charCodeAt(0));
|
|
84
|
+
function base58(bytes) {
|
|
85
|
+
let n = 0n;
|
|
86
|
+
for (const b of bytes) n = n << 8n | BigInt(b);
|
|
87
|
+
let out = "";
|
|
88
|
+
while (n > 0n) {
|
|
89
|
+
out = ALPHABET[Number(n % 58n)] + out;
|
|
90
|
+
n /= 58n;
|
|
91
|
+
}
|
|
92
|
+
for (const b of bytes) {
|
|
93
|
+
if (b !== 0) break;
|
|
94
|
+
out = `1${out}`;
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
function solanaKey(address) {
|
|
99
|
+
if (!BASE58.test(address)) return null;
|
|
100
|
+
let n = 0n;
|
|
101
|
+
for (const ch of address) n = n * 58n + BigInt(ALPHABET.indexOf(ch));
|
|
102
|
+
let zeros = 0;
|
|
103
|
+
while (address[zeros] === "1") zeros++;
|
|
104
|
+
const body = [];
|
|
105
|
+
while (n > 0n) {
|
|
106
|
+
body.unshift(Number(n & 0xffn));
|
|
107
|
+
n >>= 8n;
|
|
108
|
+
}
|
|
109
|
+
if (zeros + body.length !== 32) return null;
|
|
110
|
+
const out = new Uint8Array(32);
|
|
111
|
+
out.set(body, zeros);
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
var onCurve = (bytes) => {
|
|
115
|
+
try {
|
|
116
|
+
import_ed25519.ed25519.Point.fromBytes(bytes);
|
|
117
|
+
return true;
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
function programAddress(seeds, program) {
|
|
123
|
+
for (let bump = 255; bump >= 0; bump--) {
|
|
124
|
+
const parts = [...seeds, Uint8Array.of(bump), program, PDA_MARKER];
|
|
125
|
+
const buf = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
126
|
+
let at = 0;
|
|
127
|
+
for (const p of parts) {
|
|
128
|
+
buf.set(p, at);
|
|
129
|
+
at += p.length;
|
|
130
|
+
}
|
|
131
|
+
const hash = (0, import_sha2.sha256)(buf);
|
|
132
|
+
if (!onCurve(hash)) return hash;
|
|
133
|
+
}
|
|
134
|
+
throw new Error("no program address for these seeds");
|
|
135
|
+
}
|
|
136
|
+
function associatedTokenAccount(owner, mint, tokenProgram = solanaKey(SOLANA_TOKEN_PROGRAM)) {
|
|
137
|
+
return programAddress([owner, tokenProgram, mint], solanaKey(ASSOCIATED_TOKEN_PROGRAM));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/refusals.ts
|
|
141
|
+
var import_errors = require("@anywalletkit/protocol-swap2-core/errors");
|
|
142
|
+
var notes = (0, import_errors.refusalNotes)();
|
|
143
|
+
var note = notes.note;
|
|
144
|
+
var reasonOf = notes.reasonOf;
|
|
145
|
+
|
|
76
146
|
// src/calldata.ts
|
|
77
147
|
var GENERIC_SWAPS = {
|
|
78
148
|
"0x4630a0d8": { single: false },
|
|
@@ -292,18 +362,18 @@ function swapsAt(r, base, head) {
|
|
|
292
362
|
function checkSteps(steps, e, finalAsset) {
|
|
293
363
|
const first = steps[0];
|
|
294
364
|
const last = steps[steps.length - 1];
|
|
295
|
-
if (!same(first.sendingAssetId, e.fromToken)) return `spends ${first.sendingAssetId}, not ${e.fromToken}
|
|
296
|
-
if (first.fromAmount === 0n || first.fromAmount > e.amountIn) return `swaps ${first.fromAmount}, not the ${e.amountIn} spent
|
|
365
|
+
if (!same(first.sendingAssetId, e.fromToken)) return note(`spends ${first.sendingAssetId}, not ${e.fromToken}`, "policy_refused");
|
|
366
|
+
if (first.fromAmount === 0n || first.fromAmount > e.amountIn) return note(`swaps ${first.fromAmount}, not the ${e.amountIn} spent`, "policy_refused");
|
|
297
367
|
let deposited = 0n;
|
|
298
368
|
for (let i = 0; i < steps.length; i++) {
|
|
299
369
|
const step = steps[i];
|
|
300
|
-
if (i > 0 && !same(step.sendingAssetId, steps[i - 1].receivingAssetId)) return `step ${i} spends ${step.sendingAssetId}, which step ${i - 1} does not deliver
|
|
370
|
+
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");
|
|
301
371
|
if (!step.requiresDeposit) continue;
|
|
302
|
-
if (!same(step.sendingAssetId, e.fromToken)) return `step ${i} pulls ${step.sendingAssetId} from the wallet, not the input ${e.fromToken}
|
|
372
|
+
if (!same(step.sendingAssetId, e.fromToken)) return note(`step ${i} pulls ${step.sendingAssetId} from the wallet, not the input ${e.fromToken}`, "policy_refused");
|
|
303
373
|
deposited += step.fromAmount;
|
|
304
374
|
}
|
|
305
|
-
if (deposited > e.amountIn) return `the steps pull ${deposited} from the wallet, more than the ${e.amountIn} spent
|
|
306
|
-
if (!same(last.receivingAssetId, finalAsset)) return `delivers ${last.receivingAssetId}, not ${finalAsset}
|
|
375
|
+
if (deposited > e.amountIn) return note(`the steps pull ${deposited} from the wallet, more than the ${e.amountIn} spent`, "policy_refused");
|
|
376
|
+
if (!same(last.receivingAssetId, finalAsset)) return note(`delivers ${last.receivingAssetId}, not ${finalAsset}`, "policy_refused");
|
|
307
377
|
return null;
|
|
308
378
|
}
|
|
309
379
|
var MAYAN_REFUND_AT = {
|
|
@@ -344,32 +414,32 @@ function acrossSwapRefunds(r, at) {
|
|
|
344
414
|
}
|
|
345
415
|
var ERC20_TRANSFER = "0xa9059cbb";
|
|
346
416
|
function ecoDelivery(r, at, e) {
|
|
347
|
-
if (kindOf(e.toToken) === "native") return "an Eco route to the native token cannot be checked here";
|
|
417
|
+
if (kindOf(e.toToken) === "native") return note("an Eco route to the native token cannot be checked here", "recipient_unverifiable");
|
|
348
418
|
const destination = r.word(at + 32 * 3);
|
|
349
|
-
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
|
|
419
|
+
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");
|
|
350
420
|
const route = r.bytes(at, at + 32 * 4);
|
|
351
421
|
const tuple = route.small(0);
|
|
352
422
|
const calls = tuple + route.small(tuple + 32 * 5);
|
|
353
423
|
const n = route.small(calls);
|
|
354
|
-
if (n === 0) return "the Eco route makes no calls on the destination chain";
|
|
355
|
-
if (n > 16) return `the Eco route makes ${n} calls on the destination chain, more than the 16 read here
|
|
424
|
+
if (n === 0) return note("the Eco route makes no calls on the destination chain", "recipient_unverifiable");
|
|
425
|
+
if (n > 16) return note(`the Eco route makes ${n} calls on the destination chain, more than the 16 read here`, "recipient_unverifiable");
|
|
356
426
|
const last = calls + 32 + route.small(calls + 32 + 32 * (n - 1));
|
|
357
427
|
const target = route.address(last);
|
|
358
428
|
const data = route.bytes(last, last + 32);
|
|
359
|
-
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";
|
|
429
|
+
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");
|
|
360
430
|
const to = data.address(4);
|
|
361
431
|
const amount = data.word(36);
|
|
362
|
-
if (!same(target, e.toToken)) return `the Eco route delivers ${target}, not ${e.toToken}
|
|
363
|
-
if (!same(to, e.receiver)) return `the Eco route pays ${to}, not ${e.receiver}
|
|
364
|
-
if (amount < e.minOut) return `the Eco route delivers ${amount}, below the ${e.minOut} shown
|
|
432
|
+
if (!same(target, e.toToken)) return note(`the Eco route delivers ${target}, not ${e.toToken}`, "policy_refused");
|
|
433
|
+
if (!same(to, e.receiver)) return note(`the Eco route pays ${to}, not ${e.receiver}`, "recipient_mismatch");
|
|
434
|
+
if (amount < e.minOut) return note(`the Eco route delivers ${amount}, below the ${e.minOut} shown`, "price_check_failed");
|
|
365
435
|
return null;
|
|
366
436
|
}
|
|
367
437
|
function m0Delivery(r, at, e) {
|
|
368
|
-
if (kindOf(e.toToken) === "native") return "an M0 order cannot deliver the native token";
|
|
438
|
+
if (kindOf(e.toToken) === "native") return note("an M0 order cannot deliver the native token", "unsupported_pair");
|
|
369
439
|
const tokenOut = r.address(at + 32 * 3);
|
|
370
440
|
const amountOut = r.word(at + 32 * 5);
|
|
371
|
-
if (!same(tokenOut, e.toToken)) return `the M0 order asks for ${tokenOut}, not ${e.toToken}
|
|
372
|
-
if (amountOut < e.minOut) return `the M0 order asks for ${amountOut}, below the ${e.minOut} shown
|
|
441
|
+
if (!same(tokenOut, e.toToken)) return note(`the M0 order asks for ${tokenOut}, not ${e.toToken}`, "policy_refused");
|
|
442
|
+
if (amountOut < e.minOut) return note(`the M0 order asks for ${amountOut}, below the ${e.minOut} shown`, "price_check_failed");
|
|
373
443
|
return null;
|
|
374
444
|
}
|
|
375
445
|
var FACET_DATA = {
|
|
@@ -383,8 +453,9 @@ var FACET_DATA = {
|
|
|
383
453
|
Frax: { inline: 5, refunds: [["refundRecipient", 3]], zeroRefused: true },
|
|
384
454
|
Glacis: { inline: 4, refunds: [["refundAddress", 1]], zeroRefused: true },
|
|
385
455
|
LayerSwap: { refunds: [["refundRecipient", 2]], zeroRefused: true },
|
|
456
|
+
// The escrow order's user. Not reached while LI.FI Intents is refused on every EVM-source route (INTENTS_UNPINNED); kept for when
|
|
457
|
+
// its oracles and settler are pinned.
|
|
386
458
|
LiFiIntentEscrowV2: { refunds: [["depositAndRefundAddress", 2]], zeroRefused: true },
|
|
387
|
-
// the escrow order's user
|
|
388
459
|
M0: { inline: 7, refunds: [["refundRecipient", 1], ["orderOwner", 2]], zeroRefused: true, delivery: m0Delivery },
|
|
389
460
|
// orderOwner gets a cancelled order back
|
|
390
461
|
Mayan: { refunds: [["refundRecipient", 7]], nested: mayanRefunds, zeroRefused: true },
|
|
@@ -402,79 +473,161 @@ function facetDataAt(r, bridge, spec) {
|
|
|
402
473
|
}
|
|
403
474
|
function refundMismatch(r, bridge, e) {
|
|
404
475
|
const spec = FACET_DATA[bridge.facet];
|
|
405
|
-
if (!spec) return NO_REFUND_FIELDS.has(bridge.facet) ? null : `no known refund layout for ${bridge.facet}
|
|
476
|
+
if (!spec) return NO_REFUND_FIELDS.has(bridge.facet) ? null : note(`no known refund layout for ${bridge.facet}`, "recipient_unverifiable");
|
|
406
477
|
const at = facetDataAt(r, bridge, spec);
|
|
407
478
|
const paid = (spec.refunds ?? []).map(([field, word]) => [`${bridge.facet} ${field}`, r.address(at + 32 * word), spec.zeroRefused === true]);
|
|
408
479
|
paid.push(...spec.nested?.(r, at) ?? []);
|
|
409
480
|
for (const [what, to, zeroRefused] of paid) {
|
|
410
481
|
if (zeroRefused && /^0x0{40}$/.test(to)) continue;
|
|
411
|
-
if (!same(to, e.sender) && !same(to, e.receiver)) return `${what} pays ${to}, neither the sender ${e.sender} nor the recipient
|
|
482
|
+
if (!same(to, e.sender) && !same(to, e.receiver)) return note(`${what} pays ${to}, neither the sender ${e.sender} nor the recipient`, "recipient_mismatch");
|
|
412
483
|
}
|
|
413
484
|
return null;
|
|
414
485
|
}
|
|
486
|
+
var NON_EVM_UNREAD = "the recipient on a non-EVM destination sits in bridge-specific data and cannot be checked here";
|
|
487
|
+
var LIFI_SOLANA_CHAIN_ID = 1151111081099710n;
|
|
488
|
+
var NON_EVM_ADDRESS = "0x11f111f111f111f111f111f111f111f111f111f1";
|
|
489
|
+
var SOLANA_USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
490
|
+
var WORMHOLE_SOLANA = 1n;
|
|
491
|
+
var CCTP_DOMAIN_SOLANA = 5n;
|
|
492
|
+
var CHAINFLIP_SOLANA_ASSETS = {
|
|
493
|
+
"11111111111111111111111111111111": 9n,
|
|
494
|
+
[SOLANA_USDC]: 10n,
|
|
495
|
+
Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB: 16n
|
|
496
|
+
};
|
|
497
|
+
var DEPOSIT_FACETS = /* @__PURE__ */ new Set(["NEARIntents", "LayerSwap", "Unit"]);
|
|
498
|
+
var MAYAN_SOLANA = {
|
|
499
|
+
// Swift v2 createOrderWithToken(tokenIn, amountIn, (payloadType, trader, destAddr, destChainId, referrerAddr, tokenOut, minAmountOut, …), customPayload)
|
|
500
|
+
"0xa3a30834": { destAddr: 4, chain: 5, cctp: false, payloadType: [2, 1n], tokenOut: 7 },
|
|
501
|
+
// MCTP createOrder((tokenIn, amountIn, gasDrop, destAddr, destChain, tokenOut, minAmountOut, deadline, redeemFee, referrerAddr, referrerBps))
|
|
502
|
+
"0x1c59b7fc": { destAddr: 3, chain: 4, cctp: false, tokenOut: 5 },
|
|
503
|
+
// MCTP bridgeWithFee(tokenIn, amountIn, redeemFee, gasDrop, destAddr, destDomain, payloadType, customPayload): delivers USDC
|
|
504
|
+
"0x2072197f": { destAddr: 4, chain: 5, cctp: true, payloadType: [6, 1n] },
|
|
505
|
+
// Fast MCTP bridge(tokenIn, amountIn, redeemFee, circleMaxFee, gasDrop, destAddr, destDomain, referrerAddress, referrerBps, payloadType, minFinalityThreshold, customPayload): delivers USDC
|
|
506
|
+
"0xf58b6de8": { destAddr: 5, chain: 6, cctp: true, payloadType: [9, 1n] },
|
|
507
|
+
// Fast MCTP createOrder(tokenIn, amountIn, circleMaxFee, destDomain, minFinalityThreshold, (payloadType 3, destAddr, tokenOut, amountOutMin, …))
|
|
508
|
+
"0x2337e236": { destAddr: 6, chain: 3, cctp: true, payloadType: [5, 3n], tokenOut: 7 }
|
|
509
|
+
};
|
|
510
|
+
var hexOf = (b) => `0x${Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("")}`;
|
|
511
|
+
var keyOf = (word) => base58(Uint8Array.from(word.slice(2).match(/../g) ?? [], (h) => Number.parseInt(h, 16)));
|
|
512
|
+
function mayanToSolana(r, at, t) {
|
|
513
|
+
const declared = r.raw(at, 32);
|
|
514
|
+
if (declared !== t.ownerHex) return note(`Mayan's nonEVMReceiver is ${keyOf(declared)}, not the recipient ${t.e.receiver}`, "recipient_mismatch");
|
|
515
|
+
const p = r.bytes(at, at + 32 * 2);
|
|
516
|
+
const selector = p.raw(0, 4);
|
|
517
|
+
const o = MAYAN_SOLANA[selector];
|
|
518
|
+
if (!o) return note(`the Mayan call ${selector} is an entry point whose Solana recipient cannot be checked here`, "recipient_unverifiable");
|
|
519
|
+
const word = (i) => 4 + 32 * i;
|
|
520
|
+
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");
|
|
521
|
+
const dest = p.raw(word(o.destAddr), 32);
|
|
522
|
+
if (dest !== t.ownerHex) return note(`the Mayan order pays ${keyOf(dest)} on Solana, not the recipient ${t.e.receiver}`, "recipient_mismatch");
|
|
523
|
+
const chain = p.word(word(o.chain));
|
|
524
|
+
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");
|
|
525
|
+
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");
|
|
526
|
+
const token = p.raw(word(o.tokenOut), 32);
|
|
527
|
+
return token === t.mintHex ? null : note(`the Mayan order delivers ${keyOf(token)}, not ${t.e.toToken}`, "policy_refused");
|
|
528
|
+
}
|
|
529
|
+
function polymerToSolana(r, at, t) {
|
|
530
|
+
const declared = r.raw(at + 32 * 2, 32);
|
|
531
|
+
if (declared !== t.ownerHex) return note(`Polymer CCTP's nonEVMReceiver is ${keyOf(declared)}, not the recipient ${t.e.receiver}`, "recipient_mismatch");
|
|
532
|
+
if (t.e.toToken !== SOLANA_USDC) return note(`Polymer CCTP delivers USDC, not ${t.e.toToken}`, "policy_refused");
|
|
533
|
+
const mintTo = r.raw(at + 32 * 3, 32);
|
|
534
|
+
const account = hexOf(associatedTokenAccount(t.owner, t.mint));
|
|
535
|
+
return mintTo === account ? null : note(`the Polymer CCTP transfer mints to ${keyOf(mintTo)}, not the recipient's USDC account ${keyOf(account)}`, "recipient_mismatch");
|
|
536
|
+
}
|
|
537
|
+
function chainflipToSolana(r, at, t) {
|
|
538
|
+
const dst = r.bytes(at, at);
|
|
539
|
+
if (dst.length !== 32) return note(`Chainflip pays a ${dst.length}-byte address on Solana, not the recipient ${t.e.receiver}`, "recipient_mismatch");
|
|
540
|
+
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");
|
|
541
|
+
const asset = r.word(at + 32);
|
|
542
|
+
const want = CHAINFLIP_SOLANA_ASSETS[t.e.toToken];
|
|
543
|
+
return want !== void 0 && asset === want ? null : note(`Chainflip delivers its asset ${asset}, not ${t.e.toToken}`, "policy_refused");
|
|
544
|
+
}
|
|
545
|
+
var SOLANA_DELIVERY = {
|
|
546
|
+
Mayan: mayanToSolana,
|
|
547
|
+
PolymerCCTP: polymerToSolana,
|
|
548
|
+
Chainflip: chainflipToSolana
|
|
549
|
+
};
|
|
550
|
+
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";
|
|
551
|
+
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";
|
|
552
|
+
var INTENTS_FACET = "LiFiIntentEscrowV2";
|
|
553
|
+
function solanaRecipientMismatch(r, bridge, receiver, e) {
|
|
554
|
+
if (e.destChainId !== LIFI_SOLANA_CHAIN_ID) return note(NON_EVM_UNREAD, "recipient_unverifiable");
|
|
555
|
+
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");
|
|
556
|
+
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");
|
|
557
|
+
if (bridge.facet === INTENTS_FACET) return note(INTENTS_TO_SOLANA, "recipient_unverifiable");
|
|
558
|
+
const read = SOLANA_DELIVERY[bridge.facet];
|
|
559
|
+
if (!read) return note(NON_EVM_UNREAD, "recipient_unverifiable");
|
|
560
|
+
const owner = solanaKey(e.receiver);
|
|
561
|
+
if (!owner) return note(`the recipient ${e.receiver} is not a Solana address`, "recipient_mismatch");
|
|
562
|
+
const mint = solanaKey(e.toToken);
|
|
563
|
+
if (!mint) return note(`the destination token ${e.toToken} is not a Solana mint`, "unsupported_pair");
|
|
564
|
+
return read(r, facetDataAt(r, bridge, FACET_DATA[bridge.facet]), { owner, ownerHex: hexOf(owner), mint, mintHex: hexOf(mint), e });
|
|
565
|
+
}
|
|
415
566
|
function diamondCallMismatch(data, e) {
|
|
416
567
|
let r;
|
|
417
568
|
try {
|
|
418
569
|
r = new Reader(data);
|
|
419
570
|
} catch {
|
|
420
|
-
return "the calldata is not hex";
|
|
571
|
+
return note("the calldata is not hex", "upstream_unavailable");
|
|
421
572
|
}
|
|
422
|
-
if (r.length < 4 + 32 * 2) return "the calldata is too short";
|
|
573
|
+
if (r.length < 4 + 32 * 2) return note("the calldata is too short", "upstream_unavailable");
|
|
423
574
|
const selector = data.slice(0, 10).toLowerCase();
|
|
424
575
|
const generic = GENERIC_SWAPS[selector];
|
|
425
576
|
try {
|
|
426
577
|
if (generic) {
|
|
427
|
-
if (!e.sameChain) return "a same-chain swap call on a cross-chain route";
|
|
578
|
+
if (!e.sameChain) return note("a same-chain swap call on a cross-chain route", "policy_refused");
|
|
428
579
|
const receiver2 = r.address(4 + 96);
|
|
429
580
|
const minAmount2 = r.word(4 + 128);
|
|
430
581
|
const steps = generic.single ? [swapAt(r, 4 + r.small(4 + 160))] : swapsAt(r, 4, 4 + 160);
|
|
431
|
-
if (!e.evmDestination || !same(receiver2, e.receiver)) return `pays ${receiver2}, not ${e.receiver}
|
|
432
|
-
if (minAmount2 < e.minOut) return `accepts ${minAmount2}, below the ${e.minOut} shown
|
|
433
|
-
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"}
|
|
582
|
+
if (!e.evmDestination || !same(receiver2, e.receiver)) return note(`pays ${receiver2}, not ${e.receiver}`, "recipient_mismatch");
|
|
583
|
+
if (minAmount2 < e.minOut) return note(`accepts ${minAmount2}, below the ${e.minOut} shown`, "price_check_failed");
|
|
584
|
+
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");
|
|
434
585
|
if (generic.to && (kindOf(e.toToken) !== generic.to || kindOf(steps[steps.length - 1].receivingAssetId) !== generic.to)) {
|
|
435
|
-
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
|
|
586
|
+
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");
|
|
436
587
|
}
|
|
437
588
|
return checkSteps(steps, e, e.toToken);
|
|
438
589
|
}
|
|
439
590
|
const unbound = UNBOUND_BRIDGES[selector];
|
|
440
|
-
if (unbound) return `${unbound} delivers to a recipient the transaction does not bind (an off-chain order or unchecked facet data)
|
|
591
|
+
if (unbound) return note(`${unbound} delivers to a recipient the transaction does not bind (an off-chain order or unchecked facet data)`, "recipient_unverifiable");
|
|
441
592
|
const bridge = BRIDGE_CALLS[selector];
|
|
442
|
-
if (!bridge) return `unknown Diamond call ${selector}
|
|
593
|
+
if (!bridge) return note(`unknown Diamond call ${selector}`, "policy_refused");
|
|
594
|
+
if (bridge.facet === INTENTS_FACET && e.evmDestination) return note(INTENTS_UNPINNED, "recipient_unverifiable");
|
|
443
595
|
const head = r.small(4);
|
|
444
|
-
if (head < (bridge.sourceSwaps ? 96 : 64) || head % 32 !== 0 || head > 32 * 32) return `unknown Diamond call ${selector}
|
|
596
|
+
if (head < (bridge.sourceSwaps ? 96 : 64) || head % 32 !== 0 || head > 32 * 32) return note(`unknown Diamond call ${selector}`, "policy_refused");
|
|
445
597
|
const at = 4 + head;
|
|
446
|
-
if (r.small(at + 32) !== 320) return `unknown Diamond call ${selector}
|
|
598
|
+
if (r.small(at + 32) !== 320) return note(`unknown Diamond call ${selector}`, "policy_refused");
|
|
447
599
|
const sendingAssetId = r.address(at + 128);
|
|
448
600
|
const receiver = r.address(at + 160);
|
|
449
601
|
const minAmount = r.word(at + 192);
|
|
450
602
|
const destinationChainId = r.word(at + 224);
|
|
451
603
|
const hasSourceSwaps = r.bool(at + 256);
|
|
452
604
|
const hasDestinationCall = r.bool(at + 288);
|
|
453
|
-
if (e.sameChain) return "a bridge call on a same-chain route";
|
|
454
|
-
if (destinationChainId !== e.destChainId) return `bridges to chain ${destinationChainId}, not ${e.destChainId}
|
|
455
|
-
if (hasDestinationCall) return "the route runs a call on the destination chain, whose recipient cannot be checked here";
|
|
456
|
-
if (!e.evmDestination)
|
|
457
|
-
|
|
458
|
-
|
|
605
|
+
if (e.sameChain) return note("a bridge call on a same-chain route", "policy_refused");
|
|
606
|
+
if (destinationChainId !== e.destChainId) return note(`bridges to chain ${destinationChainId}, not ${e.destChainId}`, "policy_refused");
|
|
607
|
+
if (hasDestinationCall) return note("the route runs a call on the destination chain, whose recipient cannot be checked here", "recipient_unverifiable");
|
|
608
|
+
if (!e.evmDestination) {
|
|
609
|
+
const why = solanaRecipientMismatch(r, bridge, receiver, e);
|
|
610
|
+
if (why) return why;
|
|
611
|
+
} else if (!same(receiver, e.receiver)) return note(`pays ${receiver}, not ${e.receiver}`, "recipient_mismatch");
|
|
612
|
+
if (hasSourceSwaps !== bridge.sourceSwaps) return note(`unknown Diamond call ${selector}`, "policy_refused");
|
|
459
613
|
const refund = refundMismatch(r, bridge, e);
|
|
460
614
|
if (refund) return refund;
|
|
461
615
|
const spec = FACET_DATA[bridge.facet];
|
|
462
616
|
const delivery = spec?.delivery?.(r, facetDataAt(r, bridge, spec), e);
|
|
463
617
|
if (delivery) return delivery;
|
|
464
618
|
if (hasSourceSwaps) {
|
|
465
|
-
if (minAmount === 0n) return "bridges nothing after the source swaps";
|
|
619
|
+
if (minAmount === 0n) return note("bridges nothing after the source swaps", "policy_refused");
|
|
466
620
|
return checkSteps(swapsAt(r, 4, 4 + 32), e, sendingAssetId);
|
|
467
621
|
}
|
|
468
|
-
if (minAmount > e.amountIn || minAmount * 10000n < e.amountIn * (10000n - BRIDGE_INPUT_FEE_MAX_BPS)) return `bridges ${minAmount}, not the ${e.amountIn} spent
|
|
469
|
-
return same(sendingAssetId, e.fromToken) ? null : `bridges ${sendingAssetId}, not ${e.fromToken}
|
|
622
|
+
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");
|
|
623
|
+
return same(sendingAssetId, e.fromToken) ? null : note(`bridges ${sendingAssetId}, not ${e.fromToken}`, "policy_refused");
|
|
470
624
|
} catch (err) {
|
|
471
|
-
return `the Diamond calldata does not decode (${err instanceof Error ? err.message : String(err)})
|
|
625
|
+
return note(`the Diamond calldata does not decode (${err instanceof Error ? err.message : String(err)})`, "policy_refused");
|
|
472
626
|
}
|
|
473
627
|
}
|
|
474
628
|
|
|
475
629
|
// src/solana.ts
|
|
476
630
|
var import_util2 = require("@anywalletkit/protocol-swap2-core/util");
|
|
477
|
-
var ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
478
631
|
var MAYAN_ENTRY_PROGRAMS = /* @__PURE__ */ new Set([
|
|
479
632
|
"D8C8iW6zmoKg5TRr8nQ7h14TMWqQX8FiBdj2ju5MF3wa",
|
|
480
633
|
// CPI_PROXY_PROGRAM_ID
|
|
@@ -489,20 +642,6 @@ var MAYAN_ENTRY_PROGRAMS = /* @__PURE__ */ new Set([
|
|
|
489
642
|
]);
|
|
490
643
|
var SYSTEM_PROGRAM = "11111111111111111111111111111111";
|
|
491
644
|
var LIFI_SOLANA_FEE_WALLET = "34FKjAdVcTax2DHqV2XnbXa9J3zmyKcFuFKWbcmgxjgm";
|
|
492
|
-
function base58(bytes) {
|
|
493
|
-
let n = 0n;
|
|
494
|
-
for (const b of bytes) n = n << 8n | BigInt(b);
|
|
495
|
-
let out = "";
|
|
496
|
-
while (n > 0n) {
|
|
497
|
-
out = ALPHABET[Number(n % 58n)] + out;
|
|
498
|
-
n /= 58n;
|
|
499
|
-
}
|
|
500
|
-
for (const b of bytes) {
|
|
501
|
-
if (b !== 0) break;
|
|
502
|
-
out = `1${out}`;
|
|
503
|
-
}
|
|
504
|
-
return out;
|
|
505
|
-
}
|
|
506
645
|
var Cursor = class {
|
|
507
646
|
constructor(b) {
|
|
508
647
|
this.b = b;
|
|
@@ -532,7 +671,7 @@ var Cursor = class {
|
|
|
532
671
|
};
|
|
533
672
|
function solanaBridgeTxMismatch(base64, sender) {
|
|
534
673
|
const bytes = (0, import_util2.base64Bytes)(base64);
|
|
535
|
-
if (!bytes) return "the Solana transaction is not base64";
|
|
674
|
+
if (!bytes) return note("the Solana transaction is not base64", "upstream_unavailable");
|
|
536
675
|
const c = new Cursor(bytes);
|
|
537
676
|
try {
|
|
538
677
|
c.bytes(64 * c.short());
|
|
@@ -540,7 +679,7 @@ function solanaBridgeTxMismatch(base64, sender) {
|
|
|
540
679
|
if ((first & 128) === 0) c.at--;
|
|
541
680
|
c.bytes(3);
|
|
542
681
|
const keys = Array.from({ length: c.short() }, () => base58(c.bytes(32)));
|
|
543
|
-
if (keys[0] !== sender) return `the Solana transaction is paid by ${keys[0] ?? "nobody"}, not the sender ${sender}
|
|
682
|
+
if (keys[0] !== sender) return note(`the Solana transaction is paid by ${keys[0] ?? "nobody"}, not the sender ${sender}`, "policy_refused");
|
|
544
683
|
c.bytes(32);
|
|
545
684
|
let mayan = false;
|
|
546
685
|
const payees = [];
|
|
@@ -555,15 +694,16 @@ function solanaBridgeTxMismatch(base64, sender) {
|
|
|
555
694
|
if (to !== void 0 && to !== sender && to !== LIFI_SOLANA_FEE_WALLET) payees.push(to);
|
|
556
695
|
}
|
|
557
696
|
}
|
|
558
|
-
return mayan && payees.length ? `the Solana transaction transfers SOL to ${payees[0]}, which is neither the sender nor LI.FI's fee wallet
|
|
697
|
+
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;
|
|
559
698
|
} catch (err) {
|
|
560
|
-
return `the Solana transaction does not decode (${err instanceof Error ? err.message : String(err)})
|
|
699
|
+
return note(`the Solana transaction does not decode (${err instanceof Error ? err.message : String(err)})`, "policy_refused");
|
|
561
700
|
}
|
|
562
701
|
}
|
|
563
702
|
|
|
564
703
|
// src/provider.ts
|
|
565
704
|
var import_util3 = require("@anywalletkit/protocol-swap2-core/util");
|
|
566
705
|
var import_validate = require("@anywalletkit/protocol-swap2-core/validate");
|
|
706
|
+
var import_errors2 = require("@anywalletkit/protocol-swap2-core/errors");
|
|
567
707
|
var import_core = require("@anywalletkit/core");
|
|
568
708
|
var DEFAULT_LIFI_BASE = "https://li.quest/v1";
|
|
569
709
|
var LIFI_CHAIN = {
|
|
@@ -604,7 +744,8 @@ var LIFI_CHAIN = {
|
|
|
604
744
|
"eip155:2818": "2818",
|
|
605
745
|
"eip155:14": "14",
|
|
606
746
|
// 2026-11-10: Elysium (Kinetiq; li.quest/v1/chains id 99901, key "ely"). LI.FI's bridges there are Relay (refused
|
|
607
|
-
// below: UNBOUND_TOOLS) and LI.FI Intents
|
|
747
|
+
// below: UNBOUND_TOOLS) and LI.FI Intents (refused too: UNPINNED_TOOLS), so it has no cross-chain LI.FI route for now;
|
|
748
|
+
// same-chain swaps go through GenericSwapFacetV3 (KyberSwap, Nordstern).
|
|
608
749
|
"eip155:99901": "99901",
|
|
609
750
|
[LIFI_SOLANA_CAIP2]: "1151111081099710"
|
|
610
751
|
};
|
|
@@ -614,12 +755,12 @@ var SOLANA_NATIVE = "11111111111111111111111111111111";
|
|
|
614
755
|
var EVM_PLACEHOLDER = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
|
|
615
756
|
var SOLANA_PLACEHOLDER = "HAgk14JpMQLgt6rVgv7cBQFJWFto5Dqxi472uT3DKpqk";
|
|
616
757
|
var isSolana = (chain) => chain === LIFI_SOLANA_CAIP2;
|
|
617
|
-
var
|
|
758
|
+
var BASE582 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
618
759
|
var BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
619
760
|
var SAME_CHAIN_MAX_SECONDS = 90;
|
|
620
761
|
var MAX_INPUT_FEE_BPS = 300;
|
|
621
762
|
function sameChainAddress(chain, actual, expected) {
|
|
622
|
-
if (isSolana(chain)) return !!actual &&
|
|
763
|
+
if (isSolana(chain)) return !!actual && BASE582.test(actual) && actual === expected;
|
|
623
764
|
return sameAddress(actual, expected);
|
|
624
765
|
}
|
|
625
766
|
function nativeFor(chain, token) {
|
|
@@ -708,7 +849,16 @@ function headers(key) {
|
|
|
708
849
|
}
|
|
709
850
|
var TOOL_KEY = /^[A-Za-z0-9_-]{1,64}$/;
|
|
710
851
|
var UNBOUND_TOOLS = Object.freeze(["relaydepository", "garden", "squid", "symbiosis"]);
|
|
711
|
-
|
|
852
|
+
var UNPINNED_TOOLS = Object.freeze(["lifiIntents"]);
|
|
853
|
+
var REFUSED_EVM_TOOLS = Object.freeze([...UNBOUND_TOOLS, ...UNPINNED_TOOLS]);
|
|
854
|
+
var SOLANA_DESTINATION_TOOLS = Object.freeze(["mayan", "mayanMCTP", "mayanFastMCTP", "polymer", "polymerStandard", "chainflip"]);
|
|
855
|
+
var SMART_DEPOSITS_TOOL = /^smartdeposits?$/i;
|
|
856
|
+
var dryRecipient = (params, fromAddress) => !isSolana(params.sourceChain) && isSolana(params.destChain) ? SOLANA_PLACEHOLDER : fromAddress;
|
|
857
|
+
function placeholderIn(params) {
|
|
858
|
+
if (!params.refundAddress) return "sender";
|
|
859
|
+
return !params.destAddress && dryRecipient(params, params.refundAddress) !== params.refundAddress ? "recipient" : null;
|
|
860
|
+
}
|
|
861
|
+
function routeOptions(body, excluded, allowed) {
|
|
712
862
|
const routes = body?.routes;
|
|
713
863
|
if (!Array.isArray(routes)) return [];
|
|
714
864
|
const out = [];
|
|
@@ -718,7 +868,7 @@ function routeOptions(body, excluded) {
|
|
|
718
868
|
const cross = Array.isArray(step.includedSteps) ? step.includedSteps.find((i) => i?.type === "cross")?.tool : void 0;
|
|
719
869
|
const tool = typeof cross === "string" ? cross : step.tool;
|
|
720
870
|
const eta = step.estimate?.executionDuration;
|
|
721
|
-
if (typeof tool !== "string" || !TOOL_KEY.test(tool) || excluded.includes(tool) || typeof eta !== "number" || !Number.isFinite(eta) || eta < 0) continue;
|
|
871
|
+
if (typeof tool !== "string" || !TOOL_KEY.test(tool) || excluded.includes(tool) || allowed && !allowed.includes(tool) || typeof eta !== "number" || !Number.isFinite(eta) || eta < 0) continue;
|
|
722
872
|
if (typeof r.toAmount !== "string" || typeof r.toAmountMin !== "string" || !/^\d{1,60}$/.test(r.toAmount) || !/^\d{1,60}$/.test(r.toAmountMin)) continue;
|
|
723
873
|
out.push({
|
|
724
874
|
tool,
|
|
@@ -731,6 +881,92 @@ function routeOptions(body, excluded) {
|
|
|
731
881
|
}
|
|
732
882
|
return out;
|
|
733
883
|
}
|
|
884
|
+
var LIFI_TOOL_REASONS = {
|
|
885
|
+
NO_POSSIBLE_ROUTE: "no_route",
|
|
886
|
+
INSUFFICIENT_LIQUIDITY: "insufficient_liquidity",
|
|
887
|
+
TOOL_TIMEOUT: "upstream_unavailable",
|
|
888
|
+
RPC_ERROR: "upstream_unavailable",
|
|
889
|
+
AMOUNT_TOO_LOW: "amount_too_small",
|
|
890
|
+
AMOUNT_TOO_HIGH: "amount_too_large",
|
|
891
|
+
FEES_HIGHER_THAN_AMOUNT: "amount_too_small",
|
|
892
|
+
FEES_HGHER_THAN_AMOUNT: "amount_too_small",
|
|
893
|
+
// an older spelling of the same code
|
|
894
|
+
DIFFERENT_RECIPIENT_NOT_SUPPORTED: "unsupported_pair",
|
|
895
|
+
CANNOT_GUARANTEE_MIN_AMOUNT: "price_check_failed"
|
|
896
|
+
};
|
|
897
|
+
var LIFI_ERROR_REASONS = {
|
|
898
|
+
1001: "no_route",
|
|
899
|
+
// FailedToBuildTransactionError: "None of the available routes could successfully generate a tx"
|
|
900
|
+
1002: "no_route",
|
|
901
|
+
// NoQuoteError: "No available quotes for the requested transfer"
|
|
902
|
+
1003: "no_route",
|
|
903
|
+
// NotFoundError
|
|
904
|
+
1005: "rate_limited",
|
|
905
|
+
// RateLimitError
|
|
906
|
+
1006: "upstream_unavailable",
|
|
907
|
+
// ServerError
|
|
908
|
+
1007: "price_check_failed",
|
|
909
|
+
// SlippageError
|
|
910
|
+
1008: "upstream_unavailable",
|
|
911
|
+
// ThirdPartyError
|
|
912
|
+
1009: "upstream_unavailable",
|
|
913
|
+
// TimeoutError
|
|
914
|
+
1010: "key_rejected",
|
|
915
|
+
// UnauthorizedError
|
|
916
|
+
1012: "upstream_unavailable"
|
|
917
|
+
// RpcFailure
|
|
918
|
+
};
|
|
919
|
+
var LIFI_MESSAGE_REASONS = [
|
|
920
|
+
[/no available quotes|unable to find a quote|none of the available routes|no route/i, "no_route"],
|
|
921
|
+
[/rate limit|too many requests/i, "rate_limited"],
|
|
922
|
+
[/api[ -]?key|unauthori[sz]ed/i, "key_rejected"],
|
|
923
|
+
[/insufficient liquidity|not enough liquidity/i, "insufficient_liquidity"],
|
|
924
|
+
[/amount (?:is )?too (?:low|small)|below the minimum/i, "amount_too_small"],
|
|
925
|
+
[/amount (?:is )?too (?:high|large)|above the maximum/i, "amount_too_large"],
|
|
926
|
+
[/not supported|unsupported|unknown token|invalid token/i, "unsupported_pair"],
|
|
927
|
+
[/halted|paused|maintenance/i, "chain_halted"]
|
|
928
|
+
];
|
|
929
|
+
function lifiToolCodes(errors) {
|
|
930
|
+
const out = /* @__PURE__ */ new Set();
|
|
931
|
+
const stack = [errors];
|
|
932
|
+
for (let visited = 0; stack.length > 0 && visited < 256; visited++) {
|
|
933
|
+
const node = stack.pop();
|
|
934
|
+
if (Array.isArray(node)) {
|
|
935
|
+
stack.push(...node.slice(0, 64));
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
if (!node || typeof node !== "object") continue;
|
|
939
|
+
const code = node.code;
|
|
940
|
+
if (typeof code === "string" && Object.prototype.hasOwnProperty.call(LIFI_TOOL_REASONS, code)) out.add(code);
|
|
941
|
+
for (const [key, value] of Object.entries(node).slice(0, 64)) if (key !== "action" && value && typeof value === "object") stack.push(value);
|
|
942
|
+
}
|
|
943
|
+
return [...out];
|
|
944
|
+
}
|
|
945
|
+
function lifiBodyReason(body, filteredOut) {
|
|
946
|
+
const b = body && typeof body === "object" ? body : {};
|
|
947
|
+
const tools = [...new Set(lifiToolCodes(b.errors).map((code) => LIFI_TOOL_REASONS[code]))];
|
|
948
|
+
if (tools.length === 1) return tools[0];
|
|
949
|
+
if (filteredOut.length > 0 && filteredOut.every((r) => /^ROUTE_SLIPPAGE_FLOOR_UNREACHABLE/.test(r))) return "price_check_failed";
|
|
950
|
+
const byCode = typeof b.code === "number" ? LIFI_ERROR_REASONS[b.code] : void 0;
|
|
951
|
+
if (byCode) return byCode;
|
|
952
|
+
const message = typeof b.message === "string" ? b.message : "";
|
|
953
|
+
return LIFI_MESSAGE_REASONS.find(([pattern]) => pattern.test(message))?.[1];
|
|
954
|
+
}
|
|
955
|
+
var LIFI_TRANSIENT_REASONS = /* @__PURE__ */ new Set(["rate_limited", "insufficient_liquidity", "chain_halted"]);
|
|
956
|
+
function lifiHttpRefusal(status, retryAfter, body, filteredOut) {
|
|
957
|
+
const http = (0, import_errors2.httpRefusal)(status, retryAfter);
|
|
958
|
+
if (http && http.reason !== "upstream_unavailable") return http;
|
|
959
|
+
const info = (reason) => {
|
|
960
|
+
if (reason === "rate_limited") {
|
|
961
|
+
const wait = (0, import_errors2.parseRetryAfter)(retryAfter);
|
|
962
|
+
return { reason, httpStatus: status, ...wait !== void 0 ? { retryAfterMs: wait } : {} };
|
|
963
|
+
}
|
|
964
|
+
return reason === "upstream_unavailable" || reason === "key_rejected" ? { reason, httpStatus: status } : { reason };
|
|
965
|
+
};
|
|
966
|
+
const byBody = lifiBodyReason(body, filteredOut);
|
|
967
|
+
if (http) return byBody !== void 0 && LIFI_TRANSIENT_REASONS.has(byBody) ? info(byBody) : http;
|
|
968
|
+
return byBody !== void 0 ? info(byBody) : { reason: "no_route" };
|
|
969
|
+
}
|
|
734
970
|
function cleanChoice(choice) {
|
|
735
971
|
if (!choice) return null;
|
|
736
972
|
const allow = (choice.allowBridges ?? []).filter((t) => TOOL_KEY.test(t));
|
|
@@ -771,8 +1007,10 @@ function lifiProvider(config = {}) {
|
|
|
771
1007
|
return !!LIFI_CHAIN[from.chain] && !!LIFI_CHAIN[to.chain];
|
|
772
1008
|
},
|
|
773
1009
|
async getQuote(params, signal) {
|
|
774
|
-
const reject = (why) => {
|
|
775
|
-
|
|
1010
|
+
const reject = (why, reason) => {
|
|
1011
|
+
const info = typeof reason === "string" ? { reason } : reason;
|
|
1012
|
+
if (info) params.onRefusal?.(`lifi: ${why}`, info);
|
|
1013
|
+
else params.onRefusal?.(`lifi: ${why}`);
|
|
776
1014
|
return null;
|
|
777
1015
|
};
|
|
778
1016
|
if (params.flex) return null;
|
|
@@ -783,13 +1021,15 @@ function lifiProvider(config = {}) {
|
|
|
783
1021
|
if (!fromToken) return null;
|
|
784
1022
|
const solanaSource = isSolana(params.sourceChain);
|
|
785
1023
|
const crossChain = params.sourceChain !== params.destChain;
|
|
1024
|
+
const evmToSolana = !solanaSource && isSolana(params.destChain);
|
|
786
1025
|
const noDestinationCall = crossChain && !solanaSource;
|
|
787
1026
|
let routeScope = config.routeWideSlippage === true && !solanaSource && !isSolana(params.destChain);
|
|
788
1027
|
const dry = params.dry !== false;
|
|
789
1028
|
const fromAddress = params.refundAddress || (dry ? solanaSource ? SOLANA_PLACEHOLDER : EVM_PLACEHOLDER : "");
|
|
790
|
-
const toAddress = params.destAddress || (dry ? fromAddress : "");
|
|
1029
|
+
const toAddress = params.destAddress || (dry ? dryRecipient(params, fromAddress) : "");
|
|
791
1030
|
if (!fromAddress) return reject("a live quote needs the sender's address");
|
|
792
1031
|
if (!toAddress) return reject("a live quote needs a recipient");
|
|
1032
|
+
const placeholder = dry ? placeholderIn(params) : null;
|
|
793
1033
|
const slippageBps = params.slippageBps ?? defaultSlippageBps;
|
|
794
1034
|
let sentSlippage = slippageBps / 1e4;
|
|
795
1035
|
let deadline;
|
|
@@ -810,7 +1050,7 @@ function lifiProvider(config = {}) {
|
|
|
810
1050
|
options: {
|
|
811
1051
|
slippage: slippageBps / 1e4,
|
|
812
1052
|
...routeScope ? { slippageScope: "route" } : {},
|
|
813
|
-
...solanaSource ? {} : { bridges: { deny:
|
|
1053
|
+
...solanaSource ? {} : { bridges: { ...evmToSolana ? { allow: SOLANA_DESTINATION_TOOLS } : {}, deny: REFUSED_EVM_TOOLS } },
|
|
814
1054
|
...noDestinationCall ? { allowDestinationCall: false } : {},
|
|
815
1055
|
...integrator().trim() ? { integrator: integrator().trim() } : {}
|
|
816
1056
|
}
|
|
@@ -818,7 +1058,7 @@ function lifiProvider(config = {}) {
|
|
|
818
1058
|
signal: signal ?? (0, import_util3.timeoutSignal)(8e3)
|
|
819
1059
|
});
|
|
820
1060
|
if (res.ok) {
|
|
821
|
-
const options = routeOptions(await res.json(), solanaSource ? [] :
|
|
1061
|
+
const options = routeOptions(await res.json(), solanaSource ? [] : REFUSED_EVM_TOOLS, evmToSolana ? SOLANA_DESTINATION_TOOLS : void 0);
|
|
822
1062
|
if (options.length) choice = cleanChoice(config.chooseRoute(options, params));
|
|
823
1063
|
}
|
|
824
1064
|
} catch {
|
|
@@ -840,8 +1080,10 @@ function lifiProvider(config = {}) {
|
|
|
840
1080
|
}
|
|
841
1081
|
if (routeScope) url.searchParams.set("slippageScope", "route");
|
|
842
1082
|
if (noDestinationCall) url.searchParams.set("allowDestinationCall", "false");
|
|
843
|
-
const unbound = crossChain && !solanaSource ?
|
|
844
|
-
|
|
1083
|
+
const unbound = crossChain && !solanaSource ? REFUSED_EVM_TOOLS : [];
|
|
1084
|
+
const allow = (choice?.allowBridges ?? []).filter((b) => solanaSource || !REFUSED_EVM_TOOLS.includes(b));
|
|
1085
|
+
const solanaAllow = evmToSolana ? allow.filter((b) => SOLANA_DESTINATION_TOOLS.includes(b)) : [];
|
|
1086
|
+
for (const t of evmToSolana ? solanaAllow.length ? solanaAllow : SOLANA_DESTINATION_TOOLS : allow) url.searchParams.append("allowBridges", t);
|
|
845
1087
|
for (const t of /* @__PURE__ */ new Set([...choice?.denyBridges ?? [], ...unbound])) url.searchParams.append("denyBridges", t);
|
|
846
1088
|
if (choice?.order) url.searchParams.set("order", choice.order);
|
|
847
1089
|
deadline = signal ? void 0 : (0, import_util3.timeoutSignal)(12e3);
|
|
@@ -860,18 +1102,28 @@ function lifiProvider(config = {}) {
|
|
|
860
1102
|
continue;
|
|
861
1103
|
}
|
|
862
1104
|
const why = reasons.length === 1 ? ` (${reasons[0].slice(0, 200)})` : "";
|
|
863
|
-
params.onRefusal?.(
|
|
1105
|
+
params.onRefusal?.(
|
|
1106
|
+
`lifi: ${typeof body?.message === "string" ? body.message.slice(0, 200) : `HTTP ${res.status}`}${why}`,
|
|
1107
|
+
lifiHttpRefusal(res.status, res.headers?.get?.("retry-after"), body, reasons)
|
|
1108
|
+
);
|
|
864
1109
|
return null;
|
|
865
1110
|
}
|
|
866
1111
|
const q = await res.json();
|
|
867
1112
|
const receivedAt = Date.now();
|
|
1113
|
+
if (evmToSolana) {
|
|
1114
|
+
const steps = q.includedSteps;
|
|
1115
|
+
const tools = [q.tool, ...Array.isArray(steps) ? steps.map((i) => i?.tool) : []];
|
|
1116
|
+
if (tools.some((t) => typeof t === "string" && SMART_DEPOSITS_TOOL.test(t))) {
|
|
1117
|
+
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");
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
868
1120
|
const action = q.action;
|
|
869
1121
|
const est = q.estimate;
|
|
870
1122
|
const tr = q.transactionRequest;
|
|
871
1123
|
const expectedToToken = nativeFor(params.destChain, params.destToken);
|
|
872
1124
|
const expectedFromChain = Number(fromChain);
|
|
873
1125
|
const expectedToChain = Number(toChain);
|
|
874
|
-
if (q.type !== "lifi" || !q.id || !action || !est?.toAmount || !est.fromAmount || !tr) return reject("incomplete quote (no action, estimate or transaction)");
|
|
1126
|
+
if (q.type !== "lifi" || !q.id || !action || !est?.toAmount || !est.fromAmount || !tr) return reject("incomplete quote (no action, estimate or transaction)", "upstream_unavailable");
|
|
875
1127
|
const mismatch = [
|
|
876
1128
|
action.fromChainId !== expectedFromChain && "source chain",
|
|
877
1129
|
action.toChainId !== expectedToChain && "destination chain",
|
|
@@ -881,41 +1133,53 @@ function lifiProvider(config = {}) {
|
|
|
881
1133
|
!sameChainAddress(params.sourceChain, action.fromAddress, fromAddress) && "sender",
|
|
882
1134
|
!sameChainAddress(params.destChain, action.toAddress, toAddress) && "recipient"
|
|
883
1135
|
].filter(Boolean);
|
|
884
|
-
if (mismatch.length)
|
|
1136
|
+
if (mismatch.length) {
|
|
1137
|
+
return reject(
|
|
1138
|
+
`quote does not match the request (${mismatch.join(", ")})`,
|
|
1139
|
+
// LI.FI refunds to the sender it was asked for (fromAddress): either address differing pays or refunds someone else.
|
|
1140
|
+
mismatch.includes("recipient") || mismatch.includes("sender") ? "recipient_mismatch" : "policy_refused"
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
885
1143
|
const bridged = BigInt(est.fromAmount);
|
|
886
1144
|
const spent = BigInt(params.amountIn);
|
|
887
|
-
if (bridged > spent) return reject(`the estimate bridges ${est.fromAmount}, more than the ${params.amountIn} spent
|
|
1145
|
+
if (bridged > spent) return reject(`the estimate bridges ${est.fromAmount}, more than the ${params.amountIn} spent`, "price_check_failed");
|
|
888
1146
|
if ((spent - bridged) * 10000n > spent * BigInt(MAX_INPUT_FEE_BPS)) {
|
|
889
|
-
return reject(`the route takes ${(0, import_core.bigintToNumber)((spent - bridged) * 10000n / spent) / 100}% of the input as a fee (over ${MAX_INPUT_FEE_BPS / 100}%)
|
|
1147
|
+
return reject(`the route takes ${(0, import_core.bigintToNumber)((spent - bridged) * 10000n / spent) / 100}% of the input as a fee (over ${MAX_INPUT_FEE_BPS / 100}%)`, "policy_refused");
|
|
890
1148
|
}
|
|
891
1149
|
if (solanaSource) {
|
|
892
|
-
if (!tr.data || tr.data.length < 64 || tr.data.length % 4 !== 0 || !BASE64.test(tr.data)) return reject("the Solana transaction is not base64");
|
|
893
|
-
if (tr.to !== void 0 || tr.value !== void 0 || tr.gasLimit !== void 0) return reject("the Solana transaction carries EVM fields");
|
|
1150
|
+
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");
|
|
1151
|
+
if (tr.to !== void 0 || tr.value !== void 0 || tr.gasLimit !== void 0) return reject("the Solana transaction carries EVM fields", "upstream_unavailable");
|
|
894
1152
|
if (fromChain !== toChain) {
|
|
895
1153
|
const why = solanaBridgeTxMismatch(tr.data, fromAddress);
|
|
896
|
-
if (why) return reject(why);
|
|
1154
|
+
if (why) return reject(why, reasonOf(why));
|
|
897
1155
|
}
|
|
898
|
-
} 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))
|
|
899
|
-
|
|
1156
|
+
} 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)) {
|
|
1157
|
+
return reject(
|
|
1158
|
+
"the transaction is malformed or targets an untrusted contract",
|
|
1159
|
+
!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"
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
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");
|
|
900
1163
|
const sourceIsToken = fromToken !== EVM_ZERO && fromToken !== SOLANA_NATIVE;
|
|
901
1164
|
if (solanaSource) {
|
|
902
1165
|
} else if (sourceIsToken) {
|
|
903
|
-
if (!trustedAddress(params.sourceChain, est.approvalAddress, trustedApprovalAddresses)) return reject("the approval target is not a trusted LI.FI contract");
|
|
1166
|
+
if (!trustedAddress(params.sourceChain, est.approvalAddress, trustedApprovalAddresses)) return reject("the approval target is not a trusted LI.FI contract", "vault_mismatch");
|
|
904
1167
|
} else if (est.approvalAddress !== void 0) {
|
|
905
|
-
if (!trustedAddress(params.sourceChain, est.approvalAddress, trustedApprovalAddresses)) return reject("the approval target is not a trusted LI.FI contract");
|
|
1168
|
+
if (!trustedAddress(params.sourceChain, est.approvalAddress, trustedApprovalAddresses)) return reject("the approval target is not a trusted LI.FI contract", "vault_mismatch");
|
|
906
1169
|
}
|
|
907
1170
|
let txRequest;
|
|
1171
|
+
let nativeFee = 0n;
|
|
908
1172
|
if (solanaSource) {
|
|
909
1173
|
txRequest = { serializedTx: tr.data, value: sourceIsToken ? "0" : params.amountIn };
|
|
910
1174
|
} else {
|
|
911
1175
|
try {
|
|
912
|
-
if (BigInt(tr.value) < 0n || BigInt(tr.gasLimit) <= 0n) return reject("the transaction has a negative value or no gas limit");
|
|
1176
|
+
if (BigInt(tr.value) < 0n || BigInt(tr.gasLimit) <= 0n) return reject("the transaction has a negative value or no gas limit", "upstream_unavailable");
|
|
913
1177
|
} catch {
|
|
914
|
-
return reject("the transaction value or gas limit is not a number");
|
|
1178
|
+
return reject("the transaction value or gas limit is not a number", "upstream_unavailable");
|
|
915
1179
|
}
|
|
916
1180
|
const value = BigInt(tr.value);
|
|
917
|
-
|
|
918
|
-
if (!sourceIsToken && value < BigInt(params.amountIn)) return reject("the transaction sends less native coin than the input");
|
|
1181
|
+
nativeFee = sourceIsToken ? value : value > BigInt(params.amountIn) ? value - BigInt(params.amountIn) : 0n;
|
|
1182
|
+
if (!sourceIsToken && value < BigInt(params.amountIn)) return reject("the transaction sends less native coin than the input", "policy_refused");
|
|
919
1183
|
txRequest = {
|
|
920
1184
|
to: tr.to,
|
|
921
1185
|
data: tr.data,
|
|
@@ -927,7 +1191,7 @@ function lifiProvider(config = {}) {
|
|
|
927
1191
|
};
|
|
928
1192
|
}
|
|
929
1193
|
const feeUsd = (est.feeCosts ?? []).reduce((a, f) => a + Number(f.amountUSD ?? 0), 0);
|
|
930
|
-
if (!est.toAmountMin) return reject("the quote has no minimum output");
|
|
1194
|
+
if (!est.toAmountMin) return reject("the quote has no minimum output", "upstream_unavailable");
|
|
931
1195
|
const minDestAmount = est.toAmountMin;
|
|
932
1196
|
if (sentSlippage !== void 0) {
|
|
933
1197
|
const dest = BigInt(est.toAmount);
|
|
@@ -941,18 +1205,22 @@ function lifiProvider(config = {}) {
|
|
|
941
1205
|
continue;
|
|
942
1206
|
}
|
|
943
1207
|
params.onRefusal?.(
|
|
944
|
-
`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}
|
|
1208
|
+
`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}%`,
|
|
1209
|
+
{ reason: "price_check_failed" }
|
|
945
1210
|
);
|
|
946
1211
|
return null;
|
|
947
1212
|
}
|
|
948
1213
|
}
|
|
949
1214
|
if (params.minAmountOut !== void 0 && BigInt(minDestAmount) < BigInt(params.minAmountOut)) {
|
|
950
|
-
params.onRefusal?.(`lifi: floor breach (min=${minDestAmount} < floor=${params.minAmountOut})
|
|
1215
|
+
params.onRefusal?.(`lifi: floor breach (min=${minDestAmount} < floor=${params.minAmountOut})`, { reason: "price_check_failed" });
|
|
951
1216
|
return null;
|
|
952
1217
|
}
|
|
953
1218
|
const duration = est.executionDuration ?? 0;
|
|
954
1219
|
if (fromChain === toChain && duration > SAME_CHAIN_MAX_SECONDS) {
|
|
955
|
-
params.onRefusal?.(
|
|
1220
|
+
params.onRefusal?.(
|
|
1221
|
+
`lifi: same-chain route settles asynchronously (${duration} s via ${q.tool ?? "unknown tool"}) \u2014 a same-chain swap must deliver in the same transaction`,
|
|
1222
|
+
{ reason: "unsupported_pair" }
|
|
1223
|
+
);
|
|
956
1224
|
return null;
|
|
957
1225
|
}
|
|
958
1226
|
if (!solanaSource) {
|
|
@@ -967,7 +1235,7 @@ function lifiProvider(config = {}) {
|
|
|
967
1235
|
sameChain: fromChain === toChain,
|
|
968
1236
|
evmDestination: !isSolana(params.destChain)
|
|
969
1237
|
});
|
|
970
|
-
if (why) return reject(`the transaction does not match the quote: ${why}
|
|
1238
|
+
if (why) return reject(`the transaction does not match the quote: ${why}`, reasonOf(why) ?? "policy_refused");
|
|
971
1239
|
}
|
|
972
1240
|
return {
|
|
973
1241
|
provider: "lifi",
|
|
@@ -975,7 +1243,10 @@ function lifiProvider(config = {}) {
|
|
|
975
1243
|
intentId: `lifi:${fromChain}:${toChain}`,
|
|
976
1244
|
depositAddress: "",
|
|
977
1245
|
depositMemo: null,
|
|
978
|
-
|
|
1246
|
+
// Every check above ran on LI.FI's transaction; one built for a placeholder is not handed out (placeholderIn).
|
|
1247
|
+
// Its bridge fee on top of the input still is, as `providerData.nativeFee`, so the router's value nets it out
|
|
1248
|
+
// (usdValue reads it when there is no txRequest) instead of ranking the route on its gross output.
|
|
1249
|
+
...placeholder ? { providerData: { placeholder, ...nativeFee > 0n ? { nativeFee: nativeFee.toString() } : {} } } : { txRequest },
|
|
979
1250
|
// What the wallet spends (action.fromAmount, = the input); an input-side bridge fee is inside destAmount.
|
|
980
1251
|
sourceAmount: params.amountIn,
|
|
981
1252
|
destAmount: est.toAmount,
|
|
@@ -989,14 +1260,18 @@ function lifiProvider(config = {}) {
|
|
|
989
1260
|
validUntilMs: receivedAt + quoteTtlMs
|
|
990
1261
|
};
|
|
991
1262
|
} catch {
|
|
992
|
-
if (deadline?.aborted) params.onRefusal?.("lifi: no quote within 12 s");
|
|
1263
|
+
if (deadline?.aborted) params.onRefusal?.("lifi: no quote within 12 s", { reason: "upstream_unavailable" });
|
|
993
1264
|
return null;
|
|
994
1265
|
}
|
|
995
1266
|
return null;
|
|
996
1267
|
},
|
|
997
1268
|
async buildDeposit(quote) {
|
|
1269
|
+
const placeholder = quote.providerData?.placeholder;
|
|
1270
|
+
if (placeholder !== void 0) {
|
|
1271
|
+
throw (0, import_errors2.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`);
|
|
1272
|
+
}
|
|
998
1273
|
const tx = quote.txRequest;
|
|
999
|
-
if (!tx) throw
|
|
1274
|
+
if (!tx) throw (0, import_errors2.refusal)("policy_refused", "swap2: LI.FI quote has no transaction request");
|
|
1000
1275
|
const [, fromChain] = quote.intentId.split(":");
|
|
1001
1276
|
return {
|
|
1002
1277
|
chain: tx.serializedTx ? CAIP2_BY_LIFI_CHAIN[fromChain ?? ""] ?? "" : "",
|
|
@@ -1045,22 +1320,27 @@ function lifiProvider(config = {}) {
|
|
|
1045
1320
|
function lifiVerifyQuote(params, quote) {
|
|
1046
1321
|
const fromChain = LIFI_CHAIN[params.sourceChain];
|
|
1047
1322
|
const toChain = LIFI_CHAIN[params.destChain];
|
|
1048
|
-
if (!fromChain || !toChain) return "not a LI.FI chain pair";
|
|
1323
|
+
if (!fromChain || !toChain) return note("not a LI.FI chain pair", "unsupported_pair");
|
|
1324
|
+
const placeholder = params.dry !== false ? placeholderIn(params) : null;
|
|
1325
|
+
if (placeholder) {
|
|
1326
|
+
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");
|
|
1327
|
+
return priceOnlyMismatch(params, quote, `lifi:${fromChain}:${toChain}`);
|
|
1328
|
+
}
|
|
1049
1329
|
const tx = quote.txRequest;
|
|
1050
|
-
if (!tx) return "the quote carries no transaction";
|
|
1330
|
+
if (!tx) return note("the quote carries no transaction", "policy_refused");
|
|
1051
1331
|
if (isSolana(params.sourceChain)) {
|
|
1052
|
-
if (!tx.serializedTx || tx.data !== void 0 || tx.to !== void 0) return "a Solana source carries a serialized transaction only";
|
|
1332
|
+
if (!tx.serializedTx || tx.data !== void 0 || tx.to !== void 0) return note("a Solana source carries a serialized transaction only", "policy_refused");
|
|
1053
1333
|
return fromChain === toChain ? null : solanaBridgeTxMismatch(tx.serializedTx, params.refundAddress || (params.dry !== false ? SOLANA_PLACEHOLDER : ""));
|
|
1054
1334
|
}
|
|
1055
|
-
if (!isHexData(tx.data) || !tx.to || tx.serializedTx !== void 0) return "the quote carries no Diamond call";
|
|
1056
|
-
if (!sameAddress(tx.to, diamondFor(params.sourceChain))) return `the call targets ${tx.to}, not the LI.FI Diamond
|
|
1057
|
-
if (tx.chainId !== void 0 && String(tx.chainId) !== fromChain) return `the call is for chain ${tx.chainId}, not ${fromChain}
|
|
1335
|
+
if (!isHexData(tx.data) || !tx.to || tx.serializedTx !== void 0) return note("the quote carries no Diamond call", "policy_refused");
|
|
1336
|
+
if (!sameAddress(tx.to, diamondFor(params.sourceChain))) return note(`the call targets ${tx.to}, not the LI.FI Diamond`, "vault_mismatch");
|
|
1337
|
+
if (tx.chainId !== void 0 && String(tx.chainId) !== fromChain) return note(`the call is for chain ${tx.chainId}, not ${fromChain}`, "policy_refused");
|
|
1058
1338
|
const fromToken = lifiToken(params);
|
|
1059
|
-
if (!fromToken) return `unknown source token ${params.sourceToken}
|
|
1339
|
+
if (!fromToken) return note(`unknown source token ${params.sourceToken}`, "unsupported_pair");
|
|
1060
1340
|
const sourceIsToken = fromToken !== EVM_ZERO;
|
|
1061
1341
|
const dry = params.dry !== false;
|
|
1062
1342
|
const fromAddress = params.refundAddress || (dry ? EVM_PLACEHOLDER : "");
|
|
1063
|
-
const toAddress = params.destAddress || (dry ? fromAddress : "");
|
|
1343
|
+
const toAddress = params.destAddress || (dry ? dryRecipient(params, fromAddress) : "");
|
|
1064
1344
|
if (!fromAddress || !toAddress) return "a live quote needs the sender and the recipient";
|
|
1065
1345
|
let amountIn, minOut, value, nativeFee;
|
|
1066
1346
|
try {
|
|
@@ -1069,18 +1349,18 @@ function lifiVerifyQuote(params, quote) {
|
|
|
1069
1349
|
value = BigInt(tx.value ?? "0");
|
|
1070
1350
|
nativeFee = BigInt(tx.nativeFee ?? "0");
|
|
1071
1351
|
} catch {
|
|
1072
|
-
return "the quote's amounts are not integers";
|
|
1352
|
+
return note("the quote's amounts are not integers", "upstream_unavailable");
|
|
1073
1353
|
}
|
|
1074
|
-
if (BigInt(quote.sourceAmount) !== amountIn) return `the quote spends ${quote.sourceAmount}, not the ${amountIn} input
|
|
1075
|
-
if (params.minAmountOut !== void 0 && minOut < BigInt(params.minAmountOut)) return `the quoted minimum ${minOut} is below the caller's floor ${params.minAmountOut}
|
|
1354
|
+
if (BigInt(quote.sourceAmount) !== amountIn) return note(`the quote spends ${quote.sourceAmount}, not the ${amountIn} input`, "policy_refused");
|
|
1355
|
+
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");
|
|
1076
1356
|
if (sourceIsToken) {
|
|
1077
|
-
if (value !== nativeFee) return "a token input attaches only its declared bridge fee";
|
|
1078
|
-
if (!tx.approvalAddress || !sameAddress(tx.approvalAddress, diamondFor(params.sourceChain))) return "a token input approves the LI.FI Diamond";
|
|
1357
|
+
if (value !== nativeFee) return note("a token input attaches only its declared bridge fee", "policy_refused");
|
|
1358
|
+
if (!tx.approvalAddress || !sameAddress(tx.approvalAddress, diamondFor(params.sourceChain))) return note("a token input approves the LI.FI Diamond", "vault_mismatch");
|
|
1079
1359
|
} else {
|
|
1080
|
-
if (value < amountIn || value - amountIn !== nativeFee) return "a native input is paid as the input plus its declared bridge fee";
|
|
1081
|
-
if (tx.approvalAddress !== void 0) return "a native input approves nothing";
|
|
1360
|
+
if (value < amountIn || value - amountIn !== nativeFee) return note("a native input is paid as the input plus its declared bridge fee", "policy_refused");
|
|
1361
|
+
if (tx.approvalAddress !== void 0) return note("a native input approves nothing", "vault_mismatch");
|
|
1082
1362
|
}
|
|
1083
|
-
if (quote.intentId.replace(/~[0-9a-f]{32}$/, "") !== `lifi:${fromChain}:${toChain}`) return "the intent id does not name this route";
|
|
1363
|
+
if (quote.intentId.replace(/~[0-9a-f]{32}$/, "") !== `lifi:${fromChain}:${toChain}`) return note("the intent id does not name this route", "policy_refused");
|
|
1084
1364
|
const why = diamondCallMismatch(tx.data, {
|
|
1085
1365
|
fromToken,
|
|
1086
1366
|
toToken: nativeFor(params.destChain, params.destToken),
|
|
@@ -1092,12 +1372,27 @@ function lifiVerifyQuote(params, quote) {
|
|
|
1092
1372
|
sameChain: fromChain === toChain,
|
|
1093
1373
|
evmDestination: !isSolana(params.destChain)
|
|
1094
1374
|
});
|
|
1095
|
-
return why ? `the transaction does not match the request: ${why}
|
|
1375
|
+
return why ? note(`the transaction does not match the request: ${why}`, reasonOf(why) ?? "policy_refused") : null;
|
|
1376
|
+
}
|
|
1377
|
+
function priceOnlyMismatch(params, quote, route) {
|
|
1378
|
+
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");
|
|
1379
|
+
const fee = quote.providerData?.nativeFee;
|
|
1380
|
+
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");
|
|
1381
|
+
try {
|
|
1382
|
+
if (BigInt(quote.sourceAmount) !== BigInt(params.amountIn)) return note(`the quote spends ${quote.sourceAmount}, not the ${params.amountIn} input`, "policy_refused");
|
|
1383
|
+
const minOut = BigInt(quote.minDestAmount);
|
|
1384
|
+
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");
|
|
1385
|
+
} catch {
|
|
1386
|
+
return note("the quote's amounts are not integers", "upstream_unavailable");
|
|
1387
|
+
}
|
|
1388
|
+
return quote.intentId.replace(/~[0-9a-f]{32}$/, "") === route ? null : note("the intent id does not name this route", "policy_refused");
|
|
1096
1389
|
}
|
|
1097
1390
|
var lifiPins = Object.freeze({
|
|
1098
1391
|
callTargets: (chain) => /^eip155:[0-9]+$/.test(chain) ? [diamondFor(chain)] : [],
|
|
1099
1392
|
spenders: (chain) => /^eip155:[0-9]+$/.test(chain) ? [diamondFor(chain)] : [],
|
|
1100
|
-
verifyQuote: lifiVerifyQuote
|
|
1393
|
+
verifyQuote: lifiVerifyQuote,
|
|
1394
|
+
/** The reason of a text {@link lifiVerifyQuote} returned (`remoteProvider` puts it on the stub's refusal). */
|
|
1395
|
+
reasonOf: (text) => reasonOf(text)
|
|
1101
1396
|
});
|
|
1102
1397
|
|
|
1103
1398
|
// src/protocol.ts
|