@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,247 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * The wire contract for `POST /v1/display-quotes` — the one endpoint a
4
+ * display-quote service (or a browser calling providers directly under the
5
+ * same contract) answers. See `docs/quote-service.md`'s "Endpoint" and
6
+ * "Cache" sections, which this module implements exactly.
7
+ *
8
+ * NOTHING HERE NAMES A HOST, A KEY, OR A CHAIN. This module carries the
9
+ * SHAPE of a request and an answer, never a fact about who asks it or what
10
+ * they ask about — see `docs/quote-service.md`'s "Library" section for why:
11
+ * this package runs for any host, with its own providers, its own chains and
12
+ * its own tripwires, all supplied through a configuration object a later
13
+ * module in this package defines.
14
+ */
15
+ // ---------------------------------------------------------------------------
16
+ // Base-units amounts: the bigint <-> decimal-string wire codec
17
+ // ---------------------------------------------------------------------------
18
+ /**
19
+ * A decimal string of digits only — the wire spelling of a base-units
20
+ * amount. No sign, no decimal point, no leading `0x`: `BigInt` reads it
21
+ * directly.
22
+ */
23
+ export const baseUnitsAmountSchema = z
24
+ .string()
25
+ .regex(/^\d+$/, 'must be a decimal, non-negative base-units amount');
26
+ /**
27
+ * A decimal-string amount that answers EXACTLY the amount it was asked for
28
+ * — a genuine, un-scaled reply, safe to hand to an execution builder as the
29
+ * delivery target. Branded so it cannot be confused with {@link EstimatedDelivery}
30
+ * at the type level, even though both are, at runtime, the same shape of
31
+ * string.
32
+ *
33
+ * See `quote-service-contract.test-d.ts` for the type-level proof this
34
+ * brand exists to make possible: an {@link EstimatedDelivery} cannot reach
35
+ * {@link decodeExactAmount}, and therefore cannot reach a bigint-typed
36
+ * execution field such as `ConsolidatedQuoteRequest.toAmount`.
37
+ */
38
+ export const exactAmountSchema = baseUnitsAmountSchema.brand();
39
+ /**
40
+ * A decimal-string amount SCALED from a nearby, already-cached amount's
41
+ * real answer — never the provider's own answer for the amount asked. See
42
+ * `docs/quote-service.md`'s "Rules that never bend": "A figure from a
43
+ * nearby amount is never presented as the user's exact quote."
44
+ *
45
+ * THIS IS THE BRAND `docs/quote-service.md` CALLS FOR BY NAME. It exists so
46
+ * a scaled figure is a DIFFERENT TYPE from an exact one, not merely a
47
+ * different runtime value that happens to carry the same shape — the two
48
+ * are interchangeable as plain strings, which is exactly the danger: a
49
+ * refactor that quietly widens a parameter from {@link ExactAmount} to
50
+ * `string` would let a scaled estimate slip through unnoticed. Kept
51
+ * distinct, `decodeExactAmount` refuses to compile against this brand, and
52
+ * nothing that only accepts {@link ExactAmount} can be handed one.
53
+ */
54
+ export const estimatedDeliverySchema = baseUnitsAmountSchema.brand();
55
+ /**
56
+ * Encodes a bigint amount as the wire's plain decimal-string form. The
57
+ * inverse of {@link decodeBaseUnitsAmount}.
58
+ *
59
+ * @param amount - a non-negative amount in base units
60
+ * @returns the decimal-string wire spelling
61
+ */
62
+ export const encodeBaseUnitsAmount = (amount) => amount.toString();
63
+ /**
64
+ * Decodes a wire decimal-string amount into bigint base units.
65
+ *
66
+ * @param amount - the wire string
67
+ * @returns the bigint value
68
+ * @throws when `amount` is not a plain, non-negative decimal integer string
69
+ */
70
+ export const decodeBaseUnitsAmount = (amount) => {
71
+ const parsed = baseUnitsAmountSchema.safeParse(amount);
72
+ if (!parsed.success) {
73
+ throw new Error(`not a base-units amount: ${amount}`);
74
+ }
75
+ return BigInt(parsed.data);
76
+ };
77
+ /**
78
+ * Marks a bigint as an ask's genuine, EXACT answer, ready for the wire.
79
+ *
80
+ * @param amount - the exact delivered (or minimum) amount, in base units
81
+ * @returns the branded {@link ExactAmount}
82
+ */
83
+ export const toExactAmount = (amount) => exactAmountSchema.parse(encodeBaseUnitsAmount(amount));
84
+ /**
85
+ * Marks a bigint as a SCALED estimate, ready for the wire — never a
86
+ * genuine answer to the amount actually asked.
87
+ *
88
+ * @param amount - the scaled delivered (or minimum) amount, in base units
89
+ * @returns the branded {@link EstimatedDelivery}
90
+ */
91
+ export const toEstimatedDelivery = (amount) => estimatedDeliverySchema.parse(encodeBaseUnitsAmount(amount));
92
+ /**
93
+ * Decodes an amount an execution builder may spend as an EXACT figure.
94
+ *
95
+ * Only {@link ExactAmount} decodes here. Passing an {@link EstimatedDelivery}
96
+ * is a compile-time error — see `quote-service-contract.test-d.ts` — which is
97
+ * what makes `docs/quote-service.md`'s rule ("a scaled figure is unsafe" for
98
+ * `precision: 'exact'`) impossible to violate by accident rather than merely
99
+ * documented.
100
+ *
101
+ * @param amount - a branded exact amount
102
+ * @returns the bigint value
103
+ */
104
+ export const decodeExactAmount = (amount) => decodeBaseUnitsAmount(amount);
105
+ // ---------------------------------------------------------------------------
106
+ // The request: POST /v1/display-quotes
107
+ // ---------------------------------------------------------------------------
108
+ const providerIdSchema = z.enum(['lifi', 'relay', 'near-intents']);
109
+ /**
110
+ * A chain-and-token pair a {@link DisplayAsk} names on one side of the
111
+ * transfer.
112
+ */
113
+ export const assetRefSchema = z
114
+ .object({
115
+ chainId: z.number().int().positive(),
116
+ token: z.string().min(1),
117
+ })
118
+ .strict();
119
+ /**
120
+ * One question inside a display-quote request: what every eligible
121
+ * provider expects to deliver for `amount` of `from.token` sent as
122
+ * `to.token`.
123
+ *
124
+ * See `docs/quote-service.md`'s "Endpoint" section for the field-by-field
125
+ * contract this schema encodes exactly.
126
+ */
127
+ export const displayAskSchema = z
128
+ .object({
129
+ from: assetRefSchema,
130
+ to: assetRefSchema,
131
+ /** Decimal string, base units of `from.token`. */
132
+ amount: baseUnitsAmountSchema,
133
+ /**
134
+ * Defaults to the ecosystem `to.chainId` belongs to when omitted — see
135
+ * `ecosystemByChainId` in `@gibs/bridge-sdk`.
136
+ */
137
+ recipientEcosystem: z
138
+ .enum(['evm', 'svm', 'bvm', 'tvm', 'tonvm', 'hypevm', 'zcash'])
139
+ .optional(),
140
+ /** Defaults to every provider eligible for this pair when omitted. */
141
+ providers: z.array(providerIdSchema).min(1).optional(),
142
+ /** Defaults to `'estimate-ok'` when omitted. */
143
+ precision: z.enum(['estimate-ok', 'exact']).optional(),
144
+ })
145
+ .strict();
146
+ /** The body of `POST /v1/display-quotes`: 1 to 24 asks, answered in order. */
147
+ export const displayQuoteRequestSchema = z
148
+ .object({
149
+ asks: z.array(displayAskSchema).min(1).max(24),
150
+ })
151
+ .strict();
152
+ // ---------------------------------------------------------------------------
153
+ // The response: one answer per ask, one slot per provider that was asked
154
+ // ---------------------------------------------------------------------------
155
+ /** Why a `no-quote`, `could-not-ask`, `cannot-carry` or `invalid-api-key` slot answered as it did. Shared with `quote-refusals.ts`'s {@link QuoteRefusalKind}. */
156
+ const quoteRefusalKindSchema = z.enum(['no-quote', 'could-not-ask', 'cannot-carry', 'invalid-api-key']);
157
+ /** Where an answer came from, relative to this service's cache. */
158
+ export const displayAnswerCacheStateSchema = z.enum(['miss', 'hit', 'joined']);
159
+ const quotedDisplayAnswerCommonShape = {
160
+ status: z.literal('quoted'),
161
+ /**
162
+ * The amount this answer is FOR: always the asker's own requested amount
163
+ * (`DisplayAsk.amount`), never the nearby amount a scaled answer was
164
+ * actually derived from — that pair lives in `scaledFrom` below.
165
+ */
166
+ askedAmount: baseUnitsAmountSchema,
167
+ /** When the underlying provider answer was recorded, ISO 8601. */
168
+ quotedAt: z.string(),
169
+ /** Milliseconds between `quotedAt` and now. */
170
+ ageMs: z.number().nonnegative(),
171
+ cache: displayAnswerCacheStateSchema,
172
+ };
173
+ /** A genuine answer to exactly the amount asked. */
174
+ export const exactQuotedDisplayAnswerSchema = z
175
+ .object({
176
+ ...quotedDisplayAnswerCommonShape,
177
+ basis: z.literal('exact'),
178
+ expectedAmount: exactAmountSchema,
179
+ minimumAmount: exactAmountSchema.optional(),
180
+ })
181
+ .strict();
182
+ /**
183
+ * An answer SCALED from a nearby cached amount's real answer — see
184
+ * `docs/quote-service.md`'s "Cache" section, "Scaling". `scaledFrom` carries
185
+ * the RAW pair the scaling was computed from (the amount actually asked
186
+ * upstream, and its own unscaled answer), so a client can audit or
187
+ * re-derive the figure; `expectedAmount`/`minimumAmount` are the SCALED
188
+ * estimate for this ask's own `askedAmount`.
189
+ */
190
+ export const scaledQuotedDisplayAnswerSchema = z
191
+ .object({
192
+ ...quotedDisplayAnswerCommonShape,
193
+ basis: z.literal('scaled-from-nearby-amount'),
194
+ expectedAmount: estimatedDeliverySchema,
195
+ minimumAmount: estimatedDeliverySchema.optional(),
196
+ scaledFrom: z
197
+ .object({
198
+ askedAmount: baseUnitsAmountSchema,
199
+ expectedAmount: baseUnitsAmountSchema,
200
+ })
201
+ .strict(),
202
+ })
203
+ .strict();
204
+ export const quotedDisplayAnswerSchema = z.union([
205
+ exactQuotedDisplayAnswerSchema,
206
+ scaledQuotedDisplayAnswerSchema,
207
+ ]);
208
+ /** The provider was asked and declined, could not be asked, or was never asked at all. */
209
+ export const refusedDisplayAnswerSchema = z
210
+ .object({
211
+ status: z.literal('refused'),
212
+ kind: quoteRefusalKindSchema,
213
+ reason: z.string().nullable(),
214
+ minimumAmount: baseUnitsAmountSchema.optional(),
215
+ })
216
+ .strict();
217
+ /** The provider's own call is still in flight; ask again after `retryAfterMs`. */
218
+ export const pendingDisplayAnswerSchema = z
219
+ .object({
220
+ status: z.literal('pending'),
221
+ retryAfterMs: z.number().nonnegative(),
222
+ })
223
+ .strict();
224
+ /** One provider's answer to one {@link DisplayAsk} — the three shapes `docs/quote-service.md`'s "Endpoint" section names. */
225
+ export const displayProviderSlotSchema = z.union([
226
+ quotedDisplayAnswerSchema,
227
+ refusedDisplayAnswerSchema,
228
+ pendingDisplayAnswerSchema,
229
+ ]);
230
+ /** Every provider slot answered for one {@link DisplayAsk}, keyed by provider id. */
231
+ export const displayAnswerSchema = z
232
+ .object({
233
+ providers: z
234
+ .object({
235
+ lifi: displayProviderSlotSchema.optional(),
236
+ relay: displayProviderSlotSchema.optional(),
237
+ 'near-intents': displayProviderSlotSchema.optional(),
238
+ })
239
+ .strict(),
240
+ })
241
+ .strict();
242
+ /** The body of a `POST /v1/display-quotes` response: one answer per ask, in ask order. */
243
+ export const displayQuoteResponseSchema = z
244
+ .object({
245
+ answers: z.array(displayAnswerSchema),
246
+ })
247
+ .strict();
@@ -0,0 +1,68 @@
1
+ import { type AssetNode } from '@gibs/bridge-sdk/routing';
2
+ import type { Carrier, CarrierFusesWithNextOptions, CarrierId } from '../types.js';
3
+ /**
4
+ * The route-search maximization engine's carrier-agnostic capability index:
5
+ * "which registered carriers connect this pair of assets, and does the
6
+ * carrier performing one hop fuse its signature with the next." See
7
+ * `docs/quote-service.md`'s "Carriers" section.
8
+ *
9
+ * NOTHING HERE NAMES A HOST'S OWN CARRIER. Every fact this module used to
10
+ * hard-code about LI.FI, Relay, NEAR Intents, the omnibridge crossing or
11
+ * PulseX now lives on the {@link Carrier} objects a caller registers —
12
+ * `carriersFrom` composes their own `edgesFrom`, and `fusesWithBridgeEntry`
13
+ * consults the acting carrier's own optional `fusesWithNext`. A host that
14
+ * wants "an executing aggregator fuses with its own bridge's entry call"
15
+ * (gibs's rule) supplies that as a `fusesWithNext` on the carrier IT
16
+ * registers (`packages/bridge-sdk/src/routing/carriers/`,
17
+ * `packages/ui/src/lib/state/useRouteSearch.ts`) — this module never learns
18
+ * what "the bridge" or "PulseChain" means.
19
+ */
20
+ /** What {@link CapabilityIndex.fusesWithBridgeEntry} needs to decide one fuse question. */
21
+ export type FusesWithBridgeEntryOptions = CarrierFusesWithNextOptions & {
22
+ /** The carrier performing the hop that might fuse with what follows it. */
23
+ readonly carrier: CarrierId;
24
+ };
25
+ /**
26
+ * What {@link buildCapabilityIndex} hands back: three total, pure lookups over
27
+ * the carriers it was built from.
28
+ */
29
+ export type CapabilityIndex = {
30
+ /**
31
+ * Every carrier id that can move funds directly from `from` to `to`, in
32
+ * registration order, deduped. Empty when nothing connects them, including
33
+ * when `from` and `to` name the same asset.
34
+ */
35
+ readonly carriersFrom: (from: AssetNode, to: AssetNode) => readonly CarrierId[];
36
+ /**
37
+ * Whether a hop travelling `options.carrier` can FUSE with a following
38
+ * hop, so the two cost the reader one signature instead of two — read off
39
+ * the ACTING carrier's own `fusesWithNext`, or `false` when it declares
40
+ * none (the correct default for a carrier that never executes a
41
+ * destination call on arrival).
42
+ */
43
+ readonly fusesWithBridgeEntry: (options: FusesWithBridgeEntryOptions) => boolean;
44
+ /**
45
+ * The full registered {@link Carrier} behind one id — its `depositMode`,
46
+ * `exact`, `estimate`, `sender`, `settlementSeconds` — or `undefined` when
47
+ * `carrierId` names no registered carrier. `enumerate.ts` is the one caller
48
+ * today: it reads `depositMode` to decide whether a carrier may fill a
49
+ * pay-by-hand origin's first hop (replacing a former hard-coded
50
+ * `carrier !== 'near-intents'` check), and reads `exact`/`estimate` to
51
+ * price a candidate hop without a network call before falling back to a
52
+ * caller-supplied price-feed estimate.
53
+ *
54
+ * SHOULD NEVER RETURN `undefined` FOR AN ID {@link carriersFrom} ITSELF
55
+ * RETURNED — both are built from the same registration map — but the
56
+ * return type stays optional because a caller can ask about ANY string,
57
+ * not only one this index has ever produced.
58
+ */
59
+ readonly carrierById: (carrierId: CarrierId) => Carrier | undefined;
60
+ };
61
+ /**
62
+ * Builds the pure {@link CapabilityIndex} the route search prunes against,
63
+ * from the carriers a caller registers.
64
+ *
65
+ * @param carriers - every carrier this search may use, built-in or a host's own
66
+ * @returns the index: `carriersFrom` and `fusesWithBridgeEntry`
67
+ */
68
+ export declare const buildCapabilityIndex: (carriers: readonly Carrier[]) => CapabilityIndex;
@@ -0,0 +1,28 @@
1
+ import { assetNodeKey } from '@gibs/bridge-sdk/routing';
2
+ /**
3
+ * Builds the pure {@link CapabilityIndex} the route search prunes against,
4
+ * from the carriers a caller registers.
5
+ *
6
+ * @param carriers - every carrier this search may use, built-in or a host's own
7
+ * @returns the index: `carriersFrom` and `fusesWithBridgeEntry`
8
+ */
9
+ export const buildCapabilityIndex = (carriers) => {
10
+ const byId = new Map(carriers.map((carrier) => [carrier.id, carrier]));
11
+ const carriersFrom = (from, to) => {
12
+ if (assetNodeKey(from) === assetNodeKey(to)) {
13
+ return [];
14
+ }
15
+ const ids = [];
16
+ for (const carrier of carriers) {
17
+ for (const edge of carrier.edgesFrom(from, to)) {
18
+ if (!ids.includes(edge.carrier)) {
19
+ ids.push(edge.carrier);
20
+ }
21
+ }
22
+ }
23
+ return ids;
24
+ };
25
+ const fusesWithBridgeEntry = ({ carrier, ...nextHop }) => byId.get(carrier)?.fusesWithNext?.(nextHop) ?? false;
26
+ const carrierById = (carrierId) => byId.get(carrierId);
27
+ return { carriersFrom, fusesWithBridgeEntry, carrierById };
28
+ };
@@ -0,0 +1,162 @@
1
+ import { type AssetNode, type EstimateBasis, type HopEdge, type HopFloor, type RoutePlan } from '@gibs/bridge-sdk/routing';
2
+ import type { CapabilityIndex } from './capabilities.js';
3
+ import type { CollectedRefusal } from './waves.js';
4
+ /**
5
+ * Depth-first search over a fixed node universe, pruning BEFORE anything is
6
+ * asked over the network (route search design, section 2). Everything here
7
+ * is pure: the node universe, the {@link CapabilityIndex}, the starting
8
+ * amount and the estimate function all arrive as arguments, and nothing is
9
+ * fetched — but a hop whose carrier answers through {@link Carrier.exact}
10
+ * (a synchronous LOCAL read, never a network call) carries a real `'quote'`
11
+ * outcome straight out of this module; every other hop carries an
12
+ * `'estimate'`, refined later by `waves.ts`'s live network asks.
13
+ *
14
+ * CARRIER-AGNOSTIC. Every prior version of this module special-cased ONE
15
+ * host's carriers directly (a hard-coded PulseX-only-runs-on-PulseChain
16
+ * check, and a hard-coded "only NEAR Intents may open a pay-by-hand
17
+ * origin"). Both are now the registering carrier's OWN job: `edgesFrom` only
18
+ * ever returns an edge for the pairs it can actually carry
19
+ * (`routing/carriers/pulsex.ts`'s own scoping, for gibs), and
20
+ * {@link Carrier.depositMode} states whether a carrier accepts a by-hand
21
+ * deposit — so this module never needs to know any carrier's NAME to enforce
22
+ * either rule, only the vocabulary every registered `Carrier` speaks.
23
+ */
24
+ /** Depth cap: at most this many hops in one plan (design section 2, item 3). */
25
+ export declare const DEFAULT_MAX_HOPS = 4;
26
+ /** Signature cap: at most this many signature groups in one plan. */
27
+ export declare const DEFAULT_MAX_SIGNATURES = 3;
28
+ /**
29
+ * A hard, absolute ceiling on how many finished plans one
30
+ * {@link enumerateRoutes} call collects — a SAFETY VALVE, not a ranking cut.
31
+ * The owner's own routing model is "iterate through each possible pathway
32
+ * and order them by output": every path within `maxHops`/`maxSignatures` is
33
+ * enumerated and returned, sorted, with nothing trimmed to a top-N slice.
34
+ * This constant exists only to bound the search on a node universe
35
+ * pathological enough that the hop/signature caps alone do not keep it
36
+ * finite in practice; hitting it is a signal to revisit the node universe or
37
+ * the caps, not evidence that ranking picked correctly among what remained.
38
+ */
39
+ export declare const MAX_ENUMERATED_PATHS = 10000;
40
+ /**
41
+ * What {@link enumerateRoutes} needs to search: the endpoints, the amount, the
42
+ * node universe to branch through, the capability index to prune against, and
43
+ * an optimistic per-hop estimate function the caller supplies.
44
+ *
45
+ * THIS MODULE DOES NOT DECIDE THE NODE UNIVERSE. Building the hub set is a
46
+ * host concern — see `CapabilityIndexInputs.lifiHubTokens`'s doc in the
47
+ * former `bridge-sdk` version of this file, whose "never resolve a token by
48
+ * matching its `symbol`" rule still governs whoever builds `nodes`. The
49
+ * caller builds `nodes` from its own address list; this module only ever
50
+ * walks the graph it is handed.
51
+ */
52
+ export type EnumerateRoutesOptions = {
53
+ /** The asset the reader is starting from. */
54
+ readonly origin: AssetNode;
55
+ /** The asset the reader wants delivered. */
56
+ readonly destination: AssetNode;
57
+ /** The reader's typed amount, in `origin`'s base units. */
58
+ readonly amount: bigint;
59
+ /**
60
+ * Every intermediate node the search may branch through. `origin` and
61
+ * `destination` are always available as endpoints whether or not this list
62
+ * repeats them.
63
+ */
64
+ readonly nodes: readonly AssetNode[];
65
+ /** The capability index to prune every candidate hop against. */
66
+ readonly capabilities: CapabilityIndex;
67
+ /**
68
+ * An OPTIMISTIC estimate of what one hop delivers, given what it consumes.
69
+ * Threaded through the search to (a) prune against a known floor and (b)
70
+ * rank finished plans — see design section 2.3: "price feed × fee priors,
71
+ * with the bridge fee and PulseX output computed exactly." This module
72
+ * invents no pricing rule of its own; it only calls the one it is given.
73
+ *
74
+ * RETURNS A BASIS ALONGSIDE THE AMOUNT. A price-feed estimate and a bare
75
+ * decimal rescale (no price known for one side) look identical as a
76
+ * bigint; the caller MUST say which one it produced, per
77
+ * {@link EstimateBasis}'s own doc, or this module has no way to keep an
78
+ * ungrounded number out of {@link compareRoutePlans}'s default ranking.
79
+ */
80
+ readonly estimateHopOutput: (edge: HopEdge, inputAmount: bigint) => {
81
+ readonly deliveredAmount: bigint;
82
+ readonly basis: EstimateBasis;
83
+ };
84
+ /**
85
+ * Minimum amounts already learned for a prospective edge, keyed by its
86
+ * carrier and endpoints. Defaults to "nothing known yet" — every edge this
87
+ * module produces on its own carries empty `floors`, matching
88
+ * `HopEdge.floors`'s own doc. `waves.ts` is what feeds a floor learned on
89
+ * one search into a LATER call's pruning step here.
90
+ */
91
+ readonly knownFloors?: (edge: Pick<HopEdge, 'carrier' | 'from' | 'to'>) => readonly HopFloor[];
92
+ /** Depth cap. Defaults to {@link DEFAULT_MAX_HOPS}. */
93
+ readonly maxHops?: number;
94
+ /** Signature cap. Defaults to {@link DEFAULT_MAX_SIGNATURES}. */
95
+ readonly maxSignatures?: number;
96
+ /**
97
+ * Called once for each distinct candidate hop (edge and input amount) a
98
+ * carrier's own {@link Carrier.exact} REFUSED during the walk — a local
99
+ * read saying "this amount cannot cross", with the carrier's reason and any
100
+ * minimum it names. The refused hop is still pruned, exactly as before;
101
+ * this only stops the reason from being thrown away.
102
+ *
103
+ * WHY IT EXISTS. A search whose only crossing is refused below its minimum
104
+ * returns no plan and, without this, no refusal either — indistinguishable
105
+ * from a search that never found the pair at all. The waves' own
106
+ * `refusals` list only ever held LIVE asks, so a floor read locally was the
107
+ * one refusal nobody could see. Omitted, nothing is reported.
108
+ */
109
+ readonly onExactRefusal?: (refusal: CollectedRefusal) => void;
110
+ /**
111
+ * How to rank two finished plans, best first. Defaults to
112
+ * {@link compareRoutePlansByScoreAdjustedDeliveredAmount} (`@gibs/bridge-sdk/routing`) —
113
+ * rankable beats unrankable regardless of raw magnitude, then the larger
114
+ * SIGNATURE-PENALTY-DISCOUNTED last-hop delivered amount wins within a
115
+ * group, so a plan needing an extra signature for a marginal gain does not
116
+ * automatically win the cut. PASS A DIFFERENT COMPARATOR ONLY WHEN IT
117
+ * STILL RESPECTS THE RANKABLE-BEATS-UNRANKABLE RULE: this default is the
118
+ * SAME function `waves.ts`'s own default imports, precisely so the
119
+ * rankability rule cannot drift out of sync between the two — see that
120
+ * function's own doc.
121
+ */
122
+ readonly compareRoutePlans?: (a: RoutePlan, b: RoutePlan) => number;
123
+ };
124
+ /**
125
+ * Depth-first search from `options.origin` to `options.destination` over
126
+ * `options.nodes`, pruning every candidate hop before it is added and
127
+ * collecting every finished plan the hop/signature caps allow, up to the
128
+ * {@link MAX_ENUMERATED_PATHS} safety valve.
129
+ *
130
+ * PRUNING, APPLIED BEFORE A CANDIDATE HOP IS EVER ADDED (design section 2,
131
+ * item 2):
132
+ * - the carrier must support both nodes ({@link CapabilityIndex.carriersFrom});
133
+ * - no node is revisited within one plan;
134
+ * - the same carrier never runs twice in a row, whatever venue each hop names
135
+ * (so one carrier never splits a hop across two of its own venues);
136
+ * - a pay-by-hand origin's first hop must be filled by a carrier whose
137
+ * {@link Carrier.depositMode} accepts a by-hand deposit — `'by-hand-deposit'`
138
+ * or `'either'`, never `'wallet-signed'` alone
139
+ * ({@link isDepositAddressChain} is the pay-by-hand test);
140
+ * - a hop that starts a DEFERRED leg (every signature group after the first)
141
+ * must start on an Ethereum-Virtual-Machine chain;
142
+ * - a known floor, if any, must be at or below the amount flowing into that
143
+ * hop;
144
+ * - the acting carrier's own {@link Carrier.exact}, when it has one, must not
145
+ * refuse the amount flowing into that hop ({@link resolveHopOutcome}); a
146
+ * refusal it does give is handed to `onExactRefusal`, once per edge and
147
+ * amount;
148
+ * - the plan must not exceed `maxHops` hops or `maxSignatures` signature
149
+ * groups (grouped with {@link groupHopsBySignature}, reused rather than
150
+ * re-derived, so a fused pair always counts as one signature here exactly
151
+ * as it does everywhere else that reads a `RoutePlan`).
152
+ *
153
+ * A HOP'S `fusesNext` CANNOT BE KNOWN WHEN IT IS BUILT — it depends on the hop
154
+ * that follows it, which does not exist yet in a depth-first walk. So each
155
+ * hop is built provisionally with `fusesNext: false`, and is replaced with an
156
+ * updated copy (never mutated) the moment its successor is chosen, once
157
+ * {@link CapabilityIndex.fusesWithBridgeEntry} can actually answer.
158
+ *
159
+ * @param options - the search's endpoints, node universe, capability index and estimate function
160
+ * @returns every plan the caps allow, best first, never exceeding {@link MAX_ENUMERATED_PATHS}
161
+ */
162
+ export declare const enumerateRoutes: (options: EnumerateRoutesOptions) => readonly RoutePlan[];