@gibs/bridge-sdk 1.12.0 → 1.13.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.
Files changed (117) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +234 -432
  3. package/dist/abis.d.ts +1 -0
  4. package/dist/bridge-limits.d.ts +103 -0
  5. package/dist/bridge-limits.js +84 -0
  6. package/dist/chain-info.d.ts +169 -0
  7. package/dist/chain-info.js +410 -0
  8. package/dist/chain-names.d.ts +160 -0
  9. package/dist/chain-names.js +200 -0
  10. package/dist/chains.d.ts +65 -0
  11. package/dist/chains.js +284 -0
  12. package/dist/config.d.ts +216 -0
  13. package/dist/config.js +493 -0
  14. package/dist/consolidated-quote.d.ts +554 -0
  15. package/dist/consolidated-quote.js +773 -0
  16. package/dist/destination-calldata.d.ts +118 -0
  17. package/dist/destination-calldata.js +152 -0
  18. package/dist/ecosystems.d.ts +217 -0
  19. package/dist/ecosystems.js +288 -0
  20. package/dist/fee-type.d.ts +10 -0
  21. package/dist/fee-type.js +9 -0
  22. package/dist/hyperliquid-execution.d.ts +152 -0
  23. package/dist/hyperliquid-execution.js +168 -0
  24. package/dist/image-links.d.ts +117 -0
  25. package/dist/image-links.js +142 -0
  26. package/dist/index.d.ts +20 -0
  27. package/dist/index.js +20 -0
  28. package/dist/lifi-execution-guard.d.ts +199 -0
  29. package/dist/lifi-execution-guard.js +400 -0
  30. package/dist/near-intents-conformance.d.ts +85 -0
  31. package/dist/near-intents-conformance.js +329 -0
  32. package/dist/near-intents-route.d.ts +47 -0
  33. package/dist/near-intents-route.js +265 -0
  34. package/dist/near-intents.d.ts +524 -0
  35. package/dist/near-intents.js +1055 -0
  36. package/dist/origin-deposit.d.ts +326 -0
  37. package/dist/origin-deposit.js +68 -0
  38. package/dist/pre-quote.d.ts +198 -0
  39. package/dist/pre-quote.js +175 -0
  40. package/dist/prices.d.ts +107 -0
  41. package/dist/prices.js +162 -0
  42. package/dist/providers.d.ts +221 -0
  43. package/dist/providers.js +157 -0
  44. package/dist/quote-fees.d.ts +102 -0
  45. package/dist/quote-fees.js +195 -0
  46. package/dist/referral.d.ts +77 -0
  47. package/dist/referral.js +72 -0
  48. package/dist/relay-currencies.d.ts +47 -0
  49. package/dist/relay-currencies.js +89 -0
  50. package/dist/relay-quote.d.ts +301 -0
  51. package/dist/relay-quote.js +983 -0
  52. package/dist/route-steps.d.ts +138 -0
  53. package/dist/route-steps.js +305 -0
  54. package/dist/routing/amounts.d.ts +80 -0
  55. package/dist/routing/amounts.js +120 -0
  56. package/dist/routing/candidate-route.d.ts +43 -0
  57. package/dist/routing/candidate-route.js +45 -0
  58. package/dist/routing/candidate.d.ts +237 -0
  59. package/dist/routing/candidate.js +96 -0
  60. package/dist/routing/carriers/ask-answer.d.ts +58 -0
  61. package/dist/routing/carriers/ask-answer.js +1 -0
  62. package/dist/routing/carriers/omnibridge.d.ts +137 -0
  63. package/dist/routing/carriers/omnibridge.js +419 -0
  64. package/dist/routing/carriers/pulsex.d.ts +111 -0
  65. package/dist/routing/carriers/pulsex.js +404 -0
  66. package/dist/routing/carriers/wrap.d.ts +25 -0
  67. package/dist/routing/carriers/wrap.js +115 -0
  68. package/dist/routing/composed-route-quality.d.ts +103 -0
  69. package/dist/routing/composed-route-quality.js +108 -0
  70. package/dist/routing/entry-chain-anchor.d.ts +88 -0
  71. package/dist/routing/entry-chain-anchor.js +55 -0
  72. package/dist/routing/format.d.ts +31 -0
  73. package/dist/routing/format.js +58 -0
  74. package/dist/routing/index.d.ts +34 -0
  75. package/dist/routing/index.js +42 -0
  76. package/dist/routing/producers.d.ts +770 -0
  77. package/dist/routing/producers.js +1564 -0
  78. package/dist/routing/providers.d.ts +26 -0
  79. package/dist/routing/providers.js +38 -0
  80. package/dist/routing/route-data.d.ts +355 -0
  81. package/dist/routing/route-data.js +1 -0
  82. package/dist/routing/search/compose.d.ts +168 -0
  83. package/dist/routing/search/compose.js +98 -0
  84. package/dist/routing/search/graph.d.ts +639 -0
  85. package/dist/routing/search/graph.js +445 -0
  86. package/dist/routing/search/plan-route-data.d.ts +244 -0
  87. package/dist/routing/search/plan-route-data.js +767 -0
  88. package/dist/routing/search/score.d.ts +324 -0
  89. package/dist/routing/search/score.js +400 -0
  90. package/dist/routing/usd-value.d.ts +17 -0
  91. package/dist/routing/usd-value.js +25 -0
  92. package/dist/settings.d.ts +692 -0
  93. package/dist/settings.js +909 -0
  94. package/dist/ton-execution.d.ts +117 -0
  95. package/dist/ton-execution.js +247 -0
  96. package/dist/tron-execution.d.ts +146 -0
  97. package/dist/tron-execution.js +239 -0
  98. package/dist/types.d.ts +51 -0
  99. package/dist/types.js +1 -0
  100. package/package.json +203 -23
  101. package/src/chain-info.test.ts +0 -968
  102. package/src/chain-info.ts +0 -498
  103. package/src/chains.test.ts +0 -123
  104. package/src/chains.ts +0 -88
  105. package/src/config.test.ts +0 -679
  106. package/src/config.ts +0 -500
  107. package/src/fee-type.test.ts +0 -45
  108. package/src/fee-type.ts +0 -14
  109. package/src/image-links.test.ts +0 -160
  110. package/src/image-links.ts +0 -65
  111. package/src/index.ts +0 -8
  112. package/src/settings.test.ts +0 -2754
  113. package/src/settings.ts +0 -875
  114. package/src/types.ts +0 -48
  115. package/tsconfig.json +0 -17
  116. package/vitest.config.ts +0 -13
  117. /package/{src/abis.ts → dist/abis.js} +0 -0
