@gibs/bridge-sdk 1.13.1 → 1.17.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 (217) hide show
  1. package/README.md +244 -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 +240 -0
  28. package/dist/cjs/config.js +537 -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 +346 -0
  36. package/dist/cjs/execution.js +468 -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 +23 -0
  44. package/dist/cjs/index.js +39 -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 +107 -0
  59. package/dist/cjs/prices.js +172 -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/carriers/ask-answer.d.ts +77 -0
  81. package/dist/cjs/routing/carriers/ask-answer.js +2 -0
  82. package/dist/cjs/routing/carriers/omnibridge.d.ts +137 -0
  83. package/dist/cjs/routing/carriers/omnibridge.js +459 -0
  84. package/dist/cjs/routing/carriers/pulsex-paths.d.ts +80 -0
  85. package/dist/cjs/routing/carriers/pulsex-paths.js +97 -0
  86. package/dist/cjs/routing/carriers/pulsex-split.d.ts +175 -0
  87. package/dist/cjs/routing/carriers/pulsex-split.js +193 -0
  88. package/dist/cjs/routing/carriers/pulsex.d.ts +159 -0
  89. package/dist/cjs/routing/carriers/pulsex.js +565 -0
  90. package/dist/cjs/routing/carriers/wrap.d.ts +25 -0
  91. package/dist/cjs/routing/carriers/wrap.js +119 -0
  92. package/dist/cjs/routing/composed-route-quality.d.ts +125 -0
  93. package/dist/cjs/routing/composed-route-quality.js +144 -0
  94. package/dist/cjs/routing/entry-chain-anchor.d.ts +88 -0
  95. package/dist/cjs/routing/entry-chain-anchor.js +59 -0
  96. package/dist/cjs/routing/format.d.ts +31 -0
  97. package/dist/cjs/routing/format.js +64 -0
  98. package/dist/cjs/routing/index.d.ts +38 -0
  99. package/dist/cjs/routing/index.js +134 -0
  100. package/dist/cjs/routing/producers.d.ts +770 -0
  101. package/dist/cjs/routing/producers.js +1617 -0
  102. package/dist/cjs/routing/providers.d.ts +31 -0
  103. package/dist/cjs/routing/providers.js +48 -0
  104. package/dist/cjs/routing/route-data.d.ts +355 -0
  105. package/dist/cjs/routing/route-data.js +2 -0
  106. package/dist/cjs/routing/search/compose.d.ts +168 -0
  107. package/dist/cjs/routing/search/compose.js +104 -0
  108. package/dist/cjs/routing/search/graph.d.ts +668 -0
  109. package/dist/cjs/routing/search/graph.js +461 -0
  110. package/dist/cjs/routing/search/plan-route-data.d.ts +269 -0
  111. package/dist/cjs/routing/search/plan-route-data.js +848 -0
  112. package/dist/cjs/routing/search/score.d.ts +356 -0
  113. package/dist/cjs/routing/search/score.js +445 -0
  114. package/dist/cjs/routing/swap-transactions.d.ts +91 -0
  115. package/dist/cjs/routing/swap-transactions.js +131 -0
  116. package/dist/cjs/routing/usd-value.d.ts +17 -0
  117. package/dist/cjs/routing/usd-value.js +29 -0
  118. package/dist/cjs/settings.d.ts +785 -0
  119. package/dist/cjs/settings.js +1074 -0
  120. package/dist/cjs/ton-connect.d.ts +98 -0
  121. package/dist/cjs/ton-connect.js +206 -0
  122. package/dist/cjs/ton-execution.d.ts +7 -0
  123. package/dist/cjs/ton-execution.js +23 -0
  124. package/dist/cjs/ton.d.ts +37 -0
  125. package/dist/cjs/ton.js +83 -0
  126. package/dist/cjs/tron-execution.d.ts +146 -0
  127. package/dist/cjs/tron-execution.js +248 -0
  128. package/dist/cjs/types.d.ts +51 -0
  129. package/dist/cjs/types.js +2 -0
  130. package/dist/claims.d.ts +198 -0
  131. package/dist/claims.js +140 -0
  132. package/dist/config.d.ts +24 -0
  133. package/dist/config.js +39 -5
  134. package/dist/consolidated-quote.d.ts +109 -15
  135. package/dist/consolidated-quote.js +310 -52
  136. package/dist/execution.d.ts +346 -0
  137. package/dist/execution.js +428 -0
  138. package/dist/hyperliquid-execution.js +2 -3
  139. package/dist/index.d.ts +3 -0
  140. package/dist/index.js +3 -0
  141. package/dist/lifi-execution-guard.d.ts +13 -2
  142. package/dist/lifi-execution-guard.js +4 -0
  143. package/dist/near-intents.js +2 -3
  144. package/dist/pre-quote.d.ts +6 -6
  145. package/dist/pre-quote.js +10 -14
  146. package/dist/providers.d.ts +17 -0
  147. package/dist/providers.js +6 -0
  148. package/dist/referral.d.ts +12 -4
  149. package/dist/referral.js +12 -4
  150. package/dist/relay-depository.d.ts +2 -0
  151. package/dist/relay-depository.js +82 -0
  152. package/dist/relay-quote.js +2 -3
  153. package/dist/routing/candidate.d.ts +3 -3
  154. package/dist/routing/candidate.js +3 -3
  155. package/dist/routing/carriers/ask-answer.d.ts +17 -0
  156. package/dist/routing/carriers/omnibridge.js +23 -21
  157. package/dist/routing/carriers/pulsex-paths.d.ts +80 -0
  158. package/dist/routing/carriers/pulsex-paths.js +92 -0
  159. package/dist/routing/carriers/pulsex-split.d.ts +175 -0
  160. package/dist/routing/carriers/pulsex-split.js +185 -0
  161. package/dist/routing/carriers/pulsex.d.ts +31 -21
  162. package/dist/routing/carriers/pulsex.js +141 -71
  163. package/dist/routing/composed-route-quality.d.ts +28 -6
  164. package/dist/routing/composed-route-quality.js +36 -5
  165. package/dist/routing/entry-chain-anchor.d.ts +1 -1
  166. package/dist/routing/entry-chain-anchor.js +1 -1
  167. package/dist/routing/index.d.ts +6 -2
  168. package/dist/routing/index.js +6 -2
  169. package/dist/routing/producers.js +1 -1
  170. package/dist/routing/providers.d.ts +5 -0
  171. package/dist/routing/providers.js +5 -0
  172. package/dist/routing/search/graph.d.ts +15 -0
  173. package/dist/routing/search/plan-route-data.d.ts +25 -0
  174. package/dist/routing/search/plan-route-data.js +43 -7
  175. package/dist/routing/search/score.d.ts +3 -0
  176. package/dist/routing/search/score.js +11 -3
  177. package/dist/routing/swap-transactions.d.ts +91 -0
  178. package/dist/routing/swap-transactions.js +125 -0
  179. package/dist/settings.d.ts +94 -1
  180. package/dist/settings.js +95 -3
  181. package/dist/ton-connect.d.ts +98 -0
  182. package/dist/ton-connect.js +200 -0
  183. package/dist/ton-execution.d.ts +4 -114
  184. package/dist/ton-execution.js +4 -244
  185. package/dist/ton.d.ts +37 -0
  186. package/dist/ton.js +65 -0
  187. package/docs/examples/example-a-ether-to-pulse.ts +300 -0
  188. package/docs/examples/example-b-token-pulsechain-to-ethereum.ts +270 -0
  189. package/docs/examples/shared.ts +261 -0
  190. package/docs/examples/tsconfig.json +18 -0
  191. package/docs/integration-guide.md +617 -0
  192. package/ecosystems/package.json +6 -0
  193. package/execution/package.json +6 -0
  194. package/fee-type/package.json +6 -0
  195. package/hyperliquid-execution/package.json +6 -0
  196. package/image-links/package.json +6 -0
  197. package/lifi-execution-guard/package.json +6 -0
  198. package/llms.txt +75 -10
  199. package/near-intents/package.json +6 -0
  200. package/near-intents-route/package.json +6 -0
  201. package/origin-deposit/package.json +6 -0
  202. package/package.json +817 -10
  203. package/pre-quote/package.json +6 -0
  204. package/prices/package.json +6 -0
  205. package/providers/package.json +6 -0
  206. package/quote-fees/package.json +6 -0
  207. package/referral/package.json +6 -0
  208. package/relay-currencies/package.json +6 -0
  209. package/relay-quote/package.json +6 -0
  210. package/route-steps/package.json +6 -0
  211. package/routing/package.json +6 -0
  212. package/settings/package.json +6 -0
  213. package/ton/package.json +6 -0
  214. package/ton-connect/package.json +6 -0
  215. package/ton-execution/package.json +6 -0
  216. package/tron-execution/package.json +6 -0
  217. package/types/package.json +6 -0
