diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index 4536f6db481..509ecd5d4ad 100644 --- a/packages/assets-controllers/package.json +++ b/packages/assets-controllers/package.json @@ -53,12 +53,15 @@ "@metamask/contract-metadata": "^2.4.0", "@metamask/controller-utils": "^11.0.0", "@metamask/eth-query": "^4.0.0", + "@metamask/keyring-api": "^6.4.0", "@metamask/keyring-controller": "^17.0.0", "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/network-controller": "^19.0.0", "@metamask/polling-controller": "^8.0.0", "@metamask/preferences-controller": "^13.0.0", "@metamask/rpc-errors": "^6.2.1", + "@metamask/snaps-sdk": "^4.2.0", + "@metamask/snaps-utils": "^7.4.0", "@metamask/utils": "^8.3.0", "@types/bn.js": "^5.1.5", "@types/uuid": "^8.3.0", @@ -73,11 +76,11 @@ "devDependencies": { "@metamask/auto-changelog": "^3.4.4", "@metamask/ethjs-provider-http": "^0.3.0", - "@metamask/keyring-api": "^6.4.0", "@types/jest": "^27.4.1", "@types/lodash": "^4.14.191", "@types/node": "^16.18.54", "deepmerge": "^4.2.2", + "immer": "^9.0.6", "jest": "^27.5.1", "jest-environment-jsdom": "^27.5.1", "nock": "^13.3.1", @@ -92,7 +95,8 @@ "@metamask/approval-controller": "^7.0.0", "@metamask/keyring-controller": "^17.0.0", "@metamask/network-controller": "^19.0.0", - "@metamask/preferences-controller": "^13.0.0" + "@metamask/preferences-controller": "^13.0.0", + "@metamask/snaps-controllers": "^8.1.1" }, "engines": { "node": "^18.18 || >=20" diff --git a/packages/assets-controllers/src/AccountTrackerController.test.ts b/packages/assets-controllers/src/AccountTrackerController.test.ts index 7f32c4ad550..3a952053a57 100644 --- a/packages/assets-controllers/src/AccountTrackerController.test.ts +++ b/packages/assets-controllers/src/AccountTrackerController.test.ts @@ -1,10 +1,7 @@ +import { createMockInternalAccount } from '@metamask/accounts-controller'; import { query } from '@metamask/controller-utils'; import HttpProvider from '@metamask/ethjs-provider-http'; -import { - getDefaultPreferencesState, - type Identity, - type PreferencesState, -} from '@metamask/preferences-controller'; +import type { InternalAccount } from '@metamask/keyring-api'; import * as sinon from 'sinon'; import { advanceTime } from '../../../tests/helpers'; @@ -18,7 +15,9 @@ jest.mock('@metamask/controller-utils', () => { }); const ADDRESS_1 = '0xc38bf1ad06ef69f0c04e29dbeb4152b4175f0a8d'; +const ACCOUNT_1 = createMockInternalAccount({ address: ADDRESS_1 }); const ADDRESS_2 = '0x742d35Cc6634C0532925a3b844Bc454e4438f44e'; +const ACCOUNT_2 = createMockInternalAccount({ address: ADDRESS_2 }); const mockedQuery = query as jest.Mock< ReturnType, @@ -44,9 +43,9 @@ describe('AccountTrackerController', () => { it('should set default state', () => { const controller = new AccountTrackerController({ - onPreferencesStateChange: sinon.stub(), - getIdentities: () => ({}), - getSelectedAddress: () => '', + onSelectedAccountChange: sinon.stub(), + getInternalAccounts: () => [], + getSelectedAccount: () => ACCOUNT_1, getMultiAccountBalancesEnabled: () => true, getCurrentChainId: () => '0x1', getNetworkClientById: jest.fn(), @@ -61,9 +60,11 @@ describe('AccountTrackerController', () => { it('should throw when provider property is accessed', () => { const controller = new AccountTrackerController({ - onPreferencesStateChange: sinon.stub(), - getIdentities: () => ({}), - getSelectedAddress: () => '', + onSelectedAccountChange: sinon.stub(), + getInternalAccounts: () => [], + getSelectedAccount: () => { + return {} as InternalAccount; + }, getMultiAccountBalancesEnabled: () => true, getCurrentChainId: () => '0x1', getNetworkClientById: jest.fn(), @@ -73,31 +74,31 @@ describe('AccountTrackerController', () => { ); }); - it('should refresh when preferences state changes', async () => { - const preferencesStateChangeListeners: (( - state: PreferencesState, + it('should refresh when selectedAccount changes', async () => { + const selectedAccountChangeListeners: (( + internalAccount: InternalAccount, ) => void)[] = []; const controller = new AccountTrackerController( { - onPreferencesStateChange: (listener) => { - preferencesStateChangeListeners.push(listener); + onSelectedAccountChange: (listener) => { + selectedAccountChangeListeners.push(listener); }, - getIdentities: () => ({}), - getSelectedAddress: () => '0x0', + getInternalAccounts: () => [], + getSelectedAccount: () => ACCOUNT_1, getMultiAccountBalancesEnabled: () => true, getCurrentChainId: () => '0x1', getNetworkClientById: jest.fn(), }, { provider }, ); - const triggerPreferencesStateChange = (state: PreferencesState) => { - for (const listener of preferencesStateChangeListeners) { - listener(state); + const triggerSelectedAccountChange = (internalAccount: InternalAccount) => { + for (const listener of selectedAccountChangeListeners) { + listener(internalAccount); } }; controller.refresh = sinon.stub(); - triggerPreferencesStateChange(getDefaultPreferencesState()); + triggerSelectedAccountChange(ACCOUNT_1); // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -113,16 +114,13 @@ describe('AccountTrackerController', () => { describe('without networkClientId', () => { it('should sync addresses', async () => { + const bazAccount = createMockInternalAccount({ address: 'baz' }); + const barAccount = createMockInternalAccount({ address: 'bar' }); const controller = new AccountTrackerController( { - onPreferencesStateChange: sinon.stub(), - getIdentities: () => { - return { - bar: {} as Identity, - baz: {} as Identity, - }; - }, - getSelectedAddress: () => '0x0', + onSelectedAccountChange: sinon.stub(), + getInternalAccounts: () => [bazAccount, barAccount], + getSelectedAccount: () => barAccount, getMultiAccountBalancesEnabled: () => true, getCurrentChainId: () => '0x1', getNetworkClientById: jest.fn(), @@ -169,11 +167,9 @@ describe('AccountTrackerController', () => { const controller = new AccountTrackerController( { - onPreferencesStateChange: sinon.stub(), - getIdentities: () => { - return { [ADDRESS_1]: {} as Identity }; - }, - getSelectedAddress: () => ADDRESS_1, + onSelectedAccountChange: sinon.stub(), + getInternalAccounts: () => [ACCOUNT_1], + getSelectedAccount: () => ACCOUNT_1, getMultiAccountBalancesEnabled: () => true, getCurrentChainId: () => '0x1', getNetworkClientById: jest.fn(), @@ -206,14 +202,9 @@ describe('AccountTrackerController', () => { const controller = new AccountTrackerController( { - onPreferencesStateChange: sinon.stub(), - getIdentities: () => { - return { - [ADDRESS_1]: {} as Identity, - [ADDRESS_2]: {} as Identity, - }; - }, - getSelectedAddress: () => ADDRESS_1, + onSelectedAccountChange: sinon.stub(), + getInternalAccounts: () => [ACCOUNT_1, ACCOUNT_2], + getSelectedAccount: () => ACCOUNT_1, getMultiAccountBalancesEnabled: () => false, getCurrentChainId: () => '0x1', getNetworkClientById: jest.fn(), @@ -244,14 +235,9 @@ describe('AccountTrackerController', () => { const controller = new AccountTrackerController( { - onPreferencesStateChange: sinon.stub(), - getIdentities: () => { - return { - [ADDRESS_1]: {} as Identity, - [ADDRESS_2]: {} as Identity, - }; - }, - getSelectedAddress: () => ADDRESS_1, + onSelectedAccountChange: sinon.stub(), + getInternalAccounts: () => [ACCOUNT_1, ACCOUNT_2], + getSelectedAccount: () => ACCOUNT_1, getMultiAccountBalancesEnabled: () => true, getCurrentChainId: () => '0x1', getNetworkClientById: jest.fn(), @@ -278,16 +264,13 @@ describe('AccountTrackerController', () => { describe('with networkClientId', () => { it('should sync addresses', async () => { + const bazAccount = createMockInternalAccount({ address: 'baz' }); + const barAccount = createMockInternalAccount({ address: 'bar' }); const controller = new AccountTrackerController( { - onPreferencesStateChange: sinon.stub(), - getIdentities: () => { - return { - bar: {} as Identity, - baz: {} as Identity, - }; - }, - getSelectedAddress: () => '0x0', + onSelectedAccountChange: sinon.stub(), + getInternalAccounts: () => [bazAccount, barAccount], + getSelectedAccount: () => bazAccount, getMultiAccountBalancesEnabled: () => true, getCurrentChainId: () => '0x1', getNetworkClientById: jest.fn().mockReturnValue({ @@ -342,11 +325,9 @@ describe('AccountTrackerController', () => { mockedQuery.mockReturnValueOnce(Promise.resolve('0x10')); const controller = new AccountTrackerController({ - onPreferencesStateChange: sinon.stub(), - getIdentities: () => { - return { [ADDRESS_1]: {} as Identity }; - }, - getSelectedAddress: () => ADDRESS_1, + onSelectedAccountChange: sinon.stub(), + getInternalAccounts: () => [ACCOUNT_1], + getSelectedAccount: () => ACCOUNT_1, getMultiAccountBalancesEnabled: () => true, getCurrentChainId: () => '0x1', getNetworkClientById: jest.fn().mockReturnValue({ @@ -386,14 +367,11 @@ describe('AccountTrackerController', () => { .mockReturnValueOnce(Promise.resolve('0x11')); const controller = new AccountTrackerController({ - onPreferencesStateChange: sinon.stub(), - getIdentities: () => { - return { - [ADDRESS_1]: {} as Identity, - [ADDRESS_2]: {} as Identity, - }; + onSelectedAccountChange: sinon.stub(), + getInternalAccounts: () => { + return [ACCOUNT_1, ACCOUNT_2]; }, - getSelectedAddress: () => ADDRESS_1, + getSelectedAccount: () => ACCOUNT_1, getMultiAccountBalancesEnabled: () => false, getCurrentChainId: () => '0x1', getNetworkClientById: jest.fn().mockReturnValue({ @@ -430,14 +408,11 @@ describe('AccountTrackerController', () => { .mockReturnValueOnce(Promise.resolve('0x12')); const controller = new AccountTrackerController({ - onPreferencesStateChange: sinon.stub(), - getIdentities: () => { - return { - [ADDRESS_1]: {} as Identity, - [ADDRESS_2]: {} as Identity, - }; + onSelectedAccountChange: sinon.stub(), + getInternalAccounts: () => { + return [ACCOUNT_1, ACCOUNT_2]; }, - getSelectedAddress: () => ADDRESS_1, + getSelectedAccount: () => ACCOUNT_1, getMultiAccountBalancesEnabled: () => true, getCurrentChainId: () => '0x1', getNetworkClientById: jest.fn().mockReturnValue({ @@ -474,11 +449,9 @@ describe('AccountTrackerController', () => { it('should sync balance with addresses', async () => { const controller = new AccountTrackerController( { - onPreferencesStateChange: sinon.stub(), - getIdentities: () => { - return {}; - }, - getSelectedAddress: () => ADDRESS_1, + onSelectedAccountChange: sinon.stub(), + getInternalAccounts: () => [], + getSelectedAccount: () => ACCOUNT_1, getMultiAccountBalancesEnabled: () => true, getCurrentChainId: () => '0x1', getNetworkClientById: jest.fn(), @@ -501,9 +474,9 @@ describe('AccountTrackerController', () => { const poll = sinon.spy(AccountTrackerController.prototype, 'poll'); const controller = new AccountTrackerController( { - onPreferencesStateChange: jest.fn(), - getIdentities: () => ({}), - getSelectedAddress: () => '', + onSelectedAccountChange: jest.fn(), + getInternalAccounts: () => [], + getSelectedAccount: () => ACCOUNT_1, getMultiAccountBalancesEnabled: () => true, getCurrentChainId: () => '0x1', getNetworkClientById: jest.fn(), @@ -523,9 +496,9 @@ describe('AccountTrackerController', () => { sinon.stub(AccountTrackerController.prototype, 'poll'); const controller = new AccountTrackerController( { - onPreferencesStateChange: jest.fn(), - getIdentities: () => ({}), - getSelectedAddress: () => '', + onSelectedAccountChange: jest.fn(), + getInternalAccounts: () => [], + getSelectedAccount: () => ACCOUNT_1, getMultiAccountBalancesEnabled: () => true, getCurrentChainId: () => '0x1', getNetworkClientById: jest.fn(), diff --git a/packages/assets-controllers/src/AccountTrackerController.ts b/packages/assets-controllers/src/AccountTrackerController.ts index 63f37fc9aae..e19f4a654df 100644 --- a/packages/assets-controllers/src/AccountTrackerController.ts +++ b/packages/assets-controllers/src/AccountTrackerController.ts @@ -1,7 +1,9 @@ +import type { AccountsController } from '@metamask/accounts-controller'; import type { BaseConfig, BaseState } from '@metamask/base-controller'; import { query, safelyExecuteWithTimeout } from '@metamask/controller-utils'; import EthQuery from '@metamask/eth-query'; import type { Provider } from '@metamask/eth-query'; +import { type InternalAccount } from '@metamask/keyring-api'; import type { NetworkClientId, NetworkController, @@ -79,7 +81,11 @@ export class AccountTrackerController extends StaticIntervalPollingControllerV1< }); } - const addresses = Object.keys(this.getIdentities()); + const addresses = Object.values( + this.getInternalAccounts().map( + (internalAccount) => internalAccount.address, + ), + ); const newAddresses = addresses.filter( (address) => !existing.includes(address), ); @@ -114,9 +120,9 @@ export class AccountTrackerController extends StaticIntervalPollingControllerV1< */ override name = 'AccountTrackerController' as const; - private readonly getIdentities: () => PreferencesState['identities']; + private readonly getInternalAccounts: AccountsController['listAccounts']; - private readonly getSelectedAddress: () => PreferencesState['selectedAddress']; + private readonly getSelectedAccount: AccountsController['getSelectedAccount']; private readonly getMultiAccountBalancesEnabled: () => PreferencesState['isMultiAccountBalancesEnabled']; @@ -128,29 +134,29 @@ export class AccountTrackerController extends StaticIntervalPollingControllerV1< * Creates an AccountTracker instance. * * @param options - The controller options. - * @param options.onPreferencesStateChange - Allows subscribing to preference controller state changes. - * @param options.getIdentities - Gets the identities from the Preferences store. - * @param options.getSelectedAddress - Gets the selected address from the Preferences store. * @param options.getMultiAccountBalancesEnabled - Gets the multi account balances enabled flag from the Preferences store. * @param options.getCurrentChainId - Gets the chain ID for the current network from the Network store. * @param options.getNetworkClientById - Gets the network client with the given id from the NetworkController. + * @param options.onSelectedAccountChange - A function that subscribes to selected account changes. + * @param options.getInternalAccounts - A function that returns the internal accounts. + * @param options.getSelectedAccount - A function that returns the selected account. * @param config - Initial options used to configure this controller. * @param state - Initial state to set on this controller. */ constructor( { - onPreferencesStateChange, - getIdentities, - getSelectedAddress, + onSelectedAccountChange, + getInternalAccounts, + getSelectedAccount, getMultiAccountBalancesEnabled, getCurrentChainId, getNetworkClientById, }: { - onPreferencesStateChange: ( - listener: (preferencesState: PreferencesState) => void, + onSelectedAccountChange: ( + listener: (internalAccount: InternalAccount) => void, ) => void; - getIdentities: () => PreferencesState['identities']; - getSelectedAddress: () => PreferencesState['selectedAddress']; + getInternalAccounts: AccountsController['listAccounts']; + getSelectedAccount: AccountsController['getSelectedAccount']; getMultiAccountBalancesEnabled: () => PreferencesState['isMultiAccountBalancesEnabled']; getCurrentChainId: () => NetworkState['providerConfig']['chainId']; getNetworkClientById: NetworkController['getNetworkClientById']; @@ -170,12 +176,12 @@ export class AccountTrackerController extends StaticIntervalPollingControllerV1< }; this.initialize(); this.setIntervalLength(this.config.interval); - this.getIdentities = getIdentities; - this.getSelectedAddress = getSelectedAddress; this.getMultiAccountBalancesEnabled = getMultiAccountBalancesEnabled; this.getCurrentChainId = getCurrentChainId; this.getNetworkClientById = getNetworkClientById; - onPreferencesStateChange(() => { + this.getSelectedAccount = getSelectedAccount; + this.getInternalAccounts = getInternalAccounts; + onSelectedAccountChange(() => { this.refresh(); }); this.poll(); @@ -253,6 +259,7 @@ export class AccountTrackerController extends StaticIntervalPollingControllerV1< * @param networkClientId - Optional networkClientId to fetch a network client with */ refresh = async (networkClientId?: NetworkClientId) => { + const selectedAccount = this.getSelectedAccount(); const releaseLock = await this.refreshMutex.acquire(); try { const { chainId, ethQuery } = @@ -264,7 +271,7 @@ export class AccountTrackerController extends StaticIntervalPollingControllerV1< const accountsToUpdate = isMultiAccountBalancesEnabled ? Object.keys(accounts) - : [this.getSelectedAddress()]; + : [selectedAccount.address]; const accountsForChain = { ...accountsByChainId[chainId] }; for (const address of accountsToUpdate) { diff --git a/yarn.lock b/yarn.lock index 52d067820f5..29a60175721 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1742,6 +1742,8 @@ __metadata: "@metamask/polling-controller": ^8.0.0 "@metamask/preferences-controller": ^13.0.0 "@metamask/rpc-errors": ^6.2.1 + "@metamask/snaps-sdk": ^4.2.0 + "@metamask/snaps-utils": ^7.4.0 "@metamask/utils": ^8.3.0 "@types/bn.js": ^5.1.5 "@types/jest": ^27.4.1 @@ -1752,6 +1754,7 @@ __metadata: bn.js: ^5.2.1 cockatiel: ^3.1.2 deepmerge: ^4.2.2 + immer: ^9.0.6 jest: ^27.5.1 jest-environment-jsdom: ^27.5.1 lodash: ^4.17.21 @@ -1770,6 +1773,7 @@ __metadata: "@metamask/keyring-controller": ^17.0.0 "@metamask/network-controller": ^19.0.0 "@metamask/preferences-controller": ^13.0.0 + "@metamask/snaps-controllers": ^8.1.1 languageName: unknown linkType: soft