@metamask/assets-controller 9.0.2 → 10.0.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 (98) hide show
  1. package/CHANGELOG.md +44 -1
  2. package/dist/AssetsController.cjs +357 -105
  3. package/dist/AssetsController.cjs.map +1 -1
  4. package/dist/AssetsController.d.cts +11 -8
  5. package/dist/AssetsController.d.cts.map +1 -1
  6. package/dist/AssetsController.d.mts +11 -8
  7. package/dist/AssetsController.d.mts.map +1 -1
  8. package/dist/AssetsController.mjs +357 -105
  9. package/dist/AssetsController.mjs.map +1 -1
  10. package/dist/data-sources/AccountsApiDataSource.cjs +15 -2
  11. package/dist/data-sources/AccountsApiDataSource.cjs.map +1 -1
  12. package/dist/data-sources/AccountsApiDataSource.d.cts +8 -0
  13. package/dist/data-sources/AccountsApiDataSource.d.cts.map +1 -1
  14. package/dist/data-sources/AccountsApiDataSource.d.mts +8 -0
  15. package/dist/data-sources/AccountsApiDataSource.d.mts.map +1 -1
  16. package/dist/data-sources/AccountsApiDataSource.mjs +15 -2
  17. package/dist/data-sources/AccountsApiDataSource.mjs.map +1 -1
  18. package/dist/data-sources/BackendWebsocketDataSource.cjs +236 -149
  19. package/dist/data-sources/BackendWebsocketDataSource.cjs.map +1 -1
  20. package/dist/data-sources/BackendWebsocketDataSource.d.cts +15 -0
  21. package/dist/data-sources/BackendWebsocketDataSource.d.cts.map +1 -1
  22. package/dist/data-sources/BackendWebsocketDataSource.d.mts +15 -0
  23. package/dist/data-sources/BackendWebsocketDataSource.d.mts.map +1 -1
  24. package/dist/data-sources/BackendWebsocketDataSource.mjs +236 -146
  25. package/dist/data-sources/BackendWebsocketDataSource.mjs.map +1 -1
  26. package/dist/data-sources/PriceDataSource.cjs +82 -22
  27. package/dist/data-sources/PriceDataSource.cjs.map +1 -1
  28. package/dist/data-sources/PriceDataSource.d.cts +13 -0
  29. package/dist/data-sources/PriceDataSource.d.cts.map +1 -1
  30. package/dist/data-sources/PriceDataSource.d.mts +13 -0
  31. package/dist/data-sources/PriceDataSource.d.mts.map +1 -1
  32. package/dist/data-sources/PriceDataSource.mjs +83 -23
  33. package/dist/data-sources/PriceDataSource.mjs.map +1 -1
  34. package/dist/data-sources/RpcDataSource.cjs +29 -9
  35. package/dist/data-sources/RpcDataSource.cjs.map +1 -1
  36. package/dist/data-sources/RpcDataSource.d.cts +5 -0
  37. package/dist/data-sources/RpcDataSource.d.cts.map +1 -1
  38. package/dist/data-sources/RpcDataSource.d.mts +5 -0
  39. package/dist/data-sources/RpcDataSource.d.mts.map +1 -1
  40. package/dist/data-sources/RpcDataSource.mjs +29 -9
  41. package/dist/data-sources/RpcDataSource.mjs.map +1 -1
  42. package/dist/data-sources/SnapDataSource.cjs +2 -2
  43. package/dist/data-sources/SnapDataSource.cjs.map +1 -1
  44. package/dist/data-sources/SnapDataSource.mjs +2 -2
  45. package/dist/data-sources/SnapDataSource.mjs.map +1 -1
  46. package/dist/index.cjs +3 -1
  47. package/dist/index.cjs.map +1 -1
  48. package/dist/index.d.cts +2 -2
  49. package/dist/index.d.cts.map +1 -1
  50. package/dist/index.d.mts +2 -2
  51. package/dist/index.d.mts.map +1 -1
  52. package/dist/index.mjs +1 -1
  53. package/dist/index.mjs.map +1 -1
  54. package/dist/middlewares/DetectionMiddleware.cjs +64 -23
  55. package/dist/middlewares/DetectionMiddleware.cjs.map +1 -1
  56. package/dist/middlewares/DetectionMiddleware.d.cts +15 -13
  57. package/dist/middlewares/DetectionMiddleware.d.cts.map +1 -1
  58. package/dist/middlewares/DetectionMiddleware.d.mts +15 -13
  59. package/dist/middlewares/DetectionMiddleware.d.mts.map +1 -1
  60. package/dist/middlewares/DetectionMiddleware.mjs +64 -23
  61. package/dist/middlewares/DetectionMiddleware.mjs.map +1 -1
  62. package/dist/middlewares/ParallelMiddleware.cjs +7 -0
  63. package/dist/middlewares/ParallelMiddleware.cjs.map +1 -1
  64. package/dist/middlewares/ParallelMiddleware.d.cts.map +1 -1
  65. package/dist/middlewares/ParallelMiddleware.d.mts.map +1 -1
  66. package/dist/middlewares/ParallelMiddleware.mjs +7 -0
  67. package/dist/middlewares/ParallelMiddleware.mjs.map +1 -1
  68. package/dist/selectors/balance.cjs +191 -8
  69. package/dist/selectors/balance.cjs.map +1 -1
  70. package/dist/selectors/balance.d.cts +80 -0
  71. package/dist/selectors/balance.d.cts.map +1 -1
  72. package/dist/selectors/balance.d.mts +80 -0
  73. package/dist/selectors/balance.d.mts.map +1 -1
  74. package/dist/selectors/balance.mjs +186 -7
  75. package/dist/selectors/balance.mjs.map +1 -1
  76. package/dist/types.cjs.map +1 -1
  77. package/dist/types.d.cts +20 -4
  78. package/dist/types.d.cts.map +1 -1
  79. package/dist/types.d.mts +20 -4
  80. package/dist/types.d.mts.map +1 -1
  81. package/dist/types.mjs.map +1 -1
  82. package/dist/utils/dedupingBatchFetcher.cjs +168 -0
  83. package/dist/utils/dedupingBatchFetcher.cjs.map +1 -0
  84. package/dist/utils/dedupingBatchFetcher.d.cts +72 -0
  85. package/dist/utils/dedupingBatchFetcher.d.cts.map +1 -0
  86. package/dist/utils/dedupingBatchFetcher.d.mts +72 -0
  87. package/dist/utils/dedupingBatchFetcher.d.mts.map +1 -0
  88. package/dist/utils/dedupingBatchFetcher.mjs +164 -0
  89. package/dist/utils/dedupingBatchFetcher.mjs.map +1 -0
  90. package/dist/utils/processAccountActivityBalanceUpdates.cjs +54 -0
  91. package/dist/utils/processAccountActivityBalanceUpdates.cjs.map +1 -0
  92. package/dist/utils/processAccountActivityBalanceUpdates.d.cts +13 -0
  93. package/dist/utils/processAccountActivityBalanceUpdates.d.cts.map +1 -0
  94. package/dist/utils/processAccountActivityBalanceUpdates.d.mts +13 -0
  95. package/dist/utils/processAccountActivityBalanceUpdates.d.mts.map +1 -0
  96. package/dist/utils/processAccountActivityBalanceUpdates.mjs +47 -0
  97. package/dist/utils/processAccountActivityBalanceUpdates.mjs.map +1 -0
  98. package/package.json +7 -7
