@gibs/bridge-sdk 1.13.1 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/README.md +295 -18
  2. package/abis/package.json +6 -0
  3. package/bridge-limits/package.json +6 -0
  4. package/chain-info/package.json +6 -0
  5. package/chain-names/package.json +6 -0
  6. package/chains/package.json +6 -0
  7. package/claims/package.json +6 -0
  8. package/config/package.json +6 -0
  9. package/consolidated-quote/package.json +6 -0
  10. package/destination-calldata/package.json +6 -0
  11. package/dist/chain-info.d.ts +1 -1
  12. package/dist/chain-info.js +11 -12
  13. package/dist/chains.d.ts +25 -0
  14. package/dist/chains.js +55 -20
  15. package/dist/cjs/abis.d.ts +1 -0
  16. package/dist/cjs/abis.js +19 -0
  17. package/dist/cjs/bridge-limits.d.ts +103 -0
  18. package/dist/cjs/bridge-limits.js +89 -0
  19. package/dist/cjs/chain-info.d.ts +169 -0
  20. package/dist/cjs/chain-info.js +452 -0
  21. package/dist/cjs/chain-names.d.ts +160 -0
  22. package/dist/cjs/chain-names.js +207 -0
  23. package/dist/cjs/chains.d.ts +90 -0
  24. package/dist/cjs/chains.js +357 -0
  25. package/dist/cjs/claims.d.ts +198 -0
  26. package/dist/cjs/claims.js +182 -0
  27. package/dist/cjs/config.d.ts +255 -0
  28. package/dist/cjs/config.js +569 -0
  29. package/dist/cjs/consolidated-quote.d.ts +648 -0
  30. package/dist/cjs/consolidated-quote.js +1048 -0
  31. package/dist/cjs/destination-calldata.d.ts +118 -0
  32. package/dist/cjs/destination-calldata.js +156 -0
  33. package/dist/cjs/ecosystems.d.ts +217 -0
  34. package/dist/cjs/ecosystems.js +298 -0
  35. package/dist/cjs/execution.d.ts +359 -0
  36. package/dist/cjs/execution.js +495 -0
  37. package/dist/cjs/fee-type.d.ts +10 -0
  38. package/dist/cjs/fee-type.js +10 -0
  39. package/dist/cjs/hyperliquid-execution.d.ts +152 -0
  40. package/dist/cjs/hyperliquid-execution.js +175 -0
  41. package/dist/cjs/image-links.d.ts +117 -0
  42. package/dist/cjs/image-links.js +153 -0
  43. package/dist/cjs/index.d.ts +24 -0
  44. package/dist/cjs/index.js +40 -0
  45. package/dist/cjs/lifi-execution-guard.d.ts +210 -0
  46. package/dist/cjs/lifi-execution-guard.js +409 -0
  47. package/dist/cjs/near-intents-conformance.d.ts +85 -0
  48. package/dist/cjs/near-intents-conformance.js +337 -0
  49. package/dist/cjs/near-intents-route.d.ts +47 -0
  50. package/dist/cjs/near-intents-route.js +303 -0
  51. package/dist/cjs/near-intents.d.ts +524 -0
  52. package/dist/cjs/near-intents.js +1071 -0
  53. package/dist/cjs/origin-deposit.d.ts +326 -0
  54. package/dist/cjs/origin-deposit.js +82 -0
  55. package/dist/cjs/package.json +3 -0
  56. package/dist/cjs/pre-quote.d.ts +198 -0
  57. package/dist/cjs/pre-quote.js +180 -0
  58. package/dist/cjs/prices.d.ts +117 -0
  59. package/dist/cjs/prices.js +224 -0
  60. package/dist/cjs/providers.d.ts +238 -0
  61. package/dist/cjs/providers.js +171 -0
  62. package/dist/cjs/quote-fees.d.ts +102 -0
  63. package/dist/cjs/quote-fees.js +201 -0
  64. package/dist/cjs/referral.d.ts +85 -0
  65. package/dist/cjs/referral.js +85 -0
  66. package/dist/cjs/relay-currencies.d.ts +47 -0
  67. package/dist/cjs/relay-currencies.js +95 -0
  68. package/dist/cjs/relay-depository.d.ts +2 -0
  69. package/dist/cjs/relay-depository.js +85 -0
  70. package/dist/cjs/relay-quote.d.ts +301 -0
  71. package/dist/cjs/relay-quote.js +996 -0
  72. package/dist/cjs/route-steps.d.ts +138 -0
  73. package/dist/cjs/route-steps.js +310 -0
  74. package/dist/cjs/routing/amounts.d.ts +80 -0
  75. package/dist/cjs/routing/amounts.js +126 -0
  76. package/dist/cjs/routing/candidate-route.d.ts +43 -0
  77. package/dist/cjs/routing/candidate-route.js +50 -0
  78. package/dist/cjs/routing/candidate.d.ts +237 -0
  79. package/dist/cjs/routing/candidate.js +100 -0
  80. package/dist/cjs/routing/carrier-provider-key.d.ts +31 -0
  81. package/dist/cjs/routing/carrier-provider-key.js +42 -0
  82. package/dist/cjs/routing/carriers/ask-answer.d.ts +77 -0
  83. package/dist/cjs/routing/carriers/ask-answer.js +2 -0
  84. package/dist/cjs/routing/carriers/omnibridge.d.ts +137 -0
  85. package/dist/cjs/routing/carriers/omnibridge.js +459 -0
  86. package/dist/cjs/routing/carriers/pulsex-paths.d.ts +80 -0
  87. package/dist/cjs/routing/carriers/pulsex-paths.js +97 -0
  88. package/dist/cjs/routing/carriers/pulsex-split.d.ts +175 -0
  89. package/dist/cjs/routing/carriers/pulsex-split.js +193 -0
  90. package/dist/cjs/routing/carriers/pulsex.d.ts +184 -0
  91. package/dist/cjs/routing/carriers/pulsex.js +615 -0
  92. package/dist/cjs/routing/carriers/wrap.d.ts +25 -0
  93. package/dist/cjs/routing/carriers/wrap.js +119 -0
  94. package/dist/cjs/routing/composed-route-quality.d.ts +125 -0
  95. package/dist/cjs/routing/composed-route-quality.js +144 -0
  96. package/dist/cjs/routing/entry-chain-anchor.d.ts +88 -0
  97. package/dist/cjs/routing/entry-chain-anchor.js +59 -0
  98. package/dist/cjs/routing/format.d.ts +31 -0
  99. package/dist/cjs/routing/format.js +64 -0
  100. package/dist/cjs/routing/index.d.ts +42 -0
  101. package/dist/cjs/routing/index.js +142 -0
  102. package/dist/cjs/routing/producers.d.ts +770 -0
  103. package/dist/cjs/routing/producers.js +1617 -0
  104. package/dist/cjs/routing/providers.d.ts +31 -0
  105. package/dist/cjs/routing/providers.js +48 -0
  106. package/dist/cjs/routing/route-data.d.ts +397 -0
  107. package/dist/cjs/routing/route-data.js +2 -0
  108. package/dist/cjs/routing/search/compose.d.ts +168 -0
  109. package/dist/cjs/routing/search/compose.js +104 -0
  110. package/dist/cjs/routing/search/graph.d.ts +668 -0
  111. package/dist/cjs/routing/search/graph.js +461 -0
  112. package/dist/cjs/routing/search/plan-route-data.d.ts +285 -0
  113. package/dist/cjs/routing/search/plan-route-data.js +871 -0
  114. package/dist/cjs/routing/search/score.d.ts +356 -0
  115. package/dist/cjs/routing/search/score.js +445 -0
  116. package/dist/cjs/routing/swap-transactions.d.ts +95 -0
  117. package/dist/cjs/routing/swap-transactions.js +133 -0
  118. package/dist/cjs/routing/usd-value.d.ts +17 -0
  119. package/dist/cjs/routing/usd-value.js +29 -0
  120. package/dist/cjs/settings.d.ts +785 -0
  121. package/dist/cjs/settings.js +1074 -0
  122. package/dist/cjs/swap-venues.d.ts +43 -0
  123. package/dist/cjs/swap-venues.js +29 -0
  124. package/dist/cjs/ton-connect.d.ts +98 -0
  125. package/dist/cjs/ton-connect.js +206 -0
  126. package/dist/cjs/ton-execution.d.ts +7 -0
  127. package/dist/cjs/ton-execution.js +23 -0
  128. package/dist/cjs/ton.d.ts +37 -0
  129. package/dist/cjs/ton.js +83 -0
  130. package/dist/cjs/tron-execution.d.ts +146 -0
  131. package/dist/cjs/tron-execution.js +248 -0
  132. package/dist/cjs/types.d.ts +51 -0
  133. package/dist/cjs/types.js +2 -0
  134. package/dist/claims.d.ts +198 -0
  135. package/dist/claims.js +140 -0
  136. package/dist/config.d.ts +42 -3
  137. package/dist/config.js +87 -21
  138. package/dist/consolidated-quote.d.ts +109 -15
  139. package/dist/consolidated-quote.js +310 -52
  140. package/dist/execution.d.ts +359 -0
  141. package/dist/execution.js +455 -0
  142. package/dist/hyperliquid-execution.js +2 -3
  143. package/dist/index.d.ts +4 -0
  144. package/dist/index.js +4 -0
  145. package/dist/lifi-execution-guard.d.ts +13 -2
  146. package/dist/lifi-execution-guard.js +4 -0
  147. package/dist/near-intents.js +2 -3
  148. package/dist/pre-quote.d.ts +6 -6
  149. package/dist/pre-quote.js +10 -14
  150. package/dist/prices.d.ts +10 -0
  151. package/dist/prices.js +52 -1
  152. package/dist/providers.d.ts +17 -0
  153. package/dist/providers.js +6 -0
  154. package/dist/referral.d.ts +12 -4
  155. package/dist/referral.js +12 -4
  156. package/dist/relay-depository.d.ts +2 -0
  157. package/dist/relay-depository.js +82 -0
  158. package/dist/relay-quote.js +2 -3
  159. package/dist/routing/candidate.d.ts +3 -3
  160. package/dist/routing/candidate.js +3 -3
  161. package/dist/routing/carrier-provider-key.d.ts +31 -0
  162. package/dist/routing/carrier-provider-key.js +37 -0
  163. package/dist/routing/carriers/ask-answer.d.ts +17 -0
  164. package/dist/routing/carriers/omnibridge.js +23 -21
  165. package/dist/routing/carriers/pulsex-paths.d.ts +80 -0
  166. package/dist/routing/carriers/pulsex-paths.js +92 -0
  167. package/dist/routing/carriers/pulsex-split.d.ts +175 -0
  168. package/dist/routing/carriers/pulsex-split.js +185 -0
  169. package/dist/routing/carriers/pulsex.d.ts +81 -46
  170. package/dist/routing/carriers/pulsex.js +267 -148
  171. package/dist/routing/composed-route-quality.d.ts +28 -6
  172. package/dist/routing/composed-route-quality.js +36 -5
  173. package/dist/routing/entry-chain-anchor.d.ts +1 -1
  174. package/dist/routing/entry-chain-anchor.js +1 -1
  175. package/dist/routing/index.d.ts +11 -3
  176. package/dist/routing/index.js +8 -2
  177. package/dist/routing/producers.js +1 -1
  178. package/dist/routing/providers.d.ts +5 -0
  179. package/dist/routing/providers.js +5 -0
  180. package/dist/routing/route-data.d.ts +47 -5
  181. package/dist/routing/search/graph.d.ts +15 -0
  182. package/dist/routing/search/plan-route-data.d.ts +42 -1
  183. package/dist/routing/search/plan-route-data.js +68 -9
  184. package/dist/routing/search/score.d.ts +3 -0
  185. package/dist/routing/search/score.js +11 -3
  186. package/dist/routing/swap-transactions.d.ts +95 -0
  187. package/dist/routing/swap-transactions.js +127 -0
  188. package/dist/settings.d.ts +94 -1
  189. package/dist/settings.js +95 -3
  190. package/dist/swap-venues.d.ts +43 -0
  191. package/dist/swap-venues.js +24 -0
  192. package/dist/ton-connect.d.ts +98 -0
  193. package/dist/ton-connect.js +200 -0
  194. package/dist/ton-execution.d.ts +4 -114
  195. package/dist/ton-execution.js +4 -244
  196. package/dist/ton.d.ts +37 -0
  197. package/dist/ton.js +65 -0
  198. package/docs/examples/example-a-ether-to-pulse.ts +300 -0
  199. package/docs/examples/example-b-token-pulsechain-to-ethereum.ts +270 -0
  200. package/docs/examples/shared.ts +261 -0
  201. package/docs/examples/tsconfig.json +18 -0
  202. package/docs/integration-guide.md +617 -0
  203. package/ecosystems/package.json +6 -0
  204. package/execution/package.json +6 -0
  205. package/fee-type/package.json +6 -0
  206. package/hyperliquid-execution/package.json +6 -0
  207. package/image-links/package.json +6 -0
  208. package/lifi-execution-guard/package.json +6 -0
  209. package/llms.txt +75 -10
  210. package/near-intents/package.json +6 -0
  211. package/near-intents-route/package.json +6 -0
  212. package/origin-deposit/package.json +6 -0
  213. package/package.json +817 -10
  214. package/pre-quote/package.json +6 -0
  215. package/prices/package.json +6 -0
  216. package/providers/package.json +6 -0
  217. package/quote-fees/package.json +6 -0
  218. package/referral/package.json +6 -0
  219. package/relay-currencies/package.json +6 -0
  220. package/relay-quote/package.json +6 -0
  221. package/route-steps/package.json +6 -0
  222. package/routing/package.json +6 -0
  223. package/settings/package.json +6 -0
  224. package/ton/package.json +6 -0
  225. package/ton-connect/package.json +6 -0
  226. package/ton-execution/package.json +6 -0
  227. package/tron-execution/package.json +6 -0
  228. package/types/package.json +6 -0
