@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,38 +1,11 @@
1
1
  import { Chains, Providers, toChain } from '../config.js';
2
2
  import { sameAddress } from '../ecosystems.js';
3
- /**
4
- * Builds the PulseX swap path for a conversion leg, from the bridged anchor to the
5
- * wanted token. Routes through WPLS (`[anchor, WPLS, wanted]`) by default because
6
- * most PulseChain tokens have a deep WPLS pool but rarely a direct pool with the USD
7
- * anchor; it collapses to a direct `[anchor, wanted]` hop only when one side already
8
- * IS WPLS (no point hopping through WPLS to reach WPLS). The router prices the path
9
- * with `getAmountsOut`, so a path with no liquidity simply yields no quote.
10
- *
11
- * A FIXED PATH, NOT THE BEST ONE. The route search's PulseX carrier prices
12
- * this path alongside others through its hub tokens and signs whichever
13
- * delivers most (`carriers/pulsex-paths.ts`). This one remains the
14
- * carrier's asynchronous `ask` path (`pulseXWrappedNativePath`).
15
- *
16
- * @param anchorAddress - the bridged anchor token (the swap input)
17
- * @param wantedAddress - the token the user wants (the swap output)
18
- * @param wplsAddress - the wrapped-native intermediary (WPLS)
19
- * @returns the ordered swap path for `getAmountsOut`
20
- */
21
3
  export const pulseXConversionSwapPath = (anchorAddress, wantedAddress, wplsAddress) => {
22
4
  const isWpls = (address) => sameAddress(address, wplsAddress, 'evm');
23
5
  if (isWpls(anchorAddress) || isWpls(wantedAddress))
24
6
  return [anchorAddress, wantedAddress];
25
7
  return [anchorAddress, wplsAddress, wantedAddress];
26
8
  };