@@ -0,0 +1,185 @@
1
+ import { PULSEX_MARGINAL_REFERENCE_DIVISOR } from '../producers.js';
2
+ import { amountOutAlongPath, priceImpactAgainstMarginal } from './pulsex.js';
3
+ /**
4
+ * How many equal chunks the search divides the trade into: 20, so legs move
5
+ * in 5% steps. Finer steps buy a little output for proportionally more work.
6
+ */
7
+ export const PULSEX_SPLIT_CHUNKS = 20;
8
+ /** The most legs one split may have. Each leg is one more router call to sign or batch, and one more swap's gas. */
9
+ export const PULSEX_SPLIT_MAX_LEGS = 3;
10
+ /** How many candidate paths, best single-path output first, the search may spread the trade over. */
11
+ export const PULSEX_SPLIT_CANDIDATE_LIMIT = 4;
12
+ /**
13
+ * The gain a split must show over the best single path before it is chosen,
14
+ * in basis points of the single path's output: 25, or 0.25%.
15
+ *
16
+ * GAS IS REAL. Each extra leg costs roughly one more swap's gas, and a
17
+ * batching contract adds its own overhead. A split that wins by a rounding
18
+ * margin buys the reader nothing but a larger fee. The figure is a judgement,
19
+ * not a measurement; revisit it once an execution path has a real gas cost.
20
+ */
21
+ export const PULSEX_SPLIT_MINIMUM_GAIN_BASIS_POINTS = 25n;
22
+ const BASIS_POINTS = 10000n;
23
+ /** The key one token's reserve in one pool is held under while a split executes. */
24
+ const reserveKey = (pool, token) => `${pool}|${token.toLowerCase()}`;
25
+ /**
26
+ * Executes legs one after another against shared pool state, exactly as
27
+ * sequential router calls would. A leg that crosses a pool an earlier leg
28
+ * already traded sees that pool's moved reserves, in whichever direction the
29
+ * earlier leg traded it.
30
+ *
31
+ * Each trade adds its full input to the pool and removes its output, the
32
+ * Uniswap-V2 accounting. See the open risk in the report: if PulseX moves
33
+ * part of its fee out of the pool during a swap, a shared pool is a few
34
+ * hundredths of a percent shallower here than on chain.
35
+ *
36
+ * @param legs - each candidate with the amount it spends, in execution order
37
+ * @returns what each leg delivers, in the same order
38
+ */
39
+ export const executePulseXSplitInOrder = (legs) => {
40
+ const reserves = new Map();
41
+ const reserveOf = (pool, token, initial) => reserves.get(reserveKey(pool, token)) ?? initial;
42
+ return legs.map(({ candidate, amountIn }) => candidate.steps.reduce((runningAmount, step) => {
43
+ const reserveIn = reserveOf(step.pool, step.tokenIn, step.reserveIn);
44
+ const reserveOut = reserveOf(step.pool, step.tokenOut, step.reserveOut);
45
+ const amountOut = amountOutAlongPath(runningAmount, [{ reserveIn, reserveOut }]);
46
+ reserves.set(reserveKey(step.pool, step.tokenIn), reserveIn + runningAmount);
47
+ reserves.set(reserveKey(step.pool, step.tokenOut), reserveOut - amountOut);
48
+ return amountOut;
49
+ }, amountIn));
50
+ };
51
+ /** What one candidate alone delivers for the whole trade. */
52
+ const singlePathOutput = (candidate, amountIn) => executePulseXSplitInOrder([{ candidate, amountIn }])[0] ?? 0n;
53
+ /**
54
+ * The candidates a split may use: the {@link PULSEX_SPLIT_CANDIDATE_LIMIT}
55
+ * best by single-path output for the whole trade. A candidate that delivers
56
+ * nothing is dropped. Ties go to the shorter path, then the earlier
57
+ * candidate, so the ranking is deterministic.
58
+ *
59
+ * @param candidates - every priced path, on every venue
60
+ * @param amountIn - the whole trade
61
+ * @returns the ranked candidates, best first, each with its single-path output
62
+ */
63
+ export const rankPulseXSplitCandidates = (candidates, amountIn) => candidates
64
+ .map((candidate, index) => ({ candidate, index, deliveredAmount: singlePathOutput(candidate, amountIn) }))
65
+ .filter((ranked) => ranked.deliveredAmount > 0n)
66
+ .sort((left, right) => {
67
+ if (left.deliveredAmount !== right.deliveredAmount)
68
+ return left.deliveredAmount > right.deliveredAmount ? -1 : 1;
69
+ if (left.candidate.path.length !== right.candidate.path.length)
70
+ return left.candidate.path.length - right.candidate.path.length;
71
+ return left.index - right.index;
72
+ })
73
+ .slice(0, PULSEX_SPLIT_CANDIDATE_LIMIT)
74
+ .map(({ candidate, deliveredAmount }) => ({ candidate, deliveredAmount }));
75
+ /** The trade divided into {@link PULSEX_SPLIT_CHUNKS} chunks; the last one takes the remainder, so the chunks sum to the trade exactly. */
76
+ const chunksOf = (amountIn) => {
77
+ const count = BigInt(PULSEX_SPLIT_CHUNKS);
78
+ const chunk = amountIn / count;
79
+ return Array.from({ length: PULSEX_SPLIT_CHUNKS }, (_, index) => index === PULSEX_SPLIT_CHUNKS - 1 ? amountIn - chunk * (count - 1n) : chunk);
80
+ };
81
+ /** Each candidate with a positive allocation, in candidate order — the order the legs execute in. */
82
+ const legsOf = (candidates, allocation) => candidates.flatMap((candidate, index) => {
83
+ const amountIn = allocation[index] ?? 0n;
84
+ return amountIn > 0n ? [{ candidate, amountIn }] : [];
85
+ });
86
+ /** What an allocation delivers in total, executed in candidate order. */
87
+ const allocationOutput = (candidates, allocation) => executePulseXSplitInOrder(legsOf(candidates, allocation)).reduce((total, amount) => total + amount, 0n);
88
+ /**
89
+ * Spreads a trade over ranked candidates, one chunk at a time. Each chunk goes
90
+ * to the candidate whose share, once raised by that chunk, makes the WHOLE
91
+ * split deliver the most — the split is re-executed in full each time
92
+ * ({@link executePulseXSplitInOrder}), so a shared pool is always priced with
93
+ * the reserves every leg leaves behind. A candidate not yet used is skipped
94
+ * once {@link PULSEX_SPLIT_MAX_LEGS} legs exist. Ties go to the earlier
95
+ * candidate.
96
+ *
97
+ * BOUNDED AND DETERMINISTIC. Work per quote is at most
98
+ * `PULSEX_SPLIT_CHUNKS × PULSEX_SPLIT_CANDIDATE_LIMIT` full executions (80),
99
+ * each of at most `PULSEX_SPLIT_MAX_LEGS` legs of at most three pools: under
100
+ * 750 constant-product evaluations, no network call.
101
+ *
102
+ * On pools that share nothing, each candidate's output is concave in its
103
+ * share, so this greedy fill reaches the best allocation on the chunk grid.
104
+ * Shared pools break that guarantee; {@link bestPulseXSplitQuote} still never
105
+ * returns a split worse than the best single path.
106
+ *
107
+ * @param candidates - ranked candidates, best first
108
+ * @param amountIn - the whole trade
109
+ * @returns the share of the trade each candidate spends, in candidate order
110
+ */
111
+ export const allocatePulseXSplit = (candidates, amountIn) => chunksOf(amountIn).reduce((allocation, chunk) => {
112
+ const usedLegs = allocation.filter((amount) => amount > 0n).length;
113
+ const trials = candidates.flatMap((_, index) => {
114
+ const unused = (allocation[index] ?? 0n) === 0n;
115
+ if (unused && usedLegs >= PULSEX_SPLIT_MAX_LEGS)
116
+ return [];
117
+ const trial = allocation.map((amount, trialIndex) => (trialIndex === index ? amount + chunk : amount));
118
+ return [{ trial, output: allocationOutput(candidates, trial) }];
119
+ });
120
+ const best = trials.reduce((leader, entry) => (leader === null || entry.output > leader.output ? entry : leader), null);
121
+ return best === null ? allocation : best.trial;
122
+ }, candidates.map(() => 0n));
123
+ /**
124
+ * Price impact of a combined trade, in percent, measured with the same
125
+ * reference size the single-path figure uses
126
+ * (`amountIn / PULSEX_MARGINAL_REFERENCE_DIVISOR`).
127
+ *
128
+ * THE REFERENCE IS THE BEST MARGINAL RATE OF ANY CANDIDATE, not a blend of
129
+ * the legs' own rates. A split that leans on a path with a worse spot price
130
+ * pays for that, and the figure shows it. A blend would hide it.
131
+ *
132
+ * @param options.candidates - every ranked candidate
133
+ * @param options.amountIn - the whole trade
134
+ * @param options.deliveredAmount - what the combined trade delivers
135
+ * @returns the impact in percent, or null when the reference trade rounds away
136
+ */
137
+ export const splitPriceImpact = (options) => {
138
+ const { candidates, amountIn, deliveredAmount } = options;
139
+ const referenceAmountIn = amountIn / PULSEX_MARGINAL_REFERENCE_DIVISOR;
140
+ const referenceAmountOut = candidates.reduce((best, candidate) => {
141
+ const output = singlePathOutput(candidate, referenceAmountIn);
142
+ return output > best ? output : best;
143
+ }, 0n);
144
+ return priceImpactAgainstMarginal({ amountIn, amountOut: deliveredAmount, referenceAmountIn, referenceAmountOut });
145
+ };
146
+ /**
147
+ * The best split of a PulseX trade, or null when no split beats the best
148
+ * single path by {@link PULSEX_SPLIT_MINIMUM_GAIN_BASIS_POINTS}.
149
+ *
150
+ * NEVER WORSE THAN ONE PATH. A null answer means "take the best single path";
151
+ * this never returns a split that delivers less than that path.
152
+ *
153
+ * @param candidates - every priced path, on every venue
154
+ * @param amountIn - the whole trade
155
+ * @returns the split with its legs in execution order, or null
156
+ */
157
+ export const bestPulseXSplitQuote = (candidates, amountIn) => {
158
+ if (amountIn <= 0n)
159
+ return null;
160
+ const ranked = rankPulseXSplitCandidates(candidates, amountIn);
161
+ const singlePathDeliveredAmount = ranked[0]?.deliveredAmount;
162
+ if (singlePathDeliveredAmount === undefined)
163
+ return null;
164
+ const rankedCandidates = ranked.map(({ candidate }) => candidate);
165
+ const allocation = allocatePulseXSplit(rankedCandidates, amountIn);
166
+ const legs = legsOf(rankedCandidates, allocation);
167
+ if (legs.length < 2)
168
+ return null;
169
+ const legOutputs = executePulseXSplitInOrder(legs);
170
+ const deliveredAmount = legOutputs.reduce((total, amount) => total + amount, 0n);
171
+ const requiredAmount = singlePathDeliveredAmount + (singlePathDeliveredAmount * PULSEX_SPLIT_MINIMUM_GAIN_BASIS_POINTS) / BASIS_POINTS;
172
+ if (deliveredAmount < requiredAmount)
173
+ return null;
174
+ return {
175
+ legs: legs.map(({ candidate, amountIn: legAmountIn }, index) => ({
176
+ venue: candidate.venue,
177
+ path: candidate.path,
178
+ amountIn: legAmountIn,
179
+ deliveredAmount: legOutputs[index] ?? 0n,
180
+ })),
181
+ deliveredAmount,
182
+ singlePathDeliveredAmount,
183
+ priceImpactPercent: splitPriceImpact({ candidates: rankedCandidates, amountIn, deliveredAmount }),
184
+ };
185
+ };
@@ -1,6 +1,6 @@
1
1
  import { type Hex } from 'viem';