@@ -1,12 +1,14 @@
1
1
  import { multicallRead } from '@gibs/common/multicall';
2
- import { getAddress, parseAbi, zeroAddress } from 'viem';
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';
11
+ import { bestPulseXPathQuote, candidatePulseXPaths, pulseXHubTokens } from './pulsex-paths.js';
10
12
  /**
11
13
  * PulseX (the PulseChain-native decentralized exchange, used here for a
12
14
  * conversion swap leg) as a plug-in `Carrier` for `@gibs/quotes`'s
@@ -17,122 +19,187 @@ import { assetNodeKey } from '../search/graph.js';
17
19
  * See `omnibridge.ts`'s head doc for why this returns a plain object shaped
18
20
  * like `Carrier` rather than importing that type.
19
21
  */
20
- /** WPLS, the wrapped-native intermediary {@link pulseXConversionSwapPath} routes conversions through. */
21
- const wplsAddress = nativeAssetOut[Chains.PLS];
22
+ /** The hub addresses every candidate path on PulseChain is built from — see {@link pulseXHubTokens}. */
23
+ const hubAddresses = pulseXHubTokens.map((hub) => hub.address);
22
24
  /**
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.
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
28
30
  */
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
- });
31
+ const wrappedNativeFor = (chainId) => {
32
+ const wrapped = nativeAssetOut[toChain(chainId)];
33
+ return wrapped === undefined ? undefined : getAddress(wrapped);
34
+ };
33
35
  /**
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.
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.
38
39
  */
