@gibs/bridge-sdk 1.19.0 → 1.20.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 (111) hide show
  1. package/dist/abis.js +0 -2
  2. package/dist/bridge-limits.js +0 -38
  3. package/dist/chain-info.js +0 -116
  4. package/dist/chain-names.js +0 -151
  5. package/dist/chains.js +0 -198
  6. package/dist/cjs/abis.js +0 -2
  7. package/dist/cjs/bridge-limits.js +0 -38
  8. package/dist/cjs/chain-info.js +0 -116
  9. package/dist/cjs/chain-names.js +0 -151
  10. package/dist/cjs/chains.js +0 -198
  11. package/dist/cjs/claims.js +0 -60
  12. package/dist/cjs/config.js +1 -208
  13. package/dist/cjs/consolidated-quote.js +0 -465
  14. package/dist/cjs/destination-calldata.js +0 -71
  15. package/dist/cjs/ecosystems.js +0 -183
  16. package/dist/cjs/execution.js +0 -92
  17. package/dist/cjs/fee-type.js +0 -1
  18. package/dist/cjs/hyperliquid-execution.js +0 -98
  19. package/dist/cjs/image-links.js +0 -108
  20. package/dist/cjs/lifi-execution-guard.js +65 -172
  21. package/dist/cjs/near-intents-conformance.js +0 -110
  22. package/dist/cjs/near-intents-route.js +0 -149
  23. package/dist/cjs/near-intents.js +0 -562
  24. package/dist/cjs/origin-deposit.js +0 -56
  25. package/dist/cjs/pre-quote.js +0 -80
  26. package/dist/cjs/prices.js +0 -113
  27. package/dist/cjs/providers.js +0 -83
  28. package/dist/cjs/quote-fees.js +0 -80
  29. package/dist/cjs/referral.js +0 -75
  30. package/dist/cjs/relay-currencies.js +0 -31
  31. package/dist/cjs/relay-depository.js +52 -77
  32. package/dist/cjs/relay-quote.js +0 -394
  33. package/dist/cjs/route-steps.js +0 -100
  34. package/dist/cjs/routing/amounts.js +0 -83
  35. package/dist/cjs/routing/candidate-route.js +0 -73
  36. package/dist/cjs/routing/candidate.js +0 -46
  37. package/dist/cjs/routing/carrier-provider-key.js +0 -27
  38. package/dist/cjs/routing/carriers/omnibridge.js +0 -120
  39. package/dist/cjs/routing/carriers/pulsex-paths.js +0 -57
  40. package/dist/cjs/routing/carriers/pulsex-split.js +0 -92
  41. package/dist/cjs/routing/carriers/pulsex.js +0 -279
  42. package/dist/cjs/routing/carriers/wrap.js +0 -57
  43. package/dist/cjs/routing/composed-route-quality.js +0 -95
  44. package/dist/cjs/routing/entry-chain-anchor.js +0 -27
  45. package/dist/cjs/routing/format.js +0 -50
  46. package/dist/cjs/routing/index.js +0 -28
  47. package/dist/cjs/routing/producers.js +0 -854
  48. package/dist/cjs/routing/providers.js +0 -27
  49. package/dist/cjs/routing/search/compose.js +0 -56
  50. package/dist/cjs/routing/search/graph.js +0 -364
  51. package/dist/cjs/routing/search/plan-route-data.js +0 -508
  52. package/dist/cjs/routing/search/score.js +0 -344
  53. package/dist/cjs/routing/swap-transactions.js +0 -74
  54. package/dist/cjs/routing/usd-value.js +0 -12
  55. package/dist/cjs/settings.js +1 -456
  56. package/dist/cjs/swap-venues.js +0 -17
  57. package/dist/cjs/ton-connect.js +0 -82
  58. package/dist/cjs/ton-execution.js +0 -6
  59. package/dist/cjs/ton.js +0 -42
  60. package/dist/cjs/tron-execution.js +0 -145
  61. package/dist/claims.js +0 -60
  62. package/dist/config.js +1 -211
  63. package/dist/consolidated-quote.js +0 -465
  64. package/dist/destination-calldata.js +0 -71
  65. package/dist/ecosystems.js +0 -183
  66. package/dist/execution.js +0 -92
  67. package/dist/fee-type.js +0 -2
  68. package/dist/hyperliquid-execution.js +0 -98
  69. package/dist/image-links.js +0 -108
  70. package/dist/lifi-execution-guard.js +65 -172
  71. package/dist/near-intents-conformance.js +0 -110
  72. package/dist/near-intents-route.js +0 -149
  73. package/dist/near-intents.js +0 -562
  74. package/dist/origin-deposit.js +0 -56
  75. package/dist/pre-quote.js +0 -80
  76. package/dist/prices.js +0 -113
  77. package/dist/providers.js +0 -83
  78. package/dist/quote-fees.js +0 -80
  79. package/dist/referral.js +0 -75
  80. package/dist/relay-currencies.js +0 -31
  81. package/dist/relay-depository.js +52 -77
  82. package/dist/relay-quote.js +0 -394
  83. package/dist/route-steps.js +0 -100
  84. package/dist/routing/amounts.js +0 -83
  85. package/dist/routing/candidate-route.js +0 -73
  86. package/dist/routing/candidate.js +0 -46
  87. package/dist/routing/carrier-provider-key.js +0 -27
  88. package/dist/routing/carriers/omnibridge.js +0 -120
  89. package/dist/routing/carriers/pulsex-paths.js +0 -57
  90. package/dist/routing/carriers/pulsex-split.js +0 -92
  91. package/dist/routing/carriers/pulsex.js +0 -279
  92. package/dist/routing/carriers/wrap.js +0 -57
  93. package/dist/routing/composed-route-quality.js +0 -95
  94. package/dist/routing/entry-chain-anchor.js +0 -27
  95. package/dist/routing/format.js +0 -50
  96. package/dist/routing/index.js +0 -28
  97. package/dist/routing/producers.js +0 -854
  98. package/dist/routing/providers.js +0 -27
  99. package/dist/routing/search/compose.js +0 -56
  100. package/dist/routing/search/graph.js +0 -364
  101. package/dist/routing/search/plan-route-data.js +0 -508
  102. package/dist/routing/search/score.js +0 -344
  103. package/dist/routing/swap-transactions.js +0 -74
  104. package/dist/routing/usd-value.js +0 -12
  105. package/dist/settings.js +1 -456
  106. package/dist/swap-venues.js +0 -17
  107. package/dist/ton-connect.js +0 -82
  108. package/dist/ton-execution.js +0 -6
  109. package/dist/ton.js +0 -42
  110. package/dist/tron-execution.js +0 -145
  111. package/package.json +3 -3
@@ -1,301 +1,62 @@
1
- /**
2
- * Pure route-producer helpers and constants shared by every route builder.
3
- * Lives in the software development kit so the builders can relocate here in
4
- * subsequent tasks without pulling user-interface dependencies into the kit.
5
- *
6
- * All helpers are framework-agnostic: no React, no Svelte, no display formatters.
7
- */
8
1
  import { Chains, Providers, pathway } from '../config.js';
9
2
  import { chainDisplayName } from '../chain-names.js';
10
3
  import { usdValueOfAmount } from './usd-value.js';
11
4
  import * as imageLinks from '../image-links.js';
12
5
  import { roundUsdToCents } from './format.js';
13
6
  import { formatUnits } from 'viem';
14
- // ---------------------------------------------------------------------------
15
- // Constants
16
- // ---------------------------------------------------------------------------
17
- /**
18
- * The PulseX swap fee in basis points (hundredths of a percent). The deferred
19
- * outbound leg swaps the bridged intermediate token into the wanted token on the
20
- * entry chain at this rate, so the deferred section's fee is APPROXIMATE — the
21
- * binding quote is only known when the user returns. 29 basis points is roughly
22
- * 0.29%.
23
- */
24
7
  const PULSEX_SWAP_FEE_BASIS_POINTS = 29n;
25
- /** The denominator that turns {@link PULSEX_SWAP_FEE_BASIS_POINTS} into a fraction. */
26
8
  const BASIS_POINTS_DENOMINATOR = 10000n;
27
- /**
28
- * Divisor for the MARGINAL reference swap quote. A swap's price impact cannot be
29
- * read from the trade quote alone — `getAmountsOut(fullInput)` already folds the
30
- * impact into its output, and the PulseChain USD prices are themselves PulseX
31
- * pool quotes, so differencing them cancels the impact out (it vanishes to
32
- * $0.00). Instead the estimate quotes a TINY reference input — `1/this` of the
33
- * trade — whose own impact is negligible, giving the near-spot marginal rate. The
34
- * gap between that marginal rate and the full trade's realized rate IS the price
35
- * impact (the swap fee rides in BOTH quotes, so it cancels and the gap is impact
36
- * alone). 1/10000 of the trade is small enough to be effectively marginal yet
37
- * large enough that its output does not round away.
38
- */
39
9
  export const PULSEX_MARGINAL_REFERENCE_DIVISOR = 10000n;
40
- /**
41
- * The approximate wall-clock wait for an omnibridge crossing to settle before a
42
- * DEFERRED swap leg can be signed — the same ~20 minutes whether the swap happens
43
- * on the entry chain (outbound PulseChain → Ethereum/Binance Smart Chain) or on
44
- * PulseChain (conversion route: bridge an anchor in, then swap on PulseX).
45
- * Surfaced compactly as `~20 min` in the deferred section head, mirroring how
46
- * every other section shows its estimate, rather than spelled out inside the
47
- * deferred note.
48
- */
49
10
  export const BRIDGE_SETTLEMENT_ETA_MINUTES = 20;
50
- /**
51
- * The approximate wall-clock duration of the SWAP itself, in seconds — a PulseX
52
- * swap (or an entry-chain swap) settles in roughly ten seconds once signed. This
53
- * is the swap leg's OWN time, distinct from the ~20-minute crossing it waits
54
- * behind ({@link BRIDGE_SETTLEMENT_ETA_MINUTES}): the wait belongs on the BRIDGE
55
- * leg, the near-instant swap on the SWAP leg, so each section reads honestly
56
- * ("~20 min" to cross, then "~10 sec" to swap) rather than stamping the crossing's
57
- * twenty minutes onto the swap.
58
- */
59
11
  const PULSEX_SWAP_ETA_SECONDS = 10;
60
- /** Decimal chain id for PulseChain — the shared destination of every inbound route. */
61
12
  export const pulsechainChainId = Number(Chains.PLS);
62
- // ---------------------------------------------------------------------------
63
- // Private backing maps
64
- // ---------------------------------------------------------------------------
65
- /**
66
- * The fully spelled-out display name of each omnibridge provider, keyed by the
67
- * raw provider identifier the builders carry. These are the headline names the
68
- * route strip shows as the route's bridge.
69
- *
70
- * Typed `Record<Provider, string>`, exhaustive over the two real omnibridge
71
- * crossings, so a third one added to `Providers` without a name here is a
72
- * compile error rather than a silent gap in {@link omnibridgeProviderDisplayName}.
73
- */
74
13
  const omnibridgeProviderDisplayNames = {
75
14
  [Providers.PULSECHAIN]: 'PulseChain Bridge',
76
15
  [Providers.TOKENSEX]: 'Tokens Express Bridge',
77
16
  };