2
2
  import type { PublicClient } from 'viem';
3
- import { type AssetNode, type HopEdge } from '../search/graph.js';
3
+ import { type AssetNode, type HopEdge, type PlannedHop } from '../search/graph.js';
4
4
  import type { CarrierAskAnswer, CarrierExactResult } from './ask-answer.js';
5
5
  /**
6
6
  * One PulseX deployment a swap can execute on: the router the reader signs
@@ -30,31 +30,41 @@ export declare const pulseXVenues: readonly PulseXVenue[];
30
30
  */
31
31
  export declare const pulseXRouterForEdge: (edge: Pick<HopEdge, "carrier" | "venue">) => Hex | null;
32
32
  /**
33
- * The router swap path a PulseX hop from `from` to `to` prices and signs
34
- * along, or null when there is no swap for PulseX to do — including when
35
- * both ends resolve to the SAME pool token, which is true of PLS<->WPLS
36
- * (native PLS resolves to {@link wplsAddress} via {@link poolTokenAddress},
37
- * same as Wrapped PLS itself): that pair is the wrap carrier's job, never a
38
- * PulseX hop, so this returns null rather than a zero-length or self-paired
39
- * path.
33
+ * The fixed Wrapped-PLS path from `from` to `to` — `[from, WPLS, to]`, or
34
+ * `[from, to]` when one side is WPLS — or null when both ends resolve to the
35
+ * same pool token (PLS<->WPLS is the wrap carrier's job).
40
36
  *
41
- * EVERY PULSEX QUOTE AND SIGNATURE IN THIS FILE MUST GO THROUGH THIS ONE
42
- * FUNCTION — `pulseXCarries`, `prepare`'s pool discovery, and `exact`'s hop
43
- * pricing all call it, so native PLS resolves the same way in every one of
44
- * them. Before this existed, `pulseXCarries` and `prepare`/`exact` called
45
- * `pulseXConversionSwapPath` directly on the raw node address: a native PLS
46
- * node produced a path that LITERALLY STARTED WITH THE ZERO ADDRESS
47
- * (`[zeroAddress, wplsAddress, ...]`), which `pulseXCarries` accepted (its
48
- * length is still >= 2) but for which `prepare` could never find a pool —
49
- * the factory has no pair for the zero address — so `exact` always answered
50
- * `null` for a direct native-PLS PulseX hop, and `enumerate.ts` pruned it,
51
- * leaving only the two-signature wrap-then-swap path.
37
+ * NOT THE PATH A HOP IS SIGNED ALONG. The carrier's own `exact` picks the
38
+ * best of many paths ({@link candidatePulseXPaths}) and records the winner on
39
+ * the hop's outcome; the signer reads it back with {@link pulseXSwapPathForHop}.
40
+ * This fixed path serves only the carrier's asynchronous `ask` — the
41
+ * interface's `getAmountsOut` read, which the route search never calls for a
42
+ * PulseX hop, because the search only asks aggregators live.
43
+ *
44
+ * Native PLS resolves to WPLS first ({@link poolTokenAddress}), so a path
45
+ * never starts or ends on the zero address, which has no pool.
52
46
  *
53
47
  * @param from - the hop's origin node
54
48
  * @param to - the hop's destination node
55
- * @returns the swap path, or null when there is nothing for PulseX to swap
49
+ * @returns the fixed path, or null when there is nothing for PulseX to swap
50
+ */
51
+ export declare const pulseXWrappedNativePath: (from: AssetNode, to: AssetNode) => readonly Hex[] | null;
52
+ /**
53
+ * The swap path a planned PulseX hop must be signed along: the path its
54
+ * outcome records ({@link HopOutcome.swapPath}), which is the path the
55
+ * carrier's `exact` priced the figure on. NEVER REBUILT HERE — rebuilding a
56
+ * path at signing time is how a quote on one path becomes a signature on
57
+ * another.
58
+ *
59
+ * The recorded path is checked against the hop's own edge before it is
60
+ * returned: it must start at the edge's input pool token, end at its output
61
+ * pool token, hold only addresses, and name no token twice. A path that fails
62
+ * any check is refused (null) rather than signed.
63
+ *
64
+ * @param hop - the planned hop to sign
65
+ * @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
56
66
  */
