@gibs/bridge-sdk 1.13.1 → 1.18.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 +295 -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 +255 -0
- package/dist/cjs/config.js +569 -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 +359 -0
- package/dist/cjs/execution.js +495 -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 +24 -0
- package/dist/cjs/index.js +40 -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 +117 -0
- package/dist/cjs/prices.js +224 -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/carrier-provider-key.d.ts +31 -0
- package/dist/cjs/routing/carrier-provider-key.js +42 -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 +184 -0
- package/dist/cjs/routing/carriers/pulsex.js +615 -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 +42 -0
- package/dist/cjs/routing/index.js +142 -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 +397 -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 +285 -0
- package/dist/cjs/routing/search/plan-route-data.js +871 -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 +95 -0
- package/dist/cjs/routing/swap-transactions.js +133 -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/swap-venues.d.ts +43 -0
- package/dist/cjs/swap-venues.js +29 -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 +42 -3
- package/dist/config.js +87 -21
- package/dist/consolidated-quote.d.ts +109 -15
- package/dist/consolidated-quote.js +310 -52
- package/dist/execution.d.ts +359 -0
- package/dist/execution.js +455 -0
- package/dist/hyperliquid-execution.js +2 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -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/prices.d.ts +10 -0
- package/dist/prices.js +52 -1
- 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/carrier-provider-key.d.ts +31 -0
- package/dist/routing/carrier-provider-key.js +37 -0
- package/dist/routing/carriers/ask-answer.d.ts +17 -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 +81 -46
- package/dist/routing/carriers/pulsex.js +267 -148
- 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 +11 -3
- package/dist/routing/index.js +8 -2
- 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/route-data.d.ts +47 -5
- package/dist/routing/search/graph.d.ts +15 -0
- package/dist/routing/search/plan-route-data.d.ts +42 -1
- package/dist/routing/search/plan-route-data.js +68 -9
- package/dist/routing/search/score.d.ts +3 -0
- package/dist/routing/search/score.js +11 -3
- package/dist/routing/swap-transactions.d.ts +95 -0
- package/dist/routing/swap-transactions.js +127 -0
- package/dist/settings.d.ts +94 -1
- package/dist/settings.js +95 -3
- package/dist/swap-venues.d.ts +43 -0
- package/dist/swap-venues.js +24 -0
- 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 +75 -10
- 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 +817 -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,1048 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseLifiTokens = exports.lifiTokensUrl = exports.parseLifiChains = exports.lifiChainsUrl = exports.parseLifiStatus = exports.lifiStatusUrl = exports.parseLifiQuote = exports.parseLifiContractCallsQuote = exports.aggregatorOriginCalls = exports.evmDepositOf = exports.buildLifiContractCallsRequest = exports.buildLifiQuoteRequest = exports.lifiAttribution = exports.isNonEthereumVirtualMachineOrigin = exports.lifiRequestHeaders = exports.lifiQuoteUrl = exports.lifiContractCallsQuoteUrl = exports.lifiEndpoints = exports.lifiDefaultApiOrigin = exports.lifiReferralFeePercent = exports.lifiIntegrator = void 0;
|
|
4
|
+
const url_1 = require("@gibs/common/url");
|
|
5
|
+
const viem_1 = require("viem");
|
|
6
|
+
const config_js_1 = require("./config.js");
|
|
7
|
+
const lifi_execution_guard_js_1 = require("./lifi-execution-guard.js");
|
|
8
|
+
const origin_deposit_js_1 = require("./origin-deposit.js");
|
|
9
|
+
const quote_fees_js_1 = require("./quote-fees.js");
|
|
10
|
+
const referral_js_1 = require("./referral.js");
|
|
11
|
+
const route_steps_js_1 = require("./route-steps.js");
|
|
12
|
+
const relay_depository_js_1 = require("./relay-depository.js");
|
|
13
|
+
/**
|
|
14
|
+
* Gibs Finance's own LI.FI `integrator` label.
|
|
15
|
+
*
|
|
16
|
+
* NOT A DEFAULT. A request that leaves
|
|
17
|
+
* {@link ConsolidatedQuoteRequest.lifiIntegrator} out sends no `integrator`
|
|
18
|
+
* at all. An integrator label is attribution: LI.FI records the traffic under
|
|
19
|
+
* it, and pays any `fee` to the account behind it. A wallet that builds on this
|
|
20
|
+
* package must not attribute its users' routes to Gibs Finance without
|
|
21
|
+
* choosing to. The Gibs interface passes this value explicitly.
|
|
22
|
+
*/
|
|
23
|
+
exports.lifiIntegrator = referral_js_1.referralIntegrator;
|
|
24
|
+
/**
|
|
25
|
+
* The referral fee Gibs Finance charges on its own LI.FI routes (0.001 = 0.1%).
|
|
26
|
+
* No builder applies it. A request carries a fee only when it sets `lifiFee`.
|
|
27
|
+
*/
|
|
28
|
+
exports.lifiReferralFeePercent = 0.001;
|
|
29
|
+
/**
|
|
30
|
+
* LI.FI's HTTP origin when the interface's `PUBLIC_LIFI_API_URL` build flag is
|
|
31
|
+
* unset. Points straight at LI.FI rather than through the server-side proxy,
|
|
32
|
+
* so local development and the anonymous IPFS build still get real quotes —
|
|
33
|
+
* at LI.FI's shared, unauthenticated tier — with no configuration at all.
|
|
34
|
+
*/
|
|
35
|
+
exports.lifiDefaultApiOrigin = 'https://li.quest';
|
|
36
|
+
/**
|
|
37
|
+
* Builds every LI.FI endpoint address from one origin — THE ONE PLACE the
|
|
38
|
+
* `/v1/...` paths are spelled, so a caller pointed at a different origin (the
|
|
39
|
+
* server-side proxy at `https://lifi.gibs.finance`, or LI.FI itself) never
|
|
40
|
+
* duplicates them by hand.
|
|
41
|
+
*
|
|
42
|
+
* `apiOrigin` is the scheme-plus-host ONLY (e.g. `https://lifi.gibs.finance`),
|
|
43
|
+
* never a path — a trailing slash is trimmed, but this throws for anything
|
|
44
|
+
* that is not an absolute `http:`/`https:` URL, so a malformed configuration
|
|
45
|
+
* fails at startup rather than silently building a broken address.
|
|
46
|
+
*
|
|
47
|
+
* @param apiOrigin - the scheme-plus-host to reach LI.FI's `/v1` interface through
|
|
48
|
+
* @returns a frozen object of every endpoint this codebase calls
|
|
49
|
+
*/
|
|
50
|
+
const lifiEndpoints = (apiOrigin) => {
|
|
51
|
+
if (!(0, url_1.isHttpUrl)(apiOrigin)) {
|
|
52
|
+
throw new Error(`lifiEndpoints: "${apiOrigin}" is not an absolute http(s) origin`);
|
|
53
|
+
}
|
|
54
|
+
const base = `${apiOrigin.replace(/\/+$/, '')}/v1`;
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
quote: `${base}/quote`,
|
|
57
|
+
contractCallsQuote: `${base}/quote/contractCalls`,
|
|
58
|
+
chains: `${base}/chains`,
|
|
59
|
+
tokens: (chainId) => (0, url_1.buildUrl)(`${base}/tokens`, { chains: chainId.toString() }),
|
|
60
|
+
status: ({ txHash, fromChainId, toChainId }) => (0, url_1.buildUrl)(`${base}/status`, {
|
|
61
|
+
txHash,
|
|
62
|
+
fromChain: fromChainId?.toString(),
|
|
63
|
+
toChain: toChainId?.toString(),
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
exports.lifiEndpoints = lifiEndpoints;
|
|
68
|
+
const defaultLifiEndpoints = (0, exports.lifiEndpoints)(exports.lifiDefaultApiOrigin);
|
|
69
|
+
/** Quote endpoint that accepts destination contract calls, at {@link lifiDefaultApiOrigin}. */
|
|
70
|
+
exports.lifiContractCallsQuoteUrl = defaultLifiEndpoints.contractCallsQuote;
|
|
71
|
+
/**
|
|
72
|
+
* The plain quote endpoint, at {@link lifiDefaultApiOrigin}: carry funds and stop.
|
|
73
|
+
*
|
|
74
|
+
* WHICH ENDPOINT A ROUTE USES IS DECIDED BY WHETHER ANYTHING RUNS ON ARRIVAL,
|
|
75
|
+
* not by which chains the provider covers. A crossing into the omnibridge needs
|
|
76
|
+
* a call made on the entry chain, so it goes to
|
|
77
|
+
* {@link lifiContractCallsQuoteUrl} and arrives as one signature. A route the
|
|
78
|
+
* omnibridge does not touch has nothing to run on arrival, so it asks here.
|
|
79
|
+
*
|
|
80
|
+
* A GET, unlike its sibling — the parameters are a query string.
|
|
81
|
+
*/
|
|
82
|
+
exports.lifiQuoteUrl = defaultLifiEndpoints.quote;
|
|
83
|
+
/**
|
|
84
|
+
* Builds the headers for a LI.FI request.
|
|
85
|
+
*
|
|
86
|
+
* CARRIES NO API KEY. LI.FI's own documentation forbids setting
|
|
87
|
+
* `x-lifi-api-key` in a browser — "Never expose your x-lifi-api-key in
|
|
88
|
+
* client-side environments such as browser-based JavaScript" — so this
|
|
89
|
+
* codebase never attaches one here. The key lives on the server-side proxy
|
|
90
|
+
* (`https://lifi.gibs.finance`) that fronts `lifiApiOrigin`, added to the
|
|
91
|
+
* outbound request there, never in this bundle. LI.FI answers every endpoint
|
|
92
|
+
* with no key at a lower, shared rate limit, so a request built from this is a
|
|
93
|
+
* normal operating state, not a degraded one.
|
|
94
|
+
*
|
|
95
|
+
* ONE HELPER FOR EVERY LI.FI ENDPOINT — the plain quote, the contract-calls
|
|
96
|
+
* quote, `/v1/chains`, `/v1/tokens`, and `/v1/status` all build their headers
|
|
97
|
+
* through this, rather than each call site duplicating the content-type.
|
|
98
|
+
*/
|
|
99
|
+
const lifiRequestHeaders = () => ({
|
|
100
|
+
'Content-Type': 'application/json',
|
|
101
|
+
});
|
|
102
|
+
exports.lifiRequestHeaders = lifiRequestHeaders;
|
|
103
|
+
/**
|
|
104
|
+
* Whether a consolidated quote request originates on a non-Ethereum-Virtual-
|
|
105
|
+
* Machine chain. A missing `originVmType` is treated as Ethereum Virtual
|
|
106
|
+
* Machine, so existing callers are never classified non-Ethereum.
|
|
107
|
+
*
|
|
108
|
+
* Deliberately expressed as "anything that is not Ethereum" rather than as a
|
|
109
|
+
* list of the non-Ethereum families. The two behaviors this gates — excluding a
|
|
110
|
+
* route from LI.FI, and refunding on the ORIGIN chain instead of to an Ethereum
|
|
111
|
+
* fallback address — are both wrong by default for an unlisted ecosystem, and a
|
|
112
|
+
* positive list fails open: a newly registered family would be handed to LI.FI
|
|
113
|
+
* with a non-Ethereum address cast to `Hex`, and would name an Ethereum address
|
|
114
|
+
* as its origin-chain refund target. Inverting the test makes every future
|
|
115
|
+
* ecosystem safe on arrival.
|
|
116
|
+
*
|
|
117
|
+
* Hyperliquid counts as non-Ethereum here despite using Ethereum-style account
|
|
118
|
+
* addresses: Relay's origin for it is HyperCore, whose deposit is a signed
|
|
119
|
+
* typed-data action posted to an API rather than a broadcast transaction, and
|
|
120
|
+
* which LI.FI does not carry.
|
|
121
|
+
*
|
|
122
|
+
* @param request - the consolidated quote request to classify
|
|
123
|
+
* @returns true when the origin is any ecosystem other than Ethereum
|
|
124
|
+
*/
|
|
125
|
+
const isNonEthereumVirtualMachineOrigin = (request) => (request.originVmType ?? 'evm') !== 'evm';
|
|
126
|
+
exports.isNonEthereumVirtualMachineOrigin = isNonEthereumVirtualMachineOrigin;
|
|
127
|
+
/**
|
|
128
|
+
* The attribution fields of a LI.FI request: the `integrator` label and, when
|
|
129
|
+
* one is charged, the referral `fee`. Shared by both LI.FI builders so the two
|
|
130
|
+
* endpoints can never attribute or charge differently.
|
|
131
|
+
*
|
|
132
|
+
* @param request - the consolidated request's attribution fields
|
|
133
|
+
* @returns the fields to merge into the request; empty when none apply
|
|
134
|
+
* @throws when a positive `lifiFee` arrives without an explicit `lifiIntegrator`
|
|
135
|
+
* string — LI.FI would have nobody of yours to pay it to
|
|
136
|
+
*/
|
|
137
|
+
const lifiAttribution = (request) => {
|
|
138
|
+
const integrator = (0, referral_js_1.resolveAttribution)(request.lifiIntegrator, undefined);
|
|
139
|
+
const chargesFee = request.lifiFee !== undefined && request.lifiFee > 0;
|
|
140
|
+
if (!chargesFee) {
|
|
141
|
+
return integrator === undefined ? {} : { integrator };
|
|
142
|
+
}
|
|
143
|
+
if (typeof request.lifiIntegrator !== 'string') {
|
|
144
|
+
throw new Error('lifiFee needs an explicit lifiIntegrator: LI.FI pays the fee to the account behind the integrator label');
|
|
145
|
+
}
|
|
146
|
+
return { integrator: request.lifiIntegrator, fee: request.lifiFee };
|
|
147
|
+
};
|
|
148
|
+
exports.lifiAttribution = lifiAttribution;
|
|
149
|
+
/**
|
|
150
|
+
* Builds the LI.FI contract-calls quote request from a provider-agnostic
|
|
151
|
+
* consolidated request. LI.FI quotes backwards from `toAmount` (the guaranteed
|
|
152
|
+
* minimum arriving on Ethereum), which is what makes the fixed amount inside
|
|
153
|
+
* the destination calldata safe.
|
|
154
|
+
*
|
|
155
|
+
* @returns the request body, or null when the inputs cannot form a valid call
|
|
156
|
+
*/
|
|
157
|
+
/**
|
|
158
|
+
* Builds the query string for LI.FI's plain `GET /v1/quote`.
|
|
159
|
+
*
|
|
160
|
+
* THE ROUTE THIS EXISTS FOR. A crossing with no omnibridge leg had exactly one
|
|
161
|
+
* bidder — Relay — because our only LI.FI endpoint was the contract-calls one,
|
|
162
|
+
* which cannot express a route that calls nothing. Every any-to-any quote was
|
|
163
|
+
* therefore a price with nothing to rank it against.
|
|
164
|
+
*
|
|
165
|
+
* SOLVED FORWARDS, WHICH IS THE OPPOSITE OF ITS SIBLING. An omnibridge route is
|
|
166
|
+
* asked for an exact OUTPUT, because the entry call must receive an exact
|
|
167
|
+
* amount. Here the reader names what they spend and the delivered amount is
|
|
168
|
+
* whatever the market gives, so `fromAmount` is the input and the answer's
|
|
169
|
+
* `toAmount` is the thing that differs between providers. Ranking two forward
|
|
170
|
+
* quotes on what they cost compares two identical numbers — see
|
|
171
|
+
* `orderQuotesByCost` in the interface, which ranks these on what ARRIVES.
|
|
172
|
+
*
|
|
173
|
+
* Returns null for a request carrying a `destinationCall`: that route belongs
|
|
174
|
+
* to the other endpoint, and sending it here would quote a crossing that stops
|
|
175
|
+
* on the entry chain and never enters the bridge.
|
|
176
|
+
*
|
|
177
|
+
* @param request - the consolidated request to express
|
|
178
|
+
* @returns the query string, or null when this endpoint cannot express it
|
|
179
|
+
* @throws when `lifiFee` is positive and `lifiIntegrator` is not a string — see {@link lifiAttribution}
|
|
180
|
+
*/
|
|
181
|
+
const buildLifiQuoteRequest = (request) => {
|
|
182
|
+
const { destinationCall, fromAmount, toChainId } = request;
|
|
183
|
+
if (destinationCall !== undefined) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
if (fromAmount === undefined || fromAmount <= 0n) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
if (toChainId === undefined) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
// LI.FI quotes Ethereum-Virtual-Machine origins only, and a non-Ethereum
|
|
193
|
+
// origin address would be rejected as malformed rather than declined.
|
|
194
|
+
if ((request.originVmType ?? 'evm') !== 'evm') {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
// Same attribution as the contract-calls builder, so a fee the reader agreed
|
|
198
|
+
// to is charged on both endpoints, not only on one.
|
|
199
|
+
const { integrator, fee } = (0, exports.lifiAttribution)(request);
|
|
200
|
+
return (0, url_1.buildQuery)({
|
|
201
|
+
fromChain: request.fromChainId.toString(),
|
|
202
|
+
toChain: toChainId.toString(),
|
|
203
|
+
fromToken: request.fromTokenAddress,
|
|
204
|
+
toToken: request.toTokenAddress,
|
|
205
|
+
fromAmount: fromAmount.toString(),
|
|
206
|
+
fromAddress: request.fromAddress,
|
|
207
|
+
// WHERE THE FUNDS LAND, stated separately from who is paying. They are the
|
|
208
|
+
// same account for an ordinary transfer and are NOT when a reader has named
|
|
209
|
+
// a delivery address, which is exactly the case a bridge has to get right.
|
|
210
|
+
toAddress: request.recipient,
|
|
211
|
+
// Each optional parameter is left out when it is undefined.
|
|
212
|
+
integrator,
|
|
213
|
+
fee: fee?.toString(),
|
|
214
|
+
slippage: request.slippage?.toString(),
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
exports.buildLifiQuoteRequest = buildLifiQuoteRequest;
|
|
218
|
+
const buildLifiContractCallsRequest = (request) => {
|
|
219
|
+
const { destinationCall, toAmount } = request;
|
|
220
|
+
// THIS ENDPOINT EXISTS TO DELIVER AND THEN CALL SOMETHING. A route with no
|
|
221
|
+
// omnibridge leg has nothing to call, so it is not a request this builder can
|
|
222
|
+
// express — LI.FI's plain quote is the right endpoint for that shape, and
|
|
223
|
+
// wiring it is separate work. Refusing here leaves the route to Relay rather
|
|
224
|
+
// than sending LI.FI a contract-call request with no contract call in it.
|
|
225
|
+
if (destinationCall === undefined) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
if (toAmount <= 0n) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
// contract-call entries carry no native value; native-entry calls
|
|
232
|
+
// (wrapAndRelayTokens) stay out of scope until value support is verified
|
|
233
|
+
if (destinationCall.value > 0n) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
// LI.FI cannot bridge across virtual-machine ecosystems. The provider gate
|
|
237
|
+
// (`providerSupportsOrigin`) already keeps non-Ethereum origins away from
|
|
238
|
+
// LI.FI; this guard makes the boundary explicit so the origin fields below are
|
|
239
|
+
// genuinely Ethereum `Hex` addresses, not merely the widened `string` shape.
|
|
240
|
+
if ((0, exports.isNonEthereumVirtualMachineOrigin)(request)) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
fromChain: request.fromChainId,
|
|
245
|
+
fromToken: request.fromTokenAddress,
|
|
246
|
+
fromAddress: request.fromAddress,
|
|
247
|
+
toChain: request.toChainId ?? Number(config_js_1.Chains.ETH),
|
|
248
|
+
toToken: request.toTokenAddress,
|
|
249
|
+
toAmount: toAmount.toString(),
|
|
250
|
+
toFallbackAddress: request.fallbackAddress,
|
|
251
|
+
...(0, exports.lifiAttribution)(request),
|
|
252
|
+
...(request.slippage === undefined ? {} : { slippage: request.slippage }),
|
|
253
|
+
contractCalls: [
|
|
254
|
+
{
|
|
255
|
+
fromAmount: toAmount.toString(),
|
|
256
|
+
fromTokenAddress: request.toTokenAddress,
|
|
257
|
+
toContractAddress: destinationCall.to,
|
|
258
|
+
toContractCallData: destinationCall.data,
|
|
259
|
+
toContractGasLimit: destinationCall.gasLimit.toString(),
|
|
260
|
+
...(destinationCall.approvalTarget === null
|
|
261
|
+
? {}
|
|
262
|
+
: { toApprovalAddress: destinationCall.approvalTarget }),
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
exports.buildLifiContractCallsRequest = buildLifiContractCallsRequest;
|
|
268
|
+
/**
|
|
269
|
+
* Narrows a quote's origin deposit to the Ethereum-Virtual-Machine variant.
|
|
270
|
+
* Throws when the deposit is not Ethereum, because the Ethereum-only execute
|
|
271
|
+
* path must never silently proceed on a Solana or Bitcoin payload.
|
|
272
|
+
*/
|
|
273
|
+
const evmDepositOf = (quote) => {
|
|
274
|
+
if (!(0, origin_deposit_js_1.isEvmDeposit)(quote.originDeposit)) {
|
|
275
|
+
throw new Error(`expected an Ethereum-Virtual-Machine origin deposit, received "${(0, origin_deposit_js_1.depositFamilyOf)(quote.originDeposit)}"`);
|
|
276
|
+
}
|
|
277
|
+
return quote.originDeposit;
|
|
278
|
+
};
|
|
279
|
+
exports.evmDepositOf = evmDepositOf;
|
|
280
|
+
const refuseOrigin = (refusal) => ({ kind: 'refused', refusal });
|
|
281
|
+
const sameHexAddress = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
282
|
+
/**
|
|
283
|
+
* The amount of the origin token the reader sends, in base units.
|
|
284
|
+
*
|
|
285
|
+
* THE REQUEST IS THE AUTHORITY. A route with no omnibridge entry call is
|
|
286
|
+
* solved forwards: the reader names the input and the request carries it as
|
|
287
|
+
* `fromAmount`. {@link aggregatorOriginCalls} refuses a quote whose own
|
|
288
|
+
* `fromAmount` differs from it.
|
|
289
|
+
*
|
|
290
|
+
* A route WITH an entry call is solved backwards from an exact output, so the
|
|
291
|
+
* request names no input at all and the provider works one out. For that
|
|
292
|
+
* route the quote's `fromAmount` is the only figure there is. The deposit, the
|
|
293
|
+
* approval and the quote must still agree with each other, but no figure of
|
|
294
|
+
* the reader's exists to compare them with.
|
|
295
|
+
*/
|
|
296
|
+
const amountToSendOf = ({ quote, request }) => request.fromAmount ?? quote.fromAmount;
|
|
297
|
+
const refuseAmount = ({ provider, field, expected, actual, }) => refuseVerdict({ reason: 'amount-mismatch', provider, field, expected, actual });
|
|
298
|
+
const refuseRecipient = ({ provider, field, expected, actual, }) => refuseVerdict({ reason: 'recipient-mismatch', provider, field, expected, actual });
|
|
299
|
+
const refuseVerdict = (refusal) => ({ kind: 'refused', refusal });
|
|
300
|
+
/**
|
|
301
|
+
* The spender a provider allows, given the one contract it trusts on this
|
|
302
|
+
* chain: no approval when the quote asks for none, the trusted contract when
|
|
303
|
+
* the quote names it, and a refusal for any other address.
|
|
304
|
+
*/
|
|
305
|
+
const spenderVerdict = ({ provider, requested, trusted, }) => {
|
|
306
|
+
if (requested === null)
|
|
307
|
+
return { kind: 'verified', spender: null };
|
|
308
|
+
if (trusted === null || !sameHexAddress(requested, trusted)) {
|
|
309
|
+
return refuseVerdict({ reason: 'spender-not-allowed', provider, spender: requested });
|
|
310
|
+
}
|
|
311
|
+
// The one place a spender is branded verified: the trusted table entry.
|
|
312
|
+
return { kind: 'verified', spender: trusted };
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* LI.FI: the deposit and the spender must be LI.FI's Diamond, and the calldata
|
|
316
|
+
* must say what was asked: the receiver, the asset, and (through
|
|
317
|
+
* `originAmount`) the exact amount pulled from the reader.
|
|
318
|
+
*/
|
|
319
|
+
const lifiOriginVerdict = ({ request, quote, deposit, amountToSend }) => {
|
|
320
|
+
const diamond = lifi_execution_guard_js_1.lifiDiamondAddressByChainId.get(deposit.chainId);
|
|
321
|
+
if (diamond === undefined)
|
|
322
|
+
return refuseVerdict({ reason: 'unverified-chain', provider: 'lifi', chainId: deposit.chainId });
|
|
323
|
+
try {
|
|
324
|
+
(0, lifi_execution_guard_js_1.verifyLifiEvmExecution)(deposit, quote.approvalAddress, {
|
|
325
|
+
...lifiExecutionExpectation(request),
|
|
326
|
+
originAmount: amountToSend,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
if (error instanceof lifi_execution_guard_js_1.LifiExecutionRefusedError)
|
|
331
|
+
return refuseVerdict({ reason: 'lifi-guard', detail: error.refusal.detail });
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
return spenderVerdict({ provider: 'lifi', requested: quote.approvalAddress, trusted: diamond });
|
|
335
|
+
};
|
|
336
|
+
/**
|
|
337
|
+
* The two deposit functions Relay's depository exposes, as observed in live
|
|
338
|
+
* `POST /quote/v2` answers on 2026-10-07 (Ethereum to Base):
|
|
339
|
+
*
|
|
340
|
+
* - `depositNative(address depositor, bytes32 id)` (selector `0x49290c1c`):
|
|
341
|
+
* the amount is `msg.value`; the calldata carries NO amount.
|
|
342
|
+
* - `depositErc20(address depositor, address token, uint256 amount, bytes32 id)`
|
|
343
|
+
* (selector `0xe8017952`): the calldata carries the token and the amount.
|
|
344
|
+
*
|
|
345
|
+
* Any other selector is refused rather than guessed at.
|
|
346
|
+
*/
|
|
347
|
+
const relayDepositoryAbi = (0, viem_1.parseAbi)([
|
|
348
|
+
'function depositNative(address depositor, bytes32 id) payable',
|
|
349
|
+
'function depositErc20(address depositor, address token, uint256 amount, bytes32 id)',
|
|
350
|
+
]);
|
|
351
|
+
const decodeRelayDeposit = (data) => {
|
|
352
|
+
try {
|
|
353
|
+
return (0, viem_1.decodeFunctionData)({ abi: relayDepositoryAbi, data });
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
/** The native half of Relay's deposit: `depositNative`, with the amount as `msg.value`. */
|
|
360
|
+
const relayNativeVerdict = ({ deposit, amountToSend }) => {
|
|
361
|
+
const provider = 'relay';
|
|
362
|
+
if (deposit.value !== amountToSend) {
|
|
363
|
+
return refuseAmount({ provider, field: 'deposit-value', expected: amountToSend, actual: deposit.value });
|
|
364
|
+
}
|
|
365
|
+
const decoded = decodeRelayDeposit(deposit.data);
|
|
366
|
+
if (decoded === null || decoded.functionName !== 'depositNative') {
|
|
367
|
+
return refuseVerdict({ reason: 'deposit-shape', provider, detail: 'a native deposit is not depositNative' });
|
|
368
|
+
}
|
|
369
|
+
return null;
|
|
370
|
+
};
|
|
371
|
+
/** The token half of Relay's deposit: no value, and `depositErc20` naming the request's token and amount. */
|
|
372
|
+
const relayTokenVerdict = ({ request, deposit, amountToSend }) => {
|
|
373
|
+
const provider = 'relay';
|
|
374
|
+
if (deposit.value !== 0n) {
|
|
375
|
+
return refuseAmount({ provider, field: 'deposit-value', expected: 0n, actual: deposit.value });
|
|
376
|
+
}
|
|
377
|
+
const decoded = decodeRelayDeposit(deposit.data);
|
|
378
|
+
if (decoded === null || decoded.functionName !== 'depositErc20') {
|
|
379
|
+
return refuseVerdict({ reason: 'deposit-shape', provider, detail: 'a token deposit is not depositErc20' });
|
|
380
|
+
}
|
|
381
|
+
const [, token, amount] = decoded.args;
|
|
382
|
+
if (!sameHexAddress(token, request.fromTokenAddress)) {
|
|
383
|
+
return refuseRecipient({ provider, field: 'calldata-token', expected: request.fromTokenAddress, actual: token });
|
|
384
|
+
}
|
|
385
|
+
if (amount !== amountToSend) {
|
|
386
|
+
return refuseAmount({ provider, field: 'calldata-amount', expected: amountToSend, actual: amount });
|
|
387
|
+
}
|
|
388
|
+
return null;
|
|
389
|
+
};
|
|
390
|
+
/**
|
|
391
|
+
* Relay: the deposit and the spender must be Relay's depository on the
|
|
392
|
+
* deposit's chain, and the deposit must move exactly the amount to send.
|
|
393
|
+
*
|
|
394
|
+
* - A native origin: `value` equals the amount, and the calldata is
|
|
395
|
+
* `depositNative`, which carries no amount of its own.
|
|
396
|
+
* - A token origin: `value` is 0, and the calldata is `depositErc20` for the
|
|
397
|
+
* request's token and the amount, decoded with {@link relayDepositoryAbi}.
|
|
398
|
+
*
|
|
399
|
+
* The depositor and the order identifier inside the calldata are NOT checked.
|
|
400
|
+
* The identifier is Relay's own, and nothing in the request predicts it.
|
|
401
|
+
*/
|
|
402
|
+
const relayOriginVerdict = (inputs) => {
|
|
403
|
+
const { quote, request, deposit } = inputs;
|
|
404
|
+
const depository = relay_depository_js_1.relayDepositoryByChainId.get(deposit.chainId);
|
|
405
|
+
if (depository === undefined) {
|
|
406
|
+
return refuseVerdict({ reason: 'unverified-chain', provider: 'relay', chainId: deposit.chainId });
|
|
407
|
+
}
|
|
408
|
+
if (!sameHexAddress(deposit.to, depository)) {
|
|
409
|
+
return refuseVerdict({ reason: 'deposit-target-not-allowed', provider: 'relay', target: deposit.to });
|
|
410
|
+
}
|
|
411
|
+
const spender = spenderVerdict({ provider: 'relay', requested: quote.approvalAddress, trusted: depository });
|
|
412
|
+
if (spender.kind === 'refused')
|
|
413
|
+
return spender;
|
|
414
|
+
const refusal = request.fromTokenAddress === viem_1.zeroAddress ? relayNativeVerdict(inputs) : relayTokenVerdict(inputs);
|
|
415
|
+
return refusal ?? spender;
|
|
416
|
+
};
|
|
417
|
+
const decodeTransfer = (data) => {
|
|
418
|
+
try {
|
|
419
|
+
return (0, viem_1.decodeFunctionData)({ abi: viem_1.erc20Abi, data });
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
/**
|
|
426
|
+
* NEAR Intents: no contract pulls anything. The deposit is a plain payment to
|
|
427
|
+
* the quote's own deposit address, so the check is on its SHAPE and on the two
|
|
428
|
+
* figures that matter: who is paid and how much.
|
|
429
|
+
*
|
|
430
|
+
* - No approval.
|
|
431
|
+
* - A native origin: no calldata, `to` is the quote's deposit address
|
|
432
|
+
* (`quote.quoteId`, which `parseNearIntentsQuote` sets to it), and `value`
|
|
433
|
+
* is the amount to send.
|
|
434
|
+
* - A token origin: no value, `to` is the origin token, and the calldata is the
|
|
435
|
+
* token's own `transfer(depositAddress, amount)` for the same two figures.
|
|
436
|
+
*
|
|
437
|
+
* The deposit address itself is per quote and cannot be checked against a
|
|
438
|
+
* fixed table; the checks bind the payment to the address the quote names.
|
|
439
|
+
*/
|
|
440
|
+
const nearIntentsOriginVerdict = ({ quote, request, deposit, amountToSend }) => {
|
|
441
|
+
const provider = 'near-intents';
|
|
442
|
+
// NEAR Intents trusts no spender at all.
|
|
443
|
+
const spender = spenderVerdict({ provider, requested: quote.approvalAddress, trusted: null });
|
|
444
|
+
if (spender.kind === 'refused')
|
|
445
|
+
return spender;
|
|
446
|
+
if (request.fromTokenAddress === viem_1.zeroAddress) {
|
|
447
|
+
if (deposit.data !== '0x')
|
|
448
|
+
return refuseVerdict({ reason: 'deposit-shape', provider, detail: 'a native deposit carries calldata' });
|
|
449
|
+
if (!sameHexAddress(deposit.to, quote.quoteId)) {
|
|
450
|
+
return refuseRecipient({ provider, field: 'deposit-to', expected: quote.quoteId, actual: deposit.to });
|
|
451
|
+
}
|
|
452
|
+
if (deposit.value !== amountToSend) {
|
|
453
|
+
return refuseAmount({ provider, field: 'deposit-value', expected: amountToSend, actual: deposit.value });
|
|
454
|
+
}
|
|
455
|
+
return spender;
|
|
456
|
+
}
|
|
457
|
+
if (!sameHexAddress(deposit.to, request.fromTokenAddress)) {
|
|
458
|
+
return refuseVerdict({ reason: 'deposit-target-not-allowed', provider, target: deposit.to });
|
|
459
|
+
}
|
|
460
|
+
if (deposit.value !== 0n)
|
|
461
|
+
return refuseAmount({ provider, field: 'deposit-value', expected: 0n, actual: deposit.value });
|
|
462
|
+
const decoded = decodeTransfer(deposit.data);
|
|
463
|
+
if (decoded === null || decoded.functionName !== 'transfer') {
|
|
464
|
+
return refuseVerdict({ reason: 'deposit-shape', provider, detail: 'a token deposit is not a plain transfer' });
|
|
465
|
+
}
|
|
466
|
+
const [payee, amount] = decoded.args;
|
|
467
|
+
if (!sameHexAddress(payee, quote.quoteId)) {
|
|
468
|
+
return refuseRecipient({ provider, field: 'transfer-to', expected: quote.quoteId, actual: payee });
|
|
469
|
+
}
|
|
470
|
+
if (amount !== amountToSend) {
|
|
471
|
+
return refuseAmount({ provider, field: 'calldata-amount', expected: amountToSend, actual: amount });
|
|
472
|
+
}
|
|
473
|
+
return spender;
|
|
474
|
+
};
|
|
475
|
+
const originVerdictByProvider = {
|
|
476
|
+
lifi: lifiOriginVerdict,
|
|
477
|
+
relay: relayOriginVerdict,
|
|
478
|
+
'near-intents': nearIntentsOriginVerdict,
|
|
479
|
+
};
|
|
480
|
+
/**
|
|
481
|
+
* Reads the approval and the deposit off an aggregator quote with an Ethereum
|
|
482
|
+
* origin, after checking both against what this code knows about the
|
|
483
|
+
* provider. The interface's aggregator press (`stores/consolidated-execute.ts`)
|
|
484
|
+
* and the SDK's execution planner both read them here, so neither can sign a
|
|
485
|
+
* call the other would refuse.
|
|
486
|
+
*
|
|
487
|
+
* WHAT IS CHECKED, BEFORE ANYTHING IS APPROVED OR SENT:
|
|
488
|
+
* - Every provider: when the request names the amount to send
|
|
489
|
+
* (`request.fromAmount`), the quote's own `fromAmount` must equal it.
|
|
490
|
+
* - LI.FI: `verifyLifiEvmExecution`: the Diamond for the chain, a known
|
|
491
|
+
* facet call, calldata that matches the request, and the decoded amount
|
|
492
|
+
* pulled from the reader equal to the amount to send.
|
|
493
|
+
* - Relay: the deposit target and the spender are Relay's depository for the
|
|
494
|
+
* chain (`relayDepositoryByChainId`). A native deposit carries the amount
|
|
495
|
+
* as `value` through `depositNative`; a token deposit carries no value and
|
|
496
|
+
* is `depositErc20` for the request's token and the amount.
|
|
497
|
+
* - NEAR Intents: no spender, and a plain payment of the amount to send to the
|
|
498
|
+
* quote's own deposit address (`quote.quoteId`), native or `transfer`.
|
|
499
|
+
* - Any other provider is refused. Its spender is unknown.
|
|
500
|
+
*
|
|
501
|
+
* The approval is for exactly the amount to send (the request's, else the
|
|
502
|
+
* quote's when the route is solved backwards and the request names none), on
|
|
503
|
+
* the request's origin chain. The native coin needs none.
|
|
504
|
+
*
|
|
505
|
+
* @param options.quote - the live quote to sign
|
|
506
|
+
* @param options.request - the request it answered
|
|
507
|
+
* @returns the checked approval and deposit, or why they are refused
|
|
508
|
+
*/
|
|
509
|
+
const aggregatorOriginCalls = ({ quote, request, }) => {
|
|
510
|
+
const { originDeposit } = quote;
|
|
511
|
+
if (!(0, origin_deposit_js_1.isEvmDeposit)(originDeposit)) {
|
|
512
|
+
return refuseOrigin({ reason: 'not-evm-deposit', depositFamily: (0, origin_deposit_js_1.depositFamilyOf)(originDeposit) });
|
|
513
|
+
}
|
|
514
|
+
if (originDeposit.chainId !== request.fromChainId) {
|
|
515
|
+
return refuseOrigin({
|
|
516
|
+
reason: 'chain-mismatch',
|
|
517
|
+
depositChainId: originDeposit.chainId,
|
|
518
|
+
requestChainId: request.fromChainId,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
const check = originVerdictByProvider[quote.provider];
|
|
522
|
+
if (check === undefined)
|
|
523
|
+
return refuseOrigin({ reason: 'unverified-provider', provider: quote.provider });
|
|
524
|
+
const amountToSend = amountToSendOf({ quote, request });
|
|
525
|
+
if (amountToSend !== quote.fromAmount) {
|
|
526
|
+
return refuseOrigin({
|
|
527
|
+
reason: 'amount-mismatch',
|
|
528
|
+
provider: quote.provider,
|
|
529
|
+
field: 'quote-from-amount',
|
|
530
|
+
expected: amountToSend,
|
|
531
|
+
actual: quote.fromAmount,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
const verdict = check({ quote, request, deposit: originDeposit, amountToSend });
|
|
535
|
+
if (verdict.kind === 'refused')
|
|
536
|
+
return refuseOrigin(verdict.refusal);
|
|
537
|
+
// THE SPENDER COMES FROM THE VERDICT, never from `quote.approvalAddress`:
|
|
538
|
+
// only an address the provider check took from its own table can be
|
|
539
|
+
// approved, so no provider path can forget the check.
|
|
540
|
+
if (request.fromTokenAddress === viem_1.zeroAddress || verdict.spender === null) {
|
|
541
|
+
return { kind: 'calls', approval: null, deposit: originDeposit };
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
kind: 'calls',
|
|
545
|
+
approval: {
|
|
546
|
+
chainId: request.fromChainId,
|
|
547
|
+
// An Ethereum origin; the request widens the address to `string` only
|
|
548
|
+
// for non-Ethereum origins, which were refused above.
|
|
549
|
+
token: request.fromTokenAddress,
|
|
550
|
+
spender: verdict.spender,
|
|
551
|
+
minimum: amountToSend,
|
|
552
|
+
},
|
|
553
|
+
deposit: originDeposit,
|
|
554
|
+
};
|
|
555
|
+
};
|
|
556
|
+
exports.aggregatorOriginCalls = aggregatorOriginCalls;
|
|
557
|
+
const isHexString = (value) => typeof value === 'string' && value.startsWith('0x');
|
|
558
|
+
/**
|
|
559
|
+
* Parses the ecosystem-independent part of a LI.FI quote.
|
|
560
|
+
*
|
|
561
|
+
* @param payload - the raw JSON body from either LI.FI quote endpoint
|
|
562
|
+
* @returns the envelope, or null when a required field is missing or malformed
|
|
563
|
+
*/
|
|
564
|
+
const parseLifiQuoteEnvelope = (payload) => {
|
|
565
|
+
if (typeof payload !== 'object' || payload === null) {
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
const quote = payload;
|
|
569
|
+
const estimate = quote.estimate;
|
|
570
|
+
const action = quote.action;
|
|
571
|
+
const transactionRequest = quote.transactionRequest;
|
|
572
|
+
const toolDetails = quote.toolDetails;
|
|
573
|
+
if (typeof quote.id !== 'string' || typeof quote.tool !== 'string') {
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
if (!estimate || typeof estimate.fromAmount !== 'string') {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
if (typeof estimate.executionDuration !== 'number') {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
if (!transactionRequest) {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
try {
|
|
586
|
+
return {
|
|
587
|
+
provider: 'lifi',
|
|
588
|
+
quoteId: quote.id,
|
|
589
|
+
tool: quote.tool,
|
|
590
|
+
toolName: typeof toolDetails?.name === 'string' ? toolDetails.name : quote.tool,
|
|
591
|
+
fromAmount: BigInt(estimate.fromAmount),
|
|
592
|
+
fromAmountUsd: typeof estimate.fromAmountUSD === 'string' ? estimate.fromAmountUSD : null,
|
|
593
|
+
approvalAddress: isHexString(estimate.approvalAddress) ? estimate.approvalAddress : null,
|
|
594
|
+
executionDurationSeconds: estimate.executionDuration,
|
|
595
|
+
steps: (0, route_steps_js_1.normalizeLifiSteps)(quote.includedSteps),
|
|
596
|
+
fees: (0, quote_fees_js_1.lifiQuoteFees)(payload),
|
|
597
|
+
transactionRequest,
|
|
598
|
+
fromChainId: typeof action?.fromChainId === 'number' ? action.fromChainId : null,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
// a BigInt conversion failed on a malformed numeric string
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
/**
|
|
607
|
+
* Normalizes a quote response from the contract-calls endpoint.
|
|
608
|
+
*
|
|
609
|
+
* A present but malformed `transactionRequest.value` fails the parse — silently
|
|
610
|
+
* sending zero native value would produce a transaction that under-pays the
|
|
611
|
+
* provider's bridging fee.
|
|
612
|
+
*
|
|
613
|
+
* REQUIRES `context.request` — see {@link lifiEvmDeposit}. Every live caller
|
|
614
|
+
* (`providers.ts`'s registry entry, the interface's `consolidated-quote.ts`
|
|
615
|
+
* store) supplies it; a caller that omits it gets a thrown
|
|
616
|
+
* {@link LifiExecutionRefusedError} rather than a silently unchecked
|
|
617
|
+
* execution transaction.
|
|
618
|
+
*
|
|
619
|
+
* @returns the normalized quote, or null when the payload shape is unexpected
|
|
620
|
+
* @throws {LifiExecutionRefusedError} when the response's execution
|
|
621
|
+
* transaction disagrees with `context.request` — see `lifi-execution-guard.ts`
|
|
622
|
+
*/
|
|
623
|
+
const parseLifiContractCallsQuote = (payload, context) => {
|
|
624
|
+
const envelope = parseLifiQuoteEnvelope(payload);
|
|
625
|
+
if (envelope === null) {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
const originDeposit = lifiEvmDeposit(envelope.transactionRequest, envelope.approvalAddress, context?.request);
|
|
629
|
+
if (originDeposit === null) {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
const { transactionRequest: _request, fromChainId: _fromChainId, ...quote } = envelope;
|
|
633
|
+
return { ...quote, originDeposit, fees: (0, quote_fees_js_1.lifiQuoteFees)(payload) };
|
|
634
|
+
};
|
|
635
|
+
exports.parseLifiContractCallsQuote = parseLifiContractCallsQuote;
|
|
636
|
+
/**
|
|
637
|
+
* Builds what THIS APPLICATION expects a LI.FI execution transaction to
|
|
638
|
+
* contain, from the request that produced it — the only thing
|
|
639
|
+
* {@link verifyLifiEvmExecution} is allowed to check a response against.
|
|
640
|
+
*
|
|
641
|
+
* `receiver` and `minimumAmount` differ by whether `destinationCall` is
|
|
642
|
+
* present, because the two LI.FI endpoints populate `BridgeData.receiver`
|
|
643
|
+
* with DIFFERENT fields and only one endpoint accepts a requested minimum at
|
|
644
|
+
* all — see the field-semantics note atop `lifi-execution-guard.ts`.
|
|
645
|
+
*/
|
|
646
|
+
const lifiExecutionExpectation = (request) => {
|
|
647
|
+
const hasDestinationCall = request.destinationCall !== undefined;
|
|
648
|
+
return {
|
|
649
|
+
receiver: hasDestinationCall ? request.fallbackAddress : request.recipient,
|
|
650
|
+
destinationChainId: request.toChainId ?? Number(config_js_1.Chains.ETH),
|
|
651
|
+
sendingAssetId: request.fromTokenAddress,
|
|
652
|
+
hasDestinationCall,
|
|
653
|
+
minimumAmount: hasDestinationCall ? request.toAmount : null,
|
|
654
|
+
originAmount: request.fromAmount ?? null,
|
|
655
|
+
};
|
|
656
|
+
};
|
|
657
|
+
/**
|
|
658
|
+
* Reads LI.FI's Ethereum `transactionRequest` into an Ethereum origin deposit.
|
|
659
|
+
*
|
|
660
|
+
* A present but malformed `value` fails the parse — silently sending zero
|
|
661
|
+
* native value would produce a transaction that under-pays the provider's
|
|
662
|
+
* bridging fee.
|
|
663
|
+
*
|
|
664
|
+
* VERIFIES THE TRANSACTION BEFORE RETURNING IT. `lifiApi.quote` and
|
|
665
|
+
* `lifiApi.contractCallsQuote` are fetched through a configurable, possibly
|
|
666
|
+
* compromised host — see `lifi-execution-guard.ts`'s top-of-file doc comment
|
|
667
|
+
* for the full threat model. `request` is REQUIRED, not optional: a caller
|
|
668
|
+
* with no request to check against gets a thrown
|
|
669
|
+
* {@link LifiExecutionRefusedError} (`missing-request-context`) rather than
|
|
670
|
+
* an origin deposit nothing has verified. This is the one gap a type
|
|
671
|
+
* signature alone could not close — `ProviderQuoteContext.request` is
|
|
672
|
+
* optional on every OTHER provider, so this function still accepts
|
|
673
|
+
* `undefined` in its parameter type and refuses at the call boundary
|
|
674
|
+
* instead, keeping it assignable to `ProviderDescriptor.executionQuote.parse`.
|
|
675
|
+
*
|
|
676
|
+
* @throws {LifiExecutionRefusedError} when `request` is absent, or when the
|
|
677
|
+
* transaction disagrees with it
|
|
678
|
+
*/
|
|
679
|
+
const lifiEvmDeposit = (transactionRequest, approvalAddress, request) => {
|
|
680
|
+
if (!isHexString(transactionRequest.to) ||
|
|
681
|
+
!isHexString(transactionRequest.data) ||
|
|
682
|
+
typeof transactionRequest.chainId !== 'number') {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
if (transactionRequest.value !== undefined && !isHexString(transactionRequest.value)) {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
if (request === undefined) {
|
|
689
|
+
throw new lifi_execution_guard_js_1.LifiExecutionRefusedError({
|
|
690
|
+
reason: 'missing-request-context',
|
|
691
|
+
detail: 'the LI.FI response parser was not given the request it is answering, so the execution transaction could not be verified',
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
const value = isHexString(transactionRequest.value) ? BigInt(transactionRequest.value) : 0n;
|
|
695
|
+
(0, lifi_execution_guard_js_1.verifyLifiEvmExecution)({
|
|
696
|
+
to: transactionRequest.to,
|
|
697
|
+
data: transactionRequest.data,
|
|
698
|
+
value,
|
|
699
|
+
chainId: transactionRequest.chainId,
|
|
700
|
+
}, approvalAddress, lifiExecutionExpectation(request));
|
|
701
|
+
return {
|
|
702
|
+
ecosystem: 'evm',
|
|
703
|
+
to: transactionRequest.to,
|
|
704
|
+
data: transactionRequest.data,
|
|
705
|
+
value,
|
|
706
|
+
chainId: transactionRequest.chainId,
|
|
707
|
+
gasLimit: isHexString(transactionRequest.gasLimit) ? BigInt(transactionRequest.gasLimit) : null,
|
|
708
|
+
};
|
|
709
|
+
};
|
|
710
|
+
// A partially-signed Bitcoin transaction always begins with the five-byte magic
|
|
711
|
+
// "psbt\xff". Checking it keeps a base64 payload, or an Ethereum `0x` string,
|
|
712
|
+
// from being handed to a Bitcoin wallet as though it were a transaction.
|
|
713
|
+
const partiallySignedBitcoinMagic = '70736274ff';
|
|
714
|
+
/**
|
|
715
|
+
* Reads LI.FI's Bitcoin `transactionRequest` into a Bitcoin origin deposit.
|
|
716
|
+
*
|
|
717
|
+
* LI.FI and Relay happen to agree on the encoding here — an unprefixed
|
|
718
|
+
* hexadecimal partially-signed transaction — so this lands on the same
|
|
719
|
+
* {@link BitcoinDeposit} the Relay parser produces and needs no signer change.
|
|
720
|
+
*/
|
|
721
|
+
const lifiBitcoinDeposit = (transactionRequest, chainId) => {
|
|
722
|
+
const { to, data, value } = transactionRequest;
|
|
723
|
+
if (typeof to !== 'string' || to.length === 0) {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
if (typeof data !== 'string' || !data.toLowerCase().startsWith(partiallySignedBitcoinMagic)) {
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
if (typeof value !== 'string' || !/^[0-9]+$/.test(value)) {
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
return { ecosystem: 'bvm', psbt: data, depositAddress: to, amountSats: BigInt(value), chainId };
|
|
733
|
+
};
|
|
734
|
+
// Base64 without whitespace. Necessary but nowhere near sufficient on its own:
|
|
735
|
+
// the hexadecimal alphabet is a SUBSET of base64's, so a Bitcoin
|
|
736
|
+
// partially-signed transaction matches this pattern happily.
|
|
737
|
+
const base64Pattern = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
738
|
+
// A single-byte compact-u16 tops out below this, and no real transaction needs
|
|
739
|
+
// anything near this many signatures.
|
|
740
|
+
const maxPlausibleSignatureCount = 19;
|
|
741
|
+
const solanaSignatureByteLength = 64;
|
|
742
|
+
// A versioned message's first byte sets the high bit; version 0 is the only one
|
|
743
|
+
// that exists, so the byte is exactly 0x80.
|
|
744
|
+
const solanaVersionZeroPrefix = 0x80;
|
|
745
|
+
/**
|
|
746
|
+
* Checks that a base64 payload really is a serialized Solana transaction, by
|
|
747
|
+
* walking its wire layout: a compact-u16 signature count, that many 64-byte
|
|
748
|
+
* signatures, then a message whose leading byte is either the version-0 marker
|
|
749
|
+
* or (for a legacy message) the same signature count again.
|
|
750
|
+
*
|
|
751
|
+
* An alphabet check alone is not enough. Hexadecimal is a subset of base64, so
|
|
752
|
+
* a Bitcoin partially-signed transaction passes a pure pattern test and would
|
|
753
|
+
* be handed to a Solana wallet as a transaction.
|
|
754
|
+
*/
|
|
755
|
+
const isSerializedSolanaTransaction = (value) => {
|
|
756
|
+
if (!base64Pattern.test(value)) {
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
let bytes;
|
|
760
|
+
try {
|
|
761
|
+
const binary = atob(value);
|
|
762
|
+
bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
763
|
+
}
|
|
764
|
+
catch {
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
const signatureCount = bytes[0];
|
|
768
|
+
if (signatureCount === undefined ||
|
|
769
|
+
signatureCount < 1 ||
|
|
770
|
+
signatureCount > maxPlausibleSignatureCount) {
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
773
|
+
const messageStart = 1 + signatureCount * solanaSignatureByteLength;
|
|
774
|
+
const header = bytes[messageStart];
|
|
775
|
+
if (header === undefined) {
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
return header === solanaVersionZeroPrefix || header === signatureCount;
|
|
779
|
+
};
|
|
780
|
+
/**
|
|
781
|
+
* Reads LI.FI's Solana `transactionRequest` into a Solana origin deposit.
|
|
782
|
+
*
|
|
783
|
+
* The whole payload is a single base64 compiled transaction under `data`, with
|
|
784
|
+
* no `to`, `value` or `chainId` beside it, so the chain id comes from the
|
|
785
|
+
* quote's action rather than the request.
|
|
786
|
+
*/
|
|
787
|
+
const lifiSolanaDeposit = (transactionRequest, chainId) => {
|
|
788
|
+
const { data } = transactionRequest;
|
|
789
|
+
if (typeof data !== 'string' || !isSerializedSolanaTransaction(data)) {
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
return { ecosystem: 'svm', form: 'serialized', transaction: data, chainId };
|
|
793
|
+
};
|
|
794
|
+
/**
|
|
795
|
+
* INTENTIONALLY UNWIRED at runtime — retained as tested, parked capability.
|
|
796
|
+
*
|
|
797
|
+
* Nothing in the app calls this; the live LI.FI path is `parseLifiContractCallsQuote`
|
|
798
|
+
* (the contract-calls endpoint), which only ever produces an Ethereum deposit.
|
|
799
|
+
* This parser is the ONE piece that can decode a NON-Ethereum LI.FI origin
|
|
800
|
+
* deposit, and it exists for a provider path that is deliberately NOT built:
|
|
801
|
+
* adding LI.FI as a competing provider for non-Ethereum origins was analysed and
|
|
802
|
+
* rejected (LI.FI can produce only leg one of the consolidated route, and every
|
|
803
|
+
* way to execute leg two is strictly worse than Relay, which already serves those
|
|
804
|
+
* origins in a single signature — see docs/consolidated-bridge-route-design.md).
|
|
805
|
+
* Do not delete this while that decision could still be revisited; its test block
|
|
806
|
+
* (`parseLifiQuote across origin ecosystems`) is its executable spec.
|
|
807
|
+
*
|
|
808
|
+
* Normalizes a quote response from LI.FI's plain `GET /v1/quote`.
|
|
809
|
+
*
|
|
810
|
+
* Unlike the contract-calls endpoint, this one accepts a non-Ethereum origin,
|
|
811
|
+
* and the shape of `transactionRequest` then varies by ecosystem: Ethereum
|
|
812
|
+
* returns the familiar `to`/`data`/`value`/`chainId`, Bitcoin returns a deposit
|
|
813
|
+
* address plus a partially-signed transaction, and Solana returns nothing but a
|
|
814
|
+
* compiled transaction. Reading the Ethereum fields off the others would
|
|
815
|
+
* produce a deposit that is structurally valid and completely wrong, so the
|
|
816
|
+
* caller states the origin ecosystem and only that reader runs.
|
|
817
|
+
*
|
|
818
|
+
* The origin ecosystem is a parameter rather than something sniffed from the
|
|
819
|
+
* payload because the caller already knows it — it decided which chain to quote
|
|
820
|
+
* — and guessing from field presence would make a malformed Ethereum response
|
|
821
|
+
* look like a valid Solana one.
|
|
822
|
+
*
|
|
823
|
+
* Note that `approvalAddress` is dropped for a non-Ethereum origin. LI.FI
|
|
824
|
+
* returns its Ethereum router there regardless of where the route starts, and
|
|
825
|
+
* that address is meaningless to a Solana or Bitcoin wallet; passing it on
|
|
826
|
+
* invites a caller to build an approval for a chain that has no such concept.
|
|
827
|
+
*
|
|
828
|
+
* @param payload - the raw JSON body from `GET /v1/quote`
|
|
829
|
+
* @param options.originEcosystem - the virtual-machine family the route starts from
|
|
830
|
+
* @returns the normalized quote, or null when the payload does not match that ecosystem
|
|
831
|
+
*/
|
|
832
|
+
/**
|
|
833
|
+
* What a plain LI.FI quote says arrives, and what LI.FI says it is worth.
|
|
834
|
+
*
|
|
835
|
+
* ONLY THE PLAIN ENDPOINT CARRIES THESE ONTO THE QUOTE, and that is not a
|
|
836
|
+
* convenience. A route through this endpoint is solved FORWARDS: the reader
|
|
837
|
+
* names what they spend, so every provider is handed the same `fromAmount` and
|
|
838
|
+
* the delivered amount is the ONE figure that differs. Without it:
|
|
839
|
+
*
|
|
840
|
+
* - ranking cannot engage its forward rule — it requires every quote to carry
|
|
841
|
+
* `toAmount` — and falls back to comparing each provider's own dollar
|
|
842
|
+
* figure, which come from different price feeds. Measured: LI.FI $122.00
|
|
843
|
+
* against Relay $123.71 on the same 0.05 ether, so LI.FI won on a feed.
|
|
844
|
+
* - the route then shows the winner's delivered amount as ZERO, because the
|
|
845
|
+
* direct candidate reads `quote.toAmount ?? 0n`.
|
|
846
|
+
*
|
|
847
|
+
* Both happened: Relay's parser set these, LI.FI's never did, and a route test
|
|
848
|
+
* built its quotes by hand with `toAmount` already filled in — so it passed.
|
|
849
|
+
* It took a live round trip to see the gap.
|
|
850
|
+
*
|
|
851
|
+
* NOT added to the shared envelope. The contract-calls endpoint uses that too,
|
|
852
|
+
* and on an omnibridge route the provider's delivered side is the ENTRY chain's
|
|
853
|
+
* tokens rather than what lands on PulseChain — see `ConsolidatedQuote.toAmount`.
|
|
854
|
+
*
|
|
855
|
+
* `estimate.toAmount` is the expected amount, matching Relay's
|
|
856
|
+
* `currencyOut.amount`, so the two rank on the same kind of figure.
|
|
857
|
+
*
|
|
858
|
+
* @param payload - the raw JSON body from `GET /v1/quote`
|
|
859
|
+
* @returns the delivered amount and its dollar value, each only when present
|
|
860
|
+
*/
|
|
861
|
+
const lifiDeliveredAmount = (payload) => {
|
|
862
|
+
const estimate = payload?.estimate;
|
|
863
|
+
const delivered = {};
|
|
864
|
+
if (typeof estimate?.toAmount === 'string') {
|
|
865
|
+
try {
|
|
866
|
+
delivered.toAmount = BigInt(estimate.toAmount);
|
|
867
|
+
}
|
|
868
|
+
catch {
|
|
869
|
+
// A malformed amount leaves the field absent rather than zero: zero is a
|
|
870
|
+
// real quantity and would read as "this route delivers nothing".
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
if (typeof estimate?.toAmountUSD === 'string') {
|
|
874
|
+
delivered.toAmountUsd = estimate.toAmountUSD;
|
|
875
|
+
}
|
|
876
|
+
return delivered;
|
|
877
|
+
};
|
|
878
|
+
/**
|
|
879
|
+
* Normalizes a quote response from LI.FI's plain `GET /v1/quote`.
|
|
880
|
+
*
|
|
881
|
+
* `request` IS REQUIRED FOR AN ETHEREUM ORIGIN, though its type stays
|
|
882
|
+
* optional — omitting it there throws {@link LifiExecutionRefusedError}
|
|
883
|
+
* rather than returning an unverified deposit; see `lifiEvmDeposit`'s doc
|
|
884
|
+
* comment for why the type cannot say so directly. It is genuinely optional
|
|
885
|
+
* for a Solana or Bitcoin origin, which never reach the execution guard.
|
|
886
|
+
*
|
|
887
|
+
* @param payload - the raw JSON body from `GET /v1/quote`
|
|
888
|
+
* @param options.originEcosystem - the virtual-machine family the route starts from
|
|
889
|
+
* @param options.request - the consolidated request this response answers
|
|
890
|
+
* @returns the normalized quote, or null when the payload does not match that ecosystem
|
|
891
|
+
* @throws {LifiExecutionRefusedError} for an Ethereum origin, when `request`
|
|
892
|
+
* is absent or the execution transaction disagrees with it
|
|
893
|
+
*/
|
|
894
|
+
const parseLifiQuote = (payload, { originEcosystem, request }) => {
|
|
895
|
+
const envelope = parseLifiQuoteEnvelope(payload);
|
|
896
|
+
if (envelope === null) {
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
const { transactionRequest, fromChainId, ...envelopeFields } = envelope;
|
|
900
|
+
const quote = { ...envelopeFields, ...lifiDeliveredAmount(payload) };
|
|
901
|
+
if (originEcosystem === 'evm') {
|
|
902
|
+
const originDeposit = lifiEvmDeposit(transactionRequest, envelope.approvalAddress, request);
|
|
903
|
+
return originDeposit === null ? null : { ...quote, originDeposit };
|
|
904
|
+
}
|
|
905
|
+
if (fromChainId === null) {
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
908
|
+
const originDeposit = originEcosystem === 'svm'
|
|
909
|
+
? lifiSolanaDeposit(transactionRequest, fromChainId)
|
|
910
|
+
: originEcosystem === 'bvm'
|
|
911
|
+
? lifiBitcoinDeposit(transactionRequest, fromChainId)
|
|
912
|
+
: null;
|
|
913
|
+
if (originDeposit === null) {
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
return { ...quote, originDeposit, approvalAddress: null };
|
|
917
|
+
};
|
|
918
|
+
exports.parseLifiQuote = parseLifiQuote;
|
|
919
|
+
const consolidatedStatusValues = ['NOT_FOUND', 'INVALID', 'PENDING', 'DONE', 'FAILED'];
|
|
920
|
+
/**
|
|
921
|
+
* Builds the status-poll address for an origin transaction, at
|
|
922
|
+
* {@link lifiDefaultApiOrigin}. Chain hints narrow the provider's lookup and
|
|
923
|
+
* are recommended when known. `txHash` is typed as `string` rather than `Hex`
|
|
924
|
+
* because a non-Ethereum origin (Solana base58 signature, Bitcoin transaction
|
|
925
|
+
* id) is not `0x`-prefixed.
|
|
926
|
+
*/
|
|
927
|
+
const lifiStatusUrl = ({ txHash, fromChainId, toChainId, }) => {
|
|
928
|
+
return defaultLifiEndpoints.status({ txHash, fromChainId, toChainId });
|
|
929
|
+
};
|
|
930
|
+
exports.lifiStatusUrl = lifiStatusUrl;
|
|
931
|
+
const isTransactionHash = (value) => typeof value === 'string' && /^0x[0-9a-fA-F]{64}$/.test(value);
|
|
932
|
+
/**
|
|
933
|
+
* Normalizes a status response for an in-flight consolidated bridge.
|
|
934
|
+
*
|
|
935
|
+
* The receiving hash gates the handover from provider polling to the
|
|
936
|
+
* omnibridge indexer, so it must be a full 32-byte transaction hash — an
|
|
937
|
+
* empty or malformed value collapses to null rather than masquerading as a
|
|
938
|
+
* real transaction.
|
|
939
|
+
*
|
|
940
|
+
* @returns the normalized status, or null when the payload shape is unexpected
|
|
941
|
+
*/
|
|
942
|
+
const parseLifiStatus = (payload) => {
|
|
943
|
+
if (typeof payload !== 'object' || payload === null) {
|
|
944
|
+
return null;
|
|
945
|
+
}
|
|
946
|
+
const data = payload;
|
|
947
|
+
const status = consolidatedStatusValues.find((value) => value === data.status);
|
|
948
|
+
if (!status) {
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
const receiving = data.receiving;
|
|
952
|
+
return {
|
|
953
|
+
status,
|
|
954
|
+
substatus: typeof data.substatus === 'string' ? data.substatus : null,
|
|
955
|
+
receivingTxHash: isTransactionHash(receiving?.txHash) ? receiving.txHash : null,
|
|
956
|
+
};
|
|
957
|
+
};
|
|
958
|
+
exports.parseLifiStatus = parseLifiStatus;
|
|
959
|
+
/**
|
|
960
|
+
* Chain listing endpoint, at {@link lifiDefaultApiOrigin}. Deliberately
|
|
961
|
+
* carries no query parameters: the `chainTypes` filter requires an API key on
|
|
962
|
+
* the anonymous tier (verified live 2026-06-12, error code 1010), and the
|
|
963
|
+
* default response is EVM-only.
|
|
964
|
+
*/
|
|
965
|
+
exports.lifiChainsUrl = defaultLifiEndpoints.chains;
|
|
966
|
+
/**
|
|
967
|
+
* Normalizes a `GET /v1/chains` response body to mainnet EVM chains.
|
|
968
|
+
*
|
|
969
|
+
* @returns the chain list, or null when the payload shape is unexpected
|
|
970
|
+
*/
|
|
971
|
+
const parseLifiChains = (payload) => {
|
|
972
|
+
if (typeof payload !== 'object' || payload === null) {
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
const chains = payload.chains;
|
|
976
|
+
if (!Array.isArray(chains)) {
|
|
977
|
+
return null;
|
|
978
|
+
}
|
|
979
|
+
const parsed = [];
|
|
980
|
+
for (const chain of chains) {
|
|
981
|
+
if (typeof chain !== 'object' || chain === null) {
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
const entry = chain;
|
|
985
|
+
if (entry.chainType !== 'EVM' || entry.mainnet !== true) {
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
if (typeof entry.id !== 'number' || typeof entry.name !== 'string') {
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
parsed.push({
|
|
992
|
+
id: entry.id,
|
|
993
|
+
name: entry.name,
|
|
994
|
+
logoURI: typeof entry.logoURI === 'string' ? entry.logoURI : null,
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
return parsed;
|
|
998
|
+
};
|
|
999
|
+
exports.parseLifiChains = parseLifiChains;
|
|
1000
|
+
/** Builds the per-chain token listing URL, at {@link lifiDefaultApiOrigin}. */
|
|
1001
|
+
exports.lifiTokensUrl = defaultLifiEndpoints.tokens;
|
|
1002
|
+
/**
|
|
1003
|
+
* Normalizes a `GET /v1/tokens?chains=<id>` response body into the shared
|
|
1004
|
+
* token shape used across the bridge frontend. Malformed entries are skipped
|
|
1005
|
+
* rather than failing the whole list; `logoURI` is optional on the live
|
|
1006
|
+
* interface.
|
|
1007
|
+
*
|
|
1008
|
+
* @returns the token list, or null when the payload shape is unexpected
|
|
1009
|
+
*/
|
|
1010
|
+
const parseLifiTokens = (payload, chainId) => {
|
|
1011
|
+
if (typeof payload !== 'object' || payload === null) {
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
const tokensByChain = payload.tokens;
|
|
1015
|
+
if (typeof tokensByChain !== 'object' || tokensByChain === null) {
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
const entries = tokensByChain[chainId.toString()];
|
|
1019
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
const parsed = [];
|
|
1023
|
+
for (const item of entries) {
|
|
1024
|
+
if (typeof item !== 'object' || item === null) {
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
const entry = item;
|
|
1028
|
+
if (!isHexString(entry.address) ||
|
|
1029
|
+
typeof entry.symbol !== 'string' ||
|
|
1030
|
+
typeof entry.name !== 'string' ||
|
|
1031
|
+
typeof entry.decimals !== 'number') {
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
parsed.push({
|
|
1035
|
+
chainId,
|
|
1036
|
+
address: entry.address,
|
|
1037
|
+
symbol: entry.symbol,
|
|
1038
|
+
name: entry.name,
|
|
1039
|
+
decimals: entry.decimals,
|
|
1040
|
+
logoURI: typeof entry.logoURI === 'string' ? entry.logoURI : null,
|
|
1041
|
+
// Preserve the inline spot price the LI.FI list carries — it is the USD
|
|
1042
|
+
// price source for aggregator chains (the dapp image lists carry none).
|
|
1043
|
+
priceUSD: typeof entry.priceUSD === 'string' ? entry.priceUSD : null,
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
return parsed;
|
|
1047
|
+
};
|
|
1048
|
+
exports.parseLifiTokens = parseLifiTokens;
|