@@ -1 +1 @@
1
- {"version":3,"file":"BackendWebsocketDataSource.mjs","sourceRoot":"","sources":["../../src/data-sources/BackendWebsocketDataSource.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n BackendWebSocketServiceActions,\n BackendWebSocketServiceEvents,\n ServerNotificationMessage,\n WebSocketSubscription,\n WebSocketState,\n AccountActivityMessage,\n BalanceUpdate,\n} from '@metamask/core-backend';\nimport type { ApiPlatformClient } from '@metamask/core-backend';\nimport {\n isCaipChainId,\n KnownCaipNamespace,\n toCaipChainId,\n} from '@metamask/utils';\nimport BigNumberJS from 'bignumber.js';\n\nimport type { AssetsControllerMessenger } from '../AssetsController';\nimport { projectLogger, createModuleLogger } from '../logger';\nimport type {\n ChainId,\n Caip19AssetId,\n AssetMetadata,\n AssetBalance,\n DataResponse,\n} from '../types';\nimport { AbstractDataSource } from './AbstractDataSource';\nimport type {\n DataSourceState,\n SubscriptionRequest,\n} from './AbstractDataSource';\n\n// ============================================================================\n// CONSTANTS\n// ============================================================================\n\nconst CONTROLLER_NAME = 'BackendWebsocketDataSource';\nconst CHANNEL_TYPE = 'account-activity.v1';\n\nconst log = createModuleLogger(projectLogger, CONTROLLER_NAME);\n\n// ============================================================================\n// MESSENGER TYPES\n// ============================================================================\n\n// Allowed actions that BackendWebsocketDataSource can call\nexport type BackendWebsocketDataSourceAllowedActions =\n BackendWebSocketServiceActions;\n\n// Allowed events that BackendWebsocketDataSource can subscribe to\nexport type BackendWebsocketDataSourceAllowedEvents =\n BackendWebSocketServiceEvents;\n\n// ============================================================================\n// STATE\n// ============================================================================\n\nexport type BackendWebsocketDataSourceState = DataSourceState;\n\nconst defaultState: BackendWebsocketDataSourceState = {\n activeChains: [],\n};\n\n// ============================================================================\n// OPTIONS\n// ============================================================================\n\nexport type BackendWebsocketDataSourceOptions = {\n /** The AssetsController messenger (shared by all data sources). */\n messenger: AssetsControllerMessenger;\n /** ApiPlatformClient for fetching supported networks at init (same as AccountsApiDataSource). */\n queryApiClient: ApiPlatformClient;\n /** Called when active chains are updated. Pass dataSourceName so the controller knows the source. */\n onActiveChainsUpdated: (\n dataSourceName: string,\n chains: ChainId[],\n previousChains: ChainId[],\n ) => void;\n /** Returns the asset type ('native' | 'erc20' | 'spl') for a given CAIP-19 asset ID. */\n getAssetType: (assetId: Caip19AssetId) => 'native' | 'erc20' | 'spl';\n state?: Partial<BackendWebsocketDataSourceState>;\n};\n\n// ============================================================================\n// HELPER FUNCTIONS\n// ============================================================================\n\n/**\n * Extract namespace from a CAIP-2 chain ID.\n * E.g., \"eip155:1\" -> \"eip155\", \"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp\" -> \"solana\"\n *\n * @param chainId - The CAIP-2 chain ID to extract namespace from.\n * @returns The namespace portion of the chain ID.\n */\nfunction extractNamespace(chainId: ChainId): string {\n const [namespace] = chainId.split(':');\n return namespace;\n}\n\n/** Namespaces we always subscribe to for account activity (EVM + Solana). */\nconst ACCOUNT_ACTIVITY_NAMESPACES = ['eip155', 'solana'] as const;\n\n/**\n * Get unique namespaces for account-activity subscriptions.\n * Always includes eip155 and solana so we subscribe to both EVM and Solana account activity,\n * plus any additional namespaces from the requested chain IDs.\n *\n * @param chainIds - Array of CAIP-2 chain IDs (from the subscription request).\n * @returns Array of unique namespaces (at least eip155 and solana).\n */\nfunction getNamespacesForAccountActivity(chainIds: ChainId[]): string[] {\n const namespaces = new Set<string>(ACCOUNT_ACTIVITY_NAMESPACES);\n for (const chainId of chainIds) {\n namespaces.add(extractNamespace(chainId));\n }\n return Array.from(namespaces);\n}\n\n/**\n * Returns the address to use for account-activity subscription in the given namespace.\n * EIP-155 accounts use hex (0x...) address; Solana accounts use base58.\n * Returns null if this account type does not have an address in that namespace.\n *\n * @param account - Internal account (type + address).\n * @param account.type - Account type (e.g. \"eip155:eoa\", \"solana:data-account\").\n * @param account.address - Account address (hex for eip155, base58 for solana).\n * @param namespace - The chain namespace (e.g., \"eip155\", \"solana\").\n * @returns The address for that namespace, or null if the account does not support the namespace.\n */\nfunction getAddressForAccountActivity(\n account: { type: string; address: string },\n namespace: string,\n): string | null {\n if (namespace === 'eip155') {\n return account.type.startsWith('eip155') ? account.address : null;\n }\n if (namespace === 'solana') {\n return account.type.startsWith('solana') ? account.address : null;\n }\n // Other namespaces (e.g. from chainIds): use address if account type matches namespace\n const typePrefix = `${namespace}:`;\n return account.type.startsWith(typePrefix) ? account.address : null;\n}\n\n/**\n * Build WebSocket channel name for account activity using CAIP-10 wildcard format.\n * Uses 0 as the chain reference to subscribe to all chains in the namespace.\n * EIP-155 addresses are lowercased (hex); Solana addresses are left as-is (base58).\n *\n * @param namespace - The chain namespace (e.g., \"eip155\", \"solana\").\n * @param address - The account address (hex for eip155, base58 for solana).\n * @returns The WebSocket channel name.\n */\nfunction buildAccountActivityChannel(\n namespace: string,\n address: string,\n): string {\n const formatted = namespace === 'eip155' ? address.toLowerCase() : address;\n return `${CHANNEL_TYPE}.${namespace}:0:${formatted}`;\n}\n\n/**\n * Normalize API chain identifier to CAIP-2 ChainId.\n * Passes through strings already in CAIP-2 form (e.g. eip155:1, solana:5eykt...).\n * Converts bare decimals to eip155:decimal.\n * Uses @metamask/utils for CAIP parsing.\n *\n * @param chainIdOrDecimal - Chain ID string (CAIP-2 or decimal) or decimal number.\n * @returns CAIP-2 ChainId.\n */\nfunction toChainId(chainIdOrDecimal: number | string): ChainId {\n if (typeof chainIdOrDecimal === 'string') {\n if (isCaipChainId(chainIdOrDecimal)) {\n return chainIdOrDecimal;\n }\n return toCaipChainId(KnownCaipNamespace.Eip155, chainIdOrDecimal);\n }\n return toCaipChainId(KnownCaipNamespace.Eip155, String(chainIdOrDecimal));\n}\n\n// Note: AccountActivityMessage and BalanceUpdate types are imported from @metamask/core-backend\n\n// ============================================================================\n// BACKEND WEBSOCKET DATA SOURCE\n// ============================================================================\n\n/**\n * Data source for receiving real-time balance updates via WebSocket.\n *\n * This data source connects directly to BackendWebSocketService to receive\n * push notifications for account balance changes. Unlike AccountsApiDataSource\n * which polls for data, this provides instant updates.\n *\n * Uses Messenger pattern for all interactions:\n * - Calls BackendWebSocketService methods via messenger actions\n * - Exposes its own actions for AssetsController to call\n * - Publishes events for AssetsController to subscribe to\n *\n * Actions exposed:\n * - BackendWebsocketDataSource:getActiveChains\n * - BackendWebsocketDataSource:subscribe\n * - BackendWebsocketDataSource:unsubscribe\n *\n * Events published:\n * - BackendWebsocketDataSource:activeChainsUpdated\n * - BackendWebsocketDataSource:assetsUpdated\n *\n * Actions called (from BackendWebSocketService):\n * - BackendWebSocketService:subscribe\n * - BackendWebSocketService:getConnectionInfo\n * - BackendWebSocketService:findSubscriptionsByChannelPrefix\n */\nconst DEFAULT_CHAINS_REFRESH_INTERVAL_MS = 20 * 60 * 1000; // 20 minutes\n\nexport class BackendWebsocketDataSource extends AbstractDataSource<\n typeof CONTROLLER_NAME,\n BackendWebsocketDataSourceState\n> {\n readonly #messenger: AssetsControllerMessenger;\n\n readonly #apiClient: ApiPlatformClient;\n\n readonly #onActiveChainsUpdated: (\n dataSourceName: string,\n chains: ChainId[],\n previousChains: ChainId[],\n ) => void;\n\n readonly #getAssetType: (\n assetId: Caip19AssetId,\n ) => 'native' | 'erc20' | 'spl';\n\n /** Chains refresh timer */\n #chainsRefreshTimer: ReturnType<typeof setInterval> | null = null;\n\n /** Chains the backend API reports as supported (preserved across disconnects). */\n #supportedChains: ChainId[] = [];\n\n /** Whether the WebSocket is currently connected. Chains are only claimed when true. */\n #isConnected = false;\n\n /** WebSocket subscriptions by our internal subscription ID */\n readonly #wsSubscriptions: Map<string, WebSocketSubscription> = new Map();\n\n /** Pending subscription requests to process when WebSocket connects */\n readonly #pendingSubscriptions: Map<string, SubscriptionRequest> = new Map();\n\n /** Store original subscription requests for reconnection */\n readonly #subscriptionRequests: Map<string, SubscriptionRequest> = new Map();\n\n constructor(options: BackendWebsocketDataSourceOptions) {\n super(CONTROLLER_NAME, {\n ...defaultState,\n ...options.state,\n });\n\n this.#messenger = options.messenger;\n this.#apiClient = options.queryApiClient;\n this.#onActiveChainsUpdated = options.onActiveChainsUpdated;\n this.#getAssetType = options.getAssetType;\n\n this.#subscribeToEvents();\n this.#initializeActiveChains().catch(console.error);\n }\n\n // ============================================================================\n // INITIALIZATION\n // ============================================================================\n\n async #initializeActiveChains(): Promise<void> {\n try {\n const chains = await this.#fetchActiveChains();\n this.#supportedChains = chains;\n\n // Only claim chains if the websocket is already connected.\n // If not connected, chains stay unclaimed so AccountsApiDataSource\n // can pick them up via polling. They'll be claimed on reconnect.\n if (this.#isConnected) {\n const previous = [...this.state.activeChains];\n this.updateActiveChains(chains, (updatedChains) =>\n this.#onActiveChainsUpdated(this.getName(), updatedChains, previous),\n );\n }\n\n this.#chainsRefreshTimer = setInterval(() => {\n this.#refreshActiveChains().catch(console.error);\n }, DEFAULT_CHAINS_REFRESH_INTERVAL_MS);\n } catch (error) {\n log('Failed to fetch active chains', error);\n }\n }\n\n async #refreshActiveChains(): Promise<void> {\n try {\n const chains = await this.#fetchActiveChains();\n this.#supportedChains = chains;\n\n // Only update activeChains if connected; otherwise keep them unclaimed.\n if (!this.#isConnected) {\n return;\n }\n\n const previousChains = new Set(this.state.activeChains);\n const newChains = new Set(chains);\n\n const added = chains.filter((chain) => !previousChains.has(chain));\n const removed = Array.from(previousChains).filter(\n (chain) => !newChains.has(chain),\n );\n\n if (added.length > 0 || removed.length > 0) {\n const previous = [...this.state.activeChains];\n this.updateActiveChains(chains, (updatedChains) =>\n this.#onActiveChainsUpdated(this.getName(), updatedChains, previous),\n );\n }\n } catch (error) {\n log('Failed to refresh active chains', error);\n }\n }\n\n async #fetchActiveChains(): Promise<ChainId[]> {\n const response = await this.#apiClient.accounts.fetchV2SupportedNetworks();\n return response.fullSupport.map(toChainId);\n }\n\n #subscribeToEvents(): void {\n type ConnectionStatePayload = {\n state: WebSocketState;\n [key: string]: unknown;\n };\n // Listen for WebSocket connection state changes (event not in AssetsControllerEvents).\n (\n this.#messenger as unknown as {\n subscribe: (e: string, h: (p: ConnectionStatePayload) => void) => void;\n }\n ).subscribe(\n 'BackendWebSocketService:connectionStateChanged',\n (connectionInfo: ConnectionStatePayload) => {\n if (connectionInfo.state === ('connected' as WebSocketState)) {\n this.#isConnected = true;\n this.#handleReconnect();\n } else if (\n connectionInfo.state === ('disconnected' as WebSocketState)\n ) {\n this.#isConnected = false;\n this.#handleDisconnect();\n }\n },\n );\n }\n\n /**\n * Sync active chains from AccountsApiDataSource.\n * When the data source invokes the onActiveChainsUpdated callback, the\n * controller processes the active chains update (no messenger call; controller already updated).\n *\n * @param chains - Updated active chain IDs from AccountsApiDataSource.\n */\n setActiveChainsFromAccountsApi(chains: ChainId[]): void {\n this.updateActiveChains(chains, () => undefined);\n }\n\n /**\n * Handle WebSocket disconnection.\n * Moves all active subscriptions to pending for re-subscription on reconnect.\n */\n #handleDisconnect(): void {\n log('WebSocket disconnected, releasing chains for fallback', {\n activeSubscriptionCount: this.activeSubscriptions.size,\n wsSubscriptionCount: this.#wsSubscriptions.size,\n chainCount: this.state.activeChains.length,\n });\n\n // Move active subscriptions to pending for re-subscription\n for (const [subscriptionId] of this.activeSubscriptions) {\n const originalRequest = this.#subscriptionRequests.get(subscriptionId);\n if (originalRequest) {\n this.#pendingSubscriptions.set(subscriptionId, {\n ...originalRequest,\n isUpdate: false,\n });\n }\n }\n\n // Clear WebSocket subscriptions (server-side already cleared)\n this.#wsSubscriptions.clear();\n\n // Clear active subscriptions (they're no longer valid)\n this.activeSubscriptions.clear();\n\n // Release chains so the chain-claiming loop assigns them to\n // AccountsApiDataSource (polling fallback) on the next #subscribeAssets.\n const previous = [...this.state.activeChains];\n if (previous.length > 0) {\n this.updateActiveChains([], (updatedChains) =>\n this.#onActiveChainsUpdated(this.getName(), updatedChains, previous),\n );\n }\n }\n\n /**\n * Handle WebSocket reconnection.\n * Clears stale pending subscriptions and restores activeChains so the\n * chain-claiming loop re-assigns them to this data source, triggering\n * fresh subscriptions with current accounts and chains.\n */\n #handleReconnect(): void {\n log('WebSocket reconnected, reclaiming chains', {\n supportedChainCount: this.#supportedChains.length,\n pendingSubscriptionCount: this.#pendingSubscriptions.size,\n });\n\n // Discard stale pending subscriptions captured at disconnect time.\n // The chain reclaim below triggers #onActiveChainsUpdated →\n // #subscribeAssets() in AssetsController, which creates fresh\n // subscriptions with current accounts and chains. Processing the\n // stale pending entries afterwards would overwrite those with\n // outdated request data.\n this.#pendingSubscriptions.clear();\n\n if (this.#supportedChains.length > 0) {\n const previous = [...this.state.activeChains];\n this.updateActiveChains(this.#supportedChains, (updatedChains) =>\n this.#onActiveChainsUpdated(this.getName(), updatedChains, previous),\n );\n }\n }\n\n // ============================================================================\n // ACTIVE CHAINS\n // ============================================================================\n\n /**\n * Update active chains when AccountsApiDataSource reports new supported chains.\n *\n * @param chains - Array of supported chain IDs.\n */\n updateSupportedChains(chains: ChainId[]): void {\n const previous = [...this.state.activeChains];\n this.updateActiveChains(chains, (updatedChains) =>\n this.#onActiveChainsUpdated(this.getName(), updatedChains, previous),\n );\n }\n\n // ============================================================================\n // SUBSCRIBE\n // ============================================================================\n\n async subscribe(subscriptionRequest: SubscriptionRequest): Promise<void> {\n const { request, subscriptionId, isUpdate } = subscriptionRequest;\n\n // Filter to active chains only\n const chainsToSubscribe = request.chainIds.filter((chainId) =>\n this.state.activeChains.includes(chainId),\n );\n\n const addresses = request.accountsWithSupportedChains.map(\n (a) => a.account.address,\n );\n\n if (addresses.length === 0) {\n return;\n }\n\n // Check WebSocket connection status\n try {\n const connectionInfo = this.#messenger.call(\n 'BackendWebSocketService:getConnectionInfo',\n );\n if (connectionInfo.state !== ('connected' as WebSocketState)) {\n // Store the subscription request to process when WebSocket connects\n this.#pendingSubscriptions.set(subscriptionId, subscriptionRequest);\n return;\n }\n } catch {\n // Store anyway - will be processed when we can connect\n this.#pendingSubscriptions.set(subscriptionId, subscriptionRequest);\n return;\n }\n\n // Remove from pending if it was there (we're processing it now)\n this.#pendingSubscriptions.delete(subscriptionId);\n\n // Handle subscription update\n if (isUpdate) {\n const existing = this.activeSubscriptions.get(subscriptionId);\n if (existing) {\n // Check if accounts changed - if so, we need to re-subscribe to different channels\n const existingAddresses = existing.addresses ?? [];\n const addressesChanged =\n addresses.length !== existingAddresses.length ||\n addresses.some((addr) => !existingAddresses.includes(addr));\n\n if (!addressesChanged) {\n // Only chains changed - just update chains and return\n existing.chains = chainsToSubscribe;\n return;\n }\n // Accounts changed - fall through to re-subscribe with new channels\n }\n }\n\n // Clean up existing subscription if any\n await this.unsubscribe(subscriptionId);\n\n // Always subscribe to eip155 and solana account activity, plus any namespaces from requested chains\n const namespaces = getNamespacesForAccountActivity(chainsToSubscribe);\n\n // Build channel names: use namespace-appropriate address per account (eip155 = hex, solana = base58)\n const channels: string[] = [];\n for (const namespace of namespaces) {\n for (const { account } of request.accountsWithSupportedChains) {\n const address = getAddressForAccountActivity(account, namespace);\n if (address) {\n channels.push(buildAccountActivityChannel(namespace, address));\n }\n }\n }\n\n try {\n // Create WebSocket subscription\n const wsSubscription = await this.#messenger.call(\n 'BackendWebSocketService:subscribe',\n {\n channels,\n channelType: CHANNEL_TYPE,\n callback: (notification: ServerNotificationMessage) => {\n this.#handleNotification(notification, subscriptionId);\n },\n },\n );\n\n // Store WebSocket subscription\n this.#wsSubscriptions.set(subscriptionId, wsSubscription);\n\n // Store in abstract class tracking\n this.activeSubscriptions.set(subscriptionId, {\n cleanup: () => {\n const wsSub = this.#wsSubscriptions.get(subscriptionId);\n if (wsSub) {\n wsSub.unsubscribe().catch((unsubErr: unknown) => {\n log('Error unsubscribing', { subscriptionId, error: unsubErr });\n });\n this.#wsSubscriptions.delete(subscriptionId);\n }\n // Also clean up the stored request\n this.#subscriptionRequests.delete(subscriptionId);\n },\n chains: chainsToSubscribe,\n addresses,\n onAssetsUpdate: subscriptionRequest.onAssetsUpdate,\n });\n\n // Store original request for reconnection\n this.#subscriptionRequests.set(subscriptionId, subscriptionRequest);\n } catch (error) {\n log('WebSocket subscription FAILED', {\n subscriptionId,\n error,\n chains: chainsToSubscribe,\n });\n }\n }\n\n // ============================================================================\n // NOTIFICATION HANDLING\n // ============================================================================\n\n #handleNotification(\n notification: ServerNotificationMessage,\n subscriptionId: string,\n ): void {\n try {\n const subscription = this.activeSubscriptions.get(subscriptionId);\n const request = this.#subscriptionRequests.get(subscriptionId)?.request;\n if (!request) {\n return;\n }\n\n const activityMessage =\n notification.data as unknown as AccountActivityMessage;\n const { address, tx, updates } = activityMessage;\n\n if (!address || !tx || !updates) {\n return;\n }\n\n // Extract chain ID from transaction (CAIP-2 format, e.g., \"eip155:8453\")\n const chainId = tx.chain as ChainId;\n\n // Find matching account in request (eip155: case-insensitive hex; solana: exact base58)\n const account = request.accountsWithSupportedChains\n .map((entry) => entry.account)\n .find((a) =>\n a.address.startsWith('0x')\n ? a.address.toLowerCase() === address.toLowerCase()\n : a.address === address,\n );\n if (!account) {\n return;\n }\n const accountId = account.id;\n\n // Process all balance updates from the activity message\n const response = this.#processBalanceUpdates(updates, chainId, accountId);\n\n if (Object.keys(response).length > 0 && subscription) {\n Promise.resolve(subscription.onAssetsUpdate(response)).catch(\n console.error,\n );\n }\n } catch (error) {\n log('Error handling notification', error);\n }\n }\n\n /**\n * Process balance updates from AccountActivityMessage.\n * Each update contains asset info, post-transaction balance, and transfer details.\n *\n * @param updates - Array of balance updates from the activity message.\n * @param _chainId - The chain ID (unused but kept for context).\n * @param accountId - The account ID to process updates for.\n * @returns DataResponse containing processed balance and metadata.\n */\n #processBalanceUpdates(\n updates: BalanceUpdate[],\n _chainId: ChainId,\n accountId: string,\n ): DataResponse {\n const assetsBalance: Record<string, Record<Caip19AssetId, AssetBalance>> = {\n [accountId]: {},\n };\n const assetsMetadata: Record<Caip19AssetId, AssetMetadata> = {};\n\n for (const update of updates) {\n const { asset, postBalance } = update;\n\n if (!asset || !postBalance) {\n continue;\n }\n\n // Asset type is in CAIP format: \"eip155:1/erc20:0x...\" or \"eip155:1/slip44:60\"\n // We can use it directly as the asset ID\n const assetId = asset.type as Caip19AssetId;\n\n const tokenType = this.#getAssetType(assetId);\n\n // We assume decimals are always present; skip malformed updates\n if (asset.decimals === undefined) {\n continue;\n }\n\n // Parse raw balance (hex like \"0x26f0e5\" or decimal string)\n const rawBalanceStr = postBalance.amount.startsWith('0x')\n ? BigInt(postBalance.amount).toString()\n : postBalance.amount;\n\n const humanReadableAmount = new BigNumberJS(rawBalanceStr)\n .dividedBy(new BigNumberJS(10).pow(asset.decimals))\n .toFixed();\n\n assetsBalance[accountId][assetId] = {\n amount: humanReadableAmount,\n };\n\n assetsMetadata[assetId] = {\n type: tokenType,\n symbol: asset.unit,\n name: asset.unit, // Use unit as name (actual name may not be in the message)\n decimals: asset.decimals,\n };\n }\n\n const response: DataResponse = { updateMode: 'merge' };\n if (Object.keys(assetsBalance[accountId]).length > 0) {\n response.assetsBalance = assetsBalance;\n response.assetsInfo = assetsMetadata;\n }\n\n return response;\n }\n\n // ============================================================================\n // CLEANUP\n // ============================================================================\n\n destroy(): void {\n if (this.#chainsRefreshTimer) {\n clearInterval(this.#chainsRefreshTimer);\n this.#chainsRefreshTimer = null;\n }\n\n // Clean up WebSocket subscriptions\n // Convert to array first to avoid modifying map during iteration\n const subscriptions = [...this.#wsSubscriptions.values()];\n for (const wsSub of subscriptions) {\n try {\n // Fire and forget - don't await in destroy\n wsSub.unsubscribe().catch(() => {\n // Ignore errors during cleanup\n });\n } catch {\n // Ignore errors during cleanup\n }\n }\n this.#wsSubscriptions.clear();\n\n // Clean up base class subscriptions\n super.destroy();\n }\n}\n\n// ============================================================================\n// FACTORY FUNCTION\n// ============================================================================\n\n/**\n * Creates a BackendWebsocketDataSource instance.\n *\n * @param options - Configuration options for the data source.\n * @returns A new BackendWebsocketDataSource instance.\n */\nexport function createBackendWebsocketDataSource(\n options: BackendWebsocketDataSourceOptions,\n): BackendWebsocketDataSource {\n return new BackendWebsocketDataSource(options);\n}\n"]}
1
+ {"version":3,"file":"BackendWebsocketDataSource.mjs","sourceRoot":"","sources":["../../src/data-sources/BackendWebsocketDataSource.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n BackendWebSocketServiceActions,\n BackendWebSocketServiceEvents,\n ServerNotificationMessage,\n WebSocketSubscription,\n WebSocketState,\n AccountActivityMessage,\n BalanceUpdate,\n} from '@metamask/core-backend';\nimport type { ApiPlatformClient } from '@metamask/core-backend';\nimport {\n isCaipChainId,\n KnownCaipNamespace,\n toCaipChainId,\n} from '@metamask/utils';\n\nimport type { AssetsControllerMessenger } from '../AssetsController';\nimport { projectLogger, createModuleLogger } from '../logger';\nimport type { ChainId, Caip19AssetId, DataResponse } from '../types';\nimport { processAccountActivityBalanceUpdates } from '../utils/processAccountActivityBalanceUpdates';\nimport { AbstractDataSource } from './AbstractDataSource';\nimport type {\n DataSourceState,\n SubscriptionRequest,\n} from './AbstractDataSource';\n\n// ============================================================================\n// CONSTANTS\n// ============================================================================\n\nconst CONTROLLER_NAME = 'BackendWebsocketDataSource';\nconst CHANNEL_TYPE = 'account-activity.v1';\n\nconst log = createModuleLogger(projectLogger, CONTROLLER_NAME);\n\n// ============================================================================\n// MESSENGER TYPES\n// ============================================================================\n\n// Allowed actions that BackendWebsocketDataSource can call\nexport type BackendWebsocketDataSourceAllowedActions =\n BackendWebSocketServiceActions;\n\n// Allowed events that BackendWebsocketDataSource can subscribe to\nexport type BackendWebsocketDataSourceAllowedEvents =\n BackendWebSocketServiceEvents;\n\n// ============================================================================\n// STATE\n// ============================================================================\n\nexport type BackendWebsocketDataSourceState = DataSourceState;\n\nconst defaultState: BackendWebsocketDataSourceState = {\n activeChains: [],\n};\n\n// ============================================================================\n// OPTIONS\n// ============================================================================\n\nexport type BackendWebsocketDataSourceOptions = {\n /** The AssetsController messenger (shared by all data sources). */\n messenger: AssetsControllerMessenger;\n /** ApiPlatformClient for fetching supported networks at init (same as AccountsApiDataSource). */\n queryApiClient: ApiPlatformClient;\n /** Called when active chains are updated. Pass dataSourceName so the controller knows the source. */\n onActiveChainsUpdated: (\n dataSourceName: string,\n chains: ChainId[],\n previousChains: ChainId[],\n ) => void;\n /** Returns the asset type ('native' | 'erc20' | 'spl') for a given CAIP-19 asset ID. */\n getAssetType: (assetId: Caip19AssetId) => 'native' | 'erc20' | 'spl';\n state?: Partial<BackendWebsocketDataSourceState>;\n};\n\n// ============================================================================\n// HELPER FUNCTIONS\n// ============================================================================\n\n/**\n * Extract namespace from a CAIP-2 chain ID.\n * E.g., \"eip155:1\" -> \"eip155\", \"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp\" -> \"solana\"\n *\n * @param chainId - The CAIP-2 chain ID to extract namespace from.\n * @returns The namespace portion of the chain ID.\n */\nfunction extractNamespace(chainId: ChainId): string {\n const [namespace] = chainId.split(':');\n return namespace;\n}\n\n/** Namespaces we always subscribe to for account activity (EVM + Solana). */\nconst ACCOUNT_ACTIVITY_NAMESPACES = ['eip155', 'solana'] as const;\n\n/**\n * Get unique namespaces for account-activity subscriptions.\n * Always includes eip155 and solana so we subscribe to both EVM and Solana account activity,\n * plus any additional namespaces from the requested chain IDs.\n *\n * @param chainIds - Array of CAIP-2 chain IDs (from the subscription request).\n * @returns Array of unique namespaces (at least eip155 and solana).\n */\nfunction getNamespacesForAccountActivity(chainIds: ChainId[]): string[] {\n const namespaces = new Set<string>(ACCOUNT_ACTIVITY_NAMESPACES);\n for (const chainId of chainIds) {\n namespaces.add(extractNamespace(chainId));\n }\n return Array.from(namespaces);\n}\n\n/**\n * Returns the address to use for account-activity subscription in the given namespace.\n * EIP-155 accounts use hex (0x...) address; Solana accounts use base58.\n * Returns null if this account type does not have an address in that namespace.\n *\n * @param account - Internal account (type + address).\n * @param account.type - Account type (e.g. \"eip155:eoa\", \"solana:data-account\").\n * @param account.address - Account address (hex for eip155, base58 for solana).\n * @param namespace - The chain namespace (e.g., \"eip155\", \"solana\").\n * @returns The address for that namespace, or null if the account does not support the namespace.\n */\nfunction getAddressForAccountActivity(\n account: { type: string; address: string },\n namespace: string,\n): string | null {\n if (namespace === 'eip155') {\n return account.type.startsWith('eip155') ? account.address : null;\n }\n if (namespace === 'solana') {\n return account.type.startsWith('solana') ? account.address : null;\n }\n // Other namespaces (e.g. from chainIds): use address if account type matches namespace\n const typePrefix = `${namespace}:`;\n return account.type.startsWith(typePrefix) ? account.address : null;\n}\n\n/**\n * Build WebSocket channel name for account activity using CAIP-10 wildcard format.\n * Uses 0 as the chain reference to subscribe to all chains in the namespace.\n * EIP-155 addresses are lowercased (hex); Solana addresses are left as-is (base58).\n *\n * @param namespace - The chain namespace (e.g., \"eip155\", \"solana\").\n * @param address - The account address (hex for eip155, base58 for solana).\n * @returns The WebSocket channel name.\n */\nfunction buildAccountActivityChannel(\n namespace: string,\n address: string,\n): string {\n const formatted = namespace === 'eip155' ? address.toLowerCase() : address;\n return `${CHANNEL_TYPE}.${namespace}:0:${formatted}`;\n}\n\n/**\n * Normalize addresses for stable comparison when detecting account changes.\n *\n * @param address - Account address (hex or base58).\n * @returns Normalized address for comparison.\n */\nfunction normalizeAddressForComparison(address: string): string {\n return address.startsWith('0x') ? address.toLowerCase() : address;\n}\n\n/**\n * Check whether subscribed account addresses changed (case-insensitive for EVM).\n *\n * @param nextAddresses - Addresses from the incoming subscribe request.\n * @param existingAddresses - Addresses from the active subscription.\n * @returns True when the address sets differ.\n */\nfunction haveAddressesChanged(\n nextAddresses: string[],\n existingAddresses: string[],\n): boolean {\n if (nextAddresses.length !== existingAddresses.length) {\n return true;\n }\n\n const normalizedNext = nextAddresses\n .map(normalizeAddressForComparison)\n .sort();\n const normalizedExisting = existingAddresses\n .map(normalizeAddressForComparison)\n .sort();\n\n return normalizedNext.some(\n (address, index) => address !== normalizedExisting[index],\n );\n}\n\n/**\n * Normalize API chain identifier to CAIP-2 ChainId.\n * Passes through strings already in CAIP-2 form (e.g. eip155:1, solana:5eykt...).\n * Converts bare decimals to eip155:decimal.\n * Uses @metamask/utils for CAIP parsing.\n *\n * @param chainIdOrDecimal - Chain ID string (CAIP-2 or decimal) or decimal number.\n * @returns CAIP-2 ChainId.\n */\nfunction toChainId(chainIdOrDecimal: number | string): ChainId {\n if (typeof chainIdOrDecimal === 'string') {\n if (isCaipChainId(chainIdOrDecimal)) {\n return chainIdOrDecimal;\n }\n return toCaipChainId(KnownCaipNamespace.Eip155, chainIdOrDecimal);\n }\n return toCaipChainId(KnownCaipNamespace.Eip155, String(chainIdOrDecimal));\n}\n\n// Note: AccountActivityMessage and BalanceUpdate types are imported from @metamask/core-backend\n\n// ============================================================================\n// BACKEND WEBSOCKET DATA SOURCE\n// ============================================================================\n\n/**\n * Data source for receiving real-time balance updates via WebSocket.\n *\n * This data source connects directly to BackendWebSocketService to receive\n * push notifications for account balance changes. Unlike AccountsApiDataSource\n * which polls for data, this provides instant updates.\n *\n * Uses Messenger pattern for all interactions:\n * - Calls BackendWebSocketService methods via messenger actions\n * - Exposes its own actions for AssetsController to call\n * - Publishes events for AssetsController to subscribe to\n *\n * Actions exposed:\n * - BackendWebsocketDataSource:getActiveChains\n * - BackendWebsocketDataSource:subscribe\n * - BackendWebsocketDataSource:unsubscribe\n *\n * Events published:\n * - BackendWebsocketDataSource:activeChainsUpdated\n * - BackendWebsocketDataSource:assetsUpdated\n *\n * Actions called (from BackendWebSocketService):\n * - BackendWebSocketService:subscribe\n * - BackendWebSocketService:getConnectionInfo\n * - BackendWebSocketService:findSubscriptionsByChannelPrefix\n * - BackendWebSocketService:addChannelCallback\n * - BackendWebSocketService:removeChannelCallback\n */\nconst DEFAULT_CHAINS_REFRESH_INTERVAL_MS = 20 * 60 * 1000; // 20 minutes\n\nexport class BackendWebsocketDataSource extends AbstractDataSource<\n typeof CONTROLLER_NAME,\n BackendWebsocketDataSourceState\n> {\n readonly #messenger: AssetsControllerMessenger;\n\n readonly #apiClient: ApiPlatformClient;\n\n readonly #onActiveChainsUpdated: (\n dataSourceName: string,\n chains: ChainId[],\n previousChains: ChainId[],\n ) => void;\n\n readonly #getAssetType: (\n assetId: Caip19AssetId,\n ) => 'native' | 'erc20' | 'spl';\n\n /** Chains refresh timer */\n #chainsRefreshTimer: ReturnType<typeof setInterval> | null = null;\n\n /** Chains the backend API reports as supported (preserved across disconnects). */\n #supportedChains: ChainId[] = [];\n\n /** Whether the WebSocket is currently connected. Chains are only claimed when true. */\n #isConnected = false;\n\n /** WebSocket subscriptions by our internal subscription ID */\n readonly #wsSubscriptions: Map<string, WebSocketSubscription> = new Map();\n\n /** Pending subscription requests to process when WebSocket connects */\n readonly #pendingSubscriptions: Map<string, SubscriptionRequest> = new Map();\n\n /** Store original subscription requests for reconnection */\n readonly #subscriptionRequests: Map<string, SubscriptionRequest> = new Map();\n\n /** Channels with registered BackendWebSocketService channel callbacks */\n readonly #registeredChannelCallbacks: Set<string> = new Set();\n\n /** Serializes subscribe/unsubscribe so account switches cannot interleave. */\n #subscribeLock: Promise<void> = Promise.resolve();\n\n constructor(options: BackendWebsocketDataSourceOptions) {\n super(CONTROLLER_NAME, {\n ...defaultState,\n ...options.state,\n });\n\n this.#messenger = options.messenger;\n this.#apiClient = options.queryApiClient;\n this.#onActiveChainsUpdated = options.onActiveChainsUpdated;\n this.#getAssetType = options.getAssetType;\n\n this.#subscribeToEvents();\n this.#initializeActiveChains().catch(console.error);\n }\n\n // ============================================================================\n // INITIALIZATION\n // ============================================================================\n\n async #initializeActiveChains(): Promise<void> {\n try {\n const chains = await this.#fetchActiveChains();\n this.#supportedChains = chains;\n\n // Only claim chains if the websocket is already connected.\n // If not connected, chains stay unclaimed so AccountsApiDataSource\n // can pick them up via polling. They'll be claimed on reconnect.\n if (this.#isConnected) {\n const previous = [...this.state.activeChains];\n this.updateActiveChains(chains, (updatedChains) =>\n this.#onActiveChainsUpdated(this.getName(), updatedChains, previous),\n );\n }\n\n this.#chainsRefreshTimer = setInterval(() => {\n this.#refreshActiveChains().catch(console.error);\n }, DEFAULT_CHAINS_REFRESH_INTERVAL_MS);\n } catch (error) {\n log('Failed to fetch active chains', error);\n }\n }\n\n async #refreshActiveChains(): Promise<void> {\n try {\n const chains = await this.#fetchActiveChains();\n this.#supportedChains = chains;\n\n // Only update activeChains if connected; otherwise keep them unclaimed.\n if (!this.#isConnected) {\n return;\n }\n\n const previousChains = new Set(this.state.activeChains);\n const newChains = new Set(chains);\n\n const added = chains.filter((chain) => !previousChains.has(chain));\n const removed = Array.from(previousChains).filter(\n (chain) => !newChains.has(chain),\n );\n\n if (added.length > 0 || removed.length > 0) {\n const previous = [...this.state.activeChains];\n this.updateActiveChains(chains, (updatedChains) =>\n this.#onActiveChainsUpdated(this.getName(), updatedChains, previous),\n );\n }\n } catch (error) {\n log('Failed to refresh active chains', error);\n }\n }\n\n /**\n * Re-fetch supported networks and refresh `activeChains` when connected.\n * When disconnected, only `#supportedChains` is updated so reconnect can\n * reclaim chains. Called on EVM network switch from AssetsController.\n *\n * @returns Resolves when supported networks have been re-fetched.\n */\n refreshActiveChains(): Promise<void> {\n return this.#refreshActiveChains();\n }\n\n async #fetchActiveChains(): Promise<ChainId[]> {\n const response = await this.#apiClient.accounts.fetchV2SupportedNetworks();\n return response.fullSupport.map(toChainId);\n }\n\n #subscribeToEvents(): void {\n type ConnectionStatePayload = {\n state: WebSocketState;\n [key: string]: unknown;\n };\n // Listen for WebSocket connection state changes (event not in AssetsControllerEvents).\n (\n this.#messenger as unknown as {\n subscribe: (e: string, h: (p: ConnectionStatePayload) => void) => void;\n }\n ).subscribe(\n 'BackendWebSocketService:connectionStateChanged',\n (connectionInfo: ConnectionStatePayload) => {\n if (connectionInfo.state === ('connected' as WebSocketState)) {\n this.#isConnected = true;\n this.#handleReconnect();\n } else if (\n connectionInfo.state === ('disconnected' as WebSocketState)\n ) {\n this.#isConnected = false;\n this.#handleDisconnect();\n }\n },\n );\n }\n\n /**\n * Sync active chains from AccountsApiDataSource.\n * When the data source invokes the onActiveChainsUpdated callback, the\n * controller processes the active chains update (no messenger call; controller already updated).\n *\n * @param chains - Updated active chain IDs from AccountsApiDataSource.\n */\n setActiveChainsFromAccountsApi(chains: ChainId[]): void {\n this.updateActiveChains(chains, () => undefined);\n }\n\n /**\n * Handle WebSocket disconnection.\n * Moves all active subscriptions to pending for re-subscription on reconnect.\n */\n #handleDisconnect(): void {\n log('WebSocket disconnected, releasing chains for fallback', {\n activeSubscriptionCount: this.activeSubscriptions.size,\n wsSubscriptionCount: this.#wsSubscriptions.size,\n chainCount: this.state.activeChains.length,\n });\n\n // Move active subscriptions to pending for re-subscription\n for (const [subscriptionId] of this.activeSubscriptions) {\n const originalRequest = this.#subscriptionRequests.get(subscriptionId);\n if (originalRequest) {\n this.#pendingSubscriptions.set(subscriptionId, {\n ...originalRequest,\n isUpdate: false,\n });\n }\n }\n\n // Clear WebSocket subscriptions (server-side already cleared)\n this.#wsSubscriptions.clear();\n\n // Clear active subscriptions (they're no longer valid)\n this.activeSubscriptions.clear();\n\n // Release chains so the chain-claiming loop assigns them to\n // AccountsApiDataSource (polling fallback) on the next #subscribeAssets.\n const previous = [...this.state.activeChains];\n if (previous.length > 0) {\n this.updateActiveChains([], (updatedChains) =>\n this.#onActiveChainsUpdated(this.getName(), updatedChains, previous),\n );\n }\n }\n\n /**\n * Handle WebSocket reconnection.\n * Clears stale pending subscriptions and restores activeChains so the\n * chain-claiming loop re-assigns them to this data source, triggering\n * fresh subscriptions with current accounts and chains.\n */\n #handleReconnect(): void {\n log('WebSocket reconnected, reclaiming chains', {\n supportedChainCount: this.#supportedChains.length,\n pendingSubscriptionCount: this.#pendingSubscriptions.size,\n });\n\n // Discard stale pending subscriptions captured at disconnect time.\n // The chain reclaim below triggers #onActiveChainsUpdated →\n // #subscribeAssets() in AssetsController, which creates fresh\n // subscriptions with current accounts and chains. Processing the\n // stale pending entries afterwards would overwrite those with\n // outdated request data.\n this.#pendingSubscriptions.clear();\n\n if (this.#supportedChains.length > 0) {\n const previous = [...this.state.activeChains];\n this.updateActiveChains(this.#supportedChains, (updatedChains) =>\n this.#onActiveChainsUpdated(this.getName(), updatedChains, previous),\n );\n }\n }\n\n // ============================================================================\n // ACTIVE CHAINS\n // ============================================================================\n\n /**\n * Update active chains when AccountsApiDataSource reports new supported chains.\n *\n * @param chains - Array of supported chain IDs.\n */\n updateSupportedChains(chains: ChainId[]): void {\n const previous = [...this.state.activeChains];\n this.updateActiveChains(chains, (updatedChains) =>\n this.#onActiveChainsUpdated(this.getName(), updatedChains, previous),\n );\n }\n\n // ============================================================================\n // SUBSCRIBE\n // ============================================================================\n\n async subscribe(subscriptionRequest: SubscriptionRequest): Promise<void> {\n const previousLock = this.#subscribeLock;\n let releaseLock: () => void = () => undefined;\n this.#subscribeLock = new Promise<void>((resolve) => {\n releaseLock = resolve;\n });\n\n await previousLock;\n try {\n await this.#subscribeInternal(subscriptionRequest);\n } finally {\n releaseLock();\n }\n }\n\n async #subscribeInternal(\n subscriptionRequest: SubscriptionRequest,\n ): Promise<void> {\n const { request, subscriptionId, isUpdate } = subscriptionRequest;\n\n // Filter to active chains only\n const chainsToSubscribe = request.chainIds.filter((chainId) =>\n this.state.activeChains.includes(chainId),\n );\n\n const addresses = request.accountsWithSupportedChains.map(\n (a) => a.account.address,\n );\n\n if (addresses.length === 0) {\n return;\n }\n\n // Check WebSocket connection status\n try {\n const connectionInfo = this.#messenger.call(\n 'BackendWebSocketService:getConnectionInfo',\n );\n if (connectionInfo.state !== ('connected' as WebSocketState)) {\n // Store the subscription request to process when WebSocket connects\n this.#pendingSubscriptions.set(subscriptionId, subscriptionRequest);\n return;\n }\n } catch {\n // Store anyway - will be processed when we can connect\n this.#pendingSubscriptions.set(subscriptionId, subscriptionRequest);\n return;\n }\n\n // Remove from pending if it was there (we're processing it now)\n this.#pendingSubscriptions.delete(subscriptionId);\n\n // Handle subscription update\n if (isUpdate) {\n const existing = this.activeSubscriptions.get(subscriptionId);\n if (existing) {\n // Check if accounts changed - if so, we need to re-subscribe to different channels\n const existingAddresses = existing.addresses ?? [];\n const addressesChanged = haveAddressesChanged(\n addresses,\n existingAddresses,\n );\n\n if (!addressesChanged) {\n // Only chains changed - update chains, request, and callback\n existing.chains = chainsToSubscribe;\n existing.onAssetsUpdate = subscriptionRequest.onAssetsUpdate;\n this.#subscriptionRequests.set(subscriptionId, subscriptionRequest);\n return;\n }\n // Accounts changed - fall through to re-subscribe with new channels\n }\n }\n\n // Clean up existing subscription if any (inline teardown — subscribe holds the lock)\n await this.#teardownSubscription(subscriptionId);\n\n // Always subscribe to eip155 and solana account activity, plus any namespaces from requested chains\n const namespaces = getNamespacesForAccountActivity(chainsToSubscribe);\n\n // Build channel names: use namespace-appropriate address per account (eip155 = hex, solana = base58)\n const channels: string[] = [];\n for (const namespace of namespaces) {\n for (const { account } of request.accountsWithSupportedChains) {\n const address = getAddressForAccountActivity(account, namespace);\n if (address) {\n channels.push(buildAccountActivityChannel(namespace, address));\n }\n }\n }\n\n if (channels.length === 0) {\n return;\n }\n\n try {\n // Register request/callback before awaiting server subscribe so notifications\n // that arrive during the subscribe handshake are not dropped.\n this.#subscriptionRequests.set(subscriptionId, subscriptionRequest);\n this.activeSubscriptions.set(subscriptionId, {\n cleanup: () => {\n this.#teardownSubscription(subscriptionId).catch(() => undefined);\n },\n chains: chainsToSubscribe,\n addresses,\n onAssetsUpdate: subscriptionRequest.onAssetsUpdate,\n });\n\n // Create WebSocket subscription\n const wsSubscription = await this.#messenger.call(\n 'BackendWebSocketService:subscribe',\n {\n channels,\n channelType: CHANNEL_TYPE,\n callback: (notification: ServerNotificationMessage) => {\n this.#handleNotification(notification, subscriptionId);\n },\n },\n );\n\n this.#wsSubscriptions.set(subscriptionId, wsSubscription);\n\n try {\n this.#registerChannelCallbacks(subscriptionId, channels);\n } catch (channelCallbackError) {\n log(\n 'Channel callback registration failed; ws subscription still active',\n { subscriptionId, error: channelCallbackError },\n );\n }\n } catch (error) {\n this.activeSubscriptions.delete(subscriptionId);\n this.#subscriptionRequests.delete(subscriptionId);\n log('WebSocket subscription FAILED', {\n subscriptionId,\n error,\n chains: chainsToSubscribe,\n });\n }\n }\n\n // ============================================================================\n // NOTIFICATION HANDLING\n // ============================================================================\n\n #handleNotification(\n notification: ServerNotificationMessage,\n subscriptionId: string,\n ): void {\n try {\n const activityMessage =\n notification.data as unknown as AccountActivityMessage;\n\n const storedSubscription = this.#subscriptionRequests.get(subscriptionId);\n const request = storedSubscription?.request;\n const onAssetsUpdate =\n this.activeSubscriptions.get(subscriptionId)?.onAssetsUpdate ??\n storedSubscription?.onAssetsUpdate;\n\n if (!request) {\n return;\n }\n\n const { address, tx, updates } = activityMessage;\n\n if (!address || !tx || !updates) {\n return;\n }\n\n // Extract chain ID from transaction (CAIP-2 format, e.g., \"eip155:8453\")\n const chainId = tx.chain as ChainId;\n\n // Find matching account in request (eip155: case-insensitive hex; solana: exact base58)\n const account = request.accountsWithSupportedChains\n .map((entry) => entry.account)\n .find((a) =>\n a.address.startsWith('0x')\n ? a.address.toLowerCase() === address.toLowerCase()\n : a.address === address,\n );\n if (!account) {\n return;\n }\n const accountId = account.id;\n\n // Process all balance updates from the activity message\n const response = this.#processBalanceUpdates(updates, chainId, accountId);\n\n const balanceEntries = response.assetsBalance?.[accountId] ?? {};\n const hasBalances = Object.keys(balanceEntries).length > 0;\n\n if (hasBalances && onAssetsUpdate) {\n Promise.resolve(onAssetsUpdate(response, request)).catch((error) => {\n console.error(error);\n });\n }\n } catch (error) {\n log('Error handling notification', error);\n }\n }\n\n /**\n * Process balance updates from AccountActivityMessage.\n * Each update contains asset info, post-transaction balance, and transfer details.\n *\n * @param updates - Array of balance updates from the activity message.\n * @param _chainId - The chain ID (unused but kept for context).\n * @param accountId - The account ID to process updates for.\n * @returns DataResponse containing processed balance and metadata.\n */\n #processBalanceUpdates(\n updates: BalanceUpdate[],\n _chainId: ChainId,\n accountId: string,\n ): DataResponse {\n return processAccountActivityBalanceUpdates(updates, accountId, (assetId) =>\n this.#getAssetType(assetId),\n );\n }\n\n // ============================================================================\n // UNSUBSCRIBE\n // ============================================================================\n\n /**\n * Unsubscribe and await server-side teardown so a re-subscribe does not race\n * with stale subscription IDs on incoming notifications.\n *\n * @param subscriptionId - The ID of the subscription to cancel.\n */\n async unsubscribe(subscriptionId: string): Promise<void> {\n const previousLock = this.#subscribeLock;\n let releaseLock: () => void = () => undefined;\n this.#subscribeLock = new Promise<void>((resolve) => {\n releaseLock = resolve;\n });\n\n await previousLock;\n try {\n await this.#teardownSubscription(subscriptionId);\n } finally {\n releaseLock();\n }\n }\n\n async #teardownSubscription(subscriptionId: string): Promise<void> {\n const wsSub = this.#wsSubscriptions.get(subscriptionId);\n\n if (wsSub) {\n const channels = [...wsSub.channels];\n try {\n await wsSub.unsubscribe();\n } catch (unsubErr: unknown) {\n log('Error unsubscribing', { subscriptionId, error: unsubErr });\n }\n this.#wsSubscriptions.delete(subscriptionId);\n this.#removeChannelCallbacks(channels);\n }\n\n this.#subscriptionRequests.delete(subscriptionId);\n this.activeSubscriptions.delete(subscriptionId);\n }\n\n #registerChannelCallbacks(subscriptionId: string, channels: string[]): void {\n for (const channel of channels) {\n this.#unregisterChannelCallback(channel);\n\n try {\n this.#messenger.call('BackendWebSocketService:addChannelCallback', {\n channelName: channel,\n callback: (notification: ServerNotificationMessage) => {\n this.#handleNotification(notification, subscriptionId);\n },\n });\n this.#registeredChannelCallbacks.add(channel);\n } catch {\n // Channel callbacks are optional; ws subscription still works without them.\n }\n }\n }\n\n #unregisterChannelCallback(channel: string): void {\n if (!this.#registeredChannelCallbacks.has(channel)) {\n return;\n }\n\n try {\n this.#messenger.call(\n 'BackendWebSocketService:removeChannelCallback',\n channel,\n );\n } catch {\n // Best-effort cleanup when the channel callback was never registered.\n }\n\n this.#registeredChannelCallbacks.delete(channel);\n }\n\n #removeChannelCallbacks(channels: string[]): void {\n for (const channel of channels) {\n this.#unregisterChannelCallback(channel);\n }\n }\n\n // ============================================================================\n // CLEANUP\n // ============================================================================\n\n destroy(): void {\n if (this.#chainsRefreshTimer) {\n clearInterval(this.#chainsRefreshTimer);\n this.#chainsRefreshTimer = null;\n }\n\n const subscriptionIds = [\n ...new Set([\n ...this.#wsSubscriptions.keys(),\n ...this.activeSubscriptions.keys(),\n ]),\n ];\n for (const subscriptionId of subscriptionIds) {\n this.#teardownSubscription(subscriptionId).catch(() => undefined);\n }\n\n // Clean up base class subscriptions (no-op if already torn down)\n super.destroy();\n }\n}\n\n// ============================================================================\n// FACTORY FUNCTION\n// ============================================================================\n\n/**\n * Creates a BackendWebsocketDataSource instance.\n *\n * @param options - Configuration options for the data source.\n * @returns A new BackendWebsocketDataSource instance.\n */\nexport function createBackendWebsocketDataSource(\n options: BackendWebsocketDataSourceOptions,\n): BackendWebsocketDataSource {\n return new BackendWebsocketDataSource(options);\n}\n"]}
@@ -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, _PriceDataSource_activeSubscriptions, _PriceDataSource_fetchSpotPricesBatch, _PriceDataSource_fetchSpotPrices, _PriceDataSource_getAssetIdsFromBalanceState;
13
+ var _PriceDataSource_instances, _PriceDataSource_getSelectedCurrency, _PriceDataSource_pollInterval, _PriceDataSource_apiClient, _PriceDataSource_fetchTimeoutMs, _PriceDataSource_deduper, _PriceDataSource_activeSubscriptions, _PriceDataSource_fetchSpotPricesBatch, _PriceDataSource_executeBatchFetch, _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,6 +18,7 @@ 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");
21
22
  const evm_rpc_services_1 = require("./evm-rpc-services/index.cjs");
