@gibs/bridge-sdk 1.13.0 → 1.17.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 +244 -18
- package/abis/package.json +6 -0
- package/bridge-limits/package.json +6 -0
- package/chain-info/package.json +6 -0
- package/chain-names/package.json +6 -0
- package/chains/package.json +6 -0
- package/claims/package.json +6 -0
- package/config/package.json +6 -0
- package/consolidated-quote/package.json +6 -0
- package/destination-calldata/package.json +6 -0
- package/dist/chain-info.d.ts +1 -1
- package/dist/chain-info.js +11 -12
- package/dist/chains.d.ts +25 -0
- package/dist/chains.js +55 -20
- package/dist/cjs/abis.d.ts +1 -0
- package/dist/cjs/abis.js +19 -0
- package/dist/cjs/bridge-limits.d.ts +103 -0
- package/dist/cjs/bridge-limits.js +89 -0
- package/dist/cjs/chain-info.d.ts +169 -0
- package/dist/cjs/chain-info.js +452 -0
- package/dist/cjs/chain-names.d.ts +160 -0
- package/dist/cjs/chain-names.js +207 -0
- package/dist/cjs/chains.d.ts +90 -0
- package/dist/cjs/chains.js +357 -0
- package/dist/cjs/claims.d.ts +198 -0
- package/dist/cjs/claims.js +182 -0
- package/dist/cjs/config.d.ts +240 -0
- package/dist/cjs/config.js +537 -0
- package/dist/cjs/consolidated-quote.d.ts +648 -0
- package/dist/cjs/consolidated-quote.js +1048 -0
- package/dist/cjs/destination-calldata.d.ts +118 -0
- package/dist/cjs/destination-calldata.js +156 -0
- package/dist/cjs/ecosystems.d.ts +217 -0
- package/dist/cjs/ecosystems.js +298 -0
- package/dist/cjs/execution.d.ts +346 -0
- package/dist/cjs/execution.js +468 -0
- package/dist/cjs/fee-type.d.ts +10 -0
- package/dist/cjs/fee-type.js +10 -0
- package/dist/cjs/hyperliquid-execution.d.ts +152 -0
- package/dist/cjs/hyperliquid-execution.js +175 -0
- package/dist/cjs/image-links.d.ts +117 -0
- package/dist/cjs/image-links.js +153 -0
- package/dist/cjs/index.d.ts +23 -0
- package/dist/cjs/index.js +39 -0
- package/dist/cjs/lifi-execution-guard.d.ts +210 -0
- package/dist/cjs/lifi-execution-guard.js +409 -0
- package/dist/cjs/near-intents-conformance.d.ts +85 -0
- package/dist/cjs/near-intents-conformance.js +337 -0
- package/dist/cjs/near-intents-route.d.ts +47 -0
- package/dist/cjs/near-intents-route.js +303 -0
- package/dist/cjs/near-intents.d.ts +524 -0
- package/dist/cjs/near-intents.js +1071 -0
- package/dist/cjs/origin-deposit.d.ts +326 -0
- package/dist/cjs/origin-deposit.js +82 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/pre-quote.d.ts +198 -0
- package/dist/cjs/pre-quote.js +180 -0
- package/dist/cjs/prices.d.ts +107 -0
- package/dist/cjs/prices.js +172 -0
- package/dist/cjs/providers.d.ts +238 -0
- package/dist/cjs/providers.js +171 -0
- package/dist/cjs/quote-fees.d.ts +102 -0
- package/dist/cjs/quote-fees.js +201 -0
- package/dist/cjs/referral.d.ts +85 -0
- package/dist/cjs/referral.js +85 -0
- package/dist/cjs/relay-currencies.d.ts +47 -0
- package/dist/cjs/relay-currencies.js +95 -0
- package/dist/cjs/relay-depository.d.ts +2 -0
- package/dist/cjs/relay-depository.js +85 -0
- package/dist/cjs/relay-quote.d.ts +301 -0
- package/dist/cjs/relay-quote.js +996 -0
- package/dist/cjs/route-steps.d.ts +138 -0
- package/dist/cjs/route-steps.js +310 -0
- package/dist/cjs/routing/amounts.d.ts +80 -0
- package/dist/cjs/routing/amounts.js +126 -0
- package/dist/cjs/routing/candidate-route.d.ts +43 -0
- package/dist/cjs/routing/candidate-route.js +50 -0
- package/dist/cjs/routing/candidate.d.ts +237 -0
- package/dist/cjs/routing/candidate.js +100 -0
- package/dist/cjs/routing/carriers/ask-answer.d.ts +77 -0
- package/dist/cjs/routing/carriers/ask-answer.js +2 -0
- package/dist/cjs/routing/carriers/omnibridge.d.ts +137 -0
- package/dist/cjs/routing/carriers/omnibridge.js +459 -0
- package/dist/cjs/routing/carriers/pulsex-paths.d.ts +80 -0
- package/dist/cjs/routing/carriers/pulsex-paths.js +97 -0
- package/dist/cjs/routing/carriers/pulsex-split.d.ts +175 -0
- package/dist/cjs/routing/carriers/pulsex-split.js +193 -0
- package/dist/cjs/routing/carriers/pulsex.d.ts +159 -0
- package/dist/cjs/routing/carriers/pulsex.js +565 -0
- package/dist/cjs/routing/carriers/wrap.d.ts +25 -0
- package/dist/cjs/routing/carriers/wrap.js +119 -0
- package/dist/cjs/routing/composed-route-quality.d.ts +125 -0
- package/dist/cjs/routing/composed-route-quality.js +144 -0
- package/dist/cjs/routing/entry-chain-anchor.d.ts +88 -0
- package/dist/cjs/routing/entry-chain-anchor.js +59 -0
- package/dist/cjs/routing/format.d.ts +31 -0
- package/dist/cjs/routing/format.js +64 -0
- package/dist/cjs/routing/index.d.ts +38 -0
- package/dist/cjs/routing/index.js +134 -0
- package/dist/cjs/routing/producers.d.ts +770 -0
- package/dist/cjs/routing/producers.js +1617 -0
- package/dist/cjs/routing/providers.d.ts +31 -0
- package/dist/cjs/routing/providers.js +48 -0
- package/dist/cjs/routing/route-data.d.ts +355 -0
- package/dist/cjs/routing/route-data.js +2 -0
- package/dist/cjs/routing/search/compose.d.ts +168 -0
- package/dist/cjs/routing/search/compose.js +104 -0
- package/dist/cjs/routing/search/graph.d.ts +668 -0
- package/dist/cjs/routing/search/graph.js +461 -0
- package/dist/cjs/routing/search/plan-route-data.d.ts +269 -0
- package/dist/cjs/routing/search/plan-route-data.js +848 -0
- package/dist/cjs/routing/search/score.d.ts +356 -0
- package/dist/cjs/routing/search/score.js +445 -0
- package/dist/cjs/routing/swap-transactions.d.ts +91 -0
- package/dist/cjs/routing/swap-transactions.js +131 -0
- package/dist/cjs/routing/usd-value.d.ts +17 -0
- package/dist/cjs/routing/usd-value.js +29 -0
- package/dist/cjs/settings.d.ts +785 -0
- package/dist/cjs/settings.js +1074 -0
- package/dist/cjs/ton-connect.d.ts +98 -0
- package/dist/cjs/ton-connect.js +206 -0
- package/dist/cjs/ton-execution.d.ts +7 -0
- package/dist/cjs/ton-execution.js +23 -0
- package/dist/cjs/ton.d.ts +37 -0
- package/dist/cjs/ton.js +83 -0
- package/dist/cjs/tron-execution.d.ts +146 -0
- package/dist/cjs/tron-execution.js +248 -0
- package/dist/cjs/types.d.ts +51 -0
- package/dist/cjs/types.js +2 -0
- package/dist/claims.d.ts +198 -0
- package/dist/claims.js +140 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.js +39 -5
- package/dist/consolidated-quote.d.ts +109 -15
- package/dist/consolidated-quote.js +310 -52
- package/dist/execution.d.ts +346 -0
- package/dist/execution.js +428 -0
- package/dist/hyperliquid-execution.js +2 -3
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/lifi-execution-guard.d.ts +13 -2
- package/dist/lifi-execution-guard.js +4 -0
- package/dist/near-intents.js +2 -3
- package/dist/pre-quote.d.ts +6 -6
- package/dist/pre-quote.js +10 -14
- package/dist/providers.d.ts +17 -0
- package/dist/providers.js +6 -0
- package/dist/referral.d.ts +12 -4
- package/dist/referral.js +12 -4
- package/dist/relay-depository.d.ts +2 -0
- package/dist/relay-depository.js +82 -0
- package/dist/relay-quote.js +2 -3
- package/dist/routing/candidate.d.ts +3 -3
- package/dist/routing/candidate.js +3 -3
- package/dist/routing/carriers/ask-answer.d.ts +19 -0
- package/dist/routing/carriers/omnibridge.js +23 -21
- package/dist/routing/carriers/pulsex-paths.d.ts +80 -0
- package/dist/routing/carriers/pulsex-paths.js +92 -0
- package/dist/routing/carriers/pulsex-split.d.ts +175 -0
- package/dist/routing/carriers/pulsex-split.js +185 -0
- package/dist/routing/carriers/pulsex.d.ts +69 -21
- package/dist/routing/carriers/pulsex.js +184 -65
- package/dist/routing/composed-route-quality.d.ts +28 -6
- package/dist/routing/composed-route-quality.js +36 -5
- package/dist/routing/entry-chain-anchor.d.ts +1 -1
- package/dist/routing/entry-chain-anchor.js +1 -1
- package/dist/routing/index.d.ts +7 -3
- package/dist/routing/index.js +7 -3
- package/dist/routing/producers.js +1 -1
- package/dist/routing/providers.d.ts +5 -0
- package/dist/routing/providers.js +5 -0
- package/dist/routing/search/graph.d.ts +29 -0
- package/dist/routing/search/plan-route-data.d.ts +25 -0
- package/dist/routing/search/plan-route-data.js +43 -7
- package/dist/routing/search/score.d.ts +33 -1
- package/dist/routing/search/score.js +41 -6
- package/dist/routing/swap-transactions.d.ts +91 -0
- package/dist/routing/swap-transactions.js +125 -0
- package/dist/settings.d.ts +94 -1
- package/dist/settings.js +95 -3
- package/dist/ton-connect.d.ts +98 -0
- package/dist/ton-connect.js +200 -0
- package/dist/ton-execution.d.ts +4 -114
- package/dist/ton-execution.js +4 -244
- package/dist/ton.d.ts +37 -0
- package/dist/ton.js +65 -0
- package/docs/examples/example-a-ether-to-pulse.ts +300 -0
- package/docs/examples/example-b-token-pulsechain-to-ethereum.ts +270 -0
- package/docs/examples/shared.ts +261 -0
- package/docs/examples/tsconfig.json +18 -0
- package/docs/integration-guide.md +617 -0
- package/ecosystems/package.json +6 -0
- package/execution/package.json +6 -0
- package/fee-type/package.json +6 -0
- package/hyperliquid-execution/package.json +6 -0
- package/image-links/package.json +6 -0
- package/lifi-execution-guard/package.json +6 -0
- package/llms.txt +234 -0
- package/near-intents/package.json +6 -0
- package/near-intents-route/package.json +6 -0
- package/origin-deposit/package.json +6 -0
- package/package.json +818 -10
- package/pre-quote/package.json +6 -0
- package/prices/package.json +6 -0
- package/providers/package.json +6 -0
- package/quote-fees/package.json +6 -0
- package/referral/package.json +6 -0
- package/relay-currencies/package.json +6 -0
- package/relay-quote/package.json +6 -0
- package/route-steps/package.json +6 -0
- package/routing/package.json +6 -0
- package/settings/package.json +6 -0
- package/ton/package.json +6 -0
- package/ton-connect/package.json +6 -0
- package/ton-execution/package.json +6 -0
- package/tron-execution/package.json +6 -0
- package/types/package.json +6 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { type Hex } from 'viem';
|
|
2
|
+
/**
|
|
3
|
+
* Verifies a LI.FI EVM execution transaction before a wallet ever signs it.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. `lifiApi.quote` and `lifiApi.contractCallsQuote` are fetched
|
|
6
|
+
* through a configurable base URL — today `lifi.gibs.finance`, a server-side
|
|
7
|
+
* proxy; the interface may point at a different host in the future. Whichever
|
|
8
|
+
* host answers, this codebase used to sign whatever `transactionRequest` came
|
|
9
|
+
* back after checking only that `to` and `data` were hexadecimal strings (see
|
|
10
|
+
* `lifiEvmDeposit` in `consolidated-quote.ts`). A COMPROMISED PROXY could
|
|
11
|
+
* return ANY `to`/`data`/`value` and a connected wallet would sign it,
|
|
12
|
+
* moving the reader's funds wherever the proxy chose.
|
|
13
|
+
*
|
|
14
|
+
* WHAT THIS MODULE PROVES INSTEAD, before signing:
|
|
15
|
+
*
|
|
16
|
+
* 1. `to` (and, when present, `estimate.approvalAddress`) is LI.FI's own
|
|
17
|
+
* Diamond entryway contract for the chain the transaction runs on — a
|
|
18
|
+
* fixed table, not something the response gets to assert about itself.
|
|
19
|
+
* See {@link lifiDiamondAddressByChainId}.
|
|
20
|
+
* 2. The calldata's selector is one of a small, explicitly verified set of
|
|
21
|
+
* LI.FI facet functions whose ABI this module carries in full. An unknown
|
|
22
|
+
* selector is refused rather than guessed at.
|
|
23
|
+
* 3. The decoded `BridgeData` and leading `SwapData` entry agree with what
|
|
24
|
+
* THIS APPLICATION asked for — not with other fields of the same,
|
|
25
|
+
* potentially compromised, response. See {@link LifiExecutionExpectation}.
|
|
26
|
+
* 4. `transactionRequest.value` equals exactly the native currency the
|
|
27
|
+
* decoded calldata implies (the amount being deposited, when that amount
|
|
28
|
+
* is native, plus any protocol messaging fee the tool's own data
|
|
29
|
+
* declares) — never more, never less.
|
|
30
|
+
*
|
|
31
|
+
* WHERE THE FIELD SEMANTICS CAME FROM. Every fact this module relies on —
|
|
32
|
+
* which struct field means what, and how the three verified facet functions
|
|
33
|
+
* actually populate `BridgeData` and `SwapData` — was read from REAL
|
|
34
|
+
* responses captured live from `https://li.quest`, decoded with this exact
|
|
35
|
+
* machinery, and cross-checked against the deployed contracts' Solidity
|
|
36
|
+
* source (`lifinance/contracts`, commit `025989955bb373739737ee78ddff01620638ae3b`,
|
|
37
|
+
* 2026-09-23). Two assumptions a first reading of LI.FI's own field
|
|
38
|
+
* descriptions would suggest turned out to be wrong, and are recorded here so
|
|
39
|
+
* nobody reintroduces them:
|
|
40
|
+
*
|
|
41
|
+
* - `BridgeData.receiver` is NOT "the final recipient" in general. For the
|
|
42
|
+
* contract-calls endpoint (`hasDestinationCall: true`) it decoded to the
|
|
43
|
+
* REQUEST'S `toFallbackAddress` — the address that receives the bridged
|
|
44
|
+
* asset if the destination call reverts — not the destination call's own
|
|
45
|
+
* contract or the reader's recipient. Only for the plain endpoint
|
|
46
|
+
* (`hasDestinationCall: false`) does it equal the recipient (`toAddress`).
|
|
47
|
+
* Verified against three independently captured quotes; see the fixtures
|
|
48
|
+
* this module's test file reads.
|
|
49
|
+
* - `BridgeData.minAmount` is NOT the destination-side guaranteed amount for
|
|
50
|
+
* any of the three verified tools. It is the amount of `sendingAssetId`
|
|
51
|
+
* pulled from the depositor on the ORIGIN chain (`LibAsset.depositAsset`
|
|
52
|
+
* in every facet's Solidity), reassigned at runtime to the real swap
|
|
53
|
+
* output when `hasSourceSwaps` is true — a fact this module's calldata
|
|
54
|
+
* never carries. The genuine destination-side guarantee lives in each
|
|
55
|
+
* tool's own trailing struct (`AcrossV4Data.outputAmount`,
|
|
56
|
+
* `StargateData.sendParams.minAmountLD`) and is read from there instead.
|
|
57
|
+
* LayerSwap's own struct carries no on-chain amount field at all — its
|
|
58
|
+
* guarantee is an off-chain, backend-signed attestation — so this module
|
|
59
|
+
* cannot verify a destination minimum for it and refuses to try; see
|
|
60
|
+
* {@link DecodedLifiFacetCall.destinationMinimum}.
|
|
61
|
+
*
|
|
62
|
+
* SCOPE, DELIBERATELY NARROW. Only the exact facet-function shapes seen in a
|
|
63
|
+
* REAL captured LI.FI response are decoded; every other selector refuses.
|
|
64
|
+
* Adding a new tool means capturing a fixture for it first — see
|
|
65
|
+
* `packages/bridge-sdk/src/__fixtures__/lifi/README.md` — and extending
|
|
66
|
+
* {@link lifiFacetDecoders} from that fixture, never from documentation
|
|
67
|
+
* alone (per the hand-built-quotes-hide-parser-gaps lesson: a decoder written
|
|
68
|
+
* from a guess passes its own hand-built test and nothing else).
|
|
69
|
+
*/
|
|
70
|
+
/**
|
|
71
|
+
* Every reason this module can refuse a LI.FI execution transaction.
|
|
72
|
+
*
|
|
73
|
+
* Each maps to one property of the response this module distrusts by
|
|
74
|
+
* default: `unknown-chain` and `router-mismatch` distrust `to`;
|
|
75
|
+
* `approval-address-mismatch` distrusts `estimate.approvalAddress`;
|
|
76
|
+
* `unknown-selector` and `calldata-undecodable` distrust `data`'s shape;
|
|
77
|
+
* `receiver-mismatch`, `destination-chain-mismatch`, `sending-asset-mismatch`,
|
|
78
|
+
* `destination-call-flag-mismatch` and `minimum-amount-below-requested`
|
|
79
|
+
* distrust `data`'s DECODED fields against what this application asked for;
|
|
80
|
+
* `origin-amount-mismatch` distrusts the decoded amount pulled from the
|
|
81
|
+
* depositor; `native-value-mismatch` distrusts `value`. `missing-request-context` is
|
|
82
|
+
* different in kind: it means the CALLER never supplied what this
|
|
83
|
+
* application expected, so none of the above could even be checked — see
|
|
84
|
+
* `parseLifiContractCallsQuote` and `parseLifiQuote` in `consolidated-quote.ts`.
|
|
85
|
+
*/
|
|
86
|
+
export type LifiExecutionRefusalReason = 'unknown-chain' | 'router-mismatch' | 'approval-address-mismatch' | 'unknown-selector' | 'calldata-undecodable' | 'receiver-mismatch' | 'destination-chain-mismatch' | 'sending-asset-mismatch' | 'destination-call-flag-mismatch' | 'minimum-amount-below-requested' | 'origin-amount-mismatch' | 'native-value-mismatch' | 'missing-request-context';
|
|
87
|
+
/** Why a LI.FI execution transaction was refused before signing. */
|
|
88
|
+
export type LifiExecutionRefusal = {
|
|
89
|
+
readonly reason: LifiExecutionRefusalReason;
|
|
90
|
+
/** A short, specific explanation naming the values that disagreed. */
|
|
91
|
+
readonly detail: string;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Thrown by {@link verifyLifiEvmExecution} instead of returning a deposit a
|
|
95
|
+
* wallet could sign.
|
|
96
|
+
*
|
|
97
|
+
* NAMED AND TYPED SO A REFUSAL IS NEVER A SILENT DROP. Every caller that
|
|
98
|
+
* catches this can read `refusal.reason` and show, or log, exactly which
|
|
99
|
+
* check failed — the same discipline `@gibs/quotes`'s `ProviderQuoteError`
|
|
100
|
+
* applies to a provider declining a quote, applied here to a provider's
|
|
101
|
+
* ANSWER disagreeing with itself.
|
|
102
|
+
*/
|
|
103
|
+
export declare class LifiExecutionRefusedError extends Error {
|
|
104
|
+
readonly refusal: LifiExecutionRefusal;
|
|
105
|
+
constructor(refusal: LifiExecutionRefusal);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* LI.FI's Diamond entryway contract address, by chain id.
|
|
109
|
+
*
|
|
110
|
+
* SOURCE. `lifinance/contracts` (https://github.com/lifinance/contracts),
|
|
111
|
+
* commit `025989955bb373739737ee78ddff01620638ae3b` (2026-09-23): the
|
|
112
|
+
* `LiFiDiamond` address in `deployments/<network>.json`, matched to its
|
|
113
|
+
* chain id in `config/networks.json`, for every network whose `status` is
|
|
114
|
+
* `"active"` and `"type"` is `"mainnet"`. LI.FI's own documentation
|
|
115
|
+
* (https://docs.li.fi/introduction/lifi-architecture/smart-contract-addresses)
|
|
116
|
+
* states the address is `0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE` "on most
|
|
117
|
+
* supported networks" but differs on some — confirmed here directly against
|
|
118
|
+
* the deployment records rather than assumed: 39 of the 65 active mainnet
|
|
119
|
+
* networks use a DIFFERENT address, almost all of them newer or lower-volume
|
|
120
|
+
* chains (0G, Abstract, ApeChain, Arbitrum Nova, Berachain, Flare, zkSync
|
|
121
|
+
* Era, and more).
|
|
122
|
+
*
|
|
123
|
+
* DELIBERATELY A FIXED, BAKED-IN TABLE, NEVER A LIVE LOOKUP. LI.FI's own
|
|
124
|
+
* `/v1/chains` endpoint carries a `diamondAddress` per chain, and this
|
|
125
|
+
* codebase already fetches it for other purposes — but that endpoint is
|
|
126
|
+
* reached through the SAME configurable, potentially-compromised host this
|
|
127
|
+
* module exists to distrust. Trusting it here would make the whole guard
|
|
128
|
+
* circular: a compromised proxy could simply report its own malicious
|
|
129
|
+
* address as the "correct" one. This table is real code, reviewed and
|
|
130
|
+
* shipped with the application, and can only change through a new release.
|
|
131
|
+
*
|
|
132
|
+
* UNKNOWN CHAIN, UNKNOWN ADDRESS. A chain id absent from this table refuses
|
|
133
|
+
* rather than falling back to the common address — LI.FI adds networks
|
|
134
|
+
* continuously, and assuming a newly-added chain shares the common address
|
|
135
|
+
* would be exactly the kind of guess this module exists to avoid making.
|
|
136
|
+
*/
|
|
137
|
+
export declare const lifiDiamondAddressByChainId: ReadonlyMap<number, Hex>;
|
|
138
|
+
/** LI.FI's `ILiFi.BridgeData` struct — identical across every facet function. */
|
|
139
|
+
export type LifiBridgeData = {
|
|
140
|
+
readonly transactionId: Hex;
|
|
141
|
+
readonly bridge: string;
|
|
142
|
+
readonly integrator: string;
|
|
143
|
+
readonly referrer: Hex;
|
|
144
|
+
readonly sendingAssetId: Hex;
|
|
145
|
+
readonly receiver: Hex;
|
|
146
|
+
readonly minAmount: bigint;
|
|
147
|
+
readonly destinationChainId: bigint;
|
|
148
|
+
readonly hasSourceSwaps: boolean;
|
|
149
|
+
readonly hasDestinationCall: boolean;
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* What THIS APPLICATION asked LI.FI for, independent of anything the
|
|
153
|
+
* response itself claims — the only thing a decoded transaction is allowed
|
|
154
|
+
* to be checked against.
|
|
155
|
+
*/
|
|
156
|
+
export type LifiExecutionExpectation = {
|
|
157
|
+
/**
|
|
158
|
+
* Where the bridged asset lands if the route needs no destination call
|
|
159
|
+
* (the plain endpoint's recipient), or where it lands if the destination
|
|
160
|
+
* call reverts (the contract-calls endpoint's fallback address) — see the
|
|
161
|
+
* `BridgeData.receiver` note at the top of this file for why these two are
|
|
162
|
+
* NOT the same field despite the shared name.
|
|
163
|
+
*/
|
|
164
|
+
readonly receiver: Hex;
|
|
165
|
+
/** The chain the entry call (or the plain delivery) must land on. */
|
|
166
|
+
readonly destinationChainId: number;
|
|
167
|
+
/** The token this application is paying with on the origin chain. */
|
|
168
|
+
readonly sendingAssetId: Hex;
|
|
169
|
+
/** Whether this application attached an omnibridge entry call. */
|
|
170
|
+
readonly hasDestinationCall: boolean;
|
|
171
|
+
/**
|
|
172
|
+
* The guaranteed minimum this application asked LI.FI to deliver, or
|
|
173
|
+
* `null` for the plain (forward-solved) endpoint, which names no such
|
|
174
|
+
* minimum — the delivered amount is read back from the response instead
|
|
175
|
+
* of constrained by the request. See `ConsolidatedQuote.toAmount`.
|
|
176
|
+
*/
|
|
177
|
+
readonly minimumAmount: bigint | null;
|
|
178
|
+
/**
|
|
179
|
+
* The exact amount of the origin token this application sends, in base units,
|
|
180
|
+
* or `null` when the request names none.
|
|
181
|
+
*
|
|
182
|
+
* Compared with the decoded `swapData[0].fromAmount`, the amount pulled from
|
|
183
|
+
* the depositor. Without it a response could keep every other field honest and
|
|
184
|
+
* still pull more than the reader chose. The request, never the response,
|
|
185
|
+
* supplies this figure.
|
|
186
|
+
*/
|
|
187
|
+
readonly originAmount: bigint | null;
|
|
188
|
+
};
|
|
189
|
+
/** The transaction fields {@link verifyLifiEvmExecution} reads off the raw response. */
|
|
190
|
+
export type LifiExecutionTransactionRequest = {
|
|
191
|
+
readonly to: Hex;
|
|
192
|
+
readonly data: Hex;
|
|
193
|
+
readonly value: bigint;
|
|
194
|
+
readonly chainId: number;
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Verifies a LI.FI EVM execution transaction against what this application
|
|
198
|
+
* asked for, throwing {@link LifiExecutionRefusedError} on any disagreement.
|
|
199
|
+
* Returns normally — carrying nothing, since the caller already holds the
|
|
200
|
+
* transaction it validated — when every check passes.
|
|
201
|
+
*
|
|
202
|
+
* See this module's top-of-file doc comment for what is checked and why.
|
|
203
|
+
*
|
|
204
|
+
* @param transactionRequest - the parsed `to`/`data`/`value`/`chainId` this
|
|
205
|
+
* application is about to hand to a wallet
|
|
206
|
+
* @param approvalAddress - `estimate.approvalAddress` from the same
|
|
207
|
+
* response, or `null` when the route needs no token approval
|
|
208
|
+
* @param expectation - what this application actually asked for
|
|
209
|
+
*/
|
|
210
|
+
export declare const verifyLifiEvmExecution: (transactionRequest: LifiExecutionTransactionRequest, approvalAddress: Hex | null, expectation: LifiExecutionExpectation) => void;
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifyLifiEvmExecution = exports.lifiDiamondAddressByChainId = exports.LifiExecutionRefusedError = void 0;
|
|
4
|
+
const viem_1 = require("viem");
|
|
5
|
+
/**
|
|
6
|
+
* Thrown by {@link verifyLifiEvmExecution} instead of returning a deposit a
|
|
7
|
+
* wallet could sign.
|
|
8
|
+
*
|
|
9
|
+
* NAMED AND TYPED SO A REFUSAL IS NEVER A SILENT DROP. Every caller that
|
|
10
|
+
* catches this can read `refusal.reason` and show, or log, exactly which
|
|
11
|
+
* check failed — the same discipline `@gibs/quotes`'s `ProviderQuoteError`
|
|
12
|
+
* applies to a provider declining a quote, applied here to a provider's
|
|
13
|
+
* ANSWER disagreeing with itself.
|
|
14
|
+
*/
|
|
15
|
+
class LifiExecutionRefusedError extends Error {
|
|
16
|
+
refusal;
|
|
17
|
+
constructor(refusal) {
|
|
18
|
+
super(`LI.FI execution transaction refused: ${refusal.detail}`);
|
|
19
|
+
this.refusal = refusal;
|
|
20
|
+
this.name = 'LifiExecutionRefusedError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.LifiExecutionRefusedError = LifiExecutionRefusedError;
|
|
24
|
+
const refuse = (reason, detail) => {
|
|
25
|
+
throw new LifiExecutionRefusedError({ reason, detail });
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* LI.FI's Diamond entryway contract address, by chain id.
|
|
29
|
+
*
|
|
30
|
+
* SOURCE. `lifinance/contracts` (https://github.com/lifinance/contracts),
|
|
31
|
+
* commit `025989955bb373739737ee78ddff01620638ae3b` (2026-09-23): the
|
|
32
|
+
* `LiFiDiamond` address in `deployments/<network>.json`, matched to its
|
|
33
|
+
* chain id in `config/networks.json`, for every network whose `status` is
|
|
34
|
+
* `"active"` and `"type"` is `"mainnet"`. LI.FI's own documentation
|
|
35
|
+
* (https://docs.li.fi/introduction/lifi-architecture/smart-contract-addresses)
|
|
36
|
+
* states the address is `0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE` "on most
|
|
37
|
+
* supported networks" but differs on some — confirmed here directly against
|
|
38
|
+
* the deployment records rather than assumed: 39 of the 65 active mainnet
|
|
39
|
+
* networks use a DIFFERENT address, almost all of them newer or lower-volume
|
|
40
|
+
* chains (0G, Abstract, ApeChain, Arbitrum Nova, Berachain, Flare, zkSync
|
|
41
|
+
* Era, and more).
|
|
42
|
+
*
|
|
43
|
+
* DELIBERATELY A FIXED, BAKED-IN TABLE, NEVER A LIVE LOOKUP. LI.FI's own
|
|
44
|
+
* `/v1/chains` endpoint carries a `diamondAddress` per chain, and this
|
|
45
|
+
* codebase already fetches it for other purposes — but that endpoint is
|
|
46
|
+
* reached through the SAME configurable, potentially-compromised host this
|
|
47
|
+
* module exists to distrust. Trusting it here would make the whole guard
|
|
48
|
+
* circular: a compromised proxy could simply report its own malicious
|
|
49
|
+
* address as the "correct" one. This table is real code, reviewed and
|
|
50
|
+
* shipped with the application, and can only change through a new release.
|
|
51
|
+
*
|
|
52
|
+
* UNKNOWN CHAIN, UNKNOWN ADDRESS. A chain id absent from this table refuses
|
|
53
|
+
* rather than falling back to the common address — LI.FI adds networks
|
|
54
|
+
* continuously, and assuming a newly-added chain shares the common address
|
|
55
|
+
* would be exactly the kind of guess this module exists to avoid making.
|
|
56
|
+
*/
|
|
57
|
+
exports.lifiDiamondAddressByChainId = new Map([
|
|
58
|
+
[1, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // mainnet
|
|
59
|
+
[10, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // optimism
|
|
60
|
+
[14, '0x198FC70Dfe05E755C81e54bd67Bff3F729344B9b'], // flare
|
|
61
|
+
[25, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // cronos
|
|
62
|
+
[30, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // rootstock
|
|
63
|
+
[40, '0xa7cd44521903C65915648385E3FE881E49171582'], // telos
|
|
64
|
+
[50, '0x055d4612Ec74aD799C6cB4dF72C0Ab8dbDBCBAfa'], // xdc
|
|
65
|
+
[56, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // bsc
|
|
66
|
+
[88, '0x198FC70Dfe05E755C81e54bd67Bff3F729344B9b'], // viction
|
|
67
|
+
[100, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // gnosis
|
|
68
|
+
[122, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // fuse
|
|
69
|
+
[130, '0x864b314D4C5a0399368609581d3E8933a63b9232'], // unichain
|
|
70
|
+
[137, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // polygon
|
|
71
|
+
[143, '0x026F252016A7C47CDEf1F05a3Fc9E20C92a49C37'], // monad
|
|
72
|
+
[146, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // sonic
|
|
73
|
+
[196, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // xlayer
|
|
74
|
+
[204, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // opbnb
|
|
75
|
+
[232, '0xF3B20515d9B193531c48E47c18aF16d1e5d28f9a'], // lens
|
|
76
|
+
[252, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // fraxtal
|
|
77
|
+
[288, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // boba
|
|
78
|
+
[324, '0x341e94069f53234fE6DabeF707aD424830525715'], // zksync
|
|
79
|
+
[480, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // worldchain
|
|
80
|
+
[747, '0x026F252016A7C47CDEf1F05a3Fc9E20C92a49C37'], // flow
|
|
81
|
+
[988, '0x026F252016A7C47CDEf1F05a3Fc9E20C92a49C37'], // stable
|
|
82
|
+
[1088, '0x24ca98fB6972F5eE05f0dB00595c7f68D9FaFd68'], // metis
|
|
83
|
+
[1135, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // lisk
|
|
84
|
+
[1329, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // sei
|
|
85
|
+
[1480, '0x198FC70Dfe05E755C81e54bd67Bff3F729344B9b'], // vana
|
|
86
|
+
[1625, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // gravity
|
|
87
|
+
[1672, '0xFf70F4A1d11995621854F3692acF286d8aCd04b2'], // pharos
|
|
88
|
+
[1776, '0xeCeC3970Ca674278DA8D9B1c484ACaF6B20181F5'], // injective
|
|
89
|
+
[1868, '0x864b314D4C5a0399368609581d3E8933a63b9232'], // soneium
|
|
90
|
+
[2020, '0x452Cf1B8597E6319Cd21abd847312bF17E26d8d1'], // ronin
|
|
91
|
+
[2741, '0x4f8C9056bb8A3616693a76922FA35d53C056E5b3'], // abstract
|
|
92
|
+
[2818, '0xF7aB42D00D2399f8A4Fba2F15466Be40709fe307'], // morph
|
|
93
|
+
[4217, '0x2cAcAE8e22418E65dcf7651c67aEbe6288EB8243'], // tempo
|
|
94
|
+
[4326, '0x026F252016A7C47CDEf1F05a3Fc9E20C92a49C37'], // megaeth
|
|
95
|
+
[4663, '0xB477751B76CF82d00a686A1232f5fCD772414Af3'], // robinhood
|
|
96
|
+
[5000, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // mantle
|
|
97
|
+
[5031, '0x0bc413CAe89C5b3Ba3AA87FB98dCA23425E1828f'], // somnia
|
|
98
|
+
[5042, '0xA4072583658Fae592A3506A42431cb6316a8d40b'], // arc
|
|
99
|
+
[5734951, '0x026F252016A7C47CDEf1F05a3Fc9E20C92a49C37'], // jovay
|
|
100
|
+
[6900, '0x926c22Dc753E543Cf9CaB75CeEB46ecf865d006a'], // nibiru
|
|
101
|
+
[8217, '0x1255d17c1BC2f764d087536410879F2d0D8772fD'], // kaia
|
|
102
|
+
[8453, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // base
|
|
103
|
+
[9745, '0x026F252016A7C47CDEf1F05a3Fc9E20C92a49C37'], // plasma
|
|
104
|
+
[13371, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // immutablezkevm
|
|
105
|
+
[16661, '0x213A83c67E1Fc0334eF684571ABCB820708A6536'], // 0g
|
|
106
|
+
[33139, '0x2dea447e7dc6cd2f10b31bF10dCB30F87E838417'], // apechain
|
|
107
|
+
[34443, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // mode
|
|
108
|
+
[42161, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // arbitrum
|
|
109
|
+
[42170, '0xFf70F4A1d11995621854F3692acF286d8aCd04b2'], // arbitrumnova
|
|
110
|
+
[42220, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // celo
|
|
111
|
+
[42793, '0x977474593c982cFa8b197cAE302e6d01f789435b'], // etherlink
|
|
112
|
+
[43111, '0x026F252016A7C47CDEf1F05a3Fc9E20C92a49C37'], // hemi
|
|
113
|
+
[43114, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // avalanche
|
|
114
|
+
[57073, '0x864b314D4C5a0399368609581d3E8933a63b9232'], // ink
|
|
115
|
+
[59144, '0xDE1E598b81620773454588B85D6b5D4eEC32573e'], // linea
|
|
116
|
+
[60808, '0x452Cf1B8597E6319Cd21abd847312bF17E26d8d1'], // bob
|
|
117
|
+
[80094, '0xf909c4Ae16622898b885B89d7F839E0244851c66'], // berachain
|
|
118
|
+
[81457, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // blast
|
|
119
|
+
[98866, '0x6f5C8Bb0C5Fe4ECeAC40EE1C238EaB6bbb29761c'], // plume
|
|
120
|
+
[534352, '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'], // scroll
|
|
121
|
+
[747474, '0xC59fe32C9549e3E8B5dCcdAbC45BD287Bd5bA2bc'], // katana
|
|
122
|
+
[999, '0x0a0758d937d1059c356D4714e57F5df0239bce1A'], // hyperevm
|
|
123
|
+
]);
|
|
124
|
+
const bridgeDataAbiType = {
|
|
125
|
+
type: 'tuple',
|
|
126
|
+
components: [
|
|
127
|
+
{ name: 'transactionId', type: 'bytes32' },
|
|
128
|
+
{ name: 'bridge', type: 'string' },
|
|
129
|
+
{ name: 'integrator', type: 'string' },
|
|
130
|
+
{ name: 'referrer', type: 'address' },
|
|
131
|
+
{ name: 'sendingAssetId', type: 'address' },
|
|
132
|
+
{ name: 'receiver', type: 'address' },
|
|
133
|
+
{ name: 'minAmount', type: 'uint256' },
|
|
134
|
+
{ name: 'destinationChainId', type: 'uint256' },
|
|
135
|
+
{ name: 'hasSourceSwaps', type: 'bool' },
|
|
136
|
+
{ name: 'hasDestinationCall', type: 'bool' },
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
const swapDataArrayAbiType = {
|
|
140
|
+
type: 'tuple[]',
|
|
141
|
+
components: [
|
|
142
|
+
{ name: 'callTo', type: 'address' },
|
|
143
|
+
{ name: 'approveTo', type: 'address' },
|
|
144
|
+
{ name: 'sendingAssetId', type: 'address' },
|
|
145
|
+
{ name: 'receivingAssetId', type: 'address' },
|
|
146
|
+
{ name: 'fromAmount', type: 'uint256' },
|
|
147
|
+
{ name: 'callData', type: 'bytes' },
|
|
148
|
+
{ name: 'requiresDeposit', type: 'bool' },
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
/** Decodes `BridgeData` and the leading `SwapData` entry, shared by every verified decoder. */
|
|
152
|
+
const decodeBridgeDataAndLeadingSwap = (encodedTail) => {
|
|
153
|
+
const [bridgeData, swapData] = (0, viem_1.decodeAbiParameters)([bridgeDataAbiType, swapDataArrayAbiType], encodedTail);
|
|
154
|
+
// EVERY VERIFIED SELECTOR IS A `swapAndStartBridgeTokensViaX` FUNCTION,
|
|
155
|
+
// whose `containsSourceSwaps` modifier requires a non-empty `SwapData[]`
|
|
156
|
+
// on a genuine call. An empty array here — whether from `hasSourceSwaps`
|
|
157
|
+
// being forged false or the array itself being emptied — means this
|
|
158
|
+
// module cannot identify what was actually pulled from the depositor, so
|
|
159
|
+
// it refuses rather than reading past the end of the array.
|
|
160
|
+
const leadingSwap = swapData[0];
|
|
161
|
+
if (!bridgeData.hasSourceSwaps || leadingSwap === undefined) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return { bridgeData, leadingSwap };
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* `swapAndStartBridgeTokensViaAcrossV4((BridgeData),(SwapData[]),(AcrossV4Data))`.
|
|
168
|
+
*
|
|
169
|
+
* Selector `0x1794958f`, verified against
|
|
170
|
+
* `packages/bridge-sdk/src/__fixtures__/lifi/contract-quote-success-eth-to-base-usdc.json`
|
|
171
|
+
* (a real `GET /v1/quote` response) — signature and struct layout read from
|
|
172
|
+
* `lifinance/contracts/src/Facets/AcrossFacetV4.sol`. `AcrossV4Data` carries
|
|
173
|
+
* no native-fee field, so {@link DecodedLifiFacetCall.extraNativeFee} is
|
|
174
|
+
* always `0n`. `outputAmount` is Across's own destination-side guarantee —
|
|
175
|
+
* confirmed equal to the SAME response's `estimate.toAmountMin`.
|
|
176
|
+
*/
|
|
177
|
+
const decodeAcrossV4SwapAndStart = (data) => {
|
|
178
|
+
const acrossV4DataAbiType = {
|
|
179
|
+
type: 'tuple',
|
|
180
|
+
components: [
|
|
181
|
+
{ name: 'receiverAddress', type: 'bytes32' },
|
|
182
|
+
{ name: 'refundAddress', type: 'bytes32' },
|
|
183
|
+
{ name: 'sendingAssetId', type: 'bytes32' },
|
|
184
|
+
{ name: 'receivingAssetId', type: 'bytes32' },
|
|
185
|
+
{ name: 'outputAmount', type: 'uint256' },
|
|
186
|
+
{ name: 'outputAmountMultiplier', type: 'uint128' },
|
|
187
|
+
{ name: 'exclusiveRelayer', type: 'bytes32' },
|
|
188
|
+
{ name: 'quoteTimestamp', type: 'uint32' },
|
|
189
|
+
{ name: 'fillDeadline', type: 'uint32' },
|
|
190
|
+
{ name: 'exclusivityParameter', type: 'uint32' },
|
|
191
|
+
{ name: 'message', type: 'bytes' },
|
|
192
|
+
],
|
|
193
|
+
};
|
|
194
|
+
const encodedTail = `0x${data.slice(10)}`;
|
|
195
|
+
const decodedHead = decodeBridgeDataAndLeadingSwap(encodedTail);
|
|
196
|
+
if (decodedHead === null) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const [, , acrossData] = (0, viem_1.decodeAbiParameters)([bridgeDataAbiType, swapDataArrayAbiType, acrossV4DataAbiType], encodedTail);
|
|
200
|
+
return {
|
|
201
|
+
toolLabel: 'across',
|
|
202
|
+
bridgeData: decodedHead.bridgeData,
|
|
203
|
+
originSendingAssetId: decodedHead.leadingSwap.sendingAssetId,
|
|
204
|
+
originAmount: decodedHead.leadingSwap.fromAmount,
|
|
205
|
+
extraNativeFee: 0n,
|
|
206
|
+
destinationMinimum: acrossData.outputAmount,
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* `swapAndStartBridgeTokensViaLayerSwap((BridgeData),(SwapData[]),(LayerSwapData))`.
|
|
211
|
+
*
|
|
212
|
+
* Selector `0x4c279d6b`, verified against
|
|
213
|
+
* `packages/bridge-sdk/src/__fixtures__/lifi/quote-plain-base-to-polygon.json`
|
|
214
|
+
* (a real `GET /v1/quote` response) — signature and struct layout read from
|
|
215
|
+
* `lifinance/contracts/src/Facets/LayerSwapFacet.sol`. `LayerSwapData`
|
|
216
|
+
* carries no native-fee field ({@link DecodedLifiFacetCall.extraNativeFee}
|
|
217
|
+
* is always `0n`) and, unlike Across or Stargate, NO destination-amount
|
|
218
|
+
* field at all — LayerSwap's delivered amount is attested off-chain by an
|
|
219
|
+
* EIP-712 signature over `requestId`, not provable from this calldata, so
|
|
220
|
+
* {@link DecodedLifiFacetCall.destinationMinimum} is always `null`.
|
|
221
|
+
*/
|
|
222
|
+
const decodeLayerSwapSwapAndStart = (data) => {
|
|
223
|
+
const layerSwapDataAbiType = {
|
|
224
|
+
type: 'tuple',
|
|
225
|
+
components: [
|
|
226
|
+
{ name: 'requestId', type: 'bytes32' },
|
|
227
|
+
{ name: 'depositoryReceiver', type: 'address' },
|
|
228
|
+
{ name: 'refundRecipient', type: 'address' },
|
|
229
|
+
{ name: 'nonEVMReceiver', type: 'bytes32' },
|
|
230
|
+
{ name: 'signature', type: 'bytes' },
|
|
231
|
+
{ name: 'deadline', type: 'uint256' },
|
|
232
|
+
],
|
|
233
|
+
};
|
|
234
|
+
const encodedTail = `0x${data.slice(10)}`;
|
|
235
|
+
const decodedHead = decodeBridgeDataAndLeadingSwap(encodedTail);
|
|
236
|
+
if (decodedHead === null) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
toolLabel: 'layerswap',
|
|
241
|
+
bridgeData: decodedHead.bridgeData,
|
|
242
|
+
originSendingAssetId: decodedHead.leadingSwap.sendingAssetId,
|
|
243
|
+
originAmount: decodedHead.leadingSwap.fromAmount,
|
|
244
|
+
extraNativeFee: 0n,
|
|
245
|
+
destinationMinimum: null,
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
/**
|
|
249
|
+
* `swapAndStartBridgeTokensViaStargate((BridgeData),(SwapData[]),(StargateData))`.
|
|
250
|
+
*
|
|
251
|
+
* Selector `0xa6010a66`, verified against two live captures of
|
|
252
|
+
* `POST /v1/quote/contractCalls`
|
|
253
|
+
* (`packages/bridge-sdk/src/__fixtures__/lifi/contract-calls-native-base-to-eth-mediator.json`
|
|
254
|
+
* and its `-erc20-` sibling) — signature and struct layout read from
|
|
255
|
+
* `lifinance/contracts/src/Facets/StargateFacetV2.sol` and
|
|
256
|
+
* `src/Interfaces/IStargate.sol`. `StargateData.fee.nativeFee` is a
|
|
257
|
+
* LayerZero messaging fee that rides in `msg.value` ON TOP of any deposited
|
|
258
|
+
* native amount — confirmed by both fixtures: `transactionRequest.value`
|
|
259
|
+
* equalled `originAmount + fee.nativeFee` exactly (origin ETH) and
|
|
260
|
+
* `fee.nativeFee` alone when the origin amount was an ERC20 (USD Coin, no
|
|
261
|
+
* native component). `sendParams.minAmountLD` is Stargate's own
|
|
262
|
+
* destination-side guarantee.
|
|
263
|
+
*/
|
|
264
|
+
const decodeStargateSwapAndStart = (data) => {
|
|
265
|
+
const stargateDataAbiType = {
|
|
266
|
+
type: 'tuple',
|
|
267
|
+
components: [
|
|
268
|
+
{ name: 'assetId', type: 'uint16' },
|
|
269
|
+
{
|
|
270
|
+
name: 'sendParams',
|
|
271
|
+
type: 'tuple',
|
|
272
|
+
components: [
|
|
273
|
+
{ name: 'dstEid', type: 'uint32' },
|
|
274
|
+
{ name: 'to', type: 'bytes32' },
|
|
275
|
+
{ name: 'amountLD', type: 'uint256' },
|
|
276
|
+
{ name: 'minAmountLD', type: 'uint256' },
|
|
277
|
+
{ name: 'extraOptions', type: 'bytes' },
|
|
278
|
+
{ name: 'composeMsg', type: 'bytes' },
|
|
279
|
+
{ name: 'oftCmd', type: 'bytes' },
|
|
280
|
+
],
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: 'fee',
|
|
284
|
+
type: 'tuple',
|
|
285
|
+
components: [
|
|
286
|
+
{ name: 'nativeFee', type: 'uint256' },
|
|
287
|
+
{ name: 'lzTokenFee', type: 'uint256' },
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
{ name: 'refundAddress', type: 'address' },
|
|
291
|
+
],
|
|
292
|
+
};
|
|
293
|
+
const encodedTail = `0x${data.slice(10)}`;
|
|
294
|
+
const decodedHead = decodeBridgeDataAndLeadingSwap(encodedTail);
|
|
295
|
+
if (decodedHead === null) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
const [, , stargateData] = (0, viem_1.decodeAbiParameters)([bridgeDataAbiType, swapDataArrayAbiType, stargateDataAbiType], encodedTail);
|
|
299
|
+
return {
|
|
300
|
+
toolLabel: 'stargateV2',
|
|
301
|
+
bridgeData: decodedHead.bridgeData,
|
|
302
|
+
originSendingAssetId: decodedHead.leadingSwap.sendingAssetId,
|
|
303
|
+
originAmount: decodedHead.leadingSwap.fromAmount,
|
|
304
|
+
extraNativeFee: stargateData.fee.nativeFee,
|
|
305
|
+
destinationMinimum: stargateData.sendParams.minAmountLD,
|
|
306
|
+
};
|
|
307
|
+
};
|
|
308
|
+
/**
|
|
309
|
+
* Every LI.FI facet function this module can decode, keyed by its four-byte
|
|
310
|
+
* selector. AN UNKNOWN SELECTOR IS NEVER DECODED — {@link verifyLifiEvmExecution}
|
|
311
|
+
* refuses it outright. Extending this table means capturing a real fixture
|
|
312
|
+
* for the new tool first; see this module's top-of-file doc comment.
|
|
313
|
+
*/
|
|
314
|
+
const lifiFacetDecoders = {
|
|
315
|
+
'0x1794958f': decodeAcrossV4SwapAndStart,
|
|
316
|
+
'0x4c279d6b': decodeLayerSwapSwapAndStart,
|
|
317
|
+
'0xa6010a66': decodeStargateSwapAndStart,
|
|
318
|
+
};
|
|
319
|
+
const sameAddress = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
320
|
+
/**
|
|
321
|
+
* Verifies a LI.FI EVM execution transaction against what this application
|
|
322
|
+
* asked for, throwing {@link LifiExecutionRefusedError} on any disagreement.
|
|
323
|
+
* Returns normally — carrying nothing, since the caller already holds the
|
|
324
|
+
* transaction it validated — when every check passes.
|
|
325
|
+
*
|
|
326
|
+
* See this module's top-of-file doc comment for what is checked and why.
|
|
327
|
+
*
|
|
328
|
+
* @param transactionRequest - the parsed `to`/`data`/`value`/`chainId` this
|
|
329
|
+
* application is about to hand to a wallet
|
|
330
|
+
* @param approvalAddress - `estimate.approvalAddress` from the same
|
|
331
|
+
* response, or `null` when the route needs no token approval
|
|
332
|
+
* @param expectation - what this application actually asked for
|
|
333
|
+
*/
|
|
334
|
+
const verifyLifiEvmExecution = (transactionRequest, approvalAddress, expectation) => {
|
|
335
|
+
const router = exports.lifiDiamondAddressByChainId.get(transactionRequest.chainId);
|
|
336
|
+
if (router === undefined) {
|
|
337
|
+
refuse('unknown-chain', `no known LI.FI Diamond address for chain ${transactionRequest.chainId}`);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (!sameAddress(transactionRequest.to, router)) {
|
|
341
|
+
refuse('router-mismatch', `transactionRequest.to (${transactionRequest.to}) is not LI.FI's Diamond on chain ${transactionRequest.chainId} (${router})`);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (approvalAddress !== null && !sameAddress(approvalAddress, router)) {
|
|
345
|
+
refuse('approval-address-mismatch', `estimate.approvalAddress (${approvalAddress}) is not LI.FI's Diamond on chain ${transactionRequest.chainId} (${router})`);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (transactionRequest.data.length < 10) {
|
|
349
|
+
refuse('calldata-undecodable', 'transactionRequest.data is shorter than a function selector');
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const selector = transactionRequest.data.slice(0, 10).toLowerCase();
|
|
353
|
+
const decoder = lifiFacetDecoders[selector];
|
|
354
|
+
if (decoder === undefined) {
|
|
355
|
+
refuse('unknown-selector', `transactionRequest.data's selector (${selector}) is not a verified LI.FI facet function`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
let decoded;
|
|
359
|
+
try {
|
|
360
|
+
decoded = decoder(transactionRequest.data);
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
refuse('calldata-undecodable', `transactionRequest.data did not decode as ${selector}'s known parameters: ${error instanceof Error ? error.message : String(error)}`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (decoded === null) {
|
|
367
|
+
refuse('calldata-undecodable', `${selector} decoded with no usable source-swap entry`);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (!sameAddress(decoded.bridgeData.receiver, expectation.receiver)) {
|
|
371
|
+
refuse('receiver-mismatch', `decoded BridgeData.receiver (${decoded.bridgeData.receiver}) does not match the expected receiver (${expectation.receiver})`);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (decoded.bridgeData.destinationChainId !== BigInt(expectation.destinationChainId)) {
|
|
375
|
+
refuse('destination-chain-mismatch', `decoded BridgeData.destinationChainId (${decoded.bridgeData.destinationChainId}) does not match the requested destination chain (${expectation.destinationChainId})`);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (!sameAddress(decoded.originSendingAssetId, expectation.sendingAssetId)) {
|
|
379
|
+
refuse('sending-asset-mismatch', `decoded origin sending asset (${decoded.originSendingAssetId}) does not match the requested origin token (${expectation.sendingAssetId})`);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (decoded.bridgeData.hasDestinationCall !== expectation.hasDestinationCall) {
|
|
383
|
+
refuse('destination-call-flag-mismatch', `decoded BridgeData.hasDestinationCall (${decoded.bridgeData.hasDestinationCall}) does not match whether this request attached a destination call (${expectation.hasDestinationCall})`);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (expectation.minimumAmount !== null) {
|
|
387
|
+
if (decoded.destinationMinimum === null) {
|
|
388
|
+
refuse('minimum-amount-below-requested', `${decoded.toolLabel} carries no on-chain destination-amount field to verify against the requested minimum (${expectation.minimumAmount})`);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (decoded.destinationMinimum < expectation.minimumAmount) {
|
|
392
|
+
refuse('minimum-amount-below-requested', `decoded destination minimum (${decoded.destinationMinimum}) is below the requested minimum (${expectation.minimumAmount})`);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (expectation.originAmount !== null && decoded.originAmount !== expectation.originAmount) {
|
|
397
|
+
refuse('origin-amount-mismatch', `decoded origin amount (${decoded.originAmount}) does not equal the amount to send (${expectation.originAmount})`);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const nativeOriginComponent = sameAddress(decoded.originSendingAssetId, viem_1.zeroAddress)
|
|
401
|
+
? decoded.originAmount
|
|
402
|
+
: 0n;
|
|
403
|
+
const expectedValue = nativeOriginComponent + decoded.extraNativeFee;
|
|
404
|
+
if (transactionRequest.value !== expectedValue) {
|
|
405
|
+
refuse('native-value-mismatch', `transactionRequest.value (${transactionRequest.value}) does not equal the native amount the decoded calldata implies (${expectedValue})`);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
exports.verifyLifiEvmExecution = verifyLifiEvmExecution;
|