78
- /**
79
- * Whether `value` genuinely names an omnibridge crossing, narrowing it to
80
- * {@link Provider}.
81
- *
82
- * @param value - a string that may or may not be a real omnibridge provider
83
- * @returns whether `value` is one of the two registered crossings
84
- */
85
17
  const isOmnibridgeProvider = (value) => value === Providers.PULSECHAIN || value === Providers.TOKENSEX;
86
- /**
87
- * The display name for an omnibridge crossing, fully spelled out. Narrowed to
88
- * exactly {@link Provider} — the two real crossings — rather than any string,
89
- * so passing an aggregator's `ProviderId` (or anything else) is a COMPILE
90
- * ERROR here, not a silently wrong name.
91
- *
92
- * PREFER THIS OVER {@link providerDisplayName} wherever the caller already
93
- * holds a genuine `Provider`. `providerDisplayName` exists only because some
94
- * callers (`RouteCandidate.omnibridgeProvider`, `RouteData.headlineProviderKey`)
95
- * are typed `string` at their own source and cannot be narrowed from here.
96
- *
97
- * @param provider - a real omnibridge provider
98
- * @returns the spelled-out provider name
99
- */
100
18
  export const omnibridgeProviderDisplayName = (provider) => omnibridgeProviderDisplayNames[provider];
101
- /**
102
- * The fully spelled-out display name of an omnibridge ENTRY chain, keyed by its
103
- * decimal chain id. Acronyms are spelled out rather than read from viem's terse
104
- * `name`. Ethereum and Binance Smart Chain are the only omnibridge entry points;
105
- * any other chain id has no entry and {@link resolveEntryChainName} returns null.
106
- */
107
19
  const entryChainDisplayName = {
108
20
  [Number(Chains.ETH)]: 'Ethereum',
109
21
  [Number(Chains.BNB)]: 'Binance Smart Chain',
110
22
  };
111
- // ---------------------------------------------------------------------------
112
- // Helpers
113
- // ---------------------------------------------------------------------------
114
- /**
115
- * Adds two nullable United States dollar NUMBERS. A null operand contributes
116
- * nothing; two nulls yield null so an all-unpriced route keeps a null cost rather
117
- * than a misleading 0.
118
- *
119
- * This sums the RAW values and leaves the single cent-rounding to the display
120
- * adapter (round(a + b)). It deliberately differs from the pre-split
121
- * `sumFormattedUsd`, which summed two already-cent-rounded strings
122
- * (round(a) + round(b)); the two can diverge by a cent when both operands carry
123
- * sub-cent fractions. Rounding the total once is the more accurate result and
124
- * keeps this helper free of any display formatter, so the raw producers stay
125
- * relocatable. See the "sums the raw total once" test for the locked behavior.
126
- *
127
- * @param first - the first dollar value, or null/undefined when absent
128
- * @param second - the second dollar value, or null/undefined when absent
129
- * @returns the sum, or null when both operands are absent
130
- */
131
23
  export const addUsdValues = (first, second) => {
132
24
  if (first == null && second == null)
133
25
  return null;
134
26
  return (first ?? 0) + (second ?? 0);
135
27
  };
136
- /**
137
- * Subtracts a cost from a dollar value on the SAME basis — the operation every
138
- * producer uses to derive what a leg delivers from what entered it.
139
- *
140
- * Unlike {@link addUsdValues} this is deliberately NOT symmetric about null: an
141
- * unknown starting value cannot be reduced, so it stays unknown, while an unknown
142
- * cost is treated as no cost. That asymmetry is what keeps a route with one
143
- * unpriced leg from reporting a delivered value it never established.
144
- *
145
- * @param value - the dollar value entering the leg, or null/undefined when unknown
146
- * @param cost - the leg's dollar cost, or null/undefined when unpriced
147
- * @returns the value less the cost, or null when the starting value is unknown
148
- */
149
28
  export const subtractUsdValues = (value, cost) => {
150
29
  if (value == null)
151
30
  return null;
152
31
  return value - (cost ?? 0);
153
32
  };
154
- /**
155
- * The display name for an omnibridge crossing, accepting a raw `string` rather
156
- * than {@link Provider}.
157
- *
158
- * PREFER {@link omnibridgeProviderDisplayName} INSTEAD wherever the caller
159
- * already holds a genuine `Provider` — that one is a compile error for
160
- * anything else. This wide version exists only for callers whose OWN field is
161
- * typed `string` at its source and so cannot be narrowed here:
162
- * `RouteCandidate.omnibridgeProvider` and `RouteData.headlineProviderKey`
163
- * (`routing/candidate.ts`, `routing/route-data.ts`).
164
- *
165
- * KEPT DELIBERATELY NON-THROWING, UNLIKE {@link providerById}. The obvious
166
- * next step — throw on an unrecognized key, the way `providerById` does for
167
- * an aggregator — was tried and reverted: `toRouteView.ts`'s headline
168
- * resolution calls this with `RouteData.headlineProviderKey`, a `string` this
169
- * repository does not otherwise guarantee is a real provider of ANY kind, and
170
- * an existing, deliberate test (`toRouteView.test.ts`, "headline falls back
171
- * to 'Omnibridge' for an unknown headline provider key") requires that a
172
- * genuinely-unrecognized key render a generic label rather than crash the
173
- * whole route card. A NEAR Intents leg being mislabeled "Omnibridge" was a
174
- * real defect (see `near-intents-route.ts`), fixed there by calling the
175
- * RIGHT function (`providerById(id).label`) instead of this one — not by
176
- * making this one less forgiving of input nobody validates before it arrives.
177
- *
178
- * `'PulseX'` IS RECOGNIZED HERE TOO, unlike an aggregator `ProviderId`, and for
179
- * a different reason than the omnibridge case: it needs no registry lookup —
180
- * there is exactly one PulseX, the label IS the key — so there is nothing an
181
- * aggregator-style `providerById(id).label` call would add. A same-chain
182
- * PulseX swap route (`RouteData.kind === 'swap'`) has NO omnibridge leg at
183
- * all, so falling back to "Omnibridge" for it would name a bridge that never
184
- * ran.
185
- *
186
- * @param provider - the raw provider key (e.g. `'pulsechain'`, `'PulseX'`)
187
- * @returns the spelled-out provider name, or "Omnibridge" when unregistered
188
- */
189
33
  export const providerDisplayName = (provider) => {
190
34
  if (provider === 'PulseX')
191
35
  return 'PulseX';
192
36
  return isOmnibridgeProvider(provider) ? omnibridgeProviderDisplayName(provider) : 'Omnibridge';
193
37
  };
194
- /**
195
- * The fully spelled-out display name of an omnibridge entry chain, or null when
196
- * the chain id is null or is not one of the two entry points (Ethereum, Binance
197
- * Smart Chain). A null result makes route builders return null, so an
198
- * unrecognised destination leaves the strip empty rather than naming a chain it
199
- * cannot bridge to.
200
- *
201
- * @param entryChainId - the decimal entry chain id, or null for direct routes
202
- * @returns the spelled-out chain name, or null
203
- */
204
38
  export const resolveEntryChainName = (entryChainId) => {
205
39
  if (entryChainId === null)
206
40
  return null;
207
41
  return entryChainDisplayName[entryChainId] ?? null;
208
42
  };
209
- /**
210
- * Parses one of the aggregator's own United States dollar figures —
211
- * `fromAmountUsd` or `toAmountUsd` on a candidate's quote.
212
- *
213
- * `Number('')` IS `0`, NOT "UNKNOWN", and both the LI.FI and Relay quote
214
- * parsers can hand this module a blank amount string. Read with a bare
215
- * `Number()`, a blank `fromAmountUsd` priced a route's origin leg at zero
216
- * cost — one measured case showed a route reporting a net gain of +$273 that
217
- * had in fact cost real money, and led the strip on the strength of it — and
218
- * a blank `toAmountUsd` would do the same to what a direct route (no
219
- * omnibridge leg) reports as delivered. A dollar figure only means something
220
- * when it is a positive, finite number: blank text, non-numeric text, zero,
221
- * and a negative number are all treated as UNPRICED rather than free.
222
- *
223
- * @param raw - the provider's amount string, or null/undefined when absent
224
- * @returns the parsed value, or null when the field cannot be trusted
225
- */
226
43
  export const usdNumber = (raw) => {
227
44
  if (raw == null || raw.trim() === '')
228
45
  return null;
229
46
  const value = Number(raw);
230
47
  return Number.isFinite(value) && value > 0 ? value : null;
231
48
  };
232
- /**
233
- * What the user pays for a candidate's origin leg, in United States dollars, as
234
- * the provider itself valued it (`fromAmountUsd`). This is the candidate's SOURCE
235
- * BASIS: every dollar figure the route reports is derived from it, so the costs
236
- * shown and the value delivered can never drift onto different price sources.
237
- *
238
- * @param candidate - the priced route candidate
239
- * @returns the amount paid in United States dollars, or null when the provider gave none
240
- */
241
- /**
242
- * Exported so `search/plan-route-data.ts` can generalize this SAME valuation
243
- * to a general-search plan's first hop, per the route-search design's
244
- * `RouteScoreInputs` doc ("import and reuse them; do not write a second
245
- * valuation") — a candidate-shaped module boundary, not a second formula.
246
- */
247
49
  export const candidatePaidUsd = (candidate) => usdNumber(candidate.quote.fromAmountUsd);
248
- /**
249
- * The cost of a candidate, in United States dollars, as the difference between
250
- * what the user pays for the aggregator leg ({@link candidatePaidUsd}) and what the
251
- * delivered tokens are worth. Returns null when the provider gave no
252
- * `fromAmountUsd` or the delivered tokens are unpriced — the fee then shows only
253
- * what it can. The figure is clamped at zero because a fleeting price-source
254
- * skew should never render a negative fee.
255
- *
256
- * @param candidate - the priced route candidate
257
- * @returns the fee in United States dollars, or null when either side is unpriced
258
- */
259
50
  const candidateFeeUsd = (candidate) => {
260
51
  const paid = candidatePaidUsd(candidate);
261
52
  if (paid === null)
262
53
  return null;
263
- // Charges levied outside `fromAmount` are part of the cost even though the
264
- // difference between paid and delivered cannot see them.
265
54
  const outside = candidateOutsideFeesUsd(candidate);
266
55
  const delivered = candidateDeliveredUsd(candidate);
267
56
  if (delivered === null)
268
57
  return null;
269
58
  return Math.max(0, paid + outside - delivered);
270
59
  };
