@gibs/bridge-sdk 1.17.0 → 1.19.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 (56) hide show
  1. package/README.md +85 -1
  2. package/dist/cjs/config.d.ts +18 -3
  3. package/dist/cjs/config.js +49 -17
  4. package/dist/cjs/consolidated-quote.js +3 -0
  5. package/dist/cjs/execution.d.ts +17 -4
  6. package/dist/cjs/execution.js +40 -13
  7. package/dist/cjs/index.d.ts +1 -0
  8. package/dist/cjs/index.js +1 -0
  9. package/dist/cjs/near-intents-route.d.ts +13 -0
  10. package/dist/cjs/near-intents-route.js +19 -3
  11. package/dist/cjs/prices.d.ts +10 -0
  12. package/dist/cjs/prices.js +54 -2
  13. package/dist/cjs/routing/candidate-route.d.ts +46 -1
  14. package/dist/cjs/routing/candidate-route.js +47 -2
  15. package/dist/cjs/routing/carrier-provider-key.d.ts +31 -0
  16. package/dist/cjs/routing/carrier-provider-key.js +42 -0
  17. package/dist/cjs/routing/carriers/pulsex.d.ts +50 -25
  18. package/dist/cjs/routing/carriers/pulsex.js +135 -85
  19. package/dist/cjs/routing/index.d.ts +7 -3
  20. package/dist/cjs/routing/index.js +11 -2
  21. package/dist/cjs/routing/route-data.d.ts +47 -5
  22. package/dist/cjs/routing/search/plan-route-data.d.ts +17 -1
  23. package/dist/cjs/routing/search/plan-route-data.js +39 -13
  24. package/dist/cjs/routing/swap-transactions.d.ts +7 -3
  25. package/dist/cjs/routing/swap-transactions.js +6 -4
  26. package/dist/cjs/swap-venues.d.ts +43 -0
  27. package/dist/cjs/swap-venues.js +29 -0
  28. package/dist/cjs/types.d.ts +8 -0
  29. package/dist/config.d.ts +18 -3
  30. package/dist/config.js +48 -16
  31. package/dist/consolidated-quote.js +3 -0
  32. package/dist/execution.d.ts +17 -4
  33. package/dist/execution.js +41 -14
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.js +1 -0
  36. package/dist/near-intents-route.d.ts +13 -0
  37. package/dist/near-intents-route.js +17 -2
  38. package/dist/prices.d.ts +10 -0
  39. package/dist/prices.js +52 -1
  40. package/dist/routing/candidate-route.d.ts +46 -1
  41. package/dist/routing/candidate-route.js +45 -1
  42. package/dist/routing/carrier-provider-key.d.ts +31 -0
  43. package/dist/routing/carrier-provider-key.js +37 -0
  44. package/dist/routing/carriers/pulsex.d.ts +50 -25
  45. package/dist/routing/carriers/pulsex.js +133 -84
  46. package/dist/routing/index.d.ts +7 -3
  47. package/dist/routing/index.js +4 -2
  48. package/dist/routing/route-data.d.ts +47 -5
  49. package/dist/routing/search/plan-route-data.d.ts +17 -1
  50. package/dist/routing/search/plan-route-data.js +40 -14
  51. package/dist/routing/swap-transactions.d.ts +7 -3
  52. package/dist/routing/swap-transactions.js +7 -5
  53. package/dist/swap-venues.d.ts +43 -0
  54. package/dist/swap-venues.js +24 -0
  55. package/dist/types.d.ts +8 -0
  56. package/package.json +3 -3
@@ -2,8 +2,9 @@ import { multicallRead } from '@gibs/common/multicall';
2
2
  import { getAddress, isAddress, parseAbi, zeroAddress } from 'viem';
3
3
  import * as abis from '../../abis.js';
4
4
  import { chainsMetadata } from '../../chains.js';
5
- import { Chains, nativeAssetOut, toChain, uniV2Factories, uniV2Routers } from '../../config.js';
5
+ import { defaultSwapVenues, nativeAssetOut, toChain } from '../../config.js';
6
6
  import { sameAddress } from '../../ecosystems.js';
7
+ import { findSwapVenue, resolveSwapVenues } from '../../swap-venues.js';
7
8
  import { pulseXConversionSwapPath } from '../providers.js';
8
9
  import { PULSEX_MARGINAL_REFERENCE_DIVISOR, pulsechainChainId } from '../producers.js';
9
10
  import { assetNodeKey } from '../search/graph.js';
@@ -18,57 +19,73 @@ import { bestPulseXPathQuote, candidatePulseXPaths, pulseXHubTokens } from './pu
18
19
  * See `omnibridge.ts`'s head doc for why this returns a plain object shaped
19
20
  * like `Carrier` rather than importing that type.
20
21
  */
21
- /** WPLS, the wrapped-native intermediary {@link pulseXConversionSwapPath} routes conversions through. */
22
- const wplsAddress = nativeAssetOut[Chains.PLS];
23
- /** The hub addresses every candidate path is built from — see {@link pulseXHubTokens}. */
22
+ /** The hub addresses every candidate path on PulseChain is built from — see {@link pulseXHubTokens}. */
24
23
  const hubAddresses = pulseXHubTokens.map((hub) => hub.address);
25
24
  /**
26
- * Every PulseX deployment on PulseChain, V1 first: `uniV2Routers` paired with
27
- * `uniV2Factories` by index. Each factory is the router's own
28
- * `router.factory()` read on chain — see `uniV2Factories`'s own doc in
29
- * `config.ts`. A router with no recorded factory is left out, because its
30
- * pools cannot be read.
25
+ * The wrapped-native token a chain's swaps route through, or undefined when
26
+ * this package does not know one for the chain. Without it a path on that
27
+ * chain can only be direct.
28
+ *
29
+ * @param chainId - the numeric chain id
31
30
  */