@@ -0,0 +1,773 @@
1
+ import { Chains } from './config.js';
2
+ import { LifiExecutionRefusedError, verifyLifiEvmExecution, } from './lifi-execution-guard.js';
3
+ import { depositFamilyOf, isEvmDeposit, } from './origin-deposit.js';
4
+ import { lifiQuoteFees } from './quote-fees.js';
5
+ import { referralIntegrator, resolveAttribution } from './referral.js';
6
+ import { normalizeLifiSteps } from './route-steps.js';
7
+ /**
8
+ * The LI.FI `integrator` sent when a request leaves
9
+ * {@link ConsolidatedQuoteRequest.lifiIntegrator} out.
10
+ *
11
+ * KEPT AS A DEFAULT BECAUSE IT COSTS THE CALLER NOTHING. LI.FI's `integrator`
12
+ * is an optional label: it changes neither the quote nor the rate limit, and it
13
+ * moves no money unless a `fee` rides with it. A fee is the one case where the
14
+ * label decides who gets paid, so a request that charges a fee must name its
15
+ * own integrator — see {@link lifiAttribution}.
16
+ */
17
+ export const lifiIntegrator = referralIntegrator;
18
+ /** Referral fee charged by this integrator on LI.FI routes (0.001 = 0.1%). */
19
+ export const lifiReferralFeePercent = 0.001;
20
+ /**
21
+ * LI.FI's HTTP origin when the interface's `PUBLIC_LIFI_API_URL` build flag is
22
+ * unset. Points straight at LI.FI rather than through the server-side proxy,
23
+ * so local development and the anonymous IPFS build still get real quotes —
24
+ * at LI.FI's shared, unauthenticated tier — with no configuration at all.
25
+ */
26
+ export const lifiDefaultApiOrigin = 'https://li.quest';
27
+ const isAbsoluteHttpUrl = (value) => {
28
+ try {
29
+ const url = new URL(value);
30
+ return url.protocol === 'http:' || url.protocol === 'https:';
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ };
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
+ export const lifiEndpoints = (apiOrigin) => {
51
+ if (!isAbsoluteHttpUrl(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) => {
60
+ const url = new URL(`${base}/tokens`);
61
+ url.searchParams.set('chains', chainId.toString());
62
+ return url.toString();
63
+ },
64
+ status: ({ txHash, fromChainId, toChainId }) => {
65
+ const url = new URL(`${base}/status`);
66
+ url.searchParams.set('txHash', txHash);
67
+ if (fromChainId !== undefined) {
68
+ url.searchParams.set('fromChain', fromChainId.toString());
69
+ }
70
+ if (toChainId !== undefined) {
71
+ url.searchParams.set('toChain', toChainId.toString());
72
+ }
73
+ return url.toString();
74
+ },
75
+ });
76
+ };
77
+ const defaultLifiEndpoints = lifiEndpoints(lifiDefaultApiOrigin);
78
+ /** Quote endpoint that accepts destination contract calls, at {@link lifiDefaultApiOrigin}. */
79
+ export const lifiContractCallsQuoteUrl = defaultLifiEndpoints.contractCallsQuote;
80
+ /**
81
+ * The plain quote endpoint, at {@link lifiDefaultApiOrigin}: carry funds and stop.
82
+ *
83
+ * WHICH ENDPOINT A ROUTE USES IS DECIDED BY WHETHER ANYTHING RUNS ON ARRIVAL,
84
+ * not by which chains the provider covers. A crossing into the omnibridge needs
85
+ * a call made on the entry chain, so it goes to
86
+ * {@link lifiContractCallsQuoteUrl} and arrives as one signature. A route the
87
+ * omnibridge does not touch has nothing to run on arrival, so it asks here.
88
+ *
89
+ * A GET, unlike its sibling — the parameters are a query string.
90
+ */
91
+ export const lifiQuoteUrl = defaultLifiEndpoints.quote;
92
+ /**
93
+ * Builds the headers for a LI.FI request.
94
+ *
95
+ * CARRIES NO API KEY. LI.FI's own documentation forbids setting
96
+ * `x-lifi-api-key` in a browser — "Never expose your x-lifi-api-key in
97
+ * client-side environments such as browser-based JavaScript" — so this
98
+ * codebase never attaches one here. The key lives on the server-side proxy
99
+ * (`https://lifi.gibs.finance`) that fronts `lifiApiOrigin`, added to the
100
+ * outbound request there, never in this bundle. LI.FI answers every endpoint
101
+ * with no key at a lower, shared rate limit, so a request built from this is a
102
+ * normal operating state, not a degraded one.
103
+ *
104
+ * ONE HELPER FOR EVERY LI.FI ENDPOINT — the plain quote, the contract-calls
105
+ * quote, `/v1/chains`, `/v1/tokens`, and `/v1/status` all build their headers
106
+ * through this, rather than each call site duplicating the content-type.
107
+ */
108
+ export const lifiRequestHeaders = () => ({
109
+ 'Content-Type': 'application/json',
110
+ });
111
+ /**
112
+ * Whether a consolidated quote request originates on a non-Ethereum-Virtual-
113
+ * Machine chain. A missing `originVmType` is treated as Ethereum Virtual
114
+ * Machine, so existing callers are never classified non-Ethereum.
115
+ *
116
+ * Deliberately expressed as "anything that is not Ethereum" rather than as a
117
+ * list of the non-Ethereum families. The two behaviors this gates — excluding a
118
+ * route from LI.FI, and refunding on the ORIGIN chain instead of to an Ethereum
119
+ * fallback address — are both wrong by default for an unlisted ecosystem, and a
120
+ * positive list fails open: a newly registered family would be handed to LI.FI
121
+ * with a non-Ethereum address cast to `Hex`, and would name an Ethereum address
122
+ * as its origin-chain refund target. Inverting the test makes every future
123
+ * ecosystem safe on arrival.
124
+ *
125
+ * Hyperliquid counts as non-Ethereum here despite using Ethereum-style account
126
+ * addresses: Relay's origin for it is HyperCore, whose deposit is a signed
127
+ * typed-data action posted to an API rather than a broadcast transaction, and
128
+ * which LI.FI does not carry.
129
+ *
130
+ * @param request - the consolidated quote request to classify
131
+ * @returns true when the origin is any ecosystem other than Ethereum
132
+ */
133
+ export const isNonEthereumVirtualMachineOrigin = (request) => (request.originVmType ?? 'evm') !== 'evm';
134
+ /**
135
+ * The attribution fields of a LI.FI request: the `integrator` label and, when
136
+ * one is charged, the referral `fee`. Shared by both LI.FI builders so the two
137
+ * endpoints can never attribute or charge differently.
138
+ *
139
+ * @param request - the consolidated request's attribution fields
140
+ * @returns the fields to merge into the request; empty when none apply
141
+ * @throws when a positive `lifiFee` arrives without an explicit `lifiIntegrator`
142
+ * string — that fee would otherwise be paid to the default integrator
143
+ */
144
+ export const lifiAttribution = (request) => {
145
+ const integrator = resolveAttribution(request.lifiIntegrator, lifiIntegrator);
146
+ const chargesFee = request.lifiFee !== undefined && request.lifiFee > 0;
147
+ if (!chargesFee) {
148
+ return integrator === undefined ? {} : { integrator };
149
+ }
150
+ if (typeof request.lifiIntegrator !== 'string') {
151
+ throw new Error('lifiFee needs an explicit lifiIntegrator: LI.FI pays the fee to the integrator label, and the default label is not yours');
152
+ }
153
+ return { integrator: request.lifiIntegrator, fee: request.lifiFee };
154
+ };
155
+ /**
156
+ * Builds the LI.FI contract-calls quote request from a provider-agnostic
157
+ * consolidated request. LI.FI quotes backwards from `toAmount` (the guaranteed
158
+ * minimum arriving on Ethereum), which is what makes the fixed amount inside
159
+ * the destination calldata safe.
160
+ *
161
+ * @returns the request body, or null when the inputs cannot form a valid call
162
+ */
163
+ /**
164
+ * Builds the query string for LI.FI's plain `GET /v1/quote`.
165
+ *
166
+ * THE ROUTE THIS EXISTS FOR. A crossing with no omnibridge leg had exactly one
167
+ * bidder — Relay — because our only LI.FI endpoint was the contract-calls one,
168
+ * which cannot express a route that calls nothing. Every any-to-any quote was
169
+ * therefore a price with nothing to rank it against.
170
+ *
171
+ * SOLVED FORWARDS, WHICH IS THE OPPOSITE OF ITS SIBLING. An omnibridge route is
172
+ * asked for an exact OUTPUT, because the entry call must receive an exact
173
+ * amount. Here the reader names what they spend and the delivered amount is
174
+ * whatever the market gives, so `fromAmount` is the input and the answer's
175
+ * `toAmount` is the thing that differs between providers. Ranking two forward
176
+ * quotes on what they cost compares two identical numbers — see
177
+ * `orderQuotesByCost` in the interface, which ranks these on what ARRIVES.
178
+ *
179
+ * Returns null for a request carrying a `destinationCall`: that route belongs
180
+ * to the other endpoint, and sending it here would quote a crossing that stops
181
+ * on the entry chain and never enters the bridge.
182
+ *
183
+ * @param request - the consolidated request to express
184
+ * @returns the query string, or null when this endpoint cannot express it
185
+ * @throws when `lifiFee` is positive and `lifiIntegrator` is not a string — see {@link lifiAttribution}
186
+ */
187
+ export const buildLifiQuoteRequest = (request) => {
188
+ const { destinationCall, fromAmount, toChainId } = request;
189
+ if (destinationCall !== undefined) {
190
+ return null;
191
+ }
192
+ if (fromAmount === undefined || fromAmount <= 0n) {
193
+ return null;
194
+ }
195
+ if (toChainId === undefined) {
196
+ return null;
197
+ }
198
+ // LI.FI quotes Ethereum-Virtual-Machine origins only, and a non-Ethereum
199
+ // origin address would be rejected as malformed rather than declined.
200
+ if ((request.originVmType ?? 'evm') !== 'evm') {
201
+ return null;
202
+ }
203
+ const params = new URLSearchParams({
204
+ fromChain: request.fromChainId.toString(),
205
+ toChain: toChainId.toString(),
206
+ fromToken: request.fromTokenAddress,
207
+ toToken: request.toTokenAddress,
208
+ fromAmount: fromAmount.toString(),
209
+ fromAddress: request.fromAddress,
210
+ // WHERE THE FUNDS LAND, stated separately from who is paying. They are the
211
+ // same account for an ordinary transfer and are NOT when a reader has named
212
+ // a delivery address, which is exactly the case a bridge has to get right.
213
+ toAddress: request.recipient,
214
+ });
215
+ // Same attribution as the contract-calls builder, so a fee the reader agreed
216
+ // to is charged on both endpoints, not only on one.
217
+ const { integrator, fee } = lifiAttribution(request);
218
+ if (integrator !== undefined) {
219
+ params.set('integrator', integrator);
220
+ }
221
+ if (fee !== undefined) {
222
+ params.set('fee', fee.toString());
223
+ }
224
+ if (request.slippage !== undefined) {
225
+ params.set('slippage', request.slippage.toString());
226
+ }
227
+ return params.toString();
228
+ };
229
+ export const buildLifiContractCallsRequest = (request) => {
230
+ const { destinationCall, toAmount } = request;
231
+ // THIS ENDPOINT EXISTS TO DELIVER AND THEN CALL SOMETHING. A route with no
232
+ // omnibridge leg has nothing to call, so it is not a request this builder can
233
+ // express — LI.FI's plain quote is the right endpoint for that shape, and
234
+ // wiring it is separate work. Refusing here leaves the route to Relay rather
235
+ // than sending LI.FI a contract-call request with no contract call in it.
236
+ if (destinationCall === undefined) {
237
+ return null;
238
+ }
239
+ if (toAmount <= 0n) {
240
+ return null;
241
+ }
242
+ // contract-call entries carry no native value; native-entry calls
243
+ // (wrapAndRelayTokens) stay out of scope until value support is verified
244
+ if (destinationCall.value > 0n) {
245
+ return null;
246
+ }
247
+ // LI.FI cannot bridge across virtual-machine ecosystems. The provider gate
248
+ // (`providerSupportsOrigin`) already keeps non-Ethereum origins away from
249
+ // LI.FI; this guard makes the boundary explicit so the origin fields below are
250
+ // genuinely Ethereum `Hex` addresses, not merely the widened `string` shape.
251
+ if (isNonEthereumVirtualMachineOrigin(request)) {
252
+ return null;
253
+ }
254
+ return {
255
+ fromChain: request.fromChainId,
256
+ fromToken: request.fromTokenAddress,
257
+ fromAddress: request.fromAddress,
258
+ toChain: request.toChainId ?? Number(Chains.ETH),
259
+ toToken: request.toTokenAddress,
260
+ toAmount: toAmount.toString(),
261
+ toFallbackAddress: request.fallbackAddress,
262
+ ...lifiAttribution(request),
263
+ ...(request.slippage === undefined ? {} : { slippage: request.slippage }),
264
+ contractCalls: [
265
+ {
266
+ fromAmount: toAmount.toString(),
267
+ fromTokenAddress: request.toTokenAddress,
268
+ toContractAddress: destinationCall.to,
269
+ toContractCallData: destinationCall.data,
270
+ toContractGasLimit: destinationCall.gasLimit.toString(),
271
+ ...(destinationCall.approvalTarget === null
272
+ ? {}
273
+ : { toApprovalAddress: destinationCall.approvalTarget }),
274
+ },
275
+ ],
276
+ };
277
+ };
278
+ /**
279
+ * Narrows a quote's origin deposit to the Ethereum-Virtual-Machine variant.
280
+ * Throws when the deposit is not Ethereum, because the Ethereum-only execute
281
+ * path must never silently proceed on a Solana or Bitcoin payload.
282
+ */
283
+ export const evmDepositOf = (quote) => {
284
+ if (!isEvmDeposit(quote.originDeposit)) {
285
+ throw new Error(`expected an Ethereum-Virtual-Machine origin deposit, received "${depositFamilyOf(quote.originDeposit)}"`);
286
+ }
287
+ return quote.originDeposit;
288
+ };
289
+ const isHexString = (value) => typeof value === 'string' && value.startsWith('0x');
290
+ /**
291
+ * Parses the ecosystem-independent part of a LI.FI quote.
292
+ *
293
+ * @param payload - the raw JSON body from either LI.FI quote endpoint
294
+ * @returns the envelope, or null when a required field is missing or malformed
295
+ */
296
+ const parseLifiQuoteEnvelope = (payload) => {
297
+ if (typeof payload !== 'object' || payload === null) {
298
+ return null;
299
+ }
300
+ const quote = payload;
301
+ const estimate = quote.estimate;
302
+ const action = quote.action;
303
+ const transactionRequest = quote.transactionRequest;
304
+ const toolDetails = quote.toolDetails;
305
+ if (typeof quote.id !== 'string' || typeof quote.tool !== 'string') {
306
+ return null;
307
+ }
308
+ if (!estimate || typeof estimate.fromAmount !== 'string') {
309
+ return null;
310
+ }
311
+ if (typeof estimate.executionDuration !== 'number') {
312
+ return null;
313
+ }
314
+ if (!transactionRequest) {
315
+ return null;
316
+ }
317
+ try {
318
+ return {
319
+ provider: 'lifi',
320
+ quoteId: quote.id,
321
+ tool: quote.tool,
322
+ toolName: typeof toolDetails?.name === 'string' ? toolDetails.name : quote.tool,
323
+ fromAmount: BigInt(estimate.fromAmount),
324
+ fromAmountUsd: typeof estimate.fromAmountUSD === 'string' ? estimate.fromAmountUSD : null,
325
+ approvalAddress: isHexString(estimate.approvalAddress) ? estimate.approvalAddress : null,
326
+ executionDurationSeconds: estimate.executionDuration,
327
+ steps: normalizeLifiSteps(quote.includedSteps),
328
+ fees: lifiQuoteFees(payload),
329
+ transactionRequest,
330
+ fromChainId: typeof action?.fromChainId === 'number' ? action.fromChainId : null,
331
+ };
332
+ }
333
+ catch {
334
+ // a BigInt conversion failed on a malformed numeric string
335
+ return null;
336
+ }
337
+ };
338
+ /**
339
+ * Normalizes a quote response from the contract-calls endpoint.
340
+ *
341
+ * A present but malformed `transactionRequest.value` fails the parse — silently
342
+ * sending zero native value would produce a transaction that under-pays the
343
+ * provider's bridging fee.
344
+ *
345
+ * REQUIRES `context.request` — see {@link lifiEvmDeposit}. Every live caller
346
+ * (`providers.ts`'s registry entry, the interface's `consolidated-quote.ts`
347
+ * store) supplies it; a caller that omits it gets a thrown
348
+ * {@link LifiExecutionRefusedError} rather than a silently unchecked
349
+ * execution transaction.
350
+ *
351
+ * @returns the normalized quote, or null when the payload shape is unexpected
352
+ * @throws {LifiExecutionRefusedError} when the response's execution
353
+ * transaction disagrees with `context.request` — see `lifi-execution-guard.ts`
354
+ */
355
+ export const parseLifiContractCallsQuote = (payload, context) => {
356
+ const envelope = parseLifiQuoteEnvelope(payload);
357
+ if (envelope === null) {
358
+ return null;
359
+ }
360
+ const originDeposit = lifiEvmDeposit(envelope.transactionRequest, envelope.approvalAddress, context?.request);
361
+ if (originDeposit === null) {
362
+ return null;
363
+ }
364
+ const { transactionRequest: _request, fromChainId: _fromChainId, ...quote } = envelope;
365
+ return { ...quote, originDeposit, fees: lifiQuoteFees(payload) };
366
+ };
367
+ /**
368
+ * Builds what THIS APPLICATION expects a LI.FI execution transaction to
369
+ * contain, from the request that produced it — the only thing
370
+ * {@link verifyLifiEvmExecution} is allowed to check a response against.
371
+ *
372
+ * `receiver` and `minimumAmount` differ by whether `destinationCall` is
373
+ * present, because the two LI.FI endpoints populate `BridgeData.receiver`
374
+ * with DIFFERENT fields and only one endpoint accepts a requested minimum at
375
+ * all — see the field-semantics note atop `lifi-execution-guard.ts`.
376
+ */
377
+ const lifiExecutionExpectation = (request) => {
378
+ const hasDestinationCall = request.destinationCall !== undefined;
379
+ return {
380
+ receiver: hasDestinationCall ? request.fallbackAddress : request.recipient,
381
+ destinationChainId: request.toChainId ?? Number(Chains.ETH),
382
+ sendingAssetId: request.fromTokenAddress,
383
+ hasDestinationCall,
384
+ minimumAmount: hasDestinationCall ? request.toAmount : null,
385
+ };
386
+ };
387
+ /**
388
+ * Reads LI.FI's Ethereum `transactionRequest` into an Ethereum origin deposit.
389
+ *
390
+ * A present but malformed `value` fails the parse — silently sending zero
391
+ * native value would produce a transaction that under-pays the provider's
392
+ * bridging fee.
393
+ *
394
+ * VERIFIES THE TRANSACTION BEFORE RETURNING IT. `lifiApi.quote` and
395
+ * `lifiApi.contractCallsQuote` are fetched through a configurable, possibly
396
+ * compromised host — see `lifi-execution-guard.ts`'s top-of-file doc comment
397
+ * for the full threat model. `request` is REQUIRED, not optional: a caller
398
+ * with no request to check against gets a thrown
399
+ * {@link LifiExecutionRefusedError} (`missing-request-context`) rather than
400
+ * an origin deposit nothing has verified. This is the one gap a type
401
+ * signature alone could not close — `ProviderQuoteContext.request` is
402
+ * optional on every OTHER provider, so this function still accepts
403
+ * `undefined` in its parameter type and refuses at the call boundary
404
+ * instead, keeping it assignable to `ProviderDescriptor.executionQuote.parse`.
405
+ *
406
+ * @throws {LifiExecutionRefusedError} when `request` is absent, or when the
407
+ * transaction disagrees with it
408
+ */
409
+ const lifiEvmDeposit = (transactionRequest, approvalAddress, request) => {
410
+ if (!isHexString(transactionRequest.to) ||
411
+ !isHexString(transactionRequest.data) ||
412
+ typeof transactionRequest.chainId !== 'number') {
413
+ return null;
414
+ }
415
+ if (transactionRequest.value !== undefined && !isHexString(transactionRequest.value)) {
416
+ return null;
417
+ }
418
+ if (request === undefined) {
419
+ throw new LifiExecutionRefusedError({
420
+ reason: 'missing-request-context',
421
+ detail: 'the LI.FI response parser was not given the request it is answering, so the execution transaction could not be verified',
422
+ });
423
+ }
424
+ const value = isHexString(transactionRequest.value) ? BigInt(transactionRequest.value) : 0n;
425
+ verifyLifiEvmExecution({
426
+ to: transactionRequest.to,
427
+ data: transactionRequest.data,
428
+ value,
429
+ chainId: transactionRequest.chainId,
430
+ }, approvalAddress, lifiExecutionExpectation(request));
431
+ return {
432
+ ecosystem: 'evm',
433
+ to: transactionRequest.to,
434
+ data: transactionRequest.data,
435
+ value,
436
+ chainId: transactionRequest.chainId,
437
+ gasLimit: isHexString(transactionRequest.gasLimit) ? BigInt(transactionRequest.gasLimit) : null,
438
+ };
439
+ };
440
+ // A partially-signed Bitcoin transaction always begins with the five-byte magic
441
+ // "psbt\xff". Checking it keeps a base64 payload, or an Ethereum `0x` string,
442
+ // from being handed to a Bitcoin wallet as though it were a transaction.
443
+ const partiallySignedBitcoinMagic = '70736274ff';
444
+ /**
445
+ * Reads LI.FI's Bitcoin `transactionRequest` into a Bitcoin origin deposit.
446
+ *
447
+ * LI.FI and Relay happen to agree on the encoding here — an unprefixed
448
+ * hexadecimal partially-signed transaction — so this lands on the same
449
+ * {@link BitcoinDeposit} the Relay parser produces and needs no signer change.
450
+ */
451
+ const lifiBitcoinDeposit = (transactionRequest, chainId) => {
452
+ const { to, data, value } = transactionRequest;
453
+ if (typeof to !== 'string' || to.length === 0) {
454
+ return null;
455
+ }
456
+ if (typeof data !== 'string' || !data.toLowerCase().startsWith(partiallySignedBitcoinMagic)) {
457
+ return null;
458
+ }
459
+ if (typeof value !== 'string' || !/^[0-9]+$/.test(value)) {
460
+ return null;
461
+ }
462
+ return { ecosystem: 'bvm', psbt: data, depositAddress: to, amountSats: BigInt(value), chainId };
463
+ };
464
+ // Base64 without whitespace. Necessary but nowhere near sufficient on its own:
465
+ // the hexadecimal alphabet is a SUBSET of base64's, so a Bitcoin
466
+ // partially-signed transaction matches this pattern happily.
467
+ const base64Pattern = /^[A-Za-z0-9+/]+={0,2}$/;
468
+ // A single-byte compact-u16 tops out below this, and no real transaction needs
469
+ // anything near this many signatures.
470
+ const maxPlausibleSignatureCount = 19;
471
+ const solanaSignatureByteLength = 64;
472
+ // A versioned message's first byte sets the high bit; version 0 is the only one
473
+ // that exists, so the byte is exactly 0x80.
474
+ const solanaVersionZeroPrefix = 0x80;
475
+ /**
476
+ * Checks that a base64 payload really is a serialized Solana transaction, by
477
+ * walking its wire layout: a compact-u16 signature count, that many 64-byte
478
+ * signatures, then a message whose leading byte is either the version-0 marker
479
+ * or (for a legacy message) the same signature count again.
480
+ *
481
+ * An alphabet check alone is not enough. Hexadecimal is a subset of base64, so
482
+ * a Bitcoin partially-signed transaction passes a pure pattern test and would
483
+ * be handed to a Solana wallet as a transaction.
484
+ */
485
+ const isSerializedSolanaTransaction = (value) => {
486
+ if (!base64Pattern.test(value)) {
487
+ return false;
488
+ }
489
+ let bytes;
490
+ try {
491
+ const binary = atob(value);
492
+ bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
493
+ }
494
+ catch {
495
+ return false;
496
+ }
497
+ const signatureCount = bytes[0];
498
+ if (signatureCount === undefined ||
499
+ signatureCount < 1 ||
500
+ signatureCount > maxPlausibleSignatureCount) {
501
+ return false;
502
+ }
503
+ const messageStart = 1 + signatureCount * solanaSignatureByteLength;
504
+ const header = bytes[messageStart];
505
+ if (header === undefined) {
506
+ return false;
507
+ }
508
+ return header === solanaVersionZeroPrefix || header === signatureCount;
509
+ };
510
+ /**
511
+ * Reads LI.FI's Solana `transactionRequest` into a Solana origin deposit.
512
+ *
513
+ * The whole payload is a single base64 compiled transaction under `data`, with
514
+ * no `to`, `value` or `chainId` beside it, so the chain id comes from the
515
+ * quote's action rather than the request.
516
+ */
517
+ const lifiSolanaDeposit = (transactionRequest, chainId) => {
518
+ const { data } = transactionRequest;
519
+ if (typeof data !== 'string' || !isSerializedSolanaTransaction(data)) {
520
+ return null;
521
+ }
522
+ return { ecosystem: 'svm', form: 'serialized', transaction: data, chainId };
523
+ };
524
+ /**
525
+ * INTENTIONALLY UNWIRED at runtime — retained as tested, parked capability.
526
+ *
527
+ * Nothing in the app calls this; the live LI.FI path is `parseLifiContractCallsQuote`
528
+ * (the contract-calls endpoint), which only ever produces an Ethereum deposit.
529
+ * This parser is the ONE piece that can decode a NON-Ethereum LI.FI origin
530
+ * deposit, and it exists for a provider path that is deliberately NOT built:
531
+ * adding LI.FI as a competing provider for non-Ethereum origins was analysed and
532
+ * rejected (LI.FI can produce only leg one of the consolidated route, and every
533
+ * way to execute leg two is strictly worse than Relay, which already serves those
534
+ * origins in a single signature — see docs/consolidated-bridge-route-design.md).
535
+ * Do not delete this while that decision could still be revisited; its test block
536
+ * (`parseLifiQuote across origin ecosystems`) is its executable spec.
537
+ *
538
+ * Normalizes a quote response from LI.FI's plain `GET /v1/quote`.
539
+ *
540
+ * Unlike the contract-calls endpoint, this one accepts a non-Ethereum origin,
541
+ * and the shape of `transactionRequest` then varies by ecosystem: Ethereum
542
+ * returns the familiar `to`/`data`/`value`/`chainId`, Bitcoin returns a deposit
543
+ * address plus a partially-signed transaction, and Solana returns nothing but a
544
+ * compiled transaction. Reading the Ethereum fields off the others would
545
+ * produce a deposit that is structurally valid and completely wrong, so the
546
+ * caller states the origin ecosystem and only that reader runs.
547
+ *
548
+ * The origin ecosystem is a parameter rather than something sniffed from the
549
+ * payload because the caller already knows it — it decided which chain to quote
550
+ * — and guessing from field presence would make a malformed Ethereum response
551
+ * look like a valid Solana one.
552
+ *
553
+ * Note that `approvalAddress` is dropped for a non-Ethereum origin. LI.FI
554
+ * returns its Ethereum router there regardless of where the route starts, and
555
+ * that address is meaningless to a Solana or Bitcoin wallet; passing it on
556
+ * invites a caller to build an approval for a chain that has no such concept.
557
+ *
558
+ * @param payload - the raw JSON body from `GET /v1/quote`
559
+ * @param options.originEcosystem - the virtual-machine family the route starts from
560
+ * @returns the normalized quote, or null when the payload does not match that ecosystem
561
+ */
562
+ /**
563
+ * What a plain LI.FI quote says arrives, and what LI.FI says it is worth.
564
+ *
565
+ * ONLY THE PLAIN ENDPOINT CARRIES THESE ONTO THE QUOTE, and that is not a
566
+ * convenience. A route through this endpoint is solved FORWARDS: the reader
567
+ * names what they spend, so every provider is handed the same `fromAmount` and
568
+ * the delivered amount is the ONE figure that differs. Without it:
569
+ *
570
+ * - ranking cannot engage its forward rule — it requires every quote to carry
571
+ * `toAmount` — and falls back to comparing each provider's own dollar
572
+ * figure, which come from different price feeds. Measured: LI.FI $122.00
573
+ * against Relay $123.71 on the same 0.05 ether, so LI.FI won on a feed.
574
+ * - the route then shows the winner's delivered amount as ZERO, because the
575
+ * direct candidate reads `quote.toAmount ?? 0n`.
576
+ *
577
+ * Both happened: Relay's parser set these, LI.FI's never did, and a route test
578
+ * built its quotes by hand with `toAmount` already filled in — so it passed.
579
+ * It took a live round trip to see the gap.
580
+ *
581
+ * NOT added to the shared envelope. The contract-calls endpoint uses that too,
582
+ * and on an omnibridge route the provider's delivered side is the ENTRY chain's
583
+ * tokens rather than what lands on PulseChain — see `ConsolidatedQuote.toAmount`.
584
+ *
585
+ * `estimate.toAmount` is the expected amount, matching Relay's
586
+ * `currencyOut.amount`, so the two rank on the same kind of figure.
587
+ *
588
+ * @param payload - the raw JSON body from `GET /v1/quote`
589
+ * @returns the delivered amount and its dollar value, each only when present
590
+ */
591
+ const lifiDeliveredAmount = (payload) => {
592
+ const estimate = payload?.estimate;
593
+ const delivered = {};
594
+ if (typeof estimate?.toAmount === 'string') {
595
+ try {
596
+ delivered.toAmount = BigInt(estimate.toAmount);
597
+ }
598
+ catch {
599
+ // A malformed amount leaves the field absent rather than zero: zero is a
600
+ // real quantity and would read as "this route delivers nothing".
601
+ }
602
+ }
603
+ if (typeof estimate?.toAmountUSD === 'string') {
604
+ delivered.toAmountUsd = estimate.toAmountUSD;
605
+ }
606
+ return delivered;
607
+ };
608
+ /**
609
+ * Normalizes a quote response from LI.FI's plain `GET /v1/quote`.
610
+ *
611
+ * `request` IS REQUIRED FOR AN ETHEREUM ORIGIN, though its type stays
612
+ * optional — omitting it there throws {@link LifiExecutionRefusedError}
613
+ * rather than returning an unverified deposit; see `lifiEvmDeposit`'s doc
614
+ * comment for why the type cannot say so directly. It is genuinely optional
615
+ * for a Solana or Bitcoin origin, which never reach the execution guard.
616
+ *
617
+ * @param payload - the raw JSON body from `GET /v1/quote`
618
+ * @param options.originEcosystem - the virtual-machine family the route starts from
619
+ * @param options.request - the consolidated request this response answers
620
+ * @returns the normalized quote, or null when the payload does not match that ecosystem
621
+ * @throws {LifiExecutionRefusedError} for an Ethereum origin, when `request`
622
+ * is absent or the execution transaction disagrees with it
623
+ */
624
+ export const parseLifiQuote = (payload, { originEcosystem, request }) => {
625
+ const envelope = parseLifiQuoteEnvelope(payload);
626
+ if (envelope === null) {
627
+ return null;
628
+ }
629
+ const { transactionRequest, fromChainId, ...envelopeFields } = envelope;
630
+ const quote = { ...envelopeFields, ...lifiDeliveredAmount(payload) };
631
+ if (originEcosystem === 'evm') {
632
+ const originDeposit = lifiEvmDeposit(transactionRequest, envelope.approvalAddress, request);
633
+ return originDeposit === null ? null : { ...quote, originDeposit };
634
+ }
635
+ if (fromChainId === null) {
636
+ return null;
637
+ }
638
+ const originDeposit = originEcosystem === 'svm'
639
+ ? lifiSolanaDeposit(transactionRequest, fromChainId)
640
+ : originEcosystem === 'bvm'
641
+ ? lifiBitcoinDeposit(transactionRequest, fromChainId)
642
+ : null;
643
+ if (originDeposit === null) {
644
+ return null;
645
+ }
646
+ return { ...quote, originDeposit, approvalAddress: null };
647
+ };
648
+ const consolidatedStatusValues = ['NOT_FOUND', 'INVALID', 'PENDING', 'DONE', 'FAILED'];
649
+ /**
650
+ * Builds the status-poll address for an origin transaction, at
651
+ * {@link lifiDefaultApiOrigin}. Chain hints narrow the provider's lookup and
652
+ * are recommended when known. `txHash` is typed as `string` rather than `Hex`
653
+ * because a non-Ethereum origin (Solana base58 signature, Bitcoin transaction
654
+ * id) is not `0x`-prefixed.
655
+ */
656
+ export const lifiStatusUrl = ({ txHash, fromChainId, toChainId, }) => {
657
+ return defaultLifiEndpoints.status({ txHash, fromChainId, toChainId });
658
+ };
659
+ const isTransactionHash = (value) => typeof value === 'string' && /^0x[0-9a-fA-F]{64}$/.test(value);
660
+ /**
661
+ * Normalizes a status response for an in-flight consolidated bridge.
662
+ *
663
+ * The receiving hash gates the handover from provider polling to the
664
+ * omnibridge indexer, so it must be a full 32-byte transaction hash — an
665
+ * empty or malformed value collapses to null rather than masquerading as a
666
+ * real transaction.
667
+ *
668
+ * @returns the normalized status, or null when the payload shape is unexpected
669
+ */
670
+ export const parseLifiStatus = (payload) => {
671
+ if (typeof payload !== 'object' || payload === null) {
672
+ return null;
673
+ }
674
+ const data = payload;
675
+ const status = consolidatedStatusValues.find((value) => value === data.status);
676
+ if (!status) {
677
+ return null;
678
+ }
679
+ const receiving = data.receiving;
680
+ return {
681
+ status,
682
+ substatus: typeof data.substatus === 'string' ? data.substatus : null,
683
+ receivingTxHash: isTransactionHash(receiving?.txHash) ? receiving.txHash : null,
684
+ };
685
+ };
686
+ /**
687
+ * Chain listing endpoint, at {@link lifiDefaultApiOrigin}. Deliberately
688
+ * carries no query parameters: the `chainTypes` filter requires an API key on
689
+ * the anonymous tier (verified live 2026-06-12, error code 1010), and the
690
+ * default response is EVM-only.
691
+ */
692
+ export const lifiChainsUrl = defaultLifiEndpoints.chains;
693
+ /**
694
+ * Normalizes a `GET /v1/chains` response body to mainnet EVM chains.
695
+ *
696
+ * @returns the chain list, or null when the payload shape is unexpected
697
+ */
698
+ export const parseLifiChains = (payload) => {
699
+ if (typeof payload !== 'object' || payload === null) {
700
+ return null;
701
+ }
702
+ const chains = payload.chains;
703
+ if (!Array.isArray(chains)) {
704
+ return null;
705
+ }
706
+ const parsed = [];
707
+ for (const chain of chains) {
708
+ if (typeof chain !== 'object' || chain === null) {
709
+ continue;
710
+ }
711
+ const entry = chain;
712
+ if (entry.chainType !== 'EVM' || entry.mainnet !== true) {
713
+ continue;
714
+ }
715
+ if (typeof entry.id !== 'number' || typeof entry.name !== 'string') {
716
+ continue;
717
+ }
718
+ parsed.push({
719
+ id: entry.id,
720
+ name: entry.name,
721
+ logoURI: typeof entry.logoURI === 'string' ? entry.logoURI : null,
722
+ });
723
+ }
724
+ return parsed;
725
+ };
726
+ /** Builds the per-chain token listing URL, at {@link lifiDefaultApiOrigin}. */
727
+ export const lifiTokensUrl = defaultLifiEndpoints.tokens;
728
+ /**
729
+ * Normalizes a `GET /v1/tokens?chains=<id>` response body into the shared
730
+ * token shape used across the bridge frontend. Malformed entries are skipped
731
+ * rather than failing the whole list; `logoURI` is optional on the live
732
+ * interface.
733
+ *
734
+ * @returns the token list, or null when the payload shape is unexpected
735
+ */
736
+ export const parseLifiTokens = (payload, chainId) => {
737
+ if (typeof payload !== 'object' || payload === null) {
738
+ return null;
739
+ }
740
+ const tokensByChain = payload.tokens;
741
+ if (typeof tokensByChain !== 'object' || tokensByChain === null) {
742
+ return null;
743
+ }
744
+ const entries = tokensByChain[chainId.toString()];
745
+ if (!Array.isArray(entries) || entries.length === 0) {
746
+ return null;
747
+ }
748
+ const parsed = [];
749
+ for (const item of entries) {
750
+ if (typeof item !== 'object' || item === null) {
751
+ continue;
752
+ }
753
+ const entry = item;
754
+ if (!isHexString(entry.address) ||
755
+ typeof entry.symbol !== 'string' ||
756
+ typeof entry.name !== 'string' ||
757
+ typeof entry.decimals !== 'number') {
758
+ continue;
759
+ }
760
+ parsed.push({
761
+ chainId,
762
+ address: entry.address,
763
+ symbol: entry.symbol,
764
+ name: entry.name,
765
+ decimals: entry.decimals,
766
+ logoURI: typeof entry.logoURI === 'string' ? entry.logoURI : null,
767
+ // Preserve the inline spot price the LI.FI list carries — it is the USD
768
+ // price source for aggregator chains (the dapp image lists carry none).
769
+ priceUSD: typeof entry.priceUSD === 'string' ? entry.priceUSD : null,
770
+ });
771
+ }
772
+ return parsed;
773
+ };