@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,217 @@
|
|
|
1
|
+
import { lifiEndpoints } from '@gibs/bridge-sdk/consolidated-quote';
|
|
2
|
+
import { buildLifiPreQuoteRequest, lifiQuoteUrl, parseLifiPreQuote } from '@gibs/bridge-sdk/pre-quote';
|
|
3
|
+
import { readLifiRefusal } from '../quote-refusals.js';
|
|
4
|
+
import { createCalibrationStore } from './calibration.js';
|
|
5
|
+
import { createLifiPriceCatalogue, estimateDeliveredAmount } from './price-catalogue.js';
|
|
6
|
+
/** Headers for a LI.FI request FROM THIS CARRIER — never `@gibs/bridge-sdk/consolidated-quote`'s `lifiRequestHeaders`, which deliberately NEVER sends an api key because it is written for the browser bundle. This carrier can run on a server, where `LifiProviderConfig.apiKey` is a real secret meant to travel in this header. */
|
|
7
|
+
const lifiCarrierHeaders = (apiKey) => ({
|
|
8
|
+
'Content-Type': 'application/json',
|
|
9
|
+
...(apiKey === undefined ? {} : { 'x-lifi-api-key': apiKey }),
|
|
10
|
+
});
|
|
11
|
+
const cannotCarryAnswer = (reason) => ({
|
|
12
|
+
ok: false,
|
|
13
|
+
refusal: { kind: 'cannot-carry', reason, minimumAmount: null },
|
|
14
|
+
});
|
|
15
|
+
const couldNotAskAnswer = (reason) => ({
|
|
16
|
+
ok: false,
|
|
17
|
+
refusal: { kind: 'could-not-ask', reason, minimumAmount: null },
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Builds LI.FI as a {@link ProviderAsker}.
|
|
21
|
+
*
|
|
22
|
+
* @param options - see {@link CreateLifiCarrierOptions}
|
|
23
|
+
* @returns the carrier
|
|
24
|
+
*/
|
|
25
|
+
export const createLifiCarrier = (options) => {
|
|
26
|
+
const { config, probeAddresses, chainList } = options;
|
|
27
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
28
|
+
const priceCatalogue = options.priceCatalogue ?? createLifiPriceCatalogue({ baseUrl: config?.baseUrl, fetchImpl });
|
|
29
|
+
const calibrationStore = options.calibrationStore ?? createCalibrationStore();
|
|
30
|
+
/**
|
|
31
|
+
* A provider-model estimate for this edge — see `types.ts`'s
|
|
32
|
+
* `Carrier.estimate` doc and `price-catalogue.ts`'s `estimateDeliveredAmount`
|
|
33
|
+
* for the shared arithmetic.
|
|
34
|
+
*
|
|
35
|
+
* FEE MODEL, READ FROM LI.FI'S OWN DOCUMENTATION
|
|
36
|
+
* (`docs.li.fi/introduction/integrating-lifi/monetizing-integration`,
|
|
37
|
+
* read 2026-09-24): the `fee` query parameter is "a float number e.g 0.02
|
|
38
|
+
* refers to 2% of the transaction volume" — {@link LifiProviderConfig.fee}
|
|
39
|
+
* is forwarded there verbatim by `ask()`, so it is the one percentage
|
|
40
|
+
* charge this estimate can price EXACTLY, because it is this host's own
|
|
41
|
+
* configured number, not a guess about LI.FI's.
|
|
42
|
+
*
|
|
43
|
+
* WHAT IS NOT MODELED, ON PURPOSE. LI.FI's own "LIFI Fixed Fee" and the
|
|
44
|
+
* chosen bridge/tool's own fee (a "Relayer fee" and "Relayer gas fee" on
|
|
45
|
+
* the across-bridge quote recorded in
|
|
46
|
+
* `packages/bridge-sdk/src/__fixtures__/lifi/contract-quote-success-eth-to-base-usdc.json`,
|
|
47
|
+
* together about 0.27% of that quote) are NOT standardized: LI.FI
|
|
48
|
+
* aggregates upwards of twenty bridges, each pricing its own crossing
|
|
49
|
+
* differently, and which one a given quote picks is not known ahead of a
|
|
50
|
+
* live `ask()`. Left to {@link calibrationStore}, keyed by
|
|
51
|
+
* (`'lifi'`, `edge.from.chainId`, `edge.to.chainId`) — for a repeatedly
|
|
52
|
+
* quoted chain pair, the SAME bridge tends to win, so the gap this leaves
|
|
53
|
+
* is close to constant and calibration converges to it quickly.
|
|
54
|
+
*
|
|
55
|
+
* Destination gas is not subtracted either: the recorded fixture's own
|
|
56
|
+
* `estimate.gasCosts` entry is `"type": "SEND"` — gas the READER's wallet
|
|
57
|
+
* pays directly, in the origin chain's native currency, never taken out of
|
|
58
|
+
* `toAmount`. So there is no fixed cost, in the OUTPUT token, to subtract.
|
|
59
|
+
*/
|
|
60
|
+
const estimate = (edge, amountIn) => {
|
|
61
|
+
const priceFromUsd = priceCatalogue.priceUsd(edge.from);
|
|
62
|
+
const priceToUsd = priceCatalogue.priceUsd(edge.to);
|
|
63
|
+
if (priceFromUsd === null || priceToUsd === null)
|
|
64
|
+
return null;
|
|
65
|
+
const integratorFee = config?.fee !== undefined && config.fee > 0 ? config.fee : 0;
|
|
66
|
+
const calibrationFactor = calibrationStore.factor({
|
|
67
|
+
providerId: 'lifi',
|
|
68
|
+
fromChainId: edge.from.chainId,
|
|
69
|
+
toChainId: edge.to.chainId,
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
deliveredAmount: estimateDeliveredAmount({
|
|
73
|
+
amountIn,
|
|
74
|
+
fromDecimals: edge.from.decimals,
|
|
75
|
+
toDecimals: edge.to.decimals,
|
|
76
|
+
priceFromUsd,
|
|
77
|
+
priceToUsd,
|
|
78
|
+
percentageFee: integratorFee,
|
|
79
|
+
fixedCostUsd: 0,
|
|
80
|
+
calibrationFactor,
|
|
81
|
+
}),
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
const edgesFrom = (from, to) => {
|
|
85
|
+
// LI.FI quotes Ethereum-Virtual-Machine chains only, on both sides — see
|
|
86
|
+
// `PreQuoteRequest`'s own doc and `buildLifiPreQuoteRequest`'s non-Ethereum-origin refusal.
|
|
87
|
+
if (from.ecosystem !== 'evm' || to.ecosystem !== 'evm')
|
|
88
|
+
return [];
|
|
89
|
+
const chainIds = chainList.chainIds('lifi');
|
|
90
|
+
if (chainIds === null)
|
|
91
|
+
return [];
|
|
92
|
+
if (!chainIds.has(from.chainId) || !chainIds.has(to.chainId))
|
|
93
|
+
return [];
|
|
94
|
+
return [{ carrier: 'lifi', from, to, fusesNext: false, origin: 'wallet', floors: [] }];
|
|
95
|
+
};
|
|
96
|
+
const ask = async ({ edge, inputAmount }) => {
|
|
97
|
+
if (inputAmount <= 0n)
|
|
98
|
+
return cannotCarryAnswer('non-positive input amount');
|
|
99
|
+
const fromAddress = probeAddresses?.[edge.from.ecosystem];
|
|
100
|
+
const recipientAddress = probeAddresses?.evm;
|
|
101
|
+
if (fromAddress === undefined || recipientAddress === undefined) {
|
|
102
|
+
return cannotCarryAnswer('no probe address configured for this ecosystem');
|
|
103
|
+
}
|
|
104
|
+
const request = {
|
|
105
|
+
fromChainId: edge.from.chainId,
|
|
106
|
+
fromTokenAddress: edge.from.address,
|
|
107
|
+
fromAddress,
|
|
108
|
+
toChainId: edge.to.chainId,
|
|
109
|
+
toTokenAddress: edge.to.address,
|
|
110
|
+
amount: inputAmount,
|
|
111
|
+
recipient: recipientAddress,
|
|
112
|
+
...(edge.from.ecosystem === 'evm' ? {} : { originVmType: edge.from.ecosystem }),
|
|
113
|
+
// ALWAYS SET, NEVER LEFT `undefined`. `buildLifiPreQuoteRequest` falls
|
|
114
|
+
// back to `@gibs/bridge-sdk/consolidated-quote`'s `lifiIntegrator`
|
|
115
|
+
// constant ('gibs.finance') when `lifiIntegrator` is `undefined` —
|
|
116
|
+
// exactly the gibs default this package must never send unconfigured.
|
|
117
|
+
// `null` (not `''`) tells the builder to OMIT the query parameter
|
|
118
|
+
// entirely when the host configured none — an empty string would still
|
|
119
|
+
// be a literal, present `integrator=` LI.FI could record or reject.
|
|
120
|
+
lifiIntegrator: config?.integrator ?? null,
|
|
121
|
+
};
|
|
122
|
+
const query = buildLifiPreQuoteRequest(request);
|
|
123
|
+
if (query === null)
|
|
124
|
+
return cannotCarryAnswer(null);
|
|
125
|
+
// NEVER ASK A PROVIDER ABOUT A CHAIN IT DOES NOT LIST. `chainList.chainIds`
|
|
126
|
+
// returns null while the list has never loaded (or has aged out past its
|
|
127
|
+
// stale grace window) — that is a `could-not-ask`, distinct from a chain
|
|
128
|
+
// the list has genuinely never carried, which is `cannot-carry`.
|
|
129
|
+
const chainIds = chainList.chainIds('lifi');
|
|
130
|
+
if (chainIds === null)
|
|
131
|
+
return couldNotAskAnswer('LI.FI chain list not loaded');
|
|
132
|
+
if (!chainIds.has(edge.from.chainId) || !chainIds.has(edge.to.chainId)) {
|
|
133
|
+
return cannotCarryAnswer("LI.FI does not list one of this hop's chains");
|
|
134
|
+
}
|
|
135
|
+
const params = new URLSearchParams(query);
|
|
136
|
+
if (config?.fee !== undefined && config.fee > 0) {
|
|
137
|
+
params.set('fee', config.fee.toString());
|
|
138
|
+
}
|
|
139
|
+
const quoteUrl = config?.baseUrl === undefined ? lifiQuoteUrl : lifiEndpoints(config.baseUrl).quote;
|
|
140
|
+
let response;
|
|
141
|
+
try {
|
|
142
|
+
response = await fetchImpl(`${quoteUrl}?${params.toString()}`, {
|
|
143
|
+
headers: lifiCarrierHeaders(config?.apiKey),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
return couldNotAskAnswer(error instanceof Error ? error.message : null);
|
|
148
|
+
}
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
const body = await response.json().catch(() => null);
|
|
151
|
+
const refusal = readLifiRefusal(response.status, body);
|
|
152
|
+
return {
|
|
153
|
+
ok: false,
|
|
154
|
+
refusal: {
|
|
155
|
+
// `CarrierRefusalKind` has no `invalid-api-key` member — see
|
|
156
|
+
// `types.ts`'s `CarrierRefusalKind` doc; a rejected key means the
|
|
157
|
+
// ask never got a real answer, the same fact `could-not-ask` carries.
|
|
158
|
+
kind: refusal.kind === 'invalid-api-key' ? 'could-not-ask' : refusal.kind,
|
|
159
|
+
reason: refusal.reason,
|
|
160
|
+
minimumAmount: refusal.minimumAmount ?? null,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const result = parseLifiPreQuote(await response.json());
|
|
165
|
+
if (result === null)
|
|
166
|
+
return couldNotAskAnswer('LI.FI pre-quote response had an unexpected shape');
|
|
167
|
+
// CALIBRATES `estimate()` AGAINST THIS CONFIRMED QUOTE, when a prior
|
|
168
|
+
// catalogue price makes a comparison possible — see `estimate`'s own doc
|
|
169
|
+
// and `calibration.ts`'s head note. A `null` estimate (no price loaded
|
|
170
|
+
// for this edge yet) simply records nothing; there is no ratio to learn.
|
|
171
|
+
const priorEstimate = estimate(edge, inputAmount);
|
|
172
|
+
if (priorEstimate !== null) {
|
|
173
|
+
calibrationStore.record({ providerId: 'lifi', fromChainId: edge.from.chainId, toChainId: edge.to.chainId }, result.expectedAmount, priorEstimate.deliveredAmount);
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
ok: true,
|
|
177
|
+
deliveredAmount: result.expectedAmount,
|
|
178
|
+
minimumDeliveredAmount: result.minimumAmount,
|
|
179
|
+
claimedDurationSeconds: null,
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
return {
|
|
183
|
+
id: 'lifi',
|
|
184
|
+
edgesFrom,
|
|
185
|
+
ask,
|
|
186
|
+
// Warms `priceCatalogue` for every node the search might branch through,
|
|
187
|
+
// so `estimate()` (a synchronous read — see `types.ts`) never has to ask
|
|
188
|
+
// the network itself. See `price-catalogue.ts`'s `LifiPriceCatalogue.ensureLoaded`.
|
|
189
|
+
//
|
|
190
|
+
// FILTERED TO CHAINS LI.FI ITSELF LISTS, FIRST — the SAME
|
|
191
|
+
// "never ask a provider about a chain it does not list" gate `ask` uses
|
|
192
|
+
// a few lines up, applied here too. `nodes` is the WHOLE search's node
|
|
193
|
+
// universe (see `types.ts`'s `Carrier.prepare` doc), which on this
|
|
194
|
+
// codebase's own host includes PulseChain — a chain neither LI.FI nor
|
|
195
|
+
// Relay lists (`chainList.chainIds('lifi')` never contains it; see
|
|
196
|
+
// `packages/bridge-sdk/src/__fixtures__/relay/README.md`'s identical
|
|
197
|
+
// note for Relay). Filtering here, rather than trusting
|
|
198
|
+
// `ensureLoaded` never to be asked about an unlisted chain, is what
|
|
199
|
+
// keeps this carrier from ever naming that chain to LI.FI at all.
|
|
200
|
+
prepare: async (nodes) => {
|
|
201
|
+
const chainIds = chainList.chainIds('lifi');
|
|
202
|
+
if (chainIds === null)
|
|
203
|
+
return;
|
|
204
|
+
const listedNodes = nodes.filter((node) => chainIds.has(node.chainId));
|
|
205
|
+
await priceCatalogue.ensureLoaded(listedNodes);
|
|
206
|
+
},
|
|
207
|
+
estimate,
|
|
208
|
+
isRateLimited: true,
|
|
209
|
+
depositMode: 'wallet-signed',
|
|
210
|
+
sender: 'aggregator',
|
|
211
|
+
// A provisional prior for pre-quote scoring, not a measured figure —
|
|
212
|
+
// LI.FI itself only ever reports a duration per-quote (this carrier's
|
|
213
|
+
// own `ask` answers `claimedDurationSeconds: null`, above), because it
|
|
214
|
+
// routes through whichever underlying bridge a given quote picks.
|
|
215
|
+
settlementSeconds: 300,
|
|
216
|
+
};
|
|
217
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Ecosystem } from '@gibs/bridge-sdk/ecosystems';
|
|
2
|
+
import { type NearIntentsAsset } from '@gibs/bridge-sdk/near-intents';
|
|
3
|
+
import type { ChainListRegistry } from '../chain-lists.js';
|
|
4
|
+
import { type CalibrationStore } from './calibration.js';
|
|
5
|
+
import type { CarrierHopEdge, NearIntentsProviderConfig, ProviderAsker } from '../types.js';
|
|
6
|
+
/**
|
|
7
|
+
* NEAR Intents as a built-in {@link ProviderAsker}: ALWAYS a `dry: true`
|
|
8
|
+
* `POST /v0/quote` — never a live one — ported from
|
|
9
|
+
* `packages/ui/src/lib/state/route-search-quotes.ts`'s `askNearIntentsEdge`.
|
|
10
|
+
* See `docs/quote-service.md`'s "Rules that never bend": "Binding quotes
|
|
11
|
+
* (... NEAR live quote reserving a deposit address) are never cached or
|
|
12
|
+
* shared" — this carrier exists for DISPLAY only, and a live quote is a
|
|
13
|
+
* separate, deliberately un-built concern this package leaves to whichever
|
|
14
|
+
* caller executes a route. See `carriers/lifi.ts`'s head note for why every
|
|
15
|
+
* request here should go through `UpstreamFunnel.guardedFetch` bound to
|
|
16
|
+
* `'near-intents'`.
|
|
17
|
+
*
|
|
18
|
+
* READS ITS ASSET LIST FROM THE CHAIN-LIST REGISTRY, NOT A SEPARATE FETCH.
|
|
19
|
+
* `chain-lists.ts`'s `nearIntentsChainListSource` already fetches
|
|
20
|
+
* `GET /v0/tokens` on the shared refresh cadence and keeps the full parsed
|
|
21
|
+
* asset list as its `capability` payload — this carrier reads that rather
|
|
22
|
+
* than fetching the same endpoint a second time under a second schedule.
|
|
23
|
+
*/
|
|
24
|
+
/** What {@link createNearIntentsCarrier} needs beyond the edge and amount it is asked about. */
|
|
25
|
+
export type CreateNearIntentsCarrierOptions = {
|
|
26
|
+
/** NEAR Intents' own configuration — the host's partner key, referral tag, and base url override. */
|
|
27
|
+
readonly config?: NearIntentsProviderConfig;
|
|
28
|
+
/** A server-held address, per ecosystem — see `carriers/lifi.ts`'s identical field for the full contract. */
|
|
29
|
+
readonly probeAddresses?: Readonly<Partial<Record<Ecosystem, string>>>;
|
|
30
|
+
/**
|
|
31
|
+
* The chain-list registry `chain-lists.ts` builds — read under the
|
|
32
|
+
* `'near-intents'` key for BOTH the chain-id gate and, via `capability`,
|
|
33
|
+
* the asset list a request is built from.
|
|
34
|
+
*/
|
|
35
|
+
readonly chainList: Pick<ChainListRegistry, 'chainIds' | 'capability'>;
|
|
36
|
+
/** The `fetch` this carrier's own request goes through — see `carriers/lifi.ts`'s identical field. */
|
|
37
|
+
readonly fetchImpl?: typeof fetch;
|
|
38
|
+
/**
|
|
39
|
+
* This carrier's own {@link CalibrationStore}, correcting {@link Carrier.estimate}
|
|
40
|
+
* against confirmed `ask()` results. Defaults to an in-memory
|
|
41
|
+
* `createCalibrationStore()` — see `carriers/lifi.ts`'s identical field.
|
|
42
|
+
*/
|
|
43
|
+
readonly calibrationStore?: CalibrationStore;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Whether an edge is the "stable-same-asset" tier
|
|
47
|
+
* {@link nearIntentsAuthenticatedStablecoinFeeBasisPoints} names: "a
|
|
48
|
+
* stablecoin pair OR a same-asset multi-chain route" (that constant's own
|
|
49
|
+
* doc, read from `docs.near-intents.org/integration/distribution-channels/1click-api/authentication`
|
|
50
|
+
* on 2026-09-18). MODELED AS "same symbol on both ends," never a stablecoin
|
|
51
|
+
* whitelist: {@link NearIntentsAsset} carries a `symbol` this service itself
|
|
52
|
+
* assigned, so two assets sharing one symbol across chains is exactly the
|
|
53
|
+
* literal "same-asset multi-chain route" the discount names, measured
|
|
54
|
+
* rather than guessed. A DIFFERENT-SYMBOL stablecoin pair (say USD Coin
|
|
55
|
+
* into Tether) is NOT caught by this check and prices at the ordinary
|
|
56
|
+
* authenticated rate instead of the discount — a documented gap
|
|
57
|
+
* {@link CalibrationStore} absorbs per chain pair, same as the other two
|
|
58
|
+
* built-in carriers' own unmodeled tiers.
|
|
59
|
+
*
|
|
60
|
+
* @param options.assets - the parsed asset list
|
|
61
|
+
* @param options.edge - the candidate hop
|
|
62
|
+
* @returns whether both ends resolve to the same service-reported symbol
|
|
63
|
+
*/
|
|
64
|
+
export declare const isNearIntentsStableSameAsset: ({ assets, edge, }: {
|
|
65
|
+
readonly assets: readonly NearIntentsAsset[];
|
|
66
|
+
readonly edge: CarrierHopEdge;
|
|
67
|
+
}) => boolean;
|
|
68
|
+
/**
|
|
69
|
+
* NEAR Intents' own percentage fee for an edge, in basis points — read from
|
|
70
|
+
* `@gibs/bridge-sdk/near-intents`'s own constants, sourced from
|
|
71
|
+
* `docs.near-intents.org/integration/distribution-channels/1click-api/authentication`
|
|
72
|
+
* (2026-09-18): 25 basis points with no partner key, 20 with one, 1 on a
|
|
73
|
+
* partner-key stable-same-asset route (see {@link isNearIntentsStableSameAsset}).
|
|
74
|
+
* `withdrawFee` — the service's other real charge, quoted live per quote —
|
|
75
|
+
* is NOT modeled here; see `estimate`'s own doc.
|
|
76
|
+
*
|
|
77
|
+
* @param options.authenticated - whether a partner key is configured (`config.apiKey !== undefined`)
|
|
78
|
+
* @param options.stableSameAsset - {@link isNearIntentsStableSameAsset}'s own answer for this edge
|
|
79
|
+
* @returns the fee, in basis points
|
|
80
|
+
*/
|
|
81
|
+
export declare const nearIntentsFeeBasisPoints: ({ authenticated, stableSameAsset, }: {
|
|
82
|
+
readonly authenticated: boolean;
|
|
83
|
+
readonly stableSameAsset: boolean;
|
|
84
|
+
}) => number;
|
|
85
|
+
/**
|
|
86
|
+
* Builds NEAR Intents as a {@link ProviderAsker}.
|
|
87
|
+
*
|
|
88
|
+
* @param options - see {@link CreateNearIntentsCarrierOptions}
|
|
89
|
+
* @returns the carrier
|
|
90
|
+
*/
|
|
91
|
+
export declare const createNearIntentsCarrier: (options: CreateNearIntentsCarrierOptions) => ProviderAsker;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { buildNearIntentsQuoteRequest, findNearIntentsAsset, nearIntentsAuthenticatedFeeBasisPoints, nearIntentsAuthenticatedStablecoinFeeBasisPoints, nearIntentsQuoteUrl, nearIntentsRequestHeaders, nearIntentsUnauthenticatedFeeBasisPoints, parseNearIntentsPreQuote, } from '@gibs/bridge-sdk/near-intents';
|
|
2
|
+
import { readNearIntentsRefusal } from '../quote-refusals.js';
|
|
3
|
+
import { createCalibrationStore } from './calibration.js';
|
|
4
|
+
import { estimateDeliveredAmount, nearIntentsPriceCatalogueFromAssets } from './price-catalogue.js';
|
|
5
|
+
const cannotCarryAnswer = (reason) => ({
|
|
6
|
+
ok: false,
|
|
7
|
+
refusal: { kind: 'cannot-carry', reason, minimumAmount: null },
|
|
8
|
+
});
|
|
9
|
+
const couldNotAskAnswer = (reason) => ({
|
|
10
|
+
ok: false,
|
|
11
|
+
refusal: { kind: 'could-not-ask', reason, minimumAmount: null },
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* Reads the asset list `nearIntentsChainListSource` cached, narrowed to its
|
|
15
|
+
* real shape. `capability` is `unknown` at the registry's own boundary (see
|
|
16
|
+
* `chain-lists.ts`'s `ChainListLoadResult` doc for why); this is the one
|
|
17
|
+
* place that narrows it back, because only this carrier's own source
|
|
18
|
+
* produces it.
|
|
19
|
+
*/
|
|
20
|
+
const assetsFrom = (chainList) => {
|
|
21
|
+
const capability = chainList.capability('near-intents');
|
|
22
|
+
return Array.isArray(capability) ? capability : [];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Whether an edge is the "stable-same-asset" tier
|
|
26
|
+
* {@link nearIntentsAuthenticatedStablecoinFeeBasisPoints} names: "a
|
|
27
|
+
* stablecoin pair OR a same-asset multi-chain route" (that constant's own
|
|
28
|
+
* doc, read from `docs.near-intents.org/integration/distribution-channels/1click-api/authentication`
|
|
29
|
+
* on 2026-09-18). MODELED AS "same symbol on both ends," never a stablecoin
|
|
30
|
+
* whitelist: {@link NearIntentsAsset} carries a `symbol` this service itself
|
|
31
|
+
* assigned, so two assets sharing one symbol across chains is exactly the
|
|
32
|
+
* literal "same-asset multi-chain route" the discount names, measured
|
|
33
|
+
* rather than guessed. A DIFFERENT-SYMBOL stablecoin pair (say USD Coin
|
|
34
|
+
* into Tether) is NOT caught by this check and prices at the ordinary
|
|
35
|
+
* authenticated rate instead of the discount — a documented gap
|
|
36
|
+
* {@link CalibrationStore} absorbs per chain pair, same as the other two
|
|
37
|
+
* built-in carriers' own unmodeled tiers.
|
|
38
|
+
*
|
|
39
|
+
* @param options.assets - the parsed asset list
|
|
40
|
+
* @param options.edge - the candidate hop
|
|
41
|
+
* @returns whether both ends resolve to the same service-reported symbol
|
|
42
|
+
*/
|
|
43
|
+
export const isNearIntentsStableSameAsset = ({ assets, edge, }) => {
|
|
44
|
+
const originAsset = findNearIntentsAsset({
|
|
45
|
+
assets,
|
|
46
|
+
chainId: edge.from.chainId,
|
|
47
|
+
tokenAddress: edge.from.address,
|
|
48
|
+
ecosystem: edge.from.ecosystem,
|
|
49
|
+
});
|
|
50
|
+
const destinationAsset = findNearIntentsAsset({
|
|
51
|
+
assets,
|
|
52
|
+
chainId: edge.to.chainId,
|
|
53
|
+
tokenAddress: edge.to.address,
|
|
54
|
+
ecosystem: edge.to.ecosystem,
|
|
55
|
+
});
|
|
56
|
+
if (originAsset === null || destinationAsset === null)
|
|
57
|
+
return false;
|
|
58
|
+
return originAsset.symbol.toLowerCase() === destinationAsset.symbol.toLowerCase();
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* NEAR Intents' own percentage fee for an edge, in basis points — read from
|
|
62
|
+
* `@gibs/bridge-sdk/near-intents`'s own constants, sourced from
|
|
63
|
+
* `docs.near-intents.org/integration/distribution-channels/1click-api/authentication`
|
|
64
|
+
* (2026-09-18): 25 basis points with no partner key, 20 with one, 1 on a
|
|
65
|
+
* partner-key stable-same-asset route (see {@link isNearIntentsStableSameAsset}).
|
|
66
|
+
* `withdrawFee` — the service's other real charge, quoted live per quote —
|
|
67
|
+
* is NOT modeled here; see `estimate`'s own doc.
|
|
68
|
+
*
|
|
69
|
+
* @param options.authenticated - whether a partner key is configured (`config.apiKey !== undefined`)
|
|
70
|
+
* @param options.stableSameAsset - {@link isNearIntentsStableSameAsset}'s own answer for this edge
|
|
71
|
+
* @returns the fee, in basis points
|
|
72
|
+
*/
|
|
73
|
+
export const nearIntentsFeeBasisPoints = ({ authenticated, stableSameAsset, }) => {
|
|
74
|
+
if (!authenticated)
|
|
75
|
+
return nearIntentsUnauthenticatedFeeBasisPoints;
|
|
76
|
+
return stableSameAsset ? nearIntentsAuthenticatedStablecoinFeeBasisPoints : nearIntentsAuthenticatedFeeBasisPoints;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Builds NEAR Intents as a {@link ProviderAsker}.
|
|
80
|
+
*
|
|
81
|
+
* @param options - see {@link CreateNearIntentsCarrierOptions}
|
|
82
|
+
* @returns the carrier
|
|
83
|
+
*/
|
|
84
|
+
export const createNearIntentsCarrier = (options) => {
|
|
85
|
+
const { config, probeAddresses, chainList } = options;
|
|
86
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
87
|
+
const calibrationStore = options.calibrationStore ?? createCalibrationStore();
|
|
88
|
+
/**
|
|
89
|
+
* A provider-model estimate for this edge — see `types.ts`'s
|
|
90
|
+
* `Carrier.estimate` doc, `price-catalogue.ts`'s `estimateDeliveredAmount`
|
|
91
|
+
* for the shared arithmetic, and `nearIntentsFeeBasisPoints` for the fee
|
|
92
|
+
* model and its sources.
|
|
93
|
+
*
|
|
94
|
+
* NO FIXED COST IS MODELED. `withdrawFee` is real (the recorded fixture
|
|
95
|
+
* `packages/bridge-sdk/src/__fixtures__/near-intents/contract-quote-dry-success-base-to-eth-usdc.json`
|
|
96
|
+
* charges 300000 base units of destination USD Coin, about 1.1% of that
|
|
97
|
+
* quote's output) but the service prices it live, per quote, tracking its
|
|
98
|
+
* own operating cost — there is no published flat figure to read ahead of
|
|
99
|
+
* an `ask()`. Left to {@link CalibrationStore}, same as the other two
|
|
100
|
+
* built-in carriers' own unmodeled fixed costs.
|
|
101
|
+
*/
|
|
102
|
+
const estimate = (edge, amountIn) => {
|
|
103
|
+
const assets = assetsFrom(chainList);
|
|
104
|
+
const priceCatalogue = nearIntentsPriceCatalogueFromAssets(assets);
|
|
105
|
+
const priceFromUsd = priceCatalogue.priceUsd(edge.from);
|
|
106
|
+
const priceToUsd = priceCatalogue.priceUsd(edge.to);
|
|
107
|
+
if (priceFromUsd === null || priceToUsd === null)
|
|
108
|
+
return null;
|
|
109
|
+
const feeBasisPoints = nearIntentsFeeBasisPoints({
|
|
110
|
+
authenticated: config?.apiKey !== undefined,
|
|
111
|
+
stableSameAsset: isNearIntentsStableSameAsset({ assets, edge }),
|
|
112
|
+
});
|
|
113
|
+
const calibrationFactor = calibrationStore.factor({
|
|
114
|
+
providerId: 'near-intents',
|
|
115
|
+
fromChainId: edge.from.chainId,
|
|
116
|
+
toChainId: edge.to.chainId,
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
deliveredAmount: estimateDeliveredAmount({
|
|
120
|
+
amountIn,
|
|
121
|
+
fromDecimals: edge.from.decimals,
|
|
122
|
+
toDecimals: edge.to.decimals,
|
|
123
|
+
priceFromUsd,
|
|
124
|
+
priceToUsd,
|
|
125
|
+
percentageFee: feeBasisPoints / 10_000,
|
|
126
|
+
fixedCostUsd: 0,
|
|
127
|
+
calibrationFactor,
|
|
128
|
+
}),
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
const edgesFrom = (from, to) => {
|
|
132
|
+
// Every route this carrier expresses lands on an Ethereum-Virtual-Machine
|
|
133
|
+
// entry chain — see `ConsolidatedQuoteRequest.toTokenAddress`'s `Hex` type.
|
|
134
|
+
if (to.ecosystem !== 'evm')
|
|
135
|
+
return [];
|
|
136
|
+
const chainIds = chainList.chainIds('near-intents');
|
|
137
|
+
if (chainIds === null)
|
|
138
|
+
return [];
|
|
139
|
+
if (!chainIds.has(from.chainId) || !chainIds.has(to.chainId))
|
|
140
|
+
return [];
|
|
141
|
+
return [{ carrier: 'near-intents', from, to, fusesNext: false, origin: 'wallet', floors: [] }];
|
|
142
|
+
};
|
|
143
|
+
const ask = async ({ edge, inputAmount }) => {
|
|
144
|
+
if (inputAmount <= 0n)
|
|
145
|
+
return cannotCarryAnswer('non-positive input amount');
|
|
146
|
+
const assets = assetsFrom(chainList);
|
|
147
|
+
if (assets.length === 0)
|
|
148
|
+
return couldNotAskAnswer('NEAR Intents asset list not loaded');
|
|
149
|
+
const fromAddress = probeAddresses?.[edge.from.ecosystem];
|
|
150
|
+
const evmProbeAddress = probeAddresses?.evm;
|
|
151
|
+
if (fromAddress === undefined || evmProbeAddress === undefined) {
|
|
152
|
+
return cannotCarryAnswer('no probe address configured for this ecosystem');
|
|
153
|
+
}
|
|
154
|
+
// NEVER ASK ABOUT A CHAIN NEAR INTENTS DOES NOT LIST — see
|
|
155
|
+
// `carriers/lifi.ts`'s identical guard. Checked against the SAME derived
|
|
156
|
+
// chain-id set `edgesFrom` reads, so an ask this carrier was never
|
|
157
|
+
// offered as an edge is refused identically when asked directly.
|
|
158
|
+
const chainIds = chainList.chainIds('near-intents');
|
|
159
|
+
if (chainIds === null)
|
|
160
|
+
return couldNotAskAnswer('NEAR Intents chain list not loaded');
|
|
161
|
+
if (!chainIds.has(edge.from.chainId) || !chainIds.has(edge.to.chainId)) {
|
|
162
|
+
return cannotCarryAnswer("NEAR Intents does not list one of this hop's chains");
|
|
163
|
+
}
|
|
164
|
+
const request = {
|
|
165
|
+
fromChainId: edge.from.chainId,
|
|
166
|
+
fromTokenAddress: edge.from.address,
|
|
167
|
+
fromAddress,
|
|
168
|
+
connectedEthereumAccount: evmProbeAddress,
|
|
169
|
+
toChainId: edge.to.chainId,
|
|
170
|
+
toTokenAddress: edge.to.address,
|
|
171
|
+
toAmount: 0n,
|
|
172
|
+
fromAmount: inputAmount,
|
|
173
|
+
recipient: evmProbeAddress,
|
|
174
|
+
fallbackAddress: evmProbeAddress,
|
|
175
|
+
...(edge.from.ecosystem === 'evm' ? {} : { originVmType: edge.from.ecosystem }),
|
|
176
|
+
};
|
|
177
|
+
const context = {
|
|
178
|
+
nearIntentsAssets: assets,
|
|
179
|
+
now: new Date(),
|
|
180
|
+
...(config?.referral === undefined ? {} : { referral: config.referral }),
|
|
181
|
+
};
|
|
182
|
+
// ALWAYS dry — see this module's head note.
|
|
183
|
+
const body = buildNearIntentsQuoteRequest(request, context, { dry: true });
|
|
184
|
+
if (body === null)
|
|
185
|
+
return cannotCarryAnswer('NEAR Intents cannot express this pair');
|
|
186
|
+
const quoteUrl = config?.baseUrl === undefined ? nearIntentsQuoteUrl : `${config.baseUrl.replace(/\/+$/, '')}/v0/quote`;
|
|
187
|
+
let response;
|
|
188
|
+
try {
|
|
189
|
+
response = await fetchImpl(quoteUrl, {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
body: JSON.stringify(body),
|
|
192
|
+
headers: nearIntentsRequestHeaders(config?.apiKey),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
return couldNotAskAnswer(error instanceof Error ? error.message : null);
|
|
197
|
+
}
|
|
198
|
+
if (!response.ok) {
|
|
199
|
+
const payload = await response.json().catch(() => null);
|
|
200
|
+
const refusal = readNearIntentsRefusal(response.status, payload);
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
refusal: {
|
|
204
|
+
kind: refusal.kind === 'invalid-api-key' ? 'could-not-ask' : refusal.kind,
|
|
205
|
+
reason: refusal.reason,
|
|
206
|
+
minimumAmount: refusal.minimumAmount ?? null,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const result = parseNearIntentsPreQuote(await response.json());
|
|
211
|
+
if (result === null)
|
|
212
|
+
return couldNotAskAnswer('NEAR Intents response had an unexpected shape');
|
|
213
|
+
// CALIBRATES `estimate()` AGAINST THIS CONFIRMED QUOTE — see `lifi.ts`'s
|
|
214
|
+
// identical note.
|
|
215
|
+
const priorEstimate = estimate(edge, inputAmount);
|
|
216
|
+
if (priorEstimate !== null) {
|
|
217
|
+
calibrationStore.record({ providerId: 'near-intents', fromChainId: edge.from.chainId, toChainId: edge.to.chainId }, result.expectedAmount, priorEstimate.deliveredAmount);
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
ok: true,
|
|
221
|
+
deliveredAmount: result.expectedAmount,
|
|
222
|
+
minimumDeliveredAmount: result.minimumAmount,
|
|
223
|
+
claimedDurationSeconds: null,
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
return {
|
|
227
|
+
id: 'near-intents',
|
|
228
|
+
edgesFrom,
|
|
229
|
+
ask,
|
|
230
|
+
// No `prepare` — this carrier's own `estimate` derives its price
|
|
231
|
+
// catalogue synchronously from the asset list `chainList.capability`
|
|
232
|
+
// already carries (see `assetsFrom` and
|
|
233
|
+
// `price-catalogue.ts`'s `nearIntentsPriceCatalogueFromAssets`), fetched
|
|
234
|
+
// once on `chain-lists.ts`'s own refresh cadence — never a fetch of
|
|
235
|
+
// this carrier's own.
|
|
236
|
+
estimate,
|
|
237
|
+
isRateLimited: true,
|
|
238
|
+
// The one built-in carrier that can open a pay-by-hand origin's first
|
|
239
|
+
// hop, AND still runs as an ordinary wallet-signed aggregator mid-route
|
|
240
|
+
// — see `types.ts`'s own `CarrierDepositMode` doc for why this is
|
|
241
|
+
// `'either'` rather than a boolean.
|
|
242
|
+
depositMode: 'either',
|
|
243
|
+
sender: 'aggregator',
|
|
244
|
+
// A provisional prior for pre-quote scoring, not a measured figure — see
|
|
245
|
+
// `lifi.ts`'s own note; this carrier's own `ask` answers
|
|
246
|
+
// `claimedDurationSeconds: null` per-quote, above.
|
|
247
|
+
settlementSeconds: 120,
|
|
248
|
+
};
|
|
249
|
+
};
|