@anywalletkit/protocol-swap2-lifi 0.8.0 → 0.8.2
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/dist/{chunk-GWUGK4SG.js → chunk-G26GE46T.js} +230 -115
- package/dist/chunk-G26GE46T.js.map +1 -0
- package/dist/{chunk-OYGVC7DJ.js → chunk-TCPUWYDL.js} +2 -2
- package/dist/index.cjs +229 -114
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/protocol.cjs +229 -114
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.js +2 -2
- package/dist/provider.cjs +229 -114
- package/dist/provider.cjs.map +1 -1
- package/dist/provider.js +1 -1
- package/package.json +3 -3
- package/dist/chunk-GWUGK4SG.js.map +0 -1
- /package/dist/{chunk-OYGVC7DJ.js.map → chunk-TCPUWYDL.js.map} +0 -0
package/dist/protocol.cjs
CHANGED
|
@@ -137,6 +137,12 @@ function associatedTokenAccount(owner, mint, tokenProgram = solanaKey(SOLANA_TOK
|
|
|
137
137
|
return programAddress([owner, tokenProgram, mint], solanaKey(ASSOCIATED_TOKEN_PROGRAM));
|
|
138
138
|
}
|
|
139
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
|
+
|
|
140
146
|
// src/calldata.ts
|
|
141
147
|
var GENERIC_SWAPS = {
|
|
142
148
|
"0x4630a0d8": { single: false },
|
|
@@ -356,18 +362,18 @@ function swapsAt(r, base, head) {
|
|
|
356
362
|
function checkSteps(steps, e, finalAsset) {
|
|
357
363
|
const first = steps[0];
|
|
358
364
|
const last = steps[steps.length - 1];
|
|
359
|
-
if (!same(first.sendingAssetId, e.fromToken)) return `spends ${first.sendingAssetId}, not ${e.fromToken}
|
|
360
|
-
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");
|
|
361
367
|
let deposited = 0n;
|
|
362
368
|
for (let i = 0; i < steps.length; i++) {
|
|
363
369
|
const step = steps[i];
|
|
364
|
-
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");
|
|
365
371
|
if (!step.requiresDeposit) continue;
|
|
366
|
-
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");
|
|
367
373
|
deposited += step.fromAmount;
|
|
368
374
|
}
|
|
369
|
-
if (deposited > e.amountIn) return `the steps pull ${deposited} from the wallet, more than the ${e.amountIn} spent
|
|
370
|
-
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");
|
|
371
377
|
return null;
|
|
372
378
|
}
|
|
373
379
|
var MAYAN_REFUND_AT = {
|
|
@@ -408,32 +414,32 @@ function acrossSwapRefunds(r, at) {
|
|
|
408
414
|
}
|
|
409
415
|
var ERC20_TRANSFER = "0xa9059cbb";
|
|
410
416
|
function ecoDelivery(r, at, e) {
|
|
411
|
-
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");
|
|
412
418
|
const destination = r.word(at + 32 * 3);
|
|
413
|
-
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");
|
|
414
420
|
const route = r.bytes(at, at + 32 * 4);
|
|
415
421
|
const tuple = route.small(0);
|
|
416
422
|
const calls = tuple + route.small(tuple + 32 * 5);
|
|
417
423
|
const n = route.small(calls);
|
|
418
|
-
if (n === 0) return "the Eco route makes no calls on the destination chain";
|
|
419
|
-
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");
|
|
420
426
|
const last = calls + 32 + route.small(calls + 32 + 32 * (n - 1));
|
|
421
427
|
const target = route.address(last);
|
|
422
428
|
const data = route.bytes(last, last + 32);
|
|
423
|
-
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");
|
|
424
430
|
const to = data.address(4);
|
|
425
431
|
const amount = data.word(36);
|
|
426
|
-
if (!same(target, e.toToken)) return `the Eco route delivers ${target}, not ${e.toToken}
|
|
427
|
-
if (!same(to, e.receiver)) return `the Eco route pays ${to}, not ${e.receiver}
|
|
428
|
-
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");
|
|
429
435
|
return null;
|
|
430
436
|
}
|
|
431
437
|
function m0Delivery(r, at, e) {
|
|
432
|
-
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");
|
|
433
439
|
const tokenOut = r.address(at + 32 * 3);
|
|
434
440
|
const amountOut = r.word(at + 32 * 5);
|
|
435
|
-
if (!same(tokenOut, e.toToken)) return `the M0 order asks for ${tokenOut}, not ${e.toToken}
|
|
436
|
-
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");
|
|
437
443
|
return null;
|
|
438
444
|
}
|
|
439
445
|
var FACET_DATA = {
|
|
@@ -467,13 +473,13 @@ function facetDataAt(r, bridge, spec) {
|
|
|
467
473
|
}
|
|
468
474
|
function refundMismatch(r, bridge, e) {
|
|
469
475
|
const spec = FACET_DATA[bridge.facet];
|
|
470
|
-
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");
|
|
471
477
|
const at = facetDataAt(r, bridge, spec);
|
|
472
478
|
const paid = (spec.refunds ?? []).map(([field, word]) => [`${bridge.facet} ${field}`, r.address(at + 32 * word), spec.zeroRefused === true]);
|
|
473
479
|
paid.push(...spec.nested?.(r, at) ?? []);
|
|
474
480
|
for (const [what, to, zeroRefused] of paid) {
|
|
475
481
|
if (zeroRefused && /^0x0{40}$/.test(to)) continue;
|
|
476
|
-
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");
|
|
477
483
|
}
|
|
478
484
|
return null;
|
|
479
485
|
}
|
|
@@ -505,36 +511,36 @@ var hexOf = (b) => `0x${Array.from(b, (x) => x.toString(16).padStart(2, "0")).jo
|
|
|
505
511
|
var keyOf = (word) => base58(Uint8Array.from(word.slice(2).match(/../g) ?? [], (h) => Number.parseInt(h, 16)));
|
|
506
512
|
function mayanToSolana(r, at, t) {
|
|
507
513
|
const declared = r.raw(at, 32);
|
|
508
|
-
if (declared !== t.ownerHex) return `Mayan's nonEVMReceiver is ${keyOf(declared)}, not the recipient ${t.e.receiver}
|
|
514
|
+
if (declared !== t.ownerHex) return note(`Mayan's nonEVMReceiver is ${keyOf(declared)}, not the recipient ${t.e.receiver}`, "recipient_mismatch");
|
|
509
515
|
const p = r.bytes(at, at + 32 * 2);
|
|
510
516
|
const selector = p.raw(0, 4);
|
|
511
517
|
const o = MAYAN_SOLANA[selector];
|
|
512
|
-
if (!o) return `the Mayan call ${selector} is an entry point whose Solana recipient cannot be checked here
|
|
518
|
+
if (!o) return note(`the Mayan call ${selector} is an entry point whose Solana recipient cannot be checked here`, "recipient_unverifiable");
|
|
513
519
|
const word = (i) => 4 + 32 * i;
|
|
514
|
-
if (o.payloadType && p.word(word(o.payloadType[0])) !== o.payloadType[1]) return `the Mayan call ${selector} carries payload type ${p.word(word(o.payloadType[0]))}, not a plain delivery
|
|
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");
|
|
515
521
|
const dest = p.raw(word(o.destAddr), 32);
|
|
516
|
-
if (dest !== t.ownerHex) return `the Mayan order pays ${keyOf(dest)} on Solana, not the recipient ${t.e.receiver}
|
|
522
|
+
if (dest !== t.ownerHex) return note(`the Mayan order pays ${keyOf(dest)} on Solana, not the recipient ${t.e.receiver}`, "recipient_mismatch");
|
|
517
523
|
const chain = p.word(word(o.chain));
|
|
518
|
-
if (chain !== (o.cctp ? CCTP_DOMAIN_SOLANA : WORMHOLE_SOLANA)) return `the Mayan order goes to ${o.cctp ? "CCTP domain" : "Wormhole chain"} ${chain}, not Solana
|
|
519
|
-
if (o.tokenOut === void 0) return t.e.toToken === SOLANA_USDC ? null : `a Mayan CCTP transfer delivers USDC, not ${t.e.toToken}
|
|
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");
|
|
520
526
|
const token = p.raw(word(o.tokenOut), 32);
|
|
521
|
-
return token === t.mintHex ? null : `the Mayan order delivers ${keyOf(token)}, not ${t.e.toToken}
|
|
527
|
+
return token === t.mintHex ? null : note(`the Mayan order delivers ${keyOf(token)}, not ${t.e.toToken}`, "policy_refused");
|
|
522
528
|
}
|
|
523
529
|
function polymerToSolana(r, at, t) {
|
|
524
530
|
const declared = r.raw(at + 32 * 2, 32);
|
|
525
|
-
if (declared !== t.ownerHex) return `Polymer CCTP's nonEVMReceiver is ${keyOf(declared)}, not the recipient ${t.e.receiver}
|
|
526
|
-
if (t.e.toToken !== SOLANA_USDC) return `Polymer CCTP delivers USDC, not ${t.e.toToken}
|
|
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");
|
|
527
533
|
const mintTo = r.raw(at + 32 * 3, 32);
|
|
528
534
|
const account = hexOf(associatedTokenAccount(t.owner, t.mint));
|
|
529
|
-
return mintTo === account ? null : `the Polymer CCTP transfer mints to ${keyOf(mintTo)}, not the recipient's USDC account ${keyOf(account)}
|
|
535
|
+
return mintTo === account ? null : note(`the Polymer CCTP transfer mints to ${keyOf(mintTo)}, not the recipient's USDC account ${keyOf(account)}`, "recipient_mismatch");
|
|
530
536
|
}
|
|
531
537
|
function chainflipToSolana(r, at, t) {
|
|
532
538
|
const dst = r.bytes(at, at);
|
|
533
|
-
if (dst.length !== 32) return `Chainflip pays a ${dst.length}-byte address on Solana, not the recipient ${t.e.receiver}
|
|
534
|
-
if (dst.raw(0, 32) !== t.ownerHex) return `Chainflip pays ${keyOf(dst.raw(0, 32))} on Solana, not the recipient ${t.e.receiver}
|
|
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");
|
|
535
541
|
const asset = r.word(at + 32);
|
|
536
542
|
const want = CHAINFLIP_SOLANA_ASSETS[t.e.toToken];
|
|
537
|
-
return want !== void 0 && asset === want ? null : `Chainflip delivers its asset ${asset}, not ${t.e.toToken}
|
|
543
|
+
return want !== void 0 && asset === want ? null : note(`Chainflip delivers its asset ${asset}, not ${t.e.toToken}`, "policy_refused");
|
|
538
544
|
}
|
|
539
545
|
var SOLANA_DELIVERY = {
|
|
540
546
|
Mayan: mayanToSolana,
|
|
@@ -545,16 +551,16 @@ var INTENTS_TO_SOLANA = "a LI.FI Intents order to Solana names its own oracles,
|
|
|
545
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";
|
|
546
552
|
var INTENTS_FACET = "LiFiIntentEscrowV2";
|
|
547
553
|
function solanaRecipientMismatch(r, bridge, receiver, e) {
|
|
548
|
-
if (e.destChainId !== LIFI_SOLANA_CHAIN_ID) return NON_EVM_UNREAD;
|
|
549
|
-
if (!same(receiver, NON_EVM_ADDRESS)) return `BridgeData pays ${receiver}, not LI.FI's non-EVM placeholder ${NON_EVM_ADDRESS}, on a Solana route
|
|
550
|
-
if (DEPOSIT_FACETS.has(bridge.facet)) return `${bridge.facet} pays a deposit address whose off-chain order names the Solana recipient \u2014 the transaction does not bind it
|
|
551
|
-
if (bridge.facet === INTENTS_FACET) return INTENTS_TO_SOLANA;
|
|
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");
|
|
552
558
|
const read = SOLANA_DELIVERY[bridge.facet];
|
|
553
|
-
if (!read) return NON_EVM_UNREAD;
|
|
559
|
+
if (!read) return note(NON_EVM_UNREAD, "recipient_unverifiable");
|
|
554
560
|
const owner = solanaKey(e.receiver);
|
|
555
|
-
if (!owner) return `the recipient ${e.receiver} is not a Solana address
|
|
561
|
+
if (!owner) return note(`the recipient ${e.receiver} is not a Solana address`, "recipient_mismatch");
|
|
556
562
|
const mint = solanaKey(e.toToken);
|
|
557
|
-
if (!mint) return `the destination token ${e.toToken} is not a Solana mint
|
|
563
|
+
if (!mint) return note(`the destination token ${e.toToken} is not a Solana mint`, "unsupported_pair");
|
|
558
564
|
return read(r, facetDataAt(r, bridge, FACET_DATA[bridge.facet]), { owner, ownerHex: hexOf(owner), mint, mintHex: hexOf(mint), e });
|
|
559
565
|
}
|
|
560
566
|
function diamondCallMismatch(data, e) {
|
|
@@ -562,61 +568,61 @@ function diamondCallMismatch(data, e) {
|
|
|
562
568
|
try {
|
|
563
569
|
r = new Reader(data);
|
|
564
570
|
} catch {
|
|
565
|
-
return "the calldata is not hex";
|
|
571
|
+
return note("the calldata is not hex", "upstream_unavailable");
|
|
566
572
|
}
|
|
567
|
-
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");
|
|
568
574
|
const selector = data.slice(0, 10).toLowerCase();
|
|
569
575
|
const generic = GENERIC_SWAPS[selector];
|
|
570
576
|
try {
|
|
571
577
|
if (generic) {
|
|
572
|
-
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");
|
|
573
579
|
const receiver2 = r.address(4 + 96);
|
|
574
580
|
const minAmount2 = r.word(4 + 128);
|
|
575
581
|
const steps = generic.single ? [swapAt(r, 4 + r.small(4 + 160))] : swapsAt(r, 4, 4 + 160);
|
|
576
|
-
if (!e.evmDestination || !same(receiver2, e.receiver)) return `pays ${receiver2}, not ${e.receiver}
|
|
577
|
-
if (minAmount2 < e.minOut) return `accepts ${minAmount2}, below the ${e.minOut} shown
|
|
578
|
-
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");
|
|
579
585
|
if (generic.to && (kindOf(e.toToken) !== generic.to || kindOf(steps[steps.length - 1].receivingAssetId) !== generic.to)) {
|
|
580
|
-
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");
|
|
581
587
|
}
|
|
582
588
|
return checkSteps(steps, e, e.toToken);
|
|
583
589
|
}
|
|
584
590
|
const unbound = UNBOUND_BRIDGES[selector];
|
|
585
|
-
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");
|
|
586
592
|
const bridge = BRIDGE_CALLS[selector];
|
|
587
|
-
if (!bridge) return `unknown Diamond call ${selector}
|
|
588
|
-
if (bridge.facet === INTENTS_FACET && e.evmDestination) return INTENTS_UNPINNED;
|
|
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");
|
|
589
595
|
const head = r.small(4);
|
|
590
|
-
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");
|
|
591
597
|
const at = 4 + head;
|
|
592
|
-
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");
|
|
593
599
|
const sendingAssetId = r.address(at + 128);
|
|
594
600
|
const receiver = r.address(at + 160);
|
|
595
601
|
const minAmount = r.word(at + 192);
|
|
596
602
|
const destinationChainId = r.word(at + 224);
|
|
597
603
|
const hasSourceSwaps = r.bool(at + 256);
|
|
598
604
|
const hasDestinationCall = r.bool(at + 288);
|
|
599
|
-
if (e.sameChain) return "a bridge call on a same-chain route";
|
|
600
|
-
if (destinationChainId !== e.destChainId) return `bridges to chain ${destinationChainId}, not ${e.destChainId}
|
|
601
|
-
if (hasDestinationCall) return "the route runs a call on the destination chain, whose recipient cannot be checked here";
|
|
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");
|
|
602
608
|
if (!e.evmDestination) {
|
|
603
609
|
const why = solanaRecipientMismatch(r, bridge, receiver, e);
|
|
604
610
|
if (why) return why;
|
|
605
|
-
} else if (!same(receiver, e.receiver)) return `pays ${receiver}, not ${e.receiver}
|
|
606
|
-
if (hasSourceSwaps !== bridge.sourceSwaps) return `unknown Diamond call ${selector}
|
|
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");
|
|
607
613
|
const refund = refundMismatch(r, bridge, e);
|
|
608
614
|
if (refund) return refund;
|
|
609
615
|
const spec = FACET_DATA[bridge.facet];
|
|
610
616
|
const delivery = spec?.delivery?.(r, facetDataAt(r, bridge, spec), e);
|
|
611
617
|
if (delivery) return delivery;
|
|
612
618
|
if (hasSourceSwaps) {
|
|
613
|
-
if (minAmount === 0n) return "bridges nothing after the source swaps";
|
|
619
|
+
if (minAmount === 0n) return note("bridges nothing after the source swaps", "policy_refused");
|
|
614
620
|
return checkSteps(swapsAt(r, 4, 4 + 32), e, sendingAssetId);
|
|
615
621
|
}
|
|
616
|
-
if (minAmount > e.amountIn || minAmount * 10000n < e.amountIn * (10000n - BRIDGE_INPUT_FEE_MAX_BPS)) return `bridges ${minAmount}, not the ${e.amountIn} spent
|
|
617
|
-
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");
|
|
618
624
|
} catch (err) {
|
|
619
|
-
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");
|
|
620
626
|
}
|
|
621
627
|
}
|
|
622
628
|
|
|
@@ -665,7 +671,7 @@ var Cursor = class {
|
|
|
665
671
|
};
|
|
666
672
|
function solanaBridgeTxMismatch(base64, sender) {
|
|
667
673
|
const bytes = (0, import_util2.base64Bytes)(base64);
|
|
668
|
-
if (!bytes) return "the Solana transaction is not base64";
|
|
674
|
+
if (!bytes) return note("the Solana transaction is not base64", "upstream_unavailable");
|
|
669
675
|
const c = new Cursor(bytes);
|
|
670
676
|
try {
|
|
671
677
|
c.bytes(64 * c.short());
|
|
@@ -673,7 +679,7 @@ function solanaBridgeTxMismatch(base64, sender) {
|
|
|
673
679
|
if ((first & 128) === 0) c.at--;
|
|
674
680
|
c.bytes(3);
|
|
675
681
|
const keys = Array.from({ length: c.short() }, () => base58(c.bytes(32)));
|
|
676
|
-
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");
|
|
677
683
|
c.bytes(32);
|
|
678
684
|
let mayan = false;
|
|
679
685
|
const payees = [];
|
|
@@ -688,15 +694,16 @@ function solanaBridgeTxMismatch(base64, sender) {
|
|
|
688
694
|
if (to !== void 0 && to !== sender && to !== LIFI_SOLANA_FEE_WALLET) payees.push(to);
|
|
689
695
|
}
|
|
690
696
|
}
|
|
691
|
-
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;
|
|
692
698
|
} catch (err) {
|
|
693
|
-
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");
|
|
694
700
|
}
|
|
695
701
|
}
|
|
696
702
|
|
|
697
703
|
// src/provider.ts
|
|
698
704
|
var import_util3 = require("@anywalletkit/protocol-swap2-core/util");
|
|
699
705
|
var import_validate = require("@anywalletkit/protocol-swap2-core/validate");
|
|
706
|
+
var import_errors2 = require("@anywalletkit/protocol-swap2-core/errors");
|
|
700
707
|
var import_core = require("@anywalletkit/core");
|
|
701
708
|
var DEFAULT_LIFI_BASE = "https://li.quest/v1";
|
|
702
709
|
var LIFI_CHAIN = {
|
|
@@ -874,6 +881,92 @@ function routeOptions(body, excluded, allowed) {
|
|
|
874
881
|
}
|
|
875
882
|
return out;
|
|
876
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
|
+
}
|
|
877
970
|
function cleanChoice(choice) {
|
|
878
971
|
if (!choice) return null;
|
|
879
972
|
const allow = (choice.allowBridges ?? []).filter((t) => TOOL_KEY.test(t));
|
|
@@ -914,8 +1007,10 @@ function lifiProvider(config = {}) {
|
|
|
914
1007
|
return !!LIFI_CHAIN[from.chain] && !!LIFI_CHAIN[to.chain];
|
|
915
1008
|
},
|
|
916
1009
|
async getQuote(params, signal) {
|
|
917
|
-
const reject = (why) => {
|
|
918
|
-
|
|
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}`);
|
|
919
1014
|
return null;
|
|
920
1015
|
};
|
|
921
1016
|
if (params.flex) return null;
|
|
@@ -1007,7 +1102,10 @@ function lifiProvider(config = {}) {
|
|
|
1007
1102
|
continue;
|
|
1008
1103
|
}
|
|
1009
1104
|
const why = reasons.length === 1 ? ` (${reasons[0].slice(0, 200)})` : "";
|
|
1010
|
-
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
|
+
);
|
|
1011
1109
|
return null;
|
|
1012
1110
|
}
|
|
1013
1111
|
const q = await res.json();
|
|
@@ -1016,7 +1114,7 @@ function lifiProvider(config = {}) {
|
|
|
1016
1114
|
const steps = q.includedSteps;
|
|
1017
1115
|
const tools = [q.tool, ...Array.isArray(steps) ? steps.map((i) => i?.tool) : []];
|
|
1018
1116
|
if (tools.some((t) => typeof t === "string" && SMART_DEPOSITS_TOOL.test(t))) {
|
|
1019
|
-
return reject("LI.FI Smart Deposits pay a deposit address whose recipient the transaction does not bind \u2014 refused for a Solana destination");
|
|
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");
|
|
1020
1118
|
}
|
|
1021
1119
|
}
|
|
1022
1120
|
const action = q.action;
|
|
@@ -1025,7 +1123,7 @@ function lifiProvider(config = {}) {
|
|
|
1025
1123
|
const expectedToToken = nativeFor(params.destChain, params.destToken);
|
|
1026
1124
|
const expectedFromChain = Number(fromChain);
|
|
1027
1125
|
const expectedToChain = Number(toChain);
|
|
1028
|
-
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");
|
|
1029
1127
|
const mismatch = [
|
|
1030
1128
|
action.fromChainId !== expectedFromChain && "source chain",
|
|
1031
1129
|
action.toChainId !== expectedToChain && "destination chain",
|
|
@@ -1035,28 +1133,39 @@ function lifiProvider(config = {}) {
|
|
|
1035
1133
|
!sameChainAddress(params.sourceChain, action.fromAddress, fromAddress) && "sender",
|
|
1036
1134
|
!sameChainAddress(params.destChain, action.toAddress, toAddress) && "recipient"
|
|
1037
1135
|
].filter(Boolean);
|
|
1038
|
-
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
|
+
}
|
|
1039
1143
|
const bridged = BigInt(est.fromAmount);
|
|
1040
1144
|
const spent = BigInt(params.amountIn);
|
|
1041
|
-
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");
|
|
1042
1146
|
if ((spent - bridged) * 10000n > spent * BigInt(MAX_INPUT_FEE_BPS)) {
|
|
1043
|
-
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");
|
|
1044
1148
|
}
|
|
1045
1149
|
if (solanaSource) {
|
|
1046
|
-
if (!tr.data || tr.data.length < 64 || tr.data.length % 4 !== 0 || !BASE64.test(tr.data)) return reject("the Solana transaction is not base64");
|
|
1047
|
-
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");
|
|
1048
1152
|
if (fromChain !== toChain) {
|
|
1049
1153
|
const why = solanaBridgeTxMismatch(tr.data, fromAddress);
|
|
1050
|
-
if (why) return reject(why);
|
|
1154
|
+
if (why) return reject(why, reasonOf(why));
|
|
1051
1155
|
}
|
|
1052
|
-
} 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))
|
|
1053
|
-
|
|
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");
|
|
1054
1163
|
const sourceIsToken = fromToken !== EVM_ZERO && fromToken !== SOLANA_NATIVE;
|
|
1055
1164
|
if (solanaSource) {
|
|
1056
1165
|
} else if (sourceIsToken) {
|
|
1057
|
-
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");
|
|
1058
1167
|
} else if (est.approvalAddress !== void 0) {
|
|
1059
|
-
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");
|
|
1060
1169
|
}
|
|
1061
1170
|
let txRequest;
|
|
1062
1171
|
let nativeFee = 0n;
|
|
@@ -1064,13 +1173,13 @@ function lifiProvider(config = {}) {
|
|
|
1064
1173
|
txRequest = { serializedTx: tr.data, value: sourceIsToken ? "0" : params.amountIn };
|
|
1065
1174
|
} else {
|
|
1066
1175
|
try {
|
|
1067
|
-
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");
|
|
1068
1177
|
} catch {
|
|
1069
|
-
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");
|
|
1070
1179
|
}
|
|
1071
1180
|
const value = BigInt(tr.value);
|
|
1072
1181
|
nativeFee = sourceIsToken ? value : value > BigInt(params.amountIn) ? value - BigInt(params.amountIn) : 0n;
|
|
1073
|
-
if (!sourceIsToken && value < BigInt(params.amountIn)) return reject("the transaction sends less native coin than the input");
|
|
1182
|
+
if (!sourceIsToken && value < BigInt(params.amountIn)) return reject("the transaction sends less native coin than the input", "policy_refused");
|
|
1074
1183
|
txRequest = {
|
|
1075
1184
|
to: tr.to,
|
|
1076
1185
|
data: tr.data,
|
|
@@ -1082,7 +1191,7 @@ function lifiProvider(config = {}) {
|
|
|
1082
1191
|
};
|
|
1083
1192
|
}
|
|
1084
1193
|
const feeUsd = (est.feeCosts ?? []).reduce((a, f) => a + Number(f.amountUSD ?? 0), 0);
|
|
1085
|
-
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");
|
|
1086
1195
|
const minDestAmount = est.toAmountMin;
|
|
1087
1196
|
if (sentSlippage !== void 0) {
|
|
1088
1197
|
const dest = BigInt(est.toAmount);
|
|
@@ -1096,18 +1205,22 @@ function lifiProvider(config = {}) {
|
|
|
1096
1205
|
continue;
|
|
1097
1206
|
}
|
|
1098
1207
|
params.onRefusal?.(
|
|
1099
|
-
`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" }
|
|
1100
1210
|
);
|
|
1101
1211
|
return null;
|
|
1102
1212
|
}
|
|
1103
1213
|
}
|
|
1104
1214
|
if (params.minAmountOut !== void 0 && BigInt(minDestAmount) < BigInt(params.minAmountOut)) {
|
|
1105
|
-
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" });
|
|
1106
1216
|
return null;
|
|
1107
1217
|
}
|
|
1108
1218
|
const duration = est.executionDuration ?? 0;
|
|
1109
1219
|
if (fromChain === toChain && duration > SAME_CHAIN_MAX_SECONDS) {
|
|
1110
|
-
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
|
+
);
|
|
1111
1224
|
return null;
|
|
1112
1225
|
}
|
|
1113
1226
|
if (!solanaSource) {
|
|
@@ -1122,7 +1235,7 @@ function lifiProvider(config = {}) {
|
|
|
1122
1235
|
sameChain: fromChain === toChain,
|
|
1123
1236
|
evmDestination: !isSolana(params.destChain)
|
|
1124
1237
|
});
|
|
1125
|
-
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");
|
|
1126
1239
|
}
|
|
1127
1240
|
return {
|
|
1128
1241
|
provider: "lifi",
|
|
@@ -1147,7 +1260,7 @@ function lifiProvider(config = {}) {
|
|
|
1147
1260
|
validUntilMs: receivedAt + quoteTtlMs
|
|
1148
1261
|
};
|
|
1149
1262
|
} catch {
|
|
1150
|
-
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" });
|
|
1151
1264
|
return null;
|
|
1152
1265
|
}
|
|
1153
1266
|
return null;
|
|
@@ -1155,10 +1268,10 @@ function lifiProvider(config = {}) {
|
|
|
1155
1268
|
async buildDeposit(quote) {
|
|
1156
1269
|
const placeholder = quote.providerData?.placeholder;
|
|
1157
1270
|
if (placeholder !== void 0) {
|
|
1158
|
-
throw
|
|
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`);
|
|
1159
1272
|
}
|
|
1160
1273
|
const tx = quote.txRequest;
|
|
1161
|
-
if (!tx) throw
|
|
1274
|
+
if (!tx) throw (0, import_errors2.refusal)("policy_refused", "swap2: LI.FI quote has no transaction request");
|
|
1162
1275
|
const [, fromChain] = quote.intentId.split(":");
|
|
1163
1276
|
return {
|
|
1164
1277
|
chain: tx.serializedTx ? CAIP2_BY_LIFI_CHAIN[fromChain ?? ""] ?? "" : "",
|
|
@@ -1207,23 +1320,23 @@ function lifiProvider(config = {}) {
|
|
|
1207
1320
|
function lifiVerifyQuote(params, quote) {
|
|
1208
1321
|
const fromChain = LIFI_CHAIN[params.sourceChain];
|
|
1209
1322
|
const toChain = LIFI_CHAIN[params.destChain];
|
|
1210
|
-
if (!fromChain || !toChain) return "not a LI.FI chain pair";
|
|
1323
|
+
if (!fromChain || !toChain) return note("not a LI.FI chain pair", "unsupported_pair");
|
|
1211
1324
|
const placeholder = params.dry !== false ? placeholderIn(params) : null;
|
|
1212
1325
|
if (placeholder) {
|
|
1213
|
-
if (quote.txRequest !== void 0) return `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
|
|
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");
|
|
1214
1327
|
return priceOnlyMismatch(params, quote, `lifi:${fromChain}:${toChain}`);
|
|
1215
1328
|
}
|
|
1216
1329
|
const tx = quote.txRequest;
|
|
1217
|
-
if (!tx) return "the quote carries no transaction";
|
|
1330
|
+
if (!tx) return note("the quote carries no transaction", "policy_refused");
|
|
1218
1331
|
if (isSolana(params.sourceChain)) {
|
|
1219
|
-
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");
|
|
1220
1333
|
return fromChain === toChain ? null : solanaBridgeTxMismatch(tx.serializedTx, params.refundAddress || (params.dry !== false ? SOLANA_PLACEHOLDER : ""));
|
|
1221
1334
|
}
|
|
1222
|
-
if (!isHexData(tx.data) || !tx.to || tx.serializedTx !== void 0) return "the quote carries no Diamond call";
|
|
1223
|
-
if (!sameAddress(tx.to, diamondFor(params.sourceChain))) return `the call targets ${tx.to}, not the LI.FI Diamond
|
|
1224
|
-
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");
|
|
1225
1338
|
const fromToken = lifiToken(params);
|
|
1226
|
-
if (!fromToken) return `unknown source token ${params.sourceToken}
|
|
1339
|
+
if (!fromToken) return note(`unknown source token ${params.sourceToken}`, "unsupported_pair");
|
|
1227
1340
|
const sourceIsToken = fromToken !== EVM_ZERO;
|
|
1228
1341
|
const dry = params.dry !== false;
|
|
1229
1342
|
const fromAddress = params.refundAddress || (dry ? EVM_PLACEHOLDER : "");
|
|
@@ -1236,18 +1349,18 @@ function lifiVerifyQuote(params, quote) {
|
|
|
1236
1349
|
value = BigInt(tx.value ?? "0");
|
|
1237
1350
|
nativeFee = BigInt(tx.nativeFee ?? "0");
|
|
1238
1351
|
} catch {
|
|
1239
|
-
return "the quote's amounts are not integers";
|
|
1352
|
+
return note("the quote's amounts are not integers", "upstream_unavailable");
|
|
1240
1353
|
}
|
|
1241
|
-
if (BigInt(quote.sourceAmount) !== amountIn) return `the quote spends ${quote.sourceAmount}, not the ${amountIn} input
|
|
1242
|
-
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");
|
|
1243
1356
|
if (sourceIsToken) {
|
|
1244
|
-
if (value !== nativeFee) return "a token input attaches only its declared bridge fee";
|
|
1245
|
-
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");
|
|
1246
1359
|
} else {
|
|
1247
|
-
if (value < amountIn || value - amountIn !== nativeFee) return "a native input is paid as the input plus its declared bridge fee";
|
|
1248
|
-
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");
|
|
1249
1362
|
}
|
|
1250
|
-
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");
|
|
1251
1364
|
const why = diamondCallMismatch(tx.data, {
|
|
1252
1365
|
fromToken,
|
|
1253
1366
|
toToken: nativeFor(params.destChain, params.destToken),
|
|
@@ -1259,25 +1372,27 @@ function lifiVerifyQuote(params, quote) {
|
|
|
1259
1372
|
sameChain: fromChain === toChain,
|
|
1260
1373
|
evmDestination: !isSolana(params.destChain)
|
|
1261
1374
|
});
|
|
1262
|
-
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;
|
|
1263
1376
|
}
|
|
1264
1377
|
function priceOnlyMismatch(params, quote, route) {
|
|
1265
|
-
if (quote.depositAddress !== "") return `a LI.FI quote pays no deposit address, but this one names ${String(quote.depositAddress).slice(0, 64)}
|
|
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");
|
|
1266
1379
|
const fee = quote.providerData?.nativeFee;
|
|
1267
|
-
if (fee !== void 0 && !/^\d{1,80}$/.test(String(fee))) return `the price-only quote declares a native fee of ${String(fee).slice(0, 40)}, not a base-unit integer
|
|
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");
|
|
1268
1381
|
try {
|
|
1269
|
-
if (BigInt(quote.sourceAmount) !== BigInt(params.amountIn)) return `the quote spends ${quote.sourceAmount}, not the ${params.amountIn} input
|
|
1382
|
+
if (BigInt(quote.sourceAmount) !== BigInt(params.amountIn)) return note(`the quote spends ${quote.sourceAmount}, not the ${params.amountIn} input`, "policy_refused");
|
|
1270
1383
|
const minOut = BigInt(quote.minDestAmount);
|
|
1271
|
-
if (params.minAmountOut !== void 0 && minOut < BigInt(params.minAmountOut)) return `the quoted minimum ${minOut} is below the caller's floor ${params.minAmountOut}
|
|
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");
|
|
1272
1385
|
} catch {
|
|
1273
|
-
return "the quote's amounts are not integers";
|
|
1386
|
+
return note("the quote's amounts are not integers", "upstream_unavailable");
|
|
1274
1387
|
}
|
|
1275
|
-
return quote.intentId.replace(/~[0-9a-f]{32}$/, "") === route ? null : "the intent id does not name this route";
|
|
1388
|
+
return quote.intentId.replace(/~[0-9a-f]{32}$/, "") === route ? null : note("the intent id does not name this route", "policy_refused");
|
|
1276
1389
|
}
|
|
1277
1390
|
var lifiPins = Object.freeze({
|
|
1278
1391
|
callTargets: (chain) => /^eip155:[0-9]+$/.test(chain) ? [diamondFor(chain)] : [],
|
|
1279
1392
|
spenders: (chain) => /^eip155:[0-9]+$/.test(chain) ? [diamondFor(chain)] : [],
|
|
1280
|
-
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)
|
|
1281
1396
|
});
|
|
1282
1397
|
|
|
1283
1398
|
// src/protocol.ts
|