@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,186 @@
|
|
|
1
|
+
import { type NearIntentsAsset } from '@gibs/bridge-sdk/near-intents';
|
|
2
|
+
import type { CarrierAssetNode } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Each built-in carrier's own token price catalogue — LI.FI's `priceUSD` on
|
|
5
|
+
* `GET /v1/tokens`, Relay's `GET /currencies/token/price`, and NEAR Intents'
|
|
6
|
+
* `price` on `GET /v0/tokens`.
|
|
7
|
+
*
|
|
8
|
+
* WHY THIS EXISTS. The owner's own model for `estimate()`: "get their price
|
|
9
|
+
* for each token and their fee... then you can use some smart logic to find
|
|
10
|
+
* the best routes." A provider-model estimate needs a price for BOTH ends of
|
|
11
|
+
* an edge before it can say anything — see `types.ts`'s `EstimatedHopResult`
|
|
12
|
+
* doc. `Carrier.estimate` is synchronous (never a network call — see
|
|
13
|
+
* `types.ts`), so each catalogue here is populated ahead of time (through
|
|
14
|
+
* `Carrier.prepare`, for LI.FI and Relay) and read back with a plain,
|
|
15
|
+
* synchronous lookup.
|
|
16
|
+
*
|
|
17
|
+
* MISSING PRICE MEANS NO ESTIMATE, NEVER A GUESS. Every catalogue's
|
|
18
|
+
* `priceUsd` returns `null` for a token it cannot price, and every built-in
|
|
19
|
+
* carrier's own `estimate()` returns `null` in turn the moment either side
|
|
20
|
+
* of an edge prices to `null` — see `lifi.ts`, `relay.ts`, `near-intents.ts`.
|
|
21
|
+
*
|
|
22
|
+
* SOURCES, READ LIVE — never guessed:
|
|
23
|
+
* - LI.FI `GET /v1/tokens`: https://docs.li.fi/api-reference/fetch-all-known-tokens
|
|
24
|
+
* — each token object carries an optional `priceUSD` string. This module
|
|
25
|
+
* reuses `@gibs/bridge-sdk/consolidated-quote`'s own `lifiTokensUrl` and
|
|
26
|
+
* `parseLifiTokens` (already written for the token-picker's own price
|
|
27
|
+
* column) rather than a second parser for the same response shape.
|
|
28
|
+
* - Relay `GET /currencies/token/price?address=&chainId=`:
|
|
29
|
+
* https://docs.relay.link/references/api/get-token-price — `{ price: number }`,
|
|
30
|
+
* ONE token per call. Relay publishes no BULK priced catalogue: its bulk
|
|
31
|
+
* listing, `POST /currencies/v2`, carries `chainId`/`address`/`symbol`/`name`/
|
|
32
|
+
* `decimals`/`vmType`/`metadata` and NO price field at all (verified against
|
|
33
|
+
* `docs.relay.link/references/api/get-currencies-v2` and this repository's
|
|
34
|
+
* own recorded capture, `packages/bridge-sdk/src/__fixtures__/relay/currencies-solana.json`,
|
|
35
|
+
* 2026-09-24). So this module fetches Relay's price per token, once each,
|
|
36
|
+
* and caches every one it learns rather than re-asking.
|
|
37
|
+
* - NEAR Intents `GET /v0/tokens`: every asset already carries its own
|
|
38
|
+
* `price` (a number) and `priceUpdatedAt` (an ISO instant) — verified
|
|
39
|
+
* against the checked-in capture, `packages/bridge-sdk/src/__fixtures__/near-intents/tokens.json`
|
|
40
|
+
* ("price": 3.11 on wrapped NEAR, 2026-09-17). `@gibs/bridge-sdk/near-intents`'s
|
|
41
|
+
* `NearIntentsAsset` and `parseNearIntentsTokens` now keep both fields
|
|
42
|
+
* (previously dropped — see that module's own change note), so
|
|
43
|
+
* {@link nearIntentsPriceCatalogueFromAssets} reads them from the SAME
|
|
44
|
+
* asset list `chain-lists.ts`'s `nearIntentsChainListSource` already
|
|
45
|
+
* fetches on its own refresh cadence — never a second fetch under a
|
|
46
|
+
* second schedule, mirroring how `carriers/near-intents.ts` already reads
|
|
47
|
+
* that list for its own request-building.
|
|
48
|
+
*/
|
|
49
|
+
/** One priced token, as a provider's own catalogue reports it. */
|
|
50
|
+
export type CataloguedPrice = {
|
|
51
|
+
/** The token's own price, in United States dollars per whole (decimal-adjusted) unit. */
|
|
52
|
+
readonly priceUsd: number;
|
|
53
|
+
/** When this price was learned, in epoch milliseconds — the catalogue's own clock, not necessarily the provider's. Null when the provider names no timestamp of its own (LI.FI, Relay). */
|
|
54
|
+
readonly updatedAtMs: number | null;
|
|
55
|
+
};
|
|
56
|
+
/** Looks up one token's own most recent price, synchronously, from whatever a catalogue has already loaded. Never triggers a fetch. */
|
|
57
|
+
export type PriceCatalogue = {
|
|
58
|
+
/**
|
|
59
|
+
* @param node - the token to price
|
|
60
|
+
* @returns its United States dollar price, or null when this catalogue has none
|
|
61
|
+
*/
|
|
62
|
+
readonly priceUsd: (node: CarrierAssetNode) => number | null;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* The provider-model math every built-in carrier's `estimate()` shares — see
|
|
66
|
+
* `types.ts`'s `EstimatedHopResult` doc: "a price ratio times (one minus the
|
|
67
|
+
* carrier's typical fee) minus a fixed cost, calibrated against observed
|
|
68
|
+
* history." Only the FEE INPUTS differ between LI.FI, Relay and NEAR
|
|
69
|
+
* Intents; the arithmetic that turns a price pair and a fee model into a
|
|
70
|
+
* delivered amount is one function, not three copies of it.
|
|
71
|
+
*
|
|
72
|
+
* Works in floating-point whole-token units (`Number`, via `formatUnits`),
|
|
73
|
+
* matching this codebase's existing price-conversion style
|
|
74
|
+
* (`@gibs/bridge-sdk/prices.ts`'s `computeUsdPriceFromAmountsOut`) — an
|
|
75
|
+
* ESTIMATE, not a settlement figure, so the precision a `bigint`
|
|
76
|
+
* fixed-point scheme would add is not worth the extra machinery.
|
|
77
|
+
*
|
|
78
|
+
* @param options.amountIn - the input amount, in `from`'s base units
|
|
79
|
+
* @param options.fromDecimals - `from`'s decimal places
|
|
80
|
+
* @param options.toDecimals - `to`'s decimal places
|
|
81
|
+
* @param options.priceFromUsd - `from`'s own United States dollar price, per whole token
|
|
82
|
+
* @param options.priceToUsd - `to`'s own United States dollar price, per whole token
|
|
83
|
+
* @param options.percentageFee - every proportional charge this provider is known to apply, summed, as a fraction (0.001 = 0.1%)
|
|
84
|
+
* @param options.fixedCostUsd - every flat, United States dollar-denominated charge this provider is known to apply, summed
|
|
85
|
+
* @param options.calibrationFactor - {@link @gibs/quotes/carriers/calibration!CalibrationStore.factor}'s own answer for this provider and chain pair — 1 when uncalibrated
|
|
86
|
+
* @returns the delivered amount, in `to`'s base units; 0 when the inputs would price below zero
|
|
87
|
+
*/
|
|
88
|
+
export declare const estimateDeliveredAmount: ({ amountIn, fromDecimals, toDecimals, priceFromUsd, priceToUsd, percentageFee, fixedCostUsd, calibrationFactor, }: {
|
|
89
|
+
readonly amountIn: bigint;
|
|
90
|
+
readonly fromDecimals: number;
|
|
91
|
+
readonly toDecimals: number;
|
|
92
|
+
readonly priceFromUsd: number;
|
|
93
|
+
readonly priceToUsd: number;
|
|
94
|
+
readonly percentageFee: number;
|
|
95
|
+
readonly fixedCostUsd: number;
|
|
96
|
+
readonly calibrationFactor: number;
|
|
97
|
+
}) => bigint;
|
|
98
|
+
/** What {@link createLifiPriceCatalogue} needs. */
|
|
99
|
+
export type LifiPriceCatalogueOptions = {
|
|
100
|
+
/** Overrides LI.FI's own API root, mirroring `LifiProviderConfig.baseUrl`. */
|
|
101
|
+
readonly baseUrl?: string;
|
|
102
|
+
/** The `fetch` this catalogue's own requests go through. Defaults to the runtime global. */
|
|
103
|
+
readonly fetchImpl?: typeof fetch;
|
|
104
|
+
/** The clock a recorded price's `updatedAtMs` reads. Defaults to `Date.now`. */
|
|
105
|
+
readonly now?: () => number;
|
|
106
|
+
};
|
|
107
|
+
/** A LI.FI {@link PriceCatalogue}, with the async load step `Carrier.prepare` drives. */
|
|
108
|
+
export type LifiPriceCatalogue = PriceCatalogue & {
|
|
109
|
+
/**
|
|
110
|
+
* Fetches `GET /v1/tokens` for every chain id among `nodes` not already
|
|
111
|
+
* loaded, merging results into the cache. A chain already loaded is
|
|
112
|
+
* skipped — this catalogue fetches each chain's token list AT MOST ONCE
|
|
113
|
+
* over its own lifetime, per this module's head note.
|
|
114
|
+
*
|
|
115
|
+
* THIS METHOD TRUSTS ITS CALLER TO HAVE ALREADY FILTERED `nodes` TO CHAINS
|
|
116
|
+
* THE PROVIDER ITSELF LISTS. It has no `QuoteClientConfig.neverSendChainIds`
|
|
117
|
+
* of its own to consult (this package's carriers, not this catalogue,
|
|
118
|
+
* hold that guard) — `carriers/lifi.ts`'s own `prepare` is where a chain a
|
|
119
|
+
* host never wants named (PulseChain, for this repository's own host) is
|
|
120
|
+
* filtered out before this method is ever called.
|
|
121
|
+
*
|
|
122
|
+
* @param nodes - every node a search might branch through, already filtered to chains this provider lists — only their `ecosystem === 'evm'` chain ids matter, since LI.FI prices Ethereum-Virtual-Machine chains only
|
|
123
|
+
*/
|
|
124
|
+
readonly ensureLoaded: (nodes: readonly CarrierAssetNode[]) => Promise<void>;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Builds LI.FI's own {@link LifiPriceCatalogue}.
|
|
128
|
+
*
|
|
129
|
+
* @param options - see {@link LifiPriceCatalogueOptions}
|
|
130
|
+
* @returns the catalogue
|
|
131
|
+
*/
|
|
132
|
+
export declare const createLifiPriceCatalogue: (options?: LifiPriceCatalogueOptions) => LifiPriceCatalogue;
|
|
133
|
+
/** What {@link createRelayPriceCatalogue} needs. */
|
|
134
|
+
export type RelayPriceCatalogueOptions = {
|
|
135
|
+
/** Overrides Relay's own API root, mirroring `RelayProviderConfig.baseUrl`. */
|
|
136
|
+
readonly baseUrl?: string;
|
|
137
|
+
/** Forwarded as `x-api-key`, when set — see `docs.relay.link/references/api/get-token-price`'s optional auth header. */
|
|
138
|
+
readonly apiKey?: string;
|
|
139
|
+
/** The `fetch` this catalogue's own requests go through. Defaults to the runtime global. */
|
|
140
|
+
readonly fetchImpl?: typeof fetch;
|
|
141
|
+
/** The clock a recorded price's `updatedAtMs` reads. Defaults to `Date.now`. */
|
|
142
|
+
readonly now?: () => number;
|
|
143
|
+
};
|
|
144
|
+
/** A Relay {@link PriceCatalogue}, with the async load step `Carrier.prepare` drives. */
|
|
145
|
+
export type RelayPriceCatalogue = PriceCatalogue & {
|
|
146
|
+
/**
|
|
147
|
+
* Fetches `GET /currencies/token/price` for every node not already
|
|
148
|
+
* attempted, in parallel. A token already attempted — priced or not — is
|
|
149
|
+
* skipped, per this module's head note: Relay has no bulk price endpoint,
|
|
150
|
+
* so each token is asked for AT MOST ONCE over this catalogue's lifetime.
|
|
151
|
+
*
|
|
152
|
+
* THIS METHOD TRUSTS ITS CALLER TO HAVE ALREADY FILTERED `nodes` TO CHAINS
|
|
153
|
+
* RELAY ITSELF LISTS — see `LifiPriceCatalogue.ensureLoaded`'s identical
|
|
154
|
+
* note; `carriers/relay.ts`'s own `prepare` is where that filtering happens.
|
|
155
|
+
*
|
|
156
|
+
* @param nodes - every node a search might branch through, already filtered to chains this provider lists
|
|
157
|
+
*/
|
|
158
|
+
readonly ensureLoaded: (nodes: readonly CarrierAssetNode[]) => Promise<void>;
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Builds Relay's own {@link RelayPriceCatalogue}.
|
|
162
|
+
*
|
|
163
|
+
* @param options - see {@link RelayPriceCatalogueOptions}
|
|
164
|
+
* @returns the catalogue
|
|
165
|
+
*/
|
|
166
|
+
export declare const createRelayPriceCatalogue: (options?: RelayPriceCatalogueOptions) => RelayPriceCatalogue;
|
|
167
|
+
/**
|
|
168
|
+
* Builds a NEAR Intents {@link PriceCatalogue} from an already-loaded asset
|
|
169
|
+
* list — the SAME list `chain-lists.ts`'s `nearIntentsChainListSource`
|
|
170
|
+
* fetches and `carriers/near-intents.ts` already reads for its own request
|
|
171
|
+
* building (`chainList.capability('near-intents')`). No `ensureLoaded` and
|
|
172
|
+
* no fetch of its own: this catalogue is a pure, synchronous view over data
|
|
173
|
+
* a caller already has, refreshed on that source's own cadence — see this
|
|
174
|
+
* module's head note.
|
|
175
|
+
*
|
|
176
|
+
* READS THROUGH `findNearIntentsAsset`, NOT A SEPARATE ADDRESS MAP. That
|
|
177
|
+
* function already resolves a `CarrierAssetNode`'s chain id and address to
|
|
178
|
+
* the service's own asset record — including the native-asset and
|
|
179
|
+
* ecosystem-scoping rules `@gibs/bridge-sdk/near-intents`'s own doc lays
|
|
180
|
+
* out — so this module reuses that lookup rather than rebuilding an
|
|
181
|
+
* equivalent one that could silently disagree with it.
|
|
182
|
+
*
|
|
183
|
+
* @param assets - the parsed `GET /v0/tokens` asset list
|
|
184
|
+
* @returns the catalogue
|
|
185
|
+
*/
|
|
186
|
+
export declare const nearIntentsPriceCatalogueFromAssets: (assets: readonly NearIntentsAsset[]) => PriceCatalogue;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { formatUnits, parseUnits } from 'viem';
|
|
2
|
+
import { lifiEndpoints, lifiTokensUrl, parseLifiTokens } from '@gibs/bridge-sdk/consolidated-quote';
|
|
3
|
+
import { assetNodeKey } from '@gibs/bridge-sdk/routing';
|
|
4
|
+
import { findNearIntentsAsset } from '@gibs/bridge-sdk/near-intents';
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// estimateDeliveredAmount — the arithmetic every built-in carrier shares
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
/**
|
|
9
|
+
* The provider-model math every built-in carrier's `estimate()` shares — see
|
|
10
|
+
* `types.ts`'s `EstimatedHopResult` doc: "a price ratio times (one minus the
|
|
11
|
+
* carrier's typical fee) minus a fixed cost, calibrated against observed
|
|
12
|
+
* history." Only the FEE INPUTS differ between LI.FI, Relay and NEAR
|
|
13
|
+
* Intents; the arithmetic that turns a price pair and a fee model into a
|
|
14
|
+
* delivered amount is one function, not three copies of it.
|
|
15
|
+
*
|
|
16
|
+
* Works in floating-point whole-token units (`Number`, via `formatUnits`),
|
|
17
|
+
* matching this codebase's existing price-conversion style
|
|
18
|
+
* (`@gibs/bridge-sdk/prices.ts`'s `computeUsdPriceFromAmountsOut`) — an
|
|
19
|
+
* ESTIMATE, not a settlement figure, so the precision a `bigint`
|
|
20
|
+
* fixed-point scheme would add is not worth the extra machinery.
|
|
21
|
+
*
|
|
22
|
+
* @param options.amountIn - the input amount, in `from`'s base units
|
|
23
|
+
* @param options.fromDecimals - `from`'s decimal places
|
|
24
|
+
* @param options.toDecimals - `to`'s decimal places
|
|
25
|
+
* @param options.priceFromUsd - `from`'s own United States dollar price, per whole token
|
|
26
|
+
* @param options.priceToUsd - `to`'s own United States dollar price, per whole token
|
|
27
|
+
* @param options.percentageFee - every proportional charge this provider is known to apply, summed, as a fraction (0.001 = 0.1%)
|
|
28
|
+
* @param options.fixedCostUsd - every flat, United States dollar-denominated charge this provider is known to apply, summed
|
|
29
|
+
* @param options.calibrationFactor - {@link @gibs/quotes/carriers/calibration!CalibrationStore.factor}'s own answer for this provider and chain pair — 1 when uncalibrated
|
|
30
|
+
* @returns the delivered amount, in `to`'s base units; 0 when the inputs would price below zero
|
|
31
|
+
*/
|
|
32
|
+
export const estimateDeliveredAmount = ({ amountIn, fromDecimals, toDecimals, priceFromUsd, priceToUsd, percentageFee, fixedCostUsd, calibrationFactor, }) => {
|
|
33
|
+
const amountInWhole = Number(formatUnits(amountIn, fromDecimals));
|
|
34
|
+
const priceRatio = priceFromUsd / priceToUsd;
|
|
35
|
+
const grossOutWhole = amountInWhole * priceRatio * (1 - percentageFee);
|
|
36
|
+
const fixedCostWhole = fixedCostUsd / priceToUsd;
|
|
37
|
+
const netOutWhole = (grossOutWhole - fixedCostWhole) * calibrationFactor;
|
|
38
|
+
if (!Number.isFinite(netOutWhole) || netOutWhole <= 0) {
|
|
39
|
+
return 0n;
|
|
40
|
+
}
|
|
41
|
+
return parseUnits(netOutWhole.toFixed(toDecimals), toDecimals);
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Builds LI.FI's own {@link LifiPriceCatalogue}.
|
|
45
|
+
*
|
|
46
|
+
* @param options - see {@link LifiPriceCatalogueOptions}
|
|
47
|
+
* @returns the catalogue
|
|
48
|
+
*/
|
|
49
|
+
export const createLifiPriceCatalogue = (options = {}) => {
|
|
50
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
51
|
+
const now = options.now ?? Date.now;
|
|
52
|
+
const prices = new Map();
|
|
53
|
+
const loadedChainIds = new Set();
|
|
54
|
+
let inFlight = null;
|
|
55
|
+
const loadChain = async (chainId) => {
|
|
56
|
+
const url = options.baseUrl === undefined ? lifiTokensUrl(chainId) : lifiEndpoints(options.baseUrl).tokens(chainId);
|
|
57
|
+
let response;
|
|
58
|
+
try {
|
|
59
|
+
response = await fetchImpl(url);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Stays unloaded; a later `ensureLoaded` call over the same chain retries.
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const payload = await response.json().catch(() => null);
|
|
69
|
+
const tokens = parseLifiTokens(payload, chainId);
|
|
70
|
+
if (tokens === null) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
for (const token of tokens) {
|
|
74
|
+
if (token.priceUSD === null) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const price = Number(token.priceUSD);
|
|
78
|
+
if (!Number.isFinite(price) || price <= 0) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const key = assetNodeKey({ chainId, address: token.address, ecosystem: 'evm', decimals: token.decimals });
|
|
82
|
+
prices.set(key, { priceUsd: price, updatedAtMs: now() });
|
|
83
|
+
}
|
|
84
|
+
loadedChainIds.add(chainId);
|
|
85
|
+
};
|
|
86
|
+
const ensureLoaded = async (nodes) => {
|
|
87
|
+
const missing = Array.from(new Set(nodes.filter((node) => node.ecosystem === 'evm').map((node) => node.chainId))).filter((chainId) => !loadedChainIds.has(chainId));
|
|
88
|
+
if (missing.length === 0) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (inFlight !== null) {
|
|
92
|
+
await inFlight;
|
|
93
|
+
return ensureLoaded(nodes);
|
|
94
|
+
}
|
|
95
|
+
const task = Promise.all(missing.map(loadChain)).then(() => undefined);
|
|
96
|
+
inFlight = task;
|
|
97
|
+
try {
|
|
98
|
+
await task;
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
inFlight = null;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
return {
|
|
105
|
+
priceUsd: (node) => prices.get(assetNodeKey(node))?.priceUsd ?? null,
|
|
106
|
+
ensureLoaded,
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Builds Relay's own {@link RelayPriceCatalogue}.
|
|
111
|
+
*
|
|
112
|
+
* @param options - see {@link RelayPriceCatalogueOptions}
|
|
113
|
+
* @returns the catalogue
|
|
114
|
+
*/
|
|
115
|
+
export const createRelayPriceCatalogue = (options = {}) => {
|
|
116
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
117
|
+
const now = options.now ?? Date.now;
|
|
118
|
+
const prices = new Map();
|
|
119
|
+
const attempted = new Set();
|
|
120
|
+
const loadToken = async (node) => {
|
|
121
|
+
const key = assetNodeKey(node);
|
|
122
|
+
if (attempted.has(key)) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
attempted.add(key);
|
|
126
|
+
const base = options.baseUrl === undefined ? 'https://api.relay.link' : options.baseUrl.replace(/\/+$/, '');
|
|
127
|
+
const url = new URL(`${base}/currencies/token/price`);
|
|
128
|
+
url.searchParams.set('address', node.address);
|
|
129
|
+
url.searchParams.set('chainId', node.chainId.toString());
|
|
130
|
+
let response;
|
|
131
|
+
try {
|
|
132
|
+
response = await fetchImpl(url.toString(), {
|
|
133
|
+
headers: options.apiKey === undefined ? {} : { 'x-api-key': options.apiKey },
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const body = await response.json().catch(() => null);
|
|
143
|
+
const price = typeof body === 'object' && body !== null ? body.price : undefined;
|
|
144
|
+
if (typeof price !== 'number' || !Number.isFinite(price) || price <= 0) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
prices.set(key, { priceUsd: price, updatedAtMs: now() });
|
|
148
|
+
};
|
|
149
|
+
return {
|
|
150
|
+
priceUsd: (node) => prices.get(assetNodeKey(node))?.priceUsd ?? null,
|
|
151
|
+
ensureLoaded: async (nodes) => {
|
|
152
|
+
await Promise.all(nodes.map(loadToken));
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// NEAR Intents — derived from the already-loaded asset list, no fetch here
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
/**
|
|
160
|
+
* Builds a NEAR Intents {@link PriceCatalogue} from an already-loaded asset
|
|
161
|
+
* list — the SAME list `chain-lists.ts`'s `nearIntentsChainListSource`
|
|
162
|
+
* fetches and `carriers/near-intents.ts` already reads for its own request
|
|
163
|
+
* building (`chainList.capability('near-intents')`). No `ensureLoaded` and
|
|
164
|
+
* no fetch of its own: this catalogue is a pure, synchronous view over data
|
|
165
|
+
* a caller already has, refreshed on that source's own cadence — see this
|
|
166
|
+
* module's head note.
|
|
167
|
+
*
|
|
168
|
+
* READS THROUGH `findNearIntentsAsset`, NOT A SEPARATE ADDRESS MAP. That
|
|
169
|
+
* function already resolves a `CarrierAssetNode`'s chain id and address to
|
|
170
|
+
* the service's own asset record — including the native-asset and
|
|
171
|
+
* ecosystem-scoping rules `@gibs/bridge-sdk/near-intents`'s own doc lays
|
|
172
|
+
* out — so this module reuses that lookup rather than rebuilding an
|
|
173
|
+
* equivalent one that could silently disagree with it.
|
|
174
|
+
*
|
|
175
|
+
* @param assets - the parsed `GET /v0/tokens` asset list
|
|
176
|
+
* @returns the catalogue
|
|
177
|
+
*/
|
|
178
|
+
export const nearIntentsPriceCatalogueFromAssets = (assets) => ({
|
|
179
|
+
priceUsd: (node) => {
|
|
180
|
+
const asset = findNearIntentsAsset({
|
|
181
|
+
assets,
|
|
182
|
+
chainId: node.chainId,
|
|
183
|
+
tokenAddress: node.address,
|
|
184
|
+
ecosystem: node.ecosystem,
|
|
185
|
+
});
|
|
186
|
+
if (asset === null || asset.price === null) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
return Number.isFinite(asset.price) && asset.price > 0 ? asset.price : null;
|
|
190
|
+
},
|
|
191
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type Ecosystem } from '@gibs/bridge-sdk/ecosystems';
|
|
2
|
+
import type { ChainListRegistry } from '../chain-lists.js';
|
|
3
|
+
import { type CalibrationStore } from './calibration.js';
|
|
4
|
+
import { type RelayPriceCatalogue } from './price-catalogue.js';
|
|
5
|
+
import type { CarrierHopEdge, ProviderAsker, RelayProviderConfig } from '../types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Relay as a built-in {@link ProviderAsker}: a plain, forward-solving
|
|
8
|
+
* `POST /quote/v2` pre-quote, ported from
|
|
9
|
+
* `packages/ui/src/lib/state/route-search-quotes.ts`'s `askRelayEdge` —
|
|
10
|
+
* every `gibs.finance` fact replaced by a value read from this carrier's own
|
|
11
|
+
* {@link CreateRelayCarrierOptions}. See `carriers/lifi.ts`'s head note for
|
|
12
|
+
* why every request here should go through `UpstreamFunnel.guardedFetch`
|
|
13
|
+
* bound to `'relay'`.
|
|
14
|
+
*
|
|
15
|
+
* `POST /quote/v2` NOW REQUIRES `x-api-key` (verified live 2026-09-23; it did
|
|
16
|
+
* not before). `relayRequestHeaders` already sends the header only when
|
|
17
|
+
* `config.apiKey` is set — this carrier forwards whatever the host
|
|
18
|
+
* configured and never invents a key. WITH NO KEY CONFIGURED, this carrier
|
|
19
|
+
* still asks (a host may be mid-migration, or deliberately keyless), and the
|
|
20
|
+
* refusal Relay's own response carries is surfaced through
|
|
21
|
+
* `readRelayRefusal` exactly as recorded — never replaced with a
|
|
22
|
+
* locally-invented "missing key" refusal this module has not observed Relay
|
|
23
|
+
* actually send.
|
|
24
|
+
*/
|
|
25
|
+
/** What {@link createRelayCarrier} needs beyond the edge and amount it is asked about. */
|
|
26
|
+
export type CreateRelayCarrierOptions = {
|
|
27
|
+
/** Relay's own configuration — the host's api key, referrer, and base url override. */
|
|
28
|
+
readonly config?: RelayProviderConfig;
|
|
29
|
+
/** A server-held address, per ecosystem — see `carriers/lifi.ts`'s identical field for the full contract. */
|
|
30
|
+
readonly probeAddresses?: Readonly<Partial<Record<Ecosystem, string>>>;
|
|
31
|
+
/** The chain-list registry `chain-lists.ts` builds — read under the `'relay'` key. */
|
|
32
|
+
readonly chainList: Pick<ChainListRegistry, 'chainIds'>;
|
|
33
|
+
/** The `fetch` this carrier's own request goes through — see `carriers/lifi.ts`'s identical field. */
|
|
34
|
+
readonly fetchImpl?: typeof fetch;
|
|
35
|
+
/**
|
|
36
|
+
* This carrier's own {@link RelayPriceCatalogue}, backing {@link Carrier.estimate}.
|
|
37
|
+
* Defaults to `createRelayPriceCatalogue({ baseUrl: config?.baseUrl, apiKey: config?.apiKey, fetchImpl })`.
|
|
38
|
+
*/
|
|
39
|
+
readonly priceCatalogue?: RelayPriceCatalogue;
|
|
40
|
+
/**
|
|
41
|
+
* This carrier's own {@link CalibrationStore}, correcting {@link Carrier.estimate}
|
|
42
|
+
* against confirmed `ask()` results. Defaults to an in-memory
|
|
43
|
+
* `createCalibrationStore()` — see `carriers/lifi.ts`'s identical field.
|
|
44
|
+
*/
|
|
45
|
+
readonly calibrationStore?: CalibrationStore;
|
|
46
|
+
/**
|
|
47
|
+
* The host's own application fee on Relay routes, in basis points — Relay's
|
|
48
|
+
* `appFees` mechanism (`docs.relay.link/features/app-fees`), configured
|
|
49
|
+
* per host rather than assumed. Undefined or non-positive means no app fee
|
|
50
|
+
* is modeled — this package invents no gibs figure (compare
|
|
51
|
+
* `@gibs/bridge-sdk/referral.ts`'s `relayReferralFeeBasisPoints`, which is
|
|
52
|
+
* gibs's OWN wiring, kept out of this host-agnostic package).
|
|
53
|
+
*/
|
|
54
|
+
readonly appFeeBasisPoints?: number;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Which of Relay's published platform-fee tiers an edge most likely falls
|
|
58
|
+
* under — read from `docs.relay.link/how-it-works/fees` and
|
|
59
|
+
* `docs.relay.link/references/api/api_core_concepts/fees.md` (both fetched
|
|
60
|
+
* 2026-09-24): Token Bridge & same-chain Wrap/Unwrap 0.00%, Stablecoin Swap
|
|
61
|
+
* 0.01%, Major Swap (ETH, BTC, SOL, POL, BNB, PLUME, and any of those paired
|
|
62
|
+
* with a stablecoin) 0.06%, Minor Swap (everything else) 0.15%.
|
|
63
|
+
*
|
|
64
|
+
* WHY THIS IS A HEURISTIC, NOT A LOOKUP. {@link CarrierAssetNode} carries no
|
|
65
|
+
* token symbol (see `@gibs/bridge-sdk/routing`'s `AssetNode` — only chain
|
|
66
|
+
* id, address, ecosystem, decimals), and this host-agnostic package keeps no
|
|
67
|
+
* stablecoin/major-asset registry of its own to classify one. So this
|
|
68
|
+
* function only ever answers the two tiers it CAN determine from the edge's
|
|
69
|
+
* own shape, honestly:
|
|
70
|
+
*
|
|
71
|
+
* - `'bridge'` — the literal SAME address crosses chains (a real, checkable
|
|
72
|
+
* fact: some tokens, most commonly stablecoins deployed through a
|
|
73
|
+
* deterministic factory, share one address across chains).
|
|
74
|
+
* - `'major'` — either side is that chain's OWN native asset. Every chain
|
|
75
|
+
* this package's built-in carriers reach is presently an Ethereum-Virtual-
|
|
76
|
+
* Machine chain whose native coin is ether or a major chain's own gas
|
|
77
|
+
* token (BNB, and so on) — Relay's own "major" list — so a native-asset
|
|
78
|
+
* edge is major far more often than not.
|
|
79
|
+
* - `'minor'` — the default for everything else, INCLUDING a stablecoin pair
|
|
80
|
+
* this function cannot tell apart from an ordinary token pair without a
|
|
81
|
+
* symbol. That misclassification prices 0.15% instead of the true 0.01%,
|
|
82
|
+
* a difference `calibrationStore` (keyed per chain pair — see
|
|
83
|
+
* `carriers/lifi.ts`'s identical note) absorbs quickly for any pair quoted
|
|
84
|
+
* more than a few times, because the SAME pair keeps landing in the same
|
|
85
|
+
* true tier.
|
|
86
|
+
*
|
|
87
|
+
* @param edge - the candidate hop
|
|
88
|
+
* @returns the tier
|
|
89
|
+
*/
|
|
90
|
+
export declare const classifyRelaySwapKind: (edge: CarrierHopEdge) => "bridge" | "major" | "minor";
|
|
91
|
+
/**
|
|
92
|
+
* Builds Relay as a {@link ProviderAsker}.
|
|
93
|
+
*
|
|
94
|
+
* @param options - see {@link CreateRelayCarrierOptions}
|
|
95
|
+
* @returns the carrier
|
|
96
|
+
*/
|
|
97
|
+
export declare const createRelayCarrier: (options: CreateRelayCarrierOptions) => ProviderAsker;
|