@gibs/bridge-sdk 1.12.0 → 1.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +234 -432
  3. package/dist/abis.d.ts +1 -0
  4. package/dist/bridge-limits.d.ts +103 -0
  5. package/dist/bridge-limits.js +84 -0
  6. package/dist/chain-info.d.ts +169 -0
  7. package/dist/chain-info.js +410 -0
  8. package/dist/chain-names.d.ts +160 -0
  9. package/dist/chain-names.js +200 -0
  10. package/dist/chains.d.ts +65 -0
  11. package/dist/chains.js +284 -0
  12. package/dist/config.d.ts +216 -0
  13. package/dist/config.js +493 -0
  14. package/dist/consolidated-quote.d.ts +554 -0
  15. package/dist/consolidated-quote.js +773 -0
  16. package/dist/destination-calldata.d.ts +118 -0
  17. package/dist/destination-calldata.js +152 -0
  18. package/dist/ecosystems.d.ts +217 -0
  19. package/dist/ecosystems.js +288 -0
  20. package/dist/fee-type.d.ts +10 -0
  21. package/dist/fee-type.js +9 -0
  22. package/dist/hyperliquid-execution.d.ts +152 -0
  23. package/dist/hyperliquid-execution.js +168 -0
  24. package/dist/image-links.d.ts +117 -0
  25. package/dist/image-links.js +142 -0
  26. package/dist/index.d.ts +20 -0
  27. package/dist/index.js +20 -0
  28. package/dist/lifi-execution-guard.d.ts +199 -0
  29. package/dist/lifi-execution-guard.js +400 -0
  30. package/dist/near-intents-conformance.d.ts +85 -0
  31. package/dist/near-intents-conformance.js +329 -0
  32. package/dist/near-intents-route.d.ts +47 -0
  33. package/dist/near-intents-route.js +265 -0
  34. package/dist/near-intents.d.ts +524 -0
  35. package/dist/near-intents.js +1055 -0
  36. package/dist/origin-deposit.d.ts +326 -0
  37. package/dist/origin-deposit.js +68 -0
  38. package/dist/pre-quote.d.ts +198 -0
  39. package/dist/pre-quote.js +175 -0
  40. package/dist/prices.d.ts +107 -0
  41. package/dist/prices.js +162 -0
  42. package/dist/providers.d.ts +221 -0
  43. package/dist/providers.js +157 -0
  44. package/dist/quote-fees.d.ts +102 -0
  45. package/dist/quote-fees.js +195 -0
  46. package/dist/referral.d.ts +77 -0
  47. package/dist/referral.js +72 -0
  48. package/dist/relay-currencies.d.ts +47 -0
  49. package/dist/relay-currencies.js +89 -0
  50. package/dist/relay-quote.d.ts +301 -0
  51. package/dist/relay-quote.js +983 -0
  52. package/dist/route-steps.d.ts +138 -0
  53. package/dist/route-steps.js +305 -0
  54. package/dist/routing/amounts.d.ts +80 -0
  55. package/dist/routing/amounts.js +120 -0
  56. package/dist/routing/candidate-route.d.ts +43 -0
  57. package/dist/routing/candidate-route.js +45 -0
  58. package/dist/routing/candidate.d.ts +237 -0
  59. package/dist/routing/candidate.js +96 -0
  60. package/dist/routing/carriers/ask-answer.d.ts +60 -0
  61. package/dist/routing/carriers/ask-answer.js +1 -0
  62. package/dist/routing/carriers/omnibridge.d.ts +137 -0
  63. package/dist/routing/carriers/omnibridge.js +419 -0
  64. package/dist/routing/carriers/pulsex.d.ts +149 -0
  65. package/dist/routing/carriers/pulsex.js +453 -0
  66. package/dist/routing/carriers/wrap.d.ts +25 -0
  67. package/dist/routing/carriers/wrap.js +115 -0
  68. package/dist/routing/composed-route-quality.d.ts +103 -0
  69. package/dist/routing/composed-route-quality.js +108 -0
  70. package/dist/routing/entry-chain-anchor.d.ts +88 -0
  71. package/dist/routing/entry-chain-anchor.js +55 -0
  72. package/dist/routing/format.d.ts +31 -0
  73. package/dist/routing/format.js +58 -0
  74. package/dist/routing/index.d.ts +34 -0
  75. package/dist/routing/index.js +42 -0
  76. package/dist/routing/producers.d.ts +770 -0
  77. package/dist/routing/producers.js +1564 -0
  78. package/dist/routing/providers.d.ts +26 -0
  79. package/dist/routing/providers.js +38 -0
  80. package/dist/routing/route-data.d.ts +355 -0
  81. package/dist/routing/route-data.js +1 -0
  82. package/dist/routing/search/compose.d.ts +168 -0
  83. package/dist/routing/search/compose.js +98 -0
  84. package/dist/routing/search/graph.d.ts +653 -0
  85. package/dist/routing/search/graph.js +445 -0
  86. package/dist/routing/search/plan-route-data.d.ts +244 -0
  87. package/dist/routing/search/plan-route-data.js +767 -0
  88. package/dist/routing/search/score.d.ts +353 -0
  89. package/dist/routing/search/score.js +427 -0
  90. package/dist/routing/usd-value.d.ts +17 -0
  91. package/dist/routing/usd-value.js +25 -0
  92. package/dist/settings.d.ts +692 -0
  93. package/dist/settings.js +909 -0
  94. package/dist/ton-execution.d.ts +117 -0
  95. package/dist/ton-execution.js +247 -0
  96. package/dist/tron-execution.d.ts +146 -0
  97. package/dist/tron-execution.js +239 -0
  98. package/dist/types.d.ts +51 -0
  99. package/dist/types.js +1 -0
  100. package/llms.txt +169 -0
  101. package/package.json +204 -23
  102. package/src/chain-info.test.ts +0 -968
  103. package/src/chain-info.ts +0 -498
  104. package/src/chains.test.ts +0 -123
  105. package/src/chains.ts +0 -88
  106. package/src/config.test.ts +0 -679
  107. package/src/config.ts +0 -500
  108. package/src/fee-type.test.ts +0 -45
  109. package/src/fee-type.ts +0 -14
  110. package/src/image-links.test.ts +0 -160
  111. package/src/image-links.ts +0 -65
  112. package/src/index.ts +0 -8
  113. package/src/settings.test.ts +0 -2754
  114. package/src/settings.ts +0 -875
  115. package/src/types.ts +0 -48
  116. package/tsconfig.json +0 -17
  117. package/vitest.config.ts +0 -13
  118. /package/{src/abis.ts → dist/abis.js} +0 -0
