@gibs/bridge-sdk 1.12.0 → 1.13.1

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 (118) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +234 -432
  3. package/dist/abis.d.ts +1 -0
  4. package/dist/bridge-limits.d.ts +103 -0
  5. package/dist/bridge-limits.js +84 -0
  6. package/dist/chain-info.d.ts +169 -0
  7. package/dist/chain-info.js +410 -0
  8. package/dist/chain-names.d.ts +160 -0
  9. package/dist/chain-names.js +200 -0
  10. package/dist/chains.d.ts +65 -0
  11. package/dist/chains.js +284 -0
  12. package/dist/config.d.ts +216 -0
  13. package/dist/config.js +493 -0
  14. package/dist/consolidated-quote.d.ts +554 -0
  15. package/dist/consolidated-quote.js +773 -0
  16. package/dist/destination-calldata.d.ts +118 -0
  17. package/dist/destination-calldata.js +152 -0
  18. package/dist/ecosystems.d.ts +217 -0
  19. package/dist/ecosystems.js +288 -0
  20. package/dist/fee-type.d.ts +10 -0
  21. package/dist/fee-type.js +9 -0
  22. package/dist/hyperliquid-execution.d.ts +152 -0
  23. package/dist/hyperliquid-execution.js +168 -0
  24. package/dist/image-links.d.ts +117 -0
  25. package/dist/image-links.js +142 -0
  26. package/dist/index.d.ts +20 -0
  27. package/dist/index.js +20 -0
  28. package/dist/lifi-execution-guard.d.ts +199 -0
  29. package/dist/lifi-execution-guard.js +400 -0
  30. package/dist/near-intents-conformance.d.ts +85 -0
  31. package/dist/near-intents-conformance.js +329 -0
  32. package/dist/near-intents-route.d.ts +47 -0
  33. package/dist/near-intents-route.js +265 -0
  34. package/dist/near-intents.d.ts +524 -0
  35. package/dist/near-intents.js +1055 -0
  36. package/dist/origin-deposit.d.ts +326 -0
  37. package/dist/origin-deposit.js +68 -0
  38. package/dist/pre-quote.d.ts +198 -0
  39. package/dist/pre-quote.js +175 -0
  40. package/dist/prices.d.ts +107 -0
  41. package/dist/prices.js +162 -0
  42. package/dist/providers.d.ts +221 -0
  43. package/dist/providers.js +157 -0
  44. package/dist/quote-fees.d.ts +102 -0
  45. package/dist/quote-fees.js +195 -0
  46. package/dist/referral.d.ts +77 -0
  47. package/dist/referral.js +72 -0
  48. package/dist/relay-currencies.d.ts +47 -0
  49. package/dist/relay-currencies.js +89 -0
  50. package/dist/relay-quote.d.ts +301 -0
  51. package/dist/relay-quote.js +983 -0
  52. package/dist/route-steps.d.ts +138 -0
  53. package/dist/route-steps.js +305 -0
  54. package/dist/routing/amounts.d.ts +80 -0
  55. package/dist/routing/amounts.js +120 -0
  56. package/dist/routing/candidate-route.d.ts +43 -0
  57. package/dist/routing/candidate-route.js +45 -0
  58. package/dist/routing/candidate.d.ts +237 -0
  59. package/dist/routing/candidate.js +96 -0
  60. package/dist/routing/carriers/ask-answer.d.ts +60 -0
  61. package/dist/routing/carriers/ask-answer.js +1 -0
  62. package/dist/routing/carriers/omnibridge.d.ts +137 -0
  63. package/dist/routing/carriers/omnibridge.js +419 -0
  64. package/dist/routing/carriers/pulsex.d.ts +149 -0
  65. package/dist/routing/carriers/pulsex.js +453 -0
  66. package/dist/routing/carriers/wrap.d.ts +25 -0
  67. package/dist/routing/carriers/wrap.js +115 -0
  68. package/dist/routing/composed-route-quality.d.ts +103 -0
  69. package/dist/routing/composed-route-quality.js +108 -0
  70. package/dist/routing/entry-chain-anchor.d.ts +88 -0
  71. package/dist/routing/entry-chain-anchor.js +55 -0
  72. package/dist/routing/format.d.ts +31 -0
  73. package/dist/routing/format.js +58 -0
  74. package/dist/routing/index.d.ts +34 -0
  75. package/dist/routing/index.js +42 -0
  76. package/dist/routing/producers.d.ts +770 -0
  77. package/dist/routing/producers.js +1564 -0
  78. package/dist/routing/providers.d.ts +26 -0
  79. package/dist/routing/providers.js +38 -0
  80. package/dist/routing/route-data.d.ts +355 -0
  81. package/dist/routing/route-data.js +1 -0
  82. package/dist/routing/search/compose.d.ts +168 -0
  83. package/dist/routing/search/compose.js +98 -0
  84. package/dist/routing/search/graph.d.ts +653 -0
  85. package/dist/routing/search/graph.js +445 -0
  86. package/dist/routing/search/plan-route-data.d.ts +244 -0
  87. package/dist/routing/search/plan-route-data.js +767 -0
  88. package/dist/routing/search/score.d.ts +353 -0
  89. package/dist/routing/search/score.js +427 -0
  90. package/dist/routing/usd-value.d.ts +17 -0
  91. package/dist/routing/usd-value.js +25 -0
  92. package/dist/settings.d.ts +692 -0
  93. package/dist/settings.js +909 -0
  94. package/dist/ton-execution.d.ts +117 -0
  95. package/dist/ton-execution.js +247 -0
  96. package/dist/tron-execution.d.ts +146 -0
  97. package/dist/tron-execution.js +239 -0
  98. package/dist/types.d.ts +51 -0
  99. package/dist/types.js +1 -0
  100. package/llms.txt +169 -0
  101. package/package.json +204 -23
  102. package/src/chain-info.test.ts +0 -968
  103. package/src/chain-info.ts +0 -498
  104. package/src/chains.test.ts +0 -123
  105. package/src/chains.ts +0 -88
  106. package/src/config.test.ts +0 -679
  107. package/src/config.ts +0 -500
  108. package/src/fee-type.test.ts +0 -45
  109. package/src/fee-type.ts +0 -14
  110. package/src/image-links.test.ts +0 -160
  111. package/src/image-links.ts +0 -65
  112. package/src/index.ts +0 -8
  113. package/src/settings.test.ts +0 -2754
  114. package/src/settings.ts +0 -875
  115. package/src/types.ts +0 -48
  116. package/tsconfig.json +0 -17
  117. package/vitest.config.ts +0 -13
  118. /package/{src/abis.ts → dist/abis.js} +0 -0
