@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,240 @@
|
|
|
1
|
+
import { ecosystemRegistry } from '@gibs/bridge-sdk/ecosystems';
|
|
2
|
+
import { decodeBaseUnitsAmount } from './quote-service-contract.js';
|
|
3
|
+
import { twoSignificantFigureAmountBucket } from './search/waves.js';
|
|
4
|
+
/**
|
|
5
|
+
* The shared upstream guards `docs/quote-service.md`'s "Guards" and "Cache"
|
|
6
|
+
* sections name: a keyed concurrency limiter with a deadline, a Retry-After
|
|
7
|
+
* cooldown, the display-quote cache key, and the bigint scaling formula. A
|
|
8
|
+
* host wraps every network call to a provider through these before it ever
|
|
9
|
+
* reaches `fetch` — see `types.ts`'s `UpstreamFunnel`.
|
|
10
|
+
*/
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Keyed concurrency limiter — per-key cap, FIFO queue, deadline -> busy
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
/**
|
|
15
|
+
* Thrown by {@link KeyedConcurrencyLimiter.run} when a slot under `key` does
|
|
16
|
+
* not open before the caller's deadline. The task is never started — a host
|
|
17
|
+
* catches this and answers `could-not-ask: busy` (`docs/quote-service.md`'s
|
|
18
|
+
* "Guards" section) rather than queuing indefinitely behind a saturated
|
|
19
|
+
* provider.
|
|
20
|
+
*/
|
|
21
|
+
export class ConcurrencyDeadlineExceededError extends Error {
|
|
22
|
+
key;
|
|
23
|
+
constructor(key) {
|
|
24
|
+
super(`no slot for "${key}" opened before the deadline`);
|
|
25
|
+
this.key = key;
|
|
26
|
+
this.name = 'ConcurrencyDeadlineExceededError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const scheduleTimer = (callback, delayMs) => {
|
|
30
|
+
const handle = setTimeout(callback, Math.max(0, delayMs));
|
|
31
|
+
// `unref` exists on Node's Timeout but not on the browser's numeric handle
|
|
32
|
+
// — isomorphic code checks for it rather than assuming either shape, so a
|
|
33
|
+
// pending deadline never keeps a Node process alive on its own.
|
|
34
|
+
if (typeof handle === 'object' && handle !== null && 'unref' in handle) {
|
|
35
|
+
handle.unref();
|
|
36
|
+
}
|
|
37
|
+
return () => clearTimeout(handle);
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Builds a fresh, empty {@link KeyedConcurrencyLimiter}.
|
|
41
|
+
*
|
|
42
|
+
* @returns the limiter
|
|
43
|
+
*/
|
|
44
|
+
export const createKeyedConcurrencyLimiter = () => {
|
|
45
|
+
const activeCountByKey = new Map();
|
|
46
|
+
const queueByKey = new Map();
|
|
47
|
+
const release = (key) => {
|
|
48
|
+
const active = activeCountByKey.get(key) ?? 0;
|
|
49
|
+
activeCountByKey.set(key, Math.max(0, active - 1));
|
|
50
|
+
const next = queueByKey.get(key)?.shift();
|
|
51
|
+
if (next !== undefined)
|
|
52
|
+
next();
|
|
53
|
+
};
|
|
54
|
+
const acquire = (key, limit, deadlineMs, now) => {
|
|
55
|
+
const active = activeCountByKey.get(key) ?? 0;
|
|
56
|
+
if (active < limit) {
|
|
57
|
+
activeCountByKey.set(key, active + 1);
|
|
58
|
+
return Promise.resolve();
|
|
59
|
+
}
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
let settled = false;
|
|
62
|
+
const grant = () => {
|
|
63
|
+
if (settled)
|
|
64
|
+
return;
|
|
65
|
+
settled = true;
|
|
66
|
+
cancelTimer?.();
|
|
67
|
+
activeCountByKey.set(key, (activeCountByKey.get(key) ?? 0) + 1);
|
|
68
|
+
resolve();
|
|
69
|
+
};
|
|
70
|
+
const queue = queueByKey.get(key) ?? [];
|
|
71
|
+
queue.push(grant);
|
|
72
|
+
queueByKey.set(key, queue);
|
|
73
|
+
const cancelTimer = deadlineMs === undefined
|
|
74
|
+
? undefined
|
|
75
|
+
: scheduleTimer(() => {
|
|
76
|
+
if (settled)
|
|
77
|
+
return;
|
|
78
|
+
settled = true;
|
|
79
|
+
const currentQueue = queueByKey.get(key);
|
|
80
|
+
const index = currentQueue?.indexOf(grant) ?? -1;
|
|
81
|
+
if (currentQueue !== undefined && index !== -1)
|
|
82
|
+
currentQueue.splice(index, 1);
|
|
83
|
+
reject(new ConcurrencyDeadlineExceededError(key));
|
|
84
|
+
}, deadlineMs - now());
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
run: async (key, limit, task, options) => {
|
|
89
|
+
await acquire(key, limit, options?.deadlineMs, options?.now ?? Date.now);
|
|
90
|
+
try {
|
|
91
|
+
return await task();
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
release(key);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
queueDepth: (key) => {
|
|
98
|
+
if (key !== undefined)
|
|
99
|
+
return queueByKey.get(key)?.length ?? 0;
|
|
100
|
+
let total = 0;
|
|
101
|
+
for (const queue of queueByKey.values())
|
|
102
|
+
total += queue.length;
|
|
103
|
+
return total;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Retry-After cooldown — 30 s default, never zero, seconds or HTTP date
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
/** How long a key is treated as rate-limited when its answer carries no readable `Retry-After`. */
|
|
111
|
+
export const DEFAULT_RETRY_AFTER_COOLDOWN_MS = 30_000;
|
|
112
|
+
/**
|
|
113
|
+
* Reads a `Retry-After` header value as a millisecond delay from now.
|
|
114
|
+
*
|
|
115
|
+
* The header is either a whole number of seconds or an HTTP-date (RFC 9110
|
|
116
|
+
* §10.2.3); both are read. An absent, unreadable, non-positive, or zero
|
|
117
|
+
* value falls back to `defaultMs` — `docs/quote-service.md`'s cache rule is
|
|
118
|
+
* explicit: "30 s default, never 0." A key that rate-limited us once without
|
|
119
|
+
* saying for how long — or that names a cooldown already in the past — is
|
|
120
|
+
* not asked again on the very next call.
|
|
121
|
+
*
|
|
122
|
+
* @param retryAfterHeader - the header's raw value, or null when absent
|
|
123
|
+
* @param nowMs - the current time in epoch milliseconds
|
|
124
|
+
* @param defaultMs - the fallback cooldown
|
|
125
|
+
* @returns the delay, in milliseconds, before the key should be asked again
|
|
126
|
+
*/
|
|
127
|
+
const backoffMsFromRetryAfter = (retryAfterHeader, nowMs, defaultMs) => {
|
|
128
|
+
if (retryAfterHeader === null) {
|
|
129
|
+
return defaultMs;
|
|
130
|
+
}
|
|
131
|
+
const seconds = Number(retryAfterHeader);
|
|
132
|
+
// A FINITE NUMBER IS DECIDED HERE, NEVER HANDED TO `Date.parse`. A plain
|
|
133
|
+
// digit string such as `'0'` is unambiguously a seconds value, not a date
|
|
134
|
+
// — but `Date.parse('0')` does not throw or return `NaN`; it happily reads
|
|
135
|
+
// it as a bare year and returns a timestamp decades away. Falling through
|
|
136
|
+
// to the date branch for a non-positive number turned "never zero" into
|
|
137
|
+
// "cooldown for decades" instead of the intended 30 s default.
|
|
138
|
+
if (Number.isFinite(seconds)) {
|
|
139
|
+
return seconds > 0 ? seconds * 1000 : defaultMs;
|
|
140
|
+
}
|
|
141
|
+
const untilMs = Date.parse(retryAfterHeader);
|
|
142
|
+
if (Number.isFinite(untilMs)) {
|
|
143
|
+
const deltaMs = untilMs - nowMs;
|
|
144
|
+
return deltaMs > 0 ? deltaMs : defaultMs;
|
|
145
|
+
}
|
|
146
|
+
return defaultMs;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Builds a fresh, empty {@link RetryAfterCooldown}.
|
|
150
|
+
*
|
|
151
|
+
* @param options.defaultMs - the fallback cooldown; defaults to
|
|
152
|
+
* {@link DEFAULT_RETRY_AFTER_COOLDOWN_MS}
|
|
153
|
+
* @returns the cooldown tracker
|
|
154
|
+
*/
|
|
155
|
+
export const createRetryAfterCooldown = (options) => {
|
|
156
|
+
const defaultMs = options?.defaultMs ?? DEFAULT_RETRY_AFTER_COOLDOWN_MS;
|
|
157
|
+
const cooldownUntilMsByKey = new Map();
|
|
158
|
+
return {
|
|
159
|
+
record: (key, retryAfterHeader, nowMs = Date.now()) => {
|
|
160
|
+
cooldownUntilMsByKey.set(key, nowMs + backoffMsFromRetryAfter(retryAfterHeader, nowMs, defaultMs));
|
|
161
|
+
},
|
|
162
|
+
isActive: (key, nowMs = Date.now()) => {
|
|
163
|
+
const untilMs = cooldownUntilMsByKey.get(key);
|
|
164
|
+
return untilMs !== undefined && nowMs < untilMs;
|
|
165
|
+
},
|
|
166
|
+
clearsAtMs: (key) => cooldownUntilMsByKey.get(key) ?? null,
|
|
167
|
+
reset: () => {
|
|
168
|
+
cooldownUntilMsByKey.clear();
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// The display-quote cache key
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
/** Canonical spelling every known native-asset placeholder normalizes to. */
|
|
176
|
+
const NATIVE_TOKEN_PLACEHOLDER = 'native';
|
|
177
|
+
/** Every ecosystem's own native-asset sentinel string, lower-cased, as a lookup set. */
|
|
178
|
+
const nativeAssetSentinels = new Set(ecosystemRegistry.map((descriptor) => descriptor.nativeAssetSentinel.toLowerCase()));
|
|
179
|
+
/** A plain 20-byte Ethereum-style hex address. */
|
|
180
|
+
const evmHexAddressPattern = /^0x[0-9a-fA-F]{40}$/;
|
|
181
|
+
/**
|
|
182
|
+
* Normalizes one token reference for the cache key, per
|
|
183
|
+
* `docs/quote-service.md`'s "Cache" section: "Ethereum-style addresses
|
|
184
|
+
* lowercased; native placeholders mapped to one form; base58/bech32 keep
|
|
185
|
+
* case."
|
|
186
|
+
*
|
|
187
|
+
* @param token - the raw token address or identifier from a {@link DisplayAsk}
|
|
188
|
+
* @returns the normalized form
|
|
189
|
+
*/
|
|
190
|
+
const normalizeCacheKeyToken = (token) => {
|
|
191
|
+
if (nativeAssetSentinels.has(token.toLowerCase()))
|
|
192
|
+
return NATIVE_TOKEN_PLACEHOLDER;
|
|
193
|
+
return evmHexAddressPattern.test(token) ? token.toLowerCase() : token;
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* Builds the display-quote cache key for one provider's answer to one ask,
|
|
197
|
+
* exactly per `docs/quote-service.md`'s "Cache" section:
|
|
198
|
+
* `v1|provider|from.chainId|from.token|to.chainId|to.token|recipientEcosystem|probeSetVersion|bucket`.
|
|
199
|
+
*
|
|
200
|
+
* @param provider - which provider this key caches an answer for
|
|
201
|
+
* @param ask - the ask being cached; only the fields the key is built from
|
|
202
|
+
* @param probeSetVersion - the server's own probe-address generation, so a
|
|
203
|
+
* rotated probe address never serves a cached answer keyed to the old one
|
|
204
|
+
* @returns the cache key
|
|
205
|
+
*/
|
|
206
|
+
export const displayQuoteCacheKey = (provider, ask, probeSetVersion) => {
|
|
207
|
+
const bucket = twoSignificantFigureAmountBucket(decodeBaseUnitsAmount(ask.amount));
|
|
208
|
+
return [
|
|
209
|
+
'v1',
|
|
210
|
+
provider,
|
|
211
|
+
ask.from.chainId.toString(),
|
|
212
|
+
normalizeCacheKeyToken(ask.from.token),
|
|
213
|
+
ask.to.chainId.toString(),
|
|
214
|
+
normalizeCacheKeyToken(ask.to.token),
|
|
215
|
+
ask.recipientEcosystem ?? '',
|
|
216
|
+
probeSetVersion,
|
|
217
|
+
bucket.toString(),
|
|
218
|
+
].join('|');
|
|
219
|
+
};
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Scaling a cached figure to the amount actually asked
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
/**
|
|
224
|
+
* Scales a cached figure — answered for a NEARBY amount — into an estimate
|
|
225
|
+
* for the amount actually asked, per `docs/quote-service.md`'s "Cache"
|
|
226
|
+
* section: `scaled = figure × userAmount / askedAmount`, entirely in bigint.
|
|
227
|
+
*
|
|
228
|
+
* @param figure - the nearby amount's own raw answer
|
|
229
|
+
* @param userAmount - the amount actually asked for (this asker's own amount)
|
|
230
|
+
* @param askedAmount - the amount that was actually asked upstream (the
|
|
231
|
+
* nearby, cached amount `figure` answers)
|
|
232
|
+
* @returns the scaled estimate, in the same units as `figure`
|
|
233
|
+
* @throws when `askedAmount` is not positive — there is nothing to scale from
|
|
234
|
+
*/
|
|
235
|
+
export const scaleToAskedAmount = (figure, userAmount, askedAmount) => {
|
|
236
|
+
if (askedAmount <= 0n) {
|
|
237
|
+
throw new Error('scaleToAskedAmount: askedAmount must be positive');
|
|
238
|
+
}
|
|
239
|
+
return (figure * userAmount) / askedAmount;
|
|
240
|
+
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { ProviderId } from '@gibs/bridge-sdk/providers';
|
|
2
|
+
/**
|
|
3
|
+
* Why a quote provider gave the reader no route.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. A provider that answered "I cannot carry this" used to be
|
|
6
|
+
* dropped with no trace, and its route simply was not on the strip. That
|
|
7
|
+
* silence hid real defects: NEAR Intents refused wrapped ether on Ethereum
|
|
8
|
+
* for weeks and nothing on screen said so. Every reader here names the
|
|
9
|
+
* provider it asked and did not get a route from.
|
|
10
|
+
*
|
|
11
|
+
* THREE KINDS, AND THEY MUST READ DIFFERENTLY. Measured against LI.FI on
|
|
12
|
+
* 2026-09-21: a well-formed request with no route answers 404 with
|
|
13
|
+
* `code 1002`; a path that does not exist also answers 404, with
|
|
14
|
+
* `code 1003`; a malformed body answers 400 with `code 1011`. Only the
|
|
15
|
+
* first is the provider saying "no". The others mean the question was never
|
|
16
|
+
* answered, and a reader told "no route" when the truth is "we could not
|
|
17
|
+
* ask" draws the wrong conclusion.
|
|
18
|
+
*
|
|
19
|
+
* - `no-quote` — the provider answered and declined.
|
|
20
|
+
* - `could-not-ask` — the request failed, or the answer was unreadable.
|
|
21
|
+
* - `cannot-carry` — the provider was never asked, because it cannot express
|
|
22
|
+
* this pair at all (its request builder returned nothing).
|
|
23
|
+
* - `invalid-api-key` — the provider rejected the api key attached to this
|
|
24
|
+
* request. Kept apart from `could-not-ask` because it is not a transient
|
|
25
|
+
* failure: a rejected key does not clear on its own, and treating it as a
|
|
26
|
+
* generic "we could not ask" invites a retry that repeats the exact same
|
|
27
|
+
* rejection. See {@link readLifiRefusal}. THIS MODULE NEVER LOGS OR
|
|
28
|
+
* RECORDS THE REJECTION ITSELF — it only classifies the response. A host
|
|
29
|
+
* that wants the rejection remembered (so it stops retrying, or logs it
|
|
30
|
+
* once) does that itself by checking `kind === 'invalid-api-key'` on the
|
|
31
|
+
* returned refusal; see `packages/ui/src/lib/stores/quote-refusals.ts`'s
|
|
32
|
+
* `readLifiRefusal` wrapper for the browser-side example.
|
|
33
|
+
*/
|
|
34
|
+
export type QuoteRefusalKind = 'no-quote' | 'could-not-ask' | 'cannot-carry' | 'invalid-api-key';
|
|
35
|
+
/** One provider that was asked for a route and did not return one. */
|
|
36
|
+
export type ProviderQuoteRefusal = {
|
|
37
|
+
/** Which provider. */
|
|
38
|
+
readonly provider: ProviderId;
|
|
39
|
+
/** Whether it declined, could not be asked, or cannot carry the pair. */
|
|
40
|
+
readonly kind: QuoteRefusalKind;
|
|
41
|
+
/**
|
|
42
|
+
* The provider's own words for why, shortened for a tooltip, or null when
|
|
43
|
+
* the answer carried none. Never invented: when this is null the surface
|
|
44
|
+
* says only that no reason was given.
|
|
45
|
+
*/
|
|
46
|
+
readonly reason: string | null;
|
|
47
|
+
/**
|
|
48
|
+
* A minimum amount the refusal named, in the ORIGIN token's base units, or
|
|
49
|
+
* undefined when it named none.
|
|
50
|
+
*
|
|
51
|
+
* Only `readNearIntentsRefusal`'s `belowBridgeMinimum` case sets it: that
|
|
52
|
+
* is the one refusal among the three providers whose minimum already
|
|
53
|
+
* arrives in the ORIGIN token's own base units rather than United States
|
|
54
|
+
* dollars. A `belowChainMinimum` refusal names its floor in dollars and is
|
|
55
|
+
* left unset here for the same reason: converting it to base units needs a
|
|
56
|
+
* price this module does not have, and a floor that silently mixed units
|
|
57
|
+
* would prune the wrong edges.
|
|
58
|
+
*/
|
|
59
|
+
readonly minimumAmount?: bigint;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Thrown by a provider transport when the provider answered without a quote.
|
|
63
|
+
*
|
|
64
|
+
* Carries the classified refusal so the caller can record WHICH provider said
|
|
65
|
+
* no and why, instead of a bare `Error` whose message is the only trace.
|
|
66
|
+
*/
|
|
67
|
+
export declare class ProviderQuoteError extends Error {
|
|
68
|
+
readonly refusal: ProviderQuoteRefusal;
|
|
69
|
+
constructor(refusal: ProviderQuoteRefusal);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Thrown when no provider returned a quote, carrying every refusal.
|
|
73
|
+
*
|
|
74
|
+
* The message is unchanged from the plain error this replaces, because callers
|
|
75
|
+
* and tests match on it; the refusals are what is new.
|
|
76
|
+
*/
|
|
77
|
+
export declare class NoProviderQuotedError extends Error {
|
|
78
|
+
readonly refusals: readonly ProviderQuoteRefusal[];
|
|
79
|
+
constructor(refusals: readonly ProviderQuoteRefusal[]);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Whether a LI.FI response is specifically "the api key attached to this
|
|
83
|
+
* request was rejected" — a 401 carrying `code: 1010`, "Invalid API key".
|
|
84
|
+
* Measured live: this is the ONE 401 shape LI.FI answers with when the
|
|
85
|
+
* configured key is bad, distinct from a plain unauthenticated request
|
|
86
|
+
* (which LI.FI answers normally at a lower rate limit, never 401).
|
|
87
|
+
*
|
|
88
|
+
* Exported so every LI.FI response reader — not only the quote endpoints —
|
|
89
|
+
* can classify a failure the same way.
|
|
90
|
+
*
|
|
91
|
+
* @param status - the HTTP status
|
|
92
|
+
* @param body - the parsed JSON body, or null when it was unreadable
|
|
93
|
+
* @returns whether this response is LI.FI rejecting the configured key
|
|
94
|
+
*/
|
|
95
|
+
export declare const isLifiInvalidApiKeyResponse: (status: number, body: unknown) => boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Classifies a non-success LI.FI answer.
|
|
98
|
+
*
|
|
99
|
+
* `code 1002` is the one "no" — see {@link QuoteRefusalKind}. A rejected api
|
|
100
|
+
* key ({@link isLifiInvalidApiKeyResponse}) is its own kind,
|
|
101
|
+
* `invalid-api-key`. Everything else, including a 404 carrying a different
|
|
102
|
+
* code, is a request that could not be made.
|
|
103
|
+
*
|
|
104
|
+
* PURE: this reader never logs and never records anything. A host that wants
|
|
105
|
+
* a rejected key remembered — so it stops retrying, or logs it once — reads
|
|
106
|
+
* `kind === 'invalid-api-key'` off the return value and records it itself.
|
|
107
|
+
*
|
|
108
|
+
* @param status - the HTTP status
|
|
109
|
+
* @param body - the parsed JSON body, or null when it was unreadable
|
|
110
|
+
* @returns the refusal
|
|
111
|
+
*/
|
|
112
|
+
export declare const readLifiRefusal: (status: number, body: unknown) => ProviderQuoteRefusal;
|
|
113
|
+
/**
|
|
114
|
+
* Classifies a non-success Relay answer.
|
|
115
|
+
*
|
|
116
|
+
* Relay declines with a 4xx carrying an `errorCode` — measured:
|
|
117
|
+
* `{"message":"no routes found","errorCode":"NO_SWAP_ROUTES_FOUND"}`. A 4xx
|
|
118
|
+
* with a code is the provider saying no; a server error, an unauthenticated
|
|
119
|
+
* or rate-limited request (see {@link REQUEST_NEVER_REACHED_STATUSES}), or a
|
|
120
|
+
* body with no code, is a request that could not be made.
|
|
121
|
+
*
|
|
122
|
+
* @param status - the HTTP status
|
|
123
|
+
* @param body - the parsed JSON body, or null when it was unreadable
|
|
124
|
+
* @returns the refusal
|
|
125
|
+
*/
|
|
126
|
+
export declare const readRelayRefusal: (status: number, body: unknown) => ProviderQuoteRefusal;
|
|
127
|
+
/**
|
|
128
|
+
* Classifies a declined NEAR Intents `POST /v0/quote` answer.
|
|
129
|
+
*
|
|
130
|
+
* WORTH ITS OWN READER, SEPARATE FROM {@link readLifiRefusal} AND
|
|
131
|
+
* {@link readRelayRefusal}. NEAR Intents' decline carries no HTTP status code
|
|
132
|
+
* of its own worth branching on — every decline this service gives arrives
|
|
133
|
+
* as a 400 with a `message` — so the classification work is entirely in
|
|
134
|
+
* READING that message, which `@gibs/bridge-sdk/near-intents`'s
|
|
135
|
+
* `parseNearIntentsQuoteRefusal` already does. EVERY readable reason is a
|
|
136
|
+
* `'no-quote'` — the service considered the pair and named a real,
|
|
137
|
+
* size-shaped reason it cannot carry it — and only an UNREADABLE body falls
|
|
138
|
+
* back to `'could-not-ask'`, matching this module's own rule that a request
|
|
139
|
+
* that never got an answer must never read as a "no."
|
|
140
|
+
*
|
|
141
|
+
* @param status - the HTTP status (informational only; NEAR Intents declines
|
|
142
|
+
* are read from the body, not the status)
|
|
143
|
+
* @param body - the parsed JSON body, or null when it was unreadable
|
|
144
|
+
* @returns the refusal
|
|
145
|
+
*/
|
|
146
|
+
export declare const readNearIntentsRefusal: (status: number, body: unknown) => ProviderQuoteRefusal;
|
|
147
|
+
/**
|
|
148
|
+
* Turns one rejected provider call into a refusal, or null for an abort.
|
|
149
|
+
*
|
|
150
|
+
* An abort is not a refusal — the reader moved on and the question was
|
|
151
|
+
* withdrawn — so it is never reported. A {@link ProviderQuoteError} already
|
|
152
|
+
* carries its classification. Anything else thrown is a failure to ask.
|
|
153
|
+
*
|
|
154
|
+
* @param provider - the provider whose call rejected
|
|
155
|
+
* @param reason - the rejection value
|
|
156
|
+
* @param signal - the shared abort signal, to recognise a withdrawn question
|
|
157
|
+
* @returns the refusal, or null when the call was aborted
|
|
158
|
+
*/
|
|
159
|
+
export declare const refusalFromRejection: (provider: ProviderId, reason: unknown, signal: AbortSignal) => ProviderQuoteRefusal | null;
|
|
160
|
+
/**
|
|
161
|
+
* The refusals one route entry reports: the shared quote's, plus the
|
|
162
|
+
* separate two-step NEAR Intents question when that was asked.
|
|
163
|
+
*
|
|
164
|
+
* NEAR Intents is asked TWICE per entry, in two shapes. The shared quote asks
|
|
165
|
+
* it for a one-signature crossing, which it can never form — so it always
|
|
166
|
+
* answers `cannot-carry` there — while the two-step query asks it the
|
|
167
|
+
* question it can answer. When the two-step question was asked, its answer
|
|
168
|
+
* is NEAR Intents' real one, and the shared `cannot-carry` would contradict
|
|
169
|
+
* it.
|
|
170
|
+
*
|
|
171
|
+
* @param options.quoteRefusals - the shared quote's refusals
|
|
172
|
+
* @param options.twoStepAsked - whether the two-step question was asked
|
|
173
|
+
* @param options.twoStepError - the two-step query's error, or null
|
|
174
|
+
* @returns the entry's refusals, one per provider at most
|
|
175
|
+
*/
|
|
176
|
+
export declare const mergeEntryRefusals: ({ quoteRefusals, twoStepAsked, twoStepError, }: {
|
|
177
|
+
quoteRefusals: readonly ProviderQuoteRefusal[];
|
|
178
|
+
twoStepAsked: boolean;
|
|
179
|
+
twoStepError: unknown;
|
|
180
|
+
}) => readonly ProviderQuoteRefusal[];
|