32
- export const pulseXVenues = (uniV2Routers[Chains.PLS] ?? []).flatMap((router, index) => {
33
- const factory = uniV2Factories[Chains.PLS]?.[index];
34
- return factory === undefined ? [] : [{ router: getAddress(router), factory: getAddress(factory) }];
35
- });
31
+ const wrappedNativeFor = (chainId) => {
32
+ const wrapped = nativeAssetOut[toChain(chainId)];
33
+ return wrapped === undefined ? undefined : getAddress(wrapped);
34
+ };
36
35
  /**
37
- * The venue an edge with no {@link HopEdge.venue} runs on: V1, index 0. Every
38
- * PulseX edge built before venues existed was quoted and signed on V1 — the
39
- * same-chain swap sender's own hand-built edge still is — so this keeps that
40
- * meaning rather than guessing a new one.
36
+ * The hub tokens a path on `chainId` may pass through. PulseChain has the
37
+ * full list; any other chain has only its wrapped-native token, because this
38
+ * package knows no other hubs.
41
39
  */
42
- const defaultPulseXVenue = pulseXVenues[0];
40
+ const hubsFor = (chainId) => {
41
+ if (chainId === pulsechainChainId)
42
+ return hubAddresses;
43
+ const wrapped = wrappedNativeFor(chainId);
44
+ return wrapped === undefined ? [] : [wrapped];
45
+ };
43
46
  /**
44
- * The router a PulseX edge must be signed on — the one its figure was quoted
45
- * against. Reads the edge's own {@link HopEdge.venue}; an edge with none is a
46
- * V1 edge (see {@link defaultPulseXVenue}).
47
- *
48
- * @param edge - the edge to sign
49
- * @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
+ * Every PulseX deployment on PulseChain, V1 first: the PulseChain entry of
48
+ * {@link defaultSwapVenues}. Frozen. It lists the DEFAULTS only — a carrier
49
+ * built with its own `venues` offers those instead.
50
50
  */
51
- export const pulseXRouterForEdge = (edge) => pulseXVenueForEdge(edge)?.router ?? null;
51
+ export const pulseXVenues = defaultSwapVenues[pulsechainChainId] ?? [];
52
52
  /**
53
- * The {@link PulseXVenue} a PulseX edge runs on — see {@link pulseXRouterForEdge}.
53
+ * The {@link SwapVenue} a swap edge runs on: the one its figure was quoted
54
+ * against. Reads the edge's own {@link HopEdge.venue}; an edge with none runs
55
+ * on the FIRST venue of its chain (V1 for PulseX) — every swap edge built
56
+ * before venues existed was quoted and signed there, and the same-chain swap
57
+ * sender's own hand-built edge still is.
54
58
  *
55
- * @param edge - the edge
56
- * @returns the venue, or null when there is none
59
+ * NEVER FALLS BACK TO ANOTHER CHAIN OR ANOTHER ROUTER. An edge that names a
60
+ * router `venues` does not list gets null, so the caller can refuse it.
61
+ *
62
+ * @param venues - the venues the caller allows
63
+ * @param edge - the edge to sign; `from` names the chain, and defaults to PulseChain when absent
64
+ * @returns the venue, or null when the edge is not a swap edge or names a router that is not in `venues`
57
65
  */
