@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,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
|
+
};
|