@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/dist/index.cjs CHANGED
@@ -81,6 +81,70 @@ function describeLifiActivity(tx, ctx, isDiamond) {
81
81
  });
82
82
  }
83
83
 
84
+ // src/solana-address.ts
85
+ var import_ed25519 = require("@noble/curves/ed25519.js");
86
+ var import_sha2 = require("@noble/hashes/sha2.js");
87
+ var ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
88
+ var BASE58 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
89
+ var SOLANA_TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
90
+ var ASSOCIATED_TOKEN_PROGRAM = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
91
+ var PDA_MARKER = Uint8Array.from("ProgramDerivedAddress", (c) => c.charCodeAt(0));
92
+ function base58(bytes) {
93
+ let n = 0n;
94
+ for (const b of bytes) n = n << 8n | BigInt(b);
95
+ let out = "";
96
+ while (n > 0n) {
97
+ out = ALPHABET[Number(n % 58n)] + out;
98
+ n /= 58n;
99
+ }
100
+ for (const b of bytes) {
101
+ if (b !== 0) break;
102
+ out = `1${out}`;
103
+ }
104
+ return out;
105
+ }
106
+ function solanaKey(address) {
107
+ if (!BASE58.test(address)) return null;
108
+ let n = 0n;
109
+ for (const ch of address) n = n * 58n + BigInt(ALPHABET.indexOf(ch));
110
+ let zeros = 0;
111
+ while (address[zeros] === "1") zeros++;
112
+ const body = [];
113
+ while (n > 0n) {
114
+ body.unshift(Number(n & 0xffn));
115
+ n >>= 8n;
116
+ }
117
+ if (zeros + body.length !== 32) return null;
118
+ const out = new Uint8Array(32);
119
+ out.set(body, zeros);
120
+ return out;
121
+ }
122
+ var onCurve = (bytes) => {
123
+ try {
124
+ import_ed25519.ed25519.Point.fromBytes(bytes);
125
+ return true;
126
+ } catch {
127
+ return false;
128
+ }
129
+ };
130
+ function programAddress(seeds, program) {
131
+ for (let bump = 255; bump >= 0; bump--) {
132
+ const parts = [...seeds, Uint8Array.of(bump), program, PDA_MARKER];
133
+ const buf = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
134
+ let at = 0;
135
+ for (const p of parts) {
136
+ buf.set(p, at);
137
+ at += p.length;
138
+ }
139
+ const hash = (0, import_sha2.sha256)(buf);
140
+ if (!onCurve(hash)) return hash;
141
+ }
142
+ throw new Error("no program address for these seeds");
143
+ }
144
+ function associatedTokenAccount(owner, mint, tokenProgram = solanaKey(SOLANA_TOKEN_PROGRAM)) {
145
+ return programAddress([owner, tokenProgram, mint], solanaKey(ASSOCIATED_TOKEN_PROGRAM));
146
+ }
147
+
84
148
  // src/calldata.ts