39
- 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
+ };
40
46
  /**
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
+ * 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.
47
50
  */
48
- export const pulseXRouterForEdge = (edge) => pulseXVenueForEdge(edge)?.router ?? null;
51
+ export const pulseXVenues = defaultSwapVenues[pulsechainChainId] ?? [];
49
52
  /**
50
- * 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.
58
+ *
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.
51
61
  *
52
- * @param edge - the edge
53
- * @returns the venue, or null when there is none
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`
54
65
  */
55
- const pulseXVenueForEdge = (edge) => {
66
+ export const venueForEdge = (venues, edge) => {
56
67
  if (edge.carrier !== 'PulseX')
57
68
  return null;
69
+ const chainId = edge.from?.chainId ?? pulsechainChainId;
58
70
  if (edge.venue === undefined)
59
- return defaultPulseXVenue ?? null;
60
- const venue = edge.venue;
61
- return pulseXVenues.find((candidate) => sameAddress(candidate.router, venue, 'evm')) ?? null;
71
+ return venues[chainId]?.[0] ?? null;
72
+ return findSwapVenue(venues, chainId, edge.venue);
62
73
  };
63
74
  /**
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}
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
67
86
  * before it ever reaches {@link pulseXConversionSwapPath}. Every other node
68
- * 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.
69
89
  *
70
90
  * MIRRORS `useRouteSearch.ts`'s `quotePathAddress` (the UI's async `ask`
71
91
  * already resolves native this way through that function) — this is the
72
92
  * SAME rule, restated here because `prepare`/`exact` never go through the
73
- * UI's `readAmountOut` at all; see {@link pulseXPoolPath}'s own doc.
93
+ * UI's `readAmountOut` at all; see {@link pulseXCandidatePaths}'s own doc.
74
94
  *
75
95
  * @param node - the node to resolve
76
- * @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
77
97
  */
78
- 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;
79
99
  /**
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.
100
+ * The fixed Wrapped-PLS path from `from` to `to` — `[from, WPLS, to]`, or
101
+ * `[from, to]` when one side is WPLS — or null when both ends resolve to the
102
+ * same pool token (PLS<->WPLS is the wrap carrier's job).
87
103
  *
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.
104
+ * NOT THE PATH A HOP IS SIGNED ALONG. The carrier's own `exact` picks the
105
+ * best of many paths ({@link candidatePulseXPaths}) and records the winner on
106
+ * the hop's outcome; the signer reads it back with {@link pulseXSwapPathForHop}.
107
+ * This fixed path serves only the carrier's asynchronous `ask` — the
108
+ * interface's `getAmountsOut` read, which the route search never calls for a
109
+ * PulseX hop, because the search only asks aggregators live.
110
+ *
111
+ * Native PLS resolves to WPLS first ({@link poolTokenAddress}), so a path
112
+ * never starts or ends on the zero address, which has no pool.
99
113
  *
100
114
  * @param from - the hop's origin node
101
115
  * @param to - the hop's destination node
102
- * @returns the swap path, or null when there is nothing for PulseX to swap
116
+ * @returns the fixed path, or null when there is nothing for PulseX to swap
103
117
  */
104
- export const pulseXPoolPath = (from, to) => {
118
+ export const pulseXWrappedNativePath = (from, to) => {
105
119
  const fromAddress = poolTokenAddress(from);
106
120
  const toAddress = poolTokenAddress(to);
107
121
  if (sameAddress(fromAddress, toAddress, 'evm'))
108
122
  return null;
109
- return pulseXConversionSwapPath(fromAddress, toAddress, wplsAddress);
123
+ const wrappedNative = wrappedNativeFor(from.chainId);
124
+ return wrappedNative === undefined ? [fromAddress, toAddress] : pulseXConversionSwapPath(fromAddress, toAddress, wrappedNative);
125
+ };
126
+ /**
127
+ * Every path a PulseX hop from `from` to `to` may be priced along, or none
128
+ * when both ends resolve to the same pool token. EVERY PULSEX PRICE AND
129
+ * POOL READ IN THIS FILE GOES THROUGH THIS ONE FUNCTION, so `prepare` reads
130
+ * exactly the pools `exact` will look up, and native PLS resolves to WPLS
131
+ * the same way in both ({@link poolTokenAddress} — a path that started on
132
+ * the zero address once left every native-PLS hop unpriced).
133
+ *
134
+ * @param from - the hop's origin node
135
+ * @param to - the hop's destination node
136
+ * @returns the candidate paths, direct first; empty when there is nothing to swap
137
+ */
138
+ const pulseXCandidatePaths = (from, to) => {
139
+ const fromAddress = poolTokenAddress(from);
140
+ const toAddress = poolTokenAddress(to);
141
+ if (sameAddress(fromAddress, toAddress, 'evm'))
142
+ return [];
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) });
110
148
  };
