@gibs/bridge-sdk 1.13.1 → 1.17.0

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