85
149
  var GENERIC_SWAPS = {
86
150
  "0x4630a0d8": { single: false },
@@ -391,8 +455,9 @@ var FACET_DATA = {
391
455
  Frax: { inline: 5, refunds: [["refundRecipient", 3]], zeroRefused: true },
392
456
  Glacis: { inline: 4, refunds: [["refundAddress", 1]], zeroRefused: true },
393
457
  LayerSwap: { refunds: [["refundRecipient", 2]], zeroRefused: true },
458
+ // The escrow order's user. Not reached while LI.FI Intents is refused on every EVM-source route (INTENTS_UNPINNED); kept for when
459
+ // its oracles and settler are pinned.
394
460
  LiFiIntentEscrowV2: { refunds: [["depositAndRefundAddress", 2]], zeroRefused: true },
395
- // the escrow order's user
396
461
  M0: { inline: 7, refunds: [["refundRecipient", 1], ["orderOwner", 2]], zeroRefused: true, delivery: m0Delivery },
397
462
  // orderOwner gets a cancelled order back
398
463
  Mayan: { refunds: [["refundRecipient", 7]], nested: mayanRefunds, zeroRefused: true },
@@ -420,6 +485,86 @@ function refundMismatch(r, bridge, e) {
420
485
  }
421
486
  return null;
422
487
  }
488
+ var NON_EVM_UNREAD = "the recipient on a non-EVM destination sits in bridge-specific data and cannot be checked here";
489
+ var LIFI_SOLANA_CHAIN_ID = 1151111081099710n;
490
+ var NON_EVM_ADDRESS = "0x11f111f111f111f111f111f111f111f111f111f1";
491
+ var SOLANA_USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
492
+ var WORMHOLE_SOLANA = 1n;
493
+ var CCTP_DOMAIN_SOLANA = 5n;
494
+ var CHAINFLIP_SOLANA_ASSETS = {
495
+ "11111111111111111111111111111111": 9n,
496
+ [SOLANA_USDC]: 10n,
497
+ Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB: 16n
498
+ };
499
+ var DEPOSIT_FACETS = /* @__PURE__ */ new Set(["NEARIntents", "LayerSwap", "Unit"]);
500
+ var MAYAN_SOLANA = {
501
+ // Swift v2 createOrderWithToken(tokenIn, amountIn, (payloadType, trader, destAddr, destChainId, referrerAddr, tokenOut, minAmountOut, …), customPayload)
502
+ "0xa3a30834": { destAddr: 4, chain: 5, cctp: false, payloadType: [2, 1n], tokenOut: 7 },
503
+ // MCTP createOrder((tokenIn, amountIn, gasDrop, destAddr, destChain, tokenOut, minAmountOut, deadline, redeemFee, referrerAddr, referrerBps))
504
+ "0x1c59b7fc": { destAddr: 3, chain: 4, cctp: false, tokenOut: 5 },
505
+ // MCTP bridgeWithFee(tokenIn, amountIn, redeemFee, gasDrop, destAddr, destDomain, payloadType, customPayload): delivers USDC
506
+ "0x2072197f": { destAddr: 4, chain: 5, cctp: true, payloadType: [6, 1n] },
507
+ // Fast MCTP bridge(tokenIn, amountIn, redeemFee, circleMaxFee, gasDrop, destAddr, destDomain, referrerAddress, referrerBps, payloadType, minFinalityThreshold, customPayload): delivers USDC
508
+ "0xf58b6de8": { destAddr: 5, chain: 6, cctp: true, payloadType: [9, 1n] },
509
+ // Fast MCTP createOrder(tokenIn, amountIn, circleMaxFee, destDomain, minFinalityThreshold, (payloadType 3, destAddr, tokenOut, amountOutMin, …))
510
+ "0x2337e236": { destAddr: 6, chain: 3, cctp: true, payloadType: [5, 3n], tokenOut: 7 }
511
+ };
512
+ var hexOf = (b) => `0x${Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("")}`;
513
+ var keyOf = (word) => base58(Uint8Array.from(word.slice(2).match(/../g) ?? [], (h) => Number.parseInt(h, 16)));
514
+ function mayanToSolana(r, at, t) {
515
+ const declared = r.raw(at, 32);
516
+ if (declared !== t.ownerHex) return `Mayan's nonEVMReceiver is ${keyOf(declared)}, not the recipient ${t.e.receiver}`;
517
+ const p = r.bytes(at, at + 32 * 2);
518
+ const selector = p.raw(0, 4);
519
+ const o = MAYAN_SOLANA[selector];
520
+ if (!o) return `the Mayan call ${selector} is an entry point whose Solana recipient cannot be checked here`;
521
+ 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`;
523
+ 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}`;
525
+ 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}`;
528
+ const token = p.raw(word(o.tokenOut), 32);
529
+ return token === t.mintHex ? null : `the Mayan order delivers ${keyOf(token)}, not ${t.e.toToken}`;
530
+ }
531
+ function polymerToSolana(r, at, t) {
532
+ 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}`;
535
+ const mintTo = r.raw(at + 32 * 3, 32);
536
+ 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)}`;
538
+ }
539
+ function chainflipToSolana(r, at, t) {
540
+ 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}`;
543
+ const asset = r.word(at + 32);
544
+ 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}`;
546
+ }
547
+ var SOLANA_DELIVERY = {
548
+ Mayan: mayanToSolana,
549
+ PolymerCCTP: polymerToSolana,
550
+ Chainflip: chainflipToSolana
551
+ };
552
+ 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";
553
+ 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
+ var INTENTS_FACET = "LiFiIntentEscrowV2";
555
+ 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;
560
+ const read = SOLANA_DELIVERY[bridge.facet];
561
+ if (!read) return NON_EVM_UNREAD;
562
+ const owner = solanaKey(e.receiver);
563
+ if (!owner) return `the recipient ${e.receiver} is not a Solana address`;
564
+ const mint = solanaKey(e.toToken);
565
+ if (!mint) return `the destination token ${e.toToken} is not a Solana mint`;
566
+ return read(r, facetDataAt(r, bridge, FACET_DATA[bridge.facet]), { owner, ownerHex: hexOf(owner), mint, mintHex: hexOf(mint), e });
567
+ }
423
568
  function diamondCallMismatch(data, e) {
424
569
  let r;
425
570
  try {
@@ -448,6 +593,7 @@ function diamondCallMismatch(data, e) {
448
593
  if (unbound) return `${unbound} delivers to a recipient the transaction does not bind (an off-chain order or unchecked facet data)`;
449
594
  const bridge = BRIDGE_CALLS[selector];
450
595
  if (!bridge) return `unknown Diamond call ${selector}`;
596
+ if (bridge.facet === INTENTS_FACET && e.evmDestination) return INTENTS_UNPINNED;
451
597
  const head = r.small(4);
452
598
  if (head < (bridge.sourceSwaps ? 96 : 64) || head % 32 !== 0 || head > 32 * 32) return `unknown Diamond call ${selector}`;
453
599
  const at = 4 + head;
@@ -461,8 +607,10 @@ function diamondCallMismatch(data, e) {
461
607
  if (e.sameChain) return "a bridge call on a same-chain route";
462
608
  if (destinationChainId !== e.destChainId) return `bridges to chain ${destinationChainId}, not ${e.destChainId}`;
463
609
  if (hasDestinationCall) return "the route runs a call on the destination chain, whose recipient cannot be checked here";
464
- if (!e.evmDestination) return "the recipient on a non-EVM destination sits in bridge-specific data and cannot be checked here";
465
- if (!same(receiver, e.receiver)) return `pays ${receiver}, not ${e.receiver}`;
610
+ if (!e.evmDestination) {
611
+ const why = solanaRecipientMismatch(r, bridge, receiver, e);
612
+ if (why) return why;
613
+ } else if (!same(receiver, e.receiver)) return `pays ${receiver}, not ${e.receiver}`;
466
614
  if (hasSourceSwaps !== bridge.sourceSwaps) return `unknown Diamond call ${selector}`;
467
615
  const refund = refundMismatch(r, bridge, e);
468
616
  if (refund) return refund;
@@ -482,7 +630,6 @@ function diamondCallMismatch(data, e) {
482
630
 
483
631
  // src/solana.ts
484
632
  var import_util2 = require("@anywalletkit/protocol-swap2-core/util");
485
- var ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
486
633
  var MAYAN_ENTRY_PROGRAMS = /* @__PURE__ */ new Set([
487
634
  "D8C8iW6zmoKg5TRr8nQ7h14TMWqQX8FiBdj2ju5MF3wa",
488
635
  // CPI_PROXY_PROGRAM_ID
@@ -497,20 +644,6 @@ var MAYAN_ENTRY_PROGRAMS = /* @__PURE__ */ new Set([
497
644
  ]);
498
645
  var SYSTEM_PROGRAM = "11111111111111111111111111111111";
499
646
  var LIFI_SOLANA_FEE_WALLET = "34FKjAdVcTax2DHqV2XnbXa9J3zmyKcFuFKWbcmgxjgm";
500
- function base58(bytes) {
501
- let n = 0n;
502
- for (const b of bytes) n = n << 8n | BigInt(b);
503
- let out = "";
504
- while (n > 0n) {
505
- out = ALPHABET[Number(n % 58n)] + out;
506
- n /= 58n;
507
- }
508
- for (const b of bytes) {
509
- if (b !== 0) break;
510
- out = `1${out}`;
511
- }
512
- return out;
513
- }
514
647
  var Cursor = class {
515
648
  constructor(b) {
516
649
  this.b = b;
@@ -612,7 +745,8 @@ var LIFI_CHAIN = {
612
745
  "eip155:2818": "2818",
613
746
  "eip155:14": "14",
614
747
  // 2026-11-10: Elysium (Kinetiq; li.quest/v1/chains id 99901, key "ely"). LI.FI's bridges there are Relay (refused
615
- // below: UNBOUND_TOOLS) and LI.FI Intents; same-chain swaps go through GenericSwapFacetV3 (KyberSwap, Nordstern).
748
+ // below: UNBOUND_TOOLS) and LI.FI Intents (refused too: UNPINNED_TOOLS), so it has no cross-chain LI.FI route for now;
749
+ // same-chain swaps go through GenericSwapFacetV3 (KyberSwap, Nordstern).
616
750
  "eip155:99901": "99901",
617
751
  [LIFI_SOLANA_CAIP2]: "1151111081099710"
618
752
  };
@@ -622,12 +756,12 @@ var SOLANA_NATIVE = "11111111111111111111111111111111";
622
756
  var EVM_PLACEHOLDER = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
623
757
  var SOLANA_PLACEHOLDER = "HAgk14JpMQLgt6rVgv7cBQFJWFto5Dqxi472uT3DKpqk";
624
758
  var isSolana = (chain) => chain === LIFI_SOLANA_CAIP2;
625
- var BASE58 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
759
+ var BASE582 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
626
760
  var BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
627
761
  var SAME_CHAIN_MAX_SECONDS = 90;
628
762
  var MAX_INPUT_FEE_BPS = 300;
629
763
  function sameChainAddress(chain, actual, expected) {
630
- if (isSolana(chain)) return !!actual && BASE58.test(actual) && actual === expected;
764
+ if (isSolana(chain)) return !!actual && BASE582.test(actual) && actual === expected;
631
765
  return sameAddress(actual, expected);
632
766
  }
633
767
  function nativeFor(chain, token) {
@@ -716,7 +850,16 @@ function headers(key) {
716
850
  }
717
851
  var TOOL_KEY = /^[A-Za-z0-9_-]{1,64}$/;
718
852
  var UNBOUND_TOOLS = Object.freeze(["relaydepository", "garden", "squid", "symbiosis"]);
719
- function routeOptions(body, excluded) {
853
+ var UNPINNED_TOOLS = Object.freeze(["lifiIntents"]);
854
+ var REFUSED_EVM_TOOLS = Object.freeze([...UNBOUND_TOOLS, ...UNPINNED_TOOLS]);
855
+ var SOLANA_DESTINATION_TOOLS = Object.freeze(["mayan", "mayanMCTP", "mayanFastMCTP", "polymer", "polymerStandard", "chainflip"]);
856
+ var SMART_DEPOSITS_TOOL = /^smartdeposits?$/i;
857
+ var dryRecipient = (params, fromAddress) => !isSolana(params.sourceChain) && isSolana(params.destChain) ? SOLANA_PLACEHOLDER : fromAddress;
858
+ function placeholderIn(params) {
859
+ if (!params.refundAddress) return "sender";
860
+ return !params.destAddress && dryRecipient(params, params.refundAddress) !== params.refundAddress ? "recipient" : null;
861
+ }
862
+ function routeOptions(body, excluded, allowed) {
720
863
  const routes = body?.routes;
721
864
  if (!Array.isArray(routes)) return [];
722
865
  const out = [];
@@ -726,7 +869,7 @@ function routeOptions(body, excluded) {
726
869
  const cross = Array.isArray(step.includedSteps) ? step.includedSteps.find((i) => i?.type === "cross")?.tool : void 0;
727
870
  const tool = typeof cross === "string" ? cross : step.tool;
728
871
  const eta = step.estimate?.executionDuration;
729
- if (typeof tool !== "string" || !TOOL_KEY.test(tool) || excluded.includes(tool) || typeof eta !== "number" || !Number.isFinite(eta) || eta < 0) continue;
872
+ if (typeof tool !== "string" || !TOOL_KEY.test(tool) || excluded.includes(tool) || allowed && !allowed.includes(tool) || typeof eta !== "number" || !Number.isFinite(eta) || eta < 0) continue;
730
873
  if (typeof r.toAmount !== "string" || typeof r.toAmountMin !== "string" || !/^\d{1,60}$/.test(r.toAmount) || !/^\d{1,60}$/.test(r.toAmountMin)) continue;
731
874
  out.push({
732
875
  tool,
@@ -791,13 +934,15 @@ function lifiProvider(config = {}) {
791
934
  if (!fromToken) return null;
792
935
  const solanaSource = isSolana(params.sourceChain);
793
936
  const crossChain = params.sourceChain !== params.destChain;
937
+ const evmToSolana = !solanaSource && isSolana(params.destChain);
794
938
  const noDestinationCall = crossChain && !solanaSource;
795
939
  let routeScope = config.routeWideSlippage === true && !solanaSource && !isSolana(params.destChain);
796
940
  const dry = params.dry !== false;
797
941
  const fromAddress = params.refundAddress || (dry ? solanaSource ? SOLANA_PLACEHOLDER : EVM_PLACEHOLDER : "");
798
- const toAddress = params.destAddress || (dry ? fromAddress : "");
942
+ const toAddress = params.destAddress || (dry ? dryRecipient(params, fromAddress) : "");
799
943
  if (!fromAddress) return reject("a live quote needs the sender's address");
800
944
  if (!toAddress) return reject("a live quote needs a recipient");
945
+ const placeholder = dry ? placeholderIn(params) : null;
801
946
  const slippageBps = params.slippageBps ?? defaultSlippageBps;
802
947
  let sentSlippage = slippageBps / 1e4;
803
948
  let deadline;
@@ -818,7 +963,7 @@ function lifiProvider(config = {}) {
818
963
  options: {
819
964
  slippage: slippageBps / 1e4,
820
965
  ...routeScope ? { slippageScope: "route" } : {},
821
- ...solanaSource ? {} : { bridges: { deny: UNBOUND_TOOLS } },
966
+ ...solanaSource ? {} : { bridges: { ...evmToSolana ? { allow: SOLANA_DESTINATION_TOOLS } : {}, deny: REFUSED_EVM_TOOLS } },
822
967
  ...noDestinationCall ? { allowDestinationCall: false } : {},
823
968
  ...integrator().trim() ? { integrator: integrator().trim() } : {}
824
969
  }
@@ -826,7 +971,7 @@ function lifiProvider(config = {}) {
826
971
  signal: signal ?? (0, import_util3.timeoutSignal)(8e3)
827
972
  });
828
973
  if (res.ok) {
829
- const options = routeOptions(await res.json(), solanaSource ? [] : UNBOUND_TOOLS);
974
+ const options = routeOptions(await res.json(), solanaSource ? [] : REFUSED_EVM_TOOLS, evmToSolana ? SOLANA_DESTINATION_TOOLS : void 0);
830
975
  if (options.length) choice = cleanChoice(config.chooseRoute(options, params));
831
976
  }
832
977
  } catch {
@@ -848,8 +993,10 @@ function lifiProvider(config = {}) {
848
993
  }
849
994
  if (routeScope) url.searchParams.set("slippageScope", "route");
850
995
  if (noDestinationCall) url.searchParams.set("allowDestinationCall", "false");
851
- const unbound = crossChain && !solanaSource ? UNBOUND_TOOLS : [];
852
- for (const t of (choice?.allowBridges ?? []).filter((b) => solanaSource || !UNBOUND_TOOLS.includes(b))) url.searchParams.append("allowBridges", t);
996
+ const unbound = crossChain && !solanaSource ? REFUSED_EVM_TOOLS : [];
997
+ const allow = (choice?.allowBridges ?? []).filter((b) => solanaSource || !REFUSED_EVM_TOOLS.includes(b));
998
+ const solanaAllow = evmToSolana ? allow.filter((b) => SOLANA_DESTINATION_TOOLS.includes(b)) : [];
999
+ for (const t of evmToSolana ? solanaAllow.length ? solanaAllow : SOLANA_DESTINATION_TOOLS : allow) url.searchParams.append("allowBridges", t);
853
1000
  for (const t of /* @__PURE__ */ new Set([...choice?.denyBridges ?? [], ...unbound])) url.searchParams.append("denyBridges", t);
854
1001
  if (choice?.order) url.searchParams.set("order", choice.order);
855
1002
  deadline = signal ? void 0 : (0, import_util3.timeoutSignal)(12e3);
@@ -873,6 +1020,13 @@ function lifiProvider(config = {}) {
873
1020
  }
874
1021
  const q = await res.json();
875
1022
  const receivedAt = Date.now();
1023
+ if (evmToSolana) {
1024
+ const steps = q.includedSteps;
1025
+ const tools = [q.tool, ...Array.isArray(steps) ? steps.map((i) => i?.tool) : []];
1026
+ 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");
1028
+ }
1029
+ }
876
1030
  const action = q.action;
877
1031
  const est = q.estimate;
878
1032
  const tr = q.transactionRequest;
@@ -913,6 +1067,7 @@ function lifiProvider(config = {}) {
913
1067
  if (!trustedAddress(params.sourceChain, est.approvalAddress, trustedApprovalAddresses)) return reject("the approval target is not a trusted LI.FI contract");
914
1068
  }
915
1069
  let txRequest;
1070
+ let nativeFee = 0n;
916
1071
  if (solanaSource) {
917
1072
  txRequest = { serializedTx: tr.data, value: sourceIsToken ? "0" : params.amountIn };
918
1073
  } else {
@@ -922,7 +1077,7 @@ function lifiProvider(config = {}) {
922
1077
  return reject("the transaction value or gas limit is not a number");
923
1078
  }
924
1079
  const value = BigInt(tr.value);
925
- const nativeFee = sourceIsToken ? value : value > BigInt(params.amountIn) ? value - BigInt(params.amountIn) : 0n;
1080
+ nativeFee = sourceIsToken ? value : value > BigInt(params.amountIn) ? value - BigInt(params.amountIn) : 0n;
926
1081
  if (!sourceIsToken && value < BigInt(params.amountIn)) return reject("the transaction sends less native coin than the input");
927
1082
  txRequest = {
928
1083
  to: tr.to,
@@ -983,7 +1138,10 @@ function lifiProvider(config = {}) {
983
1138
  intentId: `lifi:${fromChain}:${toChain}`,
984
1139
  depositAddress: "",
985
1140
  depositMemo: null,
986
- txRequest,
1141
+ // Every check above ran on LI.FI's transaction; one built for a placeholder is not handed out (placeholderIn).
1142
+ // Its bridge fee on top of the input still is, as `providerData.nativeFee`, so the router's value nets it out
1143
+ // (usdValue reads it when there is no txRequest) instead of ranking the route on its gross output.
1144
+ ...placeholder ? { providerData: { placeholder, ...nativeFee > 0n ? { nativeFee: nativeFee.toString() } : {} } } : { txRequest },
987
1145
  // What the wallet spends (action.fromAmount, = the input); an input-side bridge fee is inside destAmount.
988
1146
  sourceAmount: params.amountIn,
989
1147
  destAmount: est.toAmount,
@@ -1003,6 +1161,10 @@ function lifiProvider(config = {}) {
1003
1161
  return null;
1004
1162
  },
1005
1163
  async buildDeposit(quote) {
1164
+ const placeholder = quote.providerData?.placeholder;
1165
+ if (placeholder !== void 0) {
1166
+ 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`);
1167
+ }
1006
1168
  const tx = quote.txRequest;
