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