22
23
  // ============================================================================
23
24
  // CONSTANTS
@@ -25,6 +26,13 @@ const evm_rpc_services_1 = require("./evm-rpc-services/index.cjs");
25
26
  const CONTROLLER_NAME = 'PriceDataSource';
26
27
  const DEFAULT_POLL_INTERVAL = 60000; // 1 minute for price updates
27
28
  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;
28
36
  /** Maximum number of asset IDs per Price API request. */
29
37
  const PRICE_API_BATCH_SIZE = 50;
30
38
  const log = (0, logger_1.createModuleLogger)(logger_1.projectLogger, CONTROLLER_NAME);
@@ -93,12 +101,23 @@ class PriceDataSource {
93
101
  /** ApiPlatformClient for cached API calls */
94
102
  _PriceDataSource_apiClient.set(this, void 0);
95
103
  _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);
96
111
  /** Active subscriptions by ID */
97
112
  _PriceDataSource_activeSubscriptions.set(this, new Map());
98
113
  __classPrivateFieldSet(this, _PriceDataSource_getSelectedCurrency, options.getSelectedCurrency, "f");
99
114
  __classPrivateFieldSet(this, _PriceDataSource_pollInterval, options.pollInterval ?? DEFAULT_POLL_INTERVAL, "f");
100
115
  __classPrivateFieldSet(this, _PriceDataSource_apiClient, options.queryApiClient, "f");