@@ -0,0 +1,453 @@
1
+ import { multicallRead } from '@gibs/common/multicall';
2
+ import { getAddress, parseAbi, zeroAddress } from 'viem';
3
+ import * as abis from '../../abis.js';
4
+ import { chainsMetadata } from '../../chains.js';
5
+ import { Chains, nativeAssetOut, toChain, uniV2Factories, uniV2Routers } from '../../config.js';
6
+ import { sameAddress } from '../../ecosystems.js';
7
+ import { pulseXConversionSwapPath } from '../providers.js';
8
+ import { PULSEX_MARGINAL_REFERENCE_DIVISOR, pulsechainChainId } from '../producers.js';
9
+ import { assetNodeKey } from '../search/graph.js';
10
+ /**
11
+ * PulseX (the PulseChain-native decentralized exchange, used here for a
12
+ * conversion swap leg) as a plug-in `Carrier` for `@gibs/quotes`'s
13
+ * route-search engine. GIBS-SPECIFIC — see `docs/quote-service.md`'s
14
+ * "Carriers" section: "never built in ... PulseX." Registered from
15
+ * `packages/ui/src/lib/state/useRouteSearch.ts` / `route-search-quotes.ts`.
16
+ *
17
+ * See `omnibridge.ts`'s head doc for why this returns a plain object shaped
18
+ * like `Carrier` rather than importing that type.
19
+ */
20
+ /** WPLS, the wrapped-native intermediary {@link pulseXConversionSwapPath} routes conversions through. */
21
+ const wplsAddress = nativeAssetOut[Chains.PLS];
22
+ /**
23
+ * Every PulseX deployment on PulseChain, V1 first: `uniV2Routers` paired with
24
+ * `uniV2Factories` by index. Each factory is the router's own
25
+ * `router.factory()` read on chain — see `uniV2Factories`'s own doc in
26
+ * `config.ts`. A router with no recorded factory is left out, because its
27
+ * pools cannot be read.
28
+ */
29
+ export const pulseXVenues = (uniV2Routers[Chains.PLS] ?? []).flatMap((router, index) => {
30
+ const factory = uniV2Factories[Chains.PLS]?.[index];
31
+ return factory === undefined ? [] : [{ router: getAddress(router), factory: getAddress(factory) }];
32
+ });
33
+ /**
34
+ * The venue an edge with no {@link HopEdge.venue} runs on: V1, index 0. Every
35
+ * PulseX edge built before venues existed was quoted and signed on V1 — the
36
+ * same-chain swap sender's own hand-built edge still is — so this keeps that
37
+ * meaning rather than guessing a new one.
38
+ */
39
+ const defaultPulseXVenue = pulseXVenues[0];
40
+ /**
41
+ * The router a PulseX edge must be signed on — the one its figure was quoted
42
+ * against. Reads the edge's own {@link HopEdge.venue}; an edge with none is a
43
+ * V1 edge (see {@link defaultPulseXVenue}).
44
+ *
45
+ * @param edge - the edge to sign
46
+ * @returns the router, or null when the edge is not a PulseX edge or names a venue that is not a known PulseX router
47
+ */
48
+ export const pulseXRouterForEdge = (edge) => pulseXVenueForEdge(edge)?.router ?? null;
49
+ /**
50
+ * The {@link PulseXVenue} a PulseX edge runs on — see {@link pulseXRouterForEdge}.
51
+ *
52
+ * @param edge - the edge
53
+ * @returns the venue, or null when there is none
54
+ */
55
+ const pulseXVenueForEdge = (edge) => {
56
+ if (edge.carrier !== 'PulseX')
57
+ return null;
58
+ if (edge.venue === undefined)
59
+ return defaultPulseXVenue ?? null;
60
+ const venue = edge.venue;
61
+ return pulseXVenues.find((candidate) => sameAddress(candidate.router, venue, 'evm')) ?? null;
62
+ };
63
+ /**
64
+ * A node's own address for a PulseX pool lookup: native PLS (the zero
65
+ * address) is not a token the router or its factory ever holds a pool for —
66
+ * only Wrapped PLS is — so a native node resolves to {@link wplsAddress}
67
+ * before it ever reaches {@link pulseXConversionSwapPath}. Every other node
68
+ * is unchanged.
69
+ *
70
+ * MIRRORS `useRouteSearch.ts`'s `quotePathAddress` (the UI's async `ask`
71
+ * already resolves native this way through that function) — this is the
72
+ * SAME rule, restated here because `prepare`/`exact` never go through the
73
+ * UI's `readAmountOut` at all; see {@link pulseXPoolPath}'s own doc.
74
+ *
75
+ * @param node - the node to resolve
76
+ * @returns the node's own address, or {@link wplsAddress} for native PLS
77
+ */
78
+ const poolTokenAddress = (node) => (sameAddress(node.address, zeroAddress, 'evm') ? wplsAddress : node.address);
79
+ /**
80
+ * The router swap path a PulseX hop from `from` to `to` prices and signs
81
+ * along, or null when there is no swap for PulseX to do — including when
82
+ * both ends resolve to the SAME pool token, which is true of PLS<->WPLS
83
+ * (native PLS resolves to {@link wplsAddress} via {@link poolTokenAddress},
84
+ * same as Wrapped PLS itself): that pair is the wrap carrier's job, never a
85
+ * PulseX hop, so this returns null rather than a zero-length or self-paired
86
+ * path.
87
+ *
88
+ * EVERY PULSEX QUOTE AND SIGNATURE IN THIS FILE MUST GO THROUGH THIS ONE
89
+ * FUNCTION — `pulseXCarries`, `prepare`'s pool discovery, and `exact`'s hop
90
+ * pricing all call it, so native PLS resolves the same way in every one of
91
+ * them. Before this existed, `pulseXCarries` and `prepare`/`exact` called
92
+ * `pulseXConversionSwapPath` directly on the raw node address: a native PLS
93
+ * node produced a path that LITERALLY STARTED WITH THE ZERO ADDRESS
94
+ * (`[zeroAddress, wplsAddress, ...]`), which `pulseXCarries` accepted (its
95
+ * length is still >= 2) but for which `prepare` could never find a pool —
96
+ * the factory has no pair for the zero address — so `exact` always answered
97
+ * `null` for a direct native-PLS PulseX hop, and `enumerate.ts` pruned it,
98
+ * leaving only the two-signature wrap-then-swap path.
99
+ *
100
+ * @param from - the hop's origin node
101
+ * @param to - the hop's destination node
102
+ * @returns the swap path, or null when there is nothing for PulseX to swap
103
+ */
104
+ export const pulseXPoolPath = (from, to) => {
105
+ const fromAddress = poolTokenAddress(from);
106
+ const toAddress = poolTokenAddress(to);
107
+ if (sameAddress(fromAddress, toAddress, 'evm'))
108
+ return null;
109
+ return pulseXConversionSwapPath(fromAddress, toAddress, wplsAddress);
110
+ };
111
+ /**
112
+ * Whether PulseX can carry funds directly from `from` to `to`: both must be
113
+ * distinct Ethereum-Virtual-Machine tokens on PulseChain itself — PulseX runs
114
+ * only on PulseChain, never on an entry chain.
115
+ *
116
+ * NO TOKEN ALLOWLIST IS CONSULTED, on purpose: "any PulseChain token with a
117
+ * {@link pulseXPoolPath}" is every PulseChain token, because that function
118
+ * is total over any two addresses that do not resolve to the same pool
119
+ * token — it collapses to a direct hop only when one side is already WPLS,
120
+ * and otherwise always routes through WPLS. This calls it anyway and checks
121
+ * the result is at least two addresses long, so a future change that
122
+ * narrowed it to return an empty path for some input would fail this
123
+ * predicate rather than silently offer a hop with no path.
124
+ */
125
+ const pulseXCarries = (from, to) => {
126
+ if (from.ecosystem !== 'evm' || to.ecosystem !== 'evm')
127
+ return false;
128
+ if (from.chainId !== pulsechainChainId || to.chainId !== pulsechainChainId)
129
+ return false;
130
+ const swapPath = pulseXPoolPath(from, to);
131
+ return swapPath !== null && swapPath.length >= 2;
132
+ };
133
+ /**
134
+ * PulseX's own constant-product swap fee, as a numerator over
135
+ * {@link PULSEX_FEE_DENOMINATOR} — the SAME `getAmountOut` formula every
136
+ * Uniswap-V2-style router uses: `amountOut = floor(amountIn * feeNumerator *
137
+ * reserveOut / (reserveIn * feeDenominator + amountIn * feeNumerator))`.
138
+ *
139
+ * MEASURED, NOT ASSUMED — see `uniV2Factories`'s own doc in `config.ts` for
140
+ * the full account. Reading live reserves from both PulseX router
141
+ * deployments (V1 `0x98bf93…`, V2 `0x165C34…`) and back-solving this ratio
142
+ * against each router's own `getAmountsOut`, on a one-hop and a two-hop
143
+ * path, gave the IDENTICAL ratio — `9971/10000` — for both, to the base
144
+ * unit. One constant serves both, and `pulsex-carrier.fork.test.ts` pins
145
+ * this claim against live chain data rather than trusting the arithmetic
146
+ * alone.
147
+ */
148
+ const PULSEX_FEE_NUMERATOR = 9971n;
149
+ /** The denominator {@link PULSEX_FEE_NUMERATOR} is a fraction of. */
150
+ const PULSEX_FEE_DENOMINATOR = 10000n;
151
+ /** The minimal read-only factory ABI `prepare` needs — just enough to resolve a pool's address from its two tokens. */
152
+ const univ2FactoryAbi = parseAbi([
153
+ 'function getPair(address tokenA, address tokenB) external view returns (address pair)',
154
+ ]);
155
+ /**
156
+ * The Uniswap-V2-style constant-product `getAmountOut`, applied with
157
+ * {@link PULSEX_FEE_NUMERATOR}. Reproduces the router's own on-chain formula
158
+ * exactly — see `pulsex-carrier.fork.test.ts` — so PRICE IMPACT IS ALREADY
159
+ * IN HERE: this is the same nonlinear function the router evaluates, not a
160
+ * flat-fee approximation with an impact correction bolted on afterward.
161
+ *
162
+ * @param amountIn - the input amount, in `reserveIn`'s token's base units
163
+ * @param reserveIn - the pool's reserve of the input token
164
+ * @param reserveOut - the pool's reserve of the output token
165
+ * @returns the output amount, in `reserveOut`'s token's base units; 0 when either reserve is 0
166
+ */
167
+ const constantProductAmountOut = (amountIn, reserveIn, reserveOut) => {
168
+ if (reserveIn <= 0n || reserveOut <= 0n || amountIn <= 0n)
169
+ return 0n;
170
+ const amountInWithFee = amountIn * PULSEX_FEE_NUMERATOR;
171
+ const numerator = amountInWithFee * reserveOut;
172
+ const denominator = reserveIn * PULSEX_FEE_DENOMINATOR + amountInWithFee;
173
+ return numerator / denominator;
174
+ };
175
+ /**
176
+ * What `amountIn` delivers after crossing every pool of a path in order,
177
+ * chaining {@link constantProductAmountOut}.
178
+ *
179
+ * @param amountIn - the amount entering the first pool
180
+ * @param steps - the pools in order of travel
181
+ * @returns the amount leaving the last pool; 0 when any pool cannot pay out
182
+ */
183
+ export const amountOutAlongPath = (amountIn, steps) => steps.reduce((runningAmount, step) => constantProductAmountOut(runningAmount, step.reserveIn, step.reserveOut), amountIn);
184
+ /**
185
+ * How far a trade's executed rate falls below the marginal (near-spot) rate,
186
+ * in percent. The marginal rate is a tiny reference trade through the same
187
+ * pools (`amountIn / PULSEX_MARGINAL_REFERENCE_DIVISOR`). The swap fee rides
188
+ * in both rates, so it cancels and the gap is price impact alone. A path
189
+ * through several pools composes by itself: the reference trade crosses the
190
+ * same pools in the same order.
191
+ *
192
+ * impact = (1 - (amountOut / amountIn) / (referenceAmountOut / referenceAmountIn)) * 100
193
+ *
194
+ * @param options.amountIn - the full trade's input
195
+ * @param options.amountOut - what the full trade delivers
196
+ * @param options.referenceAmountIn - the reference trade's input
197
+ * @param options.referenceAmountOut - what the reference trade delivers
198
+ * @returns the impact in percent, never below 0; null when a reference figure is zero (impact cannot be measured)
199
+ */
200
+ export const priceImpactAgainstMarginal = (options) => {
201
+ const { amountIn, amountOut, referenceAmountIn, referenceAmountOut } = options;
202
+ if (amountIn <= 0n || referenceAmountIn <= 0n || referenceAmountOut <= 0n)
203
+ return null;
204
+ // The output the trade would deliver at the marginal rate with no impact.
205
+ const idealAmountOut = (referenceAmountOut * amountIn) / referenceAmountIn;
206
+ if (idealAmountOut <= 0n)
207
+ return null;
208
+ if (amountOut >= idealAmountOut)
209
+ return 0;
210
+ const MILLIONTHS = 1000000n;
211
+ return Number(((idealAmountOut - amountOut) * MILLIONTHS * 100n) / idealAmountOut) / Number(MILLIONTHS);
212
+ };
213
+ /** The stable key one pool is stored and looked up under — the two token addresses, lower-cased and sorted, so either query order finds the same entry. */
214
+ const poolKey = (tokenA, tokenB) => {
215
+ const [lower, higher] = [tokenA.toLowerCase(), tokenB.toLowerCase()].sort();
216
+ return `${lower}|${higher}`;
217
+ };
218
+ /** The key one venue's pool is stored under: the same two tokens on V1 and on V2 are two different pools with two different reserves. */
219
+ const venuePoolKey = (venue, tokenA, tokenB) => `${venue.factory.toLowerCase()}|${poolKey(tokenA, tokenB)}`;
220
+ /** Every consecutive (tokenIn, tokenOut) hop a PulseX swap path implies. */
221
+ const hopsAlongPath = (path) => path.slice(0, -1).map((tokenIn, index) => [tokenIn, path[index + 1]]);
222
+ /**
223
+ * Batch-reads reserves for every pool `nodes`' candidate PulseX paths touch,
224
+ * on EVERY venue in `venues`, once, so {@link createPulseXCarrier}'s `exact`
225
+ * can answer every hop SYNCHRONOUSLY afterward.
226
+ *
227
+ * EACH VENUE IS READ THROUGH ITS OWN FACTORY. The same two tokens usually
228
+ * have a pool on V1 and another on V2, with different reserves; the two are
229
+ * stored apart ({@link venuePoolKey}) and never blended. `PULSEX_FEE_NUMERATOR`
230
+ * is proven identical for both deployments (see its own doc), so the formula
231
+ * is shared and only the reserves differ.
232
+ *
233
+ * TWO MULTICALLS ON PULSECHAIN, WHATEVER THE VENUE COUNT: `getPair` must
234
+ * resolve each pool's address before `getReserves` can be read from it, so
235
+ * the second read cannot join the first — but every venue's `getPair` rides
236
+ * the first multicall and every pool's `getReserves` rides the second.
237
+ *
238
+ * @param nodes - every node the search might branch through
239
+ * @param venues - the venues this carrier offers
240
+ * @param clientForChain - see {@link CreatePulseXCarrierOptions}'s own field
241
+ * @returns every pool this universe touches, keyed by {@link venuePoolKey} — pools with no `getPair` result are simply absent, not an error
242
+ */
243
+ const preparePulseXReserves = async ({ nodes, venues, clientForChain, }) => {
244
+ const reservesByPoolKey = new Map();
245
+ const client = clientForChain(pulsechainChainId);
246
+ if (client === null || venues.length === 0)
247
+ return reservesByPoolKey;
248
+ const pulsechainNodes = nodes.filter((node) => node.ecosystem === 'evm' && node.chainId === pulsechainChainId);
249
+ const poolTokenPairs = new Map();
250
+ for (const from of pulsechainNodes) {
251
+ for (const to of pulsechainNodes) {
252
+ if (assetNodeKey(from) === assetNodeKey(to))
253
+ continue;
254
+ if (!pulseXCarries(from, to))
255
+ continue;
256
+ const path = pulseXPoolPath(from, to);
257
+ if (path === null)
258
+ continue;
259
+ for (const [tokenIn, tokenOut] of hopsAlongPath(path)) {
260
+ poolTokenPairs.set(poolKey(tokenIn, tokenOut), [tokenIn, tokenOut]);
261
+ }
262
+ }
263
+ }
264
+ if (poolTokenPairs.size === 0)
265
+ return reservesByPoolKey;
266
+ const poolQueries = venues.flatMap((venue) => Array.from(poolTokenPairs.values()).map(([tokenIn, tokenOut]) => ({
267
+ key: venuePoolKey(venue, tokenIn, tokenOut),
268
+ factory: venue.factory,
269
+ tokenIn,
270
+ tokenOut,
271
+ })));
272
+ const factoryResults = await multicallRead({
273
+ chain: chainsMetadata[toChain(pulsechainChainId)],
274
+ client,
275
+ abi: univ2FactoryAbi,
276
+ calls: poolQueries.map((query) => ({
277
+ functionName: 'getPair',
278
+ target: query.factory,
279
+ args: [query.tokenIn, query.tokenOut],
280
+ allowFailure: true,
281
+ })),
282
+ });
283
+ const pairAddressByPoolKey = new Map();
284
+ poolQueries.forEach((query, index) => {
285
+ const pairAddress = factoryResults[index];
286
+ // A decoded address is exactly 42 characters; a failed `allowFailure`
287
+ // call's undecoded revert bytes are almost never that length — see
288
+ // `omnibridge.ts`'s identical guard for why `typeof` alone cannot tell
289
+ // success from failure for an address-returning call.
290
+ if (typeof pairAddress === 'string' && pairAddress.length === 42 && pairAddress !== zeroAddress) {
291
+ pairAddressByPoolKey.set(query.key, getAddress(pairAddress));
292
+ }
293
+ });
294
+ if (pairAddressByPoolKey.size === 0)
295
+ return reservesByPoolKey;
296
+ const reservePoolKeys = Array.from(pairAddressByPoolKey.keys());
297
+ const reserveResults = await multicallRead({
298
+ chain: chainsMetadata[toChain(pulsechainChainId)],
299
+ client,
300
+ abi: abis.pair,
301
+ calls: reservePoolKeys.map((key) => ({
302
+ functionName: 'getReserves',
303
+ target: pairAddressByPoolKey.get(key),
304
+ allowFailure: true,
305
+ })),
306
+ });
307
+ reservePoolKeys.forEach((key, index) => {
308
+ const result = reserveResults[index];
309
+ if (!Array.isArray(result))
310
+ return;
311
+ const [reserve0, reserve1] = result;
312
+ if (typeof reserve0 !== 'bigint' || typeof reserve1 !== 'bigint')
313
+ return;
314
+ // Standard Uniswap-V2-style factories sort a pair's tokens before
315
+ // creating it, so `token0` is always the LEXICOGRAPHICALLY LOWER
316
+ // address — confirmed against live PulseX pools 2026-09-24 (both V1 and
317
+ // V2), never re-derived with an extra `token0()` read.
318
+ reservesByPoolKey.set(key, { reserveOfLowerAddress: reserve0, reserveOfHigherAddress: reserve1 });
319
+ });
320
+ return reservesByPoolKey;
321
+ };
322
+ /**
323
+ * The venues a carrier offers: every known venue, or only the routers the
324
+ * host names. A named router that is not a known PulseX router is a
325
+ * configuration error, thrown at construction rather than offered silently.
326
+ *
327
+ * @param routers - the routers the host can sign on, or undefined for all
328
+ * @returns the venues, in {@link pulseXVenues} order
329
+ */
330
+ const selectPulseXVenues = (routers) => {
331
+ if (routers === undefined)
332
+ return pulseXVenues;
333
+ for (const router of routers) {
334
+ if (!pulseXVenues.some((venue) => sameAddress(venue.router, router, 'evm'))) {
335
+ throw new Error(`createPulseXCarrier: ${router} is not a known PulseX router on PulseChain`);
336
+ }
337
+ }
338
+ return pulseXVenues.filter((venue) => routers.some((router) => sameAddress(venue.router, router, 'evm')));
339
+ };
340
+ /**
341
+ * Builds the PulseX conversion swap as a plug-in carrier.
342
+ *
343
+ * @param options - see {@link CreatePulseXCarrierOptions}
344
+ * @returns the carrier — `id: 'PulseX'`
345
+ */
346
+ export const createPulseXCarrier = (options) => {
347
+ const { readAmountOut, clientForChain } = options;
348
+ const venues = selectPulseXVenues(options.routers);
349
+ // ONE EDGE PER VENUE. Each names its router as its `venue`, so the search
350
+ // quotes and ranks V1 and V2 as two separate hops and the better one wins
351
+ // on its own figure — and the winner is signed on the router it names.
352
+ const edgesFrom = (from, to) => {
353
+ if (assetNodeKey(from) === assetNodeKey(to))
354
+ return [];
355
+ if (!pulseXCarries(from, to))
356
+ return [];
357
+ return venues.map((venue) => ({
358
+ carrier: 'PulseX',
359
+ from,
360
+ to,
361
+ fusesNext: false,
362
+ origin: 'wallet',
363
+ floors: [],
364
+ venue: venue.router,
365
+ }));
366
+ };
367
+ const ask = async ({ edge, inputAmount }) => {
368
+ const amountOut = await readAmountOut(edge, inputAmount);
369
+ if (amountOut === null || amountOut <= 0n) {
370
+ return { ok: false, refusal: { kind: 'no-quote', reason: 'no liquidity on this PulseX path', minimumAmount: null } };
371
+ }
372
+ return {
373
+ ok: true,
374
+ deliveredAmount: amountOut,
375
+ // getAmountsOut is exact for the CURRENT block; no slippage tolerance is
376
+ // modelled here, so the guaranteed figure is left equal to the expected
377
+ // one, matching the omnibridge carrier's own convention.
378
+ minimumDeliveredAmount: amountOut,
379
+ claimedDurationSeconds: 0,
380
+ };
381
+ };
382
+ // Populated by `prepare`, read by `exact` — see `omnibridge.ts`'s identical
383
+ // field for why a mutable closure here is safe (client-side,
384
+ // one-search-at-a-time; PulseX is likewise "never built into" the shared
385
+ // display-quote service).
386
+ let reservesByPoolKey = new Map();
387
+ const prepare = async (nodes) => {
388
+ if (clientForChain === undefined)
389
+ return;
390
+ reservesByPoolKey = await preparePulseXReserves({ nodes, venues, clientForChain });
391
+ };
392
+ const exact = (edge, amountIn) => {
393
+ if (!pulseXCarries(edge.from, edge.to))
394
+ return null;
395
+ // Quoted on the edge's OWN venue — the router it will be signed on — and
396
+ // only on a venue this carrier offers.
397
+ const venue = pulseXVenueForEdge(edge);
398
+ if (venue === null || !venues.includes(venue))
399
+ return null;
400
+ const path = pulseXPoolPath(edge.from, edge.to);
401
+ if (path === null)
402
+ return null;
403
+ const hops = hopsAlongPath(path);
404
+ if (hops.length === 0)
405
+ return null;
406
+ const steps = [];
407
+ for (const [tokenIn, tokenOut] of hops) {
408
+ const reserves = reservesByPoolKey.get(venuePoolKey(venue, tokenIn, tokenOut));
409
+ // `prepare` never read this pool — a structural non-answer, not a
410
+ // refusal (`exact`'s own doc: `null` means "nothing to say"). NOTE
411
+ // THIS DOES NOT FALL BACK TO AN ESTIMATE: a carrier with `exact`
412
+ // defined never yields an `'estimate'` outcome (see
413
+ // `enumerate.ts`'s `resolveHopOutcome`, branch 1) — `null` here
414
+ // PRUNES the hop entirely, the same as a refusal.
415
+ if (reserves === undefined)
416
+ return null;
417
+ const tokenInIsLower = tokenIn.toLowerCase() < tokenOut.toLowerCase();
418
+ steps.push({
419
+ reserveIn: tokenInIsLower ? reserves.reserveOfLowerAddress : reserves.reserveOfHigherAddress,
420
+ reserveOut: tokenInIsLower ? reserves.reserveOfHigherAddress : reserves.reserveOfLowerAddress,
421
+ });
422
+ }
423
+ const deliveredAmount = amountOutAlongPath(amountIn, steps);
424
+ if (deliveredAmount <= 0n) {
425
+ return { outcome: 'refused', refusal: { kind: 'no-quote', reason: 'no liquidity on this PulseX path', minimumAmount: null } };
426
+ }
427
+ const priceImpactPercent = priceImpactAgainstMarginal({
428
+ amountIn,
429
+ amountOut: deliveredAmount,
430
+ referenceAmountIn: amountIn / PULSEX_MARGINAL_REFERENCE_DIVISOR,
431
+ referenceAmountOut: amountOutAlongPath(amountIn / PULSEX_MARGINAL_REFERENCE_DIVISOR, steps),
432
+ });
433
+ // Left off (not zero) when the reference trade rounds away: an unknown
434
+ // impact must stay unknown so the quality gate withholds the plan.
435
+ return priceImpactPercent === null
436
+ ? { outcome: 'delivered', deliveredAmount }
437
+ : { outcome: 'delivered', deliveredAmount, priceImpactPercent };
438
+ };
439
+ return {
440
+ id: 'PulseX',
441
+ edgesFrom,
442
+ ask,
443
+ prepare,
444
+ exact,
445
+ isRateLimited: false,
446
+ // A same-chain swap requires the reader's own wallet signature — never
447
+ // a by-hand deposit.
448
+ depositMode: 'wallet-signed',
449
+ sender: 'swap',
450
+ // A same-chain swap settles with the transaction itself.
451
+ settlementSeconds: 0,
452
+ };
453
+ };
@@ -0,0 +1,25 @@
1
+ import { type AssetNode, type HopEdge } from '../search/graph.js';
2
+ import type { CarrierAskAnswer, CarrierExactResult } from './ask-answer.js';
3
+ /**
4
+ * Builds the native wrap/unwrap carrier as a plug-in.
5
+ *
6
+ * NO OPTIONS, NO `prepare`. There is no local read to batch — a wrap has no
7
+ * minimum, no maximum, no fee rate and no reserve to be wrong about; it is a
8
+ * pure 1:1 identity over every chain `config.ts` names a wrapped-native
9
+ * token for. `exact` alone is total over every edge this carrier offers.
10
+ *
11
+ * @returns the carrier — `id: 'wrap'`
12
+ */
13
+ export declare const createWrapCarrier: () => {
14
+ id: string;
15
+ edgesFrom: (from: AssetNode, to: AssetNode) => readonly HopEdge[];
16
+ ask: ({ edge, inputAmount }: {
17
+ edge: HopEdge;
18
+ inputAmount: bigint;
19
+ }) => Promise<CarrierAskAnswer>;
20
+ exact: (edge: HopEdge, amountIn: bigint) => CarrierExactResult | null;
21
+ isRateLimited: boolean;
22
+ depositMode: "wallet-signed";
23
+ sender: "swap";
24
+ settlementSeconds: number;
25
+ };
@@ -0,0 +1,115 @@
1
+ import { getAddress, zeroAddress } from 'viem';
2
+ import { nativeAssetOut, toChain } from '../../config.js';
3
+ import { assetNodeKey } from '../search/graph.js';
4
+ /**
5
+ * Native-coin wrap/unwrap (PLS <-> WPLS on PulseChain, and the same pair on
6
+ * every other Ethereum-Virtual-Machine chain the search graph touches — ETH
7
+ * <-> WETH, BNB <-> WBNB) as a plug-in `Carrier` for `@gibs/quotes`'s
8
+ * route-search engine. GIBS-SPECIFIC in the sense `omnibridge.ts`/`pulsex.ts`
9
+ * are (see `omnibridge.ts`'s head doc for why this returns a plain object
10
+ * shaped like `Carrier` rather than importing that type) — though the
11
+ * carrier itself names no chain: it derives every wrap pair from
12
+ * `config.ts`'s own `nativeAssetOut` map, so a chain added there gains a
13
+ * wrap edge with no change here.
14
+ *
15
+ * WHY THIS IS A SEPARATE CARRIER FROM PulseX. `pulsex.ts`'s own
16
+ * `readPulseXAmountOut` (via `pulseXSwapPathForEdge` in
17
+ * `packages/ui/src/lib/state/useRouteSearch.ts`) quotes native Pulse AS
18
+ * Wrapped Pulse — both map to the same `quotePathAddress` — so a PLS<->WPLS
19
+ * pair collapses to "no swap to quote" and PulseX has never carried it.
20
+ * There is no PulseX POOL for native-versus-wrapped; it is a 1:1 contract
21
+ * call (`deposit()`/`withdraw(uint256)`), not a trade, so it needed its own
22
+ * carrier rather than a PulseX special case.
23
+ *
24
+ * SENDER: `'swap'`, THE SAME NAME `pulsex.ts` USES — the reader's own
25
+ * connected wallet signs a `deposit()`/`withdraw(uint256)` call on the
26
+ * wrapped token's own contract (WETH9-style; confirmed present on WPLS by
27
+ * bytecode selector 2026-09-24), exactly the wallet a same-chain PulseX swap
28
+ * already uses. WHETHER `settings.ts`'s EXISTING swap-transaction builder
29
+ * (`pulseXSwapTransactionInputs`) CAN BUILD THIS CALL: NO, not without a
30
+ * change there — that builder only ever targets a PulseX ROUTER address and
31
+ * encodes `swapExact*` calldata; a wrap needs `to` = the wrapped token
32
+ * itself and `data` = plain `deposit()`/`withdraw(amount)` calldata, which
33
+ * is a different (and simpler) shape. Sharing the `'swap'` sender name says
34
+ * "the same wallet, the same signing step in the flow" — it does not claim
35
+ * the existing builder already emits this calldata. Building that call is
36
+ * `settings.ts`'s job, out of this carrier's scope; this module answers only
37
+ * the route-search question ("what does this edge deliver, exactly").
38
+ */
39
+ /**
40
+ * Whether this carrier can wrap/unwrap directly between `from` and `to`:
41
+ * same Ethereum-Virtual-Machine chain, and the two addresses are EXACTLY the
42
+ * zero-address/wrapped-native pair `config.ts`'s `nativeAssetOut` names for
43
+ * that chain — nothing else, so a plain token pair that merely happens to
44
+ * share a chain is never mistaken for a wrap.
45
+ */
46
+ const wrapCarries = (from, to) => {
47
+ if (from.ecosystem !== 'evm' || to.ecosystem !== 'evm')
48
+ return false;
49
+ if (from.chainId !== to.chainId)
50
+ return false;
51
+ const wrappedNativeAddress = nativeAssetOut[toChain(from.chainId)];
52
+ if (wrappedNativeAddress === undefined)
53
+ return false;
54
+ const wrapped = getAddress(wrappedNativeAddress).toLowerCase();
55
+ const fromAddress = from.address.toLowerCase();
56
+ const toAddress = to.address.toLowerCase();
57
+ const fromIsNative = fromAddress === zeroAddress;
58
+ const toIsNative = toAddress === zeroAddress;
59
+ if (fromIsNative)
60
+ return toAddress === wrapped;
61
+ if (toIsNative)
62
+ return fromAddress === wrapped;
63
+ return false;
64
+ };
65
+ /** Every wrap/unwrap edge is worth exactly `amountIn`, in `edge.to`'s base units — native and wrapped share the same decimals on every chain this carrier has ever seen (18, both sides, everywhere `nativeAssetOut` names). */
66
+ const wrapExact = (edge, amountIn) => {
67
+ if (!wrapCarries(edge.from, edge.to))
68
+ return null;
69
+ return { outcome: 'delivered', deliveredAmount: amountIn };
70
+ };
71
+ /**
72
+ * Builds the native wrap/unwrap carrier as a plug-in.
73
+ *
74
+ * NO OPTIONS, NO `prepare`. There is no local read to batch — a wrap has no
75
+ * minimum, no maximum, no fee rate and no reserve to be wrong about; it is a
76
+ * pure 1:1 identity over every chain `config.ts` names a wrapped-native
77
+ * token for. `exact` alone is total over every edge this carrier offers.
78
+ *
79
+ * @returns the carrier — `id: 'wrap'`
80
+ */
81
+ export const createWrapCarrier = () => {
82
+ const edgesFrom = (from, to) => {
83
+ if (assetNodeKey(from) === assetNodeKey(to))
84
+ return [];
85
+ if (!wrapCarries(from, to))
86
+ return [];
87
+ return [{ carrier: 'wrap', from, to, fusesNext: false, origin: 'wallet', floors: [] }];
88
+ };
89
+ const ask = async ({ edge, inputAmount }) => {
90
+ const result = wrapExact(edge, inputAmount);
91
+ if (result === null || result.outcome === 'refused') {
92
+ return { ok: false, refusal: { kind: 'cannot-carry', reason: 'not a native/wrapped pair', minimumAmount: null } };
93
+ }
94
+ return {
95
+ ok: true,
96
+ deliveredAmount: result.deliveredAmount,
97
+ // 1:1, always — there is no slippage to guarantee against.
98
+ minimumDeliveredAmount: result.deliveredAmount,
99
+ claimedDurationSeconds: 0,
100
+ };
101
+ };
102
+ return {
103
+ id: 'wrap',
104
+ edgesFrom,
105
+ ask,
106
+ exact: wrapExact,
107
+ isRateLimited: false,
108
+ // A `deposit()`/`withdraw()` call requires the reader's own wallet
109
+ // signature — never a by-hand deposit.
110
+ depositMode: 'wallet-signed',
111
+ sender: 'swap',
112
+ // A same-chain call settles with the transaction itself.
113
+ settlementSeconds: 0,
114
+ };
115
+ };