@gibs/quotes 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 (44) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +45 -0
  3. package/dist/cache.d.ts +73 -0
  4. package/dist/cache.js +137 -0
  5. package/dist/carriers/calibration.d.ts +115 -0
  6. package/dist/carriers/calibration.js +83 -0
  7. package/dist/carriers/lifi.d.ts +63 -0
  8. package/dist/carriers/lifi.js +217 -0
  9. package/dist/carriers/near-intents.d.ts +91 -0
  10. package/dist/carriers/near-intents.js +249 -0
  11. package/dist/carriers/price-catalogue.d.ts +186 -0
  12. package/dist/carriers/price-catalogue.js +191 -0
  13. package/dist/carriers/relay.d.ts +97 -0
  14. package/dist/carriers/relay.js +242 -0
  15. package/dist/chain-lists.d.ts +144 -0
  16. package/dist/chain-lists.js +169 -0
  17. package/dist/client.d.ts +102 -0
  18. package/dist/client.js +402 -0
  19. package/dist/index.d.ts +19 -0
  20. package/dist/index.js +18 -0
  21. package/dist/quote-guards.d.ts +128 -0
  22. package/dist/quote-guards.js +240 -0
  23. package/dist/quote-refusals.d.ts +180 -0
  24. package/dist/quote-refusals.js +284 -0
  25. package/dist/quote-service-contract.d.ts +2240 -0
  26. package/dist/quote-service-contract.js +247 -0
  27. package/dist/search/capabilities.d.ts +68 -0
  28. package/dist/search/capabilities.js +28 -0
  29. package/dist/search/enumerate.d.ts +162 -0
  30. package/dist/search/enumerate.js +313 -0
  31. package/dist/search/index.d.ts +17 -0
  32. package/dist/search/index.js +17 -0
  33. package/dist/search/serve.d.ts +178 -0
  34. package/dist/search/serve.js +142 -0
  35. package/dist/search/waves.d.ts +481 -0
  36. package/dist/search/waves.js +939 -0
  37. package/dist/single-flight.d.ts +90 -0
  38. package/dist/single-flight.js +142 -0
  39. package/dist/types.d.ts +502 -0
  40. package/dist/types.js +1 -0
  41. package/dist/upstream.d.ts +126 -0
  42. package/dist/upstream.js +343 -0
  43. package/llms.txt +217 -0
  44. package/package.json +142 -0
