@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,284 @@
1
+ import { parseNearIntentsQuoteRefusal, } from '@gibs/bridge-sdk/near-intents';
2
+ /**
3
+ * Thrown by a provider transport when the provider answered without a quote.
4
+ *
5
+ * Carries the classified refusal so the caller can record WHICH provider said
6
+ * no and why, instead of a bare `Error` whose message is the only trace.
7
+ */
8
+ export class ProviderQuoteError extends Error {
9
+ refusal;
10
+ constructor(refusal) {
11
+ super(`${refusal.provider} gave no quote: ${refusal.reason ?? refusal.kind}`);
12
+ this.refusal = refusal;
13
+ this.name = 'ProviderQuoteError';
14
+ }
15
+ }
16
+ /**
17
+ * Thrown when no provider returned a quote, carrying every refusal.
18
+ *
19
+ * The message is unchanged from the plain error this replaces, because callers
20
+ * and tests match on it; the refusals are what is new.
21
+ */
22
+ export class NoProviderQuotedError extends Error {
23
+ refusals;
24
+ constructor(refusals) {
25
+ super('invalid consolidated quote request');
26
+ this.refusals = refusals;
27
+ this.name = 'NoProviderQuotedError';
28
+ }
29
+ }
30
+ /** Longest reason kept, so one verbose provider cannot flood a tooltip. */
31
+ const MAX_REASON_LENGTH = 160;
32
+ const asRecord = (value) => typeof value === 'object' && value !== null ? value : null;
33
+ const shorten = (text) => text.length <= MAX_REASON_LENGTH ? text : `${text.slice(0, MAX_REASON_LENGTH - 1)}…`;
34
+ /** The sentence one excluded LI.FI path carries: `reason` when filtered, `message` when failed. */
35
+ const pathReasonText = (record) => {
36
+ if (typeof record?.reason === 'string')
37
+ return record.reason;
38
+ if (typeof record?.message === 'string')
39
+ return record.message;
40
+ return null;
41
+ };
42
+ /**
43
+ * The most common reason among LI.FI's excluded paths, with its count.
44
+ *
45
+ * A LI.FI refusal lists every path it considered — measured at over a hundred
46
+ * for a small Base to Ethereum request — each with its own sentence, most of
47
+ * them identical but for the figures inside them. Grouping on the sentence
48
+ * with its numbers and parenthesised detail removed gives the one line that
49
+ * explains the refusal, rather than a hundred lines nobody reads.
50
+ *
51
+ * THE NORMALIZED KEY IS FOR COUNTING ONLY, NEVER FOR DISPLAY. It exists to
52
+ * tell two paths apart that differ only by amount; showing it to the reader
53
+ * showed a sentence with its numbers deleted — "amount ETH is below the
54
+ * minimum of ETH" — rather than an explanation. The text returned is always
55
+ * one of the ORIGINAL, unmutated sentences from the most common group.
56
+ */
57
+ const dominantLifiPathReason = (errors) => {
58
+ if (errors === null)
59
+ return null;
60
+ const entries = [
61
+ ...(Array.isArray(errors.filteredOut) ? errors.filteredOut : []),
62
+ ...(Array.isArray(errors.failed) ? errors.failed : []),
63
+ ];
64
+ const groups = new Map();
65
+ for (const entry of entries) {
66
+ const text = pathReasonText(asRecord(entry));
67
+ if (text === null)
68
+ continue;
69
+ const normalized = text.replace(/\([^)]*\)/g, '').replace(/\d+/g, '').replace(/\s+/g, ' ').trim();
70
+ if (normalized === '')
71
+ continue;
72
+ const group = groups.get(normalized);
73
+ if (group === undefined) {
74
+ groups.set(normalized, { count: 1, example: text });
75
+ continue;
76
+ }
77
+ group.count += 1;
78
+ }
79
+ let best = null;
80
+ for (const group of groups.values()) {
81
+ if (best === null || group.count > best.count)
82
+ best = group;
83
+ }
84
+ if (best === null)
85
+ return null;
86
+ return `${best.example} (${best.count} of ${entries.length} paths)`;
87
+ };
88
+ /**
89
+ * Whether a LI.FI response is specifically "the api key attached to this
90
+ * request was rejected" — a 401 carrying `code: 1010`, "Invalid API key".
91
+ * Measured live: this is the ONE 401 shape LI.FI answers with when the
92
+ * configured key is bad, distinct from a plain unauthenticated request
93
+ * (which LI.FI answers normally at a lower rate limit, never 401).
94
+ *
95
+ * Exported so every LI.FI response reader — not only the quote endpoints —
96
+ * can classify a failure the same way.
97
+ *
98
+ * @param status - the HTTP status
99
+ * @param body - the parsed JSON body, or null when it was unreadable
100
+ * @returns whether this response is LI.FI rejecting the configured key
101
+ */
102
+ export const isLifiInvalidApiKeyResponse = (status, body) => {
103
+ const record = asRecord(body);
104
+ return status === 401 && record?.code === 1010;
105
+ };
106
+ /**
107
+ * Classifies a non-success LI.FI answer.
108
+ *
109
+ * `code 1002` is the one "no" — see {@link QuoteRefusalKind}. A rejected api
110
+ * key ({@link isLifiInvalidApiKeyResponse}) is its own kind,
111
+ * `invalid-api-key`. Everything else, including a 404 carrying a different
112
+ * code, is a request that could not be made.
113
+ *
114
+ * PURE: this reader never logs and never records anything. A host that wants
115
+ * a rejected key remembered — so it stops retrying, or logs it once — reads
116
+ * `kind === 'invalid-api-key'` off the return value and records it itself.
117
+ *
118
+ * @param status - the HTTP status
119
+ * @param body - the parsed JSON body, or null when it was unreadable
120
+ * @returns the refusal
121
+ */
122
+ export const readLifiRefusal = (status, body) => {
123
+ const record = asRecord(body);
124
+ const message = typeof record?.message === 'string' ? record.message : null;
125
+ if (isLifiInvalidApiKeyResponse(status, body)) {
126
+ return {
127
+ provider: 'lifi',
128
+ kind: 'invalid-api-key',
129
+ reason: shorten(message ?? 'the configured LI.FI api key was rejected'),
130
+ };
131
+ }
132
+ if (record?.code === 1002) {
133
+ const dominant = dominantLifiPathReason(asRecord(record.errors));
134
+ const reason = [message, dominant].filter((part) => part !== null).join('. ');
135
+ return { provider: 'lifi', kind: 'no-quote', reason: reason === '' ? null : shorten(reason) };
136
+ }
137
+ return {
138
+ provider: 'lifi',
139
+ kind: 'could-not-ask',
140
+ reason: shorten(message ?? `the service answered ${status}`),
141
+ };
142
+ };
143
+ /**
144
+ * The two 4xx statuses that never mean "the provider considered this pair
145
+ * and said no" — they mean the QUESTION itself did not get through.
146
+ *
147
+ * 401 is a credentials failure and 429 is a rate limit; either can carry a
148
+ * body shaped exactly like a genuine decline (a `message` and an
149
+ * `errorCode`), because it is the same generic error envelope the API uses
150
+ * for every 4xx. Reading that envelope as a decline told the reader the
151
+ * provider had considered their transfer and refused it, when the truth is
152
+ * we never managed to ask.
153
+ */
154
+ const REQUEST_NEVER_REACHED_STATUSES = new Set([401, 429]);
155
+ /**
156
+ * Classifies a non-success Relay answer.
157
+ *
158
+ * Relay declines with a 4xx carrying an `errorCode` — measured:
159
+ * `{"message":"no routes found","errorCode":"NO_SWAP_ROUTES_FOUND"}`. A 4xx
160
+ * with a code is the provider saying no; a server error, an unauthenticated
161
+ * or rate-limited request (see {@link REQUEST_NEVER_REACHED_STATUSES}), or a
162
+ * body with no code, is a request that could not be made.
163
+ *
164
+ * @param status - the HTTP status
165
+ * @param body - the parsed JSON body, or null when it was unreadable
166
+ * @returns the refusal
167
+ */
168
+ export const readRelayRefusal = (status, body) => {
169
+ const record = asRecord(body);
170
+ const message = typeof record?.message === 'string' ? record.message : null;
171
+ const declined = status >= 400 &&
172
+ status < 500 &&
173
+ !REQUEST_NEVER_REACHED_STATUSES.has(status) &&
174
+ typeof record?.errorCode === 'string';
175
+ return {
176
+ provider: 'relay',
177
+ kind: declined ? 'no-quote' : 'could-not-ask',
178
+ reason: shorten(message ?? `the service answered ${status}`),
179
+ };
180
+ };
181
+ /**
182
+ * Turns a parsed {@link NearIntentsQuoteRefusal} into the plain sentence this
183
+ * surface shows for it.
184
+ *
185
+ * @param refusal - the parsed reason, or null when the body carried none
186
+ * @returns a plain-language description of the decline
187
+ */
188
+ const nearIntentsRefusalSentence = (refusal) => {
189
+ if (refusal === null) {
190
+ return 'the service declined the request without a readable reason';
191
+ }
192
+ switch (refusal.reason) {
193
+ case 'belowChainMinimum':
194
+ return `the amount is under this chain's temporary minimum of ${refusal.minimumUsd} United States dollars`;
195
+ case 'belowBridgeMinimum':
196
+ return `the amount is under the bridge's live minimum of ${refusal.minimumAmount} base units of the origin token`;
197
+ case 'noLiquidity':
198
+ return 'No liquidity available for this pair right now';
199
+ case 'other':
200
+ return 'the service declined the request for an unstated reason';
201
+ }
202
+ };
203
+ /**
204
+ * Classifies a declined NEAR Intents `POST /v0/quote` answer.
205
+ *
206
+ * WORTH ITS OWN READER, SEPARATE FROM {@link readLifiRefusal} AND
207
+ * {@link readRelayRefusal}. NEAR Intents' decline carries no HTTP status code
208
+ * of its own worth branching on — every decline this service gives arrives
209
+ * as a 400 with a `message` — so the classification work is entirely in
210
+ * READING that message, which `@gibs/bridge-sdk/near-intents`'s
211
+ * `parseNearIntentsQuoteRefusal` already does. EVERY readable reason is a
212
+ * `'no-quote'` — the service considered the pair and named a real,
213
+ * size-shaped reason it cannot carry it — and only an UNREADABLE body falls
214
+ * back to `'could-not-ask'`, matching this module's own rule that a request
215
+ * that never got an answer must never read as a "no."
216
+ *
217
+ * @param status - the HTTP status (informational only; NEAR Intents declines
218
+ * are read from the body, not the status)
219
+ * @param body - the parsed JSON body, or null when it was unreadable
220
+ * @returns the refusal
221
+ */
222
+ export const readNearIntentsRefusal = (status, body) => {
223
+ const refusal = parseNearIntentsQuoteRefusal(body);
224
+ if (refusal === null) {
225
+ return {
226
+ provider: 'near-intents',
227
+ kind: 'could-not-ask',
228
+ reason: shorten(`the service answered ${status} with no readable reason`),
229
+ };
230
+ }
231
+ return {
232
+ provider: 'near-intents',
233
+ kind: 'no-quote',
234
+ reason: shorten(nearIntentsRefusalSentence(refusal)),
235
+ ...(refusal.reason === 'belowBridgeMinimum' ? { minimumAmount: refusal.minimumAmount } : {}),
236
+ };
237
+ };
238
+ /**
239
+ * Turns one rejected provider call into a refusal, or null for an abort.
240
+ *
241
+ * An abort is not a refusal — the reader moved on and the question was
242
+ * withdrawn — so it is never reported. A {@link ProviderQuoteError} already
243
+ * carries its classification. Anything else thrown is a failure to ask.
244
+ *
245
+ * @param provider - the provider whose call rejected
246
+ * @param reason - the rejection value
247
+ * @param signal - the shared abort signal, to recognise a withdrawn question
248
+ * @returns the refusal, or null when the call was aborted
249
+ */
250
+ export const refusalFromRejection = (provider, reason, signal) => {
251
+ if (signal.aborted)
252
+ return null;
253
+ if (reason instanceof ProviderQuoteError)
254
+ return reason.refusal;
255
+ const message = reason instanceof Error ? reason.message : null;
256
+ return { provider, kind: 'could-not-ask', reason: message === null ? null : shorten(message) };
257
+ };
258
+ /**
259
+ * The refusals one route entry reports: the shared quote's, plus the
260
+ * separate two-step NEAR Intents question when that was asked.
261
+ *
262
+ * NEAR Intents is asked TWICE per entry, in two shapes. The shared quote asks
263
+ * it for a one-signature crossing, which it can never form — so it always
264
+ * answers `cannot-carry` there — while the two-step query asks it the
265
+ * question it can answer. When the two-step question was asked, its answer
266
+ * is NEAR Intents' real one, and the shared `cannot-carry` would contradict
267
+ * it.
268
+ *
269
+ * @param options.quoteRefusals - the shared quote's refusals
270
+ * @param options.twoStepAsked - whether the two-step question was asked
271
+ * @param options.twoStepError - the two-step query's error, or null
272
+ * @returns the entry's refusals, one per provider at most
273
+ */
274
+ export const mergeEntryRefusals = ({ quoteRefusals, twoStepAsked, twoStepError, }) => {
275
+ if (!twoStepAsked)
276
+ return quoteRefusals;
277
+ const withoutNear = quoteRefusals.filter((refusal) => refusal.provider !== 'near-intents');
278
+ if (twoStepError === null || twoStepError === undefined)
279
+ return withoutNear;
280
+ if (twoStepError instanceof ProviderQuoteError)
281
+ return [...withoutNear, twoStepError.refusal];
282
+ const message = twoStepError instanceof Error ? shorten(twoStepError.message) : null;
283
+ return [...withoutNear, { provider: 'near-intents', kind: 'could-not-ask', reason: message }];
284
+ };