58
- const pulseXVenueForEdge = (edge) => {
66
+ export const venueForEdge = (venues, edge) => {
59
67
  if (edge.carrier !== 'PulseX')
60
68
  return null;
69
+ const chainId = edge.from?.chainId ?? pulsechainChainId;
61
70
  if (edge.venue === undefined)
62
- return defaultPulseXVenue ?? null;
63
- const venue = edge.venue;
64
- return pulseXVenues.find((candidate) => sameAddress(candidate.router, venue, 'evm')) ?? null;
71
+ return venues[chainId]?.[0] ?? null;
72
+ return findSwapVenue(venues, chainId, edge.venue);
65
73
  };
66
74
  /**
67
- * A node's own address for a PulseX pool lookup: native PLS (the zero
68
- * address) is not a token the router or its factory ever holds a pool for —
69
- * only Wrapped PLS is — so a native node resolves to {@link wplsAddress}
75
+ * The router a PulseX edge must be signed on, judged against the DEFAULT
76
+ * venues only. Prefer {@link venueForEdge} with the venues your host passes.
77
+ *
78
+ * @param edge - the edge to sign
79
+ * @returns the router, or null when the edge is not a swap edge or names a router that is not a default
80
+ */
81
+ export const pulseXRouterForEdge = (edge) => venueForEdge(defaultSwapVenues, edge)?.router ?? null;
82
+ /**
83
+ * A node's own address for a pool lookup: native (the zero address) is not a
84
+ * token the router or its factory ever holds a pool for — only the chain's
85
+ * wrapped-native token is — so a native node resolves to that wrapped token
70
86
  * before it ever reaches {@link pulseXConversionSwapPath}. Every other node
71
- * is unchanged.
87
+ * is unchanged. A chain with no known wrapped-native token leaves a native
88
+ * node as it is; it has no pool, so no path prices.
72
89
  *
73
90
  * MIRRORS `useRouteSearch.ts`'s `quotePathAddress` (the UI's async `ask`
74
91
  * already resolves native this way through that function) — this is the
@@ -76,9 +93,9 @@ const pulseXVenueForEdge = (edge) => {
76
93
  * UI's `readAmountOut` at all; see {@link pulseXCandidatePaths}'s own doc.
77
94
  *
78
95
  * @param node - the node to resolve
79
- * @returns the node's own address, or {@link wplsAddress} for native PLS
96
+ * @returns the node's own address, or its chain's wrapped-native address for a native node
80
97
  */
81
- const poolTokenAddress = (node) => (sameAddress(node.address, zeroAddress, 'evm') ? wplsAddress : node.address);
98
+ const poolTokenAddress = (node) => sameAddress(node.address, zeroAddress, 'evm') ? (wrappedNativeFor(node.chainId) ?? zeroAddress) : node.address;
82
99
  /**
83
100
  * The fixed Wrapped-PLS path from `from` to `to` — `[from, WPLS, to]`, or
84
101
  * `[from, to]` when one side is WPLS — or null when both ends resolve to the
@@ -103,7 +120,8 @@ export const pulseXWrappedNativePath = (from, to) => {
103
120
  const toAddress = poolTokenAddress(to);
104
121
  if (sameAddress(fromAddress, toAddress, 'evm'))
105
122
  return null;
106
- return pulseXConversionSwapPath(fromAddress, toAddress, wplsAddress);
123
+ const wrappedNative = wrappedNativeFor(from.chainId);
124
+ return wrappedNative === undefined ? [fromAddress, toAddress] : pulseXConversionSwapPath(fromAddress, toAddress, wrappedNative);
107
125
  };
108
126
  /**
109
127
  * Every path a PulseX hop from `from` to `to` may be priced along, or none
@@ -122,21 +140,27 @@ const pulseXCandidatePaths = (from, to) => {
122
140
  const toAddress = poolTokenAddress(to);
123
141
  if (sameAddress(fromAddress, toAddress, 'evm'))
124
142
  return [];
125
- return candidatePulseXPaths({ from: fromAddress, to: toAddress, wrappedNative: wplsAddress, hubs: hubAddresses });
143
+ const wrappedNative = wrappedNativeFor(from.chainId);
144
+ // No known wrapped-native token: the direct pool is the only path.
145
+ if (wrappedNative === undefined)
146
+ return [[fromAddress, toAddress]];
147
+ return candidatePulseXPaths({ from: fromAddress, to: toAddress, wrappedNative, hubs: hubsFor(from.chainId) });
126
148
  };
127
149
  /**
128
- * Whether PulseX can carry funds directly from `from` to `to`: both must be
129
- * Ethereum-Virtual-Machine tokens on PulseChain itself — PulseX runs only on
130
- * PulseChain, never on an entry chain — that resolve to two different pool
131
- * tokens, so there is at least one path to price.
150
+ * Whether a swap venue in `venues` can carry funds directly from `from` to
151
+ * `to`: both must be Ethereum-Virtual-Machine tokens on the SAME chain, that
152
+ * chain must have at least one venue, and the two must resolve to different
153
+ * pool tokens, so there is at least one path to price.
132
154
  *
133
155
  * NO TOKEN ALLOWLIST IS CONSULTED, on purpose: whether a pool exists is the
134
156
  * reserve read's question, answered in `prepare`, not this one's.
135
157
  */
136
- const pulseXCarries = (from, to) => {
158
+ const pulseXCarries = (venues, from, to) => {
137
159
  if (from.ecosystem !== 'evm' || to.ecosystem !== 'evm')
138
160
  return false;
139
- if (from.chainId !== pulsechainChainId || to.chainId !== pulsechainChainId)
161
+ if (from.chainId !== to.chainId)
162
+ return false;
163
+ if ((venues[from.chainId] ?? []).length === 0)
140
164
  return false;
141
165
  return pulseXCandidatePaths(from, to).length > 0;
142
166
  };
@@ -173,8 +197,9 @@ export const pulseXSwapPathForHop = (hop) => {
173
197
  return path;
174
198
  };
175
199
  /**
176
- * PulseX's own constant-product swap fee, as a numerator over
177
- * {@link PULSEX_FEE_DENOMINATOR} — the SAME `getAmountOut` formula every
200
+ * PulseX's own constant-product swap fee in basis points (29 of 10000, a
201
+ * numerator of 9971) — the default for {@link amountOutAlongPath}; a
202
+ * venue carries its own `feeBasisPoints`. The SAME `getAmountOut` formula every
178
203
  * Uniswap-V2-style router uses: `amountOut = floor(amountIn * feeNumerator *
179
204
  * reserveOut / (reserveIn * feeDenominator + amountIn * feeNumerator))`.
180
205
  *
@@ -187,16 +212,16 @@ export const pulseXSwapPathForHop = (hop) => {
187
212
  * this claim against live chain data rather than trusting the arithmetic
188
213
  * alone.
189
214
  */
190
- const PULSEX_FEE_NUMERATOR = 9971n;
191
- /** The denominator {@link PULSEX_FEE_NUMERATOR} is a fraction of. */
192
- const PULSEX_FEE_DENOMINATOR = 10000n;
215
+ const PULSEX_FEE_BASIS_POINTS = 29;
216
+ /** Basis points in a whole: the denominator of every venue's fee. */
217
+ const BASIS_POINTS_DENOMINATOR = 10000n;
193
218
  /** The minimal read-only factory ABI `prepare` needs — just enough to resolve a pool's address from its two tokens. */
194
219
  const univ2FactoryAbi = parseAbi([
195
220
  'function getPair(address tokenA, address tokenB) external view returns (address pair)',
196
221
  ]);
197
222
  /**
198
223
  * The Uniswap-V2-style constant-product `getAmountOut`, applied with
199
- * {@link PULSEX_FEE_NUMERATOR}. Reproduces the router's own on-chain formula
224
+ * the venue's fee. Reproduces the router's own on-chain formula
200
225
  * exactly — see `pulsex-carrier.fork.test.ts` — so PRICE IMPACT IS ALREADY
201
226
  * IN HERE: this is the same nonlinear function the router evaluates, not a
202
227
  * flat-fee approximation with an impact correction bolted on afterward.
@@ -204,14 +229,15 @@ const univ2FactoryAbi = parseAbi([
204
229
  * @param amountIn - the input amount, in `reserveIn`'s token's base units
205
230
  * @param reserveIn - the pool's reserve of the input token
206
231
  * @param reserveOut - the pool's reserve of the output token
232
+ * @param feeBasisPoints - the venue's swap fee, in basis points
207
233
  * @returns the output amount, in `reserveOut`'s token's base units; 0 when either reserve is 0
208
234
  */
209
- const constantProductAmountOut = (amountIn, reserveIn, reserveOut) => {
235
+ const constantProductAmountOut = (amountIn, reserveIn, reserveOut, feeBasisPoints) => {
210
236
  if (reserveIn <= 0n || reserveOut <= 0n || amountIn <= 0n)
211
237
  return 0n;
212
- const amountInWithFee = amountIn * PULSEX_FEE_NUMERATOR;
238
+ const amountInWithFee = amountIn * (BASIS_POINTS_DENOMINATOR - BigInt(feeBasisPoints));
213
239
  const numerator = amountInWithFee * reserveOut;
214
- const denominator = reserveIn * PULSEX_FEE_DENOMINATOR + amountInWithFee;
240
+ const denominator = reserveIn * BASIS_POINTS_DENOMINATOR + amountInWithFee;
215
241
  return numerator / denominator;
216
242
  };
217
243
  /**
@@ -220,9 +246,10 @@ const constantProductAmountOut = (amountIn, reserveIn, reserveOut) => {
220
246
  *
221
247
  * @param amountIn - the amount entering the first pool
222
248
  * @param steps - the pools in order of travel
249
+ * @param feeBasisPoints - the venue's swap fee in basis points; PulseX's 29 when omitted
223
250
  * @returns the amount leaving the last pool; 0 when any pool cannot pay out
224
251
  */
225
- export const amountOutAlongPath = (amountIn, steps) => steps.reduce((runningAmount, step) => constantProductAmountOut(runningAmount, step.reserveIn, step.reserveOut), amountIn);
252
+ export const amountOutAlongPath = (amountIn, steps, feeBasisPoints = PULSEX_FEE_BASIS_POINTS) => steps.reduce((runningAmount, step) => constantProductAmountOut(runningAmount, step.reserveIn, step.reserveOut, feeBasisPoints), amountIn);
226
253
  /**
227
254
  * How far a trade's executed rate falls below the marginal (near-spot) rate,
228
255
  * in percent. The marginal rate is a tiny reference trade through the same
@@ -258,7 +285,7 @@ const poolKey = (tokenA, tokenB) => {
258
285
  return `${lower}|${higher}`;
259
286
  };
260
287
  /** 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. */
261
- const venuePoolKey = (venue, tokenA, tokenB) => `${venue.factory.toLowerCase()}|${poolKey(tokenA, tokenB)}`;
288
+ const venuePoolKey = (venue, tokenA, tokenB) => `${venue.chainId}|${venue.factory.toLowerCase()}|${poolKey(tokenA, tokenB)}`;
262
289
  /** Every consecutive (tokenIn, tokenOut) hop a PulseX swap path implies. */
263
290
  const hopsAlongPath = (path) => path.slice(0, -1).map((tokenIn, index) => [tokenIn, path[index + 1]]);
264
291
  /**
@@ -318,16 +345,36 @@ const stepsAlongPath = (options) => {
318
345
  */
319
346
  const preparePulseXReserves = async ({ nodes, venues, clientForChain, }) => {
320
347
  const reservesByPoolKey = new Map();
321
- const client = clientForChain(pulsechainChainId);
322
- if (client === null || venues.length === 0)
348
+ const chainIds = Object.keys(venues)
349
+ .map(Number)
350
+ .filter((chainId) => (venues[chainId] ?? []).length > 0);
351
+ for (const chainId of chainIds) {
352
+ const chainReserves = await preparePulseXReservesOnChain({ nodes, chainId, venues, clientForChain });
353
+ for (const [key, reserves] of chainReserves)
354
+ reservesByPoolKey.set(key, reserves);
355
+ }
356
+ return reservesByPoolKey;
357
+ };
358
+ /**
359
+ * One chain's share of {@link preparePulseXReserves}: two multicalls, the
360
+ * `getPair` reads and then the `getReserves` reads, over every venue of the
361
+ * chain. A chain with no client, or with no chain description to multicall
362
+ * against, reads nothing.
363
+ */
364
+ const preparePulseXReservesOnChain = async ({ nodes, chainId, venues, clientForChain, }) => {
365
+ const reservesByPoolKey = new Map();
366
+ const client = clientForChain(chainId);
367
+ const knownChain = chainsMetadata[toChain(chainId)];
368
+ const chain = knownChain ?? client?.chain;
369
+ if (client === null || chain === undefined)
323
370
  return reservesByPoolKey;
324
- const pulsechainNodes = nodes.filter((node) => node.ecosystem === 'evm' && node.chainId === pulsechainChainId);
371
+ const chainNodes = nodes.filter((node) => node.ecosystem === 'evm' && node.chainId === chainId);
325
372
  const poolTokenPairs = new Map();
326
- for (const from of pulsechainNodes) {
327
- for (const to of pulsechainNodes) {
373
+ for (const from of chainNodes) {
374
+ for (const to of chainNodes) {
328
375
  if (assetNodeKey(from) === assetNodeKey(to))
329
376
  continue;
330
- if (!pulseXCarries(from, to))
377
+ if (!pulseXCarries(venues, from, to))
331
378
  continue;
332
379
  for (const path of pulseXCandidatePaths(from, to)) {
333
380
  for (const [tokenIn, tokenOut] of hopsAlongPath(path)) {
@@ -338,14 +385,14 @@ const preparePulseXReserves = async ({ nodes, venues, clientForChain, }) => {
338
385
  }
339
386
  if (poolTokenPairs.size === 0)
340
387
  return reservesByPoolKey;
341
- const poolQueries = venues.flatMap((venue) => Array.from(poolTokenPairs.values()).map(([tokenIn, tokenOut]) => ({
388
+ const poolQueries = (venues[chainId] ?? []).flatMap((venue) => Array.from(poolTokenPairs.values()).map(([tokenIn, tokenOut]) => ({
342
389
  key: venuePoolKey(venue, tokenIn, tokenOut),
343
390
  factory: venue.factory,
344
391
  tokenIn,
345
392
  tokenOut,
346
393
  })));
347
394
  const factoryResults = await multicallRead({
348
- chain: chainsMetadata[toChain(pulsechainChainId)],
395
+ chain,
349
396
  client,
350
397
  abi: univ2FactoryAbi,
351
398
  calls: poolQueries.map((query) => ({
@@ -370,7 +417,7 @@ const preparePulseXReserves = async ({ nodes, venues, clientForChain, }) => {
370
417
  return reservesByPoolKey;
371
418
  const reservePoolKeys = Array.from(pairAddressByPoolKey.keys());
372
419
  const reserveResults = await multicallRead({
373
- chain: chainsMetadata[toChain(pulsechainChainId)],
420
+ chain,
374
421
  client,
375
422
  abi: abis.pair,
376
423
  calls: reservePoolKeys.map((key) => ({
@@ -395,22 +442,24 @@ const preparePulseXReserves = async ({ nodes, venues, clientForChain, }) => {
395
442
  return reservesByPoolKey;
396
443
  };
397
444
  /**
398
- * The venues a carrier offers: every known venue, or only the routers the
399
- * host names. A named router that is not a known PulseX router is a
400
- * configuration error, thrown at construction rather than offered silently.
445
+ * The venues a carrier offers: every venue in `venues`, or only the routers
446
+ * the host names. A named router that is in no venue is a configuration
447
+ * error, thrown at construction rather than offered silently.
401
448
  *
449
+ * @param venues - the resolved venues
402
450
  * @param routers - the routers the host can sign on, or undefined for all
403
- * @returns the venues, in {@link pulseXVenues} order
451
+ * @returns the venues, in their original order
404
452
  */
405
- const selectPulseXVenues = (routers) => {
453
+ const selectPulseXVenues = (venues, routers) => {
406
454
  if (routers === undefined)
407
- return pulseXVenues;
455
+ return venues;
456
+ const named = (venue) => routers.some((router) => sameAddress(venue.router, router, 'evm'));
408
457
  for (const router of routers) {
409
- if (!pulseXVenues.some((venue) => sameAddress(venue.router, router, 'evm'))) {
410
- throw new Error(`createPulseXCarrier: ${router} is not a known PulseX router on PulseChain`);
411
- }
458
+ const offered = Object.values(venues).some((list) => list.some((venue) => sameAddress(venue.router, router, 'evm')));
459
+ if (!offered)
460
+ throw new Error(`createPulseXCarrier: ${router} is not a known PulseX router in the offered swap venues`);
412
461
  }
413
- return pulseXVenues.filter((venue) => routers.some((router) => sameAddress(venue.router, router, 'evm')));
462
+ return Object.freeze(Object.fromEntries(Object.entries(venues).map(([chainId, list]) => [chainId, list.filter(named)])));
414
463
  };
415
464
  /**
416
465
  * Builds the PulseX conversion swap as a plug-in carrier.
@@ -420,16 +469,16 @@ const selectPulseXVenues = (routers) => {
420
469
  */
421
470
  export const createPulseXCarrier = (options) => {
422
471
  const { readAmountOut, clientForChain } = options;
423
- const venues = selectPulseXVenues(options.routers);
472
+ const venues = selectPulseXVenues(resolveSwapVenues(options.venues), options.routers);
424
473
  // ONE EDGE PER VENUE. Each names its router as its `venue`, so the search
425
474
  // quotes and ranks V1 and V2 as two separate hops and the better one wins
426
475
  // on its own figure — and the winner is signed on the router it names.
427
476
  const edgesFrom = (from, to) => {
428
477
  if (assetNodeKey(from) === assetNodeKey(to))
429
478
  return [];
430
- if (!pulseXCarries(from, to))
479
+ if (!pulseXCarries(venues, from, to))
431
480
  return [];
432
- return venues.map((venue) => ({
481
+ return (venues[from.chainId] ?? []).map((venue) => ({
433
482
  carrier: 'PulseX',
434
483
  from,
435
484
  to,
@@ -465,19 +514,19 @@ export const createPulseXCarrier = (options) => {
465
514
  reservesByPoolKey = await preparePulseXReserves({ nodes, venues, clientForChain });
466
515
  };
467
516
  const exact = (edge, amountIn) => {
468
- if (!pulseXCarries(edge.from, edge.to))
517
+ if (!pulseXCarries(venues, edge.from, edge.to))
469
518
  return null;
470
519
  // Quoted on the edge's OWN venue — the router it will be signed on — and
471
520
  // only on a venue this carrier offers.
472
- const venue = pulseXVenueForEdge(edge);
473
- if (venue === null || !venues.includes(venue))
521
+ const venue = venueForEdge(venues, edge);
522
+ if (venue === null)
474
523
  return null;
475
524
  // EVERY CANDIDATE PATH IS PRICED, AND THE BEST ONE WINS. A path with a
476
525
  // pool `prepare` never found is left out — the same as a router call
477
526
  // that would revert — and a path that pays out nothing is never picked.
478
527
  const quotes = pulseXCandidatePaths(edge.from, edge.to).flatMap((path) => {
479
528
  const steps = stepsAlongPath({ venue, path, reservesByPoolKey });
480
- return steps === null ? [] : [{ path, steps, deliveredAmount: amountOutAlongPath(amountIn, steps) }];
529
+ return steps === null ? [] : [{ path, steps, deliveredAmount: amountOutAlongPath(amountIn, steps, venue.feeBasisPoints) }];
481
530
  });
482
531
  // No path had every pool read — a structural non-answer, not a refusal
483
532
  // (`exact`'s own doc: `null` means "nothing to say"). NOTE THIS DOES NOT
@@ -497,7 +546,7 @@ export const createPulseXCarrier = (options) => {
497
546
  amountIn,
498
547
  amountOut: best.deliveredAmount,
499
548
  referenceAmountIn,
500
- referenceAmountOut: amountOutAlongPath(referenceAmountIn, best.steps),
549
+ referenceAmountOut: amountOutAlongPath(referenceAmountIn, best.steps, venue.feeBasisPoints),
501
550
  });
502
551
  // The path rides on the answer, so the signer signs exactly what was
503
552
  // priced (`pulseXSwapPathForHop`). Impact is left off (not zero) when the
@@ -8,12 +8,12 @@ export { netDeliveredAmount, restateInDecimals, valueEquivalentAmount } from './
8
8
  export { omnibridgeProviderForEntryChain, pulseXConversionSwapPath } from './providers.js';
9
9
  export { buildDirectCrossingCandidate } from './candidate.js';
10
10
  export type { RouteCandidate, DeferredCrossing, DirectCrossingCandidateInputs } from './candidate.js';
11
- export { buildRouteDataForCandidate, routeSignatureCount } from './candidate-route.js';
11
+ export { buildRouteDataForCandidate, routeSignatureCount, routeSignatureCountForAccount, type RouteSignatureCount, } from './candidate-route.js';
12
12
  export { COMPOSED_ROUTE_MAX_PRICE_IMPACT_PERCENT, COMPOSED_ROUTE_MAX_VALUE_GAIN_PERCENT, COMPOSED_ROUTE_MAX_VALUE_LOSS_PERCENT, assessComposedRoute, composedRouteMayBeOffered, } from './composed-route-quality.js';
13
13
  export type { ComposedRouteVerdict } from './composed-route-quality.js';
14
14
  export { entryChainAnchorsForPair } from './entry-chain-anchor.js';
15
15
  export type { EntryChainAnchor, EntryChainCrossingCall } from './entry-chain-anchor.js';
16
- export type { EstimateBasis, RouteData, RouteHopData, RouteKind, RouteOutputData, RouteSectionData, RouteTokenRef, } from './route-data.js';
16
+ export type { EstimateBasis, RouteData, RouteGasByChain, RouteHopData, RouteKind, RouteOutputData, RouteSectionData, RouteTokenRef, } from './route-data.js';
17
17
  export { usdValueOfAmount } from './usd-value.js';
18
18
  export { formatUsd, formatUsdNullable } from './format.js';
19
19
  export { BRIDGE_SETTLEMENT_ETA_MINUTES, PULSEX_MARGINAL_REFERENCE_DIVISOR, pulsechainChainId, addUsdValues, providerDisplayName, resolveEntryChainName, buildCandidateRouteData, buildConversionRouteData, buildDirectRouteData, buildPulseXSwapEstimateData, buildPulseXSwapSectionData, buildOutboundRouteData, buildSameChainSwapRouteData, candidateDeliveredUsd, candidateNetValueUsd, candidateOutsideFeesUsd, candidatePaidUsd, } from './producers.js';
@@ -24,14 +24,18 @@ export { composableHopBasis, composeRouteDelivery, hopOutputAmount, } from './se
24
24
  export type { ComposableHop, ComposableHopFigure, ComposeRefusalReason, ComposedRouteDelivery, ExchangeRate, } from './search/compose.js';
25
25
  export { GAS_TOPUP_PENALTY_FLOOR_USD, GAS_TOPUP_PENALTY_PPM, SIGNATURE_PENALTY_FLOOR_USD, SIGNATURE_PENALTY_PPM, compareRoutePlansByGasAdjustedDeliveredAmount, compareRoutePlansByScoreAdjustedDeliveredAmount, gasAdjustedDeliveredAmount, scoreRoutePlan, sectionIsFundedByLanding, waitPenaltyPpm, zeroGasCostFor, } from './search/score.js';
26
26
  export type { GasCostFor, GasCostQuote, RouteScoreInputs } from './search/score.js';
27
+ export { WRAP_CARRIER_ID, carrierProviderKey, carrierStepKind } from './carrier-provider-key.js';
28
+ export type { CarrierStepKind } from './carrier-provider-key.js';
27
29
  export { buildRouteCandidateFromPlan, buildRouteDataFromPlan, buildRouteScoreInputsFromPlan, deriveDeferredCrossingFromPlan, deriveEntryChainIdFromPlan, deriveOmnibridgeProviderFromPlan, deriveViaChainIdsFromPlan, derivePlanKind, } from './search/plan-route-data.js';
28
30
  export type { PlanCandidateInputs, PlanRouteDataInputs, PlanSectionPricing, PlanValuation, ResolveNodeToken, } from './search/plan-route-data.js';
29
31
  export { createOmnibridgeCarrier, withOmnibridgeEntryFusing, } from './carriers/omnibridge.js';
30
32
  export type { BridgeablePair, CreateOmnibridgeCarrierOptions, } from './carriers/omnibridge.js';
31
- export { createPulseXCarrier, pulseXRouterForEdge, pulseXSwapPathForHop, pulseXVenues, pulseXWrappedNativePath, } from './carriers/pulsex.js';
33
+ export { createPulseXCarrier, pulseXRouterForEdge, venueForEdge, pulseXSwapPathForHop, pulseXVenues, pulseXWrappedNativePath, } from './carriers/pulsex.js';
32
34
  export type { CreatePulseXCarrierOptions, PulseXVenue } from './carriers/pulsex.js';
33
35
  export { bestPulseXPathQuote, candidatePulseXPaths, pulseXHubTokens } from './carriers/pulsex-paths.js';
34
36
  export type { PulseXHubToken, PulseXPathQuote } from './carriers/pulsex-paths.js';
37
+ export { findSwapVenue, resolveSwapVenues } from '../swap-venues.js';
38
+ export type { SwapVenue, SwapVenues } from '../swap-venues.js';
35
39
  export { createWrapCarrier } from './carriers/wrap.js';
36
40
  export type { CarrierAskAnswer, CarrierExactResult } from './carriers/ask-answer.js';
37
41
  export { buildPulseXSwapTransaction, buildWrapTransaction, swapMinimumOut } from './swap-transactions.js';
@@ -7,7 +7,7 @@
7
7
  export { netDeliveredAmount, restateInDecimals, valueEquivalentAmount } from './amounts.js';
8
8
  export { omnibridgeProviderForEntryChain, pulseXConversionSwapPath } from './providers.js';
9
9
  export { buildDirectCrossingCandidate } from './candidate.js';
10
- export { buildRouteDataForCandidate, routeSignatureCount } from './candidate-route.js';
10
+ export { buildRouteDataForCandidate, routeSignatureCount, routeSignatureCountForAccount, } from './candidate-route.js';
11
11
  export { COMPOSED_ROUTE_MAX_PRICE_IMPACT_PERCENT, COMPOSED_ROUTE_MAX_VALUE_GAIN_PERCENT, COMPOSED_ROUTE_MAX_VALUE_LOSS_PERCENT, assessComposedRoute, composedRouteMayBeOffered, } from './composed-route-quality.js';
12
12
  export { entryChainAnchorsForPair } from './entry-chain-anchor.js';
13
13
  export { usdValueOfAmount } from './usd-value.js';
@@ -26,6 +26,7 @@ export { BRIDGE_SETTLEMENT_ETA_MINUTES, PULSEX_MARGINAL_REFERENCE_DIVISOR, pulse
26
26
  export { RANKABLE_ESTIMATE_BASES, assetNodeKey, combineComposedPathBasis, compareRoutePlansByDeliveredAmount, groupHopsBySignature, hopComposedBasis, hopEdgeKey, isConfirmedBasis, isRankableBasis, isRateLimitedCarrier, planComposedBasis, planIsConfirmed, planIsRankable, routePlanPathKey, } from './search/graph.js';
27
27
  export { composableHopBasis, composeRouteDelivery, hopOutputAmount, } from './search/compose.js';
28
28
  export { GAS_TOPUP_PENALTY_FLOOR_USD, GAS_TOPUP_PENALTY_PPM, SIGNATURE_PENALTY_FLOOR_USD, SIGNATURE_PENALTY_PPM, compareRoutePlansByGasAdjustedDeliveredAmount, compareRoutePlansByScoreAdjustedDeliveredAmount, gasAdjustedDeliveredAmount, scoreRoutePlan, sectionIsFundedByLanding, waitPenaltyPpm, zeroGasCostFor, } from './search/score.js';
29
+ export { WRAP_CARRIER_ID, carrierProviderKey, carrierStepKind } from './carrier-provider-key.js';
29
30
  // General route search — Phase 1: plan-route-data.ts. Turns a priced
30
31
  // RoutePlan into the same RouteData/RouteCandidate/RouteScoreInputs shapes
31
32
  // today's four hand-written producers emit — see that module's head doc.
@@ -38,8 +39,9 @@ export { buildRouteCandidateFromPlan, buildRouteDataFromPlan, buildRouteScoreInp
38
39
  // `searchRouteWaves` and their types from `@gibs/quotes/search`. Gibs's own
39
40
  // carriers (the omnibridge crossing, PulseX) live in `./carriers/`, below.
40
41
  export { createOmnibridgeCarrier, withOmnibridgeEntryFusing, } from './carriers/omnibridge.js';
41
- export { createPulseXCarrier, pulseXRouterForEdge, pulseXSwapPathForHop, pulseXVenues, pulseXWrappedNativePath, } from './carriers/pulsex.js';
42
+ export { createPulseXCarrier, pulseXRouterForEdge, venueForEdge, pulseXSwapPathForHop, pulseXVenues, pulseXWrappedNativePath, } from './carriers/pulsex.js';
42
43
  export { bestPulseXPathQuote, candidatePulseXPaths, pulseXHubTokens } from './carriers/pulsex-paths.js';
44
+ export { findSwapVenue, resolveSwapVenues } from '../swap-venues.js';
43
45
  export { createWrapCarrier } from './carriers/wrap.js';
44
46
  // The same-chain swap and wrap builders. The interface's section signing and
45
47
  // the SDK's execution planner (`../execution.ts`) share them.
@@ -1,6 +1,7 @@
1
1
  import type { Hex } from 'viem';
2
2
  import type { QuoteFee } from '../quote-fees.js';
3
3
  import type { ProviderKey } from '../providers.js';
4
+ import type { CarrierStepKind } from './carrier-provider-key.js';
4
5
  /**
5
6
  * The executing engine family a route runs through.
6
7
  *
@@ -91,7 +92,12 @@ export type RouteHopData = {
91
92
  /** Display name for the hop's provider, e.g. "LI.FI", "PulseChain Bridge". */
92
93
  providerName: string;
93
94
  /** Raw provider key for logo resolution when no `providerLogoUri` is supplied. */
94
- providerKey?: string | null;
95
+ providerKey?: ProviderKey | null;
96
+ /**
97
+ * What the hop does when no provider performs it (a native wrap). Such a hop
98
+ * has a null `providerKey`; the adapter draws its mark from this kind.
99
+ */
100
+ stepKind?: CarrierStepKind | null;
95
101
  /** Quote-supplied provider logo url (aggregator legs); preferred when present. */
96
102
  providerLogoUri?: string | null;
97
103
  /** Human-readable hop title, e.g. "Bridge to PulseChain". */
@@ -110,11 +116,27 @@ export type RouteHopData = {
110
116
  * completes. Null/undefined when the provider named no output token.
111
117
  */
112
118
  toToken?: RouteTokenRef | null;
113
- /** Per-hop fee in United States dollars as a NUMBER, or null when unknown. */
119
+ /** Per-hop fee in United States dollars as a NUMBER, or null when unknown. A search route's figure INCLUDES the signature's network gas (carried on the section's first hop). */
114
120
  feeUsdValue?: number | null;
121
+ /** The part of {@link feeUsdValue} that is not network gas: what the hop costs in value. Absent when the producer states no gas split. */
122
+ feeExcludingGasUsdValue?: number | null;
123
+ /** The network gas of the signature this hop opens, in dollars; null or absent on every hop that opens none, and when gas is unknown. */
124
+ gasUsdValue?: number | null;
115
125
  /** True when this hop runs automatically inside the section's single signature. */
116
126
  automatic?: boolean;
117
127
  };
128
+ /**
129
+ * The network gas a route needs on one signing chain, summed across the
130
+ * signatures it signs there.
131
+ */
132
+ export type RouteGasByChain = {
133
+ /** The chain the signatures broadcast on. */
134
+ chainId: number;
135
+ /** The native coin they burn, in base units (wei), or null when the chain's gas price is unknown. */
136
+ amountWei: bigint | null;
137
+ /** The same gas in dollars, or null when the amount or the native price is unknown. */
138
+ usdValue: number | null;
139
+ };
118
140
  /**
119
141
  * What a signature section's funds LAND IN when that step completes — the stop
120
142
  * the money actually comes to rest at, named in tokens rather than only drawn as
@@ -181,7 +203,9 @@ export type RouteSectionData = {
181
203
  /** Short label naming what this signature does, e.g. "Bridge to PulseChain". */
182
204
  label: string;
183
205
  /** Raw provider key for the service executing this signature, or null. */
184
- providerKey?: string | null;
206
+ providerKey?: ProviderKey | null;
207
+ /** What the signature does when no provider performs it (a native wrap); see {@link RouteHopData.stepKind}. */
208
+ stepKind?: CarrierStepKind | null;
185
209
  /**
186
210
  * The token-icon flow this signature walks, in raw references: the source, then
187
211
  * every INTERMEDIATE stop the funds pass through, then the destination. A
@@ -202,8 +226,12 @@ export type RouteSectionData = {
202
226
  * a cost.
203
227
  */
204
228
  unspent?: RouteUnspentData | null;
205
- /** Section total fee in United States dollars as a NUMBER, or null. */
229
+ /** Section total fee in United States dollars as a NUMBER, or null. A search route's figure INCLUDES this signature's network gas. */
206
230
  feeUsdValue?: number | null;
231
+ /** The part of {@link feeUsdValue} that is not network gas. Absent when the producer states no gas split. */
232
+ feeExcludingGasUsdValue?: number | null;
233
+ /** This signature's network gas in dollars, or null/absent when unknown. */
234
+ gasUsdValue?: number | null;
207
235
  /** Numeric inputs for an optional fee breakdown line (conversion mode). */
208
236
  feePercent?: number | null;
209
237
  /** Price-impact percentage for an optional fee breakdown line (conversion mode). */
@@ -316,8 +344,22 @@ export type RouteData = {
316
344
  sections: RouteSectionData[];
317
345
  /** What the route delivers — used for ranking and the output row. */
318
346
  output: RouteOutputData;
319
- /** Aggregate all-in cost in United States dollars as a NUMBER, or null. */
347
+ /**
348
+ * Aggregate ALL-IN cost in United States dollars as a NUMBER, or null:
349
+ * value lost plus network gas. The ranking reads this figure. For the
350
+ * figure without gas, read {@link costExcludingGasUsdValue}.
351
+ */
320
352
  totalCostUsdValue?: number | null;
353
+ /** {@link totalCostUsdValue} without network gas: the value the reader gives up. Absent when the producer states no gas split. */
354
+ costExcludingGasUsdValue?: number | null;
355
+ /**
356
+ * The network gas the reader pays across every signature, in dollars, or
357
+ * null/absent when no signing chain has a gas reading. Never zero for
358
+ * "unknown".
359
+ */
360
+ gasCostUsdValue?: number | null;
361
+ /** The gas by signing chain, in signing order: what to hold, and in which coin. */
362
+ gasByChain?: RouteGasByChain[];
321
363
  /** The user's net United States dollar outcome; the figure routes are ranked on. */
322
364
  netValueUsd?: number | null;
323
365
  /** Estimated end-to-end duration in minutes, or null when unknown. */
@@ -3,7 +3,7 @@ import type { Provider } from '../../config.js';
3
3
  import type { QuoteFee } from '../../quote-fees.js';
4
4
  import type { Token } from '../../types.js';
5
5
  import type { DeferredCrossing, RouteCandidate } from '../candidate.js';
6
- import type { RouteData, RouteKind } from '../route-data.js';
6
+ import type { RouteData, RouteGasByChain, RouteKind } from '../route-data.js';
7
7
  import type { RouteScoreInputs } from './score.js';
8
8
  import { type AssetNode, type RoutePlan } from './graph.js';
9
9
  /**
@@ -141,6 +141,16 @@ export type PlanSectionPricing = {
141
141
  * caller passes shares that add up to `feeUsdValue`, or nothing.
142
142
  */
143
143
  hopFeeUsdValues?: readonly (number | null)[];
144
+ /**
145
+ * The gas split of `feeUsdValue`, from a producer that prices network gas
146
+ * apart: `feeExcludingGasUsdValue` is the cost without gas, `gasUsdValue` the
147
+ * signature's gas (null when unknown). `hopFeeExcludingGasUsdValues` splits
148
+ * the first the way `hopFeeUsdValues` splits the all-in figure. The gas sits
149
+ * on the section's first hop.
150
+ */
151
+ feeExcludingGasUsdValue?: number | null;
152
+ gasUsdValue?: number | null;
153
+ hopFeeExcludingGasUsdValues?: readonly (number | null)[];
144
154
  /** The itemized charges behind `feeUsdValue`, passed straight through to `RouteSectionData.fees`. */
145
155
  fees?: readonly QuoteFee[];
146
156
  /** Estimated duration for this signature in minutes, or null/undefined to omit. */
@@ -175,6 +185,12 @@ export type PlanRouteDataInputs = {
175
185
  sections: readonly PlanSectionPricing[];
176
186
  /** The route's aggregate all-in cost in United States dollars, or null/undefined. */
177
187
  totalCostUsdValue?: number | null;
188
+ /** The all-in cost without network gas, or null/undefined. */
189
+ costExcludingGasUsdValue?: number | null;
190
+ /** The route's network gas in dollars, or null/undefined when unknown. */
191
+ gasCostUsdValue?: number | null;
192
+ /** The gas by signing chain, in signing order. */
193
+ gasByChain?: readonly RouteGasByChain[];
178
194
  /** The route's net United States dollar outcome — the figure routes are ranked on. */
179
195
  netValueUsd?: number | null;
180
196
  /** Estimated end-to-end duration in minutes, or null/undefined. */