@anywalletkit/protocol-swap2-lifi 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,11 +27,47 @@ calls are refused) and its `BridgeData` must name the reviewed recipient and cha
27
27
  input token (or what its source swaps make of it). Every refund, depositor or fallback address a
28
28
  bridge carries in its own data (Stargate's LayerZero fee refund, an expired Across deposit or LI.FI
29
29
  intent, a Mayan Swift order's trader, Chainflip's refund address, …) must be the sender or the
30
- recipient. Refused, because the recipient cannot be read from the calldata: routes with a destination call, EVM-source routes to a non-EVM chain, and the
30
+ recipient. Refused, because the recipient cannot be read from the calldata: routes with a destination call and the
31
31
  bridges whose facet does not bind the delivery to `BridgeData` (Relay, Garden, deBridge DLN,
32
32
  ThorSwap, Squid, Symbiosis) — cross-chain requests ask LI.FI to route around Relay, Garden, Squid
33
33
  and Symbiosis, and not to offer destination calls (`allowDestinationCall=false`), so their pairs get
34
- another bridge. Unknown calldata is refused.
34
+ another bridge. **LI.FI Intents** (`lifiIntents`) is refused on every route from an EVM chain and denied in every
35
+ cross-chain request from an EVM chain (Solana-source requests are unchanged): its escrowed order names its own input oracle, output oracle, output settler and
36
+ output amount multiplier, none of which is pinned here, so a matching recipient would not bind the delivery (the
37
+ escrow releases the input on the oracle's word). LI.FI serves those pairs through another bridge; where LI.FI Intents
38
+ is its only one (Elysium, 2026-11-13) LI.FI has no route. Unknown calldata is refused.
39
+
40
+ **EVM → Solana.** `BridgeData` names LI.FI's Solana chain id and, as its receiver, only LI.FI's non-EVM
41
+ placeholder (`0x11f1…11F1`, required exactly); the recipient sits in the bridge's own data and is read there for
42
+ the bridges whose delivery the transaction binds, then held to the requested owner (`destAddress`) and token:
43
+
44
+ - **Mayan** (`mayan`, `mayanMCTP`, `mayanFastMCTP`): the facet's `nonEVMReceiver` and the destination address in
45
+ the Mayan order or transfer it forwards (Swift v2, MCTP `createOrder` / `bridgeWithFee`, Fast MCTP `createOrder`
46
+ / `bridge`) must both be the owner; the order must go to Solana (Wormhole chain 1 / CCTP domain 5), carry no
47
+ custom payload and name the requested mint (32 zero bytes for SOL); a plain CCTP transfer delivers USDC. Other
48
+ Mayan entry points are refused.
49
+ - **Polymer CCTP** (`polymer`, `polymerStandard`): CCTP mints to the token account the calldata names, which the
50
+ facet cannot check; it must be the owner's USDC associated token account, derived on the device.
51
+ - **Chainflip**: the vault swap's 32-byte destination address must be the owner and its asset the requested one
52
+ (SOL, USDC or USDT on Solana).
53
+
54
+ These requests ask LI.FI for those bridges only (`allowBridges`; a `chooseRoute` pick is kept among them). NEAR
55
+ Intents and LayerSwap pay a deposit address whose off-chain order names the recipient, and Smart Deposits a
56
+ deposit address outright: deposits the device cannot verify, refused for this direction. LI.FI Intents to
57
+ Solana is refused like everywhere else (see above). Any other bridge to Solana is refused as before. Solana-source
58
+ requests are unchanged.
59
+
60
+ **Dry quotes without an address.** A dry quote (`dry` not `false`) without `refundAddress` is priced for a
61
+ placeholder sender (vitalik.eth on EVM, the public BIP-39 "abandon" account on Solana), and a dry EVM → Solana quote
62
+ without `destAddress` for the "abandon" account as the recipient. Such a quote prices the route only: it carries no
63
+ `txRequest` (its `providerData.placeholder` names the stand-in), `buildDeposit` refuses it, and `lifiVerifyQuote` /
64
+ `lifiPins` refuse a quote for such a request that carries a transaction — LI.FI's transaction for a placeholder would
65
+ pay or refund that public account from whichever wallet signs it. Quote again with both addresses to build. A dry
66
+ EVM → EVM quote with a `refundAddress` but no recipient prices the sender's own address and keeps its transaction.
67
+ A bridge's native fee on top of the input (LayerZero's on Stargate, for instance) stays on the price-only quote as
68
+ `providerData.nativeFee` (base units of the source chain's native coin), which the router's `usdValue` and
69
+ `quoteValues` net out of its value as they do `txRequest.nativeFee`; `lifiVerifyQuote` refuses one that is not a
70
+ base-unit integer.
35
71
 
36
72
  Every swap step is read, not only the ends: each must spend what the step before delivered, only
37
73
  steps that spend the input token may pull from the wallet (`requiresDeposit`), and together at most
@@ -6,6 +6,70 @@ import {
6
6
  describeLifiActivity
7
7
  } from "./chunk-EU7XQVZQ.js";
8
8
 
9
+ // src/solana-address.ts
10
+ import { ed25519 } from "@noble/curves/ed25519.js";
11
+ import { sha256 } from "@noble/hashes/sha2.js";
12
+ var ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
13
+ var BASE58 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
14
+ var SOLANA_TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
15
+ var ASSOCIATED_TOKEN_PROGRAM = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
16
+ var PDA_MARKER = Uint8Array.from("ProgramDerivedAddress", (c) => c.charCodeAt(0));
17
+ function base58(bytes) {
18
+ let n = 0n;
19
+ for (const b of bytes) n = n << 8n | BigInt(b);
20
+ let out = "";
21
+ while (n > 0n) {
22
+ out = ALPHABET[Number(n % 58n)] + out;
23
+ n /= 58n;
24
+ }
25
+ for (const b of bytes) {
26
+ if (b !== 0) break;
27
+ out = `1${out}`;
28
+ }
29
+ return out;
30
+ }
31
+ function solanaKey(address) {
32
+ if (!BASE58.test(address)) return null;
33
+ let n = 0n;
34
+ for (const ch of address) n = n * 58n + BigInt(ALPHABET.indexOf(ch));
35
+ let zeros = 0;
36
+ while (address[zeros] === "1") zeros++;
37
+ const body = [];
38
+ while (n > 0n) {
39
+ body.unshift(Number(n & 0xffn));
40
+ n >>= 8n;
41
+ }
42
+ if (zeros + body.length !== 32) return null;
43
+ const out = new Uint8Array(32);
44
+ out.set(body, zeros);
45
+ return out;
46
+ }
47
+ var onCurve = (bytes) => {
48
+ try {
49
+ ed25519.Point.fromBytes(bytes);
50
+ return true;
51
+ } catch {
52
+ return false;
53
+ }
54
+ };
55
+ function programAddress(seeds, program) {
56
+ for (let bump = 255; bump >= 0; bump--) {
57
+ const parts = [...seeds, Uint8Array.of(bump), program, PDA_MARKER];
58
+ const buf = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
59
+ let at = 0;
60
+ for (const p of parts) {
61
+ buf.set(p, at);
62
+ at += p.length;
63
+ }
64
+ const hash = sha256(buf);
65
+ if (!onCurve(hash)) return hash;
66
+ }
67
+ throw new Error("no program address for these seeds");
68
+ }
69
+ function associatedTokenAccount(owner, mint, tokenProgram = solanaKey(SOLANA_TOKEN_PROGRAM)) {
70
+ return programAddress([owner, tokenProgram, mint], solanaKey(ASSOCIATED_TOKEN_PROGRAM));
71
+ }
72
+
9
73
  // src/calldata.ts
10
74
  var GENERIC_SWAPS = {
11
75
  "0x4630a0d8": { single: false },
@@ -316,8 +380,9 @@ var FACET_DATA = {
316
380
  Frax: { inline: 5, refunds: [["refundRecipient", 3]], zeroRefused: true },
317
381
  Glacis: { inline: 4, refunds: [["refundAddress", 1]], zeroRefused: true },
318
382
  LayerSwap: { refunds: [["refundRecipient", 2]], zeroRefused: true },
383
+ // The escrow order's user. Not reached while LI.FI Intents is refused on every EVM-source route (INTENTS_UNPINNED); kept for when
384
+ // its oracles and settler are pinned.
319
385
  LiFiIntentEscrowV2: { refunds: [["depositAndRefundAddress", 2]], zeroRefused: true },
320
- // the escrow order's user
321
386
  M0: { inline: 7, refunds: [["refundRecipient", 1], ["orderOwner", 2]], zeroRefused: true, delivery: m0Delivery },
322
387
  // orderOwner gets a cancelled order back
323
388
  Mayan: { refunds: [["refundRecipient", 7]], nested: mayanRefunds, zeroRefused: true },
@@ -345,6 +410,86 @@ function refundMismatch(r, bridge, e) {
345
410
  }
346
411
  return null;
347
412
  }
413
+ var NON_EVM_UNREAD = "the recipient on a non-EVM destination sits in bridge-specific data and cannot be checked here";
414
+ var LIFI_SOLANA_CHAIN_ID = 1151111081099710n;
415
+ var NON_EVM_ADDRESS = "0x11f111f111f111f111f111f111f111f111f111f1";
416
+ var SOLANA_USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
417
+ var WORMHOLE_SOLANA = 1n;
418
+ var CCTP_DOMAIN_SOLANA = 5n;
419
+ var CHAINFLIP_SOLANA_ASSETS = {
420
+ "11111111111111111111111111111111": 9n,
421
+ [SOLANA_USDC]: 10n,
422
+ Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB: 16n
423
+ };
424
+ var DEPOSIT_FACETS = /* @__PURE__ */ new Set(["NEARIntents", "LayerSwap", "Unit"]);
425
+ var MAYAN_SOLANA = {
426
+ // Swift v2 createOrderWithToken(tokenIn, amountIn, (payloadType, trader, destAddr, destChainId, referrerAddr, tokenOut, minAmountOut, …), customPayload)
427
+ "0xa3a30834": { destAddr: 4, chain: 5, cctp: false, payloadType: [2, 1n], tokenOut: 7 },
428
+ // MCTP createOrder((tokenIn, amountIn, gasDrop, destAddr, destChain, tokenOut, minAmountOut, deadline, redeemFee, referrerAddr, referrerBps))
429
+ "0x1c59b7fc": { destAddr: 3, chain: 4, cctp: false, tokenOut: 5 },
430
+ // MCTP bridgeWithFee(tokenIn, amountIn, redeemFee, gasDrop, destAddr, destDomain, payloadType, customPayload): delivers USDC
431
+ "0x2072197f": { destAddr: 4, chain: 5, cctp: true, payloadType: [6, 1n] },
432
+ // Fast MCTP bridge(tokenIn, amountIn, redeemFee, circleMaxFee, gasDrop, destAddr, destDomain, referrerAddress, referrerBps, payloadType, minFinalityThreshold, customPayload): delivers USDC
433
+ "0xf58b6de8": { destAddr: 5, chain: 6, cctp: true, payloadType: [9, 1n] },
434
+ // Fast MCTP createOrder(tokenIn, amountIn, circleMaxFee, destDomain, minFinalityThreshold, (payloadType 3, destAddr, tokenOut, amountOutMin, …))
435
+ "0x2337e236": { destAddr: 6, chain: 3, cctp: true, payloadType: [5, 3n], tokenOut: 7 }
436
+ };
437
+ var hexOf = (b) => `0x${Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("")}`;
438
+ var keyOf = (word) => base58(Uint8Array.from(word.slice(2).match(/../g) ?? [], (h) => Number.parseInt(h, 16)));
439
+ function mayanToSolana(r, at, t) {
440
+ const declared = r.raw(at, 32);
441
+ if (declared !== t.ownerHex) return `Mayan's nonEVMReceiver is ${keyOf(declared)}, not the recipient ${t.e.receiver}`;
442
+ const p = r.bytes(at, at + 32 * 2);
443
+ const selector = p.raw(0, 4);
444
+ const o = MAYAN_SOLANA[selector];
445
+ if (!o) return `the Mayan call ${selector} is an entry point whose Solana recipient cannot be checked here`;
446
+ const word = (i) => 4 + 32 * i;
447
+ 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`;
448
+ const dest = p.raw(word(o.destAddr), 32);
449
+ if (dest !== t.ownerHex) return `the Mayan order pays ${keyOf(dest)} on Solana, not the recipient ${t.e.receiver}`;
450
+ const chain = p.word(word(o.chain));
451
+ if (chain !== (o.cctp ? CCTP_DOMAIN_SOLANA : WORMHOLE_SOLANA)) return `the Mayan order goes to ${o.cctp ? "CCTP domain" : "Wormhole chain"} ${chain}, not Solana`;
452
+ if (o.tokenOut === void 0) return t.e.toToken === SOLANA_USDC ? null : `a Mayan CCTP transfer delivers USDC, not ${t.e.toToken}`;
453
+ const token = p.raw(word(o.tokenOut), 32);
454
+ return token === t.mintHex ? null : `the Mayan order delivers ${keyOf(token)}, not ${t.e.toToken}`;
455
+ }
456
+ function polymerToSolana(r, at, t) {
457
+ const declared = r.raw(at + 32 * 2, 32);
458
+ if (declared !== t.ownerHex) return `Polymer CCTP's nonEVMReceiver is ${keyOf(declared)}, not the recipient ${t.e.receiver}`;
459
+ if (t.e.toToken !== SOLANA_USDC) return `Polymer CCTP delivers USDC, not ${t.e.toToken}`;
460
+ const mintTo = r.raw(at + 32 * 3, 32);
461
+ const account = hexOf(associatedTokenAccount(t.owner, t.mint));
462
+ return mintTo === account ? null : `the Polymer CCTP transfer mints to ${keyOf(mintTo)}, not the recipient's USDC account ${keyOf(account)}`;
463
+ }
464
+ function chainflipToSolana(r, at, t) {
465
+ const dst = r.bytes(at, at);
466
+ if (dst.length !== 32) return `Chainflip pays a ${dst.length}-byte address on Solana, not the recipient ${t.e.receiver}`;
467
+ if (dst.raw(0, 32) !== t.ownerHex) return `Chainflip pays ${keyOf(dst.raw(0, 32))} on Solana, not the recipient ${t.e.receiver}`;
468
+ const asset = r.word(at + 32);
469
+ const want = CHAINFLIP_SOLANA_ASSETS[t.e.toToken];
470
+ return want !== void 0 && asset === want ? null : `Chainflip delivers its asset ${asset}, not ${t.e.toToken}`;
471
+ }
472
+ var SOLANA_DELIVERY = {
473
+ Mayan: mayanToSolana,
474
+ PolymerCCTP: polymerToSolana,
475
+ Chainflip: chainflipToSolana
476
+ };
477
+ var INTENTS_TO_SOLANA = "a LI.FI Intents order to Solana names its own oracles, output settler and output amount, which cannot be checked here \u2014 its escrow could release the input without the delivery";
478
+ 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";
479
+ var INTENTS_FACET = "LiFiIntentEscrowV2";
480
+ function solanaRecipientMismatch(r, bridge, receiver, e) {
481
+ if (e.destChainId !== LIFI_SOLANA_CHAIN_ID) return NON_EVM_UNREAD;
482
+ if (!same(receiver, NON_EVM_ADDRESS)) return `BridgeData pays ${receiver}, not LI.FI's non-EVM placeholder ${NON_EVM_ADDRESS}, on a Solana route`;
483
+ 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`;
484
+ if (bridge.facet === INTENTS_FACET) return INTENTS_TO_SOLANA;
485
+ const read = SOLANA_DELIVERY[bridge.facet];
486
+ if (!read) return NON_EVM_UNREAD;
487
+ const owner = solanaKey(e.receiver);
488
+ if (!owner) return `the recipient ${e.receiver} is not a Solana address`;
489
+ const mint = solanaKey(e.toToken);
490
+ if (!mint) return `the destination token ${e.toToken} is not a Solana mint`;
491
+ return read(r, facetDataAt(r, bridge, FACET_DATA[bridge.facet]), { owner, ownerHex: hexOf(owner), mint, mintHex: hexOf(mint), e });
492
+ }
348
493
  function diamondCallMismatch(data, e) {
349
494
  let r;
350
495
  try {
@@ -373,6 +518,7 @@ function diamondCallMismatch(data, e) {
373
518
  if (unbound) return `${unbound} delivers to a recipient the transaction does not bind (an off-chain order or unchecked facet data)`;
374
519
  const bridge = BRIDGE_CALLS[selector];
375
520
  if (!bridge) return `unknown Diamond call ${selector}`;
521
+ if (bridge.facet === INTENTS_FACET && e.evmDestination) return INTENTS_UNPINNED;
376
522
  const head = r.small(4);
377
523
  if (head < (bridge.sourceSwaps ? 96 : 64) || head % 32 !== 0 || head > 32 * 32) return `unknown Diamond call ${selector}`;
378
524
  const at = 4 + head;
@@ -386,8 +532,10 @@ function diamondCallMismatch(data, e) {
386
532
  if (e.sameChain) return "a bridge call on a same-chain route";
387
533
  if (destinationChainId !== e.destChainId) return `bridges to chain ${destinationChainId}, not ${e.destChainId}`;
388
534
  if (hasDestinationCall) return "the route runs a call on the destination chain, whose recipient cannot be checked here";
389
- if (!e.evmDestination) return "the recipient on a non-EVM destination sits in bridge-specific data and cannot be checked here";
390
- if (!same(receiver, e.receiver)) return `pays ${receiver}, not ${e.receiver}`;
535
+ if (!e.evmDestination) {
536
+ const why = solanaRecipientMismatch(r, bridge, receiver, e);
537
+ if (why) return why;
538
+ } else if (!same(receiver, e.receiver)) return `pays ${receiver}, not ${e.receiver}`;
391
539
  if (hasSourceSwaps !== bridge.sourceSwaps) return `unknown Diamond call ${selector}`;
392
540
  const refund = refundMismatch(r, bridge, e);
393
541
  if (refund) return refund;
@@ -407,7 +555,6 @@ function diamondCallMismatch(data, e) {
407
555
 
408
556
  // src/solana.ts
409
557
  import { base64Bytes } from "@anywalletkit/protocol-swap2-core/util";
410
- var ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
411
558
  var MAYAN_ENTRY_PROGRAMS = /* @__PURE__ */ new Set([
412
559
  "D8C8iW6zmoKg5TRr8nQ7h14TMWqQX8FiBdj2ju5MF3wa",
413
560
  // CPI_PROXY_PROGRAM_ID
@@ -422,20 +569,6 @@ var MAYAN_ENTRY_PROGRAMS = /* @__PURE__ */ new Set([
422
569
  ]);
423
570
  var SYSTEM_PROGRAM = "11111111111111111111111111111111";
424
571
  var LIFI_SOLANA_FEE_WALLET = "34FKjAdVcTax2DHqV2XnbXa9J3zmyKcFuFKWbcmgxjgm";
425
- function base58(bytes) {
426
- let n = 0n;
427
- for (const b of bytes) n = n << 8n | BigInt(b);
428
- let out = "";
429
- while (n > 0n) {
430
- out = ALPHABET[Number(n % 58n)] + out;
431
- n /= 58n;
432
- }
433
- for (const b of bytes) {
434
- if (b !== 0) break;
435
- out = `1${out}`;
436
- }
437
- return out;
438
- }
439
572
  var Cursor = class {
440
573
  constructor(b) {
441
574
  this.b = b;
@@ -537,7 +670,8 @@ var LIFI_CHAIN = {
537
670
  "eip155:2818": "2818",
538
671
  "eip155:14": "14",
539
672
  // 2026-11-10: Elysium (Kinetiq; li.quest/v1/chains id 99901, key "ely"). LI.FI's bridges there are Relay (refused
540
- // below: UNBOUND_TOOLS) and LI.FI Intents; same-chain swaps go through GenericSwapFacetV3 (KyberSwap, Nordstern).
673
+ // below: UNBOUND_TOOLS) and LI.FI Intents (refused too: UNPINNED_TOOLS), so it has no cross-chain LI.FI route for now;
674
+ // same-chain swaps go through GenericSwapFacetV3 (KyberSwap, Nordstern).
541
675
  "eip155:99901": "99901",
542
676
  [LIFI_SOLANA_CAIP2]: "1151111081099710"
543
677
  };
@@ -547,12 +681,12 @@ var SOLANA_NATIVE = "11111111111111111111111111111111";
547
681
  var EVM_PLACEHOLDER = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
548
682
  var SOLANA_PLACEHOLDER = "HAgk14JpMQLgt6rVgv7cBQFJWFto5Dqxi472uT3DKpqk";
549
683
  var isSolana = (chain) => chain === LIFI_SOLANA_CAIP2;
550
- var BASE58 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
684
+ var BASE582 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
551
685
  var BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
552
686
  var SAME_CHAIN_MAX_SECONDS = 90;
553
687
  var MAX_INPUT_FEE_BPS = 300;
554
688
  function sameChainAddress(chain, actual, expected) {
555
- if (isSolana(chain)) return !!actual && BASE58.test(actual) && actual === expected;
689
+ if (isSolana(chain)) return !!actual && BASE582.test(actual) && actual === expected;
556
690
  return sameAddress(actual, expected);
557
691
  }
558
692
  function nativeFor(chain, token) {
@@ -641,7 +775,16 @@ function headers(key) {
641
775
  }
642
776
  var TOOL_KEY = /^[A-Za-z0-9_-]{1,64}$/;
643
777
  var UNBOUND_TOOLS = Object.freeze(["relaydepository", "garden", "squid", "symbiosis"]);
644
- function routeOptions(body, excluded) {
778
+ var UNPINNED_TOOLS = Object.freeze(["lifiIntents"]);
779
+ var REFUSED_EVM_TOOLS = Object.freeze([...UNBOUND_TOOLS, ...UNPINNED_TOOLS]);
780
+ var SOLANA_DESTINATION_TOOLS = Object.freeze(["mayan", "mayanMCTP", "mayanFastMCTP", "polymer", "polymerStandard", "chainflip"]);
781
+ var SMART_DEPOSITS_TOOL = /^smartdeposits?$/i;
782
+ var dryRecipient = (params, fromAddress) => !isSolana(params.sourceChain) && isSolana(params.destChain) ? SOLANA_PLACEHOLDER : fromAddress;
783
+ function placeholderIn(params) {
784
+ if (!params.refundAddress) return "sender";
785
+ return !params.destAddress && dryRecipient(params, params.refundAddress) !== params.refundAddress ? "recipient" : null;
786
+ }
787
+ function routeOptions(body, excluded, allowed) {
645
788
  const routes = body?.routes;
646
789
  if (!Array.isArray(routes)) return [];
647
790
  const out = [];
@@ -651,7 +794,7 @@ function routeOptions(body, excluded) {
651
794
  const cross = Array.isArray(step.includedSteps) ? step.includedSteps.find((i) => i?.type === "cross")?.tool : void 0;
652
795
  const tool = typeof cross === "string" ? cross : step.tool;
653
796
  const eta = step.estimate?.executionDuration;
654
- if (typeof tool !== "string" || !TOOL_KEY.test(tool) || excluded.includes(tool) || typeof eta !== "number" || !Number.isFinite(eta) || eta < 0) continue;
797
+ if (typeof tool !== "string" || !TOOL_KEY.test(tool) || excluded.includes(tool) || allowed && !allowed.includes(tool) || typeof eta !== "number" || !Number.isFinite(eta) || eta < 0) continue;
655
798
  if (typeof r.toAmount !== "string" || typeof r.toAmountMin !== "string" || !/^\d{1,60}$/.test(r.toAmount) || !/^\d{1,60}$/.test(r.toAmountMin)) continue;
656
799
  out.push({
657
800
  tool,
@@ -716,13 +859,15 @@ function lifiProvider(config = {}) {
716
859
  if (!fromToken) return null;
717
860
  const solanaSource = isSolana(params.sourceChain);
718
861
  const crossChain = params.sourceChain !== params.destChain;
862
+ const evmToSolana = !solanaSource && isSolana(params.destChain);
719
863
  const noDestinationCall = crossChain && !solanaSource;
720
864
  let routeScope = config.routeWideSlippage === true && !solanaSource && !isSolana(params.destChain);
721
865
  const dry = params.dry !== false;
722
866
  const fromAddress = params.refundAddress || (dry ? solanaSource ? SOLANA_PLACEHOLDER : EVM_PLACEHOLDER : "");
723
- const toAddress = params.destAddress || (dry ? fromAddress : "");
867
+ const toAddress = params.destAddress || (dry ? dryRecipient(params, fromAddress) : "");
724
868
  if (!fromAddress) return reject("a live quote needs the sender's address");
725
869
  if (!toAddress) return reject("a live quote needs a recipient");
870
+ const placeholder = dry ? placeholderIn(params) : null;
726
871
  const slippageBps = params.slippageBps ?? defaultSlippageBps;
727
872
  let sentSlippage = slippageBps / 1e4;
728
873
  let deadline;
@@ -743,7 +888,7 @@ function lifiProvider(config = {}) {
743
888
  options: {
744
889
  slippage: slippageBps / 1e4,
745
890
  ...routeScope ? { slippageScope: "route" } : {},
746
- ...solanaSource ? {} : { bridges: { deny: UNBOUND_TOOLS } },
891
+ ...solanaSource ? {} : { bridges: { ...evmToSolana ? { allow: SOLANA_DESTINATION_TOOLS } : {}, deny: REFUSED_EVM_TOOLS } },
747
892
  ...noDestinationCall ? { allowDestinationCall: false } : {},
748
893
  ...integrator().trim() ? { integrator: integrator().trim() } : {}
749
894
  }
@@ -751,7 +896,7 @@ function lifiProvider(config = {}) {
751
896
  signal: signal ?? timeoutSignal(8e3)
752
897
  });
753
898
  if (res.ok) {
754
- const options = routeOptions(await res.json(), solanaSource ? [] : UNBOUND_TOOLS);
899
+ const options = routeOptions(await res.json(), solanaSource ? [] : REFUSED_EVM_TOOLS, evmToSolana ? SOLANA_DESTINATION_TOOLS : void 0);
755
900
  if (options.length) choice = cleanChoice(config.chooseRoute(options, params));
756
901
  }
757
902
  } catch {
@@ -773,8 +918,10 @@ function lifiProvider(config = {}) {
773
918
  }
774
919
  if (routeScope) url.searchParams.set("slippageScope", "route");
775
920
  if (noDestinationCall) url.searchParams.set("allowDestinationCall", "false");
776
- const unbound = crossChain && !solanaSource ? UNBOUND_TOOLS : [];
777
- for (const t of (choice?.allowBridges ?? []).filter((b) => solanaSource || !UNBOUND_TOOLS.includes(b))) url.searchParams.append("allowBridges", t);
921
+ const unbound = crossChain && !solanaSource ? REFUSED_EVM_TOOLS : [];
922
+ const allow = (choice?.allowBridges ?? []).filter((b) => solanaSource || !REFUSED_EVM_TOOLS.includes(b));
923
+ const solanaAllow = evmToSolana ? allow.filter((b) => SOLANA_DESTINATION_TOOLS.includes(b)) : [];
924
+ for (const t of evmToSolana ? solanaAllow.length ? solanaAllow : SOLANA_DESTINATION_TOOLS : allow) url.searchParams.append("allowBridges", t);
778
925
  for (const t of /* @__PURE__ */ new Set([...choice?.denyBridges ?? [], ...unbound])) url.searchParams.append("denyBridges", t);
779
926
  if (choice?.order) url.searchParams.set("order", choice.order);
780
927
  deadline = signal ? void 0 : timeoutSignal(12e3);
@@ -798,6 +945,13 @@ function lifiProvider(config = {}) {
798
945
  }
799
946
  const q = await res.json();
800
947
  const receivedAt = Date.now();
948
+ if (evmToSolana) {
949
+ const steps = q.includedSteps;
950
+ const tools = [q.tool, ...Array.isArray(steps) ? steps.map((i) => i?.tool) : []];
951
+ if (tools.some((t) => typeof t === "string" && SMART_DEPOSITS_TOOL.test(t))) {
952
+ return reject("LI.FI Smart Deposits pay a deposit address whose recipient the transaction does not bind \u2014 refused for a Solana destination");
953
+ }
954
+ }
801
955
  const action = q.action;
802
956
  const est = q.estimate;
803
957
  const tr = q.transactionRequest;
@@ -838,6 +992,7 @@ function lifiProvider(config = {}) {
838
992
  if (!trustedAddress(params.sourceChain, est.approvalAddress, trustedApprovalAddresses)) return reject("the approval target is not a trusted LI.FI contract");
839
993
  }
840
994
  let txRequest;
995
+ let nativeFee = 0n;
841
996
  if (solanaSource) {
842
997
  txRequest = { serializedTx: tr.data, value: sourceIsToken ? "0" : params.amountIn };
843
998
  } else {
@@ -847,7 +1002,7 @@ function lifiProvider(config = {}) {
847
1002
  return reject("the transaction value or gas limit is not a number");
848
1003
  }
849
1004
  const value = BigInt(tr.value);
850
- const nativeFee = sourceIsToken ? value : value > BigInt(params.amountIn) ? value - BigInt(params.amountIn) : 0n;
1005
+ nativeFee = sourceIsToken ? value : value > BigInt(params.amountIn) ? value - BigInt(params.amountIn) : 0n;
851
1006
  if (!sourceIsToken && value < BigInt(params.amountIn)) return reject("the transaction sends less native coin than the input");
852
1007
  txRequest = {
853
1008
  to: tr.to,
@@ -908,7 +1063,10 @@ function lifiProvider(config = {}) {
908
1063
  intentId: `lifi:${fromChain}:${toChain}`,
909
1064
  depositAddress: "",
910
1065
  depositMemo: null,
911
- txRequest,
1066
+ // Every check above ran on LI.FI's transaction; one built for a placeholder is not handed out (placeholderIn).
1067
+ // Its bridge fee on top of the input still is, as `providerData.nativeFee`, so the router's value nets it out
1068
+ // (usdValue reads it when there is no txRequest) instead of ranking the route on its gross output.
1069
+ ...placeholder ? { providerData: { placeholder, ...nativeFee > 0n ? { nativeFee: nativeFee.toString() } : {} } } : { txRequest },
912
1070
  // What the wallet spends (action.fromAmount, = the input); an input-side bridge fee is inside destAmount.
913
1071
  sourceAmount: params.amountIn,
914
1072
  destAmount: est.toAmount,
@@ -928,6 +1086,10 @@ function lifiProvider(config = {}) {
928
1086
  return null;
929
1087
  },
930
1088
  async buildDeposit(quote) {
1089
+ const placeholder = quote.providerData?.placeholder;
1090
+ if (placeholder !== void 0) {
1091
+ throw new Error(`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`);
1092
+ }
931
1093
  const tx = quote.txRequest;
932
1094
  if (!tx) throw new Error("swap2: LI.FI quote has no transaction request");
933
1095
  const [, fromChain] = quote.intentId.split(":");
@@ -979,6 +1141,11 @@ function lifiVerifyQuote(params, quote) {
979
1141
  const fromChain = LIFI_CHAIN[params.sourceChain];
980
1142
  const toChain = LIFI_CHAIN[params.destChain];
981
1143
  if (!fromChain || !toChain) return "not a LI.FI chain pair";
1144
+ const placeholder = params.dry !== false ? placeholderIn(params) : null;
1145
+ if (placeholder) {
1146
+ 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`;
1147
+ return priceOnlyMismatch(params, quote, `lifi:${fromChain}:${toChain}`);
1148
+ }
982
1149
  const tx = quote.txRequest;
983
1150
  if (!tx) return "the quote carries no transaction";
984
1151
  if (isSolana(params.sourceChain)) {
@@ -993,7 +1160,7 @@ function lifiVerifyQuote(params, quote) {
993
1160
  const sourceIsToken = fromToken !== EVM_ZERO;
994
1161
  const dry = params.dry !== false;
995
1162
  const fromAddress = params.refundAddress || (dry ? EVM_PLACEHOLDER : "");
996
- const toAddress = params.destAddress || (dry ? fromAddress : "");
1163
+ const toAddress = params.destAddress || (dry ? dryRecipient(params, fromAddress) : "");
997
1164
  if (!fromAddress || !toAddress) return "a live quote needs the sender and the recipient";
998
1165
  let amountIn, minOut, value, nativeFee;
999
1166
  try {
@@ -1027,6 +1194,19 @@ function lifiVerifyQuote(params, quote) {
1027
1194
  });
1028
1195
  return why ? `the transaction does not match the request: ${why}` : null;
1029
1196
  }
1197
+ function priceOnlyMismatch(params, quote, route) {
1198
+ if (quote.depositAddress !== "") return `a LI.FI quote pays no deposit address, but this one names ${String(quote.depositAddress).slice(0, 64)}`;
1199
+ const fee = quote.providerData?.nativeFee;
1200
+ 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`;
1201
+ try {
1202
+ if (BigInt(quote.sourceAmount) !== BigInt(params.amountIn)) return `the quote spends ${quote.sourceAmount}, not the ${params.amountIn} input`;
1203
+ const minOut = BigInt(quote.minDestAmount);
1204
+ if (params.minAmountOut !== void 0 && minOut < BigInt(params.minAmountOut)) return `the quoted minimum ${minOut} is below the caller's floor ${params.minAmountOut}`;
1205
+ } catch {
1206
+ return "the quote's amounts are not integers";
1207
+ }
1208
+ return quote.intentId.replace(/~[0-9a-f]{32}$/, "") === route ? null : "the intent id does not name this route";
1209
+ }
1030
1210
  var lifiPins = Object.freeze({
1031
1211
  callTargets: (chain) => /^eip155:[0-9]+$/.test(chain) ? [diamondFor(chain)] : [],
1032
1212
  spenders: (chain) => /^eip155:[0-9]+$/.test(chain) ? [diamondFor(chain)] : [],
@@ -1038,4 +1218,4 @@ export {
1038
1218
  lifiVerifyQuote,
1039
1219
  lifiPins
1040
1220
  };
1041
- //# sourceMappingURL=chunk-HJRWHGMX.js.map
1221
+ //# sourceMappingURL=chunk-GWUGK4SG.js.map