@@ -0,0 +1,481 @@
1
+ import { type GasCostFor, type HopEdge, type HopFloor, type RoutePlan } from '@gibs/bridge-sdk/routing';
2
+ export { isRateLimitedCarrier } from '@gibs/bridge-sdk/routing';
3
+ /**
4
+ * PHASE 2 — ROUNDS. Turns the plans `enumerate.ts` finds into priced,
5
+ * ranked plans, asking real providers for only the hops that genuinely need
6
+ * asking. "Local steps are exact now": `enumerate.ts` already runs every
7
+ * carrier that exposes `Carrier.exact` synchronously, against the reader's
8
+ * own real amount, so a hop carried by the omnibridge crossing, a PulseX
9
+ * swap, or any other free local read arrives here ALREADY a binding
10
+ * `'quote'` — this module never asks one. What still needs asking is the
11
+ * AGGREGATOR hops (LI.FI, Relay, NEAR Intents), which `enumerate.ts` can only
12
+ * ever estimate (a provider model, or a bare price-feed guess).
13
+ *
14
+ * TWO ROUNDS, NOT THREE WAVES. Round A asks each path's FIRST aggregator hop,
15
+ * one parallel flight, capped per provider. Round B asks each path's SECOND
16
+ * aggregator hop (when it has one) at Round A's own guaranteed minimum,
17
+ * capped tighter still. There is no confirmation pass: a local hop needs no
18
+ * confirming (it was exact from the start), and a third aggregator hop in one
19
+ * plan is rare enough (at most 3 signatures total, and every plan needs at
20
+ * least one non-aggregator action to reach a bridgeable pair in practice)
21
+ * that this module accepts it staying an estimate rather than adding a third
22
+ * round to chase it — see `README`/design doc's "Rounds" section for the
23
+ * owner's own model: "estimates choose what gets quoted but never win."
24
+ *
25
+ * PURE ORCHESTRATION, NO FETCHING OF ITS OWN — unchanged from the previous
26
+ * wave-based design. This module never calls `fetch` and knows nothing about
27
+ * LI.FI, Relay or NEAR Intents by name; it is handed an {@link AskEdge}
28
+ * function and a {@link WaveClock}, and a {@link GasCostFor} for what a
29
+ * signature really costs. Everything else belongs to the interface adapter
30
+ * that supplies them (`packages/ui/src/lib/state/route-search-quotes.ts`).
31
+ *
32
+ * WHY A SEPARATE CONCURRENCY LIMITER FROM `packages/ui/.../quoteLimiter.ts`,
33
+ * RATHER THAN ONE SHARED MODULE. `bridge-sdk`/`@gibs/quotes` cannot depend on
34
+ * `packages/ui`, so this module enforces its own per-provider budget
35
+ * internally, over calls to the caller-supplied `askEdge` — the same reason
36
+ * the previous wave-based design gave for keeping its limiter internal.
37
+ */
38
+ /**
39
+ * One request this module hands to the caller-supplied {@link AskEdge}
40
+ * function: "what does this edge deliver for this input?"
41
+ */
42
+ export type AskEdgeRequest = {
43
+ /** The edge being asked. */
44
+ readonly edge: HopEdge;
45
+ /** The amount flowing INTO the edge, in `edge.from`'s base units. */
46
+ readonly inputAmount: bigint;
47
+ };
48
+ /**
49
+ * Why an edge answered with a refusal rather than a deliverable amount.
50
+ *
51
+ * A BROADER VOCABULARY THAN `quote-refusals.ts`'s `QuoteRefusalKind`, AND
52
+ * DELIBERATELY NOT THE SAME TYPE. `QuoteRefusalKind` (`packages/ui/.../quote-refusals.ts`)
53
+ * names why an AGGREGATOR declined — the three kinds measured against LI.FI
54
+ * and Relay. An edge here can also be a bridge entry call refused for
55
+ * falling outside `minPerTx`/`maxAvailablePerTx` (`../../bridge-limits.js`),
56
+ * which is neither a "no quote" nor a "could not ask" in the aggregator
57
+ * sense — it is a bound this module's own local read enforces. `bridge-sdk`
58
+ * cannot import the user-interface package's type to extend it, so this is
59
+ * its own union; `'no-quote'`, `'could-not-ask'` and `'cannot-carry'` share
60
+ * their spelling with `QuoteRefusalKind`'s members on purpose, so the
61
+ * interface adapter's translation from one to the other (`route-search-quotes.ts`)
62
+ * is a one-to-one rename, not a judgement call.
63
+ */
64
+ export type EdgeRefusalKind = 'no-quote' | 'could-not-ask' | 'cannot-carry' | 'below-minimum' | 'above-available';
65
+ /** Why an edge refused, with its reason and any minimum this module can learn from. */
66
+ export type EdgeRefusal = {
67
+ readonly kind: EdgeRefusalKind;
68
+ /** The provider's own words for why, or null when none was given. */
69
+ readonly reason: string | null;
70
+ /**
71
+ * A minimum amount this refusal names, in `edge.from`'s base units, or
72
+ * null when the refusal names no such figure (`'no-quote'`,
73
+ * `'could-not-ask'`, `'cannot-carry'` usually carry none). When present,
74
+ * this feeds the {@link FloorsCache} so a later ask of the SAME edge below
75
+ * this amount is pruned without a request.
76
+ */
77
+ readonly minimumAmount: bigint | null;
78
+ };
79
+ /** What answering one {@link AskEdgeRequest} produces: a deliverable amount, or a refusal. */
80
+ export type AskEdgeAnswer = {
81
+ readonly ok: true;
82
+ /** What the edge delivers for the asked input, in `edge.to`'s base units. */
83
+ readonly deliveredAmount: bigint;
84
+ /**
85
+ * The GUARANTEED delivered amount after slippage, when the provider
86
+ * states one; null when it does not. Round B re-asks a later
87
+ * aggregator hop at the PREVIOUS aggregator hop's minimum, falling
88
+ * back to `deliveredAmount` when null.
89
+ */
90
+ readonly minimumDeliveredAmount: bigint | null;
91
+ /** The carrier's own claimed execution duration in seconds, or null. */
92
+ readonly claimedDurationSeconds: number | null;
93
+ } | {
94
+ readonly ok: false;
95
+ readonly refusal: EdgeRefusal;
96
+ };
97
+ /**
98
+ * Asks one edge, at one amount, for what it delivers. Supplied by the
99
+ * interface adapter (`route-search-quotes.ts`); never implemented in this
100
+ * module. See this module's own doc for why.
101
+ */
102
+ export type AskEdge = (request: AskEdgeRequest) => Promise<AskEdgeAnswer>;
103
+ /**
104
+ * The two facts about time this module needs, injected so a test can drive
105
+ * a round's budget without waiting in real time.
106
+ */
107
+ export type WaveClock = {
108
+ /** The current time in milliseconds — used only to bucket and expire cache entries. */
109
+ readonly now: () => number;
110
+ /**
111
+ * Resolves after `milliseconds`. This is what a round races its
112
+ * outstanding asks against: real code resolves it with a timer, a test
113
+ * resolves it however it likes (immediately, to force a budget cutoff;
114
+ * never, to prove the budget is not consulted when nothing needs it).
115
+ */
116
+ readonly after: (milliseconds: number) => Promise<void>;
117
+ };
118
+ /** The real clock: `Date.now()` and a real `setTimeout`. What production code uses. */
119
+ export declare const realWaveClock: WaveClock;
120
+ /** Round A's time budget: every selected first-aggregator-hop ask, given six seconds to answer, one parallel flight. */
121
+ export declare const ROUND_A_TIME_BUDGET_MS = 6000;
122
+ /** Round B's time budget. */
123
+ export declare const ROUND_B_TIME_BUDGET_MS = 4000;
124
+ /** {@link confirmCandidate}'s own time budget for its single ask. */
125
+ export declare const CONFIRM_CANDIDATE_TIME_BUDGET_MS = 4000;
126
+ /** The most paths Round A asks per provider — LI.FI and Relay's own share of the owner's caps. */
127
+ export declare const DEFAULT_PROVIDER_CONCURRENCY_LIMIT = 4;
128
+ /**
129
+ * NEAR Intents' own, lower Round A cap. Measured: 40 concurrent requests
130
+ * against these providers fail about 80% of the time, while 40 in sequence
131
+ * pass — see `packages/ui/.../quoteLimiter.ts`'s own doc for the same
132
+ * measurement applied to the interface's query layer. NEAR Intents is capped
133
+ * more tightly than the other two because every one of its requests reserves
134
+ * state on the far side even when asked `dry`.
135
+ */
136
+ export declare const NEAR_INTENTS_CONCURRENCY_LIMIT = 3;
137
+ /** The most paths Round B asks per provider — a tighter follow-up pass than Round A. */
138
+ export declare const ROUND_B_MAX_REQUESTS_PER_PROVIDER = 2;
139
+ /** The most next-best, still-unconfirmed plans {@link searchRouteWaves} returns as `estimatedCandidates`. */
140
+ export declare const MAX_ESTIMATED_CANDIDATES = 5;
141
+ /**
142
+ * How far a live answer must diverge from its own pre-round estimate, as a
143
+ * fraction of the estimate, before {@link searchRouteWaves} reports it to
144
+ * {@link SearchRouteWavesOptions.onCalibrationDivergence}.
145
+ */
146
+ export declare const CALIBRATION_DIVERGENCE_RATIO_THRESHOLD = 0.01;
147
+ /** How long one cached edge answer is trusted before it is asked again. */
148
+ export declare const EDGE_ANSWER_CACHE_TTL_MS = 30000;
149
+ /**
150
+ * Rounds a positive base-unit amount to two significant figures, always by
151
+ * bigint arithmetic — never through a float, which would misround a large
152
+ * amount long before the two figures this needs. `123,456,789n` buckets to
153
+ * `120,000,000n`; `7n` buckets to `7n` (already two-or-fewer digits).
154
+ *
155
+ * WHY TWO SIGNIFICANT FIGURES. Caching lets "typing reuse" an answer — a
156
+ * reader adjusting the fourth digit of an amount should not re-ask every
157
+ * provider, but a reader who has typed a materially different amount should.
158
+ * Two significant figures is coarse enough to absorb a keystroke and fine
159
+ * enough that a doubled or halved amount still buckets differently.
160
+ *
161
+ * @param amount - the amount to bucket, in base units
162
+ * @returns the bucketed amount; zero and negative inputs bucket to zero
163
+ */
164
+ export declare const twoSignificantFigureAmountBucket: (amount: bigint) => bigint;
165
+ /** One entry recorded in an {@link EdgeAnswerCache}. */
166
+ type EdgeAnswerCacheEntry = {
167
+ /** The EXACT amount this answer was asked at (never the bucket) — see `recomposePath`. */
168
+ readonly askedAmount: bigint;
169
+ readonly answer: AskEdgeAnswer;
170
+ readonly recordedAtMs: number;
171
+ };
172
+ /**
173
+ * One edge answer, reusable for 30 seconds by any ask that lands in the same
174
+ * two-significant-figure amount bucket. Injectable so a caller can share one
175
+ * cache across the several calls a reader's typing makes to the exported
176
+ * search function, and so a test can supply a fresh, isolated one per case.
177
+ */
178
+ export type EdgeAnswerCache = {
179
+ /**
180
+ * Reads a cached answer for `edge` at `askedAmount`'s bucket, or null when
181
+ * there is none or it has aged past {@link EDGE_ANSWER_CACHE_TTL_MS}.
182
+ */
183
+ readonly get: (edge: HopEdge, askedAmount: bigint) => EdgeAnswerCacheEntry | null;
184
+ /** Records an answer for `edge` at `askedAmount`'s bucket. */
185
+ readonly set: (edge: HopEdge, askedAmount: bigint, answer: AskEdgeAnswer) => void;
186
+ };
187
+ /**
188
+ * Builds a fresh, empty {@link EdgeAnswerCache}.
189
+ *
190
+ * @param clock - supplies `now()` for recording and expiring entries;
191
+ * defaults to {@link realWaveClock}
192
+ * @returns the cache
193
+ */
194
+ export declare const createEdgeAnswerCache: (clock?: Pick<WaveClock, "now">) => EdgeAnswerCache;
195
+ /**
196
+ * Every floor a round search has learned so far, readable in the exact shape
197
+ * `enumerate.ts`'s `EnumerateRoutesOptions.knownFloors` expects — so the
198
+ * caller can feed the SAME cache straight back into the next call to
199
+ * `enumerateRoutes` and have it prune edges this search already learned are
200
+ * too small.
201
+ */
202
+ export type FloorsCache = {
203
+ /** Records a learned floor for one edge. Keeps only the LARGEST minimum seen for that edge. */
204
+ readonly record: (edge: Pick<HopEdge, 'carrier' | 'from' | 'to'>, floor: HopFloor) => void;
205
+ /** Reads the floors known for one edge — the shape `enumerate.ts` consumes directly. */
206
+ readonly read: (edge: Pick<HopEdge, 'carrier' | 'from' | 'to'>) => readonly HopFloor[];
207
+ };
208
+ /**
209
+ * Builds a fresh, empty {@link FloorsCache}.
210
+ *
211
+ * @returns the cache
212
+ */
213
+ export declare const createFloorsCache: () => FloorsCache;
214
+ /**
215
+ * The plan as its fused first hop's signature really delivers it: the hop
216
+ * hands the next one `entryTarget` instead of its guaranteed minimum.
217
+ *
218
+ * WHY. {@link recomposePath} hands the hop after a fused one exactly the
219
+ * guaranteed minimum, because the pre-quote's floor is the entry call's first
220
+ * target. That pre-quote never paid for the entry call. When the provider's
221
+ * exact-output answer prices the call past the typed amount, the host shrinks
222
+ * the target until the spend fits (`fitEntryTarget` in the interface), so the
223
+ * cost comes out of the output. A plan that still composed from the minimum
224
+ * would show the reader more than the signature can deliver, and the press
225
+ * would report a price move that was never a move.
226
+ *
227
+ * Every later hop keeps its own rate — what it delivered for what it was
228
+ * handed — applied to the smaller amount, the same way a hop whose upstream
229
+ * amount moved is composed everywhere else in this module. The first hop's
230
+ * own outcome is untouched: it still names the minimum the entry call was
231
+ * first built for, which is what the signer checks the target against.
232
+ *
233
+ * @param plan - a plan whose first hop fuses with the next
234
+ * @param entryTarget - the entry amount the signature embeds, in the first
235
+ * hop's `to` node's base units
236
+ * @returns the plan itself when `entryTarget` is the minimum, the plan
237
+ * composed from `entryTarget` when it is smaller, or null when the first
238
+ * hop is not a fused quote with a minimum, or `entryTarget` is not positive
239
+ * or is above that minimum
240
+ */
241
+ export declare const planWithEntryTarget: (plan: RoutePlan, entryTarget: bigint) => RoutePlan | null;
242
+ /**
243
+ * One live answer that diverged from its own pre-round estimate by more than
244
+ * {@link CALIBRATION_DIVERGENCE_RATIO_THRESHOLD} — handed to
245
+ * {@link SearchRouteWavesOptions.onCalibrationDivergence} so a host can feed
246
+ * its carriers' own calibration store (`route-search-quotes.ts` onward).
247
+ * Compares RATES, not raw amounts, because a live ask and its own estimate
248
+ * are rarely taken at the exact same input amount.
249
+ */
250
+ export type CalibrationDivergenceRecord = {
251
+ /** The edge whose live answer diverged. */
252
+ readonly edge: HopEdge;
253
+ /** The amount this live answer was asked at. */
254
+ readonly askedAmount: bigint;
255
+ /** What the live ask actually delivered, at `askedAmount`. */
256
+ readonly liveDeliveredAmount: bigint;
257
+ /** What this edge's pre-round estimate said it would deliver, at `estimatedInputAmount`. */
258
+ readonly estimatedDeliveredAmount: bigint;
259
+ /** The input amount the pre-round estimate was computed against. */
260
+ readonly estimatedInputAmount: bigint;
261
+ /**
262
+ * `(liveRate − estimatedRate) / estimatedRate`, where each rate is that
263
+ * side's own `deliveredAmount / inputAmount`. Positive when the live
264
+ * answer delivered MORE than estimated, negative when it delivered less.
265
+ */
266
+ readonly divergenceRatio: number;
267
+ };
268
+ /** One refusal this search collected, for the caller to surface or feed a floors cache. */
269
+ export type CollectedRefusal = {
270
+ readonly edge: HopEdge;
271
+ readonly askedAmount: bigint;
272
+ readonly refusal: EdgeRefusal;
273
+ };
274
+ /**
275
+ * How far above its own figure an estimated hop may still turn out to land,
276
+ * in parts per million, charged once for every `'estimate'` hop in a plan.
277
+ * The same 1% {@link CALIBRATION_DIVERGENCE_RATIO_THRESHOLD} names as the
278
+ * point where a live answer counts as having moved away from its estimate:
279
+ * inside it, the estimate was right; past it, the carrier's own model is
280
+ * corrected. So 1% per estimated hop is the widest honest "best case" the
281
+ * engine can state from what it knows before asking.
282
+ */
283
+ export declare const ESTIMATE_ALLOWANCE_PPM_PER_ESTIMATED_HOP = 10000n;
284
+ /**
285
+ * The most a plan could still deliver, net of every signature's gas, in its
286
+ * destination token's base units — or null when the plan has no figure a
287
+ * ranking may trust (`planIsRankable` false), which is a plan the engine can
288
+ * neither bound nor show.
289
+ *
290
+ * AN UPPER BOUND, BUILT ONLY FROM WHAT IS KNOWN AHEAD OF TIME. Each hop's
291
+ * figure already carries that carrier's own known fee (a local carrier's
292
+ * exact read, an aggregator's provider model, or the price catalogue), and
293
+ * {@link gasAdjustedDeliveredAmount} subtracts the real gas of EVERY
294
+ * signature. The only optimism added is
295
+ * {@link ESTIMATE_ALLOWANCE_PPM_PER_ESTIMATED_HOP} per hop that is still an
296
+ * estimate. A confirmed plan gets no allowance: its figure is what it is.
297
+ *
298
+ * @param plan - the plan to bound
299
+ * @param gasCostFor - the real cost of one signature
300
+ * @returns the plan's best-case net delivery, or null when it is not rankable
301
+ */
302
+ export declare const planBestCaseNetDelivery: (plan: RoutePlan, gasCostFor: GasCostFor) => bigint | null;
303
+ /**
304
+ * The best net delivery among `plans`' CONFIRMED plans — delivered minus the
305
+ * gas of every signature — or null when none of them is confirmed yet.
306
+ *
307
+ * @param plans - any plans; unconfirmed ones are ignored
308
+ * @param gasCostFor - the real cost of one signature
309
+ * @returns the best confirmed net delivery, or null
310
+ */
311
+ export declare const bestConfirmedNetDelivery: (plans: readonly RoutePlan[], gasCostFor: GasCostFor) => bigint | null;
312
+ /**
313
+ * Whether an unconfirmed plan's best case still beats `bestConfirmed`. An
314
+ * unrankable plan never does: with no figure there is nothing to compare,
315
+ * and the owner's rule is that such a path is never shown. With nothing
316
+ * confirmed yet, every rankable plan is still in the running.
317
+ *
318
+ * GENERIC AND COST-BASED. Nothing here reads a chain, a carrier or a token.
319
+ * A path that leaves a chain and comes back is dropped for the same reason
320
+ * any other path is: each extra signature costs at least its gas, and each
321
+ * crossing costs at least its known fee, so its best case falls short.
322
+ *
323
+ * @param plan - the unconfirmed plan
324
+ * @param bestConfirmed - the best confirmed net delivery, or null when none exists
325
+ * @param gasCostFor - the real cost of one signature
326
+ * @returns true when the plan could still win
327
+ */
328
+ export declare const estimateCanStillWin: (plan: RoutePlan, bestConfirmed: bigint | null, gasCostFor: GasCostFor) => boolean;
329
+ /**
330
+ * Drops every unconfirmed, RANKABLE path whose best case cannot beat the best
331
+ * path already confirmed by a local exact read — BEFORE any provider is
332
+ * asked, so a path that cannot win never spends a live quote.
333
+ *
334
+ * An unrankable path is kept here, on purpose: it has no figure to bound, so
335
+ * nothing proves it loses, and a live answer is the only way it can ever get
336
+ * one. It is still never SHOWN as an estimate — see
337
+ * {@link splitConfirmedAndEstimated}.
338
+ *
339
+ * @param paths - every enumerated path
340
+ * @param gasCostFor - the real cost of one signature
341
+ * @returns the paths still worth asking about
342
+ */
343
+ export declare const prunePathsThatCannotWin: (paths: readonly RoutePlan[], gasCostFor: GasCostFor) => readonly RoutePlan[];
344
+ /**
345
+ * Folds one plan that {@link SearchRouteWavesResult.confirmCandidate} just
346
+ * re-priced back into a finished search result — WITHOUT which a confirmed
347
+ * estimate had nowhere to go. `confirmCandidate` only writes the live answer
348
+ * into the search's own caches; the result a host renders is plain data and
349
+ * never changes on its own, so the estimate row stayed an estimate forever.
350
+ *
351
+ * The plan replaces the entry with its own `pathKey` wherever it sat. A plan
352
+ * that is now confirmed joins `plans`; one that is not stays an estimate
353
+ * only while it still carries a figure and could still win. Both lists are
354
+ * re-sorted and re-pruned, because a newly confirmed plan can raise the bar
355
+ * every remaining estimate must clear.
356
+ *
357
+ * @param result - the search result to update
358
+ * @param plan - the plan `confirmCandidate` returned
359
+ * @param options - the SAME comparator and gas model the search ranked with
360
+ * @returns a new result; `result` itself is never changed
361
+ */
362
+ export declare const applyConfirmedCandidate: (result: SearchRouteWavesResult, plan: RoutePlan, options: {
363
+ readonly compareRoutePlans: (a: RoutePlan, b: RoutePlan) => number;
364
+ readonly gasCostFor: GasCostFor;
365
+ }) => SearchRouteWavesResult;
366
+ /** What one call to {@link searchRouteWaves} needs. */
367
+ export type SearchRouteWavesOptions = {
368
+ /** The plans `enumerate.ts` found — local-exact hops already `'quote'`, aggregator hops still `'estimate'`. */
369
+ readonly paths: readonly RoutePlan[];
370
+ /** The reader's real amount entering the FIRST hop of every path. */
371
+ readonly amount: bigint;
372
+ /** Asks one edge for what it delivers. Never implemented here — see {@link AskEdge}. */
373
+ readonly askEdge: AskEdge;
374
+ /**
375
+ * Supplies the real cost of one signature — see
376
+ * `@gibs/bridge-sdk/routing`'s `GasCostFor`. Defaults to `zeroGasCostFor`
377
+ * (charges nothing), which is NOT a production model — a caller with real
378
+ * gas-price and native-price feeds should always supply its own.
379
+ */
380
+ readonly gasCostFor?: GasCostFor;
381
+ /**
382
+ * How to rank two finished plans, best first. Defaults to
383
+ * {@link compareRoutePlansByGasAdjustedDeliveredAmount} applied to
384
+ * `gasCostFor` — rankable beats unrankable, and within that a plan's REAL
385
+ * gas-adjusted delivered amount wins, never a flat per-signature guess.
386
+ */
387
+ readonly compareRoutePlans?: (a: RoutePlan, b: RoutePlan) => number;
388
+ /** The clock driving every round's time budget. Defaults to {@link realWaveClock}. */
389
+ readonly clock?: WaveClock;
390
+ /** The edge answer cache to read from and write into. Defaults to a fresh, empty one. */
391
+ readonly edgeAnswerCache?: EdgeAnswerCache;
392
+ /** The floors cache to read from and write into. Defaults to a fresh, empty one. */
393
+ readonly floorsCache?: FloorsCache;
394
+ /**
395
+ * Called once after each round settles, with the plans ranked so far —
396
+ * lets a caller stream results to a reader instead of waiting for both
397
+ * rounds.
398
+ */
399
+ readonly onWaveSettled?: (result: {
400
+ readonly round: 'A' | 'B';
401
+ readonly plans: readonly RoutePlan[];
402
+ }) => void;
403
+ /**
404
+ * Called whenever a fresh live answer diverges from its own pre-round
405
+ * estimate by more than {@link CALIBRATION_DIVERGENCE_RATIO_THRESHOLD} — a
406
+ * host wires this to its carriers' own calibration store. Omitted entirely
407
+ * (the default) records nothing.
408
+ */
409
+ readonly onCalibrationDivergence?: (record: CalibrationDivergenceRecord) => void;
410
+ };
411
+ /** What {@link searchRouteWaves} returns. */
412
+ export type SearchRouteWavesResult = {
413
+ /**
414
+ * The CONFIRMED plans (`planIsConfirmed` true on every hop), ranked by the
415
+ * search's own `compareRoutePlans`. These are the real, ask-backed
416
+ * candidates a reader may act on.
417
+ */
418
+ readonly plans: readonly RoutePlan[];
419
+ /**
420
+ * The next-best plans that never landed a live answer for every hop —
421
+ * still ranked, up to {@link MAX_ESTIMATED_CANDIDATES}, and only those that
422
+ * carry an estimate and could still beat the best confirmed plan
423
+ * ({@link estimateCanStillWin}) — for a caller to
424
+ * list below `plans`, marked as estimates, and confirm on demand with
425
+ * {@link confirmCandidate}. An estimate never appears in `plans` itself,
426
+ * however large its estimated figure: see the owner's own model,
427
+ * "estimates choose what gets quoted but never win."
428
+ */
429
+ readonly estimatedCandidates: readonly RoutePlan[];
430
+ /** Every refusal this search collected, across both rounds. */
431
+ readonly refusals: readonly CollectedRefusal[];
432
+ /**
433
+ * Asks a SINGLE path's still-unasked aggregator hops, in hop order, each
434
+ * at what the hop before it really delivers — stopping early only at a
435
+ * refusal or an ask that does not answer in time — and returns that path
436
+ * recomposed with the fresh answers, or null when
437
+ * `pathKey` names no path this search found. Touches only `pathKey`'s own
438
+ * hops; every other path's own state (cache entries, floors) is read, never
439
+ * written to, beyond what the single ask itself records. Reuses this
440
+ * search's own edge answer cache, floors cache and provider concurrency
441
+ * limiter, so an edge `confirmCandidate` asks that a later `plans`/
442
+ * `estimatedCandidates` recompute also needs is free the second time.
443
+ *
444
+ * @param pathKey - the candidate plan's own `RoutePlan.pathKey`
445
+ */
446
+ readonly confirmCandidate: (pathKey: string) => Promise<RoutePlan | null>;
447
+ };
448
+ /**
449
+ * The Round-A/Round-B route search: prices every plan `enumerate.ts` found
450
+ * against real aggregators, asking only the hops that are not already exact,
451
+ * in two bounded, budgeted rounds, re-ranking after each one.
452
+ *
453
+ * - **Round A** asks each path's first aggregator hop — see {@link buildRoundAAsks}
454
+ * — within {@link ROUND_A_TIME_BUDGET_MS}, one parallel flight.
455
+ * - **Round B** asks each ranked path's second aggregator hop, at Round A's
456
+ * own guaranteed minimum — see {@link buildRoundBAsks} — within
457
+ * {@link ROUND_B_TIME_BUDGET_MS}.
458
+ *
459
+ * Every hop without a fresh answer — a local-exact hop (never asked, on
460
+ * purpose), or an aggregator hop this search's caps left unasked — keeps
461
+ * composing from its own last-known figure, so a partial result is always a
462
+ * complete, ranked list of plans, never a list with holes in it.
463
+ *
464
+ * PRUNED FIRST: {@link prunePathsThatCannotWin} drops every path whose best
465
+ * case cannot beat a plan a local exact read already confirmed, before any
466
+ * provider is asked.
467
+ *
468
+ * THE FINAL SPLIT: `plans` (confirmed) VERSUS `estimatedCandidates`
469
+ * (unconfirmed plans that carry an estimate and could still win, capped at
470
+ * {@link MAX_ESTIMATED_CANDIDATES}) —
471
+ * {@link partitionConfirmedFirst} orders every plan confirmed-first exactly as
472
+ * before; this function then SLICES that ordering into the two output
473
+ * fields, rather than returning one merged list, per the owner's own model:
474
+ * an estimate is listed separately, never blended into the ranked winners.
475
+ *
476
+ * @param options - see {@link SearchRouteWavesOptions}
477
+ * @returns the re-priced plans, split into confirmed and estimated
478
+ * candidates, every refusal collected, and a way to confirm one candidate
479
+ * on demand
480
+ */
481
+ export declare const searchRouteWaves: (options: SearchRouteWavesOptions) => Promise<SearchRouteWavesResult>;