@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.
- package/LICENSE +15 -0
- package/README.md +234 -432
- package/dist/abis.d.ts +1 -0
- package/dist/bridge-limits.d.ts +103 -0
- package/dist/bridge-limits.js +84 -0
- package/dist/chain-info.d.ts +169 -0
- package/dist/chain-info.js +410 -0
- package/dist/chain-names.d.ts +160 -0
- package/dist/chain-names.js +200 -0
- package/dist/chains.d.ts +65 -0
- package/dist/chains.js +284 -0
- package/dist/config.d.ts +216 -0
- package/dist/config.js +493 -0
- package/dist/consolidated-quote.d.ts +554 -0
- package/dist/consolidated-quote.js +773 -0
- package/dist/destination-calldata.d.ts +118 -0
- package/dist/destination-calldata.js +152 -0
- package/dist/ecosystems.d.ts +217 -0
- package/dist/ecosystems.js +288 -0
- package/dist/fee-type.d.ts +10 -0
- package/dist/fee-type.js +9 -0
- package/dist/hyperliquid-execution.d.ts +152 -0
- package/dist/hyperliquid-execution.js +168 -0
- package/dist/image-links.d.ts +117 -0
- package/dist/image-links.js +142 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +20 -0
- package/dist/lifi-execution-guard.d.ts +199 -0
- package/dist/lifi-execution-guard.js +400 -0
- package/dist/near-intents-conformance.d.ts +85 -0
- package/dist/near-intents-conformance.js +329 -0
- package/dist/near-intents-route.d.ts +47 -0
- package/dist/near-intents-route.js +265 -0
- package/dist/near-intents.d.ts +524 -0
- package/dist/near-intents.js +1055 -0
- package/dist/origin-deposit.d.ts +326 -0
- package/dist/origin-deposit.js +68 -0
- package/dist/pre-quote.d.ts +198 -0
- package/dist/pre-quote.js +175 -0
- package/dist/prices.d.ts +107 -0
- package/dist/prices.js +162 -0
- package/dist/providers.d.ts +221 -0
- package/dist/providers.js +157 -0
- package/dist/quote-fees.d.ts +102 -0
- package/dist/quote-fees.js +195 -0
- package/dist/referral.d.ts +77 -0
- package/dist/referral.js +72 -0
- package/dist/relay-currencies.d.ts +47 -0
- package/dist/relay-currencies.js +89 -0
- package/dist/relay-quote.d.ts +301 -0
- package/dist/relay-quote.js +983 -0
- package/dist/route-steps.d.ts +138 -0
- package/dist/route-steps.js +305 -0
- package/dist/routing/amounts.d.ts +80 -0
- package/dist/routing/amounts.js +120 -0
- package/dist/routing/candidate-route.d.ts +43 -0
- package/dist/routing/candidate-route.js +45 -0
- package/dist/routing/candidate.d.ts +237 -0
- package/dist/routing/candidate.js +96 -0
- package/dist/routing/carriers/ask-answer.d.ts +60 -0
- package/dist/routing/carriers/ask-answer.js +1 -0
- package/dist/routing/carriers/omnibridge.d.ts +137 -0
- package/dist/routing/carriers/omnibridge.js +419 -0
- package/dist/routing/carriers/pulsex.d.ts +149 -0
- package/dist/routing/carriers/pulsex.js +453 -0
- package/dist/routing/carriers/wrap.d.ts +25 -0
- package/dist/routing/carriers/wrap.js +115 -0
- package/dist/routing/composed-route-quality.d.ts +103 -0
- package/dist/routing/composed-route-quality.js +108 -0
- package/dist/routing/entry-chain-anchor.d.ts +88 -0
- package/dist/routing/entry-chain-anchor.js +55 -0
- package/dist/routing/format.d.ts +31 -0
- package/dist/routing/format.js +58 -0
- package/dist/routing/index.d.ts +34 -0
- package/dist/routing/index.js +42 -0
- package/dist/routing/producers.d.ts +770 -0
- package/dist/routing/producers.js +1564 -0
- package/dist/routing/providers.d.ts +26 -0
- package/dist/routing/providers.js +38 -0
- package/dist/routing/route-data.d.ts +355 -0
- package/dist/routing/route-data.js +1 -0
- package/dist/routing/search/compose.d.ts +168 -0
- package/dist/routing/search/compose.js +98 -0
- package/dist/routing/search/graph.d.ts +653 -0
- package/dist/routing/search/graph.js +445 -0
- package/dist/routing/search/plan-route-data.d.ts +244 -0
- package/dist/routing/search/plan-route-data.js +767 -0
- package/dist/routing/search/score.d.ts +353 -0
- package/dist/routing/search/score.js +427 -0
- package/dist/routing/usd-value.d.ts +17 -0
- package/dist/routing/usd-value.js +25 -0
- package/dist/settings.d.ts +692 -0
- package/dist/settings.js +909 -0
- package/dist/ton-execution.d.ts +117 -0
- package/dist/ton-execution.js +247 -0
- package/dist/tron-execution.d.ts +146 -0
- package/dist/tron-execution.js +239 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.js +1 -0
- package/llms.txt +169 -0
- package/package.json +204 -23
- package/src/chain-info.test.ts +0 -968
- package/src/chain-info.ts +0 -498
- package/src/chains.test.ts +0 -123
- package/src/chains.ts +0 -88
- package/src/config.test.ts +0 -679
- package/src/config.ts +0 -500
- package/src/fee-type.test.ts +0 -45
- package/src/fee-type.ts +0 -14
- package/src/image-links.test.ts +0 -160
- package/src/image-links.ts +0 -65
- package/src/index.ts +0 -8
- package/src/settings.test.ts +0 -2754
- package/src/settings.ts +0 -875
- package/src/types.ts +0 -48
- package/tsconfig.json +0 -17
- package/vitest.config.ts +0 -13
- /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[];
|