diff --git a/packages/assets-controller/CHANGELOG.md b/packages/assets-controller/CHANGELOG.md index 814c75ef032..fc1682aef55 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add optional `isBasicFunctionalityEnabled` option. When it returns `false`, the controller uses only RPC for balance data (no Snap, Accounts API, or Backend WebSocket). Subscriptions and force-update fetches both respect this setting. + ### Changed +- **BREAKING:** Require `previousChains` in `handleActiveChainsUpdate(dataSourceId, activeChains, previousChains)` and in the `onActiveChainsUpdated` callback used by data sources; the third parameter is no longer optional. Callers and data sources must pass the previous chain list for correct added/removed chain diff computation ([#7867](https://github.com/MetaMask/core/pull/7867)) - Bump `@metamask/account-tree-controller` from `^4.0.0` to `^4.1.0` ([#7869](https://github.com/MetaMask/core/pull/7869)) ### Removed diff --git a/packages/assets-controller/src/AssetsController-method-action-types.ts b/packages/assets-controller/src/AssetsController-method-action-types.ts index 10d43d35409..938cc38f80e 100644 --- a/packages/assets-controller/src/AssetsController-method-action-types.ts +++ b/packages/assets-controller/src/AssetsController-method-action-types.ts @@ -82,6 +82,16 @@ export type AssetsControllerUnhideAssetAction = { handler: AssetsController['unhideAsset']; }; +/** + * Rebuild balance and price subscriptions (e.g. after the "use external services" + * / "basic functionality" setting changes). Call this from the host when the + * preference changes so subscriptions use the new data sources. + */ +export type AssetsControllerRefreshSubscriptionsAction = { + type: `AssetsController:refreshSubscriptions`; + handler: AssetsController['refreshSubscriptions']; +}; + /** * Union of all AssetsController action types. */ @@ -94,4 +104,5 @@ export type AssetsControllerMethodActions = | AssetsControllerRemoveCustomAssetAction | AssetsControllerGetCustomAssetsAction | AssetsControllerHideAssetAction - | AssetsControllerUnhideAssetAction; + | AssetsControllerUnhideAssetAction + | AssetsControllerRefreshSubscriptionsAction; diff --git a/packages/assets-controller/src/AssetsController.test.ts b/packages/assets-controller/src/AssetsController.test.ts index 404de13eb61..933c1dc2665 100644 --- a/packages/assets-controller/src/AssetsController.test.ts +++ b/packages/assets-controller/src/AssetsController.test.ts @@ -55,6 +55,13 @@ function createMockInternalAccount( type WithControllerOptions = { state?: Partial; + /** Extra options passed to AssetsController constructor. */ + controllerOptions?: Partial<{ + isBasicFunctionalityEnabled: () => boolean; + subscribeToBasicFunctionalityChange: ( + callback: (isBasicFunctionalityEnabled: boolean) => void, + ) => () => void; + }>; }; type WithControllerCallback = ({ @@ -77,7 +84,8 @@ async function withController( | [WithControllerOptions, WithControllerCallback] | [WithControllerCallback] ): Promise { - const [{ state = {} }, fn] = args.length === 2 ? args : [{}, args[0]]; + const [{ state = {}, controllerOptions = {} }, fn] = + args.length === 2 ? args : [{}, args[0]]; // Use root messenger (MOCK_ANY_NAMESPACE) so data sources can register their actions. const messenger: RootMessenger = new Messenger({ @@ -130,6 +138,7 @@ async function withController( messenger: messenger as unknown as AssetsControllerMessenger, state, queryApiClient: createMockQueryApiClient(), + ...controllerOptions, }); return fn({ controller, messenger }); @@ -384,17 +393,6 @@ describe('AssetsController', () => { }); }); - describe('registerDataSources', () => { - it('registers data sources in constructor', async () => { - await withController(({ controller }) => { - // The controller registers these data sources in the constructor: - // 'BackendWebsocketDataSource', 'AccountsApiDataSource', 'SnapDataSource', 'RpcDataSource' - // We verify initialization completed without error - expect(controller.state).toBeDefined(); - }); - }); - }); - describe('getAssetMetadata', () => { it('returns metadata for existing asset', async () => { const initialState: Partial = { @@ -462,6 +460,67 @@ describe('AssetsController', () => { expect(assets).toBeDefined(); }); }); + + it('uses only RPC when isBasicFunctionalityEnabled returns false', async () => { + await withController( + { controllerOptions: { isBasicFunctionalityEnabled: () => false } }, + async ({ controller }) => { + const accounts = [createMockInternalAccount()]; + const assets = await controller.getAssets(accounts, { + chainIds: ['eip155:1'], + forceUpdate: true, + }); + + expect(assets).toBeDefined(); + expect(assets[MOCK_ACCOUNT_ID]).toBeDefined(); + }, + ); + }); + }); + + describe('refreshSubscriptions and subscribeToBasicFunctionalityChange', () => { + it('refreshSubscriptions runs without throwing', async () => { + await withController(async ({ controller }) => { + expect(() => controller.refreshSubscriptions()).not.toThrow(); + }); + }); + + it('when subscribeToBasicFunctionalityChange is provided, callback triggers refresh', async () => { + let capturedCallback: ((enabled: boolean) => void) | undefined; + const unsubscribe = jest.fn(); + + await withController( + { + controllerOptions: { + subscribeToBasicFunctionalityChange: (callback) => { + capturedCallback = callback; + return unsubscribe; + }, + }, + }, + async ({ controller }) => { + expect(capturedCallback).toBeDefined(); + expect(() => capturedCallback?.(false)).not.toThrow(); + expect(() => capturedCallback?.(true)).not.toThrow(); + }, + ); + }); + + it('calls unsubscribe on destroy when subscribeToBasicFunctionalityChange was provided', async () => { + const unsubscribe = jest.fn(); + + await withController( + { + controllerOptions: { + subscribeToBasicFunctionalityChange: () => unsubscribe, + }, + }, + async ({ controller }) => { + controller.destroy(); + expect(unsubscribe).toHaveBeenCalledTimes(1); + }, + ); + }); }); describe('getAssetsBalance', () => { @@ -489,7 +548,7 @@ describe('AssetsController', () => { describe('handleActiveChainsUpdate', () => { it('updates data source chains', async () => { await withController(({ controller }) => { - controller.handleActiveChainsUpdate('TestDataSource', ['eip155:1']); + controller.handleActiveChainsUpdate('TestDataSource', ['eip155:1'], []); // Should not throw expect(controller.state).toBeDefined(); @@ -498,7 +557,7 @@ describe('AssetsController', () => { it('handles empty chains array', async () => { await withController(({ controller }) => { - controller.handleActiveChainsUpdate('TestDataSource', []); + controller.handleActiveChainsUpdate('TestDataSource', [], []); expect(controller.state).toBeDefined(); }); @@ -507,10 +566,10 @@ describe('AssetsController', () => { it('triggers fetch when chains are added', async () => { await withController(async ({ controller }) => { // First set no chains - controller.handleActiveChainsUpdate('TestDataSource', []); + controller.handleActiveChainsUpdate('TestDataSource', [], []); // Then add chains - this should trigger fetch for added chains - controller.handleActiveChainsUpdate('TestDataSource', ['eip155:1']); + controller.handleActiveChainsUpdate('TestDataSource', ['eip155:1'], []); // Allow async operations to complete await new Promise(process.nextTick); diff --git a/packages/assets-controller/src/AssetsController.ts b/packages/assets-controller/src/AssetsController.ts index f6d25a358db..8e23916fa42 100644 --- a/packages/assets-controller/src/AssetsController.ts +++ b/packages/assets-controller/src/AssetsController.ts @@ -30,7 +30,11 @@ import BigNumberJS from 'bignumber.js'; import { isEqual } from 'lodash'; import type { AssetsControllerMethodActions } from './AssetsController-method-action-types'; -import type { SubscriptionRequest } from './data-sources/AbstractDataSource'; +import type { + AbstractDataSource, + DataSourceState, + SubscriptionRequest, +} from './data-sources/AbstractDataSource'; import { AccountsApiDataSource } from './data-sources/AccountsApiDataSource'; import { BackendWebsocketDataSource } from './data-sources/BackendWebsocketDataSource'; import { PriceDataSource } from './data-sources/PriceDataSource'; @@ -55,7 +59,6 @@ import type { DataResponse, NextFunction, Middleware, - DataSourceDefinition, SubscriptionResponse, Asset, AssetsControllerStateInternal, @@ -79,6 +82,7 @@ const MESSENGER_EXPOSED_METHODS = [ 'getCustomAssets', 'hideAsset', 'unhideAsset', + 'refreshSubscriptions', ] as const; /** Default polling interval hint for data sources (30 seconds) */ @@ -209,6 +213,22 @@ export type AssetsControllerOptions = { queryApiClient: ApiPlatformClient; /** Optional configuration for RpcDataSource. */ rpcDataSourceConfig?: RpcDataSourceConfig; + /** + * When false, the controller uses only RPC for balance data: no Snap, + * Accounts API, or Backend WebSocket. Defaults to true (use all sources). + */ + isBasicFunctionalityEnabled?: () => boolean; + /** + * Optional. When provided, the controller subscribes to basic-functionality + * toggle changes and immediately refreshes subscriptions (stop then start) + * so WebSocket and other external sources are torn down or brought up + * without waiting for the next lifecycle event. + * @param callback - Called with the new value when the setting changes. + * @returns Unsubscribe function. + */ + subscribeToBasicFunctionalityChange?: ( + callback: (isBasicFunctionalityEnabled: boolean) => void, + ) => () => void; }; // ============================================================================ @@ -357,6 +377,12 @@ export class AssetsController extends BaseController< /** Default update interval hint passed to data sources */ readonly #defaultUpdateInterval: number; + /** When false, use only RPC (no Snap, Accounts API, WebSocket). */ + readonly #isBasicFunctionalityEnabled: () => boolean; + + /** Unsubscribe from basic-functionality toggle when provided by host. */ + #unsubscribeBasicFunctionalityChange: (() => void) | undefined; + readonly #controllerMutex = new Mutex(); /** @@ -383,13 +409,6 @@ export class AssetsController extends BaseController< ); } - /** - * Registered data sources with their available chains. - * Updated continuously and independently from subscription flows. - * Key: sourceId, Value: Set of currently available chainIds - */ - readonly #dataSources: Map> = new Map(); - readonly #backendWebsocketDataSource: BackendWebsocketDataSource; readonly #accountsApiDataSource: AccountsApiDataSource; @@ -398,6 +417,29 @@ export class AssetsController extends BaseController< readonly #rpcDataSource: RpcDataSource; + /** + * Subscription balance data sources in assignment priority order (first that supports a chain gets it). + * When basic functionality is off, returns only RpcDataSource. + * + * @returns Balance data source instances in priority order. + */ + get #subscriptionBalanceDataSources(): ( + | BackendWebsocketDataSource + | AccountsApiDataSource + | SnapDataSource + | RpcDataSource + )[] { + if (!this.#isBasicFunctionalityEnabled()) { + return [this.#rpcDataSource]; + } + return [ + this.#backendWebsocketDataSource, + this.#accountsApiDataSource, + this.#snapDataSource, + this.#rpcDataSource, + ]; + } + readonly #priceDataSource: PriceDataSource; readonly #detectionMiddleware: DetectionMiddleware; @@ -411,6 +453,8 @@ export class AssetsController extends BaseController< isEnabled = (): boolean => true, queryApiClient, rpcDataSourceConfig, + isBasicFunctionalityEnabled = (): boolean => true, + subscribeToBasicFunctionalityChange, }: AssetsControllerOptions) { super({ name: CONTROLLER_NAME, @@ -424,29 +468,33 @@ export class AssetsController extends BaseController< this.#isEnabled = isEnabled(); this.#defaultUpdateInterval = defaultUpdateInterval; + this.#isBasicFunctionalityEnabled = isBasicFunctionalityEnabled; + this.#unsubscribeBasicFunctionalityChange = undefined; const rpcConfig = rpcDataSourceConfig ?? {}; + const onActiveChainsUpdated = ( + dataSourceName: string, + chains: ChainId[], + previousChains: ChainId[], + ): void => + this.handleActiveChainsUpdate(dataSourceName, chains, previousChains); + this.#backendWebsocketDataSource = new BackendWebsocketDataSource({ messenger: this.messenger, queryApiClient, - onActiveChainsUpdated: (chains): void => - this.handleActiveChainsUpdate('BackendWebsocketDataSource', chains), + onActiveChainsUpdated, }); this.#accountsApiDataSource = new AccountsApiDataSource({ queryApiClient, - onActiveChainsUpdated: (chains): void => { - this.handleActiveChainsUpdate('AccountsApiDataSource', chains); - }, + onActiveChainsUpdated, }); this.#snapDataSource = new SnapDataSource({ messenger: this.messenger, - onActiveChainsUpdated: (chains): void => - this.handleActiveChainsUpdate('SnapDataSource', chains), + onActiveChainsUpdated, }); this.#rpcDataSource = new RpcDataSource({ messenger: this.messenger, - onActiveChainsUpdated: (chains): void => - this.handleActiveChainsUpdate('RpcDataSource', chains), + onActiveChainsUpdated, ...rpcConfig, }); this.#tokenDataSource = new TokenDataSource({ @@ -457,11 +505,6 @@ export class AssetsController extends BaseController< }); this.#detectionMiddleware = new DetectionMiddleware(); - this.#dataSources.set('BackendWebsocketDataSource', new Set()); - this.#dataSources.set('AccountsApiDataSource', new Set()); - this.#dataSources.set('SnapDataSource', new Set()); - this.#dataSources.set('RpcDataSource', new Set()); - if (!this.#isEnabled) { log('AssetsController is disabled, skipping initialization'); return; @@ -474,33 +517,12 @@ export class AssetsController extends BaseController< this.#initializeState(); this.#subscribeToEvents(); this.#registerActionHandlers(); - } - /** - * Returns the balance data source instance for subscribe/unsubscribe by sourceId. - * - * @param sourceId - Data source identifier (e.g. 'BackendWebsocketDataSource'). - * @returns The balance data source instance, or undefined if not found. - */ - #getBalanceDataSource( - sourceId: string, - ): - | BackendWebsocketDataSource - | AccountsApiDataSource - | SnapDataSource - | RpcDataSource - | undefined { - switch (sourceId) { - case 'BackendWebsocketDataSource': - return this.#backendWebsocketDataSource; - case 'AccountsApiDataSource': - return this.#accountsApiDataSource; - case 'SnapDataSource': - return this.#snapDataSource; - case 'RpcDataSource': - return this.#rpcDataSource; - default: - return undefined; + if (subscribeToBasicFunctionalityChange) { + this.#unsubscribeBasicFunctionalityChange = + subscribeToBasicFunctionalityChange(() => { + this.refreshSubscriptions(); + }); } } @@ -603,44 +625,27 @@ export class AssetsController extends BaseController< ); } - // ============================================================================ - // DATA SOURCE MANAGEMENT - // ============================================================================ - - /** - * Register data sources with the controller. - * Order of the array determines subscription order. - * - * Data sources report chain changes via the onActiveChainsUpdated callback passed at construction. - * - * @param dataSourceIds - Array of data source identifiers to register. - */ - registerDataSources(dataSourceIds: DataSourceDefinition[]): void { - for (const id of dataSourceIds) { - log('Registering data source', { id }); - - // Initialize available chains tracking for this source - this.#dataSources.set(id, new Set()); - } - } - // ============================================================================ // DATA SOURCE CHAIN MANAGEMENT // ============================================================================ /** - * Handle when a data source's active chains change. - * Active chains are chains that are both supported AND available. - * Updates centralized chain tracking and triggers re-selection if needed. + * Handle when a data source's supported chains change. + * Used to refresh balance subscriptions and run a one-time fetch when a new chain is supported. + * + * - On any add/remove: re-subscribes to data sources so chain assignment stays correct. + * - When chains are added: fetches balances for the new chains (for selected accounts on enabled networks). * - * Called from the onActiveChainsUpdated callbacks passed to data sources at construction. + * Controller does not store chains; sources report via this callback. previousChains is required for diff. * * @param dataSourceId - The identifier of the data source reporting the change. - * @param activeChains - Array of currently active chain IDs for this source. + * @param activeChains - Currently active (supported and available) chain IDs for this source. + * @param previousChains - Previous chains; used to compute added/removed. */ handleActiveChainsUpdate( dataSourceId: string, activeChains: ChainId[], + previousChains: ChainId[], ): void { log('Data source active chains changed', { dataSourceId, @@ -648,30 +653,15 @@ export class AssetsController extends BaseController< chains: activeChains, }); - // When BackendWebsocketDataSource is updated via AccountsApiDataSource callback, sync its state - if (dataSourceId === 'BackendWebsocketDataSource') { - this.#backendWebsocketDataSource.setActiveChainsFromAccountsApi( - activeChains, - ); - } - - const previousChains = this.#dataSources.get(dataSourceId) ?? new Set(); - const newChains = new Set(activeChains); - - // Update centralized available chains tracking - this.#dataSources.set(dataSourceId, newChains); + const previous: ChainId[] = previousChains; - // Check for changes - const addedChains = activeChains.filter( - (chain) => !previousChains.has(chain), - ); - const removedChains = Array.from(previousChains).filter( - (chain) => !newChains.has(chain), - ); + const previousSet = new Set(previous); + const addedChains = activeChains.filter((ch) => !previousSet.has(ch)); + const removedChains = previous.filter((ch) => !activeChains.includes(ch)); if (addedChains.length > 0 || removedChains.length > 0) { // Refresh subscriptions to use updated data source availability - this.#subscribeToDataSources(); + this.#subscribeAssets(); } // If chains were added and we have selected accounts, do one-time fetch @@ -766,11 +756,16 @@ export class AssetsController extends BaseController< customAssets: customAssets.length > 0 ? customAssets : undefined, forceUpdate: true, }); + const balanceMiddlewares = this.#isBasicFunctionalityEnabled() + ? [ + this.#accountsApiDataSource.assetsMiddleware, + this.#snapDataSource.assetsMiddleware, + this.#rpcDataSource.assetsMiddleware, + ] + : [this.#rpcDataSource.assetsMiddleware]; const response = await this.#executeMiddlewares( [ - this.#accountsApiDataSource.assetsMiddleware, - this.#snapDataSource.assetsMiddleware, - this.#rpcDataSource.assetsMiddleware, + ...balanceMiddlewares, this.#detectionMiddleware.assetsMiddleware, this.#tokenDataSource.assetsMiddleware, this.#priceDataSource.assetsMiddleware, @@ -981,48 +976,6 @@ export class AssetsController extends BaseController< // SUBSCRIPTIONS // ============================================================================ - /** - * Assign chains to data sources based on availability. - * Returns a map of sourceId -> chains to handle. - * - * @param requestedChains - Array of chain IDs to assign to data sources. - * @returns Map of sourceId to array of assigned chain IDs. - */ - #assignChainsToDataSources( - requestedChains: ChainId[], - ): Map { - const assignment = new Map(); - const remainingChains = new Set(requestedChains); - - for (const sourceId of this.#dataSources.keys()) { - // Get available chains for this data source - const availableChains = this.#dataSources.get(sourceId); - if (!availableChains || availableChains.size === 0) { - continue; - } - - const chainsForThisSource: ChainId[] = []; - - for (const chainId of remainingChains) { - // Check if this chain is available on this source - if (availableChains.has(chainId)) { - chainsForThisSource.push(chainId); - remainingChains.delete(chainId); - } - } - - if (chainsForThisSource.length > 0) { - assignment.set(sourceId, chainsForThisSource); - log('Assigned chains to data source', { - sourceId, - chains: chainsForThisSource, - }); - } - } - - return assignment; - } - /** * Subscribe to price updates for all assets held by the given accounts. * Polls PriceDataSource which fetches prices from balance state. @@ -1369,7 +1322,7 @@ export class AssetsController extends BaseController< enabledChainCount: this.#enabledChains.size, }); - this.#subscribeToDataSources(); + this.#subscribeAssets(); if (this.#selectedAccounts.length > 0) { this.getAssets(this.#selectedAccounts, { chainIds: [...this.#enabledChains], @@ -1398,32 +1351,50 @@ export class AssetsController extends BaseController< // Convert to array first to avoid modifying map during iteration const subscriptionKeys = [...this.#activeSubscriptions.keys()]; for (const subscriptionKey of subscriptionKeys) { - // Extract sourceId from subscription key (format: "ds:${sourceId}") if (subscriptionKey.startsWith('ds:')) { const sourceId = subscriptionKey.slice(3); - this.#unsubscribeDataSource(sourceId); + const source = this.#subscriptionBalanceDataSources.find( + (ds) => ds.getName() === sourceId, + ); + if (source) { + this.#unsubscribeDataSource(source); + } } } this.#activeSubscriptions.clear(); } + /** + * Rebuild balance and price subscriptions (e.g. after the "use external + * services" / "basic functionality" setting changes). Stops current + * subscriptions (including WebSocket) and starts again with the current + * data source set so the controller immediately reflects the new setting. + */ + refreshSubscriptions(): void { + log('Refreshing subscriptions (basic functionality toggle changed)'); + this.#stop(); + this.#start(); + } + /** * Subscribe to asset updates for all selected accounts. */ - #subscribeToDataSources(): void { + #subscribeAssets(): void { if (this.#selectedAccounts.length === 0) { return; } // Subscribe to balance updates (batched by data source) - this.#subscribeAssetsBalance(); + this.#subscribeAssetsBalance(this.#selectedAccounts, [ + ...this.#enabledChains, + ]); // Subscribe to price updates for all assets held by selected accounts this.subscribeAssetsPrice(this.#selectedAccounts, [...this.#enabledChains]); } /** - * Subscribe to balance updates for all selected accounts. + * Subscribe to balance updates for the given accounts and chains. * * Strategy to minimize data source calls: * 1. Collect all chains to subscribe based on enabled networks @@ -1432,52 +1403,46 @@ export class AssetsController extends BaseController< * * This ensures we make minimal subscriptions to each data source while covering * all accounts and chains. + * + * @param accounts - Accounts to subscribe balance updates for. + * @param chainIds - Chain IDs to subscribe for. */ - #subscribeAssetsBalance(): void { - // Step 1: Build chain -> accounts mapping based on account scopes and enabled networks + #subscribeAssetsBalance( + accounts: InternalAccount[], + chainIds: ChainId[], + ): void { const chainToAccounts = this.#buildChainToAccountsMap( - this.#selectedAccounts, - this.#enabledChains, + accounts, + new Set(chainIds), ); - - // Step 2: Split by data source active chains (ordered by priority) - // Get all chains that need to be subscribed const remainingChains = new Set(chainToAccounts.keys()); - // Assign chains to data sources based on availability (ordered by priority) - const chainAssignment = this.#assignChainsToDataSources( - Array.from(remainingChains), - ); + for (const source of this.#subscriptionBalanceDataSources) { + const availableChains = new Set(source.getActiveChainsSync()); + const assignedChains: ChainId[] = []; - log('Subscribe - chain assignment', { - totalChains: remainingChains.size, - dataSourceAssignments: Array.from(chainAssignment.entries()).map( - ([sourceId, chains]) => ({ sourceId, chainCount: chains.length }), - ), - }); - - // Subscribe to each data source with its assigned chains and relevant accounts - for (const sourceId of this.#dataSources.keys()) { - const assignedChains = chainAssignment.get(sourceId); - - if (!assignedChains || assignedChains.length === 0) { - // Unsubscribe from data sources with no assigned chains - this.#unsubscribeDataSource(sourceId); - continue; + for (const chainId of remainingChains) { + if (availableChains.has(chainId)) { + assignedChains.push(chainId); + remainingChains.delete(chainId); + } } - // Collect unique accounts that need any of the assigned chains - const accountsForSource = this.#getAccountsForChains( - assignedChains, - chainToAccounts, - ); - - if (accountsForSource.length === 0) { + if (assignedChains.length === 0) { + this.#unsubscribeDataSource(source); continue; } - // Subscribe with ONE call per data source - this.#subscribeToDataSource(sourceId, accountsForSource, assignedChains); + const seenIds = new Set(); + const accountsForSource = assignedChains + .flatMap((chainId) => chainToAccounts.get(chainId) ?? []) + .filter( + (account) => + !seenIds.has(account.id) && (seenIds.add(account.id), true), + ); + if (accountsForSource.length > 0) { + this.#subscribeDataSource(source, accountsForSource, assignedChains); + } } } @@ -1494,64 +1459,36 @@ export class AssetsController extends BaseController< chainsToSubscribe: Set, ): Map { const chainToAccounts = new Map(); - for (const account of accounts) { - const accountChains = this.#getEnabledChainsForAccount(account); - - for (const chainId of accountChains) { + for (const chainId of this.#getEnabledChainsForAccount(account)) { if (!chainsToSubscribe.has(chainId)) { continue; } - - const existingAccounts = chainToAccounts.get(chainId) ?? []; - existingAccounts.push(account); - chainToAccounts.set(chainId, existingAccounts); - } - } - - return chainToAccounts; - } - - /** - * Get unique accounts that need any of the specified chains. - * - * @param chains - Array of chain IDs to find accounts for. - * @param chainToAccounts - Map of chainId to accounts. - * @returns Array of unique accounts that need any of the specified chains. - */ - #getAccountsForChains( - chains: ChainId[], - chainToAccounts: Map, - ): InternalAccount[] { - const accountIds = new Set(); - const accounts: InternalAccount[] = []; - - for (const chainId of chains) { - const chainAccounts = chainToAccounts.get(chainId) ?? []; - for (const account of chainAccounts) { - if (!accountIds.has(account.id)) { - accountIds.add(account.id); - accounts.push(account); + let list = chainToAccounts.get(chainId); + if (!list) { + list = []; + chainToAccounts.set(chainId, list); } + list.push(account); } } - - return accounts; + return chainToAccounts; } /** * Subscribe to a specific data source with accounts and chains. - * Uses the data source ID as the subscription key for batching. + * Uses the data source name as the subscription key for batching. * - * @param sourceId - The data source identifier. + * @param source - The balance data source instance. * @param accounts - Array of accounts to subscribe for. * @param chains - Array of chain IDs to subscribe for. */ - #subscribeToDataSource( - sourceId: string, + #subscribeDataSource( + source: AbstractDataSource, accounts: InternalAccount[], chains: ChainId[], ): void { + const sourceId = source.getName(); const subscriptionKey = `ds:${sourceId}`; const existingSubscription = this.#activeSubscriptions.get(subscriptionKey); const isUpdate = existingSubscription !== undefined; @@ -1576,11 +1513,7 @@ export class AssetsController extends BaseController< getAssetsState: () => this.state, }; - const balanceDs = this.#getBalanceDataSource(sourceId); - if (!balanceDs) { - return; - } - balanceDs.subscribe(subscribeReq).catch((error) => { + source.subscribe(subscribeReq).catch((error) => { console.error( `[AssetsController] Failed to subscribe to '${sourceId}':`, error, @@ -1604,17 +1537,16 @@ export class AssetsController extends BaseController< /** * Unsubscribe from a data source if we have an active subscription. * - * @param sourceId - The data source identifier to unsubscribe from. + * @param source - The balance data source instance to unsubscribe from. */ - #unsubscribeDataSource(sourceId: string): void { - const subscriptionKey = `ds:${sourceId}`; + #unsubscribeDataSource( + source: AbstractDataSource, + ): void { + const subscriptionKey = `ds:${source.getName()}`; const existingSubscription = this.#activeSubscriptions.get(subscriptionKey); if (existingSubscription) { - const balanceDs = this.#getBalanceDataSource(sourceId); - if (balanceDs) { - balanceDs.unsubscribe(subscriptionKey).catch(() => undefined); - } + source.unsubscribe(subscriptionKey).catch(() => undefined); existingSubscription.unsubscribe(); } } @@ -1699,7 +1631,7 @@ export class AssetsController extends BaseController< }); // Subscribe and fetch for the new account group - this.#subscribeToDataSources(); + this.#subscribeAssets(); if (accounts.length > 0) { await this.getAssets(accounts, { chainIds: [...this.#enabledChains], @@ -1742,7 +1674,7 @@ export class AssetsController extends BaseController< // The data will simply not be updated until the network is re-enabled. // Refresh subscriptions for new chain set - this.#subscribeToDataSources(); + this.#subscribeAssets(); // Do one-time fetch for newly enabled chains if (addedChains.length > 0 && this.#selectedAccounts.length > 0) { @@ -1798,7 +1730,7 @@ export class AssetsController extends BaseController< destroy(): void { log('Destroying AssetsController', { - dataSourceCount: this.#dataSources.size, + dataSourceCount: this.#subscriptionBalanceDataSources.length, subscriptionCount: this.#activeSubscriptions.size, }); @@ -1815,9 +1747,6 @@ export class AssetsController extends BaseController< (this.#rpcDataSource as { destroy: () => void }).destroy(); } - // Clear data sources - this.#dataSources.clear(); - // Stop all active subscriptions this.#stop(); @@ -1833,5 +1762,11 @@ export class AssetsController extends BaseController< this.messenger.unregisterActionHandler('AssetsController:getCustomAssets'); this.messenger.unregisterActionHandler('AssetsController:hideAsset'); this.messenger.unregisterActionHandler('AssetsController:unhideAsset'); + this.messenger.unregisterActionHandler( + 'AssetsController:refreshSubscriptions', + ); + + this.#unsubscribeBasicFunctionalityChange?.(); + this.#unsubscribeBasicFunctionalityChange = undefined; } } diff --git a/packages/assets-controller/src/README.md b/packages/assets-controller/src/README.md index f70ecc282bb..80496f16e26 100644 --- a/packages/assets-controller/src/README.md +++ b/packages/assets-controller/src/README.md @@ -88,24 +88,9 @@ registerActionHandlers() └── AssetsController:assetsUpdate // Data sources push asset updates ``` -#### 1.5 Register Data Sources +#### 1.5 Balance data source priority -```typescript -registerDataSources([ - 'BackendWebsocketDataSource', // Real-time push updates (highest priority) - 'AccountsApiDataSource', // HTTP polling fallback - 'SnapDataSource', // Solana/Bitcoin/Tron snaps - 'RpcDataSource', // Direct blockchain queries (lowest priority) -]); -``` - -**Registration order determines subscription priority**: - -- Data sources are processed in registration order -- Earlier sources get first pick for chain assignment -- Later sources act as fallbacks for remaining chains - -Data sources report their active chains by calling `AssetsController:activeChainsUpdate` action. +Built-in balance data sources are fixed and processed in priority order: BackendWebsocketDataSource, AccountsApiDataSource, SnapDataSource, RpcDataSource. Earlier sources get first pick for chain assignment; later sources act as fallbacks. Data sources report active chains via the `onActiveChainsUpdated` callback passed at construction. #### 1.6 Middleware Chains @@ -145,15 +130,15 @@ When the keyring unlocks: ``` start() // Called by KeyringController:unlock │ -├── subscribeToDataSources() +├── subscribeAssets() │ │ -│ ├── subscribeAssetsBalance() +│ ├── subscribeAssetsBalance(selectedAccounts, enabledChains) │ │ │ │ │ ├── Build chain → accounts mapping based on account scopes │ │ ├── assignChainsToDataSources(enabledChains) // Order-based assignment │ │ │ │ │ └── For each dataSource (in registration order): -│ │ └── subscribeToDataSource(sourceId, accounts, chains) +│ │ └── subscribeDataSource(sourceId, accounts, chains) │ │ └── Call {sourceId}:subscribe via Messenger │ │ │ └── subscribeAssetsPrice(selectedAccounts, enabledChains) @@ -899,7 +884,7 @@ flowchart LR end subgraph Subscribe["Subscription Flow"] - S1[subscribeToDataSources] + S1[subscribeAssets] S2[assignChainsToDataSources] S3[Call DataSource:subscribe] S4[DataSource calls AssetsController:assetsUpdate] diff --git a/packages/assets-controller/src/data-sources/AbstractDataSource.ts b/packages/assets-controller/src/data-sources/AbstractDataSource.ts index e37f6c0327e..d4b63e32f40 100644 --- a/packages/assets-controller/src/data-sources/AbstractDataSource.ts +++ b/packages/assets-controller/src/data-sources/AbstractDataSource.ts @@ -96,6 +96,15 @@ export abstract class AbstractDataSource< return this.state.activeChains; } + /** + * Get currently active chains synchronously (no state duplication in controller). + * + * @returns Array of currently active chain IDs. + */ + getActiveChainsSync(): ChainId[] { + return this.state.activeChains; + } + /** * Subscribe to updates for the given request. */ diff --git a/packages/assets-controller/src/data-sources/AccountsApiDataSource.test.ts b/packages/assets-controller/src/data-sources/AccountsApiDataSource.test.ts index f50e23b2ead..586c4c5082a 100644 --- a/packages/assets-controller/src/data-sources/AccountsApiDataSource.test.ts +++ b/packages/assets-controller/src/data-sources/AccountsApiDataSource.test.ts @@ -153,8 +153,8 @@ async function setupController( const controller = new AccountsApiDataSource({ queryApiClient: apiClient as unknown as AccountsApiDataSourceOptions['queryApiClient'], - onActiveChainsUpdated: (chains): void => - activeChainsUpdateHandler('AccountsApiDataSource', chains), + onActiveChainsUpdated: (dataSourceName, chains, previousChains): void => + activeChainsUpdateHandler(dataSourceName, chains, previousChains), }); // Wait for async initialization @@ -215,6 +215,7 @@ describe('AccountsApiDataSource', () => { expect(activeChainsUpdateHandler).toHaveBeenCalledWith( 'AccountsApiDataSource', [CHAIN_MAINNET, CHAIN_POLYGON, CHAIN_ARBITRUM], + [], ); controller.destroy(); @@ -244,6 +245,7 @@ describe('AccountsApiDataSource', () => { expect(activeChainsUpdateHandler).toHaveBeenCalledWith( 'AccountsApiDataSource', [expected], + [], ); controller.destroy(); diff --git a/packages/assets-controller/src/data-sources/AccountsApiDataSource.ts b/packages/assets-controller/src/data-sources/AccountsApiDataSource.ts index bcf7ebfb3cd..d307223c80d 100644 --- a/packages/assets-controller/src/data-sources/AccountsApiDataSource.ts +++ b/packages/assets-controller/src/data-sources/AccountsApiDataSource.ts @@ -51,8 +51,12 @@ const defaultState: AccountsApiDataSourceState = { export type AccountsApiDataSourceOptions = { /** ApiPlatformClient for API calls with caching */ queryApiClient: ApiPlatformClient; - /** Called when active chains are updated (e.g. to sync BackendWebsocketDataSource). */ - onActiveChainsUpdated: (chains: ChainId[]) => void; + /** Called when active chains are updated. Pass dataSourceName so the controller knows the source. */ + onActiveChainsUpdated: ( + dataSourceName: string, + chains: ChainId[], + previousChains: ChainId[], + ) => void; pollInterval?: number; state?: Partial; }; @@ -104,7 +108,11 @@ export class AccountsApiDataSource extends AbstractDataSource< typeof CONTROLLER_NAME, AccountsApiDataSourceState > { - readonly #onActiveChainsUpdated: (chains: ChainId[]) => void; + readonly #onActiveChainsUpdated: ( + dataSourceName: string, + chains: ChainId[], + previousChains: ChainId[], + ) => void; readonly #pollInterval: number; @@ -134,8 +142,9 @@ export class AccountsApiDataSource extends AbstractDataSource< async #initializeActiveChains(): Promise { try { const chains = await this.#fetchActiveChains(); + const previous = [...this.state.activeChains]; this.updateActiveChains(chains, (updatedChains) => - this.#onActiveChainsUpdated(updatedChains), + this.#onActiveChainsUpdated(this.getName(), updatedChains, previous), ); // Periodically refresh active chains (every 20 minutes) @@ -163,8 +172,9 @@ export class AccountsApiDataSource extends AbstractDataSource< ); if (added.length > 0 || removed.length > 0) { + const previous = [...this.state.activeChains]; this.updateActiveChains(chains, (updatedChains) => - this.#onActiveChainsUpdated(updatedChains), + this.#onActiveChainsUpdated(this.getName(), updatedChains, previous), ); } } catch (error) { diff --git a/packages/assets-controller/src/data-sources/BackendWebsocketDataSource.test.ts b/packages/assets-controller/src/data-sources/BackendWebsocketDataSource.test.ts index 906ce1bdc83..186636622e9 100644 --- a/packages/assets-controller/src/data-sources/BackendWebsocketDataSource.test.ts +++ b/packages/assets-controller/src/data-sources/BackendWebsocketDataSource.test.ts @@ -176,8 +176,8 @@ function setupController( const controller = new BackendWebsocketDataSource({ messenger: controllerMessenger as unknown as AssetsControllerMessenger, queryApiClient: queryApiClient as unknown as ApiPlatformClient, - onActiveChainsUpdated: (chains): void => - activeChainsUpdateHandler('BackendWebsocketDataSource', chains), + onActiveChainsUpdated: (dataSourceName, chains, previousChains): void => + activeChainsUpdateHandler(dataSourceName, chains, previousChains), state: { activeChains: initialActiveChains }, }); @@ -195,7 +195,11 @@ function setupController( const triggerActiveChainsUpdate = (chains: ChainId[]): void => { controller.setActiveChainsFromAccountsApi(chains); - activeChainsUpdateHandler('BackendWebsocketDataSource', chains); + activeChainsUpdateHandler( + 'BackendWebsocketDataSource', + chains, + initialActiveChains, + ); }; return { @@ -242,6 +246,7 @@ describe('BackendWebsocketDataSource', () => { expect(activeChainsUpdateHandler).toHaveBeenCalledWith( 'BackendWebsocketDataSource', [CHAIN_MAINNET, CHAIN_POLYGON], + [], ); controller.destroy(); @@ -257,6 +262,7 @@ describe('BackendWebsocketDataSource', () => { expect(activeChainsUpdateHandler).toHaveBeenCalledWith( 'BackendWebsocketDataSource', [CHAIN_MAINNET, CHAIN_BASE], + [], ); controller.destroy(); diff --git a/packages/assets-controller/src/data-sources/BackendWebsocketDataSource.ts b/packages/assets-controller/src/data-sources/BackendWebsocketDataSource.ts index d9d6417a654..47bc2e25c99 100644 --- a/packages/assets-controller/src/data-sources/BackendWebsocketDataSource.ts +++ b/packages/assets-controller/src/data-sources/BackendWebsocketDataSource.ts @@ -64,8 +64,12 @@ export type BackendWebsocketDataSourceOptions = { messenger: AssetsControllerMessenger; /** ApiPlatformClient for fetching supported networks at init (same as AccountsApiDataSource). */ queryApiClient: ApiPlatformClient; - /** Called when active chains are updated (e.g. to notify AssetsController). */ - onActiveChainsUpdated: (chains: ChainId[]) => void; + /** Called when active chains are updated. Pass dataSourceName so the controller knows the source. */ + onActiveChainsUpdated: ( + dataSourceName: string, + chains: ChainId[], + previousChains: ChainId[], + ) => void; state?: Partial; }; @@ -207,7 +211,11 @@ export class BackendWebsocketDataSource extends AbstractDataSource< readonly #apiClient: ApiPlatformClient; - readonly #onActiveChainsUpdated: (chains: ChainId[]) => void; + readonly #onActiveChainsUpdated: ( + dataSourceName: string, + chains: ChainId[], + previousChains: ChainId[], + ) => void; /** Chains refresh timer */ #chainsRefreshTimer: ReturnType | null = null; @@ -242,8 +250,9 @@ export class BackendWebsocketDataSource extends AbstractDataSource< async #initializeActiveChains(): Promise { try { const chains = await this.#fetchActiveChains(); + const previous = [...this.state.activeChains]; this.updateActiveChains(chains, (updatedChains) => - this.#onActiveChainsUpdated(updatedChains), + this.#onActiveChainsUpdated(this.getName(), updatedChains, previous), ); this.#chainsRefreshTimer = setInterval(() => { @@ -266,8 +275,9 @@ export class BackendWebsocketDataSource extends AbstractDataSource< ); if (added.length > 0 || removed.length > 0) { + const previous = [...this.state.activeChains]; this.updateActiveChains(chains, (updatedChains) => - this.#onActiveChainsUpdated(updatedChains), + this.#onActiveChainsUpdated(this.getName(), updatedChains, previous), ); } } catch (error) { @@ -379,8 +389,9 @@ export class BackendWebsocketDataSource extends AbstractDataSource< * @param chains - Array of supported chain IDs. */ updateSupportedChains(chains: ChainId[]): void { + const previous = [...this.state.activeChains]; this.updateActiveChains(chains, (updatedChains) => - this.#onActiveChainsUpdated(updatedChains), + this.#onActiveChainsUpdated(this.getName(), updatedChains, previous), ); } diff --git a/packages/assets-controller/src/data-sources/RpcDataSource.test.ts b/packages/assets-controller/src/data-sources/RpcDataSource.test.ts index 467825cd0ce..f40a21b7cf6 100644 --- a/packages/assets-controller/src/data-sources/RpcDataSource.test.ts +++ b/packages/assets-controller/src/data-sources/RpcDataSource.test.ts @@ -120,7 +120,11 @@ type WithControllerCallback = ({ }: { controller: RpcDataSource; messenger: RootMessenger; - onActiveChainsUpdated: (chains: ChainId[]) => void; + onActiveChainsUpdated: ( + dataSourceName: string, + chains: ChainId[], + previousChains: ChainId[], + ) => void; }) => Promise | ReturnValue; async function withController( @@ -211,8 +215,15 @@ async function withController( ); const onActiveChainsUpdated = - (options as { onActiveChainsUpdated?: (chains: ChainId[]) => void }) - .onActiveChainsUpdated ?? jest.fn(); + ( + options as { + onActiveChainsUpdated?: ( + dataSourceName: string, + chains: ChainId[], + previousChains: ChainId[], + ) => void; + } + ).onActiveChainsUpdated ?? jest.fn(); const controller = new RpcDataSource({ messenger: rpcDataSourceMessenger as unknown as AssetsControllerMessenger, onActiveChainsUpdated, @@ -283,11 +294,64 @@ describe('RpcDataSource', () => { it('reports active chains on initialization', async () => { await withController(async ({ onActiveChainsUpdated }) => { - expect(onActiveChainsUpdated).toHaveBeenCalledWith([ - MOCK_CHAIN_ID_CAIP, - ]); + expect(onActiveChainsUpdated).toHaveBeenCalledWith( + 'RpcDataSource', + [MOCK_CHAIN_ID_CAIP], + [], + ); }); }); + + it('updates state.activeChains before calling onActiveChainsUpdated so getActiveChainsSync returns new chains', async () => { + let source: RpcDataSource | null = null; + let callbackResult: { + syncChains: ChainId[]; + newChains: ChainId[]; + } | null = null; + await withController( + { + // Start with unavailable so activeChains is empty; publishing Available triggers a real state change. + networkState: createMockNetworkState(NetworkStatus.Degraded), + options: { + onActiveChainsUpdated: ( + _name: string, + newChains: ChainId[], + _previousChains: ChainId[], + ) => { + // Simulate AssetsController: when handling the callback it calls + // source.getActiveChainsSync() to get available chains for subscriptions. + if (source !== null) { + callbackResult = { + syncChains: source.getActiveChainsSync(), // eslint-disable-line n/no-sync -- testing sync API used by AssetsController + newChains, + }; + } + }, + }, + }, + async ({ controller, messenger }) => { + source = controller; + // Trigger callback via network state change (first call is during construction, before source is set). + const newNetworkState = createMockNetworkState( + NetworkStatus.Available, + ); + (messenger.publish as CallableFunction)( + 'NetworkController:stateChange', + newNetworkState, + [], + ); + await new Promise(process.nextTick); + expect(callbackResult).not.toBeNull(); + const result = callbackResult as { + syncChains: ChainId[]; + newChains: ChainId[]; + }; + expect(result.syncChains).toStrictEqual(result.newChains); + const chains = await controller.getActiveChains(); + expect(chains).toContain(MOCK_CHAIN_ID_CAIP); + }, + ); + }); }); describe('getName', () => { diff --git a/packages/assets-controller/src/data-sources/RpcDataSource.ts b/packages/assets-controller/src/data-sources/RpcDataSource.ts index f729a4b6398..a509b33eb44 100644 --- a/packages/assets-controller/src/data-sources/RpcDataSource.ts +++ b/packages/assets-controller/src/data-sources/RpcDataSource.ts @@ -151,8 +151,12 @@ export type RpcDataSourceConfig = { export type RpcDataSourceOptions = { /** The AssetsController messenger (shared by all data sources). */ messenger: AssetsControllerMessenger; - /** Called when active chains are updated (e.g. to notify AssetsController). */ - onActiveChainsUpdated: (chains: ChainId[]) => void; + /** Called when active chains are updated. Pass dataSourceName so the controller knows the source. */ + onActiveChainsUpdated: ( + dataSourceName: string, + chains: ChainId[], + previousChains: ChainId[], + ) => void; /** Request timeout in ms */ timeout?: number; /** Balance polling interval in ms (default: 30s) */ @@ -221,7 +225,11 @@ export class RpcDataSource extends AbstractDataSource< > { readonly #messenger: AssetsControllerMessenger; - readonly #onActiveChainsUpdated: (chains: ChainId[]) => void; + readonly #onActiveChainsUpdated: ( + dataSourceName: string, + chains: ChainId[], + previousChains: ChainId[], + ) => void; readonly #timeout: number; @@ -582,19 +590,22 @@ export class RpcDataSource extends AbstractDataSource< }); // Check if chains changed - const previousChains = new Set(this.#activeChains); + const previousChains = [...this.#activeChains]; + const previousSet = new Set(previousChains); const hasChanges = - previousChains.size !== activeChains.length || - activeChains.some((chain) => !previousChains.has(chain)); + previousChains.length !== activeChains.length || + activeChains.some((chain) => !previousSet.has(chain)); - // Update internal state + // Update internal state and data source state before notifying, so that + // when the controller handles the callback and calls getActiveChainsSync(), + // it receives the updated chains (same order as AbstractDataSource.updateActiveChains). this.#chainStatuses = chainStatuses; this.#activeChains = activeChains; + this.state.activeChains = activeChains; if (hasChanges) { - this.#onActiveChainsUpdated(activeChains); + this.#onActiveChainsUpdated(this.getName(), activeChains, previousChains); } - this.state.activeChains = activeChains; } #getProvider(chainId: ChainId): Web3Provider | undefined { diff --git a/packages/assets-controller/src/data-sources/SnapDataSource.ts b/packages/assets-controller/src/data-sources/SnapDataSource.ts index 42941f31065..04dcb4b57db 100644 --- a/packages/assets-controller/src/data-sources/SnapDataSource.ts +++ b/packages/assets-controller/src/data-sources/SnapDataSource.ts @@ -156,8 +156,12 @@ export type SnapDataSourceAllowedActions = export type SnapDataSourceOptions = { /** The AssetsController messenger (shared by all data sources). */ messenger: AssetsControllerMessenger; - /** Called when this data source's active chains change. */ - onActiveChainsUpdated: (chains: ChainId[]) => void; + /** Called when this data source's active chains change. Pass dataSourceName so the controller knows the source. */ + onActiveChainsUpdated: ( + dataSourceName: string, + chains: ChainId[], + previousChains: ChainId[], + ) => void; /** Configured networks to support (defaults to all snap networks) */ configuredNetworks?: ChainId[]; /** Default polling interval in ms for subscriptions */ @@ -194,7 +198,11 @@ export class SnapDataSource extends AbstractDataSource< > { readonly #messenger: AssetsControllerMessenger; - readonly #onActiveChainsUpdated: (chains: ChainId[]) => void; + readonly #onActiveChainsUpdated: ( + dataSourceName: string, + chains: ChainId[], + previousChains: ChainId[], + ) => void; /** Bound handler for snap keyring balance updates, stored for cleanup */ readonly #handleSnapBalancesUpdatedBound: ( @@ -388,8 +396,9 @@ export class SnapDataSource extends AbstractDataSource< // Notify if chains changed try { + const previous = [...this.state.activeChains]; this.updateActiveChains(supportedChains, (updatedChains) => { - this.#onActiveChainsUpdated(updatedChains); + this.#onActiveChainsUpdated(this.getName(), updatedChains, previous); }); } catch { // AssetsController not ready yet - expected during initialization @@ -398,8 +407,9 @@ export class SnapDataSource extends AbstractDataSource< log('Keyring snap discovery failed', { error }); this.state.chainToSnap = {}; try { + const previous = [...this.state.activeChains]; this.updateActiveChains([], (updatedChains) => { - this.#onActiveChainsUpdated(updatedChains); + this.#onActiveChainsUpdated(this.getName(), updatedChains, previous); }); } catch { // AssetsController not ready yet - expected during initialization diff --git a/packages/assets-controller/src/index.ts b/packages/assets-controller/src/index.ts index 307a796cccd..ef6efbf2a41 100644 --- a/packages/assets-controller/src/index.ts +++ b/packages/assets-controller/src/index.ts @@ -72,9 +72,6 @@ export type { FetchContext, FetchNextFunction, FetchMiddleware, - // Data source registration - DataSourceDefinition, - RegisteredDataSource, SubscriptionResponse, // Combined asset type Asset, diff --git a/packages/assets-controller/src/types.ts b/packages/assets-controller/src/types.ts index 471986f18c0..11b3f204c93 100644 --- a/packages/assets-controller/src/types.ts +++ b/packages/assets-controller/src/types.ts @@ -517,23 +517,7 @@ export type FetchNextFunction = NextFunction; export type FetchMiddleware = Middleware; /** - * Data source ID. - * - * Data sources follow a standard messenger pattern: - * - `${id}:getActiveChains` - action to get active chains - * - `${id}:activeChainsUpdated` - event when chains change - * - * Registration order determines subscription order. - */ -export type DataSourceDefinition = string; - -/** - * Registered data source - */ -export type RegisteredDataSource = DataSourceDefinition; - -/** - * Subscription response + * Subscription response returned when subscribing to asset updates. */ export type SubscriptionResponse = { /** Chains actively subscribed */