271
- /**
272
- * What a candidate's delivered tokens are worth, in United States dollars.
273
- *
274
- * TWO SOURCES, AND WHICH ONE APPLIES IS DECIDED BY WHETHER A SECOND LEG
275
- * FOLLOWS — never by which one happens to be present.
276
- *
277
- * A route with no omnibridge leg ends where the provider drops the funds, so
278
- * the provider valued the very tokens the reader receives, in the same answer
279
- * and on the same feed as `fromAmountUsd`. The difference between the two is
280
- * then the route's cost and nothing else. This is the case that had no price at
281
- * all: the destination chain is not one the bridge's own price sources cover,
282
- * so `destinationUsdPrice` arrived null and every direct route reported no fee.
283
- *
284
- * A route that continues through the omnibridge must NOT use it. The provider's
285
- * delivered side there is the entry chain's tokens, handed to the mediator —
286
- * not what finally lands on PulseChain, which is what `deliveredAmount` counts.
287
- * Those two differ by the omnibridge's own fee, so reading the provider's
288
- * figure would report a crossing as free.
289
- *
290
- * Exported because the ranking must value a route on the SAME basis the route
291
- * states. The ranker used to price the delivered tokens against
292
- * `destinationUsdPrice` alone, which is null on every direct route, so every
293
- * direct route was unpriced and the ranker fell back to an order that could not
294
- * see what each route delivers.
295
- *
296
- * @param candidate - the priced route candidate
297
- * @returns the delivered value in United States dollars, or null when unpriced
298
- */
299
60
  export const candidateDeliveredUsd = (candidate) => {
300
61
  if (candidate.omnibridgeProvider === null) {
301
62
  const quoted = usdNumber(candidate.quote.toAmountUsd);
@@ -310,34 +71,7 @@ export const candidateDeliveredUsd = (candidate) => {
310
71
  price: candidate.destinationUsdPrice,
311
72
  });
312
73
  };
313
- /**
314
- * The share of the requested amount below which an unspent remainder is treated
315
- * as dust and not reported, in basis points (hundredths of a percent). Ten basis
316
- * points is 0.1%.
317
- *
318
- * A backward-solved quote lands a handful of base units away from the amount it
319
- * was asked for even when it consumes essentially all of it — the control case
320
- * measured eleven wei left over on a five-hundredths-of-an-ether transfer. A
321
- * remainder that small is rounding, not money the user gets to keep, and naming
322
- * it would add a line to every route that says nothing.
323
- */
324
74
  const UNSPENT_MATERIAL_BASIS_POINTS = 10n;
325
- /**
326
- * The origin tokens a candidate's quote does NOT consume — the difference between
327
- * what the user typed and what the backward-solved quote turned out to need.
328
- *
329
- * Valued from the candidate's OWN source basis (`fromAmountUsd` divided across
330
- * `fromAmount`) rather than from a separate price feed, for the same reason every
331
- * other figure on the route is: two feeds disagree, and a remainder priced against
332
- * a different one would reopen the gap this whole model exists to close.
333
- *
334
- * Returns null when the requested amount is unknown, when the quote consumed all
335
- * of it, or when the remainder is below {@link UNSPENT_MATERIAL_BASIS_POINTS} —
336
- * a route that spends what it was given says nothing about unspent tokens.
337
- *
338
- * @param candidate - the priced route candidate
339
- * @returns the unspent remainder, or null when there is none worth naming
340
- */
341
75
  const candidateUnspent = (candidate) => {
342
76
  const requested = candidate.requestedAmount;
343
77
  if (requested == null || requested <= 0n)
@@ -350,9 +84,6 @@ const candidateUnspent = (candidate) => {
350
84
  return null;
351
85
  }
352
86
  const paidUsd = candidatePaidUsd(candidate);
353
- // The remainder's value is the paid basis scaled by the ratio of the two
354
- // amounts, which is exact regardless of the origin token's decimals and needs
355
- // no per-token price at all.
356
87
  const usdValue = paidUsd === null
357
88
  ? null
358
89
  : (paidUsd * Number(formatUnits(remainder, candidate.originToken.decimals))) /
@@ -364,86 +95,22 @@ const candidateUnspent = (candidate) => {
364
95
  usdValue: usdValue !== null && Number.isFinite(usdValue) ? usdValue : null,
365
96
  };
366
97
  };
367
- /**
368
- * A token address shortened for display, for example `0x1234…5678`. Preserves
369
- * short addresses that are already readable without truncation.
370
- *
371
- * @param address - the full token address
372
- * @returns the shortened address string
373
- */
374
98
  const shortenAddress = (address) => address.length > 11 ? `${address.slice(0, 6)}…${address.slice(-4)}` : address;
375
- /**
376
- * The token label for a step endpoint: the provider-supplied SYMBOL when present
377
- * (for example "USDC"), otherwise a SHORTENED address. Never the raw full address,
378
- * which is unreadable in the hop conversion line.
379
- *
380
- * @param symbol - the token symbol, or null when not supplied by the provider
381
- * @param address - the token's full on-chain address, used as fallback
382
- * @returns the label to display for this endpoint
383
- */
384
99
  const stepTokenLabel = (symbol, address) => symbol ?? shortenAddress(address);
385
- /**
386
- * The swap conversion label for a normalized aggregator route step — the
387
- * "from token → to token" string shown in the hop's conversion line. Returns null
388
- * when the step carries no token addresses (Relay steps are empty; the hop then
389
- * shows no conversion clause).
390
- *
391
- * @param step - one normalized aggregator route step
392
- * @returns the conversion string, or null when address data is absent
393
- */
394
100
  const stepConversion = (step) => {
395
101
  if (!step.fromToken || !step.toToken)
396
102
  return null;
397
103
  return `${stepTokenLabel(step.fromTokenSymbol, step.fromToken)} → ${stepTokenLabel(step.toTokenSymbol, step.toToken)}`;
398
104
  };
399
- /**
400
- * A fee rate as percent text: 3e15 reads "0.3%". Exact, because it scales the
401
- * integer and lets `formatUnits` place the point, rather than going through a
402
- * floating point number that prints 2.9699999999999998.
403
- */
404
105
  const rateAsPercentText = (rate) => `${formatUnits(rate * 100n, 18)}%`;
405
- /** A label, with the rate beside it when the rate is known. */
406
106
  const labelWithRate = (label, rate) => rate == null ? label : `${label} (${rateAsPercentText(rate)})`;
407
- /** A dollar value as the cent string a {@link QuoteFee} carries, or null when unpriced. */
408
107
  const feeAmountUsdText = (value) => value === null ? null : roundUsdToCents(value).toFixed(2);
409
- /**
410
- * The token the bridge takes its own fee in.
411
- *
412
- * The fee manager lives on the home mediator, on PulseChain, and the pathway
413
- * says which side of the crossing that is. Leaving PulseChain it is `'from'`:
414
- * the home mediator takes the fee from the tokens it receives, before it sends
415
- * the message. Arriving it is `'to'`: the home mediator takes the fee from the
416
- * tokens it releases. So the fee is always in the PulseChain side's token.
417
- *
418
- * @returns the fee's token, or null when the bridge key names no pathway
419
- */
420
108
  const bridgeFeeToken = ({ bridgeKey, sourceToken, arrivingToken, }) => {
421
- // Not production-only: the testnet pathways live in a second map, and a
422
- // mainnet key finds its own pathway in the first map either way.
423
109
  const crossing = pathway(bridgeKey, false);
424
110
  if (!crossing)
425
111
  return null;
426
112
  return crossing.feeManager === 'from' ? sourceToken : arrivingToken;
427
113
  };