101
116
  __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");
102
121
  }
103
122
  // ============================================================================
104
123
  // MIDDLEWARE
@@ -122,20 +141,24 @@ class PriceDataSource {
122
141
  return (0, types_1.forDataTypes)(['price'], async (ctx, next) => {
123
142
  // Extract response from context
124
143
  const { response, request } = ctx;
125
- // Only fetch prices for detected assets (assets without metadata)
126
- // The subscription handles fetching prices for all existing assets
127
- if (!response.detectedAssets && !request.assetsForPriceUpdate?.length) {
128
- return next(ctx);
129
- }
144
+ const statePrices = (ctx.getAssetsState()?.assetsPrice ?? {});
130
145
  const assetIds = new Set();
146
+ for (const assetId of request.assetsForPriceUpdate ?? []) {
147
+ assetIds.add(assetId);
148
+ }
149
+ // Detected assets only need a price fetch when state has none yet.
150
+ // Explicit assetsForPriceUpdate (e.g. currency change) are always fetched.
131
151
  for (const detectedAccountAssets of Object.values(response.detectedAssets ?? {})) {
132
152
  for (const assetId of detectedAccountAssets) {
133
- assetIds.add(assetId);
153
+ const normalizedAssetId = (0, utils_2.normalizeAssetId)(assetId);
154
+ const alreadyQueued = request.assetsForPriceUpdate?.some((queuedId) => queuedId === assetId || queuedId === normalizedAssetId);
155
+ if (statePrices[assetId] === undefined &&
156
+ statePrices[normalizedAssetId] === undefined &&
157
+ !alreadyQueued) {
158
+ assetIds.add(normalizedAssetId);
159
+ }
134
160
  }
135
161
  }
136
- for (const assetId of request.assetsForPriceUpdate ?? []) {
137
- assetIds.add(assetId);
138
- }
139
162
  if (assetIds.size === 0) {
140
163
  return next(ctx);
141
164
  }
@@ -144,6 +167,9 @@ class PriceDataSource {
144
167
  if (priceableAssetIds.length === 0) {
145
168
  return next(ctx);
146
169
  }
170
+ if (request.forceUpdate) {
171
+ __classPrivateFieldGet(this, _PriceDataSource_deduper, "f").invalidateKeys(priceableAssetIds);
172
+ }
147
173
  try {
148
174
  const spotPrices = await __classPrivateFieldGet(this, _PriceDataSource_instances, "m", _PriceDataSource_fetchSpotPrices).call(this, priceableAssetIds);
149
175
  response.assetsPrice = {
@@ -212,20 +238,32 @@ class PriceDataSource {
212
238
  // Clean up existing subscription
213
239
  await this.unsubscribe(subscriptionId);
214
240
  const pollInterval = request.updateInterval ?? __classPrivateFieldGet(this, _PriceDataSource_pollInterval, "f");
215
- // Create poll function - fetches prices using getAssetsState from subscription
241
+ // Cap the freshness TTL strictly below the effective poll interval.
242
+ // `fetchedAt` is stamped when a fetch completes (slightly after the tick
243
+ // that triggered it), so a TTL equal to the poll interval would leave the
244
+ // asset still "fresh" at the next tick, making the subscription re-fetch
245
+ // only every other poll. The margin also absorbs network latency / jitter.
246
+ __classPrivateFieldGet(this, _PriceDataSource_deduper, "f").freshnessTtlMs = Math.min(__classPrivateFieldGet(this, _PriceDataSource_deduper, "f").freshnessTtlMs, Math.floor(pollInterval * FRESHNESS_TTL_POLL_RATIO));
247
+ // Create poll function - fetches prices using getAssetsState from subscription.
248
+ // The freshness TTL naturally gates re-fetches: assets fetched less than
249
+ // `priceFreshnessTtlMs` ago are skipped, preventing duplicates when middleware
250
+ // or other triggers already fetched the same assets between polls.
251
+ // Concurrent middleware calls will join the inflight promise rather than
252
+ // issuing duplicate requests.
216
253
  const pollFn = async () => {
217
254
  try {
218
255
  const subscription = __classPrivateFieldGet(this, _PriceDataSource_activeSubscriptions, "f").get(subscriptionId);
219
256
  if (!subscription) {
220
257
  return;
221
258
  }
222
- // Fetch prices for all assets in balance state (uses subscription's getAssetsState)
223
259
  const fetchResponse = await this.fetch(subscription.request, subscription.getAssetsState);
224
260
  // Only report if we got prices
225
261
  if (fetchResponse.assetsPrice &&
226
262
  Object.keys(fetchResponse.assetsPrice).length > 0) {
227
263
  await subscription.onAssetsUpdate({
228
264
  ...fetchResponse,
265
+ // merge overwrites existing spot prices on each poll; update would
266
+ // seed-only and leave the first price forever.
229
267
  updateMode: 'merge',
230
268
  });
231
269
  }
@@ -262,6 +300,14 @@ class PriceDataSource {
262
300
  __classPrivateFieldGet(this, _PriceDataSource_activeSubscriptions, "f").delete(subscriptionId);
263
301
  }
264
302
  }
303
+ /**
304
+ * Invalidate the price freshness cache, forcing the next fetch to call the
305
+ * API regardless of TTL. Use when external state changes (e.g. selected
306
+ * currency) require a full refresh.
307
+ */
308
+ invalidatePriceCache() {
309
+ __classPrivateFieldGet(this, _PriceDataSource_deduper, "f").invalidate();
310
+ }
265
311
  /**
266
312
  * Destroy the data source and clean up all subscriptions.
267
313
  */
@@ -270,10 +316,11 @@ class PriceDataSource {
270
316
  subscription.cleanup();
271
317
  }
272
318
  __classPrivateFieldGet(this, _PriceDataSource_activeSubscriptions, "f").clear();
319
+ __classPrivateFieldGet(this, _PriceDataSource_deduper, "f").destroy();
273
320
  }
274
321
  }
275
322
  exports.PriceDataSource = PriceDataSource;
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 =
323
+ _PriceDataSource_getSelectedCurrency = new WeakMap(), _PriceDataSource_pollInterval = new WeakMap(), _PriceDataSource_apiClient = new WeakMap(), _PriceDataSource_fetchTimeoutMs = new WeakMap(), _PriceDataSource_deduper = new WeakMap(), _PriceDataSource_activeSubscriptions = new WeakMap(), _PriceDataSource_instances = new WeakSet(), _PriceDataSource_fetchSpotPricesBatch =
277
324
  // ============================================================================
278
325
  // HELPERS
279
326
  // ============================================================================
@@ -303,15 +350,17 @@ async function _PriceDataSource_fetchSpotPricesBatch(assetIds, selectedCurrency)
303
350
  }), __classPrivateFieldGet(this, _PriceDataSource_fetchTimeoutMs, "f")),
304
351
  ]);
305
352
  return { selectedCurrencyPrices, usdPrices };
306
- }, _PriceDataSource_fetchSpotPrices =
353
+ }, _PriceDataSource_executeBatchFetch =
307
354
  /**
308
- * Fetch spot prices for all provided asset IDs, splitting into batches of
309
- * PRICE_API_BATCH_SIZE to respect API limits.
355
+ * Execute the actual batched API call for a set of asset IDs and return
356
+ * parsed price results. Used as the `fetchBatch` callback for the deduper,
357
+ * so it does NOT check freshness or inflight state — that is handled by
358
+ * {@link DedupingBatchFetcher}.
310
359
  *
311
- * @param assetIds - Array of CAIP-19 asset IDs
312
- * @returns Spot prices response
360
+ * @param assetIds - Asset IDs to fetch (already filtered/deduplicated).
361
+ * @returns Parsed prices keyed by CAIP-19 asset ID.
313
362
  */