1007
1169
  if (!tx) throw new Error("swap2: LI.FI quote has no transaction request");
1008
1170
  const [, fromChain] = quote.intentId.split(":");
@@ -1054,6 +1216,11 @@ function lifiVerifyQuote(params, quote) {
1054
1216
  const fromChain = LIFI_CHAIN[params.sourceChain];
1055
1217
  const toChain = LIFI_CHAIN[params.destChain];
1056
1218
  if (!fromChain || !toChain) return "not a LI.FI chain pair";
1219
+ const placeholder = params.dry !== false ? placeholderIn(params) : null;
1220
+ 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`;
1222
+ return priceOnlyMismatch(params, quote, `lifi:${fromChain}:${toChain}`);
1223
+ }
1057
1224
  const tx = quote.txRequest;
1058
1225
  if (!tx) return "the quote carries no transaction";
1059
1226
  if (isSolana(params.sourceChain)) {
@@ -1068,7 +1235,7 @@ function lifiVerifyQuote(params, quote) {
1068
1235
  const sourceIsToken = fromToken !== EVM_ZERO;
1069
1236
  const dry = params.dry !== false;
1070
1237
  const fromAddress = params.refundAddress || (dry ? EVM_PLACEHOLDER : "");
1071
- const toAddress = params.destAddress || (dry ? fromAddress : "");
1238
+ const toAddress = params.destAddress || (dry ? dryRecipient(params, fromAddress) : "");
1072
1239
  if (!fromAddress || !toAddress) return "a live quote needs the sender and the recipient";
1073
1240
  let amountIn, minOut, value, nativeFee;
1074
1241
  try {
@@ -1102,6 +1269,19 @@ function lifiVerifyQuote(params, quote) {
1102
1269
  });
1103
1270
  return why ? `the transaction does not match the request: ${why}` : null;
1104
1271
  }
1272
+ 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)}`;
1274
+ 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`;
1276
+ try {
1277
+ if (BigInt(quote.sourceAmount) !== BigInt(params.amountIn)) return `the quote spends ${quote.sourceAmount}, not the ${params.amountIn} input`;
1278
+ 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}`;
1280
+ } catch {
1281
+ return "the quote's amounts are not integers";
1282
+ }
1283
+ return quote.intentId.replace(/~[0-9a-f]{32}$/, "") === route ? null : "the intent id does not name this route";
1284
+ }
1105
1285
  var lifiPins = Object.freeze({
1106
1286
  callTargets: (chain) => /^eip155:[0-9]+$/.test(chain) ? [diamondFor(chain)] : [],
1107
1287
  spenders: (chain) => /^eip155:[0-9]+$/.test(chain) ? [diamondFor(chain)] : [],