@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,502 @@
1
+ import type { Ecosystem } from '@gibs/bridge-sdk/ecosystems';
2
+ import type { ProviderId } from '@gibs/bridge-sdk/providers';
3
+ import type { AssetNode, HopEdge, HopEdgeOrigin, HopFloor } from '@gibs/bridge-sdk/routing';
4
+ import type { AskEdge, AskEdgeAnswer, AskEdgeRequest, EdgeRefusal, EdgeRefusalKind } from './search/waves.js';
5
+ import type { DisplayProviderSlot } from './quote-service-contract.js';
6
+ /**
7
+ * The internal interfaces later steps in this package build against: the
8
+ * route-search {@link Carrier} plug-in vocabulary, the funnel every carrier's
9
+ * network calls pass through, the display-quote answer cache, and the
10
+ * configuration object that makes the whole library agnostic to any one
11
+ * host. See `docs/quote-service.md`'s "Library" and "Carriers" sections for
12
+ * the narrative this file's shapes implement.
13
+ *
14
+ * NOTHING HERE NAMES A HOST. No `gibs.finance`, no chain 369, no PulseChain,
15
+ * no omnibridge, no PulseX, no API key. Every fact a host supplies travels
16
+ * through {@link QuoteClientConfig} or a registered {@link Carrier} — never a
17
+ * constant in this file.
18
+ */
19
+ /** Identifies one registered {@link Carrier}. A plain string, not a closed union — see this section's head note for why. */
20
+ export type CarrierId = string;
21
+ /** One asset a route search can hold: a token, or a chain's native currency, on one chain. Alias of `@gibs/bridge-sdk/routing`'s `AssetNode`. */
22
+ export type CarrierAssetNode = AssetNode;
23
+ /** How a hop's crossing is initiated. Alias of `@gibs/bridge-sdk/routing`'s `HopEdgeOrigin`. */
24
+ export type CarrierHopOrigin = HopEdgeOrigin;
25
+ /** A known minimum this edge has already been refused below. Alias of `@gibs/bridge-sdk/routing`'s `HopFloor`. */
26
+ export type CarrierHopFloor = HopFloor;
27
+ /**
28
+ * One directed edge a {@link Carrier} can move funds across. Alias of
29
+ * `@gibs/bridge-sdk/routing`'s `HopEdge` — that type's own `carrier` field is
30
+ * already {@link CarrierId} (a plain string), not a closed union, so no
31
+ * narrowing is needed here.
32
+ */
33
+ export type CarrierHopEdge = HopEdge;
34
+ /** One request a {@link Carrier} is asked: "what does this edge deliver for this input?" Alias of `@gibs/bridge-sdk/routing`'s `AskEdgeRequest`. */
35
+ export type CarrierAskRequest = AskEdgeRequest;
36
+ /**
37
+ * Why an edge answered with a refusal rather than a deliverable amount.
38
+ * Alias of `@gibs/bridge-sdk/routing`'s `EdgeRefusalKind` — a broader
39
+ * vocabulary than {@link QuoteRefusalKind} in `quote-refusals.ts`, because a
40
+ * carrier can also refuse for a bound its own local read enforces
41
+ * (`'below-minimum'`, `'above-available'`), not only an aggregator's "no" or
42
+ * "could not ask."
43
+ */
44
+ export type CarrierRefusalKind = EdgeRefusalKind;
45
+ /** Why a carrier refused, with its reason and any minimum learned from it. Alias of `@gibs/bridge-sdk/routing`'s `EdgeRefusal`. */
46
+ export type CarrierRefusal = EdgeRefusal;
47
+ /** What answering one {@link CarrierAskRequest} produces. Alias of `@gibs/bridge-sdk/routing`'s `AskEdgeAnswer`. */
48
+ export type CarrierAskAnswer = AskEdgeAnswer;
49
+ /** Asks one carrier's edge, at one amount, for what it delivers. Alias of `@gibs/bridge-sdk/routing`'s `AskEdge`. */
50
+ export type CarrierAsk = AskEdge;
51
+ /** What {@link Carrier.fusesWithNext} is asked about the hop immediately following the one it performs. */
52
+ export type CarrierFusesWithNextOptions = {
53
+ /**
54
+ * What the acting hop itself spends — its `from` node. A carrier whose
55
+ * destination call is available only from some origins (LI.FI's
56
+ * contract-calls endpoint answers for Ethereum alone) reads its chain here.
57
+ */
58
+ readonly hopFrom: CarrierAssetNode;
59
+ /** The carrier of the hop immediately after this one in the same plan. */
60
+ readonly nextHopCarrier: CarrierId;
61
+ /**
62
+ * What the next hop spends — its `from` node, which is where the acting hop
63
+ * lands. A native coin here means the next hop's call is payable (the
64
+ * omnibridge's native entry), and a carrier that cannot attach value to its
65
+ * destination call must not fuse into it.
66
+ */
67
+ readonly nextHopFrom: CarrierAssetNode;
68
+ /** The chain id the next hop DELIVERS to — its `to.chainId`. */
69
+ readonly nextHopDestinationChainId: number;
70
+ };
71
+ /**
72
+ * How a reader may fund the first hop of a route section that starts with
73
+ * this carrier.
74
+ *
75
+ * REPLACES A BOOLEAN `acceptsByHandDeposit` FIELD ON PURPOSE — see this
76
+ * file's Code Style rule against boolean fields. `'wallet-signed'` and
77
+ * `'by-hand-deposit'` are not two poles of one axis a reader flips; a third,
78
+ * genuinely different carrier exists that offers BOTH (NEAR Intents, which
79
+ * carries a pay-by-hand origin's first hop today and also runs as an
80
+ * ordinary wallet-signed aggregator mid-route), and a boolean has no way to
81
+ * say "either" without a second field beside it — which is exactly the
82
+ * "two names for one fact" trap the boolean would have created.
83
+ *
84
+ * - `'wallet-signed'` — the reader's connected wallet signs a transaction.
85
+ * Every aggregator and on-chain carrier that has no deposit-address flow
86
+ * (LI.FI, Relay, the omnibridge crossing, a PulseX swap) is this.
87
+ * - `'by-hand-deposit'` — the reader sends funds to a carrier-supplied
88
+ * deposit address; no wallet signature. `@gibs/bridge-sdk/chain-names`'s
89
+ * `isDepositAddressChain` is the origin-side test this pairs with:
90
+ * `enumerate.ts` prunes every carrier whose `depositMode` is
91
+ * `'wallet-signed'` from a pay-by-hand origin's first hop.
92
+ * - `'either'` — the carrier can be funded either way, chosen elsewhere by
93
+ * which hop it fills (NEAR Intents today).
94
+ */
95
+ export type CarrierDepositMode = 'wallet-signed' | 'by-hand-deposit' | 'either';
96
+ /**
97
+ * Which signer a route section STARTING with this carrier uses — what a
98
+ * display layer names next to its "sign" prompt.
99
+ *
100
+ * AN OPEN STRING-LITERAL UNION, LIKE {@link CarrierId}'s OWN NOTE EXPLAINS:
101
+ * this package's own built-in carriers (LI.FI, Relay, NEAR Intents) are all
102
+ * `'aggregator'`; a host's own carriers name the other two known values here
103
+ * (gibs registers the omnibridge crossing as `'omnibridge'` and its PulseX
104
+ * conversion leg as `'swap'`) or coins its own term entirely — the
105
+ * `(string & {})` member keeps editor autocomplete for the known set without
106
+ * closing the union to a host's own vocabulary, exactly as `CarrierId`
107
+ * itself stays a plain string rather than a closed union.
108
+ */
109
+ export type CarrierSender = 'omnibridge' | 'aggregator' | 'swap' | 'deposit-address' | (string & {});
110
+ /**
111
+ * What {@link Carrier.exact} answers for one edge at one input amount, once
112
+ * {@link Carrier.prepare} has read whatever local data (a fee rate, a pool's
113
+ * reserves, a known limit) the answer needs.
114
+ *
115
+ * TWO CASES, BOTH DISTINCT FROM THE BARE `null` {@link Carrier.exact} MAY ALSO
116
+ * RETURN. `'delivered'` is the exact answer. `'refused'` is ALSO a real
117
+ * answer — the carrier read its own data and that data says this amount
118
+ * cannot cross (below a minimum, above an available reserve) — so it carries
119
+ * a {@link CarrierRefusal}, reusing the SAME reason vocabulary a live
120
+ * provider's own `ask()` refusal already uses, rather than inventing a
121
+ * second one for a local read. Bare `null` is reserved for a carrier that
122
+ * has NOTHING to say about this edge at all (its `prepare` was never called,
123
+ * or this edge fell outside what it read) — a structural non-answer, not a
124
+ * reasoned refusal; `enumerate.ts` treats both the same way for pruning
125
+ * (this candidate hop is dropped), but only `'refused'` carries a reason
126
+ * worth keeping.
127
+ */
128
+ export type ExactHopResult = {
129
+ readonly outcome: 'delivered';
130
+ /** What this edge delivers for exactly the requested input amount, in `to`'s base units. */
131
+ readonly deliveredAmount: bigint;
132
+ } | {
133
+ readonly outcome: 'refused';
134
+ /** Why this carrier's own data says the amount cannot cross. */
135
+ readonly refusal: CarrierRefusal;
136
+ };
137
+ /**
138
+ * What {@link Carrier.estimate} answers for one edge at one input amount — a
139
+ * PROVIDER MODEL, not a live quote and not a local exact read: a price ratio
140
+ * times (one minus the carrier's typical fee) minus a fixed cost, calibrated
141
+ * against observed history. `enumerate.ts` always grounds a figure this
142
+ * method produces on the `'provider-model'` {@link @gibs/bridge-sdk/routing!EstimateBasis} — RANKABLE
143
+ * (a search may compare and prune on it) but NEVER CONFIRMED (a search may
144
+ * never report it as the final winner without a live quote first) — see
145
+ * `@gibs/bridge-sdk/routing`'s `RANKABLE_ESTIMATE_BASES` and
146
+ * `isConfirmedBasis` for where that split is enforced.
147
+ */
148
+ export type EstimatedHopResult = {
149
+ /** What this carrier's provider model expects this edge to deliver for the requested input amount, in `to`'s base units. */
150
+ readonly deliveredAmount: bigint;
151
+ };
152
+ /**
153
+ * One plug-in the route-search maximization engine can ask: a built-in
154
+ * aggregator this package ships (LI.FI, Relay, NEAR Intents) or a host's own
155
+ * local read (an omnibridge crossing, a PulseX swap, or anything else a
156
+ * host's own code registers).
157
+ *
158
+ * `ProviderAsker` (the built-in aggregator's own vocabulary) IS a `Carrier` —
159
+ * see {@link ProviderAsker}'s doc for why the two are one vocabulary, not two.
160
+ */
161
+ export type Carrier = {
162
+ /** This carrier's own id — what {@link CarrierHopEdge.carrier} names when this carrier performs a hop. */
163
+ readonly id: CarrierId;
164
+ /**
165
+ * Every edge this carrier can move funds across right now, given its own
166
+ * capability data (a chain list, a token list, bridgeable pairs — whatever
167
+ * it was built from). Mirrors `routing/search/capabilities.ts`'s
168
+ * `CapabilityIndex.carriersFrom`, scoped to one carrier rather than every
169
+ * registered one.
170
+ */
171
+ readonly edgesFrom: (from: CarrierAssetNode, to: CarrierAssetNode) => readonly CarrierHopEdge[];
172
+ /** Asks one of this carrier's own edges. */
173
+ readonly ask: CarrierAsk;
174
+ /**
175
+ * Runs ONE BATCHED set of reads this carrier needs before {@link exact} can
176
+ * answer synchronously — a local carrier's own fee rate, pool reserves, or
177
+ * a known limit, read once per search rather than once per candidate hop.
178
+ * Called at most once per {@link enumerateRoutes} call, before any `exact`
179
+ * call, over every node the search might branch through; omitted entirely
180
+ * by a carrier with no local read to batch (every built-in aggregator this
181
+ * package ships — LI.FI, Relay, NEAR Intents — has none; they answer
182
+ * through {@link estimate} or a live `ask` instead).
183
+ *
184
+ * @param nodes - every node the search might branch through
185
+ */
186
+ readonly prepare?: (nodes: readonly CarrierAssetNode[]) => Promise<void>;
187
+ /**
188
+ * A SYNCHRONOUS, EXACT answer for one edge at one input amount, read from
189
+ * whatever {@link prepare} already loaded — never a network call. The
190
+ * owner's own routing model ("each individual step and its associated fees
191
+ * can be known ahead of time") is what this method exists to express: a
192
+ * local carrier whose crossing or swap formula is closed-form given its own
193
+ * read (the omnibridge crossing, a PulseX swap, a native wrap/unwrap)
194
+ * answers here instead of estimating. Omitted by every carrier that has no
195
+ * closed-form answer — an aggregator supplies {@link estimate} instead, or
196
+ * neither, falling back to a caller-supplied price-feed estimate.
197
+ *
198
+ * WHEN THIS IS DEFINED, `enumerate.ts` NEVER FALLS THROUGH TO `estimate` OR
199
+ * THE PRICE-FEED DEFAULT FOR THIS CARRIER'S EDGES — a hop this method
200
+ * answers always carries a `'quote'` {@link @gibs/bridge-sdk/routing!HopOutcome},
201
+ * never an `'estimate'` one, because a carrier that KNOWS its own exact
202
+ * math has nothing left to estimate.
203
+ *
204
+ * @param edge - the candidate hop
205
+ * @param amountIn - the amount flowing into `edge`, in `edge.from`'s base units
206
+ * @returns the exact result, or null — see {@link ExactHopResult}'s own doc
207
+ * for the difference between its `'refused'` case and a bare `null`
208
+ */
209
+ readonly exact?: (edge: CarrierHopEdge, amountIn: bigint) => ExactHopResult | null;
210
+ /**
211
+ * A PROVIDER-MODEL estimate for one edge at one input amount — a price
212
+ * ratio times (one minus this carrier's typical fee) minus a fixed cost,
213
+ * calibrated against observed history, never a live quote. The aggregator
214
+ * counterpart to {@link exact}: a carrier with no closed-form crossing
215
+ * formula (LI.FI, Relay, NEAR Intents) supplies this instead, so
216
+ * `enumerate.ts` can rank its edges on more than a bare price-feed
217
+ * conversion without asking the network during enumeration. See
218
+ * {@link EstimatedHopResult}'s own doc for the `'provider-model'`
219
+ * {@link @gibs/bridge-sdk/routing!EstimateBasis} every answer here is grounded on.
220
+ *
221
+ * RETURNS NULL WHEN THIS CARRIER HAS NOTHING TO SAY, MOST COMMONLY A
222
+ * MISSING PRICE — a provider-model figure needs both ends of an edge
223
+ * priced (`@gibs/quotes/carriers/price-catalogue!PriceCatalogue`); a token
224
+ * neither this carrier's own catalogue nor the search has learned a price
225
+ * for cannot be estimated, and guessing would rank it on a fabricated
226
+ * number. `enumerate.ts` reads `null` here exactly as it reads this method
227
+ * being absent altogether: it falls through to the caller-supplied
228
+ * price-feed `estimateHopOutput` instead of stopping the search.
229
+ *
230
+ * @param edge - the candidate hop
231
+ * @param amountIn - the amount flowing into `edge`, in `edge.from`'s base units
232
+ * @returns the modeled result, or null when this carrier cannot estimate this edge right now
233
+ */
234
+ readonly estimate?: (edge: CarrierHopEdge, amountIn: bigint) => EstimatedHopResult | null;
235
+ /**
236
+ * Whether this carrier competes for a shared, rate-limited network budget,
237
+ * as opposed to a free local read (an on-chain fee-rate read, a
238
+ * `getAmountsOut` call). Mirrors `routing/search/graph.ts`'s
239
+ * `isRateLimitedCarrier`, expressed per-carrier rather than as a lookup
240
+ * over a fixed set.
241
+ */
242
+ readonly isRateLimited: boolean;
243
+ /**
244
+ * How many of this carrier's requests may run at once, across every asker
245
+ * sharing it. Required when {@link isRateLimited} is true; meaningless (and
246
+ * ignored) for a free local read, which is never funnelled through a
247
+ * concurrency cap at all.
248
+ */
249
+ readonly concurrencyLimit?: number;
250
+ /** How a reader may fund a section starting with this carrier — see {@link CarrierDepositMode}'s own doc. */
251
+ readonly depositMode: CarrierDepositMode;
252
+ /** Which signer a section starting with this carrier uses — see {@link CarrierSender}'s own doc. */
253
+ readonly sender: CarrierSender;
254
+ /**
255
+ * A typical wait, in seconds, for this carrier's own crossing to settle —
256
+ * used to score a plan before any live quote names a real figure (a live
257
+ * `ask()` answer's own `claimedDurationSeconds`, when one comes back,
258
+ * always takes precedence; see `routing/carriers/ask-answer.ts`'s
259
+ * `CarrierAskAnswer`). Static per carrier, not per edge, because this
260
+ * package's own route-search maximization runs this figure through scoring
261
+ * BEFORE any edge is asked — see the owner's own model, "each individual
262
+ * step and its associated fees can be known ahead of time."
263
+ */
264
+ readonly settlementSeconds: number;
265
+ /**
266
+ * Whether THIS carrier's own execution, on arrival, also performs the
267
+ * NEXT hop in the plan — so the two hops together cost the reader only ONE
268
+ * signature. Mirrors `routing/search/graph.ts`'s `HopEdge.fusesNext`
269
+ * (which records the OUTCOME of this check for one specific pair of hops)
270
+ * and the OLD `hopFusesWithBridgeEntry`'s role (which HARD-CODED "the next
271
+ * hop is the omnibridge entry call" — a fact only gibs's own carriers may
272
+ * know).
273
+ *
274
+ * OMITTED MEANS "NEVER FUSES" — the correct default for every carrier that
275
+ * does not execute a destination call on arrival (every built-in
276
+ * aggregator here that merely delivers to the reader's own wallet, and any
277
+ * free local read with no executor of its own). A carrier that DOES
278
+ * execute destination calls, and that recognizes a following hop as its
279
+ * own kind of "entry call," supplies this to decide the two cases: gibs's
280
+ * own LI.FI/Relay-wrapping registration (`packages/ui/src/lib/state/useRouteSearch.ts`)
281
+ * is where `'pulsechain'`/`'tokensex'` ever appear in this check — never in
282
+ * this package.
283
+ *
284
+ * @param nextHop - the carrier and destination chain of the hop immediately following this one
285
+ * @returns whether this hop's signature also covers the next one
286
+ */
287
+ readonly fusesWithNext?: (nextHop: CarrierFusesWithNextOptions) => boolean;
288
+ };
289
+ /**
290
+ * A built-in carrier this library ships: LI.FI, Relay, or NEAR Intents.
291
+ *
292
+ * THE SAME THING AS A {@link Carrier}, NOT A SECOND VOCABULARY. Before this
293
+ * type existed, `bridge-sdk`'s `providers.ts` used `ProviderQuoteContext` to
294
+ * describe what a provider needs beyond the route request, and the
295
+ * route-search engine used `AskEdge`/`HopCarrier` to describe the same
296
+ * question in different words — two names for "something that answers what
297
+ * an edge delivers." `ProviderAsker` is a plain alias for `Carrier` whose
298
+ * `id` happens to be a {@link ProviderId}, so a `Carrier & { id: ProviderId }`
299
+ * and a `ProviderAsker` are interchangeable: a later step's `UpstreamFunnel`
300
+ * wraps either one identically, and a caller never has to know which kind it
301
+ * holds.
302
+ */
303
+ export type ProviderAsker = Carrier & {
304
+ readonly id: ProviderId;
305
+ };
306
+ /**
307
+ * Wraps a {@link Carrier}'s own `ask` so every network call it makes passes
308
+ * through this library's shared guards before it ever reaches `fetch`: the
309
+ * keyed concurrency limiter and Retry-After cooldown (`quote-guards.ts`), and
310
+ * the host's `neverSendChainIds` tripwire (`QuoteClientConfig`). One funnel
311
+ * wraps every carrier a client holds, so the guards govern the WHOLE client
312
+ * rather than being left to whichever carrier happens to remember to call
313
+ * them — the same reasoning `routing/search/waves.ts`'s own doc gives for
314
+ * keeping its concurrency limiter internal rather than trusting each caller
315
+ * to apply one.
316
+ *
317
+ * A FREE CARRIER (`isRateLimited: false`) IS RETURNED UNCHANGED, except for
318
+ * the chain-guard check, which applies to every carrier — a local read that
319
+ * happened to be pointed at a guarded chain is exactly as dangerous as a
320
+ * network call to one.
321
+ */
322
+ export type UpstreamFunnel = {
323
+ /**
324
+ * Wraps one carrier, returning a carrier whose `ask` goes through every
325
+ * shared guard. The returned carrier's `id`, `edgesFrom` and
326
+ * `isRateLimited` are unchanged; only `ask` is wrapped.
327
+ *
328
+ * @param carrier - the carrier to wrap
329
+ * @returns a carrier with the same identity, guarded `ask`
330
+ */
331
+ readonly wrap: (carrier: Carrier) => Carrier;
332
+ };
333
+ /** One cached display-quote answer, with the bookkeeping needed to compute `ageMs` and `cache` on a later read. */
334
+ export type AnswerCacheEntry = {
335
+ readonly answer: DisplayProviderSlot;
336
+ /** Epoch milliseconds this entry was recorded — the basis for `ageMs` and time-to-live expiry. */
337
+ readonly recordedAtMs: number;
338
+ };
339
+ /**
340
+ * The display-quote endpoint's shared cache: keyed by
341
+ * `quote-guards.ts`'s `displayQuoteCacheKey`, single-flight over identical
342
+ * concurrent requests, least-recently-used eviction around 10,000 entries —
343
+ * see `docs/quote-service.md`'s "Cache" section for the exact policy a
344
+ * concrete implementation must follow.
345
+ */
346
+ export type AnswerCache = {
347
+ /**
348
+ * Reads a cached entry, or null when there is none or it has aged out
349
+ * past the caller-supplied time-to-live for its {@link DisplayProviderSlot}
350
+ * status (30 s for `quoted`/`no-quote`, 5 s for `could-not-ask`, until the
351
+ * next chain-list refresh for `cannot-carry`).
352
+ *
353
+ * @param key - a {@link displayQuoteCacheKey} result
354
+ * @returns the entry, or null
355
+ */
356
+ readonly get: (key: string) => AnswerCacheEntry | null;
357
+ /**
358
+ * Records an answer under `key`, evicting the least-recently-used entry
359
+ * first when the cache is already at capacity.
360
+ *
361
+ * @param key - a {@link displayQuoteCacheKey} result
362
+ * @param answer - the answer to cache
363
+ * @param recordedAtMs - when it was recorded
364
+ */
365
+ readonly set: (key: string, answer: DisplayProviderSlot, recordedAtMs: number) => void;
366
+ /**
367
+ * Runs `task` at most once per `key` among concurrent callers: the first
368
+ * caller under a key runs `task`; every concurrent caller under the SAME
369
+ * key joins that one call rather than starting a second. Implements
370
+ * `docs/quote-service.md`'s "Cache" section: "identical concurrent
371
+ * requests wait on one upstream call," and "a client disconnect never
372
+ * cancels the shared call."
373
+ *
374
+ * @param key - a {@link displayQuoteCacheKey} result
375
+ * @param task - the upstream call to make, if nobody else is already making it
376
+ * @returns the answer, and whether this caller joined another's in-flight
377
+ * call rather than starting its own
378
+ */
379
+ readonly joinOrRun: (key: string, task: () => Promise<DisplayProviderSlot>) => Promise<{
380
+ readonly answer: DisplayProviderSlot;
381
+ readonly joined: boolean;
382
+ }>;
383
+ };
384
+ /** What a LI.FI carrier needs beyond the route request — every field a host's own, never a default this library supplies. */
385
+ export type LifiProviderConfig = {
386
+ /** Forwarded as the `x-lifi-api-key` header, when set. */
387
+ readonly apiKey?: string;
388
+ /** Forwarded as the `integrator` query parameter LI.FI attributes traffic and referral fees to. */
389
+ readonly integrator?: string;
390
+ /** Overrides LI.FI's own API root, for a host proxying it under its own domain. */
391
+ readonly baseUrl?: string;
392
+ /** Referral fee as a decimal fraction (e.g. `0.001` = 0.1%), forwarded as `fee` when positive. */
393
+ readonly fee?: number;
394
+ };
395
+ /** What a Relay carrier needs beyond the route request. */
396
+ export type RelayProviderConfig = {
397
+ /** Forwarded as the `x-api-key` header, when set. */
398
+ readonly apiKey?: string;
399
+ /** Forwarded as `referrer` on every request. */
400
+ readonly referrer?: string;
401
+ /** Overrides Relay's own API root. */
402
+ readonly baseUrl?: string;
403
+ };
404
+ /** What a NEAR Intents carrier needs beyond the route request. */
405
+ export type NearIntentsProviderConfig = {
406
+ /** Forwarded as `X-API-Key`, when set — NEAR Intents' own partner key. */
407
+ readonly apiKey?: string;
408
+ /** Forwarded as `referral` on every quote request. */
409
+ readonly referral?: string;
410
+ /** Overrides NEAR Intents' own API root. */
411
+ readonly baseUrl?: string;
412
+ };
413
+ /** Every built-in carrier's own configuration, all optional — a host wires only the providers it uses. */
414
+ export type QuoteClientProvidersConfig = {
415
+ readonly lifi?: LifiProviderConfig;
416
+ readonly relay?: RelayProviderConfig;
417
+ readonly nearIntents?: NearIntentsProviderConfig;
418
+ };
419
+ /**
420
+ * How a {@link QuoteClientConfig} behaves when its own `serviceUrl` cannot be
421
+ * reached: `'direct'` runs the same built-in carriers against the providers
422
+ * directly (the browser fallback `docs/quote-service.md`'s "Decisions"
423
+ * section describes); `'none'` surfaces the failure instead of ever calling
424
+ * a provider straight from this runtime — the right choice for a server that
425
+ * IS the service, which has no "direct" to fall back to beyond itself.
426
+ */
427
+ export type QuoteClientFallbackMode = 'direct' | 'none';
428
+ /** A host's own limits, layered over this library's neutral defaults. */
429
+ export type QuoteClientLimitsConfig = {
430
+ /** Per-carrier in-flight request cap, keyed by {@link CarrierId}. Falls back to the carrier's own `concurrencyLimit` when a carrier is not named here. */
431
+ readonly concurrencyByCarrier?: Readonly<Record<CarrierId, number>>;
432
+ /** Overrides `quote-guards.ts`'s `DEFAULT_RETRY_AFTER_COOLDOWN_MS` (30 s). */
433
+ readonly defaultRetryAfterMs?: number;
434
+ };
435
+ /** A host's own cache sizing, layered over this library's neutral defaults. */
436
+ export type QuoteClientCacheConfig = {
437
+ /** Overrides the answer time-to-live, in milliseconds, for a `quoted`/`no-quote` entry. */
438
+ readonly ttlMs?: number;
439
+ /** Overrides the least-recently-used eviction size (about 10,000 entries by default). */
440
+ readonly maxEntries?: number;
441
+ };
442
+ /**
443
+ * The one configuration object a host builds this library's client from.
444
+ * Nothing about a host — its name, its allowed origins, its chain policy,
445
+ * its keys — lives anywhere else in this package; it all arrives here.
446
+ *
447
+ * THREE MODES, ONE SHAPE. See `docs/quote-service.md`'s "Library" section
448
+ * for the full description of each:
449
+ *
450
+ * - **Backend** — `providers` carries real secrets (`apiKey`s); this config
451
+ * builds the same client the display-quote service's server process runs.
452
+ * - **Frontend, direct** — `providers` carries no secret (or only a public,
453
+ * rate-limit-budget identifier, exactly as today's browser bundle already
454
+ * does for Relay); `fallback: 'none'` or `serviceUrl` absent, so the
455
+ * client calls providers straight from the browser.
456
+ * - **Frontend, via a service with a fallback** — `serviceUrl` names a host's
457
+ * own display-quote service; `fallback: 'direct'` runs the same built-in
458
+ * carriers straight from the browser when that service cannot be reached,
459
+ * under `providers`' own (public) values.
460
+ *
461
+ * @see LifiProviderConfig
462
+ * @see RelayProviderConfig
463
+ * @see NearIntentsProviderConfig
464
+ */
465
+ export type QuoteClientConfig = {
466
+ /** Every built-in carrier's own configuration. */
467
+ readonly providers?: QuoteClientProvidersConfig;
468
+ /** A host's own display-quote service, when this client should prefer it over asking providers directly. */
469
+ readonly serviceUrl?: string;
470
+ /** What to do when `serviceUrl` is set but unreachable. Required, so a caller always states its intent rather than inheriting a silent default. */
471
+ readonly fallback: QuoteClientFallbackMode;
472
+ /**
473
+ * A server-held address this host controls, per ecosystem, that a
474
+ * pre-quote can name as its own recipient without exposing a real user
475
+ * address — see `docs/quote-service.md`'s "Endpoint" section, "a
476
+ * server-held probe address." A host supplies one per ecosystem it wants
477
+ * to quote non-Ethereum origins for; an ecosystem with none configured is
478
+ * simply not quoted for that origin.
479
+ */
480
+ readonly probeAddresses?: Readonly<Partial<Record<Ecosystem, string>>>;
481
+ /**
482
+ * Chain ids no request this client builds may ever name, in an address or
483
+ * a request body, including a forwarding path — the tripwire
484
+ * `docs/quote-service.md`'s "Rules that never bend" section requires. A
485
+ * host with no such restriction passes an empty array or omits the field;
486
+ * this library enforces nothing on its own, because it does not know any
487
+ * host's chain ids to forbid.
488
+ */
489
+ readonly neverSendChainIds?: readonly number[];
490
+ readonly cache?: QuoteClientCacheConfig;
491
+ readonly limits?: QuoteClientLimitsConfig;
492
+ /**
493
+ * The `fetch` implementation every carrier calls through. Defaults to the
494
+ * runtime global when omitted; a host — or a test — that needs a
495
+ * different one (logging, a fixture replay, a Node runtime with no global
496
+ * fetch) supplies its own. Never imported from `node:*`, so this stays
497
+ * isomorphic.
498
+ */
499
+ readonly fetch?: typeof fetch;
500
+ /** The clock every time-sensitive guard reads. Defaults to `Date.now` when omitted; a test supplies its own. */
501
+ readonly now?: () => number;
502
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,126 @@
1
+ import type { CarrierId, UpstreamFunnel } from './types.js';
2
+ /**
3
+ * The single choke point every outbound network call a {@link Carrier} makes
4
+ * passes through — see `docs/quote-service.md`'s "Carriers" section for the
5
+ * narrative and `types.ts`'s {@link UpstreamFunnel} for the contract this
6
+ * module implements.
7
+ *
8
+ * TWO LAYERS, BECAUSE `Carrier.ask` HIDES THE WIRE. `UpstreamFunnel.wrap`
9
+ * only sees a {@link CarrierAskRequest} (an edge and an amount) and a
10
+ * {@link CarrierAskAnswer} (delivered/refusal) — never a URL, a request body,
11
+ * or a response header, because that is exactly what makes `Carrier` usable
12
+ * for a free local read as well as a network call. The guards this module
13
+ * must enforce — the Retry-After cooldown, the 401-code-1010 breaker, and
14
+ * the chain tripwire's URL/body scan — all need that wire-level view. So
15
+ * this module builds it in two pieces:
16
+ *
17
+ * - {@link createUpstreamFunnel}'s `wrap` applies the STRUCTURAL guards every
18
+ * `Carrier.ask` call can be judged on without touching the network: the
19
+ * chain-id tripwire read off the edge itself, the per-carrier concurrency
20
+ * cap, and refusing locally while a carrier's cooldown is active.
21
+ * - Its `guardedFetch` is the WIRE-level guard: every built-in carrier
22
+ * (`carriers/lifi.ts`, `carriers/relay.ts`, `carriers/near-intents.ts`)
23
+ * must call it — bound to its own {@link CarrierId} — instead of calling
24
+ * `fetch` directly. It re-runs the chain tripwire against the actual
25
+ * outgoing URL and body (the backstop `docs/quote-service.md` calls for:
26
+ * "rejects any 369/943 in the address or body"), records a `429`'s
27
+ * `Retry-After` into the SAME cooldown `wrap` reads, and — for `'lifi'`
28
+ * only — arms the five-minute breaker on a `401` carrying LI.FI's
29
+ * `code: 1010`.
30
+ *
31
+ * A caller wires the two together: build the funnel first, construct each
32
+ * carrier with `(input, init) => funnel.guardedFetch(carrierId, input, init)`
33
+ * as its `fetchImpl`, THEN call `funnel.wrap(carrier)` on the result. Skipping
34
+ * either half leaves a real gap — `wrap` alone never sees a 429 to cool down
35
+ * from, and `guardedFetch` alone never caps concurrency.
36
+ */
37
+ /** Every built-in carrier's own concurrency cap, unless `concurrencyByCarrier` overrides it. */
38
+ export declare const DEFAULT_CARRIER_CONCURRENCY = 4;
39
+ /** NEAR Intents' own default concurrency cap — see `docs/quote-service.md`'s "Guards" section: "4, NEAR 3." */
40
+ export declare const NEAR_INTENTS_CONCURRENCY = 3;
41
+ /** Per-provider defaults, read when `concurrencyByCarrier` names no override for a carrier. */
42
+ export declare const DEFAULT_CONCURRENCY_BY_CARRIER: Readonly<Record<string, number>>;
43
+ /** How long the LI.FI carrier is refused after a `401` carrying `code: 1010` — five minutes, per `docs/quote-service.md`'s "Cache" section. */
44
+ export declare const LIFI_INVALID_API_KEY_BREAKER_MS: number;
45
+ /**
46
+ * Thrown by {@link GuardedFetch} BEFORE any network call, when the outgoing
47
+ * request names a chain id the host listed in `neverSendChainIds` — either in
48
+ * a query parameter or inside the JSON body, under one of the structured
49
+ * chain-id field names this module reads (never a raw string search over the
50
+ * whole request, which would also match an amount or an address that merely
51
+ * CONTAINS the digits).
52
+ */
53
+ export declare class NeverSendChainIdError extends Error {
54
+ readonly chainId: number;
55
+ readonly carrierId: CarrierId;
56
+ constructor(chainId: number, carrierId: CarrierId);
57
+ }
58
+ /**
59
+ * Thrown by {@link GuardedFetch} when a configured provider key would
60
+ * otherwise appear in the outgoing URL or body — `docs/quote-service.md`'s
61
+ * rule, "keys go only into headers, never URLs or bodies, never logs,"
62
+ * enforced rather than merely documented.
63
+ */
64
+ export declare class KeyLeakError extends Error {
65
+ readonly carrierId: CarrierId;
66
+ constructor(carrierId: CarrierId);
67
+ }
68
+ /**
69
+ * Thrown by {@link GuardedFetch} for `'lifi'` while its five-minute breaker
70
+ * is active — see {@link LIFI_INVALID_API_KEY_BREAKER_MS}. Never thrown for
71
+ * any other carrier; only LI.FI answers a rejected key with a 401 this
72
+ * module can classify (`isLifiInvalidApiKeyResponse`).
73
+ */
74
+ export declare class LifiApiKeyBreakerActiveError extends Error {
75
+ constructor();
76
+ }
77
+ /**
78
+ * A `fetch` every built-in carrier's own network call must go through,
79
+ * keyed by which carrier is calling — see this module's head note for why a
80
+ * carrier id, rather than a plain `typeof fetch`, is required here.
81
+ */
82
+ export type GuardedFetch = (carrierId: CarrierId, input: string | URL, init?: RequestInit) => Promise<Response>;
83
+ export type CreateUpstreamFunnelOptions = {
84
+ /** Chain ids no request this funnel guards may ever name — see `QuoteClientConfig.neverSendChainIds`. */
85
+ readonly neverSendChainIds?: readonly number[];
86
+ /** Every configured provider key, checked against the outgoing URL and body so a key never travels anywhere but a header. */
87
+ readonly keys?: readonly string[];
88
+ /** Per-carrier in-flight cap, overriding {@link DEFAULT_CONCURRENCY_BY_CARRIER}. */
89
+ readonly concurrencyByCarrier?: Readonly<Record<CarrierId, number>>;
90
+ /** Overrides `quote-guards.ts`'s `DEFAULT_RETRY_AFTER_COOLDOWN_MS` for a carrier's own Retry-After cooldown. */
91
+ readonly defaultRetryAfterMs?: number;
92
+ /**
93
+ * How long, from the moment an `ask` call is queued, it may wait for a
94
+ * concurrency slot before {@link createUpstreamFunnel}'s `wrap` answers
95
+ * `could-not-ask: busy` instead. Omitted waits indefinitely, matching
96
+ * `KeyedConcurrencyLimiter.run`'s own default.
97
+ */
98
+ readonly askDeadlineMs?: number;
99
+ readonly fetch?: typeof fetch;
100
+ readonly now?: () => number;
101
+ };
102
+ /** {@link UpstreamFunnel}, widened with the wire-level {@link GuardedFetch} every built-in carrier is constructed with. */
103
+ export type QuoteUpstreamFunnel = UpstreamFunnel & {
104
+ readonly guardedFetch: GuardedFetch;
105
+ /**
106
+ * Total `429` responses `guardedFetch` has seen across every carrier since
107
+ * this funnel was built — the figure `/stats`'s `rateLimited429s` reports,
108
+ * per `docs/quote-service.md`'s "Endpoint" section. Counts the response,
109
+ * not the cooldown it starts, so a burst of calls that all land during one
110
+ * already-active cooldown (refused locally by `wrap`, never reaching
111
+ * `guardedFetch`) does not inflate it.
112
+ */
113
+ readonly rateLimited429Count: () => number;
114
+ /** Every carrier's own queued-task count, summed — see `quote-guards.ts`'s `KeyedConcurrencyLimiter.queueDepth`. */
115
+ readonly queueDepth: () => number;
116
+ };
117
+ /**
118
+ * Builds the funnel every carrier's `ask` — built-in or host-registered —
119
+ * should be wrapped through, and the wire-level {@link GuardedFetch} every
120
+ * built-in carrier's own HTTP call should go through. See this module's
121
+ * head note for how the two fit together.
122
+ *
123
+ * @param options - see {@link CreateUpstreamFunnelOptions}
124
+ * @returns the funnel
125
+ */
126
+ export declare const createUpstreamFunnel: (options?: CreateUpstreamFunnelOptions) => QuoteUpstreamFunnel;