@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,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[];
|