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