57
- export declare const pulseXPoolPath: (from: AssetNode, to: AssetNode) => readonly Hex[] | null;
67
+ export declare const pulseXSwapPathForHop: (hop: Pick<PlannedHop, "edge" | "outcome">) => readonly Hex[] | null;
58
68
  /** One pool a swap path crosses, read in the direction of travel. */
59
69
  export type PulseXPathStep = {
60
70
  /** The pool's reserve of the token flowing in. */
@@ -1,5 +1,5 @@
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
5
  import { Chains, nativeAssetOut, toChain, uniV2Factories, uniV2Routers } from '../../config.js';
@@ -7,6 +7,7 @@ import { sameAddress } from '../../ecosystems.js';
7
7
  import { pulseXConversionSwapPath } from '../providers.js';
8
8
  import { PULSEX_MARGINAL_REFERENCE_DIVISOR, pulsechainChainId } from '../producers.js';
9
9
  import { assetNodeKey } from '../search/graph.js';
10
+ import { bestPulseXPathQuote, candidatePulseXPaths, pulseXHubTokens } from './pulsex-paths.js';
10
11
  /**
11
12
  * PulseX (the PulseChain-native decentralized exchange, used here for a
12
13
  * conversion swap leg) as a plug-in `Carrier` for `@gibs/quotes`'s
@@ -19,6 +20,8 @@ import { assetNodeKey } from '../search/graph.js';
19
20
  */
20
21
  /** WPLS, the wrapped-native intermediary {@link pulseXConversionSwapPath} routes conversions through. */
21
22
  const wplsAddress = nativeAssetOut[Chains.PLS];
23
+ /** The hub addresses every candidate path is built from — see {@link pulseXHubTokens}. */
24
+ const hubAddresses = pulseXHubTokens.map((hub) => hub.address);
22
25
  /**
23
26
  * Every PulseX deployment on PulseChain, V1 first: `uniV2Routers` paired with
24
27
  * `uniV2Factories` by index. Each factory is the router's own
@@ -70,65 +73,104 @@ const pulseXVenueForEdge = (edge) => {
70
73
  * MIRRORS `useRouteSearch.ts`'s `quotePathAddress` (the UI's async `ask`
71
74
  * already resolves native this way through that function) — this is the
72
75
  * SAME rule, restated here because `prepare`/`exact` never go through the
73
- * UI's `readAmountOut` at all; see {@link pulseXPoolPath}'s own doc.
76
+ * UI's `readAmountOut` at all; see {@link pulseXCandidatePaths}'s own doc.
74
77
  *
75
78
  * @param node - the node to resolve
76
79
  * @returns the node's own address, or {@link wplsAddress} for native PLS
77
80
  */
78
81
  const poolTokenAddress = (node) => (sameAddress(node.address, zeroAddress, 'evm') ? wplsAddress : node.address);
79
82
  /**
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.
83
+ * The fixed Wrapped-PLS path from `from` to `to` — `[from, WPLS, to]`, or
84
+ * `[from, to]` when one side is WPLS — or null when both ends resolve to the
85
+ * same pool token (PLS<->WPLS is the wrap carrier's job).
87
86
  *
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.
87
+ * NOT THE PATH A HOP IS SIGNED ALONG. The carrier's own `exact` picks the
88
+ * best of many paths ({@link candidatePulseXPaths}) and records the winner on
89
+ * the hop's outcome; the signer reads it back with {@link pulseXSwapPathForHop}.
90
+ * This fixed path serves only the carrier's asynchronous `ask` — the
91
+ * interface's `getAmountsOut` read, which the route search never calls for a
92
+ * PulseX hop, because the search only asks aggregators live.
93
+ *
94
+ * Native PLS resolves to WPLS first ({@link poolTokenAddress}), so a path
95
+ * never starts or ends on the zero address, which has no pool.
99
96
  *
100
97
  * @param from - the hop's origin node
101
98
  * @param to - the hop's destination node
102
- * @returns the swap path, or null when there is nothing for PulseX to swap
99
+ * @returns the fixed path, or null when there is nothing for PulseX to swap
103
100
  */
104
- export const pulseXPoolPath = (from, to) => {
101
+ export const pulseXWrappedNativePath = (from, to) => {
105
102
  const fromAddress = poolTokenAddress(from);
106
103
  const toAddress = poolTokenAddress(to);
107
104
  if (sameAddress(fromAddress, toAddress, 'evm'))
108
105
  return null;
109
106
  return pulseXConversionSwapPath(fromAddress, toAddress, wplsAddress);
110
107
  };
108
+ /**
109
+ * Every path a PulseX hop from `from` to `to` may be priced along, or none
110
+ * when both ends resolve to the same pool token. EVERY PULSEX PRICE AND
111
+ * POOL READ IN THIS FILE GOES THROUGH THIS ONE FUNCTION, so `prepare` reads
112
+ * exactly the pools `exact` will look up, and native PLS resolves to WPLS
113
+ * the same way in both ({@link poolTokenAddress} — a path that started on
114
+ * the zero address once left every native-PLS hop unpriced).
115
+ *
116
+ * @param from - the hop's origin node
117
+ * @param to - the hop's destination node
118
+ * @returns the candidate paths, direct first; empty when there is nothing to swap
119
+ */
120
+ const pulseXCandidatePaths = (from, to) => {
121
+ const fromAddress = poolTokenAddress(from);
122
+ const toAddress = poolTokenAddress(to);
123
+ if (sameAddress(fromAddress, toAddress, 'evm'))
124
+ return [];
125
+ return candidatePulseXPaths({ from: fromAddress, to: toAddress, wrappedNative: wplsAddress, hubs: hubAddresses });
126
+ };
111
127
  /**
112
128
  * 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.
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.
115
132
  *
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.
133
+ * NO TOKEN ALLOWLIST IS CONSULTED, on purpose: whether a pool exists is the
134
+ * reserve read's question, answered in `prepare`, not this one's.
124
135
  */
125
136
  const pulseXCarries = (from, to) => {
126
137
  if (from.ecosystem !== 'evm' || to.ecosystem !== 'evm')
127
138
  return false;
128
139
  if (from.chainId !== pulsechainChainId || to.chainId !== pulsechainChainId)
129
140
  return false;
130
- const swapPath = pulseXPoolPath(from, to);
131
- return swapPath !== null && swapPath.length >= 2;
141
+ return pulseXCandidatePaths(from, to).length > 0;
142
+ };
143
+ /**
144
+ * The swap path a planned PulseX hop must be signed along: the path its
145
+ * outcome records ({@link HopOutcome.swapPath}), which is the path the
146
+ * carrier's `exact` priced the figure on. NEVER REBUILT HERE — rebuilding a
147
+ * path at signing time is how a quote on one path becomes a signature on
148
+ * another.
149
+ *
150
+ * The recorded path is checked against the hop's own edge before it is
151
+ * returned: it must start at the edge's input pool token, end at its output
152
+ * pool token, hold only addresses, and name no token twice. A path that fails
153
+ * any check is refused (null) rather than signed.
154
+ *
155
+ * @param hop - the planned hop to sign
156
+ * @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
157
+ */
158
+ export const pulseXSwapPathForHop = (hop) => {
159
+ if (hop.edge.carrier !== 'PulseX')
160
+ return null;
161
+ const swapPath = hop.outcome.swapPath;
162
+ if (swapPath === undefined || swapPath.length < 2)
163
+ return null;
164
+ if (!swapPath.every((address) => isAddress(address, { strict: false })))
165
+ return null;
166
+ const path = swapPath.map((address) => getAddress(address));
167
+ if (new Set(path).size !== path.length)
168
+ return null;
169
+ if (!sameAddress(path[0], poolTokenAddress(hop.edge.from), 'evm'))
170
+ return null;
171
+ if (!sameAddress(path[path.length - 1], poolTokenAddress(hop.edge.to), 'evm'))
172
+ return null;
173
+ return path;
132
174
  };
133
175
  /**
134
176
  * PulseX's own constant-product swap fee, as a numerator over
@@ -219,6 +261,32 @@ const poolKey = (tokenA, tokenB) => {
219
261
  const venuePoolKey = (venue, tokenA, tokenB) => `${venue.factory.toLowerCase()}|${poolKey(tokenA, tokenB)}`;
220
262
  /** Every consecutive (tokenIn, tokenOut) hop a PulseX swap path implies. */
221
263
  const hopsAlongPath = (path) => path.slice(0, -1).map((tokenIn, index) => [tokenIn, path[index + 1]]);
264
+ /**
265
+ * The pools a path crosses on one venue, each read in the direction of
266
+ * travel, or null when any of them was never read — `getPair` found no pool,
267
+ * or the read failed. A path with a missing pool cannot be priced, exactly as
268
+ * the router's own `getAmountsOut` would revert on it.
269
+ *
270
+ * @param options.venue - the deployment whose pools the path trades against
271
+ * @param options.path - the tokens, in order
272
+ * @param options.reservesByPoolKey - every pool `prepare` read, keyed by {@link venuePoolKey}
273
+ * @returns the steps in order, or null when a pool is missing
274
+ */
275
+ const stepsAlongPath = (options) => {
276
+ const { venue, path, reservesByPoolKey } = options;
277
+ const steps = [];
278
+ for (const [tokenIn, tokenOut] of hopsAlongPath(path)) {
279
+ const reserves = reservesByPoolKey.get(venuePoolKey(venue, tokenIn, tokenOut));
280
+ if (reserves === undefined)
281
+ return null;
282
+ const tokenInIsLower = tokenIn.toLowerCase() < tokenOut.toLowerCase();
283
+ steps.push({
284
+ reserveIn: tokenInIsLower ? reserves.reserveOfLowerAddress : reserves.reserveOfHigherAddress,
285
+ reserveOut: tokenInIsLower ? reserves.reserveOfHigherAddress : reserves.reserveOfLowerAddress,
286
+ });
287
+ }
288
+ return steps;
289
+ };
222
290
  /**
223
291
  * Batch-reads reserves for every pool `nodes`' candidate PulseX paths touch,
224
292
  * on EVERY venue in `venues`, once, so {@link createPulseXCarrier}'s `exact`
@@ -235,6 +303,14 @@ const hopsAlongPath = (path) => path.slice(0, -1).map((tokenIn, index) => [token
235
303
  * the second read cannot join the first — but every venue's `getPair` rides
236
304
  * the first multicall and every pool's `getReserves` rides the second.
237
305
  *
306
+ * THE HUB SEARCH WIDENS EACH READ, NOT THEIR NUMBER. Every pool any candidate
307
+ * path crosses is read here ({@link pulseXCandidatePaths}), so `exact` can
308
+ * price every path with no further call. Measured 2026-10-06: an 8-token
309
+ * PulseChain universe asked 84 entries across the two reads (the fixed
310
+ * Wrapped-PLS path asked about 16). The shared multicall helper splits a read
311
+ * past its request-size budget into sequential posts; `getPair` crosses that
312
+ * line at about 11 pool tokens (198 entries over two venues).
313
+ *
238
314
  * @param nodes - every node the search might branch through
239
315
  * @param venues - the venues this carrier offers
240
316
  * @param clientForChain - see {@link CreatePulseXCarrierOptions}'s own field
@@ -253,11 +329,10 @@ const preparePulseXReserves = async ({ nodes, venues, clientForChain, }) => {
253
329
  continue;
254
330
  if (!pulseXCarries(from, to))
255
331
  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]);
332
+ for (const path of pulseXCandidatePaths(from, to)) {
333
+ for (const [tokenIn, tokenOut] of hopsAlongPath(path)) {
334
+ poolTokenPairs.set(poolKey(tokenIn, tokenOut), [tokenIn, tokenOut]);
335
+ }
261
336
  }
262
337
  }
263
338
  }
@@ -397,44 +472,39 @@ export const createPulseXCarrier = (options) => {
397
472
  const venue = pulseXVenueForEdge(edge);
398
473
  if (venue === null || !venues.includes(venue))
399
474
  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)
475
+ // EVERY CANDIDATE PATH IS PRICED, AND THE BEST ONE WINS. A path with a
476
+ // pool `prepare` never found is left out — the same as a router call
477
+ // that would revert — and a path that pays out nothing is never picked.
478
+ const quotes = pulseXCandidatePaths(edge.from, edge.to).flatMap((path) => {
479
+ const steps = stepsAlongPath({ venue, path, reservesByPoolKey });
480
+ return steps === null ? [] : [{ path, steps, deliveredAmount: amountOutAlongPath(amountIn, steps) }];
481
+ });
482
+ // No path had every pool read — a structural non-answer, not a refusal
483
+ // (`exact`'s own doc: `null` means "nothing to say"). NOTE THIS DOES NOT
484
+ // FALL BACK TO AN ESTIMATE: a carrier with `exact` defined never yields
485
+ // an `'estimate'` outcome (see `enumerate.ts`'s `resolveHopOutcome`,
486
+ // branch 1) — `null` here PRUNES the hop entirely, the same as a refusal.
487
+ if (quotes.length === 0)
405
488
  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) {
489
+ const best = bestPulseXPathQuote(quotes);
490
+ if (best === null) {
425
491
  return { outcome: 'refused', refusal: { kind: 'no-quote', reason: 'no liquidity on this PulseX path', minimumAmount: null } };
426
492
  }
493
+ // Price impact is measured on the CHOSEN path's own pools, with the same
494
+ // reference trade as before.
495
+ const referenceAmountIn = amountIn / PULSEX_MARGINAL_REFERENCE_DIVISOR;
427
496
  const priceImpactPercent = priceImpactAgainstMarginal({
428
497
  amountIn,
429
- amountOut: deliveredAmount,
430
- referenceAmountIn: amountIn / PULSEX_MARGINAL_REFERENCE_DIVISOR,
431
- referenceAmountOut: amountOutAlongPath(amountIn / PULSEX_MARGINAL_REFERENCE_DIVISOR, steps),
498
+ amountOut: best.deliveredAmount,
499
+ referenceAmountIn,
500
+ referenceAmountOut: amountOutAlongPath(referenceAmountIn, best.steps),
432
501
  });
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 };
502
+ // The path rides on the answer, so the signer signs exactly what was
503
+ // priced (`pulseXSwapPathForHop`). Impact is left off (not zero) when the
504
+ // reference trade rounds away: an unknown impact must stay unknown so the
505
+ // quality gate withholds the plan.
506
+ const delivered = { outcome: 'delivered', deliveredAmount: best.deliveredAmount, swapPath: best.path };
507
+ return priceImpactPercent === null ? delivered : { ...delivered, priceImpactPercent };
438
508
  };
439
509
  return {
440
510
  id: 'PulseX',
@@ -47,6 +47,26 @@ export declare const COMPOSED_ROUTE_MAX_PRICE_IMPACT_PERCENT = 3;
47
47
  * of the reader's money.
48
48
  */
49
49
  export declare const COMPOSED_ROUTE_MAX_VALUE_LOSS_PERCENT = 5;
50
+ /**
51
+ * The end-to-end value GAIN above which a composed route is withdrawn, in
52
+ * percent of what the reader pays.
53
+ *
54
+ * A ROUTE CANNOT MAKE MONEY. Every leg charges a fee, so a route that
55
+ * delivers more value than it takes says one of its figures is wrong. Only a
56
+ * disagreement between the two price feeds can make a gain look real, and the
57
+ * value-loss ceiling already trusts those feeds to within the same five
58
+ * percent. So the bound is symmetric with
59
+ * {@link COMPOSED_ROUTE_MAX_VALUE_LOSS_PERCENT}.
60
+ *
61
+ * THE DEFECT THIS STOPS. On 2026-10-03 the engine served 0.0002 BNB to HEX at
62
+ * 495,256 HEX, about seven thousand times the rate of the next tier. The
63
+ * value-loss test read that as a loss of minus seven hundred thousand percent
64
+ * and passed it. A PulseX hop rescaled from an upstream live answer keeps the
65
+ * price impact measured at its original, smaller input
66
+ * (`recomposePath` in `@gibs/quotes`'s `search/waves.ts`), so the impact test
67
+ * cannot see an inflated amount either. Only the two ends of the journey can.
68
+ */
69
+ export declare const COMPOSED_ROUTE_MAX_VALUE_GAIN_PERCENT = 5;
50
70
  /**
51
71
  * The verdict on one composed route.
52
72
  *
@@ -62,18 +82,20 @@ export type ComposedRouteVerdict = {
62
82
  readonly missing: 'price-impact' | 'route-value';
63
83
  } | {
64
84
  readonly state: 'rejected';
65
- readonly reason: 'price-impact' | 'value-loss';
85
+ readonly reason: 'price-impact' | 'value-loss' | 'value-gain';
66
86
  /** The measured figure, in percent, that failed its ceiling. */
67
87
  readonly measuredPercent: number;
68
88
  /** The ceiling it failed. */
69
89
  readonly ceilingPercent: number;
70
90
  };
71
91
  /**
72
- * Judges a composed route against both ceilings.
92
+ * Judges a composed route against its three ceilings.
73
93
  *
74
- * Checks price impact first and value loss second, because impact names the
75
- * specific leg at fault while value loss only says the journey as a whole went
76
- * wrong — so reporting impact when both fail is the more useful answer.
94
+ * Checks price impact first, then value loss, then value gain. Impact names
95
+ * the specific leg at fault, while the two value tests only say the journey as
96
+ * a whole went wrong, so impact is the more useful answer when several fail.
97
+ * In the measured percent of a `'value-gain'` rejection, the gain is a
98
+ * positive number.
77
99
  *
78
100
  * @param options.priceImpactPercent - the swap leg's price impact, from the
79
101
  * estimate's marginal reference quote; null when not yet quoted
@@ -98,6 +120,6 @@ export declare const assessComposedRoute: ({ priceImpactPercent, paidUsdValue, d
98
120
  * pricing" rather than "no route", which a boolean alone could not.
99
121
  *
100
122
  * @param verdict - the verdict from {@link assessComposedRoute}
101
- * @returns true only when the route passed both ceilings
123
+ * @returns true only when the route passed every ceiling
102
124
  */
103
125
  export declare const composedRouteMayBeOffered: (verdict: ComposedRouteVerdict) => boolean;