@gibs/bridge-sdk 1.12.0 → 1.13.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.
Files changed (118) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +234 -432
  3. package/dist/abis.d.ts +1 -0
  4. package/dist/bridge-limits.d.ts +103 -0
  5. package/dist/bridge-limits.js +84 -0
  6. package/dist/chain-info.d.ts +169 -0
  7. package/dist/chain-info.js +410 -0
  8. package/dist/chain-names.d.ts +160 -0
  9. package/dist/chain-names.js +200 -0
  10. package/dist/chains.d.ts +65 -0
  11. package/dist/chains.js +284 -0
  12. package/dist/config.d.ts +216 -0
  13. package/dist/config.js +493 -0
  14. package/dist/consolidated-quote.d.ts +554 -0
  15. package/dist/consolidated-quote.js +773 -0
  16. package/dist/destination-calldata.d.ts +118 -0
  17. package/dist/destination-calldata.js +152 -0
  18. package/dist/ecosystems.d.ts +217 -0
  19. package/dist/ecosystems.js +288 -0
  20. package/dist/fee-type.d.ts +10 -0
  21. package/dist/fee-type.js +9 -0
  22. package/dist/hyperliquid-execution.d.ts +152 -0
  23. package/dist/hyperliquid-execution.js +168 -0
  24. package/dist/image-links.d.ts +117 -0
  25. package/dist/image-links.js +142 -0
  26. package/dist/index.d.ts +20 -0
  27. package/dist/index.js +20 -0
  28. package/dist/lifi-execution-guard.d.ts +199 -0
  29. package/dist/lifi-execution-guard.js +400 -0
  30. package/dist/near-intents-conformance.d.ts +85 -0
  31. package/dist/near-intents-conformance.js +329 -0
  32. package/dist/near-intents-route.d.ts +47 -0
  33. package/dist/near-intents-route.js +265 -0
  34. package/dist/near-intents.d.ts +524 -0
  35. package/dist/near-intents.js +1055 -0
  36. package/dist/origin-deposit.d.ts +326 -0
  37. package/dist/origin-deposit.js +68 -0
  38. package/dist/pre-quote.d.ts +198 -0
  39. package/dist/pre-quote.js +175 -0
  40. package/dist/prices.d.ts +107 -0
  41. package/dist/prices.js +162 -0
  42. package/dist/providers.d.ts +221 -0
  43. package/dist/providers.js +157 -0
  44. package/dist/quote-fees.d.ts +102 -0
  45. package/dist/quote-fees.js +195 -0
  46. package/dist/referral.d.ts +77 -0
  47. package/dist/referral.js +72 -0
  48. package/dist/relay-currencies.d.ts +47 -0
  49. package/dist/relay-currencies.js +89 -0
  50. package/dist/relay-quote.d.ts +301 -0
  51. package/dist/relay-quote.js +983 -0
  52. package/dist/route-steps.d.ts +138 -0
  53. package/dist/route-steps.js +305 -0
  54. package/dist/routing/amounts.d.ts +80 -0
  55. package/dist/routing/amounts.js +120 -0
  56. package/dist/routing/candidate-route.d.ts +43 -0
  57. package/dist/routing/candidate-route.js +45 -0
  58. package/dist/routing/candidate.d.ts +237 -0
  59. package/dist/routing/candidate.js +96 -0
  60. package/dist/routing/carriers/ask-answer.d.ts +60 -0
  61. package/dist/routing/carriers/ask-answer.js +1 -0
  62. package/dist/routing/carriers/omnibridge.d.ts +137 -0
  63. package/dist/routing/carriers/omnibridge.js +419 -0
  64. package/dist/routing/carriers/pulsex.d.ts +149 -0
  65. package/dist/routing/carriers/pulsex.js +453 -0
  66. package/dist/routing/carriers/wrap.d.ts +25 -0
  67. package/dist/routing/carriers/wrap.js +115 -0
  68. package/dist/routing/composed-route-quality.d.ts +103 -0
  69. package/dist/routing/composed-route-quality.js +108 -0
  70. package/dist/routing/entry-chain-anchor.d.ts +88 -0
  71. package/dist/routing/entry-chain-anchor.js +55 -0
  72. package/dist/routing/format.d.ts +31 -0
  73. package/dist/routing/format.js +58 -0
  74. package/dist/routing/index.d.ts +34 -0
  75. package/dist/routing/index.js +42 -0
  76. package/dist/routing/producers.d.ts +770 -0
  77. package/dist/routing/producers.js +1564 -0
  78. package/dist/routing/providers.d.ts +26 -0
  79. package/dist/routing/providers.js +38 -0
  80. package/dist/routing/route-data.d.ts +355 -0
  81. package/dist/routing/route-data.js +1 -0
  82. package/dist/routing/search/compose.d.ts +168 -0
  83. package/dist/routing/search/compose.js +98 -0
  84. package/dist/routing/search/graph.d.ts +653 -0
  85. package/dist/routing/search/graph.js +445 -0
  86. package/dist/routing/search/plan-route-data.d.ts +244 -0
  87. package/dist/routing/search/plan-route-data.js +767 -0
  88. package/dist/routing/search/score.d.ts +353 -0
  89. package/dist/routing/search/score.js +427 -0
  90. package/dist/routing/usd-value.d.ts +17 -0
  91. package/dist/routing/usd-value.js +25 -0
  92. package/dist/settings.d.ts +692 -0
  93. package/dist/settings.js +909 -0
  94. package/dist/ton-execution.d.ts +117 -0
  95. package/dist/ton-execution.js +247 -0
  96. package/dist/tron-execution.d.ts +146 -0
  97. package/dist/tron-execution.js +239 -0
  98. package/dist/types.d.ts +51 -0
  99. package/dist/types.js +1 -0
  100. package/llms.txt +169 -0
  101. package/package.json +204 -23
  102. package/src/chain-info.test.ts +0 -968
  103. package/src/chain-info.ts +0 -498
  104. package/src/chains.test.ts +0 -123
  105. package/src/chains.ts +0 -88
  106. package/src/config.test.ts +0 -679
  107. package/src/config.ts +0 -500
  108. package/src/fee-type.test.ts +0 -45
  109. package/src/fee-type.ts +0 -14
  110. package/src/image-links.test.ts +0 -160
  111. package/src/image-links.ts +0 -65
  112. package/src/index.ts +0 -8
  113. package/src/settings.test.ts +0 -2754
  114. package/src/settings.ts +0 -875
  115. package/src/types.ts +0 -48
  116. package/tsconfig.json +0 -17
  117. package/vitest.config.ts +0 -13
  118. /package/{src/abis.ts → dist/abis.js} +0 -0
