@anywalletkit/protocol-swap2-lifi 0.7.0 → 0.8.1

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