428
- /**
429
- * The cost of one omnibridge crossing: the bridge's own fee plus the Gibs
430
- * delivery fee, both valued on the route's source basis.
431
- *
432
- * A PARTIAL SUM IS NEVER A COST. When either fee is unknown the figure is null,
433
- * because the known half alone would read as the whole cost and understate it.
434
- * A delivery the reader did not ask for is not unknown: it is a real zero.
435
- *
436
- * The delivery fee is in the ARRIVING token: the destination router takes it
437
- * from the value that reaches it.
438
- *
439
- * EVERY AMOUNT IS COUNTED IN SOURCE BASE UNITS, because the settings cascade
440
- * derives both fees from `amountToBridge`, which is. So both are valued with the
441
- * source token's decimals and price, the same basis the route's delivered value
442
- * uses. The bridge keeps a token's decimals when it creates the counterpart, so
443
- * that count is also the count in the arriving token each charge names.
444
- *
445
- * @returns the crossing's cost and the charges behind it
446
- */
447
114
  const crossingCost = ({ bridgeKey, feeRoots, sourceToken, arrivingToken, sourceUsdPrice, }) => {
448
115
  const [provider] = bridgeKey;
449
116
  const { bridgeFeeAmount, bridgeFeeRate, deliveryFee } = feeRoots;
@@ -487,25 +154,11 @@ const crossingCost = ({ bridgeKey, feeRoots, sourceToken, arrivingToken, sourceU
487
154
  : bridgeFeeUsdValue + deliveryFeeUsdValue;
488
155
  return { feeUsdValue, fees: [...bridgeFees, ...deliveryFees] };
489
156
  };
490
- /** A token as the fee token a {@link QuoteFee} names, on the chain it is presented on. */
491
157
  const toFeeToken = (token, chainId) => ({
492
158
  symbol: token.symbol,
493
159
  decimals: token.decimals,
494
160
  chainId,
495
161
  });
496
- /**
497
- * The raw token reference for one end of a normalized aggregator step — the shape
498
- * an intermediate stop is rendered from.
499
- *
500
- * The provider's own token image wins when present. When it supplies none, the
501
- * logo is left UNSET for a `0x` address so the adapter derives it from the image
502
- * service, and set to null for a non-Ethereum address (Bitcoin, Solana, Tron, The
503
- * Open Network), which the image service cannot key by a checksummed address.
504
- * Decimals are omitted: an intermediate stop formats no amount.
505
- *
506
- * @param endpoint - one end of a normalized step
507
- * @returns the raw token reference for that end
508
- */
509
162
  const stepTokenRef = (endpoint) => ({
510
163
  symbol: endpoint.symbol ?? shortenAddress(endpoint.address),
511
164
  address: endpoint.address,
@@ -513,25 +166,18 @@ const stepTokenRef = (endpoint) => ({
513
166
  logoUri: endpoint.logoUri ?? (endpoint.address.startsWith('0x') ? undefined : null),
514
167
  chainLogoUri: imageLinks.networkOrPseudo(endpoint.chainId),
515
168
  });
516
- /** The token a step consumes, as a raw reference. */
517
169
  const stepFromTokenRef = (step) => stepTokenRef({
518
170
  address: step.fromToken,
519
171
  symbol: step.fromTokenSymbol,
520
172
  chainId: step.fromChainId,
521
173
  logoUri: step.fromTokenLogoUri,
522
174
  });
523
- /** The token a step produces, as a raw reference — the stop it lands the funds on. */
524
175
  const stepToTokenRef = (step) => stepTokenRef({
525
176
  address: step.toToken,
526
177
  symbol: step.toTokenSymbol,
527
178
  chainId: step.toChainId,
528
179
  logoUri: step.toTokenLogoUri,
529
180
  });
530
- /**
531
- * True when two references name the same token on the same chain. Hex addresses
532
- * compare case-insensitively (providers disagree on checksum casing); anything
533
- * else compares exactly, because a base58 address is genuinely case-sensitive.
534
- */
535
181
  const isSameTokenRef = (left, right) => {
536
182
  if (left.chainId !== right.chainId)
537
183
  return false;
@@ -541,20 +187,6 @@ const isSameTokenRef = (left, right) => {
541
187
  return false;
542
188
  return left.address.toLowerCase() === right.address.toLowerCase();
543
189
  };
544
- /**
545
- * The token flow a set of hops walks: the origin, then the token each hop
546
- * PRODUCES. Every entry except the first is a stop the funds actually reach, and
547
- * the last hop's output is the destination — so the intermediate stops are exactly
548
- * the hops minus the last one.
549
- *
550
- * A hop that names no output token contributes nothing (the flow still reaches the
551
- * destination through the hops that do), and a hop that produces the token the
552
- * flow is already holding is collapsed rather than drawn twice.
553
- *
554
- * @param origin - the token the route starts from
555
- * @param hops - the hops the signature performs, first to last
556
- * @returns the ordered token flow, origin first and destination last
557
- */
558
190
  const deriveTokenFlow = (origin, hops) => hops.reduce((flow, hop) => {
559
191
  const produced = hop.toToken;
560
192
  if (!produced)
@@ -564,24 +196,6 @@ const deriveTokenFlow = (origin, hops) => hops.reduce((flow, hop) => {
564
196
  return flow;
565
197
  return [...flow, produced];
566
198
  }, [origin]);
567
- /**
568
- * Everything a candidate is charged that sits OUTSIDE `fromAmount`, in United
569
- * States dollars.
570
- *
571
- * THIS REPLACES A PER-STEP GAS SUM that could only ever see part of the picture.
572
- * That sum read `step.gasUsd`, so it was ZERO for every Relay quote — Relay
573
- * publishes no per-leg breakdown — and for LI.FI it counted the network gas
574
- * while missing the larger charge: a LayerZero fee of about 1.37 dollars marked
575
- * `included: false`, paid on top in native currency. Reading the quote's own
576
- * normalized fee list catches both, and cannot double-count the per-step gas
577
- * against the aggregate the provider reports.
578
- *
579
- * A charge whose dollar value the provider did not give is skipped rather than
580
- * guessed. It is still named in the breakdown the reader can open.
581
- *
582
- * Exported for the same reason {@link candidatePaidUsd} is — `search/plan-route-data.ts`
583
- * reuses this exact valuation rather than restating it for a plan's last hop.
584
- */
585
199
  export const candidateOutsideFeesUsd = (candidate) => {
586
200
  const fromQuote = candidate.quote.fees.reduce((total, fee) => {
587
201
  if (fee.includedInFromAmount) {
@@ -590,36 +204,12 @@ export const candidateOutsideFeesUsd = (candidate) => {
590
204
  const value = Number(fee.amountUsd);
591
205
  return Number.isFinite(value) ? total + value : total;
592
206
  }, 0);
593
- // A provider that reports its network cost once, for the whole route, gives a
594
- // gas row here and that row is the whole of it. One that reports gas only per
595
- // leg gives no such row, and the per-step figures are then the only record of
596
- // it — counting them as well when the row IS present would bill the same gas
597
- // twice, so this is a fallback and never an addition.
598
207
  const reportsGasOnce = candidate.quote.fees.some((fee) => !fee.includedInFromAmount && fee.kind === 'gas');
599
208
  if (reportsGasOnce) {
600
209
  return fromQuote;
601
210
  }
602
211
  return fromQuote + candidate.quote.steps.reduce((total, step) => total + (step.gasUsd ?? 0), 0);
603
212
  };
604
- /**
605
- * The user's NET dollar outcome for a candidate: the value delivered minus
606
- * everything paid to reach it. Three cost components:
607
- *
608
- * - the origin tokens spent (`quote.fromAmountUsd`);
609
- * - every charge paid on top ({@link candidateOutsideFeesUsd}), NOT part of
610
- * `fromAmount` — the sender's own network gas, and any provider fee the
611
- * quote marked as charged outside the amount;
612
- * - the omnibridge CROSSING fee, already netted into `deliveredAmount`.
613
- *
614
- * The delivered value comes from {@link candidateDeliveredUsd}, the same figure
615
- * the route's own dollar value and cost are built from. This is the one ranking
616
- * rule: the route strip orders by it, so the route the reader sees costing least
617
- * is the route that leads.
618
- *
619
- * @param candidate - the priced route candidate
620
- * @returns the net outcome in United States dollars, or null when the leg or
621
- * the delivered tokens are unpriced
622
- */
623
213
  export const candidateNetValueUsd = (candidate) => {
624
214
  const paid = candidatePaidUsd(candidate);
625
215
  if (paid === null)
@@ -629,41 +219,8 @@ export const candidateNetValueUsd = (candidate) => {
629
219
  return null;
630
220
  return delivered - paid - candidateOutsideFeesUsd(candidate);
631
221
  };
632
- /**
633
- * Builds the RAW, data-only {@link RouteData} for one priced aggregator candidate
634
- * — the framework-agnostic half of the candidate path, carrying NUMBERS, raw
635
- * token references, and raw provider keys ONLY. The display wrapper runs this
636
- * through `toRouteView` to resolve every logo, name, formatted string, and the
637
- * structural id.
638
- *
639
- * The candidate is a single signature (the aggregator swap auto-calls the
640
- * omnibridge), so it produces exactly one {@link RouteSectionData} whose hops are
641
- * the aggregator legs the provider reported (Relay reports none) followed by the
642
- * omnibridge crossing leg. Each hop carries its OWN numeric fee so the user sees
643
- * where cost accrues.
644
- *
645
- * @param candidate - one priced route candidate
646
- * @returns the raw, data-only route for the candidate
647
- */
648
222
  export const buildCandidateRouteData = (candidate) => {
649
223
  const feeUsdValue = candidateFeeUsd(candidate);
650
- // THE PROVIDER'S OWN FIGURE, IN ITS OWN UNIT, CARRIED THROUGH UNROUNDED.
651
- //
652
- // This used to keep only `Math.ceil(seconds / 60)`, and that one expression
653
- // erased the difference the reader most needed to see. Measured live on Base
654
- // to Polygon: LI.FI claimed 29 seconds and Relay claimed 3, and both rendered
655
- // "~1 min". A reader comparing them had no way to tell a route that settles
656
- // before they look away from one that claims ten times the wait — so the
657
- // strip handed the choice to a 0.05% difference in delivered tokens, which is
658
- // nine cents on a hundred-and-twenty-six-dollar transfer.
659
- //
660
- // The section therefore carries the seconds and the view picks the unit (see
661
- // `sectionWaitLabel` in the route strip). `etaMinutes` stays beside it for the
662
- // route-level field and for every consumer that still reads minutes.
663
- //
664
- // It is the provider's CLAIM and nothing more. One live route ran about
665
- // seventy-five times its own estimate, which is why the strip labels this the
666
- // provider's estimate rather than presenting it as a fact.
667
224
  const etaSeconds = candidate.quote.executionDurationSeconds > 0 ? candidate.quote.executionDurationSeconds : null;
668
225
  const etaMinutes = candidate.quote.executionDurationSeconds > 0
669
226
  ? Math.ceil(candidate.quote.executionDurationSeconds / 60)
@@ -674,79 +231,38 @@ export const buildCandidateRouteData = (candidate) => {
674
231
  ` entryChainId=${candidate.entryChainId}` +
675
232
  ` omnibridgeProvider=${candidate.omnibridgeProvider}` +
676
233
  ` quote.fromAmountUsd=${candidate.quote.fromAmountUsd}` +
677
- // BOTH delivered-value sources, because which one applies depends on
678
- // whether a second leg follows — naming only one sends whoever reads
679
- // this to the wrong place. See `candidateDeliveredUsd`.
680
234
  ` quote.toAmountUsd=${candidate.quote.toAmountUsd ?? null}` +
681
235
  ` destinationUsdPrice=${candidate.destinationUsdPrice}`);
682
236
  }
683
237
  const { originToken, destinationToken } = candidate;
684
- // Token logos are pre-resolved here via the image service (which lives in the
685
- // software development kit, so the producer stays framework-agnostic), passing
686
- // the FULL token so its own `logoURI` is preferred over the address-derived
687
- // fallback — matching the pre-split `buildCandidateTokenFlow` exactly.
688
238
  const originNode = {
689
239
  symbol: originToken.symbol,
690
240
  address: originToken.address,
691
241
  chainId: originToken.chainId,
692
242
  decimals: originToken.decimals,
693
- // `tokenImageOrChainFallback` — see the identical note on `destinationNode`
694
- // below; the same "address not resolvable yet" case applies to either end.
695
243
  logoUri: imageLinks.tokenImageOrChainFallback(originToken),
696
- // A pay-by-hand origin (Zcash) carries a local, invented chain id
697
- // `imageLinks.network` cannot look up by number — see `networkOrPseudo`.
698
244
  chainLogoUri: imageLinks.networkOrPseudo(originToken.chainId),
699
245
  };
700
- // Read up front, before the node below, so the node itself never claims
701
- // PulseChain for a candidate that does not enter the omnibridge. A candidate
702
- // whose omnibridge leg is absent (NEAR Intents, or any aggregator delivering
703
- // straight to its own destination chain) lands on `destinationToken.chainId`
704
- // — Base, in the case that pinned this — and a node hardcoded to PulseChain
705
- // named the wrong chain for every stop that reused it, including the
706
- // token-flow fallback below.
707
246
  const omnibridgeProviderKey = candidate.omnibridgeProvider;
708
- // THE CHAIN THIS SIGNATURE ACTUALLY ENDS ON. The label was the constant
709
- // 'Bridge to PulseChain', true of every route that existed when it was
710
- // written. A route the omnibridge does not carry ends somewhere else
711
- // entirely, and titled that way the tracker named a chain the funds never
712
- // touch — over a transfer to Base that had already landed.
713
247
  const landingChainId = omnibridgeProviderKey === null ? destinationToken.chainId : pulsechainChainId;
714
248
  const destinationNode = {
715
249
  symbol: destinationToken.symbol,
716
250
  address: destinationToken.address,
717
251
  chainId: landingChainId,
718
252
  decimals: destinationToken.decimals,
719
- // `tokenImageOrChainFallback`, NOT `image` — a not-yet-bridged token's
720
- // address can still be unresolved when this producer draws the DELIVERED
721
- // node (see the repository's own "Not-Yet-Bridged Tokens" gotcha), and
722
- // `image` either throws on it or silently draws nothing. See that
723
- // helper's own note.
724
253
  logoUri: imageLinks.tokenImageOrChainFallback(destinationToken),
725
- // `networkOrPseudo`, matching `originNode` — a destination is not known to
726
- // ever be a deposit-address pseudo chain today, but the badge helper is the
727
- // one every other node in this builder goes through, and there is no reason
728
- // this one should be the exception that 404s if that ever changes.
729
254
  chainLogoUri: imageLinks.networkOrPseudo(landingChainId),
730
255
  };
731
256
  const aggregatorHops = candidate.quote.steps.map((step, index) => ({
732
257
  providerName: step.label,
733
- // The aggregator step carries its tool's real logo url straight from the quote
734
- // — raw data, not a derived view concern, so it rides on the raw hop.
735
258
  providerLogoUri: step.providerLogoUri,
736
259
  title: step.label,
737
260
  conversion: stepConversion(step),
738
- // The tokens this leg moves between, structurally — the section's token flow is
739
- // derived from these, so every stop the funds make is one the provider named.
740
261
  fromToken: stepFromTokenRef(step),
741
262
  toToken: stepToTokenRef(step),
742
263
  feeUsdValue: step.feeUsd,
743
- // The first hop is the user's single signature; later hops run inside it.
744
264
  automatic: index > 0,
745
265
  }));
746
- // NO BRIDGE LEG MEANS NO BRIDGE HOP. A candidate the omnibridge does not
747
- // carry ends where the aggregator leaves it, so the crossing hop below simply
748
- // does not exist — appending one would draw a step nobody performs and name a
749
- // bridge that is not involved.
750
266
  const omnibridgeHop = omnibridgeProviderKey === null
751
267
  ? null
752
268
  : {
@@ -754,8 +270,6 @@ export const buildCandidateRouteData = (candidate) => {
754
270
  providerKey: omnibridgeProviderKey,
755
271
  title: 'Bridge to PulseChain',
756
272
  conversion: `→ ${candidate.destinationToken.symbol}`,
757
- // The crossing consumes whatever the last aggregator leg produced — the anchor
758
- // token on the entry chain — or the origin token when there were no legs.
759
273
  fromToken: aggregatorHops[aggregatorHops.length - 1]?.toToken ?? originNode,
760
274
  toToken: destinationNode,
761
275
  feeUsdValue: usdValueOfAmount({
@@ -763,38 +277,17 @@ export const buildCandidateRouteData = (candidate) => {
763
277
  decimals: candidate.destinationToken.decimals,
764
278
  price: candidate.destinationUsdPrice,
765
279
  }),
766
- // The omnibridge call is always auto-called by the aggregator leg before it.
767
280
  automatic: aggregatorHops.length > 0,
768
281
  };
769
282
  const hops = omnibridgeHop === null ? aggregatorHops : [...aggregatorHops, omnibridgeHop];
770
- // Origin, then every stop the funds reach on the way — NOT a collapsed pair. The
771
- // last hop is the crossing, so the flow still ends on the destination, EXCEPT
772
- // when the provider itemised no legs at all (Relay and NEAR Intents both report
773
- // zero `quote.steps`) and there is no omnibridge hop to supply one either — a
774
- // candidate delivering straight to its own destination chain. `deriveTokenFlow`
775
- // then has nothing to reduce over and stops at the origin, which is the exact
776
- // defect that showed a NEAR Intents route to Base USD Coin as "At ETH on
777
- // Ethereum": the pill drew the one-and-only flow node it was given, and that
778
- // node had never left the origin. The route's own `destinationToken` is known
779
- // regardless of what got itemised, so it is appended here whenever the flow
780
- // does not already end on it — deduplicated, so an itemised leg that DID reach
781
- // the destination is never drawn twice.
782
283
  const rawTokenFlow = deriveTokenFlow(originNode, hops);
783
284
  const lastStop = rawTokenFlow[rawTokenFlow.length - 1];
784
285
  const tokenFlow = lastStop && isSameTokenRef(lastStop, destinationNode) ? rawTokenFlow : [...rawTokenFlow, destinationNode];
785
286
  const section = {
786
- // The section id stays a stable string key derived from the candidate id —
787
- // section ids are within-route keys and do not need to be structural hashes.
788
287
  id: `${candidate.id}-signature-1`,
789
288
  label: `Bridge to ${chainDisplayName(landingChainId)}`,
790
- // The signature is executed by the cross-chain AGGREGATOR (LI.FI / Relay) that
791
- // carries the bridge leg — that is the provider to surface on this section, not
792
- // the omnibridge headline.
793
289
  providerKey: candidate.quote.provider,
794
290
  tokenFlow,
795
- // What this signature leaves the user holding on PulseChain. Valued on the
796
- // same source basis as the route's output so the step's landing figure and
797
- // the route's delivered figure are the same number, not two derivations of it.
798
291
  landing: {
799
292
  amount: candidate.deliveredAmount,
800
293
  decimals: candidate.destinationToken.decimals,
@@ -803,8 +296,6 @@ export const buildCandidateRouteData = (candidate) => {
803
296
  },
804
297
  unspent: candidateUnspent(candidate),
805
298
  feeUsdValue,
806
- // The itemization behind `feeUsdValue`, plus the charges that difference
807
- // cannot see: anything the provider levies outside `fromAmount`.
808
299
  fees: candidate.quote.fees,
809
300
  etaMinutes,
810
301
  etaSeconds,
@@ -812,26 +303,13 @@ export const buildCandidateRouteData = (candidate) => {
812
303
  };
813
304
  return {
814
305
  kind: 'aggregated',
815
- // The entry chain this aggregator route crosses through (Ethereum or Binance
816
- // Smart Chain), carried explicitly so Bridge.tsx can link the selected route
817
- // back to its quoted entry candidate without string-parsing the id.
818
306
  entryChainId: candidate.entryChainId,
819
- // The route's headline provider is the omnibridge it crosses on — or, when
820
- // it crosses none, the aggregator that carries the whole journey. A route
821
- // has to be named after something that actually performs it.
822
307
  headlineProviderKey: omnibridgeProviderKey ?? candidate.quote.provider,
823
308
  sections: [section],
824
309
  output: {
825
310
  amount: candidate.deliveredAmount,
826
311
  decimals: candidate.destinationToken.decimals,
827
312
  symbol: candidate.destinationToken.symbol,
828
- // Valued on the SOURCE basis — what was paid, less this route's cost — NOT by
829
- // re-pricing the delivered token against its own feed. The two differ whenever
830
- // the destination asset trades away from its origin-chain counterpart, and that
831
- // difference used to land silently in this figure: the page then stated the
832
- // route's cost twice and the two never agreed. Deriving it keeps the fee clamp
833
- // above from opening a gap too. Falls back to the destination feed only when
834
- // the provider gave no `fromAmountUsd`, so an unpriced leg still shows what it can.
835
313
  usdValue: subtractUsdValues(candidatePaidUsd(candidate), feeUsdValue) ??
836
314
  usdValueOfAmount({
837
315
  balance: candidate.deliveredAmount,
@@ -839,36 +317,16 @@ export const buildCandidateRouteData = (candidate) => {
839
317
  price: candidate.destinationUsdPrice,
840
318
  }),
841
319
  },
842
- // The aggregate cost is the single section's spread (this route is one
843
- // signature), so it equals the section fee; null when the leg is unpriced.
844
320
  totalCostUsdValue: feeUsdValue,
845
- // The net dollar outcome this route is ranked on (delivered value minus origin
846
- // tokens paid, network gas, and the omnibridge crossing fee).
847
321
  netValueUsd: candidateNetValueUsd(candidate),
848
322
  etaMinutes,
849
323
  };
850
324
  };
851
- // ---------------------------------------------------------------------------
852
- // Producer: direct route
853
- // ---------------------------------------------------------------------------
854
- /**
855
- * Builds the ALWAYS-PRESENT direct omnibridge route from the current outbound
856
- * settings, so the strip is never empty for a resolvable pathway.
857
- *
858
- * The direct route is a SINGLE SIGNATURE — the user signs the omnibridge call
859
- * once — so it is exactly ONE {@link RouteSectionData}. Returns null only when
860
- * the pathway is NOT resolvable (a missing input or output asset).
861
- *
862
- * @param roots - the resolved settings + price + logo roots (see {@link DirectRouteRoots})
863
- * @returns the single direct {@link RouteData}, or null when unresolvable
864
- */
865
325
  export const buildDirectRouteData = (roots) => {
866
326
  const { bridgeKey, assetIn, assetOut, estimatedAmountOut, assetInUsdPrice, assetOutUsdPrice, assetInLogoUri, assetOutLogoUri, fromChainLogoUri, toChainLogoUri, advanced, } = roots;
867
327
  if (!assetIn || !assetOut)
868
328
  return null;
869
329
  const [provider, fromChain, toChain] = bridgeKey;
870
- // The bridge's own fee plus the delivery fee, valued on the source basis; null
871
- // when either is unknown or the input asset is unpriced.
872
330
  const { feeUsdValue, fees } = crossingCost({
873
331
  bridgeKey,
874
332
  feeRoots: roots,
@@ -878,30 +336,15 @@ export const buildDirectRouteData = (roots) => {
878
336
  });
879
337
  const hop = {
880
338
  providerName: omnibridgeProviderDisplayName(provider),
881
- // The direct route's provider is the omnibridge it crosses on; the adapter
882
- // resolves its logo from this key.
883
339
  providerKey: provider,
884
340
  title: omnibridgeProviderDisplayName(provider),
885
341
  conversion: `${assetIn.symbol} → ${assetOut.symbol}`,
886
342
  feeUsdValue,
887
- // Delivery auto-calls the completion on the PulseChain side, so the user's
888
- // single signature covers the whole leg.
889
343
  automatic: true,
890
344
  };
891
345
  const section = {
892
346
  id: `direct-${provider}-${fromChain}-signature-1`,
893
- // NAMED FROM `toChain`, NOT HARD-CODED. A direct crossing runs both ways —
894
- // a foreign chain into PulseChain, or PulseChain out to a foreign chain —
895
- // and a literal "Bridge to PulseChain" on every direct route named the
896
- // wrong destination on every outbound crossing: the label read "Bridge to
897
- // PulseChain" while the arrival stage it produces (derived correctly, from
898
- // the section's own last token-flow node — see `tracker/trackerView.ts`'s
899
- // `sectionArrivalStage`) correctly read "Arrive on Ethereum" for the exact
900
- // same signature. One route, two destinations stated for it.
901
347
  label: `Bridge to ${chainDisplayName(Number(toChain))}`,
902
- // The direct section carries NO section provider (matching the pre-split
903
- // builder): the route strip falls back to the hop's provider for the badge,
904
- // and the structural id stays identical.
905
348
  providerKey: null,
906
349
  tokenFlow: [
907
350
  {
@@ -921,9 +364,6 @@ export const buildDirectRouteData = (roots) => {
921
364
  chainLogoUri: toChainLogoUri,
922
365
  },
923
366
  ],
924
- // The direct crossing IS the whole route, so what this step lands in is what
925
- // the route delivers — priced on the source basis for the same reason the
926
- // output is (see the output note below).
927
367
  landing: {
928
368
  amount: estimatedAmountOut,
929
369
  decimals: assetOut.decimals,
@@ -935,22 +375,13 @@ export const buildDirectRouteData = (roots) => {
935
375
  }),
936
376
  },
937
377
  feeUsdValue,
938
- // Who takes what: the bridge, then the delivery service.
939
378
  fees,
940
- // The direct crossing waits on the SAME omnibridge settlement as the
941
- // outbound route's own "Bridge to {entry chain}" section
942
- // ({@link buildOutboundRouteData}), which quotes this identical constant for
943
- // the identical wait. Leaving it null here was the one crossing section that
944
- // never got it, so the strip and the tracker both had no duration to show for
945
- // the most common route in the product.
946
379
  etaMinutes: BRIDGE_SETTLEMENT_ETA_MINUTES,
947
380
  hops: [hop],
948
381
  advanced,
949
382
  };
950
383
  return {
951
384
  kind: 'direct',
952
- // The direct omnibridge route IS the crossing — it does not pivot through a
953
- // distinct aggregator entry chain — so `entryChainId` is null.
954
385
  entryChainId: null,
955
386
  headlineProviderKey: provider,
956
387
  sections: [section],
@@ -958,65 +389,18 @@ export const buildDirectRouteData = (roots) => {
958
389
  amount: estimatedAmountOut,
959
390
  decimals: assetOut.decimals,
960
391
  symbol: assetOut.symbol,
961
- // Priced with the SOURCE asset's price, deliberately, even though the amount is
962
- // counted in the destination token's units. A direct crossing hands back the
963
- // same asset on another chain, and the bridged counterpart routinely trades at
964
- // a discount to it — pricing the two sides from their own feeds made a
965
- // zero-fee crossing report a loss (100 USDC in, 100 USDC out, "$100.03" paid
966
- // and "$99.42" received). The delivered COUNT still comes from the destination
967
- // token's decimals; only the basis is shared. The destination's own price
968
- // remains the fallback so a route whose SOURCE side is unpriced still shows a
969
- // value rather than nothing.
970
392
  usdValue: usdValueOfAmount({
971
393
  balance: estimatedAmountOut,
972
394
  decimals: assetOut.decimals,
973
395
  price: assetInUsdPrice ?? assetOutUsdPrice ?? null,
974
396
  }),
975
397
  },
976
- // The direct route is a single signature, so its aggregate cost is that one
977
- // section's fee: the bridge's own fee plus delivery. Null when either is unknown.
978
398
  totalCostUsdValue: feeUsdValue,
979
- // THE SAME BASIS AGGREGATED ROUTES ARE RANKED ON: delivered value less what
980
- // was paid. The delivered amount is what is sent less both fees, and both
981
- // sides are priced on the source token, so the difference is exactly the
982
- // negated cost. It leaves out the sender's own network gas, which this
983
- // route does not know; an aggregated route counts it when its quote does.
984
399
  netValueUsd: feeUsdValue === null ? null : -feeUsdValue,
985
- // The direct route is the only option for now, so it is always the best.
986
400
  isBest: true,
987
401
  deltaPercent: null,
988
402
  };
989
403
  };
990
- /**
991
- * Builds the TWO-SIGNATURE OUTBOUND raw {@link RouteData} from PulseChain, across
992
- * an omnibridge entry chain (Ethereum or Binance Smart Chain), to wherever the
993
- * second signature lands.
994
- *
995
- * The route is exactly TWO {@link RouteSectionData}s:
996
- * - SECTION 1 ("Bridge to {entry chain}") — the omnibridge out.
997
- * - SECTION 2 ("Complete at {final chain}") — DEFERRED. Signed only after section
998
- * 1's crossing settles, and carrying no binding quote until then.
999
- *
1000
- * SECTION 2 HAS TWO SHAPES, and `roots.onward` is what picks between them.
1001
- * Without it the deferred leg is a swap that stays on the entry chain: the
1002
- * existing behaviour, down to its approximate ~0.29% PulseX fee. With it the
1003
- * deferred leg is an onward BRIDGE to `onward.chainId`, carried by the named
1004
- * provider, and it states NO fee — see the fee note in the body for why quoting
1005
- * the swap constant there would be a fabricated number.
1006
- *
1007
- * WHY THE FIRST LEG CANNOT WIDEN THE SAME WAY. The bridged message carries
1008
- * `(token, receiver, value, data)` and no chain id, and a mediator's counterpart
1009
- * is wired in at deploy time, so the omnibridge's destination is structural
1010
- * rather than a parameter. The reach comes from composing a second signature on
1011
- * the far side, which is what this builder draws.
1012
- *
1013
- * Returns null when `assetIn`, `intermediateToken`, or `finalToken` is missing,
1014
- * when the crossing's destination is not one of the two omnibridge entry chains,
1015
- * or when `onward.chainId` disagrees with the final token's own `chainId`.
1016
- *
1017
- * @param roots - the resolved settings + price + logo roots (see {@link OutboundRouteRoots})
1018
- * @returns the two-section raw outbound {@link RouteData}, or null when unresolvable
1019
- */
1020
404
  export const buildOutboundRouteData = (roots) => {
1021
405
  const { bridgeKey, assetIn, intermediateToken, finalToken, estimatedAmountOut, assetInUsdPrice, intermediateUsdPrice, assetInLogoUri, assetOutLogoUri, finalTokenLogoUri, fromChainLogoUri, intermediateChainLogoUri, onward = null, } = roots;
1022
406
  if (!assetIn || !intermediateToken || !finalToken)
@@ -1026,21 +410,10 @@ export const buildOutboundRouteData = (roots) => {
1026
410
  const entryChainName = resolveEntryChainName(entryChainId);
1027
411
  if (entryChainName === null)
1028
412
  return null;
1029
- // A TOKEN DOES NOT CARRY A CHAIN, so the caller states the onward chain and the
1030
- // token states its own, and the two must agree. Reading one from the other is
1031
- // the cross-chain address mistake this codebase has already paid for: an origin
1032
- // address asked for on the destination chain fails silently, and succeeds by
1033
- // coincidence when some unrelated contract answers at the same address.
1034
- // Disagreement here means one of the two is wrong and there is no way to tell
1035
- // which, so the route is not drawn at all.
1036
413
  if (onward !== null && onward.chainId !== finalToken.chainId)
1037
414
  return null;
1038
- // Where the SECOND signature leaves the user. Same as the crossing's landing
1039
- // when the deferred leg is a swap; the onward chain when it is a bridge.
1040
415
  const finalChainId = onward === null ? entryChainId : onward.chainId;
1041
416
  const finalChainName = onward === null ? entryChainName : onward.chainName;
1042
- // SECTION 1 fee: the crossing's bridge fee plus its delivery fee, priced in the
1043
- // source token. Null when either is unknown.
1044
417
  const { feeUsdValue: sectionOneFeeValue, fees: sectionOneFees } = crossingCost({
1045
418
  bridgeKey,
1046
419
  feeRoots: roots,
@@ -1048,16 +421,6 @@ export const buildOutboundRouteData = (roots) => {
1048
421
  arrivingToken: toFeeToken(intermediateToken, entryChainId),
1049
422
  sourceUsdPrice: assetInUsdPrice,
1050
423
  });
1051
- // SECTION 2's swap fee is APPROXIMATE: PulseX takes ~0.29% of the swapped value,
1052
- // applied here to the bridged intermediate amount.
1053
- //
1054
- // AN ONWARD HOP GETS NO FIGURE AT ALL, and that is the honest answer rather
1055
- // than a gap. `PULSEX_SWAP_FEE_BASIS_POINTS` prices a PulseX swap; an
1056
- // aggregator bridge to another chain charges its own spread plus that chain's
1057
- // gas, and neither is 0.29%. Reusing the constant would print a number that
1058
- // looks measured and is not, beside section 1's fee, which really is measured.
1059
- // The aggregator quotes the leg when the user returns to sign it, and until
1060
- // then null is what we know.
1061
424
  const sectionTwoFeeValue = onward !== null
1062
425
  ? null
1063
426
  : usdValueOfAmount({
@@ -1071,8 +434,6 @@ export const buildOutboundRouteData = (roots) => {
1071
434
  title: omnibridgeProviderDisplayName(provider),
1072
435
  conversion: `${assetIn.symbol} → ${intermediateToken.symbol}`,
1073
436
  feeUsdValue: sectionOneFeeValue,
1074
- // Delivery auto-completes on the entry chain, so the single signature covers
1075
- // the whole omnibridge leg.
1076
437
  automatic: true,
1077
438
  };
1078
439
  const sectionOne = {
@@ -1096,9 +457,6 @@ export const buildOutboundRouteData = (roots) => {
1096
457
  chainLogoUri: intermediateChainLogoUri,
1097
458
  },
1098
459
  ],
1099
- // The crossing lands the user in the INTERMEDIATE token on the entry chain —
1100
- // the whole point of naming a step's landing, since this is exactly the stop
1101
- // a two-signature route leaves you sitting at while you wait to sign again.
1102
460
  landing: {
1103
461
  amount: estimatedAmountOut,
1104
462
  decimals: intermediateToken.decimals,
@@ -1110,16 +468,10 @@ export const buildOutboundRouteData = (roots) => {
1110
468
  }),
1111
469
  },
1112
470
  feeUsdValue: sectionOneFeeValue,
1113
- // Who takes what on the crossing: the bridge, then the delivery service.
1114
471
  fees: sectionOneFees,
1115
- // The omnibridge crossing out is the ~20-minute wait the deferred swap sits
1116
- // behind, so the wait is shown HERE, on the crossing leg.
1117
472
  etaMinutes: BRIDGE_SETTLEMENT_ETA_MINUTES,
1118
473
  hops: [bridgeHop],
1119
474
  };
1120
- // SECTION 2 — DEFERRED. Signed only after section 1 settles. A swap on the entry
1121
- // chain (approximate ~0.29% fee), or an onward bridge carried by a named
1122
- // aggregator (no fee stated until it is quoted).
1123
475
  const deferredHop = onward === null
1124
476
  ? {
1125
477
  providerName: 'Swap',
@@ -1129,9 +481,6 @@ export const buildOutboundRouteData = (roots) => {
1129
481
  automatic: false,
1130
482
  }
1131
483
  : {
1132
- // THE SERVICE THAT WILL ACTUALLY CARRY IT. "Swap" names PulseX, and a
1133
- // reader who saw it beside a Base destination would be told the wrong
1134
- // thing about who holds their funds for the second leg.
1135
484
  providerName: onward.providerName,
1136
485
  title: `Bridge to ${onward.chainName}`,
1137
486
  conversion: `${intermediateToken.symbol} → ${finalToken.symbol}`,
@@ -1154,17 +503,11 @@ export const buildOutboundRouteData = (roots) => {
1154
503
  symbol: finalToken.symbol,
1155
504
  address: finalToken.address,
1156
505
  decimals: finalToken.decimals,
1157
- // THE LINE THIS WHOLE CHANGE IS ABOUT. Pinned to `entryChainId`, the
1158
- // deferred leg could only ever be a swap where the crossing landed, and
1159
- // the route strip drew a Base token wearing Ethereum's badge.
1160
506
  chainId: finalChainId,
1161
507
  logoUri: finalTokenLogoUri,
1162
508
  chainLogoUri: onward === null ? intermediateChainLogoUri : onward.chainLogoUri,
1163
509
  },
1164
510
  ],
1165
- // The deferred swap names the token it will land in but NOT an amount: it is
1166
- // quoted only when the user returns, and stating a figure now would present an
1167
- // estimate with the same weight as the crossing's real one.
1168
511
  landing: {
1169
512
  amount: null,
1170
513
  decimals: finalToken.decimals,
@@ -1172,13 +515,6 @@ export const buildOutboundRouteData = (roots) => {
1172
515
  usdValue: null,
1173
516
  },
1174
517
  feeUsdValue: sectionTwoFeeValue,
1175
- // The swap itself is near-instant (~10 seconds); the ~20-minute wait it sits
1176
- // behind is shown on section 1 (the crossing).
1177
- //
1178
- // AN ONWARD HOP STATES NO ESTIMATE, for the same reason it states no fee. Ten
1179
- // seconds is how long a PulseX swap takes. How long an aggregator takes to
1180
- // carry value to another chain depends on the pair and on the route it picks,
1181
- // so the figure would be borrowed from a different operation.
1182
518
  ...(onward === null ? { etaSeconds: PULSEX_SWAP_ETA_SECONDS } : {}),
1183
519
  deferred: true,
1184
520
  deferredNote: onward === null
@@ -1187,71 +523,36 @@ export const buildOutboundRouteData = (roots) => {
1187
523
  hops: [deferredHop],
1188
524
  };
1189
525
  return {
1190
- // 'direct' EVEN WHEN THE SECOND LEG IS AN AGGREGATOR, and this is a decision
1191
- // rather than a leftover. `kind` is what the interface dispatches EXECUTION
1192
- // on: `executesViaConsolidated` sends a 'direct' route through the omnibridge
1193
- // sender and everything else through the aggregator sender. The only
1194
- // signature this route asks for now is section 1 — a plain omnibridge
1195
- // crossing out of PulseChain — so the omnibridge sender is the correct one.
1196
- // Calling it 'aggregated' would hand the button to a sender holding no quote
1197
- // for it, because the onward leg is not quoted until the user returns and
1198
- // starts a fresh route from the entry chain.
1199
526
  kind: 'direct',
1200
527
  entryChainId: null,
1201
- // The route is shown crossing VIA the chain it bridges to. Already correct
1202
- // for an onward hop: PulseChain to Base really does travel via Ethereum.
1203
528
  viaChainId: entryChainId,
1204
529
  headlineProviderKey: provider,
1205
530
  sections: [sectionOne, sectionTwo],
1206
531
  output: {
1207
- // The final amount is quoted on return (deferred leg) — unknown at display time.
1208
532
  amount: null,
1209
533
  decimals: finalToken.decimals,
1210
534
  symbol: finalToken.symbol,
1211
535
  usdValue: null,
1212
536
  },
1213
- // Aggregate cost is section 1's crossing fees plus section 2's APPROXIMATE swap
1214
- // fee (~0.29%). Section 2 drops out when it states no figure (an onward hop
1215
- // is quoted only on return). Section 1 cannot: it is the signature the reader
1216
- // makes now, so without its cost there is no total, only the swap's guess.
1217
537
  totalCostUsdValue: sectionOneFeeValue === null ? null : addUsdValues(sectionOneFeeValue, sectionTwoFeeValue),
1218
538
  isBest: true,
1219
539
  deltaPercent: null,
1220
540
  };
1221
541
  };
1222
- /**
1223
- * Builds a single composable raw {@link RouteSectionData} for a swap on PulseX,
1224
- * the decentralized exchange on PulseChain. A composing route appends this to its
1225
- * `sections` array. Both tokens live on PulseChain (chain id 369), so both
1226
- * token-flow nodes carry the PulseChain chain badge.
1227
- *
1228
- * The section carries raw numeric fields (`feePercent`, `priceImpactPercent`,
1229
- * `feeUsdValue`); the adapter (`toRouteView`) formats them into the display
1230
- * breakdown string.
1231
- *
1232
- * @param roots - the section's identity, tokens, pricing, and logos
1233
- * @returns one raw PulseX swap {@link RouteSectionData}
1234
- */
1235
542
  export const buildPulseXSwapSectionData = (roots) => {
1236
543
  const { id, fromToken, toToken, feeUsdValue = null, priceImpactPercent = null, landedAmount = null, landedUsdValue = null, etaMinutes = null, etaSeconds = null, fromTokenLogoUri, toTokenLogoUri, pulsechainChainLogoUri, deferred = false, advanced = null, } = roots;
1237
544
  const swapHop = {
1238
545
  providerName: 'PulseX',
1239
- // Resolve the PulseX logo via the provider key in the adapter; the raw hop
1240
- // carries only the key, not the url.
1241
546
  providerKey: 'PulseX',
1242
547
  title: `Swap to ${toToken.symbol}`,
1243
548
  conversion: `${fromToken.symbol} → ${toToken.symbol}`,
1244
549
  feeUsdValue,
1245
- // A standalone PulseX swap is the user's own signature, not an automatic hop.
1246
550
  automatic: false,
1247
551
  };
1248
552
  const tokenFlow = [
1249
553
  { symbol: fromToken.symbol, address: fromToken.address, chainId: pulsechainChainId, decimals: fromToken.decimals, logoUri: fromTokenLogoUri, chainLogoUri: pulsechainChainLogoUri },
1250
554
  { symbol: toToken.symbol, address: toToken.address, chainId: pulsechainChainId, decimals: toToken.decimals, logoUri: toTokenLogoUri, chainLogoUri: pulsechainChainLogoUri },
1251
555
  ];
1252
- // The swap section always shows the flat PulseX fee in its breakdown; the price
1253
- // impact clause is added by the adapter only when the impact is present and
1254
- // non-trivial. Round the impact to two decimals and drop it when it is <= 0.
1255
556
  const roundedImpact = priceImpactPercent === null || Number(priceImpactPercent.toFixed(2)) <= 0
1256
557
  ? null
1257
558
  : Number(priceImpactPercent.toFixed(2));
@@ -1260,8 +561,6 @@ export const buildPulseXSwapSectionData = (roots) => {
1260
561
  label: 'Swap on PulseX',
1261
562
  providerKey: 'PulseX',
1262
563
  tokenFlow,
1263
- // The swap is the last step of whatever composes it, so what it lands in is
1264
- // what the user ends up holding.
1265
564
  landing: {
1266
565
  amount: landedAmount,
1267
566
  decimals: toToken.decimals,
@@ -1269,175 +568,62 @@ export const buildPulseXSwapSectionData = (roots) => {
1269
568
  usdValue: landedUsdValue,
1270
569
  },
1271
570
  feeUsdValue,
1272
- // 0.29 — the flat PulseX fee percentage, kept in lockstep with the basis-point
1273
- // constant so the breakdown line tracks the rate the estimate applies.
1274
571
  feePercent: Number(PULSEX_SWAP_FEE_BASIS_POINTS) / 100,
1275
572
  priceImpactPercent: roundedImpact,
1276
573
  etaMinutes,
1277
574
  etaSeconds,
1278
575
  hops: [swapHop],
1279
- // A PulseX swap is normally an immediate signature; deferred only when it sits
1280
- // behind a prior section's crossing.
1281
576
  deferred: deferred ? true : undefined,
1282
- // NO `bridgeFrom`/`bridgeTo` — a same-chain swap crosses no bridge, and
1283
- // leaving them unset (rather than null) keeps the disclosure from naming a
1284
- // mediator that never runs. `destinationRouter` is the PulseX router
1285
- // itself here, not a delivery leg inside a crossing — see the field's own
1286
- // note on `RouteSectionData`.
1287
577
  advanced: advanced === null ? undefined : advanced,
1288
578
  };
1289
579
  };
1290
- /**
1291
- * Composes a raw {@link RouteData} for the conversion route: bridge a liquid anchor
1292
- * token onto PulseChain, then a DEFERRED PulseX swap to the wanted token.
1293
- *
1294
- * Operates entirely at the raw data level — no display formatting. The caller runs
1295
- * the result through `toRouteView` to get the display-ready `RouteOption`.
1296
- *
1297
- * Per the owner's two locked decisions:
1298
- *
1299
- * - **LOCKED DECISION 1 — normalise via the liquid anchor.** When the omnibridge
1300
- * cannot deliver the wanted token directly (the wanted token has not yet been
1301
- * bridged, or its canonical foreign-chain pair is illiquid), the rails bridge a
1302
- * well-known liquid anchor token instead and a PulseX swap leg converts it on
1303
- * PulseChain. The {@link ConversionRouteComposition.baseData} is the raw route
1304
- * that delivers that anchor; this function appends the conversion leg. The two
1305
- * combined sections represent one complete user journey: bridge to the anchor,
1306
- * then swap the anchor to the wanted token.
1307
- *
1308
- * - **LOCKED DECISION 2 — the swap is a deferred second signature.** The PulseX
1309
- * swap can only be signed AFTER the PulseChain Bridge lands the anchor on
1310
- * PulseChain — the funds must arrive before the user can authorise the swap. The
1311
- * appended section therefore carries `deferred: true` and an ESTIMATED fee that
1312
- * firms up on arrival (price impact and pool depth may shift in the ~20 minutes
1313
- * the crossing takes). Do not collapse these into one signature.
1314
- *
1315
- * The composed route:
1316
- * - keeps the base route's provider and entry chain (`entryChainId` / `via`) — it
1317
- * is the same PulseChain Bridge crossing with a swap bolted on at the
1318
- * destination;
1319
- * - takes a DISTINCT identifier (`<baseRouteId>-pulsex-swap`) so React keys and
1320
- * selection never collide with the bare anchor-delivery route;
1321
- * - re-drives the output to the wanted token: the estimate's delivered amount, its
1322
- * United States dollar value, and the wanted token's symbol (the bare anchor
1323
- * route would otherwise claim the anchor as the delivered token);
1324
- * - sums the swap's total cost (fee + price impact) into `totalCostUsdValue` so
1325
- * the aggregate cost is honest — aggregator spread + omnibridge crossing +
1326
- * PulseX swap — and subtracts it from `netValueUsd`.
1327
- *
1328
- * @param composition - the base route data, swap tokens, estimate, logos, and the
1329
- * injected base route id (computed by the caller via `toRouteView`)
1330
- * @returns the composed raw two-section conversion route (bridge, then deferred PulseX swap)
1331
- */
1332
580
  export const buildConversionRouteData = (composition) => {
1333
581
  const { baseData, baseRouteId, anchorToken, wantedToken, estimate, anchorTokenLogoUri, wantedTokenLogoUri, pulsechainChainLogoUri, } = composition;
1334
- // The anchor bridges the two legs: it is BOTH the base route's delivered node and
1335
- // the swap's input node. Reuse the delivered node's pre-resolved logo so the same
1336
- // token shows ONE icon across the boundary; fall back to the threaded anchor logo.
1337
582
  const deliveredAnchorNode = baseData.sections.at(-1)?.tokenFlow.at(-1);
1338
583
  const boundaryAnchorLogoUri = deliveredAnchorNode?.logoUri ?? anchorTokenLogoUri;
1339
584
  const swapSection = buildPulseXSwapSectionData({
1340
585
  id: `${baseRouteId}-pulsex-swap`,
1341
586
  fromToken: anchorToken,
1342
587
  toToken: wantedToken,
1343
- // The section cost is the swap's TOTAL value loss (fee + price impact) so the
1344
- // route reconciles: input value − each step's cost = delivered output.
1345
588
  feeUsdValue: estimate.costUsdValue,
1346
589
  priceImpactPercent: estimate.priceImpactPercent,
1347
- // The swap lands the user in the wanted token. Its value is the bridge leg's
1348
- // landing less what the swap costs — the same subtraction the route's output
1349
- // uses, so the last step's landing figure and the route's delivered figure
1350
- // cannot drift apart.
1351
590
  landedAmount: estimate.outputAmount,
1352
591
  landedUsdValue: subtractUsdValues(baseData.output.usdValue, estimate.costUsdValue),
1353
- // The swap is near-instant (~10s); the ~20-minute wait sits on the crossing below.
1354
592
  etaSeconds: PULSEX_SWAP_ETA_SECONDS,
1355
593
  fromTokenLogoUri: boundaryAnchorLogoUri,
1356
594
  toTokenLogoUri: wantedTokenLogoUri,
1357
595
  pulsechainChainLogoUri,
1358
- // The bridge must land on PulseChain before the swap can be signed.
1359
596
  deferred: true,
1360
597
  });
1361
- // The ~20-minute crossing wait belongs on the section that LANDS THE FUNDS ON
1362
- // PULSECHAIN, replacing whatever entry-chain-only estimate that section carried.
1363
- //
1364
- // THE INDEX IS NOT A PROXY HERE, and the difference is worth stating because
1365
- // this project has been bitten by one. A positional "last section" stood in for
1366
- // "the section Gibs delivers" elsewhere and broke the moment a deferred leg
1367
- // appeared. This is the other case: a base route is BUILT to end on PulseChain —
1368
- // that is what makes it a base a swap can be appended to — so its final section
1369
- // IS the landing section by construction, for a one-signature base and for the
1370
- // two-signature NEAR Intents base alike. The name says which fact is meant, so a
1371
- // future base that ends somewhere else fails a test rather than a reader.
1372
598
  const pulsechainLandingSectionIndex = baseData.sections.length - 1;
1373
599
  const bridgeSections = baseData.sections.map((section, index) => index === pulsechainLandingSectionIndex
1374
600
  ? { ...section, etaMinutes: BRIDGE_SETTLEMENT_ETA_MINUTES, etaSeconds: null }
1375
601
  : section);
1376
602
  return {
1377
603
  ...baseData,
1378
- // The conversion route crosses the aggregator entry chain then swaps on PulseX.
1379
604
  kind: 'conversion',
1380
- // Carry the aggregator entry chain so Bridge.tsx can link back to the candidate.
1381
605
  entryChainId: baseData.entryChainId ?? null,
1382
606
  sections: [...bridgeSections, swapSection],
1383
607
  output: {
1384
608
  amount: estimate.outputAmount,
1385
609
  decimals: wantedToken.decimals,
1386
610
  symbol: wantedToken.symbol,
1387
- // The wanted token's COUNT comes from the swap quote, but its value continues
1388
- // the base route's basis: what the anchor was worth on arrival, less the swap's
1389
- // cost. Re-pricing the wanted token against its own feed — which is what this
1390
- // used to do — sets the figure by the router's realized rate instead, and that
1391
- // rate routinely disagrees with the ratio of the two feeds by about a dollar on
1392
- // a hundred. The route then appeared to deliver more than its own cost rows allowed.
1393
611
  usdValue: subtractUsdValues(baseData.output.usdValue, estimate.costUsdValue),
1394
612
  },
1395
- // Aggregate cost = base cost + the swap's total loss; null only when both unknown.
1396
613
  totalCostUsdValue: addUsdValues(baseData.totalCostUsdValue, estimate.costUsdValue),
1397
- // Net = base net − the swap's total loss; null when either is unknown.
1398
614
  netValueUsd: baseData.netValueUsd == null || estimate.costUsdValue == null
1399
615
  ? null
1400
616
  : baseData.netValueUsd - estimate.costUsdValue,
1401
617
  };
1402
618
  };
1403
- /**
1404
- * Derives a {@link PulseXSwapEstimateData} — the raw NUMERIC form of the PulseX
1405
- * swap estimate — from a PulseX `getAmountsOut` quote.
1406
- *
1407
- * Returns raw numbers rather than formatted currency strings, so the conversion
1408
- * composition can flow them straight into a {@link RouteData} without re-parsing
1409
- * currency strings.
1410
- *
1411
- * Returns null when the quote is absent or empty (the router reverted or has not
1412
- * loaded) — the composing route then has no swap leg to append.
1413
- *
1414
- * @param params.amountsOut - the router's amounts-out array, or null when unquoted
1415
- * @param params.inputAmount - the anchor amount entering the swap, in its base units
1416
- * @param params.inputToken - the anchor token (the swap input)
1417
- * @param params.inputUsdPrice - the anchor token's per-token dollar price, or null
1418
- * @param params.outputToken - the wanted token (the swap output)
1419
- * @param params.outputUsdPrice - the wanted token's per-token dollar price, or null
1420
- * @param params.referenceAmountsOut - marginal reference quote amounts-out, or null
1421
- * @param params.referenceInputAmount - the reference quote's input amount
1422
- * @returns the derived estimate with numeric fields, or null when the quote yields no output amount
1423
- */
1424
619
  export const buildPulseXSwapEstimateData = ({ amountsOut, inputAmount, inputToken, inputUsdPrice, outputToken, outputUsdPrice, referenceAmountsOut = null, referenceInputAmount = 0n, }) => {
1425
620
  if (!amountsOut || amountsOut.length === 0)
1426
621
  return null;
1427
622
  const outputAmount = amountsOut[amountsOut.length - 1];
1428
623
  if (typeof outputAmount !== 'bigint')
1429
624
  return null;
1430
- // The swap fee is APPROXIMATE: PulseX takes a flat ~0.29% of the trade. Apply that
1431
- // rate to the swapped value, preferring the input (anchor) side and falling back
1432
- // to the output (wanted) side, so the fee still shows when only ONE side is priced.
1433
625
  const feeUsdValue = usdValueOfAmount({ balance: (inputAmount * PULSEX_SWAP_FEE_BASIS_POINTS) / BASIS_POINTS_DENOMINATOR, decimals: inputToken.decimals, price: inputUsdPrice }) ??
1434
626
  usdValueOfAmount({ balance: (outputAmount * PULSEX_SWAP_FEE_BASIS_POINTS) / BASIS_POINTS_DENOMINATOR, decimals: outputToken.decimals, price: outputUsdPrice });
1435
- // PRICE IMPACT comes from the MARGINAL reference quote, NOT from differencing the
1436
- // two PulseChain USD prices: those prices are themselves PulseX pool quotes, so
1437
- // value-in minus value-out cancels the fee and impact to ~$0 (the bug this
1438
- // replaces). Instead compare the full trade's realized rate against the tiny
1439
- // reference quote's near-spot rate. The swap fee rides in BOTH `getAmountsOut`
1440
- // results, so it cancels in the ratio and the gap is price impact ALONE.
1441
627
  const marginalRate = (() => {
1442
628
  if (!referenceAmountsOut || referenceAmountsOut.length === 0)
1443
629
  return null;
@@ -1454,18 +640,11 @@ export const buildPulseXSwapEstimateData = ({ amountsOut, inputAmount, inputToke
1454
640
  })();
1455
641
  const inputWhole = Number(formatUnits(inputAmount, inputToken.decimals));
1456
642
  const outputWhole = Number(formatUnits(outputAmount, outputToken.decimals));
1457
- // The output the trade WOULD deliver at the marginal (near-spot) rate with no
1458
- // price impact — fee still applied, since the marginal rate carries it too.
1459
643
  const idealOutputWhole = marginalRate !== null ? inputWhole * marginalRate : null;
1460
- // The impact loss in OUTPUT-token units: what the marginal rate promised minus
1461
- // what the curve actually delivered. The fee is in both, so this is impact only.
1462
644
  const impactLossOutput = idealOutputWhole !== null ? Math.max(0, idealOutputWhole - outputWhole) : null;
1463
645
  const priceImpactPercent = impactLossOutput !== null && idealOutputWhole !== null && idealOutputWhole > 0
1464
646
  ? Math.max(0, (impactLossOutput / idealOutputWhole) * 100)
1465
647
  : null;
1466
- // Value the impact loss in dollars — preferring the output token's price, falling
1467
- // back to the input side via the marginal rate so the impact still shows when
1468
- // only the anchor is priced.
1469
648
  const impactUsd = (() => {
1470
649
  if (impactLossOutput === null)
1471
650
  return null;
@@ -1476,9 +655,6 @@ export const buildPulseXSwapEstimateData = ({ amountsOut, inputAmount, inputToke
1476
655
  }
1477
656
  return null;
1478
657
  })();
1479
- // Total cost is the flat fee PLUS the price impact — both real, neither cancelled.
1480
- // Round the fee to cents the SAME way the pre-split builder did (it summed the
1481
- // already-formatted fee string), so the total cost is identical to the cent.
1482
658
  const feeRounded = feeUsdValue === null ? null : roundUsdToCents(feeUsdValue);
1483
659
  const costUsdValue = feeRounded !== null && Number.isFinite(feeRounded)
1484
660
  ? feeRounded + (impactUsd ?? 0)
@@ -1497,33 +673,6 @@ export const buildPulseXSwapEstimateData = ({ amountsOut, inputAmount, inputToke
1497
673
  etaMinutes: null,
1498
674
  };
1499
675
  };
1500
- /**
1501
- * Composes a raw {@link RouteData} for a SAME-CHAIN PulseX swap: PulseChain on
1502
- * both sides of the selection, one signature, no crossing at all.
1503
- *
1504
- * WHY THIS IS ITS OWN PRODUCER, RATHER THAN `buildConversionRouteData` WITH NO
1505
- * BASE. The conversion route APPENDS a swap section to a bridge route that
1506
- * already delivered the anchor onto PulseChain; its cost, its `entryChainId`,
1507
- * and its "deferred" swap all inherit from that base. A same-chain swap has no
1508
- * base — the reader already holds the input token on PulseChain before
1509
- * pressing anything — so the section is signed IMMEDIATELY (`deferred` stays
1510
- * false, the {@link buildPulseXSwapSectionData} default) and the route has no
1511
- * `entryChainId` at all, exactly like the direct omnibridge route it sits
1512
- * beside in the strip.
1513
- *
1514
- * NOT DEFERRED, AND THAT IS LOAD-BEARING FOR THE TRACKER. `deferred` means "the
1515
- * reader has not yet signed this" (see `sectionProgress.ts`); a same-chain swap
1516
- * is signed in the very press that builds it, so marking it deferred would tell
1517
- * the tracker a signature is still owed for a transaction already broadcast.
1518
- * What DOES need care downstream is the opposite failure the tracker's own
1519
- * `routeChainSequence` can produce for a route whose every stop shares one
1520
- * chain id — see `routeProgress.ts`'s `reachedChainIndex` for the fix that
1521
- * keeps a same-chain route from reading "delivered" the instant it is merely
1522
- * broadcast.
1523
- *
1524
- * @param roots - the route's identity, tokens, pricing, and logos
1525
- * @returns the one-section same-chain swap {@link RouteData}
1526
- */
1527
676
  export const buildSameChainSwapRouteData = (roots) => {
1528
677
  const { id, fromToken, toToken, estimate, inputUsdValue, fromTokenLogoUri, toTokenLogoUri, pulsechainChainLogoUri, advanced, } = roots;
1529
678
  const swapSection = buildPulseXSwapSectionData({
@@ -1538,13 +687,11 @@ export const buildSameChainSwapRouteData = (roots) => {
1538
687
  fromTokenLogoUri,
1539
688
  toTokenLogoUri,
1540
689
  pulsechainChainLogoUri,
1541
- // Signed immediately — see the function doc above for why this must stay false.
1542
690
  deferred: false,
1543
691
  advanced,
1544
692
  });
1545
693
  return {
1546
694
  kind: 'swap',
1547
- // No aggregator entry chain: the whole journey is one signature on PulseChain.
1548
695
  entryChainId: null,
1549
696
  headlineProviderKey: 'PulseX',
1550
697
  sections: [swapSection],
@@ -1558,7 +705,6 @@ export const buildSameChainSwapRouteData = (roots) => {
1558
705
  netValueUsd: inputUsdValue == null || estimate.costUsdValue == null
1559
706
  ? null
1560
707
  : inputUsdValue - estimate.costUsdValue,
1561
- // The swap's own near-instant duration; there is no crossing wait to add.
1562
708
  etaMinutes: null,
1563
709
  };
1564
710
  };