@anywalletkit/protocol-swap2-lifi 0.3.1 → 0.5.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
@@ -24,9 +24,13 @@ do the same: a same-chain swap (GenericSwapFacet / V3) pays the reviewed recipie
24
24
  reviewed minimum, spending the input token and delivering the requested one; a bridge call must be
25
25
  one of LI.FI's listed `startBridgeTokensVia*` / `swapAndStartBridgeTokensVia*` entry points (packed
26
26
  calls are refused) and its `BridgeData` must name the reviewed recipient and chain and bridge the
27
- input token (or what its source swaps make of it). Refused, because the recipient cannot be read
28
- from the calldata: routes with a destination call, and EVM-source routes to a non-EVM chain.
29
- Unknown calldata is refused.
27
+ input token (or what its source swaps make of it). Every refund, depositor or fallback address a
28
+ bridge carries in its own data (Stargate's LayerZero fee refund, an expired Across deposit or LI.FI
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
31
+ bridges whose facet does not bind the delivery to `BridgeData` (Relay, Garden, deBridge DLN,
32
+ ThorSwap, Squid, Symbiosis) — cross-chain requests ask LI.FI to route around Relay, Garden, Squid
33
+ and Symbiosis, so their pairs get another bridge. Unknown calldata is refused.
30
34
 
31
35
  Every swap step is read, not only the ends: each must spend what the step before delivered, only
32
36
  steps that spend the input token may pull from the wallet (`requiresDeposit`), and together at most
@@ -44,8 +48,10 @@ reviewed minimum relies on LI.FI's API (or your `apiUrl` proxy) answering honest
44
48
 
45
49
  When the provider runs on your server, spread its pins into the app's stub:
46
50
  `remoteProvider({name: "lifi", baseUrl, ...lifiPins})`. The engine then lets the EVM route call and
47
- approve only the LI.FI Diamond of that chain, never an address the backend names; without the pins the stub gets no
48
- EVM calldata route (fail closed).
51
+ approve only the LI.FI Diamond of that chain, never an address the backend names, and `lifiVerifyQuote` decodes the
52
+ server's Diamond call on the device with the same check the provider runs on LI.FI's answer (receiver, minimum, input
53
+ token and amount, output token, destination chain, refund addresses; value and approval for the input kind). Without the pins the stub
54
+ gets no EVM calldata route (fail closed).
49
55
 
50
56
  ## Activity
51
57
 
@@ -26,144 +26,162 @@ var GENERIC_SWAPS = {
26
26
  var BRIDGE_INPUT_FEE_MAX_BPS = 100n;
27
27
  var kindOf = (asset) => /^0x0{40}$/i.test(asset) ? "native" : "erc20";
28
28
  var BRIDGE_CALLS = {
29
- "0x1fd8010c": { sourceSwaps: false },
29
+ "0x1fd8010c": { sourceSwaps: false, facet: "Across" },
30
30
  // startBridgeTokensViaAcross (AcrossFacet)
31
- "0x3a3f7332": { sourceSwaps: true },
31
+ "0x3a3f7332": { sourceSwaps: true, facet: "Across" },
32
32
  // swapAndStartBridgeTokensViaAcross (AcrossFacet)
33
- "0xa1f1ce43": { sourceSwaps: false },
33
+ "0xa1f1ce43": { sourceSwaps: false, facet: "AcrossV4" },
34
34
  // startBridgeTokensViaAcrossV4 (AcrossFacetV4)
35
- "0x1794958f": { sourceSwaps: true },
35
+ "0x1794958f": { sourceSwaps: true, facet: "AcrossV4" },
36
36
  // swapAndStartBridgeTokensViaAcrossV4 (AcrossFacetV4)
37
- "0x6a90d66e": { sourceSwaps: false },
37
+ "0x6a90d66e": { sourceSwaps: false, facet: "AcrossV4Swap" },
38
38
  // startBridgeTokensViaAcrossV4Swap (AcrossV4SwapFacet)
39
- "0x9b054bc4": { sourceSwaps: true },
39
+ "0x9b054bc4": { sourceSwaps: true, facet: "AcrossV4Swap" },
40
40
  // swapAndStartBridgeTokensViaAcrossV4Swap (AcrossV4SwapFacet)
41
- "0x6a51e9a9": { sourceSwaps: false },
41
+ "0x6a51e9a9": { sourceSwaps: false, facet: "AllBridge" },
42
42
  // startBridgeTokensViaAllBridge (AllBridgeFacet)
43
- "0x63267469": { sourceSwaps: true },
43
+ "0x63267469": { sourceSwaps: true, facet: "AllBridge" },
44
44
  // swapAndStartBridgeTokensViaAllBridge (AllBridgeFacet)
45
- "0xc9851d0b": { sourceSwaps: false },
45
+ "0xc9851d0b": { sourceSwaps: false, facet: "ArbitrumBridge" },
46
46
  // startBridgeTokensViaArbitrumBridge (ArbitrumBridgeFacet)
47
- "0x3cc9517b": { sourceSwaps: true },
47
+ "0x3cc9517b": { sourceSwaps: true, facet: "ArbitrumBridge" },
48
48
  // swapAndStartBridgeTokensViaArbitrumBridge (ArbitrumBridgeFacet)
49
- "0xe0a4201c": { sourceSwaps: false },
49
+ "0xe0a4201c": { sourceSwaps: false, facet: "CelerCircleBridge" },
50
50
  // startBridgeTokensViaCelerCircleBridge (CelerCircleBridgeFacet)
51
- "0x4f3b0759": { sourceSwaps: true },
51
+ "0x4f3b0759": { sourceSwaps: true, facet: "CelerCircleBridge" },
52
52
  // swapAndStartBridgeTokensViaCelerCircleBridge (CelerCircleBridgeFacet)
53
- "0x0315138f": { sourceSwaps: false },
53
+ "0x0315138f": { sourceSwaps: false, facet: "Centrifuge" },
54
54
  // startBridgeTokensViaCentrifuge (CentrifugeFacet)
55
- "0x8fd4d4dd": { sourceSwaps: true },
55
+ "0x8fd4d4dd": { sourceSwaps: true, facet: "Centrifuge" },
56
56
  // swapAndStartBridgeTokensViaCentrifuge (CentrifugeFacet)
57
- "0x0ad553b3": { sourceSwaps: false },
57
+ "0x0ad553b3": { sourceSwaps: false, facet: "Chainflip" },
58
58
  // startBridgeTokensViaChainflip (ChainflipFacet)
59
- "0xee3314a1": { sourceSwaps: true },
59
+ "0xee3314a1": { sourceSwaps: true, facet: "Chainflip" },
60
60
  // swapAndStartBridgeTokensViaChainflip (ChainflipFacet)
61
- "0x4004633e": { sourceSwaps: false },
62
- // startBridgeTokensViaDeBridgeDln (DeBridgeDlnFacet)
63
- "0x2c7d2db0": { sourceSwaps: true },
64
- // swapAndStartBridgeTokensViaDeBridgeDln (DeBridgeDlnFacet)
65
- "0xbff90b61": { sourceSwaps: false },
61
+ "0xbff90b61": { sourceSwaps: false, facet: "Eco" },
66
62
  // startBridgeTokensViaEco (EcoFacet)
67
- "0x762aea18": { sourceSwaps: true },
63
+ "0x762aea18": { sourceSwaps: true, facet: "Eco" },
68
64
  // swapAndStartBridgeTokensViaEco (EcoFacet)
69
- "0x678786ea": { sourceSwaps: false },
65
+ "0x678786ea": { sourceSwaps: false, facet: "Frax" },
70
66
  // startBridgeTokensViaFrax (FraxFacet)
71
- "0x07b8e4c7": { sourceSwaps: true },
67
+ "0x07b8e4c7": { sourceSwaps: true, facet: "Frax" },
72
68
  // swapAndStartBridgeTokensViaFrax (FraxFacet)
73
- "0x5f9af35d": { sourceSwaps: false },
74
- // startBridgeTokensViaGarden (GardenFacet)
75
- "0x76ad76fe": { sourceSwaps: true },
76
- // swapAndStartBridgeTokensViaGarden (GardenFacet)
77
- "0xfc5f1003": { sourceSwaps: false },
69
+ "0xfc5f1003": { sourceSwaps: false, facet: "GasZip" },
78
70
  // startBridgeTokensViaGasZip (GasZipFacet)
79
- "0x606326ff": { sourceSwaps: true },
71
+ "0x606326ff": { sourceSwaps: true, facet: "GasZip" },
80
72
  // swapAndStartBridgeTokensViaGasZip (GasZipFacet)
81
- "0x6f9206ba": { sourceSwaps: false },
73
+ "0x6f9206ba": { sourceSwaps: false, facet: "Glacis" },
82
74
  // startBridgeTokensViaGlacis (GlacisFacet)
83
- "0x9c4b6dd9": { sourceSwaps: true },
75
+ "0x9c4b6dd9": { sourceSwaps: true, facet: "Glacis" },
84
76
  // swapAndStartBridgeTokensViaGlacis (GlacisFacet)
85
- "0xf66fe519": { sourceSwaps: false },
77
+ "0xf66fe519": { sourceSwaps: false, facet: "GnosisBridge" },
86
78
  // startBridgeTokensViaGnosisBridge (GnosisBridgeFacet)
87
- "0x7bf96e0a": { sourceSwaps: true },
79
+ "0x7bf96e0a": { sourceSwaps: true, facet: "GnosisBridge" },
88
80
  // swapAndStartBridgeTokensViaGnosisBridge (GnosisBridgeFacet)
89
- "0xee9e98e0": { sourceSwaps: false },
81
+ "0xee9e98e0": { sourceSwaps: false, facet: "LayerSwap" },
90
82
  // startBridgeTokensViaLayerSwap (LayerSwapFacet)
91
- "0x4c279d6b": { sourceSwaps: true },
83
+ "0x4c279d6b": { sourceSwaps: true, facet: "LayerSwap" },
92
84
  // swapAndStartBridgeTokensViaLayerSwap (LayerSwapFacet)
93
- "0x7dbcf1d9": { sourceSwaps: false },
85
+ "0x7dbcf1d9": { sourceSwaps: false, facet: "LiFiIntentEscrowV2" },
94
86
  // startBridgeTokensViaLiFiIntentEscrowV2 (LiFiIntentEscrowFacetV2)
95
- "0x6d21c5df": { sourceSwaps: true },
87
+ "0x6d21c5df": { sourceSwaps: true, facet: "LiFiIntentEscrowV2" },
96
88
  // swapAndStartBridgeTokensViaLiFiIntentEscrowV2 (LiFiIntentEscrowFacetV2)
97
- "0x65782c4b": { sourceSwaps: false },
89
+ "0x65782c4b": { sourceSwaps: false, facet: "M0" },
98
90
  // startBridgeTokensViaM0 (M0Facet)
99
- "0xb9eaf86d": { sourceSwaps: true },
91
+ "0xb9eaf86d": { sourceSwaps: true, facet: "M0" },
100
92
  // swapAndStartBridgeTokensViaM0 (M0Facet)
101
- "0xb4875292": { sourceSwaps: false },
93
+ "0xb4875292": { sourceSwaps: false, facet: "Mayan" },
102
94
  // startBridgeTokensViaMayan (MayanFacet)
103
- "0x80c65808": { sourceSwaps: true },
95
+ "0x80c65808": { sourceSwaps: true, facet: "Mayan" },
104
96
  // swapAndStartBridgeTokensViaMayan (MayanFacet)
105
- "0x4213dfff": { sourceSwaps: false },
97
+ "0x4213dfff": { sourceSwaps: false, facet: "MegaETHBridge" },
106
98
  // startBridgeTokensViaMegaETHBridge (MegaETHBridgeFacet)
107
- "0x22256e89": { sourceSwaps: true },
99
+ "0x22256e89": { sourceSwaps: true, facet: "MegaETHBridge" },
108
100
  // swapAndStartBridgeTokensViaMegaETHBridge (MegaETHBridgeFacet)
109
- "0x4698f032": { sourceSwaps: false },
101
+ "0x4698f032": { sourceSwaps: false, facet: "NEARIntents" },
110
102
  // startBridgeTokensViaNEARIntents (NEARIntentsFacet)
111
- "0x02631b09": { sourceSwaps: true },
103
+ "0x02631b09": { sourceSwaps: true, facet: "NEARIntents" },
112
104
  // swapAndStartBridgeTokensViaNEARIntents (NEARIntentsFacet)
113
- "0x782621d8": { sourceSwaps: false },
105
+ "0x782621d8": { sourceSwaps: false, facet: "OmniBridge" },
114
106
  // startBridgeTokensViaOmniBridge (OmniBridgeFacet)
115
- "0x95726782": { sourceSwaps: true },
107
+ "0x95726782": { sourceSwaps: true, facet: "OmniBridge" },
116
108
  // swapAndStartBridgeTokensViaOmniBridge (OmniBridgeFacet)
117
- "0xce8a97a5": { sourceSwaps: false },
109
+ "0xce8a97a5": { sourceSwaps: false, facet: "OptimismBridge" },
118
110
  // startBridgeTokensViaOptimismBridge (OptimismBridgeFacet)
119
- "0x5bb5d448": { sourceSwaps: true },
111
+ "0x5bb5d448": { sourceSwaps: true, facet: "OptimismBridge" },
120
112
  // swapAndStartBridgeTokensViaOptimismBridge (OptimismBridgeFacet)
121
- "0xaf62c7d6": { sourceSwaps: false },
113
+ "0xaf62c7d6": { sourceSwaps: false, facet: "PolygonBridge" },
122
114
  // startBridgeTokensViaPolygonBridge (PolygonBridgeFacet)
123
- "0xb4f37581": { sourceSwaps: true },
115
+ "0xb4f37581": { sourceSwaps: true, facet: "PolygonBridge" },
124
116
  // swapAndStartBridgeTokensViaPolygonBridge (PolygonBridgeFacet)
125
- "0xf434d6ca": { sourceSwaps: false },
117
+ "0xf434d6ca": { sourceSwaps: false, facet: "PolymerCCTP" },
126
118
  // startBridgeTokensViaPolymerCCTP (PolymerCCTPFacet)
127
- "0x17917a4e": { sourceSwaps: true },
119
+ "0x17917a4e": { sourceSwaps: true, facet: "PolymerCCTP" },
128
120
  // swapAndStartBridgeTokensViaPolymerCCTP (PolymerCCTPFacet)
129
- "0x092e8fa4": { sourceSwaps: false },
130
- // startBridgeTokensViaRelayDepository (RelayDepositoryFacet)
131
- "0xa3443faa": { sourceSwaps: true },
132
- // swapAndStartBridgeTokensViaRelayDepository (RelayDepositoryFacet)
133
- "0x3f313808": { sourceSwaps: false },
134
- // startBridgeTokensViaSquid (SquidFacet)
135
- "0xa8f66666": { sourceSwaps: true },
136
- // swapAndStartBridgeTokensViaSquid (SquidFacet)
137
- "0x14d53077": { sourceSwaps: false },
121
+ "0x14d53077": { sourceSwaps: false, facet: "Stargate" },
138
122
  // startBridgeTokensViaStargate (StargateFacetV2)
139
- "0xa6010a66": { sourceSwaps: true },
123
+ "0xa6010a66": { sourceSwaps: true, facet: "Stargate" },
140
124
  // swapAndStartBridgeTokensViaStargate (StargateFacetV2)
141
- "0x0691ff78": { sourceSwaps: false },
125
+ "0x0691ff78": { sourceSwaps: false, facet: "Superset" },
142
126
  // startBridgeTokensViaSuperset (SupersetFacet)
143
- "0xf26e657f": { sourceSwaps: true },
127
+ "0xf26e657f": { sourceSwaps: true, facet: "Superset" },
144
128
  // swapAndStartBridgeTokensViaSuperset (SupersetFacet)
145
- "0xe23b7a08": { sourceSwaps: false },
146
- // startBridgeTokensViaSymbiosis (SymbiosisFacet)
147
- "0xc46059b2": { sourceSwaps: true },
148
- // swapAndStartBridgeTokensViaSymbiosis (SymbiosisFacet)
149
- "0x2541ec57": { sourceSwaps: false },
150
- // startBridgeTokensViaThorSwap (ThorSwapFacet)
151
- "0xad673d88": { sourceSwaps: true },
152
- // swapAndStartBridgeTokensViaThorSwap (ThorSwapFacet)
153
- "0x64261d58": { sourceSwaps: false },
129
+ "0x64261d58": { sourceSwaps: false, facet: "Unit" },
154
130
  // startBridgeTokensViaUnit (UnitFacet)
155
- "0x21a3af52": { sourceSwaps: true }
131
+ "0x21a3af52": { sourceSwaps: true, facet: "Unit" }
156
132
  // swapAndStartBridgeTokensViaUnit (UnitFacet)
157
133
  };
158
- var Reader = class {
134
+ var UNBOUND_BRIDGES = {
135
+ "0x092e8fa4": "Relay",
136
+ // startBridgeTokensViaRelayDepository
137
+ "0xa3443faa": "Relay",
138
+ // swapAndStartBridgeTokensViaRelayDepository
139
+ "0x5f9af35d": "Garden",
140
+ // startBridgeTokensViaGarden
141
+ "0x76ad76fe": "Garden",
142
+ // swapAndStartBridgeTokensViaGarden
143
+ "0x4004633e": "deBridge DLN",
144
+ // startBridgeTokensViaDeBridgeDln
145
+ "0x2c7d2db0": "deBridge DLN",
146
+ // swapAndStartBridgeTokensViaDeBridgeDln
147
+ "0x2541ec57": "ThorSwap",
148
+ // startBridgeTokensViaThorSwap
149
+ "0xad673d88": "ThorSwap",
150
+ // swapAndStartBridgeTokensViaThorSwap
151
+ "0x3f313808": "Squid",
152
+ // startBridgeTokensViaSquid
153
+ "0xa8f66666": "Squid",
154
+ // swapAndStartBridgeTokensViaSquid
155
+ "0xe23b7a08": "Symbiosis",
156
+ // startBridgeTokensViaSymbiosis
157
+ "0xc46059b2": "Symbiosis"
158
+ // swapAndStartBridgeTokensViaSymbiosis
159
+ };
160
+ var Reader = class _Reader {
159
161
  b;
160
- constructor(hex) {
161
- if (!/^0x([0-9a-fA-F]{2})*$/.test(hex)) throw new Error("not hex");
162
- this.b = Uint8Array.from(hex.slice(2).match(/../g) ?? [], (h) => Number.parseInt(h, 16));
162
+ constructor(src) {
163
+ if (typeof src !== "string") {
164
+ this.b = src;
165
+ return;
166
+ }
167
+ if (!/^0x([0-9a-fA-F]{2})*$/.test(src)) throw new Error("not hex");
168
+ this.b = Uint8Array.from(src.slice(2).match(/../g) ?? [], (h) => Number.parseInt(h, 16));
163
169
  }
164
170
  get length() {
165
171
  return this.b.length;
166
172
  }
173
+ /** `n` raw bytes at `at`, as 0x-hex. */
174
+ raw(at, n) {
175
+ if (at < 0 || at + n > this.b.length) throw new Error("out of bounds");
176
+ return `0x${Array.from(this.b.subarray(at, at + n), (x) => x.toString(16).padStart(2, "0")).join("")}`;
177
+ }
178
+ /** The `bytes` whose offset word sits at `head` (relative to `base`), as a reader over its contents only. */
179
+ bytes(base, head) {
180
+ const at = base + this.small(head);
181
+ const n = this.small(at);
182
+ if (at + 32 + n > this.b.length) throw new Error("bytes out of bounds");
183
+ return new _Reader(this.b.subarray(at + 32, at + 32 + n));
184
+ }
167
185
  word(at) {
168
186
  if (at < 0 || at + 32 > this.b.length) throw new Error("out of bounds");
169
187
  let v = 0n;
@@ -221,6 +239,79 @@ function checkSteps(steps, e, finalAsset) {
221
239
  if (!same(last.receivingAssetId, finalAsset)) return `delivers ${last.receivingAssetId}, not ${finalAsset}`;
222
240
  return null;
223
241
  }
242
+ var MAYAN_REFUND_AT = {
243
+ "0xa3a30834": 4 + 32 * 3,
244
+ // Swift v2 createOrderWithToken(tokenIn, amountIn, (payloadType, trader, destAddr, …), customPayload)
245
+ "0x6147435b": 4 + 32 * 3,
246
+ // Swift v2 createOrderWithSig: the same order tuple
247
+ "0xb866e173": 4,
248
+ // Swift createOrderWithEth((trader, tokenOut, …))
249
+ "0x8e8d142b": 4 + 32 * 2,
250
+ // Swift createOrderWithToken(tokenIn, amountIn, (trader, tokenOut, …))
251
+ "0x6111ad25": 4 + 32 * 9,
252
+ // MayanSwap swap(relayerFees, (mayanAddr, mayanChainId, auctionAddr, destAddr, destChainId, referrer, refundAddr), …)
253
+ "0x1eb1cff0": 4 + 32 * 9
254
+ // MayanSwap wrapAndSwapETH: the same recipient tuple
255
+ };
256
+ function mayanRefunds(r, at) {
257
+ const p = r.bytes(at, at + 32 * 2);
258
+ const pos = MAYAN_REFUND_AT[p.raw(0, 4)];
259
+ return pos === void 0 ? [] : [["the Mayan order's refund address", p.address(pos)]];
260
+ }
261
+ function chainflipRefunds(r, at) {
262
+ const p = r.bytes(at, at + 32 * 5);
263
+ if (p.length < 25 || p.raw(0, 1) !== "0x01") throw new Error("unknown Chainflip parameters");
264
+ return [["the Chainflip refund address", p.raw(5, 20)]];
265
+ }
266
+ function acrossSwapRefunds(r, at) {
267
+ const target = r.word(at);
268
+ const c = r.bytes(at, at + 32);
269
+ if (target === 0n) return [["the Across deposit's depositor", c.address(c.small(0)), true]];
270
+ if (target === 1n) {
271
+ const swap = c.small(0);
272
+ return [["the Across deposit's depositor", c.address(swap + c.small(swap + 64) + 96), true]];
273
+ }
274
+ if (target === 2n) return [["the sponsored OFT refund recipient", c.address(c.small(0) + 32), true]];
275
+ if (target === 3n) return [["the sponsored CCTP refund recipient", c.address(64), true]];
276
+ throw new Error("unknown Across Swap API target");
277
+ }
278
+ var FACET_DATA = {
279
+ AcrossV4: { refunds: [["refundAddress", 1]], zeroRefused: true },
280
+ // the SpokePool depositor: an expired deposit refunds the input there
281
+ AcrossV4Swap: { nested: acrossSwapRefunds },
282
+ Centrifuge: { inline: 2, refunds: [["refundRecipient", 1]], zeroRefused: true },
283
+ Chainflip: { nested: chainflipRefunds },
284
+ Eco: { refunds: [["refundRecipient", 5]], zeroRefused: true },
285
+ // the intent's creator, refunded its reward (the input) on expiry
286
+ Frax: { inline: 5, refunds: [["refundRecipient", 3]], zeroRefused: true },
287
+ Glacis: { inline: 4, refunds: [["refundAddress", 1]], zeroRefused: true },
288
+ LayerSwap: { refunds: [["refundRecipient", 2]], zeroRefused: true },
289
+ LiFiIntentEscrowV2: { refunds: [["depositAndRefundAddress", 2]], zeroRefused: true },
290
+ // the escrow order's user
291
+ M0: { inline: 7, refunds: [["refundRecipient", 1], ["orderOwner", 2]], zeroRefused: true },
292
+ // orderOwner gets a cancelled order back
293
+ Mayan: { refunds: [["refundRecipient", 7]], nested: mayanRefunds, zeroRefused: true },
294
+ NEARIntents: { refunds: [["refundRecipient", 6]], zeroRefused: true },
295
+ // also the 1Click quote's refundTo
296
+ PolymerCCTP: { refunds: [["refundRecipient", 5]], zeroRefused: true },
297
+ Stargate: { refunds: [["refundAddress", 4]] },
298
+ // (assetId, sendParams, fee.nativeFee, fee.lzTokenFee, refundAddress): LayerZero's fee refund
299
+ Superset: { refunds: [["refundAddress", 2], ["fallbackEoA", 3]], zeroRefused: true }
300
+ };
301
+ var NO_REFUND_FIELDS = /* @__PURE__ */ new Set(["Across", "AllBridge", "ArbitrumBridge", "CelerCircleBridge", "GasZip", "GnosisBridge", "MegaETHBridge", "OmniBridge", "OptimismBridge", "PolygonBridge", "Unit"]);
302
+ function refundMismatch(r, bridge, e) {
303
+ const spec = FACET_DATA[bridge.facet];
304
+ if (!spec) return NO_REFUND_FIELDS.has(bridge.facet) ? null : `no known refund layout for ${bridge.facet}`;
305
+ const slot = 4 + (bridge.sourceSwaps ? 64 : 32);
306
+ const at = spec.inline ? slot : 4 + r.small(slot);
307
+ const paid = (spec.refunds ?? []).map(([field, word]) => [`${bridge.facet} ${field}`, r.address(at + 32 * word), spec.zeroRefused === true]);
308
+ paid.push(...spec.nested?.(r, at) ?? []);
309
+ for (const [what, to, zeroRefused] of paid) {
310
+ if (zeroRefused && /^0x0{40}$/.test(to)) continue;
311
+ if (!same(to, e.sender) && !same(to, e.receiver)) return `${what} pays ${to}, neither the sender ${e.sender} nor the recipient`;
312
+ }
313
+ return null;
314
+ }
224
315
  function diamondCallMismatch(data, e) {
225
316
  let r;
226
317
  try {
@@ -245,6 +336,8 @@ function diamondCallMismatch(data, e) {
245
336
  }
246
337
  return checkSteps(steps, e, e.toToken);
247
338
  }
339
+ const unbound = UNBOUND_BRIDGES[selector];
340
+ if (unbound) return `${unbound} delivers to a recipient the transaction does not bind (an off-chain order or unchecked facet data)`;
248
341
  const bridge = BRIDGE_CALLS[selector];
249
342
  if (!bridge) return `unknown Diamond call ${selector}`;
250
343
  const head = r.small(4);
@@ -263,6 +356,8 @@ function diamondCallMismatch(data, e) {
263
356
  if (!e.evmDestination) return "the recipient on a non-EVM destination sits in bridge-specific data and cannot be checked here";
264
357
  if (!same(receiver, e.receiver)) return `pays ${receiver}, not ${e.receiver}`;
265
358
  if (hasSourceSwaps !== bridge.sourceSwaps) return `unknown Diamond call ${selector}`;
359
+ const refund = refundMismatch(r, bridge, e);
360
+ if (refund) return refund;
266
361
  if (hasSourceSwaps) {
267
362
  if (minAmount === 0n) return "bridges nothing after the source swaps";
268
363
  return checkSteps(swapsAt(r, 4, 4 + 32), e, sendingAssetId);
@@ -274,8 +369,98 @@ function diamondCallMismatch(data, e) {
274
369
  }
275
370
  }
276
371
 
372
+ // src/solana.ts
373
+ import { base64Bytes } from "@anywalletkit/protocol-swap2-core/util";
374
+ var ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
375
+ var MAYAN_ENTRY_PROGRAMS = /* @__PURE__ */ new Set([
376
+ "D8C8iW6zmoKg5TRr8nQ7h14TMWqQX8FiBdj2ju5MF3wa",
377
+ // CPI_PROXY_PROGRAM_ID
378
+ "BLZRi6frs4X4DNLw56V4EXai1b6QVESN1BhHBTYM9VcY",
379
+ // SWIFT_PROGRAM_ID
380
+ "mayan34VedncxdK2XobtvWFDXQASUTBXhUVzt2kKgny",
381
+ // SWIFT_V2_PROGRAM_ID
382
+ "dkpZqrxHFrhziEMQ931GLtfy11nFkCsfMftH9u6QwBU",
383
+ // MCTP_PROGRAM_ID
384
+ "Gx9rivpS3YR8pBFwMuP6omYqVxunpLvLkNn7ubNyuZZ5"
385
+ // FAST_MCTP_PROGRAM_ID
386
+ ]);
387
+ var SYSTEM_PROGRAM = "11111111111111111111111111111111";
388
+ var LIFI_SOLANA_FEE_WALLET = "34FKjAdVcTax2DHqV2XnbXa9J3zmyKcFuFKWbcmgxjgm";
389
+ function base58(bytes) {
390
+ let n = 0n;
391
+ for (const b of bytes) n = n << 8n | BigInt(b);
392
+ let out = "";
393
+ while (n > 0n) {
394
+ out = ALPHABET[Number(n % 58n)] + out;
395
+ n /= 58n;
396
+ }
397
+ for (const b of bytes) {
398
+ if (b !== 0) break;
399
+ out = `1${out}`;
400
+ }
401
+ return out;
402
+ }
403
+ var Cursor = class {
404
+ constructor(b) {
405
+ this.b = b;
406
+ }
407
+ b;
408
+ at = 0;
409
+ byte() {
410
+ if (this.at >= this.b.length) throw new Error("truncated");
411
+ return this.b[this.at++];
412
+ }
413
+ bytes(n) {
414
+ if (n < 0 || this.at + n > this.b.length) throw new Error("truncated");
415
+ const out = this.b.subarray(this.at, this.at + n);
416
+ this.at += n;
417
+ return out;
418
+ }
419
+ /** compact-u16 */
420
+ short() {
421
+ let v = 0;
422
+ for (let shift = 0; shift < 21; shift += 7) {
423
+ const b = this.byte();
424
+ v |= (b & 127) << shift;
425
+ if (!(b & 128)) return v;
426
+ }
427
+ throw new Error("bad length");
428
+ }
429
+ };
430
+ function solanaBridgeTxMismatch(base64, sender) {
431
+ const bytes = base64Bytes(base64);
432
+ if (!bytes) return "the Solana transaction is not base64";
433
+ const c = new Cursor(bytes);
434
+ try {
435
+ c.bytes(64 * c.short());
436
+ const first = c.byte();
437
+ if ((first & 128) === 0) c.at--;
438
+ c.bytes(3);
439
+ const keys = Array.from({ length: c.short() }, () => base58(c.bytes(32)));
440
+ if (keys[0] !== sender) return `the Solana transaction is paid by ${keys[0] ?? "nobody"}, not the sender ${sender}`;
441
+ c.bytes(32);
442
+ let mayan = false;
443
+ const payees = [];
444
+ const n = c.short();
445
+ for (let i = 0; i < n; i++) {
446
+ const program = keys[c.byte()];
447
+ const accounts = Array.from(c.bytes(c.short()));
448
+ const data = c.bytes(c.short());
449
+ if (program !== void 0 && MAYAN_ENTRY_PROGRAMS.has(program)) mayan = true;
450
+ if (program === SYSTEM_PROGRAM && data.length === 12 && data[0] === 2 && data[1] === 0 && data[2] === 0 && data[3] === 0) {
451
+ const to = keys[accounts[1] ?? -1];
452
+ if (to !== void 0 && to !== sender && to !== LIFI_SOLANA_FEE_WALLET) payees.push(to);
453
+ }
454
+ }
455
+ return mayan && payees.length ? `the Solana transaction transfers SOL to ${payees[0]}, which is neither the sender nor LI.FI's fee wallet` : null;
456
+ } catch (err) {
457
+ return `the Solana transaction does not decode (${err instanceof Error ? err.message : String(err)})`;
458
+ }
459
+ }
460
+
277
461
  // src/provider.ts
278
462
  import { timeoutSignal, sourceAssetOf } from "@anywalletkit/protocol-swap2-core/util";
463
+ import { slippageFloor } from "@anywalletkit/protocol-swap2-core/validate";
279
464
  import { bigintToNumber } from "@anywalletkit/core";
280
465
  var DEFAULT_LIFI_BASE = "https://li.quest/v1";
281
466
  var LIFI_CHAIN = {
@@ -412,7 +597,8 @@ function headers(key) {
412
597
  return h;
413
598
  }
414
599
  var TOOL_KEY = /^[A-Za-z0-9_-]{1,64}$/;
415
- function routeOptions(body) {
600
+ var UNBOUND_TOOLS = Object.freeze(["relaydepository", "garden", "squid", "symbiosis"]);
601
+ function routeOptions(body, excluded) {
416
602
  const routes = body?.routes;
417
603
  if (!Array.isArray(routes)) return [];
418
604
  const out = [];
@@ -422,7 +608,7 @@ function routeOptions(body) {
422
608
  const cross = Array.isArray(step.includedSteps) ? step.includedSteps.find((i) => i?.type === "cross")?.tool : void 0;
423
609
  const tool = typeof cross === "string" ? cross : step.tool;
424
610
  const eta = step.estimate?.executionDuration;
425
- if (typeof tool !== "string" || !TOOL_KEY.test(tool) || typeof eta !== "number" || !Number.isFinite(eta) || eta < 0) continue;
611
+ if (typeof tool !== "string" || !TOOL_KEY.test(tool) || excluded.includes(tool) || typeof eta !== "number" || !Number.isFinite(eta) || eta < 0) continue;
426
612
  if (typeof r.toAmount !== "string" || typeof r.toAmountMin !== "string" || !/^\d{1,60}$/.test(r.toAmount) || !/^\d{1,60}$/.test(r.toAmountMin)) continue;
427
613
  out.push({
428
614
  tool,
@@ -508,12 +694,12 @@ function lifiProvider(config = {}) {
508
694
  fromAmount: params.amountIn,
509
695
  fromAddress,
510
696
  toAddress,
511
- options: { slippage: slippageBps / 1e4, ...integrator().trim() ? { integrator: integrator().trim() } : {} }
697
+ options: { slippage: slippageBps / 1e4, ...solanaSource ? {} : { bridges: { deny: UNBOUND_TOOLS } }, ...integrator().trim() ? { integrator: integrator().trim() } : {} }
512
698
  }),
513
699
  signal: signal ?? timeoutSignal(8e3)
514
700
  });
515
701
  if (res.ok) {
516
- const options = routeOptions(await res.json());
702
+ const options = routeOptions(await res.json(), solanaSource ? [] : UNBOUND_TOOLS);
517
703
  if (options.length) choice = cleanChoice(config.chooseRoute(options, params));
518
704
  }
519
705
  } catch {
@@ -533,8 +719,9 @@ function lifiProvider(config = {}) {
533
719
  if (sentSlippage !== void 0) {
534
720
  url.searchParams.set("slippage", String(sentSlippage));
535
721
  }
536
- for (const t of choice?.allowBridges ?? []) url.searchParams.append("allowBridges", t);
537
- for (const t of choice?.denyBridges ?? []) url.searchParams.append("denyBridges", t);
722
+ const unbound = params.sourceChain !== params.destChain && !solanaSource ? UNBOUND_TOOLS : [];
723
+ for (const t of (choice?.allowBridges ?? []).filter((b) => solanaSource || !UNBOUND_TOOLS.includes(b))) url.searchParams.append("allowBridges", t);
724
+ for (const t of /* @__PURE__ */ new Set([...choice?.denyBridges ?? [], ...unbound])) url.searchParams.append("denyBridges", t);
538
725
  if (choice?.order) url.searchParams.set("order", choice.order);
539
726
  deadline = signal ? void 0 : timeoutSignal(12e3);
540
727
  const res = await fetch(url, { headers: headers(apiKey()), signal: signal ?? deadline });
@@ -578,6 +765,10 @@ function lifiProvider(config = {}) {
578
765
  if (solanaSource) {
579
766
  if (!tr.data || tr.data.length < 64 || tr.data.length % 4 !== 0 || !BASE64.test(tr.data)) return reject("the Solana transaction is not base64");
580
767
  if (tr.to !== void 0 || tr.value !== void 0 || tr.gasLimit !== void 0) return reject("the Solana transaction carries EVM fields");
768
+ if (fromChain !== toChain) {
769
+ const why = solanaBridgeTxMismatch(tr.data, fromAddress);
770
+ if (why) return reject(why);
771
+ }
581
772
  } 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");
582
773
  if (sentSlippage !== void 0 && (typeof action.slippage !== "number" || Math.abs(action.slippage - sentSlippage) > 1e-12)) return reject("the quote ignored the requested slippage");
583
774
  const sourceIsToken = fromToken !== EVM_ZERO && fromToken !== SOLANA_NATIVE;
@@ -615,7 +806,7 @@ function lifiProvider(config = {}) {
615
806
  if (sentSlippage !== void 0) {
616
807
  const dest = BigInt(est.toAmount);
617
808
  const min = BigInt(minDestAmount);
618
- if (dest > 0n && min < dest * BigInt(1e4 - slippageBps) / 10000n) {
809
+ if (dest > 0n && min < slippageFloor(dest, slippageBps)) {
619
810
  const effective = bigintToNumber((dest - min) * 1000000n / dest) / 1e6;
620
811
  const requested = slippageBps / 1e4;
621
812
  const scaled = Math.floor(sentSlippage * requested / effective * 0.995 * 1e6) / 1e6;
@@ -644,6 +835,7 @@ function lifiProvider(config = {}) {
644
835
  toToken: expectedToToken,
645
836
  amountIn: BigInt(params.amountIn),
646
837
  receiver: toAddress,
838
+ sender: fromAddress,
647
839
  minOut: BigInt(minDestAmount),
648
840
  destChainId: BigInt(toChain),
649
841
  sameChain: fromChain === toChain,
@@ -662,6 +854,7 @@ function lifiProvider(config = {}) {
662
854
  sourceAmount: params.amountIn,
663
855
  destAmount: est.toAmount,
664
856
  minDestAmount,
857
+ ...Number.isInteger(action.toToken?.decimals) && action.toToken.decimals >= 0 && action.toToken.decimals <= 36 ? { destDecimals: action.toToken.decimals } : {},
665
858
  feeUsd,
666
859
  etaSeconds: est.executionDuration ?? 120,
667
860
  reliability: 0.97,
@@ -723,13 +916,67 @@ function lifiProvider(config = {}) {
723
916
  }
724
917
  };
725
918
  }
919
+ function lifiVerifyQuote(params, quote) {
920
+ const fromChain = LIFI_CHAIN[params.sourceChain];
921
+ const toChain = LIFI_CHAIN[params.destChain];
922
+ if (!fromChain || !toChain) return "not a LI.FI chain pair";
923
+ const tx = quote.txRequest;
924
+ if (!tx) return "the quote carries no transaction";
925
+ if (isSolana(params.sourceChain)) {
926
+ if (!tx.serializedTx || tx.data !== void 0 || tx.to !== void 0) return "a Solana source carries a serialized transaction only";
927
+ return fromChain === toChain ? null : solanaBridgeTxMismatch(tx.serializedTx, params.refundAddress || (params.dry !== false ? SOLANA_PLACEHOLDER : ""));
928
+ }
929
+ if (!isHexData(tx.data) || !tx.to || tx.serializedTx !== void 0) return "the quote carries no Diamond call";
930
+ if (!sameAddress(tx.to, diamondFor(params.sourceChain))) return `the call targets ${tx.to}, not the LI.FI Diamond`;
931
+ if (tx.chainId !== void 0 && String(tx.chainId) !== fromChain) return `the call is for chain ${tx.chainId}, not ${fromChain}`;
932
+ const fromToken = lifiToken(params);
933
+ if (!fromToken) return `unknown source token ${params.sourceToken}`;
934
+ const sourceIsToken = fromToken !== EVM_ZERO;
935
+ const dry = params.dry !== false;
936
+ const fromAddress = params.refundAddress || (dry ? EVM_PLACEHOLDER : "");
937
+ const toAddress = params.destAddress || (dry ? fromAddress : "");
938
+ if (!fromAddress || !toAddress) return "a live quote needs the sender and the recipient";
939
+ let amountIn, minOut, value, nativeFee;
940
+ try {
941
+ amountIn = BigInt(params.amountIn);
942
+ minOut = BigInt(quote.minDestAmount);
943
+ value = BigInt(tx.value ?? "0");
944
+ nativeFee = BigInt(tx.nativeFee ?? "0");
945
+ } catch {
946
+ return "the quote's amounts are not integers";
947
+ }
948
+ if (BigInt(quote.sourceAmount) !== amountIn) return `the quote spends ${quote.sourceAmount}, not the ${amountIn} input`;
949
+ if (params.minAmountOut !== void 0 && minOut < BigInt(params.minAmountOut)) return `the quoted minimum ${minOut} is below the caller's floor ${params.minAmountOut}`;
950
+ if (sourceIsToken) {
951
+ if (value !== nativeFee) return "a token input attaches only its declared bridge fee";
952
+ if (!tx.approvalAddress || !sameAddress(tx.approvalAddress, diamondFor(params.sourceChain))) return "a token input approves the LI.FI Diamond";
953
+ } else {
954
+ if (value < amountIn || value - amountIn !== nativeFee) return "a native input is paid as the input plus its declared bridge fee";
955
+ if (tx.approvalAddress !== void 0) return "a native input approves nothing";
956
+ }
957
+ if (quote.intentId.replace(/~[0-9a-f]{32}$/, "") !== `lifi:${fromChain}:${toChain}`) return "the intent id does not name this route";
958
+ const why = diamondCallMismatch(tx.data, {
959
+ fromToken,
960
+ toToken: nativeFor(params.destChain, params.destToken),
961
+ amountIn,
962
+ receiver: toAddress,
963
+ sender: fromAddress,
964
+ minOut,
965
+ destChainId: BigInt(toChain),
966
+ sameChain: fromChain === toChain,
967
+ evmDestination: !isSolana(params.destChain)
968
+ });
969
+ return why ? `the transaction does not match the request: ${why}` : null;
970
+ }
726
971
  var lifiPins = Object.freeze({
727
972
  callTargets: (chain) => /^eip155:[0-9]+$/.test(chain) ? [diamondFor(chain)] : [],
728
- spenders: (chain) => /^eip155:[0-9]+$/.test(chain) ? [diamondFor(chain)] : []
973
+ spenders: (chain) => /^eip155:[0-9]+$/.test(chain) ? [diamondFor(chain)] : [],
974
+ verifyQuote: lifiVerifyQuote
729
975
  });
730
976
 
731
977
  export {
732
978
  lifiProvider,
979
+ lifiVerifyQuote,
733
980
  lifiPins
734
981
  };
735
- //# sourceMappingURL=chunk-Z27B4UI4.js.map
982
+ //# sourceMappingURL=chunk-67QPKWAY.js.map