@gibs/bridge-sdk 1.13.0 → 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 +19 -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 +69 -21
  162. package/dist/routing/carriers/pulsex.js +184 -65
  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 +7 -3
  168. package/dist/routing/index.js +7 -3
  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 +29 -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 +33 -1
  176. package/dist/routing/search/score.js +41 -6
  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 +234 -0
  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 +818 -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,668 @@
1
+ import { type Ecosystem } from '../../ecosystems.js';
2
+ import { type ProviderId } from '../../providers.js';
3
+ import type { EstimateBasis } from '../route-data.js';
4
+ /**
5
+ * PHASE 0 — FOUNDATION ONLY. This module fixes the pure data model for the
6
+ * general route search described in the route-search design (section 1). It
7
+ * introduces no search, no quoting, and no behaviour change: nothing outside
8
+ * this file constructs one of these types yet. Phase 1 (`capabilities.ts`,
9
+ * `enumerate.ts`) is what starts building {@link AssetNode}s, {@link HopEdge}s
10
+ * and {@link RoutePlan}s from live provider data.
11
+ *
12
+ * WHY A GRAPH AT ALL. Today the bridge special-cases each shape a route can
13
+ * take: a same-chain crossing, an aggregator fused with the bridge, a two-step
14
+ * NEAR Intents route, a conversion swap. Modelling every route as a walk over
15
+ * a small graph of {@link AssetNode}s joined by {@link HopEdge}s replaces the
16
+ * special cases with one search over one structure — see the design's
17
+ * section 6, "what it absorbs."
18
+ */
19
+ /**
20
+ * One stop in the route graph: a specific token, on a specific chain, in a
21
+ * specific ecosystem. Nodes are the graph's vertices; {@link HopEdge}s are its
22
+ * directed edges.
23
+ *
24
+ * KEYED AS `chainId:address`, not by object identity, so two independently
25
+ * built references to "USD Coin on Ethereum" collapse to the same graph
26
+ * vertex. Use {@link assetNodeKey} to compute that key — never format the
27
+ * template by hand, because the key normalizes address casing per ecosystem
28
+ * (see {@link assetNodeKey}) and a hand-formatted string would silently skip
29
+ * that normalization.
30
+ *
31
+ * WHY A COIN AND ITS WRAPPED FORM ARE NOT MERGED HERE. `AssetNode` names one
32
+ * literal address; native ether (the zero address) and wrapped ether (WETH)
33
+ * are and stay two distinct nodes. The two are equivalent ONLY in one
34
+ * specific circumstance — the next hop is an omnibridge entry call — because
35
+ * the router wraps the coin on the way in and mints the SAME PulseChain token
36
+ * either anchor would have minted (`wrapAndRelayTokens`; see
37
+ * `entryChainAnchorsForPair` in `../entry-chain-anchor.js` and its module doc
38
+ * for the measurements behind this). That equivalence is a property of ONE
39
+ * EDGE (an entry call accepting either anchor), not of the node graph as a
40
+ * whole — a USD Coin pair gains no such alias, and neither does a native
41
+ * coin whose next hop is NOT a bridge entry call. So the aliasing is Phase
42
+ * 1's job, applied when `enumerate.ts` builds edges INTO a bridge entry hop,
43
+ * by trying both anchors from `entryChainAnchorsForPair` rather than by
44
+ * collapsing nodes here. Collapsing it into the node graph itself would make
45
+ * every consumer of `AssetNode` reason about an aliasing rule that applies to
46
+ * only one kind of edge.
47
+ */
48
+ export type AssetNode = {
49
+ /** Decimal chain id this asset lives on. */
50
+ readonly chainId: number;
51
+ /**
52
+ * The token's on-chain address, in the ecosystem's own notation — hex for
53
+ * an EVM chain, base58 for Solana or Tron, a user-friendly string for The
54
+ * Open Network, and so on. Treat it as opaque; only {@link assetNodeKey}
55
+ * and code that already knows the ecosystem should normalize it.
56
+ */
57
+ readonly address: string;
58
+ /** Which ecosystem `chainId` belongs to — governs address casing and carrier support. */
59
+ readonly ecosystem: Ecosystem;
60
+ /**
61
+ * The number of decimal places this token's base units carry. A fixed fact
62
+ * about the token, kept on the node so any hop that touches it — most
63
+ * directly `search/compose.ts`, composing a delivered amount across hops —
64
+ * can check two mentions of the SAME node against each other without a
65
+ * second lookup.
66
+ *
67
+ * ADDED IN PHASE 1, FOR `compose.ts`. Composing amounts hop to hop must
68
+ * refuse rather than guess when two hops disagree about a shared token's
69
+ * decimals — two independent provider integrations naming the same token
70
+ * with two different decimal counts is exactly the kind of silent data bug
71
+ * `compose.ts` is required to catch instead of composing through. Carrying
72
+ * `decimals` on the node is what makes that check possible without adding
73
+ * a second, decimals-only lookup structure that could itself drift from
74
+ * this one. Phase 0 shipped {@link AssetNode} without this field because
75
+ * nothing yet constructed one; this is the first module that does.
76
+ */
77
+ readonly decimals: number;
78
+ };
79
+ /**
80
+ * The stable, collision-free key for one {@link AssetNode}.
81
+ *
82
+ * Normalizes address casing through the ecosystem registry
83
+ * (`ecosystemById(...).addressCasing`) before keying, so a checksummed and a
84
+ * lower-cased reference to the SAME Ethereum address produce the SAME key —
85
+ * two representations of one asset must key identically, or the search would
86
+ * treat one node as two and silently duplicate work. An ecosystem whose
87
+ * addresses are case-SIGNIFICANT (Solana, Tron, The Open Network, Bitcoin,
88
+ * Zcash — see `ecosystems.ts`) is left exactly as given, because lower-casing
89
+ * a case-significant address can turn it into a different, unrelated one.
90
+ *
91
+ * Collision-free for any two distinct `(chainId, normalizedAddress)` pairs:
92
+ * `chainId` renders as a plain decimal numeral (never containing `:`), so the
93
+ * FIRST `:` in the key always marks the same boundary a caller would expect,
94
+ * and no two distinct pairs can render the same string.
95
+ *
96
+ * @param node - the asset to key
97
+ * @returns the node's graph key, e.g. `"1:0xa0b8...eb48"`
98
+ */
99
+ export declare const assetNodeKey: (node: AssetNode) => string;
100
+ /**
101
+ * Identifies one carrier that can move funds across a {@link HopEdge}: a
102
+ * plain string, NOT a closed union.
103
+ *
104
+ * FORMERLY `ProviderKey` (`ProviderId | Provider | 'PulseX'`) — a union that
105
+ * baked gibs's own carrier names (`'pulsechain'`, `'tokensex'`, `'PulseX'`)
106
+ * directly into the search engine's data model. The engine (this module,
107
+ * `capabilities.ts`, `enumerate.ts`, `waves.ts` — the latter three now living
108
+ * in `@gibs/quotes`'s `search/` directory) is a plug-in host: it takes
109
+ * whatever carriers it is registered with and names none of them itself. A
110
+ * host names its own carriers by registering objects whose `id` is one of
111
+ * these strings — gibs's own `'pulsechain'`, `'tokensex'` and `'PulseX'`
112
+ * among them, from `routing/carriers/` — and every existing lookup table
113
+ * keyed on a carrier's identity (a logo, a display name) stays keyed on the
114
+ * SAME strings as before; only the TYPE that names them widened from a
115
+ * closed union to `string`.
116
+ */
117
+ export type CarrierId = string;
118
+ /**
119
+ * @deprecated Use {@link CarrierId}. Kept as an alias so existing imports of
120
+ * this name keep compiling; it is no longer the closed `ProviderKey` union.
121
+ */
122
+ export type HopCarrier = CarrierId;
123
+ /**
124
+ * Whether `carrier` is one of the three rate-limited aggregators
125
+ * (`ProviderId`) rather than a FREE local read (the omnibridge crossing or a
126
+ * PulseX swap). Free carriers are asked on EVERY wave without counting
127
+ * against `waves.ts`'s own request budget or per-provider concurrency cap —
128
+ * design section 3: "Bridge and PulseX hops are free local reads ... done
129
+ * exactly at any amount."
130
+ *
131
+ * CANONICAL HOME FOR THIS CHECK — PHASE 3. Moved here from `waves.ts` (which
132
+ * re-exports it) because {@link compareRoutePlansByDeliveredAmount}'s own
133
+ * tiebreak among unpriced plans needs it too (see that function's doc): a
134
+ * free-carrier first hop is guaranteed a real answer, a rate-limited one is
135
+ * not, and that distinction belongs on `HopCarrier` itself, not inside the
136
+ * one module that happened to need it first.
137
+ *
138
+ * @param carrier - the carrier to classify
139
+ * @returns true for `'lifi'`, `'relay'` or `'near-intents'`
140
+ */
141
+ export declare const isRateLimitedCarrier: (carrier: HopCarrier) => carrier is ProviderId;
142
+ /**
143
+ * How a hop's crossing is initiated.
144
+ *
145
+ * `'wallet'` — the reader (or a prior hop's aggregator, on arrival) signs a
146
+ * transaction. `'by-hand'` — the reader sends funds to a deposit address
147
+ * outside any wallet flow this application controls (a Zcash shielded send,
148
+ * for example), which is how a NEAR Intents pay-by-hand origin funds its
149
+ * first hop. See design section 2: "a pay-by-hand origin's first hop must be
150
+ * NEAR."
151
+ */
152
+ export type HopEdgeOrigin = 'wallet' | 'by-hand';
153
+ /**
154
+ * A known minimum amount this edge has already been refused below.
155
+ *
156
+ * EMPTY ON EVERY EDGE PHASE 0 OR PHASE 1's `enumerate.ts` PRODUCES. Floors
157
+ * are learned from a provider's own refusal — NEAR's `belowChainMinimum`, the
158
+ * bridge's `minPerTx`, a Relay or LI.FI minimum from `quote-refusals.ts` —
159
+ * which only happens once a real request has been made. Phase 1's
160
+ * `waves.ts` is what populates this, feeding a floor learned on one search
161
+ * into the pruning step of the next (design section 2: "a known floor must
162
+ * be at or below the amount"; section 3: "refusals feed a floors cache that
163
+ * prunes later searches"). The type is fixed here because `HopEdge` needs a
164
+ * concrete shape for the field, not because anything writes to it yet.
165
+ */
166
+ export type HopFloor = {
167
+ /** Why the floor exists, e.g. `"near-chain-minimum"`, `"bridge-min-per-tx"`. */
168
+ readonly reason: string;
169
+ /** The minimum amount this edge accepts, in the edge's `from` node's base units. */
170
+ readonly minimumAmount: bigint;
171
+ };
172
+ /**
173
+ * One directed edge in the route graph: a single carrier moving funds from
174
+ * one {@link AssetNode} to another.
175
+ */
176
+ export type HopEdge = {
177
+ /** Which carrier performs this hop. */
178
+ readonly carrier: HopCarrier;
179
+ /** The asset this hop consumes. */
180
+ readonly from: AssetNode;
181
+ /** The asset this hop produces. */
182
+ readonly to: AssetNode;
183
+ /**
184
+ * Whether this hop's carrier performs the NEXT hop's entry call itself on
185
+ * arrival, so the two hops together cost the reader only ONE signature.
186
+ *
187
+ * True only when BOTH hold: this carrier's `executesDestinationCalls` is
188
+ * true (see `../../providers.js`), and the next hop is a bridge entry call
189
+ * — `relayTokens` or `wrapAndRelayTokens` — on the chain this hop lands on.
190
+ * Today that means an LI.FI or Relay leg immediately followed by the
191
+ * omnibridge crossing; every other adjacent pair costs a second signature.
192
+ *
193
+ * NEAR IS ALWAYS FALSE. Its `executesDestinationCalls` is false — it
194
+ * delivers to the reader's own wallet and stops (see the head of
195
+ * `../../providers.js`, the `nearIntentsDescriptor` entry) — so a NEAR hop
196
+ * can never satisfy the first condition, whatever follows it. This is WHY
197
+ * every NEAR Intents route today takes two signatures, and it is also why
198
+ * {@link groupHopsBySignature} needs no special case for NEAR: a hop whose
199
+ * `fusesNext` is always false always ends its own signature group on its
200
+ * own, by the same rule that governs every other hop.
201
+ *
202
+ * LEAVING PULSECHAIN IS ALSO ALWAYS FALSE. The omnibridge's outbound leg
203
+ * delivers the anchor token to the reader's own wallet on the entry chain
204
+ * and stops — it has no executor of its own that could carry an aggregator
205
+ * call forward — so a hop that leaves PulseChain can never fuse with
206
+ * whatever aggregator leg follows it either. This is the existing
207
+ * "outbound with onward hop" case (design section 6): the bridge-out leg
208
+ * and the aggregator that continues from it are always two signatures.
209
+ */
210
+ readonly fusesNext: boolean;
211
+ /** How this hop's crossing is initiated. */
212
+ readonly origin: HopEdgeOrigin;
213
+ /** Minimum amounts already learned for this edge; empty until a search records a refusal. */
214
+ readonly floors: readonly HopFloor[];
215
+ /**
216
+ * Which of the carrier's own execution venues performs this hop, when the
217
+ * carrier has more than one between the same two assets — opaque to the
218
+ * graph. Two edges that differ only here are two different hops: each is
219
+ * quoted, ranked and signed on its own venue, never on the other's.
220
+ *
221
+ * ABSENT for a carrier with one venue. The carrier that sets it is the one
222
+ * that reads it back (for example, which router signs a swap), so the
223
+ * route search never interprets the value.
224
+ */
225
+ readonly venue?: string;
226
+ };
227
+ /**
228
+ * The provider-specific request and outcome for one planned hop.
229
+ *
230
+ * `request` is deliberately `unknown` in Phase 0: each carrier's request is a
231
+ * different shape (`ConsolidatedQuoteRequest` for LI.FI/Relay, NEAR's own
232
+ * pre-quote/quote request, a bare fee-rate read for the bridge, a
233
+ * `getAmountsOut` call for PulseX), and Phase 0 introduces no coupling to any
234
+ * one of them. Phase 1's `waves.ts` is what gives this field a real,
235
+ * carrier-discriminated type once the quoting waves exist to fill it.
236
+ */
237
+ export type HopRequest = unknown;
238
+ /**
239
+ * PHASE 3 — what GROUNDS an `'estimate'` {@link HopOutcome} (or a composed
240
+ * path figure, see `compose.ts`'s {@link ComposedPathBasis}): `'probe-rate'`
241
+ * (a real provider answered this edge, at some reference amount),
242
+ * `'priced-conversion'` (both endpoints carry a real United States dollar
243
+ * price and the figure converts value through them), or `'unpriced'` (a bare
244
+ * decimal rescale with no value behind it, `restateInDecimals`, `../amounts.js`).
245
+ *
246
+ * DECLARED IN `../route-data.ts`, IMPORTED HERE, NOT REDECLARED — that module
247
+ * is this search engine's lower, display-facing data layer (`RouteData`,
248
+ * `RouteLandingData.estimateBasis`, `RouteOutputData.estimateBasis`), so the
249
+ * union belongs where ITS consumers can read it without depending upward on
250
+ * `search/`; see that module's own doc for the full rule, including why
251
+ * `'unpriced'` never reaches a `RouteLandingData`/`RouteOutputData` field.
252
+ * Re-exported from here because this whole module (and `compose.ts`,
253
+ * `enumerate.ts`, `waves.ts`) already names it relative to `./graph.js` —
254
+ * one declaration, reachable both ways.
255
+ *
256
+ * THIS IS THE TYPE THE ROUTE-SEARCH DESIGN'S OWN RISK LIST WAS MISSING: a
257
+ * decimal-rescaled bigint and a genuinely priced one are indistinguishable BY
258
+ * SHAPE — both are a positive bigint — so nothing downstream could tell one
259
+ * from the other without this field. Shadow mode against the running search
260
+ * found exactly that gap: on three of five test routes the engine's
261
+ * top-ranked path delivered about 276 PLS where today's routing delivers
262
+ * about 23 million PLS, because a hop with no price-feed entry fell back to
263
+ * a bare decimal rescale and nothing marked that number as untrustworthy
264
+ * before it reached the ranking — see {@link RANKABLE_ESTIMATE_BASES}.
265
+ */
266
+ export type { EstimateBasis };
267
+ /**
268
+ * The bases a ranking may compare a hop's `deliveredAmount` on — every
269
+ * {@link EstimateBasis} except `'unpriced'`. `'unpriced'` is EXCLUDED ON
270
+ * PURPOSE: it names a figure with no value behind it (see that member's own
271
+ * doc), and a ranking that compared on it would be comparing one path's real
272
+ * number against another path's decimal-shifted guess — exactly the defect
273
+ * shadow mode found. A caller checking whether a hop or a whole plan may
274
+ * enter a ranking reads this set (or calls {@link planIsRankable}), never a
275
+ * hand-rolled `!== 'unpriced'` check, so the one rule lives in one place.
276
+ *
277
+ * THIS IS A SEARCH-ORDER RULE, NOT A REPORTING RULE — PHASE 3, SECOND ROUND.
278
+ * `'priced-conversion'` stays in this set on purpose: `enumerate.ts`'s own
279
+ * cut and `waves.ts`'s between-wave top-candidate selection both need SOME
280
+ * way to compare two not-yet-quoted plans, and a price-feed conversion is a
281
+ * far better signal for THAT purpose than no signal at all. It is a
282
+ * DIFFERENT question whether a plan may be handed back to a reader as the
283
+ * winner while still carrying `'priced-conversion'` on any hop — it may not;
284
+ * see {@link isConfirmedBasis} and {@link planIsConfirmed}, the stricter pair
285
+ * `waves.ts`'s own final return step applies, for that rule and the live
286
+ * shadow-mode finding it closes (1 Zcash reported as 131.6 million Wrapped
287
+ * Pulse via `'priced-conversion'`, against a real 33.3 million from the
288
+ * pool's own `getAmountsOut` — a thin-pool price-impact gap `'priced-conversion'`
289
+ * cannot see, because it never asks the pool at all).
290
+ */
291
+ export declare const RANKABLE_ESTIMATE_BASES: ReadonlySet<EstimateBasis>;
292
+ /**
293
+ * What a planned hop is expected to deliver.
294
+ *
295
+ * `'quote'` — a live, provider-confirmed answer (signature 1 must always be
296
+ * one of these; see design section 3, "only signature 1 must be binding").
297
+ * `'estimate'` — a probed or composed figure for a hop that has not been
298
+ * bindingly quoted yet (every later hop, until the reader returns to it),
299
+ * carrying the {@link EstimateBasis} that grounds it — see that type's own
300
+ * doc for why this field exists and what breaks without it.
301
+ */
302
+ export type HopOutcome = {
303
+ readonly kind: 'quote';
304
+ /** What the carrier is bound to deliver, in the hop's `to` node's base units. */
305
+ readonly deliveredAmount: bigint;
306
+ /**
307
+ * The least the carrier promises to deliver after its own slippage
308
+ * tolerance, in the same units — present only when a provider's live
309
+ * answer stated one.
310
+ *
311
+ * WHY A SIGNER NEEDS IT. A provider hop that FUSES with the omnibridge
312
+ * entry (`HopEdge.fusesNext`) is signed as an exact-output request, and
313
+ * the entry call it performs embeds a fixed amount. That amount is this
314
+ * minimum — the owner's two-phase rule: the pre-quote's floor becomes
315
+ * the target, and the unspent remainder is the reader's cushion. So the
316
+ * hop that follows a fused one is composed from this figure, never from
317
+ * {@link deliveredAmount}, and the section's signer reads it back to
318
+ * build exactly the request the route was priced for.
319
+ */
320
+ readonly minimumDeliveredAmount?: bigint;
321
+ /**
322
+ * How far the hop's executed rate falls below its marginal rate, in
323
+ * percent — present only when the carrier measured it from its own
324
+ * pool reserves (a PulseX swap). Absent means "not measured", never zero.
325
+ */
326
+ readonly priceImpactPercent?: number;
327
+ /**
328
+ * The token addresses the carrier's execution passes through, in order,
329
+ * when the carrier chose among several (a PulseX swap path). The figure
330
+ * was priced along exactly this path, and the signer must sign exactly
331
+ * this path — never rebuild one. Opaque to the search, like
332
+ * {@link HopEdge.venue}. Absent when the carrier makes no such choice.
333
+ */
334
+ readonly swapPath?: readonly string[];
335
+ } | {
336
+ readonly kind: 'estimate';
337
+ /** The composed/probed figure, in the hop's `to` node's base units. */
338
+ readonly deliveredAmount: bigint;
339
+ /** What grounds this figure — see {@link EstimateBasis}. */
340
+ readonly basis: EstimateBasis;
341
+ /**
342
+ * Non-null when a REAL ask for this hop came back refused rather than
343
+ * simply never being asked (or still in flight) — `waves.ts`'s
344
+ * `recomposePath` is the one place that sets it, from the cached
345
+ * {@link @gibs/bridge-sdk/routing/search/waves!EdgeRefusal}'s own
346
+ * `reason` (or its `kind` when the provider gave no reason). `null` or
347
+ * `undefined` means "not refused": never asked, still pending, or
348
+ * genuinely answered.
349
+ *
350
+ * WHY THIS EXISTS, SEPARATELY FROM `basis`. `basis` says what GROUNDS
351
+ * `deliveredAmount` — where the number came from. It says nothing about
352
+ * whether a real provider has SINCE said this edge cannot carry the
353
+ * plan's amount at all. A hop can carry a perfectly good
354
+ * `'priced-conversion'` basis (a real United States dollar price on
355
+ * both ends) and still be refused — a shadow comparison against the
356
+ * live search found exactly this: a refused hop's price-feed estimate
357
+ * has no fees behind it and kept outranking real quotes, because
358
+ * nothing marked it untrustworthy once the refusal came back. See
359
+ * {@link planIsRankable}, which is where a non-null reason here takes
360
+ * effect.
361
+ *
362
+ * KEPT FOR DISPLAY, NOT HIDDEN LIKE `'unpriced'`. Reusing `'unpriced'`
363
+ * for a refused hop would also blank its `deliveredAmount` from every
364
+ * display that checks `basis === 'unpriced'` (`plan-route-data.ts`'s
365
+ * `hopOutcomeIsUnpriced`) — losing a real, price-feed-grounded number
366
+ * the reader can still usefully see, just not one bridge-sdk may rank
367
+ * a plan on any more.
368
+ */
369
+ readonly refusalReason?: string | null;
370
+ /**
371
+ * The price impact measured when the carrier answered this hop exactly,
372
+ * carried onto the rescaled estimate when an upstream amount moved and
373
+ * the exact figure no longer applies verbatim. Impact grows roughly in
374
+ * proportion to the trade, so the carried figure stays close for the
375
+ * small moves a live upstream quote makes. Absent means "not measured".
376
+ */
377
+ readonly priceImpactPercent?: number;
378
+ /**
379
+ * The swap path the hop was priced along when the carrier answered it
380
+ * exactly, carried onto the rescaled estimate like
381
+ * {@link priceImpactPercent}. The rescale changes the amount, not the
382
+ * path, so this is still the path to sign.
383
+ */
384
+ readonly swapPath?: readonly string[];
385
+ };
386
+ /**
387
+ * Whether every hop of `hops` carries a figure a ranking may trust: a live
388
+ * quote, or an estimate whose {@link EstimateBasis} is in
389
+ * {@link RANKABLE_ESTIMATE_BASES} AND was never refused
390
+ * ({@link HopOutcome.refusalReason} is `null`/`undefined`). False the moment
391
+ * ANY hop is `'unpriced'` — one ungrounded hop taints the whole path's
392
+ * composed figure, because composition multiplies every hop's figure into
393
+ * the next (`compose.ts`), so a single meaningless number anywhere in the
394
+ * chain makes the FINAL delivered amount meaningless too, however real the
395
+ * other hops were.
396
+ *
397
+ * A REFUSED HOP IS UNRANKABLE EVEN WITH A GOOD BASIS. A hop that carries
398
+ * `'priced-conversion'` or `'probe-rate'` still fails this check once a real
399
+ * ask for it came back declined — see {@link HopOutcome.refusalReason}'s own
400
+ * doc for the shadow-mode finding this closes: a refused hop's leftover
401
+ * price-feed estimate has no fees behind it and kept beating real quotes,
402
+ * because a provider's own refusal was never checked before the hop's
403
+ * ORIGINAL basis was trusted again.
404
+ *
405
+ * A plan that fails this check is never dropped by this function — it is the
406
+ * CALLER's job to decide what "not yet known" means for it (excluded from
407
+ * being the ranked winner, but still held back in the list; see
408
+ * `waves.ts`'s and `useRouteSearch.ts`'s own comparators, both of which sort
409
+ * every unrankable plan below every rankable one rather than comparing raw
410
+ * amounts across the two groups).
411
+ *
412
+ * @param hops - the plan's hops
413
+ * @returns true when every hop's figure is trustworthy enough to rank on
414
+ */
415
+ export declare const planIsRankable: (hops: readonly PlannedHop[]) => boolean;
416
+ /**
417
+ * What grounds a WHOLE COMPOSED PATH's figure — {@link EstimateBasis} widened
418
+ * with `'quoted'`, for the one case a single hop's basis cannot express: every
419
+ * hop in the path was itself a live `'quote'`. `compose.ts`'s
420
+ * {@link @gibs/bridge-sdk/routing/search/compose!ComposedRouteDelivery} carries
421
+ * one of these, computed as the WORST (least-grounded) basis across every hop
422
+ * composed — see {@link combineComposedPathBasis} for why "worst wins" is the
423
+ * only correct combination rule.
424
+ */
425
+ export type ComposedPathBasis = 'quoted' | EstimateBasis;
426
+ /**
427
+ * Combines two hops' (or two paths') bases into the basis their COMPOSITION
428
+ * deserves: the WORSE of the two, never the better one and never an average.
429
+ * A composed figure is a chain — `hop2`'s output is computed FROM `hop1`'s
430
+ * output — so if either link is `'unpriced'`, the number at the far end
431
+ * carries no value regardless of how solid the other link was. "Worst wins"
432
+ * is the only rule that keeps that true; picking the better basis (or the
433
+ * LAST hop's own basis, which an earlier draft of this reasoning tried) would
434
+ * let one grounded hop launder an ungrounded one earlier in the same chain.
435
+ *
436
+ * @param a - one basis being combined
437
+ * @param b - the other
438
+ * @returns whichever of `a`/`b` a ranking trusts less
439
+ */
440
+ export declare const combineComposedPathBasis: (a: ComposedPathBasis, b: ComposedPathBasis) => ComposedPathBasis;
441
+ /**
442
+ * Whether a ranking may compare on `basis` — true for every
443
+ * {@link ComposedPathBasis} except `'unpriced'`. The whole-path counterpart to
444
+ * {@link RANKABLE_ESTIMATE_BASES}; see that constant's doc for the rule both
445
+ * enforce.
446
+ *
447
+ * @param basis - the composed path basis to check
448
+ * @returns false only for `'unpriced'`
449
+ */
450
+ export declare const isRankableBasis: (basis: ComposedPathBasis) => boolean;
451
+ /**
452
+ * Whether a ranking may report `basis` as a WINNER — true only for
453
+ * `'quoted'` and `'probe-rate'`, i.e. {@link COMPOSED_PATH_BASIS_RANK} of 2 or
454
+ * higher. FALSE for `'priced-conversion'`, even though {@link isRankableBasis}
455
+ * accepts it — see that function's own "SEARCH-ORDER RULE, NOT A REPORTING
456
+ * RULE" note for why the two disagree on that one basis, on purpose.
457
+ *
458
+ * THE DEFECT THIS CLOSES. A live shadow comparison found 1 Zcash routed to
459
+ * Pulse (Wrapped) through the Binance Smart Chain omnibridge reported 131.6
460
+ * million Wrapped Pulse on `'priced-conversion'` — a United States dollar
461
+ * price-feed conversion that never asks the destination PulseX pool at all
462
+ * — while the pool's OWN `getAmountsOut` answers 33.3 million for the same
463
+ * size (86,540 per unit at 1 unit, 13,121 per unit at 1,500 units: an 84.8%
464
+ * worse rate at real size). `isRankableBasis` treated the two bases as
465
+ * equally trustworthy, so the price-feed number stood as the reported
466
+ * winner. `'priced-conversion'` IGNORES PRICE IMPACT BY CONSTRUCTION
467
+ * (`buildEstimateHopOutput`, `packages/ui/src/lib/state/useRouteSearch.ts`,
468
+ * converts through a flat United States dollar rate on each side, with no
469
+ * pool, no depth and no size in the computation) — it is not merely
470
+ * unconfirmed, it is blind to the one thing that broke here, so no amount of
471
+ * "it is probably close enough" reasoning can rescue it.
472
+ *
473
+ * WHY `'probe-rate'` IS STILL ACCEPTED HERE. A hop's basis is `'probe-rate'`
474
+ * ONLY WHEN A REAL PROVIDER (or a free local read — `getAmountsOut` included)
475
+ * ANSWERED THAT EXACT EDGE — see {@link HopOutcome}'s own doc and
476
+ * `waves.ts`'s `recomposePath`, whose fallback branch always carries forward
477
+ * a hop's ORIGINAL, pre-network basis (`'priced-conversion'`/`'unpriced'`,
478
+ * NEVER `'probe-rate'`) rather than inventing one. So a `'probe-rate'` hop is
479
+ * always grounded in a real, size-aware answer for SOME amount — the one
480
+ * residual imprecision is that `waves.ts`'s early waves may have asked it at
481
+ * an amount close to, but not exactly, the plan's final fully-propagated
482
+ * figure (see `waves.ts`'s own "confirmation pass" doc for how the final
483
+ * return step narrows that gap for a plan's TOP candidates before this
484
+ * function is ever consulted). A real answer at a slightly-off size still
485
+ * beats a number no provider was ever asked for at any size.
486
+ *
487
+ * @param basis - the composed path basis to check
488
+ * @returns true only for `'quoted'` and `'probe-rate'`
489
+ */
490
+ export declare const isConfirmedBasis: (basis: ComposedPathBasis) => boolean;
491
+ /**
492
+ * What grounds a WHOLE PLAN's delivered figure, read directly off its hops'
493
+ * own `outcome`s — the WORST (least-grounded) {@link ComposedPathBasis} across
494
+ * every hop, folded with {@link combineComposedPathBasis} exactly as
495
+ * `compose.ts`'s `composeRouteDelivery` folds one while a path is being
496
+ * actively composed.
497
+ *
498
+ * USE THIS ONCE A PLAN'S HOPS ALREADY CARRY THEIR FINAL `outcome`S — a
499
+ * finished `enumerate.ts`/`waves.ts` plan — rather than re-deriving a fresh
500
+ * `ComposedRouteDelivery` from scratch. `buildRouteScoreInputsFromPlan`
501
+ * (`plan-route-data.ts`) is the first caller, feeding `scoreRoutePlan`
502
+ * (`score.ts`) the one figure it needs to refuse ever scoring an unpriced
503
+ * plan as if its `deliveredUsd` meant anything.
504
+ *
505
+ * AGREES WITH {@link planIsRankable} BY CONSTRUCTION, NEVER BY COINCIDENCE:
506
+ * `isRankableBasis(planComposedBasis(hops)) === planIsRankable(hops)` for
507
+ * every plan, because "worst wins" and "true unless any hop is unpriced" are
508
+ * the same rule read two ways — see `graph.test.ts`'s own test pinning that
509
+ * equivalence so the two can never quietly drift apart.
510
+ *
511
+ * A REFUSED HOP FOLDS AS `'unpriced'`, WHATEVER ITS OWN `basis` SAYS — the
512
+ * same rule {@link planIsRankable} applies, read through
513
+ * {@link combineComposedPathBasis}'s "worst wins": a hop a real provider has
514
+ * declined is worse-grounded than any {@link EstimateBasis}, refused or not.
515
+ * This is WHY {@link isRankableBasis}`(planComposedBasis(hops)) === planIsRankable(hops)`
516
+ * still holds for every plan — see this function's own "AGREES WITH..." note
517
+ * below.
518
+ *
519
+ * @param hops - the plan's hops
520
+ * @returns the plan's composed basis; `'quoted'` (the fold's identity value)
521
+ * for an empty plan
522
+ */
523
+ export declare const planComposedBasis: (hops: readonly PlannedHop[]) => ComposedPathBasis;
524
+ /**
525
+ * The {@link ComposedPathBasis} one hop contributes to
526
+ * {@link planComposedBasis}'s fold: `'quoted'` for a live quote, `'unpriced'`
527
+ * for a REFUSED estimate regardless of its own `basis` (see
528
+ * {@link planComposedBasis}'s own "REFUSED HOP" note), else the estimate's
529
+ * own `basis`.
530
+ *
531
+ * EXPORTED FOR `waves.ts`'s OWN "confirmation pass" — the final step,
532
+ * described on {@link isConfirmedBasis}'s own doc, that upgrades a
533
+ * TOP-ranked plan's still-estimated free-carrier hops to a real quote before
534
+ * the search returns. That pass needs the SAME per-hop fold this function
535
+ * already applies (a hop a real provider has since refused is no better
536
+ * grounded than one that was never asked) to decide WHICH hops still need
537
+ * upgrading, so it reuses this rather than re-deriving the fold by hand.
538
+ */
539
+ export declare const hopComposedBasis: (hop: PlannedHop) => ComposedPathBasis;
540
+ /**
541
+ * Whether every hop of `hops` carries a CONFIRMED figure — read straight off
542
+ * {@link planComposedBasis} through {@link isConfirmedBasis}, so this agrees
543
+ * with that function BY CONSTRUCTION, never by coincidence, exactly as
544
+ * {@link planIsRankable} agrees with {@link isRankableBasis} (see that pair's
545
+ * own "AGREES WITH" note above). STRICTER than {@link planIsRankable}: every
546
+ * confirmed plan is rankable, but a rankable plan carrying `'priced-conversion'`
547
+ * on any hop is not confirmed — see {@link isConfirmedBasis}'s own doc for why.
548
+ *
549
+ * USE THIS TO DECIDE WHAT A SEARCH MAY REPORT AS A WINNER. `waves.ts`'s own
550
+ * final return step is the one caller that PARTITIONS on this (confirmed
551
+ * plans ahead of unconfirmed ones, each group's own search-order preserved)
552
+ * — see that module's "confirmation pass" doc. NEVER use this to prune or
553
+ * rank DURING search: `planIsRankable` is still the correct predicate for
554
+ * `enumerate.ts`'s cut and `waves.ts`'s between-wave top-candidate
555
+ * selection, so `'priced-conversion'` keeps doing its job there exactly as
556
+ * before — see {@link RANKABLE_ESTIMATE_BASES}'s own "SEARCH-ORDER RULE, NOT
557
+ * A REPORTING RULE" note.
558
+ *
559
+ * @param hops - the plan's hops
560
+ * @returns true when every hop's figure is confirmed by a real ask
561
+ */
562
+ export declare const planIsConfirmed: (hops: readonly PlannedHop[]) => boolean;
563
+ /** One hop of a {@link RoutePlan}: the edge it travels, its request, and its outcome. */
564
+ export type PlannedHop = {
565
+ /** The graph edge this hop travels. */
566
+ readonly edge: HopEdge;
567
+ /** The provider-specific request that would produce (or produced) {@link outcome}. */
568
+ readonly request: HopRequest;
569
+ /** What this hop delivers — a binding quote, or an estimate awaiting one. */
570
+ readonly outcome: HopOutcome;
571
+ };
572
+ /**
573
+ * One complete, priced-or-estimated path through the route graph, from the
574
+ * reader's origin asset to their destination asset.
575
+ */
576
+ export type RoutePlan = {
577
+ /** The plan's hops, first to last. */
578
+ readonly hops: readonly PlannedHop[];
579
+ /**
580
+ * The plan's hops grouped into signatures, as hop INDICES into {@link hops}.
581
+ * Build this with {@link groupHopsBySignature} rather than by hand — see its
582
+ * doc for the one rule it applies.
583
+ */
584
+ readonly signatures: readonly (readonly number[])[];
585
+ /**
586
+ * The plan's stable identity, for deduplication and for `pathKeyFor` in
587
+ * `journeyPlan.ts` to prefer over its current entry-chain-only key (design
588
+ * section 1). Build this with {@link routePlanPathKey}, never by hand.
589
+ */
590
+ readonly pathKey: string;
591
+ };
592
+ /**
593
+ * The stable, collision-free key for one {@link HopEdge}.
594
+ *
595
+ * The {@link HopEdge.venue}, when present, is a fourth segment: two venues
596
+ * between the same assets are two edges, so they must never share a key (a
597
+ * path deduplication or an answer cache would otherwise merge them). An edge
598
+ * with no venue keeps its three-segment key unchanged.
599
+ *
600
+ * @param edge - the edge to key
601
+ * @returns the edge's key, built from its carrier, its two node keys, and its venue when it has one
602
+ */
603
+ export declare const hopEdgeKey: (edge: HopEdge) => string;
604
+ /**
605
+ * The stable, collision-free key for a whole path — the value
606
+ * {@link RoutePlan.pathKey} must hold.
607
+ *
608
+ * Two DIFFERENT hop sequences never produce the same key, and the SAME
609
+ * sequence always produces the same key, because each hop's key is
610
+ * length-prefixed before joining (see {@link netstringEncode}). This is what
611
+ * makes the key usable for deduplicating plans found by different search
612
+ * paths that happen to walk the same edges.
613
+ *
614
+ * @param hops - the plan's hops, first to last
615
+ * @returns the path's key
616
+ */
617
+ export declare const routePlanPathKey: (hops: readonly PlannedHop[]) => string;
618
+ /**
619
+ * Groups a plan's hops into signature groups, as hop indices.
620
+ *
621
+ * THE ONE RULE: a hop whose PREDECESSOR fused it (`fusesNext` was true on the
622
+ * hop before it) joins that predecessor's group; every other hop — including
623
+ * the first — starts a new one. A pay-by-hand hop needs no special case
624
+ * either: {@link HopEdge.origin} only says how a hop is FUNDED, not whether it
625
+ * groups with what follows, and design section 1 is explicit that a
626
+ * pay-by-hand deposit "counts as one reader action but needs no wallet
627
+ * signature" — a fact for the ACTION count Phase 1's `score.ts` will compute
628
+ * (design section 4, the `A` term), not a fact about signature grouping.
629
+ *
630
+ * @param hops - the plan's hops, first to last
631
+ * @returns the hop index groups, in signature order — the outer array's
632
+ * length is the plan's signature count
633
+ */
634
+ export declare const groupHopsBySignature: (hops: readonly PlannedHop[]) => readonly (readonly number[])[];
635
+ /**
636
+ * THE ONE PLACE THIS COMPARATOR IS WRITTEN. Every rankable plan
637
+ * ({@link planIsRankable}) sorts ahead of every unrankable one, whatever its
638
+ * raw `deliveredAmount` claims to be — a QUOTABLE plan never loses its slot
639
+ * to one that is not, at the enumeration cut or anywhere else this
640
+ * comparator runs. Within the same rankability group: two RANKABLE plans
641
+ * compare on the larger last-hop delivered amount, a real signal; two
642
+ * UNRANKABLE plans compare with {@link compareUnrankablePlans} instead, per
643
+ * that function's own doc, because `deliveredAmount` carries no signal for
644
+ * that group at all. Bigint-safe throughout for the rankable branch — never
645
+ * widens to `Number`, which would lose precision on a large delivered amount
646
+ * long before the comparison needed it to.
647
+ *
648
+ * IMPORTED, NEVER RESTATED — PHASE 3's OWN CLOSE-THE-GAP RULE. Three
649
+ * independent call sites need exactly this rule: `enumerate.ts`'s own
650
+ * default (its top-N keep, applied to freshly enumerated plans — the SAME
651
+ * cut that decides which plans even reach `waves.ts`'s request budget),
652
+ * `waves.ts`'s default (its between-wave re-ranking), and
653
+ * `packages/ui/src/lib/state/useRouteSearch.ts`'s final sort. Before this
654
+ * function existed, two of those three each carried their own hand-written
655
+ * copy of the identical rule — a DRY violation that is also a correctness
656
+ * risk: a future edit to one copy (say, widening `RANKABLE_ESTIMATE_BASES`)
657
+ * could silently fail to reach the other, reopening the exact defect shadow
658
+ * mode measured (an unpriced 276 PLS outranking a priced 23 million PLS).
659
+ * Importing this one function everywhere a plan-to-plan comparison happens
660
+ * is what makes that drift structurally impossible rather than merely
661
+ * against convention.
662
+ *
663
+ * @param a - one plan being compared
664
+ * @param b - the other
665
+ * @returns negative when `a` ranks ahead of `b`, positive when `b` ranks
666
+ * ahead of `a`, zero when they tie
667
+ */
668
+ export declare const compareRoutePlansByDeliveredAmount: (a: RoutePlan, b: RoutePlan) => number;