@@ -0,0 +1,400 @@
1
+ import { decodeAbiParameters, zeroAddress } from 'viem';
2
+ /**
3
+ * Thrown by {@link verifyLifiEvmExecution} instead of returning a deposit a
4
+ * wallet could sign.
5
+ *
6
+ * NAMED AND TYPED SO A REFUSAL IS NEVER A SILENT DROP. Every caller that
7
+ * catches this can read `refusal.reason` and show, or log, exactly which
8
+ * check failed — the same discipline `@gibs/quotes`'s `ProviderQuoteError`
9
+ * applies to a provider declining a quote, applied here to a provider's
10
+ * ANSWER disagreeing with itself.
11
+ */
12
+ export class LifiExecutionRefusedError extends Error {
13
+ refusal;
14
+ constructor(refusal) {
15
+ super(`LI.FI execution transaction refused: ${refusal.detail}`);
16
+ this.refusal = refusal;
17
+ this.name = 'LifiExecutionRefusedError';
18
+ }
19
+ }
20
+ const refuse = (reason, detail) => {
21
+ throw new LifiExecutionRefusedError({ reason, detail });
22
+ };
23
+ /**
24
+ * LI.FI's Diamond entryway contract address, by chain id.
25
+ *
26
+ * SOURCE. `lifinance/contracts` (https://github.com/lifinance/contracts),
27
+ * commit `025989955bb373739737ee78ddff01620638ae3b` (2026-09-23): the
28
+ * `LiFiDiamond` address in `deployments/<network>.json`, matched to its
29
+ * chain id in `config/networks.json`, for every network whose `status` is
30
+ * `"active"` and `"type"` is `"mainnet"`. LI.FI's own documentation
31
+ * (https://docs.li.fi/introduction/lifi-architecture/smart-contract-addresses)
32
+ * states the address is `0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE` "on most
33
+ * supported networks" but differs on some — confirmed here directly against
34
+ * the deployment records rather than assumed: 39 of the 65 active mainnet
35
+ * networks use a DIFFERENT address, almost all of them newer or lower-volume
36
+ * chains (0G, Abstract, ApeChain, Arbitrum Nova, Berachain, Flare, zkSync
37
+ * Era, and more).
38
+ *
39
+ * DELIBERATELY A FIXED, BAKED-IN TABLE, NEVER A LIVE LOOKUP. LI.FI's own
40
+ * `/v1/chains` endpoint carries a `diamondAddress` per chain, and this
41
+ * codebase already fetches it for other purposes — but that endpoint is
42
+ * reached through the SAME configurable, potentially-compromised host this
43
+ * module exists to distrust. Trusting it here would make the whole guard
44
+ * circular: a compromised proxy could simply report its own malicious
45
+ * address as the "correct" one. This table is real code, reviewed and
46
+ * shipped with the application, and can only change through a new release.
47
+ *
48
+ * UNKNOWN CHAIN, UNKNOWN ADDRESS. A chain id absent from this table refuses
49
+ * rather than falling back to the common address — LI.FI adds networks
50
+ * continuously, and assuming a newly-added chain shares the common address
51
+ * would be exactly the kind of guess this module exists to avoid making.
52
+ */
53
+ export const lifiDiamondAddressByChainId = new Map([
54
+ [1, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // mainnet
55
+ [10, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // optimism
56
+ [14, '0x198FC70Dfe05E755C81e54bd67Bff3F729344B9b'], // flare
57
+ [25, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // cronos
58
+ [30, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // rootstock
59
+ [40, '0xa7cd44521903C65915648385E3FE881E49171582'], // telos
60
+ [50, '0x055d4612Ec74aD799C6cB4dF72C0Ab8dbDBCBAfa'], // xdc
61
+ [56, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // bsc
62
+ [88, '0x198FC70Dfe05E755C81e54bd67Bff3F729344B9b'], // viction
63
+ [100, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // gnosis
64
+ [122, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // fuse
65
+ [130, '0x864b314D4C5a0399368609581d3E8933a63b9232'], // unichain
66
+ [137, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // polygon
67
+ [143, '0x026F252016A7C47CDEf1F05a3Fc9E20C92a49C37'], // monad
68
+ [146, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // sonic
69
+ [196, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // xlayer
70
+ [204, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // opbnb
71
+ [232, '0xF3B20515d9B193531c48E47c18aF16d1e5d28f9a'], // lens
72
+ [252, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // fraxtal
73
+ [288, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // boba
74
+ [324, '0x341e94069f53234fE6DabeF707aD424830525715'], // zksync
75
+ [480, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // worldchain
76
+ [747, '0x026F252016A7C47CDEf1F05a3Fc9E20C92a49C37'], // flow
77
+ [988, '0x026F252016A7C47CDEf1F05a3Fc9E20C92a49C37'], // stable
78
+ [1088, '0x24ca98fB6972F5eE05f0dB00595c7f68D9FaFd68'], // metis
79
+ [1135, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // lisk
80
+ [1329, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // sei
81
+ [1480, '0x198FC70Dfe05E755C81e54bd67Bff3F729344B9b'], // vana
82
+ [1625, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // gravity
83
+ [1672, '0xFf70F4A1d11995621854F3692acF286d8aCd04b2'], // pharos
84
+ [1776, '0xeCeC3970Ca674278DA8D9B1c484ACaF6B20181F5'], // injective
85
+ [1868, '0x864b314D4C5a0399368609581d3E8933a63b9232'], // soneium
86
+ [2020, '0x452Cf1B8597E6319Cd21abd847312bF17E26d8d1'], // ronin
87
+ [2741, '0x4f8C9056bb8A3616693a76922FA35d53C056E5b3'], // abstract
88
+ [2818, '0xF7aB42D00D2399f8A4Fba2F15466Be40709fe307'], // morph
89
+ [4217, '0x2cAcAE8e22418E65dcf7651c67aEbe6288EB8243'], // tempo
90
+ [4326, '0x026F252016A7C47CDEf1F05a3Fc9E20C92a49C37'], // megaeth
91
+ [4663, '0xB477751B76CF82d00a686A1232f5fCD772414Af3'], // robinhood
92
+ [5000, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // mantle
93
+ [5031, '0x0bc413CAe89C5b3Ba3AA87FB98dCA23425E1828f'], // somnia
94
+ [5042, '0xA4072583658Fae592A3506A42431cb6316a8d40b'], // arc
95
+ [5734951, '0x026F252016A7C47CDEf1F05a3Fc9E20C92a49C37'], // jovay
96
+ [6900, '0x926c22Dc753E543Cf9CaB75CeEB46ecf865d006a'], // nibiru
97
+ [8217, '0x1255d17c1BC2f764d087536410879F2d0D8772fD'], // kaia
98
+ [8453, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // base
99
+ [9745, '0x026F252016A7C47CDEf1F05a3Fc9E20C92a49C37'], // plasma
100
+ [13371, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // immutablezkevm
101
+ [16661, '0x213A83c67E1Fc0334eF684571ABCB820708A6536'], // 0g
102
+ [33139, '0x2dea447e7dc6cd2f10b31bF10dCB30F87E838417'], // apechain
103
+ [34443, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // mode
104
+ [42161, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // arbitrum
105
+ [42170, '0xFf70F4A1d11995621854F3692acF286d8aCd04b2'], // arbitrumnova
106
+ [42220, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // celo
107
+ [42793, '0x977474593c982cFa8b197cAE302e6d01f789435b'], // etherlink
108
+ [43111, '0x026F252016A7C47CDEf1F05a3Fc9E20C92a49C37'], // hemi
109
+ [43114, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // avalanche
110
+ [57073, '0x864b314D4C5a0399368609581d3E8933a63b9232'], // ink
111
+ [59144, '0xDE1E598b81620773454588B85D6b5D4eEC32573e'], // linea
112
+ [60808, '0x452Cf1B8597E6319Cd21abd847312bF17E26d8d1'], // bob
113
+ [80094, '0xf909c4Ae16622898b885B89d7F839E0244851c66'], // berachain
114
+ [81457, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // blast
115
+ [98866, '0x6f5C8Bb0C5Fe4ECeAC40EE1C238EaB6bbb29761c'], // plume
116
+ [534352, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // scroll
117
+ [747474, '0xC59fe32C9549e3E8B5dCcdAbC45BD287Bd5bA2bc'], // katana
118
+ [999, '0x0a0758d937d1059c356D4714e57F5df0239bce1A'], // hyperevm
119
+ ]);
120
+ const bridgeDataAbiType = {
121
+ type: 'tuple',
122
+ components: [
123
+ { name: 'transactionId', type: 'bytes32' },
124
+ { name: 'bridge', type: 'string' },
125
+ { name: 'integrator', type: 'string' },
126
+ { name: 'referrer', type: 'address' },
127
+ { name: 'sendingAssetId', type: 'address' },
128
+ { name: 'receiver', type: 'address' },
129
+ { name: 'minAmount', type: 'uint256' },
130
+ { name: 'destinationChainId', type: 'uint256' },
131
+ { name: 'hasSourceSwaps', type: 'bool' },
132
+ { name: 'hasDestinationCall', type: 'bool' },
133
+ ],
134
+ };
135
+ const swapDataArrayAbiType = {
136
+ type: 'tuple[]',
137
+ components: [
138
+ { name: 'callTo', type: 'address' },
139
+ { name: 'approveTo', type: 'address' },
140
+ { name: 'sendingAssetId', type: 'address' },
141
+ { name: 'receivingAssetId', type: 'address' },
142
+ { name: 'fromAmount', type: 'uint256' },
143
+ { name: 'callData', type: 'bytes' },
144
+ { name: 'requiresDeposit', type: 'bool' },
145
+ ],
146
+ };
147
+ /** Decodes `BridgeData` and the leading `SwapData` entry, shared by every verified decoder. */
148
+ const decodeBridgeDataAndLeadingSwap = (encodedTail) => {
149
+ const [bridgeData, swapData] = decodeAbiParameters([bridgeDataAbiType, swapDataArrayAbiType], encodedTail);
150
+ // EVERY VERIFIED SELECTOR IS A `swapAndStartBridgeTokensViaX` FUNCTION,
151
+ // whose `containsSourceSwaps` modifier requires a non-empty `SwapData[]`
152
+ // on a genuine call. An empty array here — whether from `hasSourceSwaps`
153
+ // being forged false or the array itself being emptied — means this
154
+ // module cannot identify what was actually pulled from the depositor, so
155
+ // it refuses rather than reading past the end of the array.
156
+ const leadingSwap = swapData[0];
157
+ if (!bridgeData.hasSourceSwaps || leadingSwap === undefined) {
158
+ return null;
159
+ }
160
+ return { bridgeData, leadingSwap };
161
+ };
162
+ /**
163
+ * `swapAndStartBridgeTokensViaAcrossV4((BridgeData),(SwapData[]),(AcrossV4Data))`.
164
+ *
165
+ * Selector `0x1794958f`, verified against
166
+ * `packages/bridge-sdk/src/__fixtures__/lifi/contract-quote-success-eth-to-base-usdc.json`
167
+ * (a real `GET /v1/quote` response) — signature and struct layout read from
168
+ * `lifinance/contracts/src/Facets/AcrossFacetV4.sol`. `AcrossV4Data` carries
169
+ * no native-fee field, so {@link DecodedLifiFacetCall.extraNativeFee} is
170
+ * always `0n`. `outputAmount` is Across's own destination-side guarantee —
171
+ * confirmed equal to the SAME response's `estimate.toAmountMin`.
172
+ */
173
+ const decodeAcrossV4SwapAndStart = (data) => {
174
+ const acrossV4DataAbiType = {
175
+ type: 'tuple',
176
+ components: [
177
+ { name: 'receiverAddress', type: 'bytes32' },
178
+ { name: 'refundAddress', type: 'bytes32' },
179
+ { name: 'sendingAssetId', type: 'bytes32' },
180
+ { name: 'receivingAssetId', type: 'bytes32' },
181
+ { name: 'outputAmount', type: 'uint256' },
182
+ { name: 'outputAmountMultiplier', type: 'uint128' },
183
+ { name: 'exclusiveRelayer', type: 'bytes32' },
184
+ { name: 'quoteTimestamp', type: 'uint32' },
185
+ { name: 'fillDeadline', type: 'uint32' },
186
+ { name: 'exclusivityParameter', type: 'uint32' },
187
+ { name: 'message', type: 'bytes' },
188
+ ],
189
+ };
190
+ const encodedTail = `0x${data.slice(10)}`;
191
+ const decodedHead = decodeBridgeDataAndLeadingSwap(encodedTail);
192
+ if (decodedHead === null) {
193
+ return null;
194
+ }
195
+ const [, , acrossData] = decodeAbiParameters([bridgeDataAbiType, swapDataArrayAbiType, acrossV4DataAbiType], encodedTail);
196
+ return {
197
+ toolLabel: 'across',
198
+ bridgeData: decodedHead.bridgeData,
199
+ originSendingAssetId: decodedHead.leadingSwap.sendingAssetId,
200
+ originAmount: decodedHead.leadingSwap.fromAmount,
201
+ extraNativeFee: 0n,
202
+ destinationMinimum: acrossData.outputAmount,
203
+ };
204
+ };
205
+ /**
206
+ * `swapAndStartBridgeTokensViaLayerSwap((BridgeData),(SwapData[]),(LayerSwapData))`.
207
+ *
208
+ * Selector `0x4c279d6b`, verified against
209
+ * `packages/bridge-sdk/src/__fixtures__/lifi/quote-plain-base-to-polygon.json`
210
+ * (a real `GET /v1/quote` response) — signature and struct layout read from
211
+ * `lifinance/contracts/src/Facets/LayerSwapFacet.sol`. `LayerSwapData`
212
+ * carries no native-fee field ({@link DecodedLifiFacetCall.extraNativeFee}
213
+ * is always `0n`) and, unlike Across or Stargate, NO destination-amount
214
+ * field at all — LayerSwap's delivered amount is attested off-chain by an
215
+ * EIP-712 signature over `requestId`, not provable from this calldata, so
216
+ * {@link DecodedLifiFacetCall.destinationMinimum} is always `null`.
217
+ */
218
+ const decodeLayerSwapSwapAndStart = (data) => {
219
+ const layerSwapDataAbiType = {
220
+ type: 'tuple',
221
+ components: [
222
+ { name: 'requestId', type: 'bytes32' },
223
+ { name: 'depositoryReceiver', type: 'address' },
224
+ { name: 'refundRecipient', type: 'address' },
225
+ { name: 'nonEVMReceiver', type: 'bytes32' },
226
+ { name: 'signature', type: 'bytes' },
227
+ { name: 'deadline', type: 'uint256' },
228
+ ],
229
+ };
230
+ const encodedTail = `0x${data.slice(10)}`;
231
+ const decodedHead = decodeBridgeDataAndLeadingSwap(encodedTail);
232
+ if (decodedHead === null) {
233
+ return null;
234
+ }
235
+ return {
236
+ toolLabel: 'layerswap',
237
+ bridgeData: decodedHead.bridgeData,
238
+ originSendingAssetId: decodedHead.leadingSwap.sendingAssetId,
239
+ originAmount: decodedHead.leadingSwap.fromAmount,
240
+ extraNativeFee: 0n,
241
+ destinationMinimum: null,
242
+ };
243
+ };
244
+ /**
245
+ * `swapAndStartBridgeTokensViaStargate((BridgeData),(SwapData[]),(StargateData))`.
246
+ *
247
+ * Selector `0xa6010a66`, verified against two live captures of
248
+ * `POST /v1/quote/contractCalls`
249
+ * (`packages/bridge-sdk/src/__fixtures__/lifi/contract-calls-native-base-to-eth-mediator.json`
250
+ * and its `-erc20-` sibling) — signature and struct layout read from
251
+ * `lifinance/contracts/src/Facets/StargateFacetV2.sol` and
252
+ * `src/Interfaces/IStargate.sol`. `StargateData.fee.nativeFee` is a
253
+ * LayerZero messaging fee that rides in `msg.value` ON TOP of any deposited
254
+ * native amount — confirmed by both fixtures: `transactionRequest.value`
255
+ * equalled `originAmount + fee.nativeFee` exactly (origin ETH) and
256
+ * `fee.nativeFee` alone when the origin amount was an ERC20 (USD Coin, no
257
+ * native component). `sendParams.minAmountLD` is Stargate's own
258
+ * destination-side guarantee.
259
+ */
260
+ const decodeStargateSwapAndStart = (data) => {
261
+ const stargateDataAbiType = {
262
+ type: 'tuple',
263
+ components: [
264
+ { name: 'assetId', type: 'uint16' },
265
+ {
266
+ name: 'sendParams',
267
+ type: 'tuple',
268
+ components: [
269
+ { name: 'dstEid', type: 'uint32' },
270
+ { name: 'to', type: 'bytes32' },
271
+ { name: 'amountLD', type: 'uint256' },
272
+ { name: 'minAmountLD', type: 'uint256' },
273
+ { name: 'extraOptions', type: 'bytes' },
274
+ { name: 'composeMsg', type: 'bytes' },
275
+ { name: 'oftCmd', type: 'bytes' },
276
+ ],
277
+ },
278
+ {
279
+ name: 'fee',
280
+ type: 'tuple',
281
+ components: [
282
+ { name: 'nativeFee', type: 'uint256' },
283
+ { name: 'lzTokenFee', type: 'uint256' },
284
+ ],
285
+ },
286
+ { name: 'refundAddress', type: 'address' },
287
+ ],
288
+ };
289
+ const encodedTail = `0x${data.slice(10)}`;
290
+ const decodedHead = decodeBridgeDataAndLeadingSwap(encodedTail);
291
+ if (decodedHead === null) {
292
+ return null;
293
+ }
294
+ const [, , stargateData] = decodeAbiParameters([bridgeDataAbiType, swapDataArrayAbiType, stargateDataAbiType], encodedTail);
295
+ return {
296
+ toolLabel: 'stargateV2',
297
+ bridgeData: decodedHead.bridgeData,
298
+ originSendingAssetId: decodedHead.leadingSwap.sendingAssetId,
299
+ originAmount: decodedHead.leadingSwap.fromAmount,
300
+ extraNativeFee: stargateData.fee.nativeFee,
301
+ destinationMinimum: stargateData.sendParams.minAmountLD,
302
+ };
303
+ };
304
+ /**
305
+ * Every LI.FI facet function this module can decode, keyed by its four-byte
306
+ * selector. AN UNKNOWN SELECTOR IS NEVER DECODED — {@link verifyLifiEvmExecution}
307
+ * refuses it outright. Extending this table means capturing a real fixture
308
+ * for the new tool first; see this module's top-of-file doc comment.
309
+ */
310
+ const lifiFacetDecoders = {
311
+ '0x1794958f': decodeAcrossV4SwapAndStart,
312
+ '0x4c279d6b': decodeLayerSwapSwapAndStart,
313
+ '0xa6010a66': decodeStargateSwapAndStart,
314
+ };
315
+ const sameAddress = (a, b) => a.toLowerCase() === b.toLowerCase();
316
+ /**
317
+ * Verifies a LI.FI EVM execution transaction against what this application
318
+ * asked for, throwing {@link LifiExecutionRefusedError} on any disagreement.
319
+ * Returns normally — carrying nothing, since the caller already holds the
320
+ * transaction it validated — when every check passes.
321
+ *
322
+ * See this module's top-of-file doc comment for what is checked and why.
323
+ *
324
+ * @param transactionRequest - the parsed `to`/`data`/`value`/`chainId` this
325
+ * application is about to hand to a wallet
326
+ * @param approvalAddress - `estimate.approvalAddress` from the same
327
+ * response, or `null` when the route needs no token approval
328
+ * @param expectation - what this application actually asked for
329
+ */
330
+ export const verifyLifiEvmExecution = (transactionRequest, approvalAddress, expectation) => {
331
+ const router = lifiDiamondAddressByChainId.get(transactionRequest.chainId);
332
+ if (router === undefined) {
333
+ refuse('unknown-chain', `no known LI.FI Diamond address for chain ${transactionRequest.chainId}`);
334
+ return;
335
+ }
336
+ if (!sameAddress(transactionRequest.to, router)) {
337
+ refuse('router-mismatch', `transactionRequest.to (${transactionRequest.to}) is not LI.FI's Diamond on chain ${transactionRequest.chainId} (${router})`);
338
+ return;
339
+ }
340
+ if (approvalAddress !== null && !sameAddress(approvalAddress, router)) {
341
+ refuse('approval-address-mismatch', `estimate.approvalAddress (${approvalAddress}) is not LI.FI's Diamond on chain ${transactionRequest.chainId} (${router})`);
342
+ return;
343
+ }
344
+ if (transactionRequest.data.length < 10) {
345
+ refuse('calldata-undecodable', 'transactionRequest.data is shorter than a function selector');
346
+ return;
347
+ }
348
+ const selector = transactionRequest.data.slice(0, 10).toLowerCase();
349
+ const decoder = lifiFacetDecoders[selector];
350
+ if (decoder === undefined) {
351
+ refuse('unknown-selector', `transactionRequest.data's selector (${selector}) is not a verified LI.FI facet function`);
352
+ return;
353
+ }
354
+ let decoded;
355
+ try {
356
+ decoded = decoder(transactionRequest.data);
357
+ }
358
+ catch (error) {
359
+ refuse('calldata-undecodable', `transactionRequest.data did not decode as ${selector}'s known parameters: ${error instanceof Error ? error.message : String(error)}`);
360
+ return;
361
+ }
362
+ if (decoded === null) {
363
+ refuse('calldata-undecodable', `${selector} decoded with no usable source-swap entry`);
364
+ return;
365
+ }
366
+ if (!sameAddress(decoded.bridgeData.receiver, expectation.receiver)) {
367
+ refuse('receiver-mismatch', `decoded BridgeData.receiver (${decoded.bridgeData.receiver}) does not match the expected receiver (${expectation.receiver})`);
368
+ return;
369
+ }
370
+ if (decoded.bridgeData.destinationChainId !== BigInt(expectation.destinationChainId)) {
371
+ refuse('destination-chain-mismatch', `decoded BridgeData.destinationChainId (${decoded.bridgeData.destinationChainId}) does not match the requested destination chain (${expectation.destinationChainId})`);
372
+ return;
373
+ }
374
+ if (!sameAddress(decoded.originSendingAssetId, expectation.sendingAssetId)) {
375
+ refuse('sending-asset-mismatch', `decoded origin sending asset (${decoded.originSendingAssetId}) does not match the requested origin token (${expectation.sendingAssetId})`);
376
+ return;
377
+ }
378
+ if (decoded.bridgeData.hasDestinationCall !== expectation.hasDestinationCall) {
379
+ refuse('destination-call-flag-mismatch', `decoded BridgeData.hasDestinationCall (${decoded.bridgeData.hasDestinationCall}) does not match whether this request attached a destination call (${expectation.hasDestinationCall})`);
380
+ return;
381
+ }
382
+ if (expectation.minimumAmount !== null) {
383
+ if (decoded.destinationMinimum === null) {
384
+ refuse('minimum-amount-below-requested', `${decoded.toolLabel} carries no on-chain destination-amount field to verify against the requested minimum (${expectation.minimumAmount})`);
385
+ return;
386
+ }
387
+ if (decoded.destinationMinimum < expectation.minimumAmount) {
388
+ refuse('minimum-amount-below-requested', `decoded destination minimum (${decoded.destinationMinimum}) is below the requested minimum (${expectation.minimumAmount})`);
389
+ return;
390
+ }
391
+ }
392
+ const nativeOriginComponent = sameAddress(decoded.originSendingAssetId, zeroAddress)
393
+ ? decoded.originAmount
394
+ : 0n;
395
+ const expectedValue = nativeOriginComponent + decoded.extraNativeFee;
396
+ if (transactionRequest.value !== expectedValue) {
397
+ refuse('native-value-mismatch', `transactionRequest.value (${transactionRequest.value}) does not equal the native amount the decoded calldata implies (${expectedValue})`);
398
+ return;
399
+ }
400
+ };
@@ -0,0 +1,85 @@
1
+ /**
2
+ * One way a payload departs from the shape the parsers were written against.
3
+ *
4
+ * `found` is a description rather than the value itself, so a finding can be
5
+ * printed in a continuous-integration log without carrying an address, an
6
+ * amount or anything else from a real response into it.
7
+ */
8
+ export type NearIntentsShapeFinding = {
9
+ /** Where in the payload, as a dotted path from the root. */
10
+ readonly path: string;
11
+ /** What the parsers require there. */
12
+ readonly expected: string;
13
+ /** What was there instead, described rather than quoted. */
14
+ readonly found: string;
15
+ };
16
+ /**
17
+ * Checks a `POST /v0/quote` answer against everything the quote parsers read.
18
+ *
19
+ * THE DOLLAR FIGURES ARE CHECKED THOUGH THE PARSER TOLERATES THEIR ABSENCE.
20
+ * `parseNearIntentsQuote` turns a missing `amountInUsd` into null and carries
21
+ * on, which is right for a quote and wrong for a conformance check: losing the
22
+ * dollar figure leaves the reader a route with no price beside it, and nothing
23
+ * else in the system would report that the service stopped sending one.
24
+ *
25
+ * @param payload - the raw JSON body the service answered with
26
+ * @param options.expectDepositAddress - true for an executable (`dry: false`)
27
+ * answer, false for a dry one. A dry answer omits the deposit address by
28
+ * design, so requiring it there would report a finding on a healthy service
29
+ * @returns every departure from the expected shape; empty means conformant
30
+ */
31
+ export declare const nearIntentsQuoteShapeFindings: (payload: unknown, { expectDepositAddress }: {
32
+ expectDepositAddress: boolean;
33
+ }) => readonly NearIntentsShapeFinding[];
34
+ /**
35
+ * Checks a `GET /v0/tokens` answer, including the chain names we map by hand.
36
+ *
37
+ * AN UNMAPPED CHAIN NAME IS THE DRIFT MOST LIKELY TO HAPPEN AND LEAST LIKELY
38
+ * TO BE NOTICED. `chainIdByNearIntentsChain` is a hand-written table, and
39
+ * `findNearIntentsAsset` matches through it — so if the service renames a
40
+ * chain or adds one, every asset on that chain becomes invisible to this
41
+ * repository while the asset list parses perfectly and every test stays green.
42
+ * The names are reported rather than the assets, because a new chain we do not
43
+ * carry is ordinary and a renamed chain we DO carry is the emergency.
44
+ *
45
+ * @param payload - the raw JSON body from the asset endpoint
46
+ * @param options.requiredChains - the service's own names for the chains this
47
+ * repository must keep reaching. A name missing from the answer means the
48
+ * service renamed or dropped it
49
+ * @returns every departure from the expected shape; empty means conformant
50
+ */
51
+ export declare const nearIntentsTokenListShapeFindings: (payload: unknown, { requiredChains }: {
52
+ requiredChains: readonly string[];
53
+ }) => readonly NearIntentsShapeFinding[];
54
+ /**
55
+ * The service's own names for every chain this repository maps to a chain
56
+ * identifier, which is exactly the set whose renaming would be invisible.
57
+ */
58
+ export declare const nearIntentsMappedChainNames: () => readonly string[];
59
+ /**
60
+ * Checks a `GET /v0/status` answer against what the status parser reads.
61
+ *
62
+ * THE DELIVERY HASH IS NOT REQUIRED, and that is not laxity. A route awaiting
63
+ * its deposit has genuinely not produced one, and the captured
64
+ * `status-pending-deposit.json` is exactly that case — so requiring it would
65
+ * report a finding on a perfectly healthy in-flight transfer. What is required
66
+ * is that the ARRAY exists, because its absence is the difference between "no
67
+ * hash yet" and "the service stopped telling us where the funds landed".
68
+ *
69
+ * @param payload - the raw JSON body from the status endpoint
70
+ * @returns every departure from the expected shape; empty means conformant
71
+ */
72
+ export declare const nearIntentsStatusShapeFindings: (payload: unknown) => readonly NearIntentsShapeFinding[];
73
+ /**
74
+ * Checks a refusal body carries the message the refusal parser reads.
75
+ *
76
+ * BOTH SIZE FLOORS ARRIVE ONLY AS PROSE IN THIS FIELD. There is no code, no
77
+ * limits endpoint and no structured figure — `parseNearIntentsQuoteRefusal`
78
+ * runs a regular expression over the sentence. So if the wording changes, a
79
+ * refusal that should withdraw the route from the competition instead reads as
80
+ * `other`, and the reader waits for a bid that will never come.
81
+ *
82
+ * @param payload - the raw JSON body of a refused quote
83
+ * @returns every departure from the expected shape; empty means conformant
84
+ */
85
+ export declare const nearIntentsRefusalShapeFindings: (payload: unknown) => readonly NearIntentsShapeFinding[];