111
149
  /**
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.
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.
115
154
  *
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.
155
+ * NO TOKEN ALLOWLIST IS CONSULTED, on purpose: whether a pool exists is the
156
+ * reserve read's question, answered in `prepare`, not this one's.
124
157
  */
125
- const pulseXCarries = (from, to) => {
158
+ const pulseXCarries = (venues, from, to) => {
126
159
  if (from.ecosystem !== 'evm' || to.ecosystem !== 'evm')
127
160
  return false;
128
- if (from.chainId !== pulsechainChainId || to.chainId !== pulsechainChainId)
161
+ if (from.chainId !== to.chainId)
129
162
  return false;
130
- const swapPath = pulseXPoolPath(from, to);
131
- return swapPath !== null && swapPath.length >= 2;
163
+ if ((venues[from.chainId] ?? []).length === 0)
164
+ return false;
165
+ return pulseXCandidatePaths(from, to).length > 0;
132
166
  };
133
167
  /**
134
- * PulseX's own constant-product swap fee, as a numerator over
135
- * {@link PULSEX_FEE_DENOMINATOR} — the SAME `getAmountOut` formula every
168
+ * The swap path a planned PulseX hop must be signed along: the path its
169
+ * outcome records ({@link HopOutcome.swapPath}), which is the path the
170
+ * carrier's `exact` priced the figure on. NEVER REBUILT HERE — rebuilding a
171
+ * path at signing time is how a quote on one path becomes a signature on
172
+ * another.
173
+ *
174
+ * The recorded path is checked against the hop's own edge before it is
175
+ * returned: it must start at the edge's input pool token, end at its output
176
+ * pool token, hold only addresses, and name no token twice. A path that fails
177
+ * any check is refused (null) rather than signed.
178
+ *
179
+ * @param hop - the planned hop to sign
180
+ * @returns the path to sign, or null when the hop is not a PulseX hop, records no path, or records one that does not fit its edge
181
+ */
182
+ export const pulseXSwapPathForHop = (hop) => {
183
+ if (hop.edge.carrier !== 'PulseX')
184
+ return null;
185
+ const swapPath = hop.outcome.swapPath;
186
+ if (swapPath === undefined || swapPath.length < 2)
187
+ return null;
188
+ if (!swapPath.every((address) => isAddress(address, { strict: false })))
189
+ return null;
190
+ const path = swapPath.map((address) => getAddress(address));
191
+ if (new Set(path).size !== path.length)
192
+ return null;
193
+ if (!sameAddress(path[0], poolTokenAddress(hop.edge.from), 'evm'))
194
+ return null;
195
+ if (!sameAddress(path[path.length - 1], poolTokenAddress(hop.edge.to), 'evm'))
196
+ return null;
197
+ return path;
198
+ };
199
+ /**
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
136
203
  * Uniswap-V2-style router uses: `amountOut = floor(amountIn * feeNumerator *
137
204
  * reserveOut / (reserveIn * feeDenominator + amountIn * feeNumerator))`.
138
205
  *
@@ -145,16 +212,16 @@ const pulseXCarries = (from, to) => {
145
212
  * this claim against live chain data rather than trusting the arithmetic
146
213
  * alone.
147
214
  */
148
- const PULSEX_FEE_NUMERATOR = 9971n;
149
- /** The denominator {@link PULSEX_FEE_NUMERATOR} is a fraction of. */
150
- 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;
151
218
  /** The minimal read-only factory ABI `prepare` needs — just enough to resolve a pool's address from its two tokens. */
152
219
  const univ2FactoryAbi = parseAbi([
153
220
  'function getPair(address tokenA, address tokenB) external view returns (address pair)',
154
221
  ]);
155
222
  /**
156
223
  * The Uniswap-V2-style constant-product `getAmountOut`, applied with
157
- * {@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
158
225
  * exactly — see `pulsex-carrier.fork.test.ts` — so PRICE IMPACT IS ALREADY
159
226
  * IN HERE: this is the same nonlinear function the router evaluates, not a
160
227
  * flat-fee approximation with an impact correction bolted on afterward.
@@ -162,14 +229,15 @@ const univ2FactoryAbi = parseAbi([
162
229
  * @param amountIn - the input amount, in `reserveIn`'s token's base units
163
230
  * @param reserveIn - the pool's reserve of the input token
164
231
  * @param reserveOut - the pool's reserve of the output token
232
+ * @param feeBasisPoints - the venue's swap fee, in basis points
165
233
  * @returns the output amount, in `reserveOut`'s token's base units; 0 when either reserve is 0
166
234
  */
167
- const constantProductAmountOut = (amountIn, reserveIn, reserveOut) => {
235
+ const constantProductAmountOut = (amountIn, reserveIn, reserveOut, feeBasisPoints) => {
168
236
  if (reserveIn <= 0n || reserveOut <= 0n || amountIn <= 0n)
169
237
  return 0n;
170
- const amountInWithFee = amountIn * PULSEX_FEE_NUMERATOR;
238
+ const amountInWithFee = amountIn * (BASIS_POINTS_DENOMINATOR - BigInt(feeBasisPoints));
171
239
  const numerator = amountInWithFee * reserveOut;
172
- const denominator = reserveIn * PULSEX_FEE_DENOMINATOR + amountInWithFee;
240
+ const denominator = reserveIn * BASIS_POINTS_DENOMINATOR + amountInWithFee;
173
241
  return numerator / denominator;
174
242
  };
175
243
  /**
@@ -178,9 +246,10 @@ const constantProductAmountOut = (amountIn, reserveIn, reserveOut) => {
178
246
  *
179
247
  * @param amountIn - the amount entering the first pool
180
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
181
250
  * @returns the amount leaving the last pool; 0 when any pool cannot pay out
182
251
  */
183
- 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);
184
253
  /**
185
254
  * How far a trade's executed rate falls below the marginal (near-spot) rate,
186
255
  * in percent. The marginal rate is a tiny reference trade through the same
@@ -216,9 +285,35 @@ const poolKey = (tokenA, tokenB) => {
216
285
  return `${lower}|${higher}`;
217
286
  };
218
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. */
219
- 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)}`;
220
289
  /** Every consecutive (tokenIn, tokenOut) hop a PulseX swap path implies. */
221
290
  const hopsAlongPath = (path) => path.slice(0, -1).map((tokenIn, index) => [tokenIn, path[index + 1]]);
291
+ /**
292
+ * The pools a path crosses on one venue, each read in the direction of
293
+ * travel, or null when any of them was never read — `getPair` found no pool,
294
+ * or the read failed. A path with a missing pool cannot be priced, exactly as
295
+ * the router's own `getAmountsOut` would revert on it.
296
+ *
297
+ * @param options.venue - the deployment whose pools the path trades against
298
+ * @param options.path - the tokens, in order
299
+ * @param options.reservesByPoolKey - every pool `prepare` read, keyed by {@link venuePoolKey}
300
+ * @returns the steps in order, or null when a pool is missing
301
+ */
302
+ const stepsAlongPath = (options) => {
303
+ const { venue, path, reservesByPoolKey } = options;
304
+ const steps = [];
305
+ for (const [tokenIn, tokenOut] of hopsAlongPath(path)) {
306
+ const reserves = reservesByPoolKey.get(venuePoolKey(venue, tokenIn, tokenOut));
307
+ if (reserves === undefined)
308
+ return null;
309
+ const tokenInIsLower = tokenIn.toLowerCase() < tokenOut.toLowerCase();
310
+ steps.push({
311
+ reserveIn: tokenInIsLower ? reserves.reserveOfLowerAddress : reserves.reserveOfHigherAddress,
312
+ reserveOut: tokenInIsLower ? reserves.reserveOfHigherAddress : reserves.reserveOfLowerAddress,
313
+ });
314
+ }
315
+ return steps;
316
+ };
222
317
  /**
223
318
  * Batch-reads reserves for every pool `nodes`' candidate PulseX paths touch,
224
319
  * on EVERY venue in `venues`, once, so {@link createPulseXCarrier}'s `exact`
@@ -235,6 +330,14 @@ const hopsAlongPath = (path) => path.slice(0, -1).map((tokenIn, index) => [token
235
330
  * the second read cannot join the first — but every venue's `getPair` rides
236
331
  * the first multicall and every pool's `getReserves` rides the second.
237
332
  *
333
+ * THE HUB SEARCH WIDENS EACH READ, NOT THEIR NUMBER. Every pool any candidate
334
+ * path crosses is read here ({@link pulseXCandidatePaths}), so `exact` can
335
+ * price every path with no further call. Measured 2026-10-06: an 8-token
336
+ * PulseChain universe asked 84 entries across the two reads (the fixed
337
+ * Wrapped-PLS path asked about 16). The shared multicall helper splits a read
338
+ * past its request-size budget into sequential posts; `getPair` crosses that
339
+ * line at about 11 pool tokens (198 entries over two venues).
340
+ *
238
341
  * @param nodes - every node the search might branch through
239
342
  * @param venues - the venues this carrier offers
240
343
  * @param clientForChain - see {@link CreatePulseXCarrierOptions}'s own field
@@ -242,35 +345,54 @@ const hopsAlongPath = (path) => path.slice(0, -1).map((tokenIn, index) => [token
242
345
  */
243
346
  const preparePulseXReserves = async ({ nodes, venues, clientForChain, }) => {
244
347
  const reservesByPoolKey = new Map();
245
- const client = clientForChain(pulsechainChainId);
246
- 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)
247
370
  return reservesByPoolKey;
248
- const pulsechainNodes = nodes.filter((node) => node.ecosystem === 'evm' && node.chainId === pulsechainChainId);
371
+ const chainNodes = nodes.filter((node) => node.ecosystem === 'evm' && node.chainId === chainId);
249
372
  const poolTokenPairs = new Map();
250
- for (const from of pulsechainNodes) {
251
- for (const to of pulsechainNodes) {
373
+ for (const from of chainNodes) {
374
+ for (const to of chainNodes) {
252
375
  if (assetNodeKey(from) === assetNodeKey(to))
253
376
  continue;
254
- if (!pulseXCarries(from, to))
255
- continue;
256
- const path = pulseXPoolPath(from, to);
257
- if (path === null)
377
+ if (!pulseXCarries(venues, from, to))
258
378
  continue;
259
- for (const [tokenIn, tokenOut] of hopsAlongPath(path)) {
260
- poolTokenPairs.set(poolKey(tokenIn, tokenOut), [tokenIn, tokenOut]);
379
+ for (const path of pulseXCandidatePaths(from, to)) {
380
+ for (const [tokenIn, tokenOut] of hopsAlongPath(path)) {
381
+ poolTokenPairs.set(poolKey(tokenIn, tokenOut), [tokenIn, tokenOut]);
382
+ }
261
383
  }
262
384
  }
263
385
  }
264
386
  if (poolTokenPairs.size === 0)
265
387
  return reservesByPoolKey;
266
- 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]) => ({
267
389
  key: venuePoolKey(venue, tokenIn, tokenOut),
268
390
  factory: venue.factory,
269
391
  tokenIn,
270
392
  tokenOut,
271
393
  })));
272
394
  const factoryResults = await multicallRead({
273
- chain: chainsMetadata[toChain(pulsechainChainId)],
395
+ chain,
274
396
  client,
275
397
  abi: univ2FactoryAbi,
276
398
  calls: poolQueries.map((query) => ({
@@ -295,7 +417,7 @@ const preparePulseXReserves = async ({ nodes, venues, clientForChain, }) => {
295
417
  return reservesByPoolKey;
296
418
  const reservePoolKeys = Array.from(pairAddressByPoolKey.keys());
297
419
  const reserveResults = await multicallRead({
298
- chain: chainsMetadata[toChain(pulsechainChainId)],
420
+ chain,
299
421
  client,
300
422
  abi: abis.pair,
301
423
  calls: reservePoolKeys.map((key) => ({
@@ -320,22 +442,24 @@ const preparePulseXReserves = async ({ nodes, venues, clientForChain, }) => {
320
442
  return reservesByPoolKey;
321
443
  };
322
444
  /**
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.
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.
326
448
  *
449
+ * @param venues - the resolved venues
327
450
  * @param routers - the routers the host can sign on, or undefined for all
328
- * @returns the venues, in {@link pulseXVenues} order
451
+ * @returns the venues, in their original order
329
452
  */
330
- const selectPulseXVenues = (routers) => {
453
+ const selectPulseXVenues = (venues, routers) => {
331
454
  if (routers === undefined)
332
- return pulseXVenues;
455
+ return venues;
456
+ const named = (venue) => routers.some((router) => sameAddress(venue.router, router, 'evm'));
333
457
  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
- }
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`);
337
461
  }
