@metamask-previews/assets-controller 9.0.2-preview-8633f7f2a → 9.0.2-preview-9de9aa4
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/CHANGELOG.md +0 -10
- package/dist/AssetsController.cjs +15 -6
- package/dist/AssetsController.cjs.map +1 -1
- package/dist/AssetsController.d.cts.map +1 -1
- package/dist/AssetsController.d.mts.map +1 -1
- package/dist/AssetsController.mjs +15 -6
- package/dist/AssetsController.mjs.map +1 -1
- package/dist/data-sources/PriceDataSource.cjs +13 -64
- package/dist/data-sources/PriceDataSource.cjs.map +1 -1
- package/dist/data-sources/PriceDataSource.d.cts +0 -13
- package/dist/data-sources/PriceDataSource.d.cts.map +1 -1
- package/dist/data-sources/PriceDataSource.d.mts +0 -13
- package/dist/data-sources/PriceDataSource.d.mts.map +1 -1
- package/dist/data-sources/PriceDataSource.mjs +13 -64
- package/dist/data-sources/PriceDataSource.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/utils/dedupingBatchFetcher.cjs +0 -157
- package/dist/utils/dedupingBatchFetcher.cjs.map +0 -1
- package/dist/utils/dedupingBatchFetcher.d.cts +0 -65
- package/dist/utils/dedupingBatchFetcher.d.cts.map +0 -1
- package/dist/utils/dedupingBatchFetcher.d.mts +0 -65
- package/dist/utils/dedupingBatchFetcher.d.mts.map +0 -1
- package/dist/utils/dedupingBatchFetcher.mjs +0 -153
- package/dist/utils/dedupingBatchFetcher.mjs.map +0 -1
|
@@ -10,7 +10,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
10
10
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
11
11
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
12
12
|
};
|
|
13
|
-
var _PriceDataSource_instances, _PriceDataSource_getSelectedCurrency, _PriceDataSource_pollInterval, _PriceDataSource_apiClient, _PriceDataSource_fetchTimeoutMs,
|
|
13
|
+
var _PriceDataSource_instances, _PriceDataSource_getSelectedCurrency, _PriceDataSource_pollInterval, _PriceDataSource_apiClient, _PriceDataSource_fetchTimeoutMs, _PriceDataSource_activeSubscriptions, _PriceDataSource_fetchSpotPricesBatch, _PriceDataSource_fetchSpotPrices, _PriceDataSource_getAssetIdsFromBalanceState;
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.PriceDataSource = void 0;
|
|
16
16
|
const core_backend_1 = require("@metamask/core-backend");
|
|
@@ -18,7 +18,6 @@ const utils_1 = require("@metamask/utils");
|
|
|
18
18
|
const logger_1 = require("../logger.cjs");
|
|
19
19
|
const types_1 = require("../types.cjs");
|
|
20
20
|
const utils_2 = require("../utils/index.cjs");
|
|
21
|
-
const dedupingBatchFetcher_1 = require("../utils/dedupingBatchFetcher.cjs");
|
|
22
21
|
const evm_rpc_services_1 = require("./evm-rpc-services/index.cjs");
|
|
23
22
|
// ============================================================================
|
|
24
23
|
// CONSTANTS
|
|
@@ -26,13 +25,6 @@ const evm_rpc_services_1 = require("./evm-rpc-services/index.cjs");
|
|
|
26
25
|
const CONTROLLER_NAME = 'PriceDataSource';
|
|
27
26
|
const DEFAULT_POLL_INTERVAL = 60000; // 1 minute for price updates
|
|
28
27
|
const DEFAULT_FETCH_TIMEOUT_MS = 15000;
|
|
29
|
-
/**
|
|
30
|
-
* Fraction of the poll interval used to cap the freshness TTL. Kept strictly
|
|
31
|
-
* below 1 so an asset fetched on one poll is reliably stale by the next poll;
|
|
32
|
-
* the margin absorbs network latency and timer jitter (see the cap in
|
|
33
|
-
* `subscribe`).
|
|
34
|
-
*/
|
|
35
|
-
const FRESHNESS_TTL_POLL_RATIO = 0.9;
|
|
36
28
|
/** Maximum number of asset IDs per Price API request. */
|
|
37
29
|
const PRICE_API_BATCH_SIZE = 50;
|
|
38
30
|
const log = (0, logger_1.createModuleLogger)(logger_1.projectLogger, CONTROLLER_NAME);
|
|
@@ -101,23 +93,12 @@ class PriceDataSource {
|
|
|
101
93
|
/** ApiPlatformClient for cached API calls */
|
|
102
94
|
_PriceDataSource_apiClient.set(this, void 0);
|
|
103
95
|
_PriceDataSource_fetchTimeoutMs.set(this, void 0);
|
|
104
|
-
/**
|
|
105
|
-
* Deduplicates price fetches by asset ID: skips assets fetched within the
|
|
106
|
-
* freshness TTL and joins concurrent in-flight fetches for the same asset so
|
|
107
|
-
* overlapping triggers (middleware + subscription poll) don't issue duplicate
|
|
108
|
-
* API requests.
|
|
109
|
-
*/
|
|
110
|
-
_PriceDataSource_deduper.set(this, void 0);
|
|
111
96
|
/** Active subscriptions by ID */
|
|
112
97
|
_PriceDataSource_activeSubscriptions.set(this, new Map());
|
|
113
98
|
__classPrivateFieldSet(this, _PriceDataSource_getSelectedCurrency, options.getSelectedCurrency, "f");
|
|
114
99
|
__classPrivateFieldSet(this, _PriceDataSource_pollInterval, options.pollInterval ?? DEFAULT_POLL_INTERVAL, "f");
|
|
115
100
|
__classPrivateFieldSet(this, _PriceDataSource_apiClient, options.queryApiClient, "f");
|
|
116
101
|
__classPrivateFieldSet(this, _PriceDataSource_fetchTimeoutMs, options.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS, "f");
|
|
117
|
-
__classPrivateFieldSet(this, _PriceDataSource_deduper, new dedupingBatchFetcher_1.DedupingBatchFetcher({
|
|
118
|
-
fetchBatch: (assetIds) => __classPrivateFieldGet(this, _PriceDataSource_instances, "m", _PriceDataSource_executeBatchFetch).call(this, assetIds),
|
|
119
|
-
freshnessTtlMs: options.priceFreshnessTtlMs ?? __classPrivateFieldGet(this, _PriceDataSource_pollInterval, "f"),
|
|
120
|
-
}), "f");
|
|
121
102
|
}
|
|
122
103
|
// ============================================================================
|
|
123
104
|
// MIDDLEWARE
|
|
@@ -231,24 +212,14 @@ class PriceDataSource {
|
|
|
231
212
|
// Clean up existing subscription
|
|
232
213
|
await this.unsubscribe(subscriptionId);
|
|
233
214
|
const pollInterval = request.updateInterval ?? __classPrivateFieldGet(this, _PriceDataSource_pollInterval, "f");
|
|
234
|
-
//
|
|
235
|
-
// `fetchedAt` is stamped when a fetch completes (slightly after the tick
|
|
236
|
-
// that triggered it), so a TTL equal to the poll interval would leave the
|
|
237
|
-
// asset still "fresh" at the next tick, making the subscription re-fetch
|
|
238
|
-
// only every other poll. The margin also absorbs network latency / jitter.
|
|
239
|
-
__classPrivateFieldGet(this, _PriceDataSource_deduper, "f").freshnessTtlMs = Math.min(__classPrivateFieldGet(this, _PriceDataSource_deduper, "f").freshnessTtlMs, Math.floor(pollInterval * FRESHNESS_TTL_POLL_RATIO));
|
|
240
|
-
// Create poll function - fetches prices using getAssetsState from subscription.
|
|
241
|
-
// The freshness TTL naturally gates re-fetches: assets fetched less than
|
|
242
|
-
// `priceFreshnessTtlMs` ago are skipped, preventing duplicates when middleware
|
|
243
|
-
// or other triggers already fetched the same assets between polls.
|
|
244
|
-
// Concurrent middleware calls will join the inflight promise rather than
|
|
245
|
-
// issuing duplicate requests.
|
|
215
|
+
// Create poll function - fetches prices using getAssetsState from subscription
|
|
246
216
|
const pollFn = async () => {
|
|
247
217
|
try {
|
|
248
218
|
const subscription = __classPrivateFieldGet(this, _PriceDataSource_activeSubscriptions, "f").get(subscriptionId);
|
|
249
219
|
if (!subscription) {
|
|
250
220
|
return;
|
|
251
221
|
}
|
|
222
|
+
// Fetch prices for all assets in balance state (uses subscription's getAssetsState)
|
|
252
223
|
const fetchResponse = await this.fetch(subscription.request, subscription.getAssetsState);
|
|
253
224
|
// Only report if we got prices
|
|
254
225
|
if (fetchResponse.assetsPrice &&
|
|
@@ -291,14 +262,6 @@ class PriceDataSource {
|
|
|
291
262
|
__classPrivateFieldGet(this, _PriceDataSource_activeSubscriptions, "f").delete(subscriptionId);
|
|
292
263
|
}
|
|
293
264
|
}
|
|
294
|
-
/**
|
|
295
|
-
* Invalidate the price freshness cache, forcing the next fetch to call the
|
|
296
|
-
* API regardless of TTL. Use when external state changes (e.g. selected
|
|
297
|
-
* currency) require a full refresh.
|
|
298
|
-
*/
|
|
299
|
-
invalidatePriceCache() {
|
|
300
|
-
__classPrivateFieldGet(this, _PriceDataSource_deduper, "f").invalidate();
|
|
301
|
-
}
|
|
302
265
|
/**
|
|
303
266
|
* Destroy the data source and clean up all subscriptions.
|
|
304
267
|
*/
|
|
@@ -307,11 +270,10 @@ class PriceDataSource {
|
|
|
307
270
|
subscription.cleanup();
|
|
308
271
|
}
|
|
309
272
|
__classPrivateFieldGet(this, _PriceDataSource_activeSubscriptions, "f").clear();
|
|
310
|
-
__classPrivateFieldGet(this, _PriceDataSource_deduper, "f").destroy();
|
|
311
273
|
}
|
|
312
274
|
}
|
|
313
275
|
exports.PriceDataSource = PriceDataSource;
|
|
314
|
-
_PriceDataSource_getSelectedCurrency = new WeakMap(), _PriceDataSource_pollInterval = new WeakMap(), _PriceDataSource_apiClient = new WeakMap(), _PriceDataSource_fetchTimeoutMs = new WeakMap(),
|
|
276
|
+
_PriceDataSource_getSelectedCurrency = new WeakMap(), _PriceDataSource_pollInterval = new WeakMap(), _PriceDataSource_apiClient = new WeakMap(), _PriceDataSource_fetchTimeoutMs = new WeakMap(), _PriceDataSource_activeSubscriptions = new WeakMap(), _PriceDataSource_instances = new WeakSet(), _PriceDataSource_fetchSpotPricesBatch =
|
|
315
277
|
// ============================================================================
|
|
316
278
|
// HELPERS
|
|
317
279
|
// ============================================================================
|
|
@@ -341,17 +303,15 @@ async function _PriceDataSource_fetchSpotPricesBatch(assetIds, selectedCurrency)
|
|
|
341
303
|
}), __classPrivateFieldGet(this, _PriceDataSource_fetchTimeoutMs, "f")),
|
|
342
304
|
]);
|
|
343
305
|
return { selectedCurrencyPrices, usdPrices };
|
|
344
|
-
},
|
|
306
|
+
}, _PriceDataSource_fetchSpotPrices =
|
|
345
307
|
/**
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
* so it does NOT check freshness or inflight state — that is handled by
|
|
349
|
-
* {@link DedupingBatchFetcher}.
|
|
308
|
+
* Fetch spot prices for all provided asset IDs, splitting into batches of
|
|
309
|
+
* PRICE_API_BATCH_SIZE to respect API limits.
|
|
350
310
|
*
|
|
351
|
-
* @param assetIds -
|
|
352
|
-
* @returns
|
|
311
|
+
* @param assetIds - Array of CAIP-19 asset IDs
|
|
312
|
+
* @returns Spot prices response
|
|
353
313
|
*/
|
|
354
|
-
async function
|
|
314
|
+
async function _PriceDataSource_fetchSpotPrices(assetIds) {
|
|
355
315
|
const selectedCurrency = __classPrivateFieldGet(this, _PriceDataSource_getSelectedCurrency, "f").call(this);
|
|
356
316
|
const batchResults = await (0, evm_rpc_services_1.reduceInBatchesSerially)({
|
|
357
317
|
values: assetIds,
|
|
@@ -362,7 +322,6 @@ async function _PriceDataSource_executeBatchFetch(assetIds) {
|
|
|
362
322
|
},
|
|
363
323
|
initialResult: [],
|
|
364
324
|
});
|
|
365
|
-
const fetchedAt = Date.now();
|
|
366
325
|
const prices = {};
|
|
367
326
|
for (const { selectedCurrencyPrices, usdPrices } of batchResults) {
|
|
368
327
|
for (const [assetId, marketData] of Object.entries(selectedCurrencyPrices)) {
|
|
@@ -371,26 +330,16 @@ async function _PriceDataSource_executeBatchFetch(assetIds) {
|
|
|
371
330
|
!isValidMarketData(usdMarketData)) {
|
|
372
331
|
continue;
|
|
373
332
|
}
|
|
374
|
-
|
|
333
|
+
const caipAssetId = assetId;
|
|
334
|
+
prices[caipAssetId] = {
|
|
375
335
|
...marketData,
|
|
376
336
|
assetPriceType: 'fungible',
|
|
377
337
|
usdPrice: usdMarketData.price,
|
|
378
|
-
lastUpdated:
|
|
338
|
+
lastUpdated: Date.now(),
|
|
379
339
|
};
|
|
380
340
|
}
|
|
381
341
|
}
|
|
382
342
|
return prices;
|
|
383
|
-
}, _PriceDataSource_fetchSpotPrices =
|
|
384
|
-
/**
|
|
385
|
-
* Fetch spot prices for all provided asset IDs, deduplicating via the
|
|
386
|
-
* deduper (freshness TTL + per-asset inflight coalescing).
|
|
387
|
-
*
|
|
388
|
-
* @param assetIds - Array of CAIP-19 asset IDs.
|
|
389
|
-
* @returns Spot prices response (only contains entries for assets that were
|
|
390
|
-
* actually fetched or joined from inflight).
|
|
391
|
-
*/
|
|
392
|
-
async function _PriceDataSource_fetchSpotPrices(assetIds) {
|
|
393
|
-
return __classPrivateFieldGet(this, _PriceDataSource_deduper, "f").fetch(assetIds);
|
|
394
343
|
}, _PriceDataSource_getAssetIdsFromBalanceState = function _PriceDataSource_getAssetIdsFromBalanceState(request, getAssetsState) {
|
|
395
344
|
if (!getAssetsState) {
|
|
396
345
|
return [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PriceDataSource.cjs","sourceRoot":"","sources":["../../src/data-sources/PriceDataSource.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n SupportedCurrency,\n V3SpotPricesResponse,\n} from '@metamask/core-backend';\nimport { ApiPlatformClient } from '@metamask/core-backend';\nimport { parseCaipAssetType } from '@metamask/utils';\n\nimport { projectLogger, createModuleLogger } from '../logger';\nimport { forDataTypes } from '../types';\nimport type {\n Caip19AssetId,\n DataRequest,\n DataResponse,\n FungibleAssetPrice,\n Middleware,\n AssetsControllerStateInternal,\n} from '../types';\nimport { fetchWithTimeout } from '../utils';\nimport { DedupingBatchFetcher } from '../utils/dedupingBatchFetcher';\nimport type { SubscriptionRequest } from './AbstractDataSource';\nimport { reduceInBatchesSerially } from './evm-rpc-services';\n\n// ============================================================================\n// CONSTANTS\n// ============================================================================\n\nconst CONTROLLER_NAME = 'PriceDataSource';\nconst DEFAULT_POLL_INTERVAL = 60_000; // 1 minute for price updates\nconst DEFAULT_FETCH_TIMEOUT_MS = 15_000;\n\n/**\n * Fraction of the poll interval used to cap the freshness TTL. Kept strictly\n * below 1 so an asset fetched on one poll is reliably stale by the next poll;\n * the margin absorbs network latency and timer jitter (see the cap in\n * `subscribe`).\n */\nconst FRESHNESS_TTL_POLL_RATIO = 0.9;\n\n/** Maximum number of asset IDs per Price API request. */\nconst PRICE_API_BATCH_SIZE = 50;\n\nconst log = createModuleLogger(projectLogger, CONTROLLER_NAME);\n\n// ============================================================================\n// OPTIONS\n// ============================================================================\n\n/** Optional configuration for PriceDataSource. */\nexport type PriceDataSourceConfig = {\n /** Polling interval in ms (default: 60000) */\n pollInterval?: number;\n /**\n * Timeout in ms for a single Price API call (default: 15000). When it fires,\n * the batch rejects so the caller can proceed without prices.\n */\n fetchTimeoutMs?: number;\n /**\n * Minimum age (ms) before a price is considered stale and re-fetched.\n * Assets fetched more recently than this are skipped to avoid redundant\n * API calls from overlapping middleware / subscription / manual triggers.\n * Defaults to pollInterval (60 000 ms).\n */\n priceFreshnessTtlMs?: number;\n};\n\nexport type PriceDataSourceOptions = PriceDataSourceConfig & {\n /** ApiPlatformClient for API calls with caching */\n queryApiClient: ApiPlatformClient;\n /** Function returning the currently-active ISO 4217 currency code */\n getSelectedCurrency: () => SupportedCurrency;\n};\n\n// ============================================================================\n// HELPER FUNCTIONS\n// ============================================================================\n\n/**\n * Asset reference patterns that should NOT be sent to the Price API.\n * These are internal resource tracking values without market prices.\n */\nconst NON_PRICEABLE_ASSET_PATTERNS = [\n // Synthetic slip44 staking-position assets: the Price API only knows about\n // pure numeric coin-type references (e.g. slip44:195). Any suffix after the\n // number (e.g. slip44:195-ready-for-withdrawal, slip44:195-in-lock-period,\n // slip44:195-staking-rewards, slip44:195-staked-for-…) is a MetaMask-internal\n // synthetic asset that has no market price.\n /\\/slip44:\\d+-/u,\n // Tron non-price resource assets (bandwidth, energy)\n /\\/slip44:bandwidth$/u,\n /\\/slip44:energy$/u,\n /\\/slip44:maximum-bandwidth$/u,\n /\\/slip44:maximum-energy$/u,\n];\n\n/**\n * Check if an asset ID represents a priceable asset.\n * Filters out internal resource tracking values that don't have market prices.\n *\n * @param assetId - The CAIP-19 asset ID to check.\n * @returns True if the asset has market price data.\n */\nfunction isPriceableAsset(assetId: Caip19AssetId): boolean {\n return !NON_PRICEABLE_ASSET_PATTERNS.some((pattern) => pattern.test(assetId));\n}\n\n/** Market data item from spot prices response (same as FungibleAssetPrice without lastUpdated) */\ntype SpotPriceMarketData = Omit<\n FungibleAssetPrice,\n 'lastUpdated' | 'assetPriceType'\n>;\n\n/**\n * Type guard to check if market data has a valid price\n *\n * @param data - The data to check.\n * @returns True if data is valid SpotPriceMarketData.\n */\nfunction isValidMarketData(data: unknown): data is SpotPriceMarketData {\n return (\n typeof data === 'object' &&\n data !== null &&\n typeof (data as Record<string, unknown>).price === 'number'\n );\n}\n\n// ============================================================================\n// PRICE DATA SOURCE\n// ============================================================================\n\n/**\n * PriceDataSource fetches asset prices from the Price API.\n *\n * This data source:\n * - Fetches prices from Price API v3 spot-prices endpoint\n * - Supports one-time fetch and subscription-based polling\n * - In subscribe mode, uses getAssetsState from SubscriptionRequest to read assetsBalance and fetch prices\n *\n * Usage: Create with queryApiClient; subscribe() requires getAssetsState in the request for balance-based pricing.\n */\nexport class PriceDataSource {\n static readonly controllerName = CONTROLLER_NAME;\n\n getName(): string {\n return PriceDataSource.controllerName;\n }\n\n readonly #getSelectedCurrency: () => SupportedCurrency;\n\n readonly #pollInterval: number;\n\n /** ApiPlatformClient for cached API calls */\n readonly #apiClient: ApiPlatformClient;\n\n readonly #fetchTimeoutMs: number;\n\n /**\n * Deduplicates price fetches by asset ID: skips assets fetched within the\n * freshness TTL and joins concurrent in-flight fetches for the same asset so\n * overlapping triggers (middleware + subscription poll) don't issue duplicate\n * API requests.\n */\n readonly #deduper: DedupingBatchFetcher<Caip19AssetId, FungibleAssetPrice>;\n\n /** Active subscriptions by ID */\n readonly #activeSubscriptions: Map<\n string,\n {\n cleanup: () => void;\n request: DataRequest;\n onAssetsUpdate: (response: DataResponse) => void | Promise<void>;\n getAssetsState?: () => AssetsControllerStateInternal;\n }\n > = new Map();\n\n constructor(options: PriceDataSourceOptions) {\n this.#getSelectedCurrency = options.getSelectedCurrency;\n this.#pollInterval = options.pollInterval ?? DEFAULT_POLL_INTERVAL;\n this.#apiClient = options.queryApiClient;\n this.#fetchTimeoutMs = options.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;\n this.#deduper = new DedupingBatchFetcher({\n fetchBatch: (\n assetIds,\n ): Promise<Record<Caip19AssetId, FungibleAssetPrice>> =>\n this.#executeBatchFetch(assetIds),\n freshnessTtlMs: options.priceFreshnessTtlMs ?? this.#pollInterval,\n });\n }\n\n // ============================================================================\n // MIDDLEWARE\n // ============================================================================\n\n /**\n * Get the middleware for enriching responses with price data.\n *\n * This middleware:\n * 1. Extracts the response from context\n * 2. Fetches prices for detected assets (assets without metadata)\n * 3. Enriches the response with fetched prices\n * 4. Calls next() at the end to continue the middleware chain\n *\n * Note: This middleware ONLY fetches prices for detected assets.\n * For fetching prices for all assets, use the subscription mechanism\n * which polls prices for all assets in the balance state.\n *\n * @returns The middleware function for the assets pipeline.\n */\n get assetsMiddleware(): Middleware {\n return forDataTypes(['price'], async (ctx, next) => {\n // Extract response from context\n const { response, request } = ctx;\n\n // Only fetch prices for detected assets (assets without metadata)\n // The subscription handles fetching prices for all existing assets\n if (!response.detectedAssets && !request.assetsForPriceUpdate?.length) {\n return next(ctx);\n }\n\n const assetIds = new Set<Caip19AssetId>();\n for (const detectedAccountAssets of Object.values(\n response.detectedAssets ?? {},\n )) {\n for (const assetId of detectedAccountAssets) {\n assetIds.add(assetId);\n }\n }\n\n for (const assetId of request.assetsForPriceUpdate ?? []) {\n assetIds.add(assetId);\n }\n\n if (assetIds.size === 0) {\n return next(ctx);\n }\n\n // Filter to only priceable assets\n const priceableAssetIds = [...assetIds].filter(isPriceableAsset);\n\n if (priceableAssetIds.length === 0) {\n return next(ctx);\n }\n\n try {\n const spotPrices = await this.#fetchSpotPrices(priceableAssetIds);\n response.assetsPrice = {\n ...(response.assetsPrice ?? {}),\n ...spotPrices,\n };\n } catch (error) {\n log('Failed to fetch prices via middleware', { error });\n }\n\n // Call next() at the end to continue the middleware chain\n return next(ctx);\n });\n }\n\n // ============================================================================\n // HELPERS\n // ============================================================================\n\n /**\n * Fetch spot prices for a single batch of asset IDs (must be ≤ PRICE_API_BATCH_SIZE).\n *\n * @param assetIds - Array of CAIP-19 asset IDs (already within batch size limit).\n * @param selectedCurrency - The user's selected display currency.\n * @returns Raw spot-prices responses for the selected currency and USD.\n */\n async #fetchSpotPricesBatch(\n assetIds: string[],\n selectedCurrency: SupportedCurrency,\n ): Promise<{\n selectedCurrencyPrices: V3SpotPricesResponse;\n usdPrices: V3SpotPricesResponse;\n }> {\n if (selectedCurrency === 'usd') {\n const selectedCurrencyPrices = await fetchWithTimeout(\n () =>\n this.#apiClient.prices.fetchV3SpotPrices(assetIds, {\n currency: selectedCurrency,\n includeMarketData: true,\n }),\n this.#fetchTimeoutMs,\n );\n return { selectedCurrencyPrices, usdPrices: selectedCurrencyPrices };\n }\n\n const [selectedCurrencyPrices, usdPrices] = await Promise.all([\n fetchWithTimeout(\n () =>\n this.#apiClient.prices.fetchV3SpotPrices(assetIds, {\n currency: selectedCurrency,\n includeMarketData: true,\n }),\n this.#fetchTimeoutMs,\n ),\n fetchWithTimeout(\n () =>\n this.#apiClient.prices.fetchV3SpotPrices(assetIds, {\n currency: 'usd',\n includeMarketData: true,\n }),\n this.#fetchTimeoutMs,\n ),\n ]);\n\n return { selectedCurrencyPrices, usdPrices };\n }\n\n /**\n * Execute the actual batched API call for a set of asset IDs and return\n * parsed price results. Used as the `fetchBatch` callback for the deduper,\n * so it does NOT check freshness or inflight state — that is handled by\n * {@link DedupingBatchFetcher}.\n *\n * @param assetIds - Asset IDs to fetch (already filtered/deduplicated).\n * @returns Parsed prices keyed by CAIP-19 asset ID.\n */\n async #executeBatchFetch(\n assetIds: Caip19AssetId[],\n ): Promise<Record<Caip19AssetId, FungibleAssetPrice>> {\n const selectedCurrency = this.#getSelectedCurrency();\n\n type BatchResult = {\n selectedCurrencyPrices: V3SpotPricesResponse;\n usdPrices: V3SpotPricesResponse;\n };\n\n const batchResults = await reduceInBatchesSerially<string, BatchResult[]>({\n values: assetIds,\n batchSize: PRICE_API_BATCH_SIZE,\n eachBatch: async (workingResult, batch) => {\n const result = await this.#fetchSpotPricesBatch(\n batch,\n selectedCurrency,\n );\n return [...(workingResult as BatchResult[]), result];\n },\n initialResult: [],\n });\n\n const fetchedAt = Date.now();\n const prices: Record<Caip19AssetId, FungibleAssetPrice> = {};\n\n for (const { selectedCurrencyPrices, usdPrices } of batchResults) {\n for (const [assetId, marketData] of Object.entries(\n selectedCurrencyPrices,\n )) {\n const usdMarketData = usdPrices[assetId];\n\n if (\n !isValidMarketData(marketData) ||\n !isValidMarketData(usdMarketData)\n ) {\n continue;\n }\n\n prices[assetId as Caip19AssetId] = {\n ...marketData,\n assetPriceType: 'fungible',\n usdPrice: usdMarketData.price,\n lastUpdated: fetchedAt,\n };\n }\n }\n\n return prices;\n }\n\n /**\n * Fetch spot prices for all provided asset IDs, deduplicating via the\n * deduper (freshness TTL + per-asset inflight coalescing).\n *\n * @param assetIds - Array of CAIP-19 asset IDs.\n * @returns Spot prices response (only contains entries for assets that were\n * actually fetched or joined from inflight).\n */\n async #fetchSpotPrices(\n assetIds: Caip19AssetId[],\n ): Promise<Record<Caip19AssetId, FungibleAssetPrice>> {\n return this.#deduper.fetch(assetIds);\n }\n\n /**\n * Get unique asset IDs from the assetsBalance state.\n * Filters by accounts and chains from the request.\n *\n * @param request - Data request with accounts and chainIds filters.\n * @param getAssetsState - State access; when omitted, returns [].\n * @returns Array of CAIP-19 asset IDs from balance state.\n */\n #getAssetIdsFromBalanceState(\n request: DataRequest,\n getAssetsState?: () => AssetsControllerStateInternal,\n ): Caip19AssetId[] {\n if (!getAssetsState) {\n return [];\n }\n try {\n const state = getAssetsState();\n const assetIds = new Set<Caip19AssetId>();\n\n const accountIds = request.accountsWithSupportedChains.map(\n (a) => a.account.id,\n );\n const accountFilter =\n accountIds.length > 0 ? new Set(accountIds) : undefined;\n const chainFilter =\n request.chainIds.length > 0 ? new Set(request.chainIds) : undefined;\n\n if (state?.assetsBalance) {\n for (const [accountId, accountBalances] of Object.entries(\n state.assetsBalance,\n )) {\n // Filter by account if specified\n if (accountFilter && !accountFilter.has(accountId)) {\n continue;\n }\n\n for (const assetId of Object.keys(\n accountBalances as Record<string, unknown>,\n )) {\n // Filter by chain if specified; skip malformed asset IDs for this entry only\n if (chainFilter) {\n try {\n const { chainId } = parseCaipAssetType(\n assetId as Caip19AssetId,\n );\n if (!chainFilter.has(chainId)) {\n continue;\n }\n } catch (error) {\n log('Skipping malformed asset ID in balance state', {\n assetId,\n error,\n });\n continue;\n }\n }\n assetIds.add(assetId as Caip19AssetId);\n }\n }\n }\n\n return [...assetIds];\n } catch (error) {\n log('Failed to get asset IDs from balance state', { error });\n return [];\n }\n }\n\n // ============================================================================\n // FETCH\n // ============================================================================\n\n /**\n * Fetch prices for assets held by the accounts and chains in the request.\n * When getAssetsState is provided, gets asset IDs from balance state; otherwise returns empty.\n *\n * @param request - The data request specifying accounts and chains.\n * @param getAssetsState - Optional state access (e.g. from SubscriptionRequest).\n * @returns DataResponse containing asset prices.\n */\n async fetch(\n request: DataRequest,\n getAssetsState?: () => AssetsControllerStateInternal,\n ): Promise<DataResponse> {\n const response: DataResponse = {};\n\n // Get asset IDs from balance state when state access is provided\n const rawAssetIds = this.#getAssetIdsFromBalanceState(\n request,\n getAssetsState,\n );\n\n // Filter out non-priceable assets (e.g., Tron bandwidth/energy resources)\n const assetIds = rawAssetIds.filter(isPriceableAsset);\n\n if (assetIds.length === 0) {\n return response;\n }\n\n try {\n const spotPrices = await this.#fetchSpotPrices([...assetIds]);\n\n response.assetsPrice = {\n ...(response.assetsPrice ?? {}),\n ...spotPrices,\n };\n } catch (error) {\n log('Failed to fetch prices', { error });\n }\n\n return response;\n }\n\n // ============================================================================\n // SUBSCRIBE\n // ============================================================================\n\n /**\n * Subscribe to price updates.\n * Sets up polling that fetches prices for all assets in assetsBalance state.\n *\n * @param subscriptionRequest - The subscription request configuration.\n */\n async subscribe(subscriptionRequest: SubscriptionRequest): Promise<void> {\n const { request, subscriptionId, isUpdate } = subscriptionRequest;\n\n // Handle subscription update - just update the request\n if (isUpdate) {\n const existing = this.#activeSubscriptions.get(subscriptionId);\n if (existing) {\n existing.request = request;\n return;\n }\n }\n\n // Clean up existing subscription\n await this.unsubscribe(subscriptionId);\n\n const pollInterval = request.updateInterval ?? this.#pollInterval;\n\n // Cap the freshness TTL strictly below the effective poll interval.\n // `fetchedAt` is stamped when a fetch completes (slightly after the tick\n // that triggered it), so a TTL equal to the poll interval would leave the\n // asset still \"fresh\" at the next tick, making the subscription re-fetch\n // only every other poll. The margin also absorbs network latency / jitter.\n this.#deduper.freshnessTtlMs = Math.min(\n this.#deduper.freshnessTtlMs,\n Math.floor(pollInterval * FRESHNESS_TTL_POLL_RATIO),\n );\n\n // Create poll function - fetches prices using getAssetsState from subscription.\n // The freshness TTL naturally gates re-fetches: assets fetched less than\n // `priceFreshnessTtlMs` ago are skipped, preventing duplicates when middleware\n // or other triggers already fetched the same assets between polls.\n // Concurrent middleware calls will join the inflight promise rather than\n // issuing duplicate requests.\n const pollFn = async (): Promise<void> => {\n try {\n const subscription = this.#activeSubscriptions.get(subscriptionId);\n if (!subscription) {\n return;\n }\n\n const fetchResponse = await this.fetch(\n subscription.request,\n subscription.getAssetsState,\n );\n\n // Only report if we got prices\n if (\n fetchResponse.assetsPrice &&\n Object.keys(fetchResponse.assetsPrice).length > 0\n ) {\n await subscription.onAssetsUpdate({\n ...fetchResponse,\n updateMode: 'merge',\n });\n }\n } catch (error) {\n log('Subscription poll failed', { subscriptionId, error });\n }\n };\n\n // Set up polling\n const timer = setInterval(() => {\n pollFn().catch(console.error);\n }, pollInterval);\n\n // Store subscription (getAssetsState from request for balance-based pricing)\n this.#activeSubscriptions.set(subscriptionId, {\n cleanup: () => {\n clearInterval(timer);\n },\n request,\n onAssetsUpdate: subscriptionRequest.onAssetsUpdate,\n getAssetsState: subscriptionRequest.getAssetsState,\n });\n\n // Initial fetch\n await pollFn();\n }\n\n /**\n * Unsubscribe from price updates.\n *\n * @param subscriptionId - The ID of the subscription to cancel.\n */\n async unsubscribe(subscriptionId: string): Promise<void> {\n const subscription = this.#activeSubscriptions.get(subscriptionId);\n if (subscription) {\n subscription.cleanup();\n this.#activeSubscriptions.delete(subscriptionId);\n }\n }\n\n /**\n * Invalidate the price freshness cache, forcing the next fetch to call the\n * API regardless of TTL. Use when external state changes (e.g. selected\n * currency) require a full refresh.\n */\n invalidatePriceCache(): void {\n this.#deduper.invalidate();\n }\n\n /**\n * Destroy the data source and clean up all subscriptions.\n */\n destroy(): void {\n for (const subscription of this.#activeSubscriptions.values()) {\n subscription.cleanup();\n }\n this.#activeSubscriptions.clear();\n this.#deduper.destroy();\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"PriceDataSource.cjs","sourceRoot":"","sources":["../../src/data-sources/PriceDataSource.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n SupportedCurrency,\n V3SpotPricesResponse,\n} from '@metamask/core-backend';\nimport { ApiPlatformClient } from '@metamask/core-backend';\nimport { parseCaipAssetType } from '@metamask/utils';\n\nimport { projectLogger, createModuleLogger } from '../logger';\nimport { forDataTypes } from '../types';\nimport type {\n Caip19AssetId,\n DataRequest,\n DataResponse,\n FungibleAssetPrice,\n Middleware,\n AssetsControllerStateInternal,\n} from '../types';\nimport { fetchWithTimeout } from '../utils';\nimport type { SubscriptionRequest } from './AbstractDataSource';\nimport { reduceInBatchesSerially } from './evm-rpc-services';\n\n// ============================================================================\n// CONSTANTS\n// ============================================================================\n\nconst CONTROLLER_NAME = 'PriceDataSource';\nconst DEFAULT_POLL_INTERVAL = 60_000; // 1 minute for price updates\nconst DEFAULT_FETCH_TIMEOUT_MS = 15_000;\n\n/** Maximum number of asset IDs per Price API request. */\nconst PRICE_API_BATCH_SIZE = 50;\n\nconst log = createModuleLogger(projectLogger, CONTROLLER_NAME);\n\n// ============================================================================\n// OPTIONS\n// ============================================================================\n\n/** Optional configuration for PriceDataSource. */\nexport type PriceDataSourceConfig = {\n /** Polling interval in ms (default: 60000) */\n pollInterval?: number;\n /**\n * Timeout in ms for a single Price API call (default: 15000). When it fires,\n * the batch rejects so the caller can proceed without prices.\n */\n fetchTimeoutMs?: number;\n};\n\nexport type PriceDataSourceOptions = PriceDataSourceConfig & {\n /** ApiPlatformClient for API calls with caching */\n queryApiClient: ApiPlatformClient;\n /** Function returning the currently-active ISO 4217 currency code */\n getSelectedCurrency: () => SupportedCurrency;\n};\n\n// ============================================================================\n// HELPER FUNCTIONS\n// ============================================================================\n\n/**\n * Asset reference patterns that should NOT be sent to the Price API.\n * These are internal resource tracking values without market prices.\n */\nconst NON_PRICEABLE_ASSET_PATTERNS = [\n // Synthetic slip44 staking-position assets: the Price API only knows about\n // pure numeric coin-type references (e.g. slip44:195). Any suffix after the\n // number (e.g. slip44:195-ready-for-withdrawal, slip44:195-in-lock-period,\n // slip44:195-staking-rewards, slip44:195-staked-for-…) is a MetaMask-internal\n // synthetic asset that has no market price.\n /\\/slip44:\\d+-/u,\n // Tron non-price resource assets (bandwidth, energy)\n /\\/slip44:bandwidth$/u,\n /\\/slip44:energy$/u,\n /\\/slip44:maximum-bandwidth$/u,\n /\\/slip44:maximum-energy$/u,\n];\n\n/**\n * Check if an asset ID represents a priceable asset.\n * Filters out internal resource tracking values that don't have market prices.\n *\n * @param assetId - The CAIP-19 asset ID to check.\n * @returns True if the asset has market price data.\n */\nfunction isPriceableAsset(assetId: Caip19AssetId): boolean {\n return !NON_PRICEABLE_ASSET_PATTERNS.some((pattern) => pattern.test(assetId));\n}\n\n/** Market data item from spot prices response (same as FungibleAssetPrice without lastUpdated) */\ntype SpotPriceMarketData = Omit<\n FungibleAssetPrice,\n 'lastUpdated' | 'assetPriceType'\n>;\n\n/**\n * Type guard to check if market data has a valid price\n *\n * @param data - The data to check.\n * @returns True if data is valid SpotPriceMarketData.\n */\nfunction isValidMarketData(data: unknown): data is SpotPriceMarketData {\n return (\n typeof data === 'object' &&\n data !== null &&\n typeof (data as Record<string, unknown>).price === 'number'\n );\n}\n\n// ============================================================================\n// PRICE DATA SOURCE\n// ============================================================================\n\n/**\n * PriceDataSource fetches asset prices from the Price API.\n *\n * This data source:\n * - Fetches prices from Price API v3 spot-prices endpoint\n * - Supports one-time fetch and subscription-based polling\n * - In subscribe mode, uses getAssetsState from SubscriptionRequest to read assetsBalance and fetch prices\n *\n * Usage: Create with queryApiClient; subscribe() requires getAssetsState in the request for balance-based pricing.\n */\nexport class PriceDataSource {\n static readonly controllerName = CONTROLLER_NAME;\n\n getName(): string {\n return PriceDataSource.controllerName;\n }\n\n readonly #getSelectedCurrency: () => SupportedCurrency;\n\n readonly #pollInterval: number;\n\n /** ApiPlatformClient for cached API calls */\n readonly #apiClient: ApiPlatformClient;\n\n readonly #fetchTimeoutMs: number;\n\n /** Active subscriptions by ID */\n readonly #activeSubscriptions: Map<\n string,\n {\n cleanup: () => void;\n request: DataRequest;\n onAssetsUpdate: (response: DataResponse) => void | Promise<void>;\n getAssetsState?: () => AssetsControllerStateInternal;\n }\n > = new Map();\n\n constructor(options: PriceDataSourceOptions) {\n this.#getSelectedCurrency = options.getSelectedCurrency;\n this.#pollInterval = options.pollInterval ?? DEFAULT_POLL_INTERVAL;\n this.#apiClient = options.queryApiClient;\n this.#fetchTimeoutMs = options.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;\n }\n\n // ============================================================================\n // MIDDLEWARE\n // ============================================================================\n\n /**\n * Get the middleware for enriching responses with price data.\n *\n * This middleware:\n * 1. Extracts the response from context\n * 2. Fetches prices for detected assets (assets without metadata)\n * 3. Enriches the response with fetched prices\n * 4. Calls next() at the end to continue the middleware chain\n *\n * Note: This middleware ONLY fetches prices for detected assets.\n * For fetching prices for all assets, use the subscription mechanism\n * which polls prices for all assets in the balance state.\n *\n * @returns The middleware function for the assets pipeline.\n */\n get assetsMiddleware(): Middleware {\n return forDataTypes(['price'], async (ctx, next) => {\n // Extract response from context\n const { response, request } = ctx;\n\n // Only fetch prices for detected assets (assets without metadata)\n // The subscription handles fetching prices for all existing assets\n if (!response.detectedAssets && !request.assetsForPriceUpdate?.length) {\n return next(ctx);\n }\n\n const assetIds = new Set<Caip19AssetId>();\n for (const detectedAccountAssets of Object.values(\n response.detectedAssets ?? {},\n )) {\n for (const assetId of detectedAccountAssets) {\n assetIds.add(assetId);\n }\n }\n\n for (const assetId of request.assetsForPriceUpdate ?? []) {\n assetIds.add(assetId);\n }\n\n if (assetIds.size === 0) {\n return next(ctx);\n }\n\n // Filter to only priceable assets\n const priceableAssetIds = [...assetIds].filter(isPriceableAsset);\n\n if (priceableAssetIds.length === 0) {\n return next(ctx);\n }\n\n try {\n const spotPrices = await this.#fetchSpotPrices(priceableAssetIds);\n response.assetsPrice = {\n ...(response.assetsPrice ?? {}),\n ...spotPrices,\n };\n } catch (error) {\n log('Failed to fetch prices via middleware', { error });\n }\n\n // Call next() at the end to continue the middleware chain\n return next(ctx);\n });\n }\n\n // ============================================================================\n // HELPERS\n // ============================================================================\n\n /**\n * Fetch spot prices for a single batch of asset IDs (must be ≤ PRICE_API_BATCH_SIZE).\n *\n * @param assetIds - Array of CAIP-19 asset IDs (already within batch size limit).\n * @param selectedCurrency - The user's selected display currency.\n * @returns Raw spot-prices responses for the selected currency and USD.\n */\n async #fetchSpotPricesBatch(\n assetIds: string[],\n selectedCurrency: SupportedCurrency,\n ): Promise<{\n selectedCurrencyPrices: V3SpotPricesResponse;\n usdPrices: V3SpotPricesResponse;\n }> {\n if (selectedCurrency === 'usd') {\n const selectedCurrencyPrices = await fetchWithTimeout(\n () =>\n this.#apiClient.prices.fetchV3SpotPrices(assetIds, {\n currency: selectedCurrency,\n includeMarketData: true,\n }),\n this.#fetchTimeoutMs,\n );\n return { selectedCurrencyPrices, usdPrices: selectedCurrencyPrices };\n }\n\n const [selectedCurrencyPrices, usdPrices] = await Promise.all([\n fetchWithTimeout(\n () =>\n this.#apiClient.prices.fetchV3SpotPrices(assetIds, {\n currency: selectedCurrency,\n includeMarketData: true,\n }),\n this.#fetchTimeoutMs,\n ),\n fetchWithTimeout(\n () =>\n this.#apiClient.prices.fetchV3SpotPrices(assetIds, {\n currency: 'usd',\n includeMarketData: true,\n }),\n this.#fetchTimeoutMs,\n ),\n ]);\n\n return { selectedCurrencyPrices, usdPrices };\n }\n\n /**\n * Fetch spot prices for all provided asset IDs, splitting into batches of\n * PRICE_API_BATCH_SIZE to respect API limits.\n *\n * @param assetIds - Array of CAIP-19 asset IDs\n * @returns Spot prices response\n */\n async #fetchSpotPrices(\n assetIds: string[],\n ): Promise<Record<Caip19AssetId, FungibleAssetPrice>> {\n const selectedCurrency = this.#getSelectedCurrency();\n\n type BatchResult = {\n selectedCurrencyPrices: V3SpotPricesResponse;\n usdPrices: V3SpotPricesResponse;\n };\n\n const batchResults = await reduceInBatchesSerially<string, BatchResult[]>({\n values: assetIds,\n batchSize: PRICE_API_BATCH_SIZE,\n eachBatch: async (workingResult, batch) => {\n const result = await this.#fetchSpotPricesBatch(\n batch,\n selectedCurrency,\n );\n return [...(workingResult as BatchResult[]), result];\n },\n initialResult: [],\n });\n\n const prices: Record<Caip19AssetId, FungibleAssetPrice> = {};\n\n for (const { selectedCurrencyPrices, usdPrices } of batchResults) {\n for (const [assetId, marketData] of Object.entries(\n selectedCurrencyPrices,\n )) {\n const usdMarketData = usdPrices[assetId];\n\n if (\n !isValidMarketData(marketData) ||\n !isValidMarketData(usdMarketData)\n ) {\n continue;\n }\n\n const caipAssetId = assetId as Caip19AssetId;\n prices[caipAssetId] = {\n ...marketData,\n assetPriceType: 'fungible',\n usdPrice: usdMarketData.price,\n lastUpdated: Date.now(),\n };\n }\n }\n\n return prices;\n }\n\n /**\n * Get unique asset IDs from the assetsBalance state.\n * Filters by accounts and chains from the request.\n *\n * @param request - Data request with accounts and chainIds filters.\n * @param getAssetsState - State access; when omitted, returns [].\n * @returns Array of CAIP-19 asset IDs from balance state.\n */\n #getAssetIdsFromBalanceState(\n request: DataRequest,\n getAssetsState?: () => AssetsControllerStateInternal,\n ): Caip19AssetId[] {\n if (!getAssetsState) {\n return [];\n }\n try {\n const state = getAssetsState();\n const assetIds = new Set<Caip19AssetId>();\n\n const accountIds = request.accountsWithSupportedChains.map(\n (a) => a.account.id,\n );\n const accountFilter =\n accountIds.length > 0 ? new Set(accountIds) : undefined;\n const chainFilter =\n request.chainIds.length > 0 ? new Set(request.chainIds) : undefined;\n\n if (state?.assetsBalance) {\n for (const [accountId, accountBalances] of Object.entries(\n state.assetsBalance,\n )) {\n // Filter by account if specified\n if (accountFilter && !accountFilter.has(accountId)) {\n continue;\n }\n\n for (const assetId of Object.keys(\n accountBalances as Record<string, unknown>,\n )) {\n // Filter by chain if specified; skip malformed asset IDs for this entry only\n if (chainFilter) {\n try {\n const { chainId } = parseCaipAssetType(\n assetId as Caip19AssetId,\n );\n if (!chainFilter.has(chainId)) {\n continue;\n }\n } catch (error) {\n log('Skipping malformed asset ID in balance state', {\n assetId,\n error,\n });\n continue;\n }\n }\n assetIds.add(assetId as Caip19AssetId);\n }\n }\n }\n\n return [...assetIds];\n } catch (error) {\n log('Failed to get asset IDs from balance state', { error });\n return [];\n }\n }\n\n // ============================================================================\n // FETCH\n // ============================================================================\n\n /**\n * Fetch prices for assets held by the accounts and chains in the request.\n * When getAssetsState is provided, gets asset IDs from balance state; otherwise returns empty.\n *\n * @param request - The data request specifying accounts and chains.\n * @param getAssetsState - Optional state access (e.g. from SubscriptionRequest).\n * @returns DataResponse containing asset prices.\n */\n async fetch(\n request: DataRequest,\n getAssetsState?: () => AssetsControllerStateInternal,\n ): Promise<DataResponse> {\n const response: DataResponse = {};\n\n // Get asset IDs from balance state when state access is provided\n const rawAssetIds = this.#getAssetIdsFromBalanceState(\n request,\n getAssetsState,\n );\n\n // Filter out non-priceable assets (e.g., Tron bandwidth/energy resources)\n const assetIds = rawAssetIds.filter(isPriceableAsset);\n\n if (assetIds.length === 0) {\n return response;\n }\n\n try {\n const spotPrices = await this.#fetchSpotPrices([...assetIds]);\n\n response.assetsPrice = {\n ...(response.assetsPrice ?? {}),\n ...spotPrices,\n };\n } catch (error) {\n log('Failed to fetch prices', { error });\n }\n\n return response;\n }\n\n // ============================================================================\n // SUBSCRIBE\n // ============================================================================\n\n /**\n * Subscribe to price updates.\n * Sets up polling that fetches prices for all assets in assetsBalance state.\n *\n * @param subscriptionRequest - The subscription request configuration.\n */\n async subscribe(subscriptionRequest: SubscriptionRequest): Promise<void> {\n const { request, subscriptionId, isUpdate } = subscriptionRequest;\n\n // Handle subscription update - just update the request\n if (isUpdate) {\n const existing = this.#activeSubscriptions.get(subscriptionId);\n if (existing) {\n existing.request = request;\n return;\n }\n }\n\n // Clean up existing subscription\n await this.unsubscribe(subscriptionId);\n\n const pollInterval = request.updateInterval ?? this.#pollInterval;\n\n // Create poll function - fetches prices using getAssetsState from subscription\n const pollFn = async (): Promise<void> => {\n try {\n const subscription = this.#activeSubscriptions.get(subscriptionId);\n if (!subscription) {\n return;\n }\n\n // Fetch prices for all assets in balance state (uses subscription's getAssetsState)\n const fetchResponse = await this.fetch(\n subscription.request,\n subscription.getAssetsState,\n );\n\n // Only report if we got prices\n if (\n fetchResponse.assetsPrice &&\n Object.keys(fetchResponse.assetsPrice).length > 0\n ) {\n await subscription.onAssetsUpdate({\n ...fetchResponse,\n updateMode: 'merge',\n });\n }\n } catch (error) {\n log('Subscription poll failed', { subscriptionId, error });\n }\n };\n\n // Set up polling\n const timer = setInterval(() => {\n pollFn().catch(console.error);\n }, pollInterval);\n\n // Store subscription (getAssetsState from request for balance-based pricing)\n this.#activeSubscriptions.set(subscriptionId, {\n cleanup: () => {\n clearInterval(timer);\n },\n request,\n onAssetsUpdate: subscriptionRequest.onAssetsUpdate,\n getAssetsState: subscriptionRequest.getAssetsState,\n });\n\n // Initial fetch\n await pollFn();\n }\n\n /**\n * Unsubscribe from price updates.\n *\n * @param subscriptionId - The ID of the subscription to cancel.\n */\n async unsubscribe(subscriptionId: string): Promise<void> {\n const subscription = this.#activeSubscriptions.get(subscriptionId);\n if (subscription) {\n subscription.cleanup();\n this.#activeSubscriptions.delete(subscriptionId);\n }\n }\n\n /**\n * Destroy the data source and clean up all subscriptions.\n */\n destroy(): void {\n for (const subscription of this.#activeSubscriptions.values()) {\n subscription.cleanup();\n }\n this.#activeSubscriptions.clear();\n }\n}\n"]}
|
|
@@ -11,13 +11,6 @@ export type PriceDataSourceConfig = {
|
|
|
11
11
|
* the batch rejects so the caller can proceed without prices.
|
|
12
12
|
*/
|
|
13
13
|
fetchTimeoutMs?: number;
|
|
14
|
-
/**
|
|
15
|
-
* Minimum age (ms) before a price is considered stale and re-fetched.
|
|
16
|
-
* Assets fetched more recently than this are skipped to avoid redundant
|
|
17
|
-
* API calls from overlapping middleware / subscription / manual triggers.
|
|
18
|
-
* Defaults to pollInterval (60 000 ms).
|
|
19
|
-
*/
|
|
20
|
-
priceFreshnessTtlMs?: number;
|
|
21
14
|
};
|
|
22
15
|
export type PriceDataSourceOptions = PriceDataSourceConfig & {
|
|
23
16
|
/** ApiPlatformClient for API calls with caching */
|
|
@@ -78,12 +71,6 @@ export declare class PriceDataSource {
|
|
|
78
71
|
* @param subscriptionId - The ID of the subscription to cancel.
|
|
79
72
|
*/
|
|
80
73
|
unsubscribe(subscriptionId: string): Promise<void>;
|
|
81
|
-
/**
|
|
82
|
-
* Invalidate the price freshness cache, forcing the next fetch to call the
|
|
83
|
-
* API regardless of TTL. Use when external state changes (e.g. selected
|
|
84
|
-
* currency) require a full refresh.
|
|
85
|
-
*/
|
|
86
|
-
invalidatePriceCache(): void;
|
|
87
74
|
/**
|
|
88
75
|
* Destroy the data source and clean up all subscriptions.
|
|
89
76
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PriceDataSource.d.cts","sourceRoot":"","sources":["../../src/data-sources/PriceDataSource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EAElB,+BAA+B;AAChC,OAAO,EAAE,iBAAiB,EAAE,+BAA+B;AAK3D,OAAO,KAAK,EAEV,WAAW,EACX,YAAY,EAEZ,UAAU,EACV,6BAA6B,EAC9B,qBAAiB;
|
|
1
|
+
{"version":3,"file":"PriceDataSource.d.cts","sourceRoot":"","sources":["../../src/data-sources/PriceDataSource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EAElB,+BAA+B;AAChC,OAAO,EAAE,iBAAiB,EAAE,+BAA+B;AAK3D,OAAO,KAAK,EAEV,WAAW,EACX,YAAY,EAEZ,UAAU,EACV,6BAA6B,EAC9B,qBAAiB;AAElB,OAAO,KAAK,EAAE,mBAAmB,EAAE,iCAA6B;AAoBhE,kDAAkD;AAClD,MAAM,MAAM,qBAAqB,GAAG;IAClC,8CAA8C;IAC9C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG,qBAAqB,GAAG;IAC3D,mDAAmD;IACnD,cAAc,EAAE,iBAAiB,CAAC;IAClC,qEAAqE;IACrE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;CAC9C,CAAC;AA2DF;;;;;;;;;GASG;AACH,qBAAa,eAAe;;IAC1B,MAAM,CAAC,QAAQ,CAAC,cAAc,qBAAmB;IAEjD,OAAO,IAAI,MAAM;gBAwBL,OAAO,EAAE,sBAAsB;IAW3C;;;;;;;;;;;;;;OAcG;IACH,IAAI,gBAAgB,IAAI,UAAU,CAgDjC;IAwLD;;;;;;;OAOG;IACG,KAAK,CACT,OAAO,EAAE,WAAW,EACpB,cAAc,CAAC,EAAE,MAAM,6BAA6B,GACnD,OAAO,CAAC,YAAY,CAAC;IAkCxB;;;;;OAKG;IACG,SAAS,CAAC,mBAAmB,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAiExE;;;;OAIG;IACG,WAAW,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQxD;;OAEG;IACH,OAAO,IAAI,IAAI;CAMhB"}
|
|
@@ -11,13 +11,6 @@ export type PriceDataSourceConfig = {
|
|
|
11
11
|
* the batch rejects so the caller can proceed without prices.
|
|
12
12
|
*/
|
|
13
13
|
fetchTimeoutMs?: number;
|
|
14
|
-
/**
|
|
15
|
-
* Minimum age (ms) before a price is considered stale and re-fetched.
|
|
16
|
-
* Assets fetched more recently than this are skipped to avoid redundant
|
|
17
|
-
* API calls from overlapping middleware / subscription / manual triggers.
|
|
18
|
-
* Defaults to pollInterval (60 000 ms).
|
|
19
|
-
*/
|
|
20
|
-
priceFreshnessTtlMs?: number;
|
|
21
14
|
};
|
|
22
15
|
export type PriceDataSourceOptions = PriceDataSourceConfig & {
|
|
23
16
|
/** ApiPlatformClient for API calls with caching */
|
|
@@ -78,12 +71,6 @@ export declare class PriceDataSource {
|
|
|
78
71
|
* @param subscriptionId - The ID of the subscription to cancel.
|
|
79
72
|
*/
|
|
80
73
|
unsubscribe(subscriptionId: string): Promise<void>;
|
|
81
|
-
/**
|
|
82
|
-
* Invalidate the price freshness cache, forcing the next fetch to call the
|
|
83
|
-
* API regardless of TTL. Use when external state changes (e.g. selected
|
|
84
|
-
* currency) require a full refresh.
|
|
85
|
-
*/
|
|
86
|
-
invalidatePriceCache(): void;
|
|
87
74
|
/**
|
|
88
75
|
* Destroy the data source and clean up all subscriptions.
|
|
89
76
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PriceDataSource.d.mts","sourceRoot":"","sources":["../../src/data-sources/PriceDataSource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EAElB,+BAA+B;AAChC,OAAO,EAAE,iBAAiB,EAAE,+BAA+B;AAK3D,OAAO,KAAK,EAEV,WAAW,EACX,YAAY,EAEZ,UAAU,EACV,6BAA6B,EAC9B,qBAAiB;
|
|
1
|
+
{"version":3,"file":"PriceDataSource.d.mts","sourceRoot":"","sources":["../../src/data-sources/PriceDataSource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EAElB,+BAA+B;AAChC,OAAO,EAAE,iBAAiB,EAAE,+BAA+B;AAK3D,OAAO,KAAK,EAEV,WAAW,EACX,YAAY,EAEZ,UAAU,EACV,6BAA6B,EAC9B,qBAAiB;AAElB,OAAO,KAAK,EAAE,mBAAmB,EAAE,iCAA6B;AAoBhE,kDAAkD;AAClD,MAAM,MAAM,qBAAqB,GAAG;IAClC,8CAA8C;IAC9C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG,qBAAqB,GAAG;IAC3D,mDAAmD;IACnD,cAAc,EAAE,iBAAiB,CAAC;IAClC,qEAAqE;IACrE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;CAC9C,CAAC;AA2DF;;;;;;;;;GASG;AACH,qBAAa,eAAe;;IAC1B,MAAM,CAAC,QAAQ,CAAC,cAAc,qBAAmB;IAEjD,OAAO,IAAI,MAAM;gBAwBL,OAAO,EAAE,sBAAsB;IAW3C;;;;;;;;;;;;;;OAcG;IACH,IAAI,gBAAgB,IAAI,UAAU,CAgDjC;IAwLD;;;;;;;OAOG;IACG,KAAK,CACT,OAAO,EAAE,WAAW,EACpB,cAAc,CAAC,EAAE,MAAM,6BAA6B,GACnD,OAAO,CAAC,YAAY,CAAC;IAkCxB;;;;;OAKG;IACG,SAAS,CAAC,mBAAmB,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAiExE;;;;OAIG;IACG,WAAW,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQxD;;OAEG;IACH,OAAO,IAAI,IAAI;CAMhB"}
|
|
@@ -9,13 +9,12 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
9
9
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
10
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
11
|
};
|
|
12
|
-
var _PriceDataSource_instances, _PriceDataSource_getSelectedCurrency, _PriceDataSource_pollInterval, _PriceDataSource_apiClient, _PriceDataSource_fetchTimeoutMs,
|
|
12
|
+
var _PriceDataSource_instances, _PriceDataSource_getSelectedCurrency, _PriceDataSource_pollInterval, _PriceDataSource_apiClient, _PriceDataSource_fetchTimeoutMs, _PriceDataSource_activeSubscriptions, _PriceDataSource_fetchSpotPricesBatch, _PriceDataSource_fetchSpotPrices, _PriceDataSource_getAssetIdsFromBalanceState;
|
|
13
13
|
import { ApiPlatformClient } from "@metamask/core-backend";
|
|
14
14
|
import { parseCaipAssetType } from "@metamask/utils";
|
|
15
15
|
import { projectLogger, createModuleLogger } from "../logger.mjs";
|
|
16
16
|
import { forDataTypes } from "../types.mjs";
|
|
17
17
|
import { fetchWithTimeout } from "../utils/index.mjs";
|
|
18
|
-
import { DedupingBatchFetcher } from "../utils/dedupingBatchFetcher.mjs";
|
|
19
18
|
import { reduceInBatchesSerially } from "./evm-rpc-services/index.mjs";
|
|
20
19
|
// ============================================================================
|
|
21
20
|
// CONSTANTS
|
|
@@ -23,13 +22,6 @@ import { reduceInBatchesSerially } from "./evm-rpc-services/index.mjs";
|
|
|
23
22
|
const CONTROLLER_NAME = 'PriceDataSource';
|
|
24
23
|
const DEFAULT_POLL_INTERVAL = 60000; // 1 minute for price updates
|
|
25
24
|
const DEFAULT_FETCH_TIMEOUT_MS = 15000;
|
|
26
|
-
/**
|
|
27
|
-
* Fraction of the poll interval used to cap the freshness TTL. Kept strictly
|
|
28
|
-
* below 1 so an asset fetched on one poll is reliably stale by the next poll;
|
|
29
|
-
* the margin absorbs network latency and timer jitter (see the cap in
|
|
30
|
-
* `subscribe`).
|
|
31
|
-
*/
|
|
32
|
-
const FRESHNESS_TTL_POLL_RATIO = 0.9;
|
|
33
25
|
/** Maximum number of asset IDs per Price API request. */
|
|
34
26
|
const PRICE_API_BATCH_SIZE = 50;
|
|
35
27
|
const log = createModuleLogger(projectLogger, CONTROLLER_NAME);
|
|
@@ -98,23 +90,12 @@ export class PriceDataSource {
|
|
|
98
90
|
/** ApiPlatformClient for cached API calls */
|
|
99
91
|
_PriceDataSource_apiClient.set(this, void 0);
|
|
100
92
|
_PriceDataSource_fetchTimeoutMs.set(this, void 0);
|
|
101
|
-
/**
|
|
102
|
-
* Deduplicates price fetches by asset ID: skips assets fetched within the
|
|
103
|
-
* freshness TTL and joins concurrent in-flight fetches for the same asset so
|
|
104
|
-
* overlapping triggers (middleware + subscription poll) don't issue duplicate
|
|
105
|
-
* API requests.
|
|
106
|
-
*/
|
|
107
|
-
_PriceDataSource_deduper.set(this, void 0);
|
|
108
93
|
/** Active subscriptions by ID */
|
|
109
94
|
_PriceDataSource_activeSubscriptions.set(this, new Map());
|
|
110
95
|
__classPrivateFieldSet(this, _PriceDataSource_getSelectedCurrency, options.getSelectedCurrency, "f");
|
|
111
96
|
__classPrivateFieldSet(this, _PriceDataSource_pollInterval, options.pollInterval ?? DEFAULT_POLL_INTERVAL, "f");
|
|
112
97
|
__classPrivateFieldSet(this, _PriceDataSource_apiClient, options.queryApiClient, "f");
|
|
113
98
|
__classPrivateFieldSet(this, _PriceDataSource_fetchTimeoutMs, options.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS, "f");
|
|
114
|
-
__classPrivateFieldSet(this, _PriceDataSource_deduper, new DedupingBatchFetcher({
|
|
115
|
-
fetchBatch: (assetIds) => __classPrivateFieldGet(this, _PriceDataSource_instances, "m", _PriceDataSource_executeBatchFetch).call(this, assetIds),
|
|
116
|
-
freshnessTtlMs: options.priceFreshnessTtlMs ?? __classPrivateFieldGet(this, _PriceDataSource_pollInterval, "f"),
|
|
117
|
-
}), "f");
|
|
118
99
|
}
|
|
119
100
|
// ============================================================================
|
|
120
101
|
// MIDDLEWARE
|
|
@@ -228,24 +209,14 @@ export class PriceDataSource {
|
|
|
228
209
|
// Clean up existing subscription
|
|
229
210
|
await this.unsubscribe(subscriptionId);
|
|
230
211
|
const pollInterval = request.updateInterval ?? __classPrivateFieldGet(this, _PriceDataSource_pollInterval, "f");
|
|
231
|
-
//
|
|
232
|
-
// `fetchedAt` is stamped when a fetch completes (slightly after the tick
|
|
233
|
-
// that triggered it), so a TTL equal to the poll interval would leave the
|
|
234
|
-
// asset still "fresh" at the next tick, making the subscription re-fetch
|
|
235
|
-
// only every other poll. The margin also absorbs network latency / jitter.
|
|
236
|
-
__classPrivateFieldGet(this, _PriceDataSource_deduper, "f").freshnessTtlMs = Math.min(__classPrivateFieldGet(this, _PriceDataSource_deduper, "f").freshnessTtlMs, Math.floor(pollInterval * FRESHNESS_TTL_POLL_RATIO));
|
|
237
|
-
// Create poll function - fetches prices using getAssetsState from subscription.
|
|
238
|
-
// The freshness TTL naturally gates re-fetches: assets fetched less than
|
|
239
|
-
// `priceFreshnessTtlMs` ago are skipped, preventing duplicates when middleware
|
|
240
|
-
// or other triggers already fetched the same assets between polls.
|
|
241
|
-
// Concurrent middleware calls will join the inflight promise rather than
|
|
242
|
-
// issuing duplicate requests.
|
|
212
|
+
// Create poll function - fetches prices using getAssetsState from subscription
|
|
243
213
|
const pollFn = async () => {
|
|
244
214
|
try {
|
|
245
215
|
const subscription = __classPrivateFieldGet(this, _PriceDataSource_activeSubscriptions, "f").get(subscriptionId);
|
|
246
216
|
if (!subscription) {
|
|
247
217
|
return;
|
|
248
218
|
}
|
|
219
|
+
// Fetch prices for all assets in balance state (uses subscription's getAssetsState)
|
|
249
220
|
const fetchResponse = await this.fetch(subscription.request, subscription.getAssetsState);
|
|
250
221
|
// Only report if we got prices
|
|
251
222
|
if (fetchResponse.assetsPrice &&
|
|
@@ -288,14 +259,6 @@ export class PriceDataSource {
|
|
|
288
259
|
__classPrivateFieldGet(this, _PriceDataSource_activeSubscriptions, "f").delete(subscriptionId);
|
|
289
260
|
}
|
|
290
261
|
}
|
|
291
|
-
/**
|
|
292
|
-
* Invalidate the price freshness cache, forcing the next fetch to call the
|
|
293
|
-
* API regardless of TTL. Use when external state changes (e.g. selected
|
|
294
|
-
* currency) require a full refresh.
|
|
295
|
-
*/
|
|
296
|
-
invalidatePriceCache() {
|
|
297
|
-
__classPrivateFieldGet(this, _PriceDataSource_deduper, "f").invalidate();
|
|
298
|
-
}
|
|
299
262
|
/**
|
|
300
263
|
* Destroy the data source and clean up all subscriptions.
|
|
301
264
|
*/
|
|
@@ -304,10 +267,9 @@ export class PriceDataSource {
|
|
|
304
267
|
subscription.cleanup();
|
|
305
268
|
}
|
|
306
269
|
__classPrivateFieldGet(this, _PriceDataSource_activeSubscriptions, "f").clear();
|
|
307
|
-
__classPrivateFieldGet(this, _PriceDataSource_deduper, "f").destroy();
|
|
308
270
|
}
|
|
309
271
|
}
|
|
310
|
-
_PriceDataSource_getSelectedCurrency = new WeakMap(), _PriceDataSource_pollInterval = new WeakMap(), _PriceDataSource_apiClient = new WeakMap(), _PriceDataSource_fetchTimeoutMs = new WeakMap(),
|
|
272
|
+
_PriceDataSource_getSelectedCurrency = new WeakMap(), _PriceDataSource_pollInterval = new WeakMap(), _PriceDataSource_apiClient = new WeakMap(), _PriceDataSource_fetchTimeoutMs = new WeakMap(), _PriceDataSource_activeSubscriptions = new WeakMap(), _PriceDataSource_instances = new WeakSet(), _PriceDataSource_fetchSpotPricesBatch =
|
|
311
273
|
// ============================================================================
|
|
312
274
|
// HELPERS
|
|
313
275
|
// ============================================================================
|
|
@@ -337,17 +299,15 @@ async function _PriceDataSource_fetchSpotPricesBatch(assetIds, selectedCurrency)
|
|
|
337
299
|
}), __classPrivateFieldGet(this, _PriceDataSource_fetchTimeoutMs, "f")),
|
|
338
300
|
]);
|
|
339
301
|
return { selectedCurrencyPrices, usdPrices };
|
|
340
|
-
},
|
|
302
|
+
}, _PriceDataSource_fetchSpotPrices =
|
|
341
303
|
/**
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
* so it does NOT check freshness or inflight state — that is handled by
|
|
345
|
-
* {@link DedupingBatchFetcher}.
|
|
304
|
+
* Fetch spot prices for all provided asset IDs, splitting into batches of
|
|
305
|
+
* PRICE_API_BATCH_SIZE to respect API limits.
|
|
346
306
|
*
|
|
347
|
-
* @param assetIds -
|
|
348
|
-
* @returns
|
|
307
|
+
* @param assetIds - Array of CAIP-19 asset IDs
|
|
308
|
+
* @returns Spot prices response
|
|
349
309
|
*/
|
|
350
|
-
async function
|
|
310
|
+
async function _PriceDataSource_fetchSpotPrices(assetIds) {
|
|
351
311
|
const selectedCurrency = __classPrivateFieldGet(this, _PriceDataSource_getSelectedCurrency, "f").call(this);
|
|
352
312
|
const batchResults = await reduceInBatchesSerially({
|
|
353
313
|
values: assetIds,
|
|
@@ -358,7 +318,6 @@ async function _PriceDataSource_executeBatchFetch(assetIds) {
|
|
|
358
318
|
},
|
|
359
319
|
initialResult: [],
|
|
360
320
|
});
|
|
361
|
-
const fetchedAt = Date.now();
|
|
362
321
|
const prices = {};
|
|
363
322
|
for (const { selectedCurrencyPrices, usdPrices } of batchResults) {
|
|
364
323
|
for (const [assetId, marketData] of Object.entries(selectedCurrencyPrices)) {
|
|
@@ -367,26 +326,16 @@ async function _PriceDataSource_executeBatchFetch(assetIds) {
|
|
|
367
326
|
!isValidMarketData(usdMarketData)) {
|
|
368
327
|
continue;
|
|
369
328
|
}
|
|
370
|
-
|
|
329
|
+
const caipAssetId = assetId;
|
|
330
|
+
prices[caipAssetId] = {
|
|
371
331
|
...marketData,
|
|
372
332
|
assetPriceType: 'fungible',
|
|
373
333
|
usdPrice: usdMarketData.price,
|
|
374
|
-
lastUpdated:
|
|
334
|
+
lastUpdated: Date.now(),
|
|
375
335
|
};
|
|
376
336
|
}
|
|
377
337
|
}
|
|
378
338
|
return prices;
|
|
379
|
-
}, _PriceDataSource_fetchSpotPrices =
|
|
380
|
-
/**
|
|
381
|
-
* Fetch spot prices for all provided asset IDs, deduplicating via the
|
|
382
|
-
* deduper (freshness TTL + per-asset inflight coalescing).
|
|
383
|
-
*
|
|
384
|
-
* @param assetIds - Array of CAIP-19 asset IDs.
|
|
385
|
-
* @returns Spot prices response (only contains entries for assets that were
|
|
386
|
-
* actually fetched or joined from inflight).
|
|
387
|
-
*/
|
|
388
|
-
async function _PriceDataSource_fetchSpotPrices(assetIds) {
|
|
389
|
-
return __classPrivateFieldGet(this, _PriceDataSource_deduper, "f").fetch(assetIds);
|
|
390
339
|
}, _PriceDataSource_getAssetIdsFromBalanceState = function _PriceDataSource_getAssetIdsFromBalanceState(request, getAssetsState) {
|
|
391
340
|
if (!getAssetsState) {
|
|
392
341
|
return [];
|