@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.
- package/LICENSE +15 -0
- package/README.md +45 -0
- package/dist/cache.d.ts +73 -0
- package/dist/cache.js +137 -0
- package/dist/carriers/calibration.d.ts +115 -0
- package/dist/carriers/calibration.js +83 -0
- package/dist/carriers/lifi.d.ts +63 -0
- package/dist/carriers/lifi.js +217 -0
- package/dist/carriers/near-intents.d.ts +91 -0
- package/dist/carriers/near-intents.js +249 -0
- package/dist/carriers/price-catalogue.d.ts +186 -0
- package/dist/carriers/price-catalogue.js +191 -0
- package/dist/carriers/relay.d.ts +97 -0
- package/dist/carriers/relay.js +242 -0
- package/dist/chain-lists.d.ts +144 -0
- package/dist/chain-lists.js +169 -0
- package/dist/client.d.ts +102 -0
- package/dist/client.js +402 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +18 -0
- package/dist/quote-guards.d.ts +128 -0
- package/dist/quote-guards.js +240 -0
- package/dist/quote-refusals.d.ts +180 -0
- package/dist/quote-refusals.js +284 -0
- package/dist/quote-service-contract.d.ts +2240 -0
- package/dist/quote-service-contract.js +247 -0
- package/dist/search/capabilities.d.ts +68 -0
- package/dist/search/capabilities.js +28 -0
- package/dist/search/enumerate.d.ts +162 -0
- package/dist/search/enumerate.js +313 -0
- package/dist/search/index.d.ts +17 -0
- package/dist/search/index.js +17 -0
- package/dist/search/serve.d.ts +178 -0
- package/dist/search/serve.js +142 -0
- package/dist/search/waves.d.ts +481 -0
- package/dist/search/waves.js +939 -0
- package/dist/single-flight.d.ts +90 -0
- package/dist/single-flight.js +142 -0
- package/dist/types.d.ts +502 -0
- package/dist/types.js +1 -0
- package/dist/upstream.d.ts +126 -0
- package/dist/upstream.js +343 -0
- package/llms.txt +217 -0
- 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>;
|