@gibs/quotes 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +45 -0
  3. package/dist/cache.d.ts +73 -0
  4. package/dist/cache.js +137 -0
  5. package/dist/carriers/calibration.d.ts +115 -0
  6. package/dist/carriers/calibration.js +83 -0
  7. package/dist/carriers/lifi.d.ts +63 -0
  8. package/dist/carriers/lifi.js +217 -0
  9. package/dist/carriers/near-intents.d.ts +91 -0
  10. package/dist/carriers/near-intents.js +249 -0
  11. package/dist/carriers/price-catalogue.d.ts +186 -0
  12. package/dist/carriers/price-catalogue.js +191 -0
  13. package/dist/carriers/relay.d.ts +97 -0
  14. package/dist/carriers/relay.js +242 -0
  15. package/dist/chain-lists.d.ts +144 -0
  16. package/dist/chain-lists.js +169 -0
  17. package/dist/client.d.ts +102 -0
  18. package/dist/client.js +402 -0
  19. package/dist/index.d.ts +19 -0
  20. package/dist/index.js +18 -0
  21. package/dist/quote-guards.d.ts +128 -0
  22. package/dist/quote-guards.js +240 -0
  23. package/dist/quote-refusals.d.ts +180 -0
  24. package/dist/quote-refusals.js +284 -0
  25. package/dist/quote-service-contract.d.ts +2240 -0
  26. package/dist/quote-service-contract.js +247 -0
  27. package/dist/search/capabilities.d.ts +68 -0
  28. package/dist/search/capabilities.js +28 -0
  29. package/dist/search/enumerate.d.ts +162 -0
  30. package/dist/search/enumerate.js +313 -0
  31. package/dist/search/index.d.ts +17 -0
  32. package/dist/search/index.js +17 -0
  33. package/dist/search/serve.d.ts +178 -0
  34. package/dist/search/serve.js +142 -0
  35. package/dist/search/waves.d.ts +481 -0
  36. package/dist/search/waves.js +939 -0
  37. package/dist/single-flight.d.ts +90 -0
  38. package/dist/single-flight.js +142 -0
  39. package/dist/types.d.ts +502 -0
  40. package/dist/types.js +1 -0
  41. package/dist/upstream.d.ts +126 -0
  42. package/dist/upstream.js +343 -0
  43. package/llms.txt +217 -0
  44. package/package.json +142 -0
@@ -0,0 +1,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
+ };