314
- async function _PriceDataSource_fetchSpotPrices(assetIds) {
363
+ async function _PriceDataSource_executeBatchFetch(assetIds) {
315
364
  const selectedCurrency = __classPrivateFieldGet(this, _PriceDataSource_getSelectedCurrency, "f").call(this);
316
365
  const batchResults = await (0, evm_rpc_services_1.reduceInBatchesSerially)({
317
366
  values: assetIds,
@@ -322,6 +371,7 @@ async function _PriceDataSource_fetchSpotPrices(assetIds) {
322
371
  },
323
372
  initialResult: [],
324
373
  });
374
+ const fetchedAt = Date.now();
325
375
  const prices = {};
326
376
  for (const { selectedCurrencyPrices, usdPrices } of batchResults) {
327
377
  for (const [assetId, marketData] of Object.entries(selectedCurrencyPrices)) {
@@ -330,16 +380,26 @@ async function _PriceDataSource_fetchSpotPrices(assetIds) {
330
380
  !isValidMarketData(usdMarketData)) {
331
381
  continue;
332
382
  }
333
- const caipAssetId = assetId;
334
- prices[caipAssetId] = {
383
+ prices[assetId] = {
335
384
  ...marketData,
336
385
  assetPriceType: 'fungible',
337
386
  usdPrice: usdMarketData.price,
338
- lastUpdated: Date.now(),
387
+ lastUpdated: fetchedAt,
339
388
  };
340
389
  }
341
390
  }
342
391
  return prices;
392
+ }, _PriceDataSource_fetchSpotPrices =
393
+ /**
394
+ * Fetch spot prices for all provided asset IDs, deduplicating via the
395
+ * deduper (freshness TTL + per-asset inflight coalescing).
396
+ *
397
+ * @param assetIds - Array of CAIP-19 asset IDs.
398
+ * @returns Spot prices response (only contains entries for assets that were
399
+ * actually fetched or joined from inflight).
400
+ */
401
+ async function _PriceDataSource_fetchSpotPrices(assetIds) {
402
+ return __classPrivateFieldGet(this, _PriceDataSource_deduper, "f").fetch(assetIds);
343
403
  }, _PriceDataSource_getAssetIdsFromBalanceState = function _PriceDataSource_getAssetIdsFromBalanceState(request, getAssetsState) {
344
404
  if (!getAssetsState) {
345
405
  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 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"]}
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, normalizeAssetId } 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 const statePrices = (ctx.getAssetsState()?.assetsPrice ?? {}) as Record<\n string,\n FungibleAssetPrice\n >;\n\n const assetIds = new Set<Caip19AssetId>();\n\n for (const assetId of request.assetsForPriceUpdate ?? []) {\n assetIds.add(assetId);\n }\n\n // Detected assets only need a price fetch when state has none yet.\n // Explicit assetsForPriceUpdate (e.g. currency change) are always fetched.\n for (const detectedAccountAssets of Object.values(\n response.detectedAssets ?? {},\n )) {\n for (const assetId of detectedAccountAssets) {\n const normalizedAssetId = normalizeAssetId(assetId);\n const alreadyQueued = request.assetsForPriceUpdate?.some(\n (queuedId) =>\n queuedId === assetId || queuedId === normalizedAssetId,\n );\n if (\n statePrices[assetId] === undefined &&\n statePrices[normalizedAssetId] === undefined &&\n !alreadyQueued\n ) {\n assetIds.add(normalizedAssetId);\n }\n }\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 if (request.forceUpdate) {\n this.#deduper.invalidateKeys(priceableAssetIds);\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 // merge overwrites existing spot prices on each poll; update would\n // seed-only and leave the first price forever.\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"]}
@@ -11,6 +11,13 @@ 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;
14
21
  };
15
22
  export type PriceDataSourceOptions = PriceDataSourceConfig & {
16
23
  /** ApiPlatformClient for API calls with caching */
@@ -71,6 +78,12 @@ export declare class PriceDataSource {
71
78
  * @param subscriptionId - The ID of the subscription to cancel.
72
79
  */
73
80
  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;
74
87
  /**
75
88
  * Destroy the data source and clean up all subscriptions.
76
89
  */
@@ -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;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"}
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;AAGlB,OAAO,KAAK,EAAE,mBAAmB,EAAE,iCAA6B;AA4BhE,kDAAkD;AAClD,MAAM,MAAM,qBAAqB,GAAG;IAClC,8CAA8C;IAC9C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B,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;gBAgCL,OAAO,EAAE,sBAAsB;IAkB3C;;;;;;;;;;;;;;OAcG;IACH,IAAI,gBAAgB,IAAI,UAAU,CAiEjC;IAwMD;;;;;;;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;IAiFxE;;;;OAIG;IACG,WAAW,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQxD;;;;OAIG;IACH,oBAAoB,IAAI,IAAI;IAI5B;;OAEG;IACH,OAAO,IAAI,IAAI;CAOhB"}