338
- 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)])));
339
463
  };
340
464
  /**
341
465
  * Builds the PulseX conversion swap as a plug-in carrier.
@@ -345,16 +469,16 @@ const selectPulseXVenues = (routers) => {
345
469
  */
346
470
  export const createPulseXCarrier = (options) => {
347
471
  const { readAmountOut, clientForChain } = options;
348
- const venues = selectPulseXVenues(options.routers);
472
+ const venues = selectPulseXVenues(resolveSwapVenues(options.venues), options.routers);
349
473
  // ONE EDGE PER VENUE. Each names its router as its `venue`, so the search
350
474
  // quotes and ranks V1 and V2 as two separate hops and the better one wins
351
475
  // on its own figure — and the winner is signed on the router it names.
352
476
  const edgesFrom = (from, to) => {
353
477
  if (assetNodeKey(from) === assetNodeKey(to))
354
478
  return [];
355
- if (!pulseXCarries(from, to))
479
+ if (!pulseXCarries(venues, from, to))
356
480
  return [];
357
- return venues.map((venue) => ({
481
+ return (venues[from.chainId] ?? []).map((venue) => ({
358
482
  carrier: 'PulseX',
359
483
  from,
360
484
  to,
@@ -390,51 +514,46 @@ export const createPulseXCarrier = (options) => {
390
514
  reservesByPoolKey = await preparePulseXReserves({ nodes, venues, clientForChain });
391
515
  };
392
516
  const exact = (edge, amountIn) => {
393
- if (!pulseXCarries(edge.from, edge.to))
517
+ if (!pulseXCarries(venues, edge.from, edge.to))
394
518
  return null;
395
519
  // Quoted on the edge's OWN venue — the router it will be signed on — and
396
520
  // only on a venue this carrier offers.
397
- const venue = pulseXVenueForEdge(edge);
398
- if (venue === null || !venues.includes(venue))
521
+ const venue = venueForEdge(venues, edge);
522
+ if (venue === null)
399
523
  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)
524
+ // EVERY CANDIDATE PATH IS PRICED, AND THE BEST ONE WINS. A path with a
525
+ // pool `prepare` never found is left out — the same as a router call
526
+ // that would revert — and a path that pays out nothing is never picked.
527
+ const quotes = pulseXCandidatePaths(edge.from, edge.to).flatMap((path) => {
528
+ const steps = stepsAlongPath({ venue, path, reservesByPoolKey });
529
+ return steps === null ? [] : [{ path, steps, deliveredAmount: amountOutAlongPath(amountIn, steps, venue.feeBasisPoints) }];
530
+ });
531
+ // No path had every pool read — a structural non-answer, not a refusal
532
+ // (`exact`'s own doc: `null` means "nothing to say"). NOTE THIS DOES NOT
533
+ // FALL BACK TO AN ESTIMATE: a carrier with `exact` defined never yields
534
+ // an `'estimate'` outcome (see `enumerate.ts`'s `resolveHopOutcome`,
535
+ // branch 1) — `null` here PRUNES the hop entirely, the same as a refusal.
536
+ if (quotes.length === 0)
405
537
  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) {
538
+ const best = bestPulseXPathQuote(quotes);
539
+ if (best === null) {
425
540
  return { outcome: 'refused', refusal: { kind: 'no-quote', reason: 'no liquidity on this PulseX path', minimumAmount: null } };
426
541
  }
542
+ // Price impact is measured on the CHOSEN path's own pools, with the same
543
+ // reference trade as before.
544
+ const referenceAmountIn = amountIn / PULSEX_MARGINAL_REFERENCE_DIVISOR;
427
545
  const priceImpactPercent = priceImpactAgainstMarginal({
428
546
  amountIn,
429
- amountOut: deliveredAmount,
430
- referenceAmountIn: amountIn / PULSEX_MARGINAL_REFERENCE_DIVISOR,
431
- referenceAmountOut: amountOutAlongPath(amountIn / PULSEX_MARGINAL_REFERENCE_DIVISOR, steps),
547
+ amountOut: best.deliveredAmount,
548
+ referenceAmountIn,
549
+ referenceAmountOut: amountOutAlongPath(referenceAmountIn, best.steps, venue.feeBasisPoints),
432
550
  });
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 };
551
+ // The path rides on the answer, so the signer signs exactly what was
552
+ // priced (`pulseXSwapPathForHop`). Impact is left off (not zero) when the
553
+ // reference trade rounds away: an unknown impact must stay unknown so the
554
+ // quality gate withholds the plan.
555
+ const delivered = { outcome: 'delivered', deliveredAmount: best.deliveredAmount, swapPath: best.path };
556
+ return priceImpactPercent === null ? delivered : { ...delivered, priceImpactPercent };
438
557
  };
439
558
  return {
440
559
  id: 'PulseX',