@@ -0,0 +1,653 @@
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
+ readonly kind: 'estimate';
329
+ /** The composed/probed figure, in the hop's `to` node's base units. */
330
+ readonly deliveredAmount: bigint;
331
+ /** What grounds this figure — see {@link EstimateBasis}. */
332
+ readonly basis: EstimateBasis;
333
+ /**
334
+ * Non-null when a REAL ask for this hop came back refused rather than
335
+ * simply never being asked (or still in flight) — `waves.ts`'s
336
+ * `recomposePath` is the one place that sets it, from the cached
337
+ * {@link @gibs/bridge-sdk/routing/search/waves!EdgeRefusal}'s own
338
+ * `reason` (or its `kind` when the provider gave no reason). `null` or
339
+ * `undefined` means "not refused": never asked, still pending, or
340
+ * genuinely answered.
341
+ *
342
+ * WHY THIS EXISTS, SEPARATELY FROM `basis`. `basis` says what GROUNDS
343
+ * `deliveredAmount` — where the number came from. It says nothing about
344
+ * whether a real provider has SINCE said this edge cannot carry the
345
+ * plan's amount at all. A hop can carry a perfectly good
346
+ * `'priced-conversion'` basis (a real United States dollar price on
347
+ * both ends) and still be refused — a shadow comparison against the
348
+ * live search found exactly this: a refused hop's price-feed estimate
349
+ * has no fees behind it and kept outranking real quotes, because
350
+ * nothing marked it untrustworthy once the refusal came back. See
351
+ * {@link planIsRankable}, which is where a non-null reason here takes
352
+ * effect.
353
+ *
354
+ * KEPT FOR DISPLAY, NOT HIDDEN LIKE `'unpriced'`. Reusing `'unpriced'`
355
+ * for a refused hop would also blank its `deliveredAmount` from every
356
+ * display that checks `basis === 'unpriced'` (`plan-route-data.ts`'s
357
+ * `hopOutcomeIsUnpriced`) — losing a real, price-feed-grounded number
358
+ * the reader can still usefully see, just not one bridge-sdk may rank
359
+ * a plan on any more.
360
+ */
361
+ readonly refusalReason?: string | null;
362
+ /**
363
+ * The price impact measured when the carrier answered this hop exactly,
364
+ * carried onto the rescaled estimate when an upstream amount moved and
365
+ * the exact figure no longer applies verbatim. Impact grows roughly in
366
+ * proportion to the trade, so the carried figure stays close for the
367
+ * small moves a live upstream quote makes. Absent means "not measured".
368
+ */
369
+ readonly priceImpactPercent?: number;
370
+ };
371
+ /**
372
+ * Whether every hop of `hops` carries a figure a ranking may trust: a live
373
+ * quote, or an estimate whose {@link EstimateBasis} is in
374
+ * {@link RANKABLE_ESTIMATE_BASES} AND was never refused
375
+ * ({@link HopOutcome.refusalReason} is `null`/`undefined`). False the moment
376
+ * ANY hop is `'unpriced'` — one ungrounded hop taints the whole path's
377
+ * composed figure, because composition multiplies every hop's figure into
378
+ * the next (`compose.ts`), so a single meaningless number anywhere in the
379
+ * chain makes the FINAL delivered amount meaningless too, however real the
380
+ * other hops were.
381
+ *
382
+ * A REFUSED HOP IS UNRANKABLE EVEN WITH A GOOD BASIS. A hop that carries
383
+ * `'priced-conversion'` or `'probe-rate'` still fails this check once a real
384
+ * ask for it came back declined — see {@link HopOutcome.refusalReason}'s own
385
+ * doc for the shadow-mode finding this closes: a refused hop's leftover
386
+ * price-feed estimate has no fees behind it and kept beating real quotes,
387
+ * because a provider's own refusal was never checked before the hop's
388
+ * ORIGINAL basis was trusted again.
389
+ *
390
+ * A plan that fails this check is never dropped by this function — it is the
391
+ * CALLER's job to decide what "not yet known" means for it (excluded from
392
+ * being the ranked winner, but still held back in the list; see
393
+ * `waves.ts`'s and `useRouteSearch.ts`'s own comparators, both of which sort
394
+ * every unrankable plan below every rankable one rather than comparing raw
395
+ * amounts across the two groups).
396
+ *
397
+ * @param hops - the plan's hops
398
+ * @returns true when every hop's figure is trustworthy enough to rank on
399
+ */
400
+ export declare const planIsRankable: (hops: readonly PlannedHop[]) => boolean;
401
+ /**
402
+ * What grounds a WHOLE COMPOSED PATH's figure — {@link EstimateBasis} widened
403
+ * with `'quoted'`, for the one case a single hop's basis cannot express: every
404
+ * hop in the path was itself a live `'quote'`. `compose.ts`'s
405
+ * {@link @gibs/bridge-sdk/routing/search/compose!ComposedRouteDelivery} carries
406
+ * one of these, computed as the WORST (least-grounded) basis across every hop
407
+ * composed — see {@link combineComposedPathBasis} for why "worst wins" is the
408
+ * only correct combination rule.
409
+ */
410
+ export type ComposedPathBasis = 'quoted' | EstimateBasis;
411
+ /**
412
+ * Combines two hops' (or two paths') bases into the basis their COMPOSITION
413
+ * deserves: the WORSE of the two, never the better one and never an average.
414
+ * A composed figure is a chain — `hop2`'s output is computed FROM `hop1`'s
415
+ * output — so if either link is `'unpriced'`, the number at the far end
416
+ * carries no value regardless of how solid the other link was. "Worst wins"
417
+ * is the only rule that keeps that true; picking the better basis (or the
418
+ * LAST hop's own basis, which an earlier draft of this reasoning tried) would
419
+ * let one grounded hop launder an ungrounded one earlier in the same chain.
420
+ *
421
+ * @param a - one basis being combined
422
+ * @param b - the other
423
+ * @returns whichever of `a`/`b` a ranking trusts less
424
+ */
425
+ export declare const combineComposedPathBasis: (a: ComposedPathBasis, b: ComposedPathBasis) => ComposedPathBasis;
426
+ /**
427
+ * Whether a ranking may compare on `basis` — true for every
428
+ * {@link ComposedPathBasis} except `'unpriced'`. The whole-path counterpart to
429
+ * {@link RANKABLE_ESTIMATE_BASES}; see that constant's doc for the rule both
430
+ * enforce.
431
+ *
432
+ * @param basis - the composed path basis to check
433
+ * @returns false only for `'unpriced'`
434
+ */
435
+ export declare const isRankableBasis: (basis: ComposedPathBasis) => boolean;
436
+ /**
437
+ * Whether a ranking may report `basis` as a WINNER — true only for
438
+ * `'quoted'` and `'probe-rate'`, i.e. {@link COMPOSED_PATH_BASIS_RANK} of 2 or
439
+ * higher. FALSE for `'priced-conversion'`, even though {@link isRankableBasis}
440
+ * accepts it — see that function's own "SEARCH-ORDER RULE, NOT A REPORTING
441
+ * RULE" note for why the two disagree on that one basis, on purpose.
442
+ *
443
+ * THE DEFECT THIS CLOSES. A live shadow comparison found 1 Zcash routed to
444
+ * Pulse (Wrapped) through the Binance Smart Chain omnibridge reported 131.6
445
+ * million Wrapped Pulse on `'priced-conversion'` — a United States dollar
446
+ * price-feed conversion that never asks the destination PulseX pool at all
447
+ * — while the pool's OWN `getAmountsOut` answers 33.3 million for the same
448
+ * size (86,540 per unit at 1 unit, 13,121 per unit at 1,500 units: an 84.8%
449
+ * worse rate at real size). `isRankableBasis` treated the two bases as
450
+ * equally trustworthy, so the price-feed number stood as the reported
451
+ * winner. `'priced-conversion'` IGNORES PRICE IMPACT BY CONSTRUCTION
452
+ * (`buildEstimateHopOutput`, `packages/ui/src/lib/state/useRouteSearch.ts`,
453
+ * converts through a flat United States dollar rate on each side, with no
454
+ * pool, no depth and no size in the computation) — it is not merely
455
+ * unconfirmed, it is blind to the one thing that broke here, so no amount of
456
+ * "it is probably close enough" reasoning can rescue it.
457
+ *
458
+ * WHY `'probe-rate'` IS STILL ACCEPTED HERE. A hop's basis is `'probe-rate'`
459
+ * ONLY WHEN A REAL PROVIDER (or a free local read — `getAmountsOut` included)
460
+ * ANSWERED THAT EXACT EDGE — see {@link HopOutcome}'s own doc and
461
+ * `waves.ts`'s `recomposePath`, whose fallback branch always carries forward
462
+ * a hop's ORIGINAL, pre-network basis (`'priced-conversion'`/`'unpriced'`,
463
+ * NEVER `'probe-rate'`) rather than inventing one. So a `'probe-rate'` hop is
464
+ * always grounded in a real, size-aware answer for SOME amount — the one
465
+ * residual imprecision is that `waves.ts`'s early waves may have asked it at
466
+ * an amount close to, but not exactly, the plan's final fully-propagated
467
+ * figure (see `waves.ts`'s own "confirmation pass" doc for how the final
468
+ * return step narrows that gap for a plan's TOP candidates before this
469
+ * function is ever consulted). A real answer at a slightly-off size still
470
+ * beats a number no provider was ever asked for at any size.
471
+ *
472
+ * @param basis - the composed path basis to check
473
+ * @returns true only for `'quoted'` and `'probe-rate'`
474
+ */
475
+ export declare const isConfirmedBasis: (basis: ComposedPathBasis) => boolean;
476
+ /**
477
+ * What grounds a WHOLE PLAN's delivered figure, read directly off its hops'
478
+ * own `outcome`s — the WORST (least-grounded) {@link ComposedPathBasis} across
479
+ * every hop, folded with {@link combineComposedPathBasis} exactly as
480
+ * `compose.ts`'s `composeRouteDelivery` folds one while a path is being
481
+ * actively composed.
482
+ *
483
+ * USE THIS ONCE A PLAN'S HOPS ALREADY CARRY THEIR FINAL `outcome`S — a
484
+ * finished `enumerate.ts`/`waves.ts` plan — rather than re-deriving a fresh
485
+ * `ComposedRouteDelivery` from scratch. `buildRouteScoreInputsFromPlan`
486
+ * (`plan-route-data.ts`) is the first caller, feeding `scoreRoutePlan`
487
+ * (`score.ts`) the one figure it needs to refuse ever scoring an unpriced
488
+ * plan as if its `deliveredUsd` meant anything.
489
+ *
490
+ * AGREES WITH {@link planIsRankable} BY CONSTRUCTION, NEVER BY COINCIDENCE:
491
+ * `isRankableBasis(planComposedBasis(hops)) === planIsRankable(hops)` for
492
+ * every plan, because "worst wins" and "true unless any hop is unpriced" are
493
+ * the same rule read two ways — see `graph.test.ts`'s own test pinning that
494
+ * equivalence so the two can never quietly drift apart.
495
+ *
496
+ * A REFUSED HOP FOLDS AS `'unpriced'`, WHATEVER ITS OWN `basis` SAYS — the
497
+ * same rule {@link planIsRankable} applies, read through
498
+ * {@link combineComposedPathBasis}'s "worst wins": a hop a real provider has
499
+ * declined is worse-grounded than any {@link EstimateBasis}, refused or not.
500
+ * This is WHY {@link isRankableBasis}`(planComposedBasis(hops)) === planIsRankable(hops)`
501
+ * still holds for every plan — see this function's own "AGREES WITH..." note
502
+ * below.
503
+ *
504
+ * @param hops - the plan's hops
505
+ * @returns the plan's composed basis; `'quoted'` (the fold's identity value)
506
+ * for an empty plan
507
+ */
508
+ export declare const planComposedBasis: (hops: readonly PlannedHop[]) => ComposedPathBasis;
509
+ /**
510
+ * The {@link ComposedPathBasis} one hop contributes to
511
+ * {@link planComposedBasis}'s fold: `'quoted'` for a live quote, `'unpriced'`
512
+ * for a REFUSED estimate regardless of its own `basis` (see
513
+ * {@link planComposedBasis}'s own "REFUSED HOP" note), else the estimate's
514
+ * own `basis`.
515
+ *
516
+ * EXPORTED FOR `waves.ts`'s OWN "confirmation pass" — the final step,
517
+ * described on {@link isConfirmedBasis}'s own doc, that upgrades a
518
+ * TOP-ranked plan's still-estimated free-carrier hops to a real quote before
519
+ * the search returns. That pass needs the SAME per-hop fold this function
520
+ * already applies (a hop a real provider has since refused is no better
521
+ * grounded than one that was never asked) to decide WHICH hops still need
522
+ * upgrading, so it reuses this rather than re-deriving the fold by hand.
523
+ */
524
+ export declare const hopComposedBasis: (hop: PlannedHop) => ComposedPathBasis;
525
+ /**
526
+ * Whether every hop of `hops` carries a CONFIRMED figure — read straight off
527
+ * {@link planComposedBasis} through {@link isConfirmedBasis}, so this agrees
528
+ * with that function BY CONSTRUCTION, never by coincidence, exactly as
529
+ * {@link planIsRankable} agrees with {@link isRankableBasis} (see that pair's
530
+ * own "AGREES WITH" note above). STRICTER than {@link planIsRankable}: every
531
+ * confirmed plan is rankable, but a rankable plan carrying `'priced-conversion'`
532
+ * on any hop is not confirmed — see {@link isConfirmedBasis}'s own doc for why.
533
+ *
534
+ * USE THIS TO DECIDE WHAT A SEARCH MAY REPORT AS A WINNER. `waves.ts`'s own
535
+ * final return step is the one caller that PARTITIONS on this (confirmed
536
+ * plans ahead of unconfirmed ones, each group's own search-order preserved)
537
+ * — see that module's "confirmation pass" doc. NEVER use this to prune or
538
+ * rank DURING search: `planIsRankable` is still the correct predicate for
539
+ * `enumerate.ts`'s cut and `waves.ts`'s between-wave top-candidate
540
+ * selection, so `'priced-conversion'` keeps doing its job there exactly as
541
+ * before — see {@link RANKABLE_ESTIMATE_BASES}'s own "SEARCH-ORDER RULE, NOT
542
+ * A REPORTING RULE" note.
543
+ *
544
+ * @param hops - the plan's hops
545
+ * @returns true when every hop's figure is confirmed by a real ask
546
+ */
547
+ export declare const planIsConfirmed: (hops: readonly PlannedHop[]) => boolean;
548
+ /** One hop of a {@link RoutePlan}: the edge it travels, its request, and its outcome. */
549
+ export type PlannedHop = {
550
+ /** The graph edge this hop travels. */
551
+ readonly edge: HopEdge;
552
+ /** The provider-specific request that would produce (or produced) {@link outcome}. */
553
+ readonly request: HopRequest;
554
+ /** What this hop delivers — a binding quote, or an estimate awaiting one. */
555
+ readonly outcome: HopOutcome;
556
+ };
557
+ /**
558
+ * One complete, priced-or-estimated path through the route graph, from the
559
+ * reader's origin asset to their destination asset.
560
+ */
561
+ export type RoutePlan = {
562
+ /** The plan's hops, first to last. */
563
+ readonly hops: readonly PlannedHop[];
564
+ /**
565
+ * The plan's hops grouped into signatures, as hop INDICES into {@link hops}.
566
+ * Build this with {@link groupHopsBySignature} rather than by hand — see its
567
+ * doc for the one rule it applies.
568
+ */
569
+ readonly signatures: readonly (readonly number[])[];
570
+ /**
571
+ * The plan's stable identity, for deduplication and for `pathKeyFor` in
572
+ * `journeyPlan.ts` to prefer over its current entry-chain-only key (design
573
+ * section 1). Build this with {@link routePlanPathKey}, never by hand.
574
+ */
575
+ readonly pathKey: string;
576
+ };
577
+ /**
578
+ * The stable, collision-free key for one {@link HopEdge}.
579
+ *
580
+ * The {@link HopEdge.venue}, when present, is a fourth segment: two venues
581
+ * between the same assets are two edges, so they must never share a key (a
582
+ * path deduplication or an answer cache would otherwise merge them). An edge
583
+ * with no venue keeps its three-segment key unchanged.
584
+ *
585
+ * @param edge - the edge to key
586
+ * @returns the edge's key, built from its carrier, its two node keys, and its venue when it has one
587
+ */
588
+ export declare const hopEdgeKey: (edge: HopEdge) => string;
589
+ /**
590
+ * The stable, collision-free key for a whole path — the value
591
+ * {@link RoutePlan.pathKey} must hold.
592
+ *
593
+ * Two DIFFERENT hop sequences never produce the same key, and the SAME
594
+ * sequence always produces the same key, because each hop's key is
595
+ * length-prefixed before joining (see {@link netstringEncode}). This is what
596
+ * makes the key usable for deduplicating plans found by different search
597
+ * paths that happen to walk the same edges.
598
+ *
599
+ * @param hops - the plan's hops, first to last
600
+ * @returns the path's key
601
+ */
602
+ export declare const routePlanPathKey: (hops: readonly PlannedHop[]) => string;
603
+ /**
604
+ * Groups a plan's hops into signature groups, as hop indices.
605
+ *
606
+ * THE ONE RULE: a hop whose PREDECESSOR fused it (`fusesNext` was true on the
607
+ * hop before it) joins that predecessor's group; every other hop — including
608
+ * the first — starts a new one. A pay-by-hand hop needs no special case
609
+ * either: {@link HopEdge.origin} only says how a hop is FUNDED, not whether it
610
+ * groups with what follows, and design section 1 is explicit that a
611
+ * pay-by-hand deposit "counts as one reader action but needs no wallet
612
+ * signature" — a fact for the ACTION count Phase 1's `score.ts` will compute
613
+ * (design section 4, the `A` term), not a fact about signature grouping.
614
+ *
615
+ * @param hops - the plan's hops, first to last
616
+ * @returns the hop index groups, in signature order — the outer array's
617
+ * length is the plan's signature count
618
+ */
619
+ export declare const groupHopsBySignature: (hops: readonly PlannedHop[]) => readonly (readonly number[])[];
620
+ /**
621
+ * THE ONE PLACE THIS COMPARATOR IS WRITTEN. Every rankable plan
622
+ * ({@link planIsRankable}) sorts ahead of every unrankable one, whatever its
623
+ * raw `deliveredAmount` claims to be — a QUOTABLE plan never loses its slot
624
+ * to one that is not, at the enumeration cut or anywhere else this
625
+ * comparator runs. Within the same rankability group: two RANKABLE plans
626
+ * compare on the larger last-hop delivered amount, a real signal; two
627
+ * UNRANKABLE plans compare with {@link compareUnrankablePlans} instead, per
628
+ * that function's own doc, because `deliveredAmount` carries no signal for
629
+ * that group at all. Bigint-safe throughout for the rankable branch — never
630
+ * widens to `Number`, which would lose precision on a large delivered amount
631
+ * long before the comparison needed it to.
632
+ *
633
+ * IMPORTED, NEVER RESTATED — PHASE 3's OWN CLOSE-THE-GAP RULE. Three
634
+ * independent call sites need exactly this rule: `enumerate.ts`'s own
635
+ * default (its top-N keep, applied to freshly enumerated plans — the SAME
636
+ * cut that decides which plans even reach `waves.ts`'s request budget),
637
+ * `waves.ts`'s default (its between-wave re-ranking), and
638
+ * `packages/ui/src/lib/state/useRouteSearch.ts`'s final sort. Before this
639
+ * function existed, two of those three each carried their own hand-written
640
+ * copy of the identical rule — a DRY violation that is also a correctness
641
+ * risk: a future edit to one copy (say, widening `RANKABLE_ESTIMATE_BASES`)
642
+ * could silently fail to reach the other, reopening the exact defect shadow
643
+ * mode measured (an unpriced 276 PLS outranking a priced 23 million PLS).
644
+ * Importing this one function everywhere a plan-to-plan comparison happens
645
+ * is what makes that drift structurally impossible rather than merely
646
+ * against convention.
647
+ *
648
+ * @param a - one plan being compared
649
+ * @param b - the other
650
+ * @returns negative when `a` ranks ahead of `b`, positive when `b` ranks
651
+ * ahead of `a`, zero when they tie
652
+ */
653
+ export declare const compareRoutePlansByDeliveredAmount: (a: RoutePlan, b: RoutePlan) => number;