@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/index.cjs
CHANGED
|
@@ -145,6 +145,12 @@ function associatedTokenAccount(owner, mint, tokenProgram = solanaKey(SOLANA_TOK
|
|
|
145
145
|
return programAddress([owner, tokenProgram, mint], solanaKey(ASSOCIATED_TOKEN_PROGRAM));
|
|
146
146
|
}
|
|
147
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
|
+
|
|
148
154
|
// src/calldata.ts
|
|
149
155
|
var GENERIC_SWAPS = {
|
|
150
156
|
"0x4630a0d8": { single: false },
|
|
@@ -364,18 +370,18 @@ function swapsAt(r, base, head) {
|
|
|
364
370
|
function checkSteps(steps, e, finalAsset) {
|
|
365
371
|
const first = steps[0];
|
|
366
372
|
const last = steps[steps.length - 1];
|
|
367
|
-
if (!same(first.sendingAssetId, e.fromToken)) return `spends ${first.sendingAssetId}, not ${e.fromToken}
|
|
368
|
-
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");
|
|
369
375
|
let deposited = 0n;
|
|
370
376
|
for (let i = 0; i < steps.length; i++) {
|
|
371
377
|
const step = steps[i];
|
|
372
|
-
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");
|
|
373
379
|
if (!step.requiresDeposit) continue;
|
|
374
|
-
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");
|
|
375
381
|
deposited += step.fromAmount;
|
|
376
382
|
}
|
|
377
|
-
if (deposited > e.amountIn) return `the steps pull ${deposited} from the wallet, more than the ${e.amountIn} spent
|
|
378
|
-
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");
|
|
379
385
|
return null;
|
|
380
386
|
}
|
|
381
387
|
var MAYAN_REFUND_AT = {
|
|
@@ -416,32 +422,32 @@ function acrossSwapRefunds(r, at) {
|
|
|
416
422
|
}
|
|
417
423
|
var ERC20_TRANSFER = "0xa9059cbb";
|
|
418
424
|
function ecoDelivery(r, at, e) {
|
|
419
|
-
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");
|
|
420
426
|
const destination = r.word(at + 32 * 3);
|
|
421
|
-
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");
|
|
422
428
|
const route = r.bytes(at, at + 32 * 4);
|
|
423
429
|
const tuple = route.small(0);
|
|
424
430
|
const calls = tuple + route.small(tuple + 32 * 5);
|
|
425
431
|
const n = route.small(calls);
|
|
426
|
-
if (n === 0) return "the Eco route makes no calls on the destination chain";
|
|
427
|
-
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");
|
|
428
434
|
const last = calls + 32 + route.small(calls + 32 + 32 * (n - 1));
|
|
429
435
|
const target = route.address(last);
|
|
430
436
|
const data = route.bytes(last, last + 32);
|
|
431
|
-
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");
|
|
432
438
|
const to = data.address(4);
|
|
433
439
|
const amount = data.word(36);
|
|
434
|
-
if (!same(target, e.toToken)) return `the Eco route delivers ${target}, not ${e.toToken}
|
|
435
|
-
if (!same(to, e.receiver)) return `the Eco route pays ${to}, not ${e.receiver}
|
|
436
|
-
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");
|
|
437
443
|
return null;
|
|
438
444
|
}
|
|
439
445
|
function m0Delivery(r, at, e) {
|
|
440
|
-
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");
|
|
441
447
|
const tokenOut = r.address(at + 32 * 3);
|
|
442
448
|
const amountOut = r.word(at + 32 * 5);
|
|
443
|
-
if (!same(tokenOut, e.toToken)) return `the M0 order asks for ${tokenOut}, not ${e.toToken}
|
|
444
|
-
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");
|
|
445
451
|
return null;
|
|
446
452
|
}
|
|
447
453
|
var FACET_DATA = {
|
|
@@ -475,13 +481,13 @@ function facetDataAt(r, bridge, spec) {
|
|
|
475
481
|
}
|
|
476
482
|
function refundMismatch(r, bridge, e) {
|
|
477
483
|
const spec = FACET_DATA[bridge.facet];
|
|
478
|
-
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");
|
|
479
485
|
const at = facetDataAt(r, bridge, spec);
|
|
480
486
|
const paid = (spec.refunds ?? []).map(([field, word]) => [`${bridge.facet} ${field}`, r.address(at + 32 * word), spec.zeroRefused === true]);
|
|
481
487
|
paid.push(...spec.nested?.(r, at) ?? []);
|
|
482
488
|
for (const [what, to, zeroRefused] of paid) {
|
|
483
489
|
if (zeroRefused && /^0x0{40}$/.test(to)) continue;
|
|
484
|
-
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");
|
|
485
491
|
}
|
|
486
492
|
return null;
|
|
487
493
|
}
|
|
@@ -513,36 +519,36 @@ var hexOf = (b) => `0x${Array.from(b, (x) => x.toString(16).padStart(2, "0")).jo
|
|
|
513
519
|
var keyOf = (word) => base58(Uint8Array.from(word.slice(2).match(/../g) ?? [], (h) => Number.parseInt(h, 16)));
|
|
514
520
|
function mayanToSolana(r, at, t) {
|
|
515
521
|
const declared = r.raw(at, 32);
|
|
516
|
-
if (declared !== t.ownerHex) return `Mayan's nonEVMReceiver is ${keyOf(declared)}, not the recipient ${t.e.receiver}
|
|
522
|
+
if (declared !== t.ownerHex) return note(`Mayan's nonEVMReceiver is ${keyOf(declared)}, not the recipient ${t.e.receiver}`, "recipient_mismatch");
|
|
517
523
|
const p = r.bytes(at, at + 32 * 2);
|
|
518
524
|
const selector = p.raw(0, 4);
|
|
519
525
|
const o = MAYAN_SOLANA[selector];
|
|
520
|
-
if (!o) return `the Mayan call ${selector} is an entry point whose Solana recipient cannot be checked here
|
|
526
|
+
if (!o) return note(`the Mayan call ${selector} is an entry point whose Solana recipient cannot be checked here`, "recipient_unverifiable");
|
|
521
527
|
const word = (i) => 4 + 32 * i;
|
|
522
|
-
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
|
|
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");
|
|
523
529
|
const dest = p.raw(word(o.destAddr), 32);
|
|
524
|
-
if (dest !== t.ownerHex) return `the Mayan order pays ${keyOf(dest)} on Solana, not the recipient ${t.e.receiver}
|
|
530
|
+
if (dest !== t.ownerHex) return note(`the Mayan order pays ${keyOf(dest)} on Solana, not the recipient ${t.e.receiver}`, "recipient_mismatch");
|
|
525
531
|
const chain = p.word(word(o.chain));
|
|
526
|
-
if (chain !== (o.cctp ? CCTP_DOMAIN_SOLANA : WORMHOLE_SOLANA)) return `the Mayan order goes to ${o.cctp ? "CCTP domain" : "Wormhole chain"} ${chain}, not Solana
|
|
527
|
-
if (o.tokenOut === void 0) return t.e.toToken === SOLANA_USDC ? null : `a Mayan CCTP transfer delivers USDC, not ${t.e.toToken}
|
|
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");
|
|
528
534
|
const token = p.raw(word(o.tokenOut), 32);
|
|
529
|
-
return token === t.mintHex ? null : `the Mayan order delivers ${keyOf(token)}, not ${t.e.toToken}
|
|
535
|
+
return token === t.mintHex ? null : note(`the Mayan order delivers ${keyOf(token)}, not ${t.e.toToken}`, "policy_refused");
|
|
530
536
|
}
|
|
531
537
|
function polymerToSolana(r, at, t) {
|
|
532
538
|
const declared = r.raw(at + 32 * 2, 32);
|
|
533
|
-
if (declared !== t.ownerHex) return `Polymer CCTP's nonEVMReceiver is ${keyOf(declared)}, not the recipient ${t.e.receiver}
|
|
534
|
-
if (t.e.toToken !== SOLANA_USDC) return `Polymer CCTP delivers USDC, not ${t.e.toToken}
|
|
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");
|
|
535
541
|
const mintTo = r.raw(at + 32 * 3, 32);
|
|
536
542
|
const account = hexOf(associatedTokenAccount(t.owner, t.mint));
|
|
537
|
-
return mintTo === account ? null : `the Polymer CCTP transfer mints to ${keyOf(mintTo)}, not the recipient's USDC account ${keyOf(account)}
|
|
543
|
+
return mintTo === account ? null : note(`the Polymer CCTP transfer mints to ${keyOf(mintTo)}, not the recipient's USDC account ${keyOf(account)}`, "recipient_mismatch");
|
|
538
544
|
}
|
|
539
545
|
function chainflipToSolana(r, at, t) {
|
|
540
546
|
const dst = r.bytes(at, at);
|
|
541
|
-
if (dst.length !== 32) return `Chainflip pays a ${dst.length}-byte address on Solana, not the recipient ${t.e.receiver}
|
|
542
|
-
if (dst.raw(0, 32) !== t.ownerHex) return `Chainflip pays ${keyOf(dst.raw(0, 32))} on Solana, not the recipient ${t.e.receiver}
|
|
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");
|
|
543
549
|
const asset = r.word(at + 32);
|
|
544
550
|
const want = CHAINFLIP_SOLANA_ASSETS[t.e.toToken];
|
|
545
|
-
return want !== void 0 && asset === want ? null : `Chainflip delivers its asset ${asset}, not ${t.e.toToken}
|
|
551
|
+
return want !== void 0 && asset === want ? null : note(`Chainflip delivers its asset ${asset}, not ${t.e.toToken}`, "policy_refused");
|
|
546
552
|
}
|
|
547
553
|
var SOLANA_DELIVERY = {
|
|
548
554
|
Mayan: mayanToSolana,
|
|
@@ -553,16 +559,16 @@ var INTENTS_TO_SOLANA = "a LI.FI Intents order to Solana names its own oracles,
|
|
|
553
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";
|
|
554
560
|
var INTENTS_FACET = "LiFiIntentEscrowV2";
|
|
555
561
|
function solanaRecipientMismatch(r, bridge, receiver, e) {
|
|
556
|
-
if (e.destChainId !== LIFI_SOLANA_CHAIN_ID) return NON_EVM_UNREAD;
|
|
557
|
-
if (!same(receiver, NON_EVM_ADDRESS)) return `BridgeData pays ${receiver}, not LI.FI's non-EVM placeholder ${NON_EVM_ADDRESS}, on a Solana route
|
|
558
|
-
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
|
|
559
|
-
if (bridge.facet === INTENTS_FACET) return INTENTS_TO_SOLANA;
|
|
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");
|
|
560
566
|
const read = SOLANA_DELIVERY[bridge.facet];
|
|
561
|
-
if (!read) return NON_EVM_UNREAD;
|
|
567
|
+
if (!read) return note(NON_EVM_UNREAD, "recipient_unverifiable");
|
|
562
568
|
const owner = solanaKey(e.receiver);
|
|
563
|
-
if (!owner) return `the recipient ${e.receiver} is not a Solana address
|
|
569
|
+
if (!owner) return note(`the recipient ${e.receiver} is not a Solana address`, "recipient_mismatch");
|
|
564
570
|
const mint = solanaKey(e.toToken);
|
|
565
|
-
if (!mint) return `the destination token ${e.toToken} is not a Solana mint
|
|
571
|
+
if (!mint) return note(`the destination token ${e.toToken} is not a Solana mint`, "unsupported_pair");
|
|
566
572
|
return read(r, facetDataAt(r, bridge, FACET_DATA[bridge.facet]), { owner, ownerHex: hexOf(owner), mint, mintHex: hexOf(mint), e });
|
|
567
573
|
}
|
|
568
574
|
function diamondCallMismatch(data, e) {
|
|
@@ -570,61 +576,61 @@ function diamondCallMismatch(data, e) {
|
|
|
570
576
|
try {
|
|
571
577
|
r = new Reader(data);
|
|
572
578
|
} catch {
|
|
573
|
-
return "the calldata is not hex";
|
|
579
|
+
return note("the calldata is not hex", "upstream_unavailable");
|
|
574
580
|
}
|
|
575
|
-
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");
|
|
576
582
|
const selector = data.slice(0, 10).toLowerCase();
|
|
577
583
|
const generic = GENERIC_SWAPS[selector];
|
|
578
584
|
try {
|
|
579
585
|
if (generic) {
|
|
580
|
-
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");
|
|
581
587
|
const receiver2 = r.address(4 + 96);
|
|
582
588
|
const minAmount2 = r.word(4 + 128);
|
|
583
589
|
const steps = generic.single ? [swapAt(r, 4 + r.small(4 + 160))] : swapsAt(r, 4, 4 + 160);
|
|
584
|
-
if (!e.evmDestination || !same(receiver2, e.receiver)) return `pays ${receiver2}, not ${e.receiver}
|
|
585
|
-
if (minAmount2 < e.minOut) return `accepts ${minAmount2}, below the ${e.minOut} shown
|
|
586
|
-
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");
|
|
587
593
|
if (generic.to && (kindOf(e.toToken) !== generic.to || kindOf(steps[steps.length - 1].receivingAssetId) !== generic.to)) {
|
|
588
|
-
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");
|
|
589
595
|
}
|
|
590
596
|
return checkSteps(steps, e, e.toToken);
|
|
591
597
|
}
|
|
592
598
|
const unbound = UNBOUND_BRIDGES[selector];
|
|
593
|
-
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");
|
|
594
600
|
const bridge = BRIDGE_CALLS[selector];
|
|
595
|
-
if (!bridge) return `unknown Diamond call ${selector}
|
|
596
|
-
if (bridge.facet === INTENTS_FACET && e.evmDestination) return INTENTS_UNPINNED;
|
|
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");
|
|
597
603
|
const head = r.small(4);
|
|
598
|
-
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");
|
|
599
605
|
const at = 4 + head;
|
|
600
|
-
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");
|
|
601
607
|
const sendingAssetId = r.address(at + 128);
|
|
602
608
|
const receiver = r.address(at + 160);
|
|
603
609
|
const minAmount = r.word(at + 192);
|
|
604
610
|
const destinationChainId = r.word(at + 224);
|
|
605
611
|
const hasSourceSwaps = r.bool(at + 256);
|
|
606
612
|
const hasDestinationCall = r.bool(at + 288);
|
|
607
|
-
if (e.sameChain) return "a bridge call on a same-chain route";
|
|
608
|
-
if (destinationChainId !== e.destChainId) return `bridges to chain ${destinationChainId}, not ${e.destChainId}
|
|
609
|
-
if (hasDestinationCall) return "the route runs a call on the destination chain, whose recipient cannot be checked here";
|
|
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");
|
|
610
616
|
if (!e.evmDestination) {
|
|
611
617
|
const why = solanaRecipientMismatch(r, bridge, receiver, e);
|
|
612
618
|
if (why) return why;
|
|
613
|
-
} else if (!same(receiver, e.receiver)) return `pays ${receiver}, not ${e.receiver}
|
|
614
|
-
if (hasSourceSwaps !== bridge.sourceSwaps) return `unknown Diamond call ${selector}
|
|
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");
|
|
615
621
|
const refund = refundMismatch(r, bridge, e);
|
|
616
622
|
if (refund) return refund;
|
|
617
623
|
const spec = FACET_DATA[bridge.facet];
|
|
618
624
|
const delivery = spec?.delivery?.(r, facetDataAt(r, bridge, spec), e);
|
|
619
625
|
if (delivery) return delivery;
|
|
620
626
|
if (hasSourceSwaps) {
|
|
621
|
-
if (minAmount === 0n) return "bridges nothing after the source swaps";
|
|
627
|
+
if (minAmount === 0n) return note("bridges nothing after the source swaps", "policy_refused");
|
|
622
628
|
return checkSteps(swapsAt(r, 4, 4 + 32), e, sendingAssetId);
|
|
623
629
|
}
|
|
624
|
-
if (minAmount > e.amountIn || minAmount * 10000n < e.amountIn * (10000n - BRIDGE_INPUT_FEE_MAX_BPS)) return `bridges ${minAmount}, not the ${e.amountIn} spent
|
|
625
|
-
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");
|
|
626
632
|
} catch (err) {
|
|
627
|
-
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");
|
|
628
634
|
}
|
|
629
635
|
}
|
|
630
636
|
|
|
@@ -673,7 +679,7 @@ var Cursor = class {
|
|
|
673
679
|
};
|
|
674
680
|
function solanaBridgeTxMismatch(base64, sender) {
|
|
675
681
|
const bytes = (0, import_util2.base64Bytes)(base64);
|
|
676
|
-
if (!bytes) return "the Solana transaction is not base64";
|
|
682
|
+
if (!bytes) return note("the Solana transaction is not base64", "upstream_unavailable");
|
|
677
683
|
const c = new Cursor(bytes);
|
|
678
684
|
try {
|
|
679
685
|
c.bytes(64 * c.short());
|
|
@@ -681,7 +687,7 @@ function solanaBridgeTxMismatch(base64, sender) {
|
|
|
681
687
|
if ((first & 128) === 0) c.at--;
|
|
682
688
|
c.bytes(3);
|
|
683
689
|
const keys = Array.from({ length: c.short() }, () => base58(c.bytes(32)));
|
|
684
|
-
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");
|
|
685
691
|
c.bytes(32);
|
|
686
692
|
let mayan = false;
|
|
687
693
|
const payees = [];
|
|
@@ -696,15 +702,16 @@ function solanaBridgeTxMismatch(base64, sender) {
|
|
|
696
702
|
if (to !== void 0 && to !== sender && to !== LIFI_SOLANA_FEE_WALLET) payees.push(to);
|
|
697
703
|
}
|
|
698
704
|
}
|
|
699
|
-
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;
|
|
700
706
|
} catch (err) {
|
|
701
|
-
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");
|
|
702
708
|
}
|
|
703
709
|
}
|
|
704
710
|
|
|
705
711
|
// src/provider.ts
|
|
706
712
|
var import_util3 = require("@anywalletkit/protocol-swap2-core/util");
|
|
707
713
|
var import_validate = require("@anywalletkit/protocol-swap2-core/validate");
|
|
714
|
+
var import_errors2 = require("@anywalletkit/protocol-swap2-core/errors");
|
|
708
715
|
var import_core = require("@anywalletkit/core");
|
|
709
716
|
var DEFAULT_LIFI_BASE = "https://li.quest/v1";
|
|
710
717
|
var LIFI_CHAIN = {
|
|
@@ -882,6 +889,92 @@ function routeOptions(body, excluded, allowed) {
|
|
|
882
889
|
}
|
|
883
890
|
return out;
|
|
884
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
|
+
}
|
|
885
978
|
function cleanChoice(choice) {
|
|
886
979
|
if (!choice) return null;
|
|
887
980
|
const allow = (choice.allowBridges ?? []).filter((t) => TOOL_KEY.test(t));
|
|
@@ -922,8 +1015,10 @@ function lifiProvider(config = {}) {
|
|
|
922
1015
|
return !!LIFI_CHAIN[from.chain] && !!LIFI_CHAIN[to.chain];
|
|
923
1016
|
},
|
|
924
1017
|
async getQuote(params, signal) {
|
|
925
|
-
const reject = (why) => {
|
|
926
|
-
|
|
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}`);
|
|
927
1022
|
return null;
|
|
928
1023
|
};
|
|
929
1024
|
if (params.flex) return null;
|
|
@@ -1015,7 +1110,10 @@ function lifiProvider(config = {}) {
|
|
|
1015
1110
|
continue;
|
|
1016
1111
|
}
|
|
1017
1112
|
const why = reasons.length === 1 ? ` (${reasons[0].slice(0, 200)})` : "";
|
|
1018
|
-
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
|
+
);
|
|
1019
1117
|
return null;
|
|
1020
1118
|
}
|
|
1021
1119
|
const q = await res.json();
|
|
@@ -1024,7 +1122,7 @@ function lifiProvider(config = {}) {
|
|
|
1024
1122
|
const steps = q.includedSteps;
|
|
1025
1123
|
const tools = [q.tool, ...Array.isArray(steps) ? steps.map((i) => i?.tool) : []];
|
|
1026
1124
|
if (tools.some((t) => typeof t === "string" && SMART_DEPOSITS_TOOL.test(t))) {
|
|
1027
|
-
return reject("LI.FI Smart Deposits pay a deposit address whose recipient the transaction does not bind \u2014 refused for a Solana destination");
|
|
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");
|
|
1028
1126
|
}
|
|
1029
1127
|
}
|
|
1030
1128
|
const action = q.action;
|
|
@@ -1033,7 +1131,7 @@ function lifiProvider(config = {}) {
|
|
|
1033
1131
|
const expectedToToken = nativeFor(params.destChain, params.destToken);
|
|
1034
1132
|
const expectedFromChain = Number(fromChain);
|
|
1035
1133
|
const expectedToChain = Number(toChain);
|
|
1036
|
-
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");
|
|
1037
1135
|
const mismatch = [
|
|
1038
1136
|
action.fromChainId !== expectedFromChain && "source chain",
|
|
1039
1137
|
action.toChainId !== expectedToChain && "destination chain",
|
|
@@ -1043,28 +1141,39 @@ function lifiProvider(config = {}) {
|
|
|
1043
1141
|
!sameChainAddress(params.sourceChain, action.fromAddress, fromAddress) && "sender",
|
|
1044
1142
|
!sameChainAddress(params.destChain, action.toAddress, toAddress) && "recipient"
|
|
1045
1143
|
].filter(Boolean);
|
|
1046
|
-
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
|
+
}
|
|
1047
1151
|
const bridged = BigInt(est.fromAmount);
|
|
1048
1152
|
const spent = BigInt(params.amountIn);
|
|
1049
|
-
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");
|
|
1050
1154
|
if ((spent - bridged) * 10000n > spent * BigInt(MAX_INPUT_FEE_BPS)) {
|
|
1051
|
-
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");
|
|
1052
1156
|
}
|
|
1053
1157
|
if (solanaSource) {
|
|
1054
|
-
if (!tr.data || tr.data.length < 64 || tr.data.length % 4 !== 0 || !BASE64.test(tr.data)) return reject("the Solana transaction is not base64");
|
|
1055
|
-
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");
|
|
1056
1160
|
if (fromChain !== toChain) {
|
|
1057
1161
|
const why = solanaBridgeTxMismatch(tr.data, fromAddress);
|
|
1058
|
-
if (why) return reject(why);
|
|
1162
|
+
if (why) return reject(why, reasonOf(why));
|
|
1059
1163
|
}
|
|
1060
|
-
} 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))
|
|
1061
|
-
|
|
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");
|
|
1062
1171
|
const sourceIsToken = fromToken !== EVM_ZERO && fromToken !== SOLANA_NATIVE;
|
|
1063
1172
|
if (solanaSource) {
|
|
1064
1173
|
} else if (sourceIsToken) {
|
|
1065
|
-
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");
|
|
1066
1175
|
} else if (est.approvalAddress !== void 0) {
|
|
1067
|
-
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");
|
|
1068
1177
|
}
|
|
1069
1178
|
let txRequest;
|
|
1070
1179
|
let nativeFee = 0n;
|
|
@@ -1072,13 +1181,13 @@ function lifiProvider(config = {}) {
|
|
|
1072
1181
|
txRequest = { serializedTx: tr.data, value: sourceIsToken ? "0" : params.amountIn };
|
|
1073
1182
|
} else {
|
|
1074
1183
|
try {
|
|
1075
|
-
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");
|
|
1076
1185
|
} catch {
|
|
1077
|
-
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");
|
|
1078
1187
|
}
|
|
1079
1188
|
const value = BigInt(tr.value);
|
|
1080
1189
|
nativeFee = sourceIsToken ? value : value > BigInt(params.amountIn) ? value - BigInt(params.amountIn) : 0n;
|
|
1081
|
-
if (!sourceIsToken && value < BigInt(params.amountIn)) return reject("the transaction sends less native coin than the input");
|
|
1190
|
+
if (!sourceIsToken && value < BigInt(params.amountIn)) return reject("the transaction sends less native coin than the input", "policy_refused");
|
|
1082
1191
|
txRequest = {
|
|
1083
1192
|
to: tr.to,
|
|
1084
1193
|
data: tr.data,
|
|
@@ -1090,7 +1199,7 @@ function lifiProvider(config = {}) {
|
|
|
1090
1199
|
};
|
|
1091
1200
|
}
|
|
1092
1201
|
const feeUsd = (est.feeCosts ?? []).reduce((a, f) => a + Number(f.amountUSD ?? 0), 0);
|
|
1093
|
-
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");
|
|
1094
1203
|
const minDestAmount = est.toAmountMin;
|
|
1095
1204
|
if (sentSlippage !== void 0) {
|
|
1096
1205
|
const dest = BigInt(est.toAmount);
|
|
@@ -1104,18 +1213,22 @@ function lifiProvider(config = {}) {
|
|
|
1104
1213
|
continue;
|
|
1105
1214
|
}
|
|
1106
1215
|
params.onRefusal?.(
|
|
1107
|
-
`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" }
|
|
1108
1218
|
);
|
|
1109
1219
|
return null;
|
|
1110
1220
|
}
|
|
1111
1221
|
}
|
|
1112
1222
|
if (params.minAmountOut !== void 0 && BigInt(minDestAmount) < BigInt(params.minAmountOut)) {
|
|
1113
|
-
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" });
|
|
1114
1224
|
return null;
|
|
1115
1225
|
}
|
|
1116
1226
|
const duration = est.executionDuration ?? 0;
|
|
1117
1227
|
if (fromChain === toChain && duration > SAME_CHAIN_MAX_SECONDS) {
|
|
1118
|
-
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
|
+
);
|
|
1119
1232
|
return null;
|
|
1120
1233
|
}
|
|
1121
1234
|
if (!solanaSource) {
|
|
@@ -1130,7 +1243,7 @@ function lifiProvider(config = {}) {
|
|
|
1130
1243
|
sameChain: fromChain === toChain,
|
|
1131
1244
|
evmDestination: !isSolana(params.destChain)
|
|
1132
1245
|
});
|
|
1133
|
-
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");
|
|
1134
1247
|
}
|
|
1135
1248
|
return {
|
|
1136
1249
|
provider: "lifi",
|
|
@@ -1155,7 +1268,7 @@ function lifiProvider(config = {}) {
|
|
|
1155
1268
|
validUntilMs: receivedAt + quoteTtlMs
|
|
1156
1269
|
};
|
|
1157
1270
|
} catch {
|
|
1158
|
-
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" });
|
|
1159
1272
|
return null;
|
|
1160
1273
|
}
|
|
1161
1274
|
return null;
|
|
@@ -1163,10 +1276,10 @@ function lifiProvider(config = {}) {
|
|
|
1163
1276
|
async buildDeposit(quote) {
|
|
1164
1277
|
const placeholder = quote.providerData?.placeholder;
|
|
1165
1278
|
if (placeholder !== void 0) {
|
|
1166
|
-
throw
|
|
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`);
|
|
1167
1280
|
}
|
|
1168
1281
|
const tx = quote.txRequest;
|
|
1169
|
-
if (!tx) throw
|
|
1282
|
+
if (!tx) throw (0, import_errors2.refusal)("policy_refused", "swap2: LI.FI quote has no transaction request");
|
|
1170
1283
|
const [, fromChain] = quote.intentId.split(":");
|
|
1171
1284
|
return {
|
|
1172
1285
|
chain: tx.serializedTx ? CAIP2_BY_LIFI_CHAIN[fromChain ?? ""] ?? "" : "",
|
|
@@ -1215,23 +1328,23 @@ function lifiProvider(config = {}) {
|
|
|
1215
1328
|
function lifiVerifyQuote(params, quote) {
|
|
1216
1329
|
const fromChain = LIFI_CHAIN[params.sourceChain];
|
|
1217
1330
|
const toChain = LIFI_CHAIN[params.destChain];
|
|
1218
|
-
if (!fromChain || !toChain) return "not a LI.FI chain pair";
|
|
1331
|
+
if (!fromChain || !toChain) return note("not a LI.FI chain pair", "unsupported_pair");
|
|
1219
1332
|
const placeholder = params.dry !== false ? placeholderIn(params) : null;
|
|
1220
1333
|
if (placeholder) {
|
|
1221
|
-
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
|
|
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");
|
|
1222
1335
|
return priceOnlyMismatch(params, quote, `lifi:${fromChain}:${toChain}`);
|
|
1223
1336
|
}
|
|
1224
1337
|
const tx = quote.txRequest;
|
|
1225
|
-
if (!tx) return "the quote carries no transaction";
|
|
1338
|
+
if (!tx) return note("the quote carries no transaction", "policy_refused");
|
|
1226
1339
|
if (isSolana(params.sourceChain)) {
|
|
1227
|
-
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");
|
|
1228
1341
|
return fromChain === toChain ? null : solanaBridgeTxMismatch(tx.serializedTx, params.refundAddress || (params.dry !== false ? SOLANA_PLACEHOLDER : ""));
|
|
1229
1342
|
}
|
|
1230
|
-
if (!isHexData(tx.data) || !tx.to || tx.serializedTx !== void 0) return "the quote carries no Diamond call";
|
|
1231
|
-
if (!sameAddress(tx.to, diamondFor(params.sourceChain))) return `the call targets ${tx.to}, not the LI.FI Diamond
|
|
1232
|
-
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");
|
|
1233
1346
|
const fromToken = lifiToken(params);
|
|
1234
|
-
if (!fromToken) return `unknown source token ${params.sourceToken}
|
|
1347
|
+
if (!fromToken) return note(`unknown source token ${params.sourceToken}`, "unsupported_pair");
|
|
1235
1348
|
const sourceIsToken = fromToken !== EVM_ZERO;
|
|
1236
1349
|
const dry = params.dry !== false;
|
|
1237
1350
|
const fromAddress = params.refundAddress || (dry ? EVM_PLACEHOLDER : "");
|
|
@@ -1244,18 +1357,18 @@ function lifiVerifyQuote(params, quote) {
|
|
|
1244
1357
|
value = BigInt(tx.value ?? "0");
|
|
1245
1358
|
nativeFee = BigInt(tx.nativeFee ?? "0");
|
|
1246
1359
|
} catch {
|
|
1247
|
-
return "the quote's amounts are not integers";
|
|
1360
|
+
return note("the quote's amounts are not integers", "upstream_unavailable");
|
|
1248
1361
|
}
|
|
1249
|
-
if (BigInt(quote.sourceAmount) !== amountIn) return `the quote spends ${quote.sourceAmount}, not the ${amountIn} input
|
|
1250
|
-
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");
|
|
1251
1364
|
if (sourceIsToken) {
|
|
1252
|
-
if (value !== nativeFee) return "a token input attaches only its declared bridge fee";
|
|
1253
|
-
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");
|
|
1254
1367
|
} else {
|
|
1255
|
-
if (value < amountIn || value - amountIn !== nativeFee) return "a native input is paid as the input plus its declared bridge fee";
|
|
1256
|
-
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");
|
|
1257
1370
|
}
|
|
1258
|
-
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");
|
|
1259
1372
|
const why = diamondCallMismatch(tx.data, {
|
|
1260
1373
|
fromToken,
|
|
1261
1374
|
toToken: nativeFor(params.destChain, params.destToken),
|
|
@@ -1267,25 +1380,27 @@ function lifiVerifyQuote(params, quote) {
|
|
|
1267
1380
|
sameChain: fromChain === toChain,
|
|
1268
1381
|
evmDestination: !isSolana(params.destChain)
|
|
1269
1382
|
});
|
|
1270
|
-
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;
|
|
1271
1384
|
}
|
|
1272
1385
|
function priceOnlyMismatch(params, quote, route) {
|
|
1273
|
-
if (quote.depositAddress !== "") return `a LI.FI quote pays no deposit address, but this one names ${String(quote.depositAddress).slice(0, 64)}
|
|
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");
|
|
1274
1387
|
const fee = quote.providerData?.nativeFee;
|
|
1275
|
-
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
|
|
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");
|
|
1276
1389
|
try {
|
|
1277
|
-
if (BigInt(quote.sourceAmount) !== BigInt(params.amountIn)) return `the quote spends ${quote.sourceAmount}, not the ${params.amountIn} input
|
|
1390
|
+
if (BigInt(quote.sourceAmount) !== BigInt(params.amountIn)) return note(`the quote spends ${quote.sourceAmount}, not the ${params.amountIn} input`, "policy_refused");
|
|
1278
1391
|
const minOut = BigInt(quote.minDestAmount);
|
|
1279
|
-
if (params.minAmountOut !== void 0 && minOut < BigInt(params.minAmountOut)) return `the quoted minimum ${minOut} is below the caller's floor ${params.minAmountOut}
|
|
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");
|
|
1280
1393
|
} catch {
|
|
1281
|
-
return "the quote's amounts are not integers";
|
|
1394
|
+
return note("the quote's amounts are not integers", "upstream_unavailable");
|
|
1282
1395
|
}
|
|
1283
|
-
return quote.intentId.replace(/~[0-9a-f]{32}$/, "") === route ? null : "the intent id does not name this route";
|
|
1396
|
+
return quote.intentId.replace(/~[0-9a-f]{32}$/, "") === route ? null : note("the intent id does not name this route", "policy_refused");
|
|
1284
1397
|
}
|
|
1285
1398
|
var lifiPins = Object.freeze({
|
|
1286
1399
|
callTargets: (chain) => /^eip155:[0-9]+$/.test(chain) ? [diamondFor(chain)] : [],
|
|
1287
1400
|
spenders: (chain) => /^eip155:[0-9]+$/.test(chain) ? [diamondFor(chain)] : [],
|
|
1288
|
-
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)
|
|
1289
1404
|
});
|
|
1290
1405
|
|
|
1291
1406
|
// src/protocol.ts
|