27
- /**
28
- * The bridge provider that crosses each omnibridge entry point to PulseChain.
29
- *
30
- * Typed `Provider | null` rather than `string | null` — this is the root
31
- * source `RouteCandidate.omnibridgeProvider` and `RouteData.headlineProviderKey`
32
- * both derive from, so a widened return type here forced every caller downstream
33
- * to re-widen too, which is what let a wrong-kind key reach `providerDisplayName`
34
- * unnoticed. See `routing/candidate.ts` and `routing/route-data.ts`.
35
- */
36
9
  export const omnibridgeProviderForEntryChain = (entryChainId) => {
37
10
  const entryChain = toChain(entryChainId);
38
11
  if (entryChain === Chains.ETH)
@@ -1,35 +1,6 @@
1
1
  import { assetNodeKey, combineComposedPathBasis } from './graph.js';
2
- /** The {@link ComposedPathBasis} one hop's figure contributes: `'quoted'` for a bound quote, its own basis for a `'probeRate'`. */
3
2
  export const composableHopBasis = (figure) => figure.kind === 'quote' ? 'quoted' : figure.basis;
4
- /**
5
- * Applies one {@link ExchangeRate} to an actual input amount, as an exact
6
- * bigint multiply-then-divide (floor division — the same rounding direction
7
- * a real on-chain quote would apply against the reader). Never routes the
8
- * amount through a float.
9
- *
10
- * @param inputAmount - the actual base-unit amount arriving at the hop
11
- * @param rate - the hop's observed {@link ExchangeRate}
12
- * @returns the estimated base-unit output
13
- */
14
3
  const applyExchangeRate = (inputAmount, rate) => (inputAmount * rate.numerator) / rate.denominator;
15
- /**
16
- * Composes a path's per-hop figures into one end-to-end estimated delivered
17
- * amount, applying design section 3's rule: a later hop's output is its
18
- * probe rate applied to the ACTUAL amount arriving at that hop — never the
19
- * reference amount the probe itself was taken at.
20
- *
21
- * REFUSES RATHER THAN GUESSES in three cases, checked hop by hop so the
22
- * refusal names the exact hop responsible: the path is empty
23
- * ({@link ComposeRefusalReason} `'empty-path'`); a hop does not connect to the
24
- * one before it (`'discontinuous-path'`); two hops disagree about a shared
25
- * asset's decimals (`'decimals-mismatch'`); or a `'probeRate'` figure names
26
- * no real ratio (`'missing-rate'`). See {@link ComposeRefusalReason} for why
27
- * each of these is a refusal and not a best-effort answer.
28
- *
29
- * @param inputAmount - the base-unit amount entering the path's first hop
30
- * @param hops - the path's hops, first to last
31
- * @returns the composed estimate, or the refusal that stopped composition
32
- */
33
4
  export const composeRouteDelivery = (inputAmount, hops) => {
34
5
  if (hops.length === 0) {
35
6
  return { kind: 'refused', reason: 'empty-path', hopIndex: 0 };
@@ -51,15 +22,6 @@ export const composeRouteDelivery = (inputAmount, hops) => {
51
22
  }
52
23
  return { kind: 'estimate', deliveredAmount: actualInput, basis };
53
24
  };
54
- /**
55
- * Checks that hop `hopIndex` connects to the hop before it: the same asset,
56
- * by {@link assetNodeKey}, with the same declared decimals. The first hop has
57
- * nothing before it and always passes.
58
- *
59
- * @param hops - the full path being composed
60
- * @param hopIndex - the hop to check against its predecessor
61
- * @returns the refusal reason, or null when the hop connects cleanly
62
- */
63
25
  const checkContinuity = (hops, hopIndex) => {
64
26
  if (hopIndex === 0)
65
27
  return null;
@@ -71,24 +33,6 @@ const checkContinuity = (hops, hopIndex) => {
71
33
  return 'decimals-mismatch';
72
34
  return null;
73
35
  };
74
- /**
75
- * One hop's output amount for the actual amount arriving at it: the quote's
76
- * own `outputAmount` when the figure is a real quote, or the probe rate
77
- * applied to `actualInput` when it is a `'probeRate'`. Returns null — never a
78
- * guessed number — when a `'probeRate'` figure's rate names no positive
79
- * denominator.
80
- *
81
- * EXPORTED SO `waves.ts` REUSES THIS MATH RATHER THAN RESTATING IT.
82
- * `recomposePath` (`waves.ts`) needs each hop's OWN output amount, not just
83
- * the path's final total {@link composeRouteDelivery} returns, to refresh
84
- * every hop's individual `outcome` — this is the one place that division
85
- * happens, so both call sites agree by construction instead of by two
86
- * separately-maintained copies of `numerator * input / denominator`.
87
- *
88
- * @param hop - the hop to price
89
- * @param actualInput - the actual base-unit amount arriving at this hop
90
- * @returns the hop's output amount, or null when its rate is missing
91
- */
92
36
  export const hopOutputAmount = (hop, actualInput) => {
93
37
  if (hop.figure.kind === 'quote')
94
38
  return hop.figure.outputAmount;
@@ -1,236 +1,30 @@
1
1
  import { ecosystemById } from '../../ecosystems.js';
2
2
  import { providerRegistry } from '../../providers.js';
3
- /**
4
- * The stable, collision-free key for one {@link AssetNode}.
5
- *
6
- * Normalizes address casing through the ecosystem registry
7
- * (`ecosystemById(...).addressCasing`) before keying, so a checksummed and a
8
- * lower-cased reference to the SAME Ethereum address produce the SAME key —
9
- * two representations of one asset must key identically, or the search would
10
- * treat one node as two and silently duplicate work. An ecosystem whose
11
- * addresses are case-SIGNIFICANT (Solana, Tron, The Open Network, Bitcoin,
12
- * Zcash — see `ecosystems.ts`) is left exactly as given, because lower-casing
13
- * a case-significant address can turn it into a different, unrelated one.
14
- *
15
- * Collision-free for any two distinct `(chainId, normalizedAddress)` pairs:
16
- * `chainId` renders as a plain decimal numeral (never containing `:`), so the
17
- * FIRST `:` in the key always marks the same boundary a caller would expect,
18
- * and no two distinct pairs can render the same string.
19
- *
20
- * @param node - the asset to key
21
- * @returns the node's graph key, e.g. `"1:0xa0b8...eb48"`
22
- */
23
3
  export const assetNodeKey = (node) => {
24
4
  const casing = ecosystemById(node.ecosystem).addressCasing;
25
5
  const normalizedAddress = casing === 'insensitive' ? node.address.toLowerCase() : node.address;
26
6
  return `${node.chainId}:${normalizedAddress}`;
27
7
  };
28
- /** Every registered rate-limited provider id (LI.FI, Relay, NEAR Intents), as a lookup set. */
29
8
  const registeredProviderIds = new Set(providerRegistry.map((descriptor) => descriptor.id));
30
- /**
31
- * Whether `carrier` is one of the three rate-limited aggregators
32
- * (`ProviderId`) rather than a FREE local read (the omnibridge crossing or a
33
- * PulseX swap). Free carriers are asked on EVERY wave without counting
34
- * against `waves.ts`'s own request budget or per-provider concurrency cap —
35
- * design section 3: "Bridge and PulseX hops are free local reads ... done
36
- * exactly at any amount."
37
- *
38
- * CANONICAL HOME FOR THIS CHECK — PHASE 3. Moved here from `waves.ts` (which
39
- * re-exports it) because {@link compareRoutePlansByDeliveredAmount}'s own
40
- * tiebreak among unpriced plans needs it too (see that function's doc): a
41
- * free-carrier first hop is guaranteed a real answer, a rate-limited one is
42
- * not, and that distinction belongs on `HopCarrier` itself, not inside the
43
- * one module that happened to need it first.
44
- *
45
- * @param carrier - the carrier to classify
46
- * @returns true for `'lifi'`, `'relay'` or `'near-intents'`
47
- */
48
9
  export const isRateLimitedCarrier = (carrier) => registeredProviderIds.has(carrier);
49
- /**
50
- * The bases a ranking may compare a hop's `deliveredAmount` on — every
51
- * {@link EstimateBasis} except `'unpriced'`. `'unpriced'` is EXCLUDED ON
52
- * PURPOSE: it names a figure with no value behind it (see that member's own
53
- * doc), and a ranking that compared on it would be comparing one path's real
54
- * number against another path's decimal-shifted guess — exactly the defect
55
- * shadow mode found. A caller checking whether a hop or a whole plan may
56
- * enter a ranking reads this set (or calls {@link planIsRankable}), never a
57
- * hand-rolled `!== 'unpriced'` check, so the one rule lives in one place.
58
- *
59
- * THIS IS A SEARCH-ORDER RULE, NOT A REPORTING RULE — PHASE 3, SECOND ROUND.
60
- * `'priced-conversion'` stays in this set on purpose: `enumerate.ts`'s own
61
- * cut and `waves.ts`'s between-wave top-candidate selection both need SOME
62
- * way to compare two not-yet-quoted plans, and a price-feed conversion is a
63
- * far better signal for THAT purpose than no signal at all. It is a
64
- * DIFFERENT question whether a plan may be handed back to a reader as the
65
- * winner while still carrying `'priced-conversion'` on any hop — it may not;
66
- * see {@link isConfirmedBasis} and {@link planIsConfirmed}, the stricter pair
67
- * `waves.ts`'s own final return step applies, for that rule and the live
68
- * shadow-mode finding it closes (1 Zcash reported as 131.6 million Wrapped
69
- * Pulse via `'priced-conversion'`, against a real 33.3 million from the
70
- * pool's own `getAmountsOut` — a thin-pool price-impact gap `'priced-conversion'`
71
- * cannot see, because it never asks the pool at all).
72
- */
73
10
  export const RANKABLE_ESTIMATE_BASES = new Set([
74
11
  'probe-rate',
75
12
  'priced-conversion',
76
13
  'provider-model',
77
14
  ]);
78
- /**
79
- * Whether every hop of `hops` carries a figure a ranking may trust: a live
80
- * quote, or an estimate whose {@link EstimateBasis} is in
81
- * {@link RANKABLE_ESTIMATE_BASES} AND was never refused
82
- * ({@link HopOutcome.refusalReason} is `null`/`undefined`). False the moment
83
- * ANY hop is `'unpriced'` — one ungrounded hop taints the whole path's
84
- * composed figure, because composition multiplies every hop's figure into
85
- * the next (`compose.ts`), so a single meaningless number anywhere in the
86
- * chain makes the FINAL delivered amount meaningless too, however real the
87
- * other hops were.
88
- *
89
- * A REFUSED HOP IS UNRANKABLE EVEN WITH A GOOD BASIS. A hop that carries
90
- * `'priced-conversion'` or `'probe-rate'` still fails this check once a real
91
- * ask for it came back declined — see {@link HopOutcome.refusalReason}'s own
92
- * doc for the shadow-mode finding this closes: a refused hop's leftover
93
- * price-feed estimate has no fees behind it and kept beating real quotes,
94
- * because a provider's own refusal was never checked before the hop's
95
- * ORIGINAL basis was trusted again.
96
- *
97
- * A plan that fails this check is never dropped by this function — it is the
98
- * CALLER's job to decide what "not yet known" means for it (excluded from
99
- * being the ranked winner, but still held back in the list; see
100
- * `waves.ts`'s and `useRouteSearch.ts`'s own comparators, both of which sort
101
- * every unrankable plan below every rankable one rather than comparing raw
102
- * amounts across the two groups).
103
- *
104
- * @param hops - the plan's hops
105
- * @returns true when every hop's figure is trustworthy enough to rank on
106
- */
107
15
  export const planIsRankable = (hops) => hops.every((hop) => hop.outcome.kind === 'quote' ||
108
16
  (RANKABLE_ESTIMATE_BASES.has(hop.outcome.basis) && (hop.outcome.refusalReason ?? null) === null));
109
- /** How much a ranking trusts each {@link ComposedPathBasis}, highest first — private to this module's own combination and rankability logic. */
110
17
  const COMPOSED_PATH_BASIS_RANK = {
111
18
  quoted: 3,
112
19
  'probe-rate': 2,
113
20
  'priced-conversion': 1,
114
- // SAME TIER AS `'priced-conversion'`, ON PURPOSE — both are rankable (see
115
- // `RANKABLE_ESTIMATE_BASES`) but neither ever asked a real provider for
116
- // THIS edge, so neither may report as a confirmed winner
117
- // (`isConfirmedBasis` requires rank 2 or higher, `probe-rate`'s own tier).
118
21
  'provider-model': 1,
119
22
  unpriced: 0,
120
23
  };
121
- /**
122
- * Combines two hops' (or two paths') bases into the basis their COMPOSITION
123
- * deserves: the WORSE of the two, never the better one and never an average.
124
- * A composed figure is a chain — `hop2`'s output is computed FROM `hop1`'s
125
- * output — so if either link is `'unpriced'`, the number at the far end
126
- * carries no value regardless of how solid the other link was. "Worst wins"
127
- * is the only rule that keeps that true; picking the better basis (or the
128
- * LAST hop's own basis, which an earlier draft of this reasoning tried) would
129
- * let one grounded hop launder an ungrounded one earlier in the same chain.
130
- *
131
- * @param a - one basis being combined
132
- * @param b - the other
133
- * @returns whichever of `a`/`b` a ranking trusts less
134
- */
135
24
  export const combineComposedPathBasis = (a, b) => (COMPOSED_PATH_BASIS_RANK[a] <= COMPOSED_PATH_BASIS_RANK[b] ? a : b);
136
- /**
137
- * Whether a ranking may compare on `basis` — true for every
138
- * {@link ComposedPathBasis} except `'unpriced'`. The whole-path counterpart to
139
- * {@link RANKABLE_ESTIMATE_BASES}; see that constant's doc for the rule both
140
- * enforce.
141
- *
142
- * @param basis - the composed path basis to check
143
- * @returns false only for `'unpriced'`
144
- */
145
25
  export const isRankableBasis = (basis) => basis !== 'unpriced';
146
- /**
147
- * Whether a ranking may report `basis` as a WINNER — true only for
148
- * `'quoted'` and `'probe-rate'`, i.e. {@link COMPOSED_PATH_BASIS_RANK} of 2 or
149
- * higher. FALSE for `'priced-conversion'`, even though {@link isRankableBasis}
150
- * accepts it — see that function's own "SEARCH-ORDER RULE, NOT A REPORTING
151
- * RULE" note for why the two disagree on that one basis, on purpose.
152
- *
153
- * THE DEFECT THIS CLOSES. A live shadow comparison found 1 Zcash routed to
154
- * Pulse (Wrapped) through the Binance Smart Chain omnibridge reported 131.6
155
- * million Wrapped Pulse on `'priced-conversion'` — a United States dollar
156
- * price-feed conversion that never asks the destination PulseX pool at all
157
- * — while the pool's OWN `getAmountsOut` answers 33.3 million for the same
158
- * size (86,540 per unit at 1 unit, 13,121 per unit at 1,500 units: an 84.8%
159
- * worse rate at real size). `isRankableBasis` treated the two bases as
160
- * equally trustworthy, so the price-feed number stood as the reported
161
- * winner. `'priced-conversion'` IGNORES PRICE IMPACT BY CONSTRUCTION
162
- * (`buildEstimateHopOutput`, `packages/ui/src/lib/state/useRouteSearch.ts`,
163
- * converts through a flat United States dollar rate on each side, with no
164
- * pool, no depth and no size in the computation) — it is not merely
165
- * unconfirmed, it is blind to the one thing that broke here, so no amount of
166
- * "it is probably close enough" reasoning can rescue it.
167
- *
168
- * WHY `'probe-rate'` IS STILL ACCEPTED HERE. A hop's basis is `'probe-rate'`
169
- * ONLY WHEN A REAL PROVIDER (or a free local read — `getAmountsOut` included)
170
- * ANSWERED THAT EXACT EDGE — see {@link HopOutcome}'s own doc and
171
- * `waves.ts`'s `recomposePath`, whose fallback branch always carries forward
172
- * a hop's ORIGINAL, pre-network basis (`'priced-conversion'`/`'unpriced'`,
173
- * NEVER `'probe-rate'`) rather than inventing one. So a `'probe-rate'` hop is
174
- * always grounded in a real, size-aware answer for SOME amount — the one
175
- * residual imprecision is that `waves.ts`'s early waves may have asked it at
176
- * an amount close to, but not exactly, the plan's final fully-propagated
177
- * figure (see `waves.ts`'s own "confirmation pass" doc for how the final
178
- * return step narrows that gap for a plan's TOP candidates before this
179
- * function is ever consulted). A real answer at a slightly-off size still
180
- * beats a number no provider was ever asked for at any size.
181
- *
182
- * @param basis - the composed path basis to check
183
- * @returns true only for `'quoted'` and `'probe-rate'`
184
- */
185
26
  export const isConfirmedBasis = (basis) => COMPOSED_PATH_BASIS_RANK[basis] >= COMPOSED_PATH_BASIS_RANK['probe-rate'];
186
- /**
187
- * What grounds a WHOLE PLAN's delivered figure, read directly off its hops'
188
- * own `outcome`s — the WORST (least-grounded) {@link ComposedPathBasis} across
189
- * every hop, folded with {@link combineComposedPathBasis} exactly as
190
- * `compose.ts`'s `composeRouteDelivery` folds one while a path is being
191
- * actively composed.
192
- *
193
- * USE THIS ONCE A PLAN'S HOPS ALREADY CARRY THEIR FINAL `outcome`S — a
194
- * finished `enumerate.ts`/`waves.ts` plan — rather than re-deriving a fresh
195
- * `ComposedRouteDelivery` from scratch. `buildRouteScoreInputsFromPlan`
196
- * (`plan-route-data.ts`) is the first caller, feeding `scoreRoutePlan`
197
- * (`score.ts`) the one figure it needs to refuse ever scoring an unpriced
198
- * plan as if its `deliveredUsd` meant anything.
199
- *
200
- * AGREES WITH {@link planIsRankable} BY CONSTRUCTION, NEVER BY COINCIDENCE:
201
- * `isRankableBasis(planComposedBasis(hops)) === planIsRankable(hops)` for
202
- * every plan, because "worst wins" and "true unless any hop is unpriced" are
203
- * the same rule read two ways — see `graph.test.ts`'s own test pinning that
204
- * equivalence so the two can never quietly drift apart.
205
- *
206
- * A REFUSED HOP FOLDS AS `'unpriced'`, WHATEVER ITS OWN `basis` SAYS — the
207
- * same rule {@link planIsRankable} applies, read through
208
- * {@link combineComposedPathBasis}'s "worst wins": a hop a real provider has
209
- * declined is worse-grounded than any {@link EstimateBasis}, refused or not.
210
- * This is WHY {@link isRankableBasis}`(planComposedBasis(hops)) === planIsRankable(hops)`
211
- * still holds for every plan — see this function's own "AGREES WITH..." note
212
- * below.
213
- *
214
- * @param hops - the plan's hops
215
- * @returns the plan's composed basis; `'quoted'` (the fold's identity value)
216
- * for an empty plan
217
- */
218
27
  export const planComposedBasis = (hops) => hops.reduce((basis, hop) => combineComposedPathBasis(basis, hopComposedBasis(hop)), 'quoted');
219
- /**
220
- * The {@link ComposedPathBasis} one hop contributes to
221
- * {@link planComposedBasis}'s fold: `'quoted'` for a live quote, `'unpriced'`
222
- * for a REFUSED estimate regardless of its own `basis` (see
223
- * {@link planComposedBasis}'s own "REFUSED HOP" note), else the estimate's
224
- * own `basis`.
225
- *
226
- * EXPORTED FOR `waves.ts`'s OWN "confirmation pass" — the final step,
227
- * described on {@link isConfirmedBasis}'s own doc, that upgrades a
228
- * TOP-ranked plan's still-estimated free-carrier hops to a real quote before
229
- * the search returns. That pass needs the SAME per-hop fold this function
230
- * already applies (a hop a real provider has since refused is no better
231
- * grounded than one that was never asked) to decide WHICH hops still need
232
- * upgrading, so it reuses this rather than re-deriving the fold by hand.
233
- */
234
28
  export const hopComposedBasis = (hop) => {
235
29
  if (hop.outcome.kind === 'quote')
236
30
  return 'quoted';
@@ -238,90 +32,12 @@ export const hopComposedBasis = (hop) => {
238
32
  return 'unpriced';
239
33
  return hop.outcome.basis;
240
34
  };
241
- /**
242
- * Whether every hop of `hops` carries a CONFIRMED figure — read straight off
243
- * {@link planComposedBasis} through {@link isConfirmedBasis}, so this agrees
244
- * with that function BY CONSTRUCTION, never by coincidence, exactly as
245
- * {@link planIsRankable} agrees with {@link isRankableBasis} (see that pair's
246
- * own "AGREES WITH" note above). STRICTER than {@link planIsRankable}: every
247
- * confirmed plan is rankable, but a rankable plan carrying `'priced-conversion'`
248
- * on any hop is not confirmed — see {@link isConfirmedBasis}'s own doc for why.
249
- *
250
- * USE THIS TO DECIDE WHAT A SEARCH MAY REPORT AS A WINNER. `waves.ts`'s own
251
- * final return step is the one caller that PARTITIONS on this (confirmed
252
- * plans ahead of unconfirmed ones, each group's own search-order preserved)
253
- * — see that module's "confirmation pass" doc. NEVER use this to prune or
254
- * rank DURING search: `planIsRankable` is still the correct predicate for
255
- * `enumerate.ts`'s cut and `waves.ts`'s between-wave top-candidate
256
- * selection, so `'priced-conversion'` keeps doing its job there exactly as
257
- * before — see {@link RANKABLE_ESTIMATE_BASES}'s own "SEARCH-ORDER RULE, NOT
258
- * A REPORTING RULE" note.
259
- *
260
- * @param hops - the plan's hops
261
- * @returns true when every hop's figure is confirmed by a real ask
262
- */
263
35
  export const planIsConfirmed = (hops) => isConfirmedBasis(planComposedBasis(hops));
264
- /**
265
- * Encodes one string as a length-prefixed ("netstring") segment: `"3:abc"`.
266
- *
267
- * WHY THIS AND NOT A DELIMITER. Joining segments with a fixed delimiter
268
- * (`"|"`, `":"`, ...) collides whenever a segment can itself contain that
269
- * delimiter — and a `HopEdge` key is built FROM asset addresses, which this
270
- * module does not control the alphabet of. Prefixing each segment with its
271
- * own length makes the boundary self-describing: reconstructing the segment
272
- * count from the encoded string requires no assumption about what characters
273
- * appear inside a segment, so two different segment sequences can never
274
- * encode to the same string. See `graph.test.ts` for a worked collision that
275
- * a plain delimiter join would produce and this does not.
276
- *
277
- * @param value - the segment to encode
278
- * @returns the length-prefixed segment
279
- */
280
36
  const netstringEncode = (value) => `${value.length}:${value}`;
281
- /**
282
- * The stable, collision-free key for one {@link HopEdge}.
283
- *
284
- * The {@link HopEdge.venue}, when present, is a fourth segment: two venues
285
- * between the same assets are two edges, so they must never share a key (a
286
- * path deduplication or an answer cache would otherwise merge them). An edge
287
- * with no venue keeps its three-segment key unchanged.
288
- *
289
- * @param edge - the edge to key
290
- * @returns the edge's key, built from its carrier, its two node keys, and its venue when it has one
291
- */
292
37
  export const hopEdgeKey = (edge) => [edge.carrier, assetNodeKey(edge.from), assetNodeKey(edge.to), ...(edge.venue === undefined ? [] : [edge.venue])]
293
38
  .map(netstringEncode)
294
39
  .join('');
295
- /**
296
- * The stable, collision-free key for a whole path — the value
297
- * {@link RoutePlan.pathKey} must hold.
298
- *
299
- * Two DIFFERENT hop sequences never produce the same key, and the SAME
300
- * sequence always produces the same key, because each hop's key is
301
- * length-prefixed before joining (see {@link netstringEncode}). This is what
302
- * makes the key usable for deduplicating plans found by different search
303
- * paths that happen to walk the same edges.
304
- *
305
- * @param hops - the plan's hops, first to last
306
- * @returns the path's key
307
- */
308
40
  export const routePlanPathKey = (hops) => hops.map((hop) => netstringEncode(hopEdgeKey(hop.edge))).join('');
309
- /**
310
- * Groups a plan's hops into signature groups, as hop indices.
311
- *
312
- * THE ONE RULE: a hop whose PREDECESSOR fused it (`fusesNext` was true on the
313
- * hop before it) joins that predecessor's group; every other hop — including
314
- * the first — starts a new one. A pay-by-hand hop needs no special case
315
- * either: {@link HopEdge.origin} only says how a hop is FUNDED, not whether it
316
- * groups with what follows, and design section 1 is explicit that a
317
- * pay-by-hand deposit "counts as one reader action but needs no wallet
318
- * signature" — a fact for the ACTION count Phase 1's `score.ts` will compute
319
- * (design section 4, the `A` term), not a fact about signature grouping.
320
- *
321
- * @param hops - the plan's hops, first to last
322
- * @returns the hop index groups, in signature order — the outer array's
323
- * length is the plan's signature count
324
- */
325
41
  export const groupHopsBySignature = (hops) => {
326
42
  const groups = [];
327
43
  hops.forEach((_currentHop, index) => {
@@ -335,57 +51,10 @@ export const groupHopsBySignature = (hops) => {
335
51
  });
336
52
  return groups;
337
53
  };
338
- /**
339
- * Whether `plan`'s FIRST hop is a FREE carrier (the omnibridge crossing or a
340
- * PulseX swap) rather than a rate-limited aggregator ({@link isRateLimitedCarrier}).
341
- * A free first hop is asked on every wave of `waves.ts`'s own search
342
- * unconditionally — never skipped for a spent request budget — so a plan
343
- * with one is guaranteed a real answer; a rate-limited first hop competes
344
- * with every other rate-limited ask for `WAVE_0_MAX_REQUESTS` and can be
345
- * silently skipped when the budget runs out before it. {@link compareUnrankablePlans}
346
- * is the one place this distinguishes two plans neither of which has a
347
- * trustworthy figure yet.
348
- *
349
- * @param plan - the plan to classify
350
- * @returns true when the plan's first hop is free, false for a rate-limited
351
- * first hop or an empty plan
352
- */
353
54
  const firstHopIsFreeCarrier = (plan) => {
354
55
  const firstHop = plan.hops[0];
355
56
  return firstHop !== undefined && !isRateLimitedCarrier(firstHop.edge.carrier);
356
57
  };
357
- /**
358
- * Orders two UNRANKABLE plans (both fail {@link planIsRankable}) — PHASE 3,
359
- * closing the gap `compareRoutePlansByDeliveredAmount` left: within this
360
- * group `deliveredAmount` is not a real signal at all, it is a bare decimal
361
- * rescale with no value behind it (`EstimateBasis`'s own doc), so an earlier
362
- * version of that comparator fell back to comparing it anyway — an EMERGENT
363
- * tiebreak that happened to work only because nothing else was tried. That
364
- * let an entirely unrelated, equally-ungrounded plan win the enumeration
365
- * cut's remaining slots (and `waves.ts`'s wave-0 request-budget priority,
366
- * which reads plans in this same order) purely because its decimal point
367
- * happened to land somewhere bigger — discarding, in the worst case, the
368
- * price-feed-blind but perfectly real DIRECT omnibridge crossing in favour
369
- * of aggregator noise.
370
- *
371
- * THE EXPLICIT RESERVATION, IN RANK ORDER:
372
- *
373
- * 1. A plan whose first hop is a FREE carrier ({@link firstHopIsFreeCarrier})
374
- * sorts ahead of one whose first hop is rate-limited — a free hop is
375
- * GUARANTEED a real wave-0 answer; a rate-limited one is only a maybe,
376
- * gated by a shared, exhaustible budget.
377
- * 2. Within the same free-or-not group, fewer hops sorts first — a shorter
378
- * plan costs less of that shared budget to try and is simpler to execute,
379
- * matching this search's own hop/signature caps (design section 2).
380
- * 3. A final, purely COSMETIC tiebreak — the plan's own stable `pathKey`,
381
- * lexicographically — keeps the comparator total and deterministic.
382
- * Never `deliveredAmount`: see this function's own head note for why.
383
- *
384
- * @param a - one unrankable plan being compared
385
- * @param b - the other
386
- * @returns negative when `a` ranks ahead of `b`, positive when `b` ranks
387
- * ahead of `a`, zero when they tie
388
- */
389
58
  const compareUnrankablePlans = (a, b) => {
390
59
  const freeFirstHopA = firstHopIsFreeCarrier(a);
391
60
  const freeFirstHopB = firstHopIsFreeCarrier(b);
@@ -397,39 +66,6 @@ const compareUnrankablePlans = (a, b) => {
397
66
  return 0;
398
67
  return a.pathKey < b.pathKey ? -1 : 1;
399
68
  };
400
- /**
401
- * THE ONE PLACE THIS COMPARATOR IS WRITTEN. Every rankable plan
402
- * ({@link planIsRankable}) sorts ahead of every unrankable one, whatever its
403
- * raw `deliveredAmount` claims to be — a QUOTABLE plan never loses its slot
404
- * to one that is not, at the enumeration cut or anywhere else this
405
- * comparator runs. Within the same rankability group: two RANKABLE plans
406
- * compare on the larger last-hop delivered amount, a real signal; two
407
- * UNRANKABLE plans compare with {@link compareUnrankablePlans} instead, per
408
- * that function's own doc, because `deliveredAmount` carries no signal for
409
- * that group at all. Bigint-safe throughout for the rankable branch — never
410
- * widens to `Number`, which would lose precision on a large delivered amount
411
- * long before the comparison needed it to.
412
- *
413
- * IMPORTED, NEVER RESTATED — PHASE 3's OWN CLOSE-THE-GAP RULE. Three
414
- * independent call sites need exactly this rule: `enumerate.ts`'s own
415
- * default (its top-N keep, applied to freshly enumerated plans — the SAME
416
- * cut that decides which plans even reach `waves.ts`'s request budget),
417
- * `waves.ts`'s default (its between-wave re-ranking), and
418
- * `packages/ui/src/lib/state/useRouteSearch.ts`'s final sort. Before this
419
- * function existed, two of those three each carried their own hand-written
420
- * copy of the identical rule — a DRY violation that is also a correctness
421
- * risk: a future edit to one copy (say, widening `RANKABLE_ESTIMATE_BASES`)
422
- * could silently fail to reach the other, reopening the exact defect shadow
423
- * mode measured (an unpriced 276 PLS outranking a priced 23 million PLS).
424
- * Importing this one function everywhere a plan-to-plan comparison happens
425
- * is what makes that drift structurally impossible rather than merely
426
- * against convention.
427
- *
428
- * @param a - one plan being compared
429
- * @param b - the other
430
- * @returns negative when `a` ranks ahead of `b`, positive when `b` ranks
431
- * ahead of `a`, zero when they tie
432
- */
433
69
  export const compareRoutePlansByDeliveredAmount = (a, b) => {
434
70
  const rankableA = planIsRankable(a.hops);
435
71
  const rankableB = planIsRankable(b.hops);