From 8c88b088ab9366ffa5aab054b54c2f5961788d7b Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Mon, 20 May 2024 15:52:48 +0800 Subject: [PATCH 01/24] fix: type error --- .../src/AccountsController.ts | 52 +++++++++++++++---- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index 2c976d5a84b..f057685e534 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -306,7 +306,20 @@ export class AccountsController extends BaseController< ...account, metadata: { ...account.metadata, name: accountName }, }; - currentState.internalAccounts.accounts[accountId] = internalAccount; + // deep clone of old state to get around Type instantiation is excessively deep and possibly infinite. + const oldState = JSON.parse(JSON.stringify(currentState)); + + const newState: AccountsControllerState = { + internalAccounts: { + ...oldState.internalAccounts, + accounts: { + ...oldState.internalAccounts.accounts, + [accountId]: internalAccount, + }, + }, + }; + + return newState; }); } @@ -362,8 +375,15 @@ export class AccountsController extends BaseController< }, {} as Record); this.update((currentState: Draft) => { - (currentState as AccountsControllerState).internalAccounts.accounts = - accounts; + const newState: AccountsControllerState = { + ...currentState, + internalAccounts: { + selectedAccount: currentState.internalAccounts.selectedAccount, + accounts, + }, + }; + + return newState; }); } @@ -374,9 +394,11 @@ export class AccountsController extends BaseController< */ loadBackup(backup: AccountsControllerState): void { if (backup.internalAccounts) { - this.update((currentState: Draft) => { - (currentState as AccountsControllerState).internalAccounts = - backup.internalAccounts; + this.update(() => { + const newState: AccountsControllerState = { + internalAccounts: backup.internalAccounts, + }; + return newState; }); } } @@ -764,17 +786,29 @@ export class AccountsController extends BaseController< const accountName = `${accountPrefix} ${indexToUse}`; this.update((currentState: Draft) => { - (currentState as AccountsControllerState).internalAccounts.accounts[ - newAccount.id - ] = { + // deep clone of old state to get around Type instantiation is excessively deep and possibly infinite. + const oldState = JSON.parse(JSON.stringify(currentState)); + + const newState: AccountsControllerState = { + ...oldState, + internalAccounts: { + ...oldState.internalAccounts, + accounts: { + ...oldState.internalAccounts.accounts, + [newAccount.id]: { ...newAccount, metadata: { ...newAccount.metadata, name: accountName, importTime: Date.now(), lastSelected: Date.now(), + }, + }, + }, }, }; + + return newState; }); this.setSelectedAccount(newAccount.id); From 059fe12e510d8e53d2ec41d039ff231df28ebdff Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Mon, 20 May 2024 16:35:34 +0800 Subject: [PATCH 02/24] feat: add selectedEvmAccountChange event --- .../src/AccountsController.test.ts | 180 ++++++++++++------ .../src/AccountsController.ts | 15 +- 2 files changed, 137 insertions(+), 58 deletions(-) diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index e84a87cb215..494e0e6ae3d 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -1,6 +1,15 @@ import { ControllerMessenger } from '@metamask/base-controller'; -import type { InternalAccount } from '@metamask/keyring-api'; -import { EthAccountType, EthMethod } from '@metamask/keyring-api'; +import type { + InternalAccount, + InternalAccountType, +} from '@metamask/keyring-api'; +import { + BtcAccountType, + BtcMethod, + EthAccountType, + EthErc4337Method, + EthMethod, +} from '@metamask/keyring-api'; import { KeyringTypes } from '@metamask/keyring-controller'; import type { SnapControllerState } from '@metamask/snaps-controllers'; import { SnapStatus } from '@metamask/snaps-utils'; @@ -145,6 +154,7 @@ class MockNormalAccountUUID { * @param props.keyringType - The type of the keyring associated with the account. * @param props.snapId - The id of the snap. * @param props.snapEnabled - The status of the snap + * @param props.type - Account Type to create * @returns The `InternalAccount` object created from the normal account properties. */ function createExpectedInternalAccount({ @@ -154,6 +164,7 @@ function createExpectedInternalAccount({ keyringType, snapId, snapEnabled = true, + type = EthAccountType.Eoa, }: { id: string; name: string; @@ -161,22 +172,30 @@ function createExpectedInternalAccount({ keyringType: string; snapId?: string; snapEnabled?: boolean; + type?: InternalAccountType; }): InternalAccount { - const account: InternalAccount = { + const accountTypeToMethods = { + [`${EthAccountType.Eoa}`]: [...Object.values(EthMethod)], + [`${EthAccountType.Erc4337}`]: [...Object.values(EthErc4337Method)], + [`${BtcAccountType.P2wpkh}`]: [...Object.values(BtcMethod)], + }; + + const methods = + accountTypeToMethods[type as unknown as keyof typeof accountTypeToMethods]; + + const account = { id, address, options: {}, - methods: [...EOA_METHODS], - type: EthAccountType.Eoa, + methods, + type, metadata: { name, keyring: { type: keyringType }, importTime: expect.any(Number), - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore lastSelected: undefined, }, - }; + } as InternalAccount; if (snapId) { account.metadata.snap = { @@ -261,7 +280,13 @@ function setupAccountsController({ AccountsControllerActions | AllowedActions, AccountsControllerEvents | AllowedEvents >; -}): AccountsController { +}): { + accountsController: AccountsController; + messenger: ControllerMessenger< + AccountsControllerActions | AllowedActions, + AccountsControllerEvents | AllowedEvents + >; +} { const accountsControllerMessenger = buildAccountsControllerMessenger(messenger); @@ -269,7 +294,7 @@ function setupAccountsController({ messenger: accountsControllerMessenger, state: { ...defaultState, ...initialState }, }); - return accountsController; + return { accountsController, messenger }; } describe('AccountsController', () => { @@ -300,7 +325,7 @@ describe('AccountsController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any as SnapControllerState; - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { @@ -342,7 +367,7 @@ describe('AccountsController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any as SnapControllerState; - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { @@ -384,7 +409,7 @@ describe('AccountsController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any as SnapControllerState; - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { @@ -412,7 +437,7 @@ describe('AccountsController', () => { }); it('should not update state when only keyring is unlocked without any keyrings', async () => { const messenger = buildMessenger(); - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: {}, @@ -445,7 +470,7 @@ describe('AccountsController', () => { }, ], }; - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: {}, @@ -483,7 +508,7 @@ describe('AccountsController', () => { }, ], }; - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { @@ -541,7 +566,7 @@ describe('AccountsController', () => { ], }; - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { @@ -607,7 +632,7 @@ describe('AccountsController', () => { ], }; - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { @@ -655,7 +680,7 @@ describe('AccountsController', () => { }, ], }; - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { @@ -718,7 +743,7 @@ describe('AccountsController', () => { }, ], }; - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { @@ -781,7 +806,7 @@ describe('AccountsController', () => { ], }; - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: {}, @@ -817,7 +842,7 @@ describe('AccountsController', () => { }, ], }; - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { @@ -861,7 +886,7 @@ describe('AccountsController', () => { }, ], }; - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { @@ -924,7 +949,7 @@ describe('AccountsController', () => { }, ], }; - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { @@ -995,7 +1020,7 @@ describe('AccountsController', () => { }, ], }; - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { @@ -1063,7 +1088,7 @@ describe('AccountsController', () => { }, ], }; - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { @@ -1161,7 +1186,7 @@ describe('AccountsController', () => { ]), ); - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: {}, @@ -1207,7 +1232,7 @@ describe('AccountsController', () => { ]), ); - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: {}, @@ -1256,7 +1281,7 @@ describe('AccountsController', () => { mockGetKeyringByType.mockReturnValueOnce([undefined]), ); - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: {}, @@ -1296,7 +1321,7 @@ describe('AccountsController', () => { ]), ); - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { @@ -1347,7 +1372,7 @@ describe('AccountsController', () => { .mockResolvedValueOnce({ type: KeyringTypes.snap }), ); - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: {}, @@ -1402,7 +1427,7 @@ describe('AccountsController', () => { .mockResolvedValueOnce({ type: KeyringTypes.hd }), ); - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: {}, @@ -1464,7 +1489,7 @@ describe('AccountsController', () => { ]), ); - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: {}, @@ -1511,7 +1536,7 @@ describe('AccountsController', () => { ]), ); - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: {}, @@ -1529,7 +1554,7 @@ describe('AccountsController', () => { describe('loadBackup', () => { it('should load a backup', async () => { - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: {}, @@ -1558,7 +1583,7 @@ describe('AccountsController', () => { }); it('should not load backup if the data is undefined', () => { - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { [mockAccount.id]: mockAccount }, @@ -1583,7 +1608,7 @@ describe('AccountsController', () => { describe('getAccount', () => { it('should return an account by ID', () => { - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { [mockAccount.id]: mockAccount }, @@ -1599,7 +1624,7 @@ describe('AccountsController', () => { ); }); it('should return undefined for an unknown account ID', () => { - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { [mockAccount.id]: mockAccount }, @@ -1616,7 +1641,7 @@ describe('AccountsController', () => { describe('listAccounts', () => { it('should return a list of accounts', () => { - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { @@ -1639,7 +1664,7 @@ describe('AccountsController', () => { describe('getAccountExpect', () => { it('should return an account by ID', () => { - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { [mockAccount.id]: mockAccount }, @@ -1656,7 +1681,7 @@ describe('AccountsController', () => { it('should throw an error for an unknown account ID', () => { const accountId = 'unknown id'; - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { [mockAccount.id]: mockAccount }, @@ -1671,7 +1696,7 @@ describe('AccountsController', () => { }); it('should handle the edge case of undefined accountId during onboarding', async () => { - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { [mockAccount.id]: mockAccount }, @@ -1700,7 +1725,7 @@ describe('AccountsController', () => { describe('getSelectedAccount', () => { it('should return the selected account', () => { - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { [mockAccount.id]: mockAccount }, @@ -1718,7 +1743,7 @@ describe('AccountsController', () => { describe('setSelectedAccount', () => { it('should set the selected account', () => { - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { @@ -1736,11 +1761,52 @@ describe('AccountsController', () => { accountsController.state.internalAccounts.selectedAccount, ).toStrictEqual(mockAccount2.id); }); + + it('should not emit setSelectedEvmAccountChange if the account is non evm', () => { + const mockNonEvmAccount = createExpectedInternalAccount({ + id: 'mock-non-evm', + name: 'non-evm', + address: '36jxQb2bkX32PEdnQ3m2iLefDssNeMmXtb', + keyringType: KeyringTypes.snap, + type: BtcAccountType.P2wpkh, + }); + const { accountsController, messenger } = setupAccountsController({ + initialState: { + internalAccounts: { + accounts: { + [mockAccount.id]: mockAccount, + [mockNonEvmAccount.id]: mockNonEvmAccount, + }, + selectedAccount: mockAccount.id, + }, + }, + }); + + const messengerSpy = jest.spyOn(messenger, 'publish'); + + accountsController.setSelectedAccount(mockNonEvmAccount.id); + + expect( + accountsController.state.internalAccounts.selectedAccount, + ).toStrictEqual(mockNonEvmAccount.id); + + expect(messengerSpy.mock.calls).toBe(2); // state change and then selectedAccountChange + + expect(messengerSpy).not.toHaveBeenCalledWith( + 'AccountsController:selectedEvmAccountChange', + mockNonEvmAccount, + ); + + expect(messengerSpy).toHaveBeenCalledWith( + 'AccountsController:selectedAccountChange', + mockNonEvmAccount, + ); + }); }); describe('setAccountName', () => { it('should set the name of an existing account', () => { - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { [mockAccount.id]: mockAccount }, @@ -1756,7 +1822,7 @@ describe('AccountsController', () => { }); it('should throw an error if the account name already exists', () => { - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { @@ -1821,7 +1887,7 @@ describe('AccountsController', () => { .mockReturnValueOnce('mock-id2') // call to add account .mockReturnValueOnce('mock-id3'); // call to add account - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { @@ -1860,7 +1926,7 @@ describe('AccountsController', () => { ]); mockUUID.mockImplementation(mockAccountUUIDs.mock.bind(mockAccountUUIDs)); - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { @@ -1910,7 +1976,7 @@ describe('AccountsController', () => { describe('getAccountByAddress', () => { it('should return an account by address', async () => { - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { [mockAccount.id]: mockAccount }, @@ -1927,7 +1993,7 @@ describe('AccountsController', () => { }); it("should return undefined if there isn't an account with the address", () => { - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { [mockAccount.id]: mockAccount }, @@ -1956,7 +2022,7 @@ describe('AccountsController', () => { describe('setSelectedAccount', () => { it('should set the selected account', async () => { const messenger = buildMessenger(); - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { [mockAccount.id]: mockAccount }, @@ -1976,7 +2042,7 @@ describe('AccountsController', () => { describe('listAccounts', () => { it('should retrieve a list of accounts', async () => { const messenger = buildMessenger(); - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { [mockAccount.id]: mockAccount }, @@ -1994,7 +2060,7 @@ describe('AccountsController', () => { describe('setAccountName', () => { it('should set the account name', async () => { const messenger = buildMessenger(); - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { [mockAccount.id]: mockAccount }, @@ -2032,7 +2098,7 @@ describe('AccountsController', () => { mockGetKeyringForAccount.mockResolvedValueOnce([]), ); - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { [mockAccount.id]: mockAccount }, @@ -2051,7 +2117,7 @@ describe('AccountsController', () => { it('should get account by address', async () => { const messenger = buildMessenger(); - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { [mockAccount.id]: mockAccount }, @@ -2076,7 +2142,7 @@ describe('AccountsController', () => { it('should get account by address', async () => { const messenger = buildMessenger(); - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { [mockAccount.id]: mockAccount }, @@ -2096,7 +2162,7 @@ describe('AccountsController', () => { it('should get account by id', async () => { const messenger = buildMessenger(); - const accountsController = setupAccountsController({ + const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { [mockAccount.id]: mockAccount }, diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index f057685e534..91bfdde7ee1 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -104,11 +104,17 @@ export type AccountsControllerSelectedAccountChangeEvent = { payload: [InternalAccount]; }; +export type AccountsControllerSelectedEvmAccountChangeEvent = { + type: `${typeof controllerName}:selectedEvmAccountChange`; + payload: [InternalAccount]; +}; + export type AllowedEvents = SnapStateChange | KeyringControllerStateChangeEvent; export type AccountsControllerEvents = | AccountsControllerChangeEvent - | AccountsControllerSelectedAccountChangeEvent; + | AccountsControllerSelectedAccountChangeEvent + | AccountsControllerSelectedEvmAccountChangeEvent; export type AccountsControllerMessenger = RestrictedControllerMessenger< typeof controllerName, @@ -275,6 +281,13 @@ export class AccountsController extends BaseController< currentState.internalAccounts.selectedAccount = account.id; }); + if (account.type.startsWith('eip155:')) { + this.messagingSystem.publish( + 'AccountsController:selectedEvmAccountChange', + account, + ); + } + this.messagingSystem.publish( 'AccountsController:selectedAccountChange', account, From ade1ce809c290d2bd907696412c984d890de30f5 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Mon, 20 May 2024 17:31:04 +0800 Subject: [PATCH 03/24] feat: update getSelectedAccount to have optional chainId parameter --- .../src/AccountsController.test.ts | 179 +++++++++++++++++- .../src/AccountsController.ts | 52 ++++- 2 files changed, 218 insertions(+), 13 deletions(-) diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index 494e0e6ae3d..98ecd1588b5 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -155,6 +155,8 @@ class MockNormalAccountUUID { * @param props.snapId - The id of the snap. * @param props.snapEnabled - The status of the snap * @param props.type - Account Type to create + * @param props.lastSelected - The last selected time of the account. + * @param props.importTime - The import time of the account. * @returns The `InternalAccount` object created from the normal account properties. */ function createExpectedInternalAccount({ @@ -165,6 +167,8 @@ function createExpectedInternalAccount({ snapId, snapEnabled = true, type = EthAccountType.Eoa, + importTime, + lastSelected, }: { id: string; name: string; @@ -173,6 +177,8 @@ function createExpectedInternalAccount({ snapId?: string; snapEnabled?: boolean; type?: InternalAccountType; + importTime?: number; + lastSelected?: number; }): InternalAccount { const accountTypeToMethods = { [`${EthAccountType.Eoa}`]: [...Object.values(EthMethod)], @@ -192,8 +198,8 @@ function createExpectedInternalAccount({ metadata: { name, keyring: { type: keyringType }, - importTime: expect.any(Number), - lastSelected: undefined, + importTime: importTime || expect.any(Number), + lastSelected: lastSelected || undefined, }, } as InternalAccount; @@ -728,6 +734,8 @@ describe('AccountsController', () => { name: 'Custom Name', address: mockAccount2.address, keyringType: KeyringTypes.hd, + importTime: 1955565967656, + lastSelected: 1955565967656, }); const mockNewKeyringState = { @@ -1639,6 +1647,171 @@ describe('AccountsController', () => { }); }); + describe('getSelectedAccount', () => { + it('should return the evm account by default', () => { + const { accountsController } = setupAccountsController({ + initialState: { + internalAccounts: { + accounts: { [mockAccount.id]: mockAccount }, + selectedAccount: mockAccount.id, + }, + }, + }); + + const result = accountsController.getSelectedAccount(); + + expect(result).toStrictEqual(setLastSelectedAsAny(mockAccount)); + }); + + it('should return the selected account if chainId is non evm', () => { + const mockNonEvmAccount = createExpectedInternalAccount({ + id: 'mock-non-evm', + name: 'non-evm', + address: '36jxQb2bkX32PEdnQ3m2iLefDssNeMmXtb', + keyringType: KeyringTypes.snap, + type: BtcAccountType.P2wpkh, + }); + + const { accountsController } = setupAccountsController({ + initialState: { + internalAccounts: { + accounts: { + [mockAccount.id]: mockAccount, + [mockNonEvmAccount.id]: mockNonEvmAccount, + }, + selectedAccount: mockNonEvmAccount.id, + }, + }, + }); + + const result = accountsController.getSelectedAccount('bip122:1'); + + expect(result).toStrictEqual(mockNonEvmAccount); + }); + + it('should return the last selected evm account if the selected account is non evm', () => { + const mockOlderEvmAccount = createExpectedInternalAccount({ + id: 'mock-id-1', + name: 'mock account 1', + address: 'mock-address-1', + keyringType: KeyringTypes.hd, + lastSelected: 11111, + }); + const mockNewerEvmAccount = createExpectedInternalAccount({ + id: 'mock-id-2', + name: 'mock account 2', + address: 'mock-address-2', + keyringType: KeyringTypes.hd, + lastSelected: 22222, + }); + const mockNonEvmAccount = createExpectedInternalAccount({ + id: 'mock-non-evm', + name: 'non-evm', + address: '36jxQb2bkX32PEdnQ3m2iLefDssNeMmXtb', + keyringType: KeyringTypes.snap, + type: BtcAccountType.P2wpkh, + }); + + const { accountsController } = setupAccountsController({ + initialState: { + internalAccounts: { + accounts: { + [mockOlderEvmAccount.id]: mockOlderEvmAccount, + [mockNewerEvmAccount.id]: mockNewerEvmAccount, + [mockNonEvmAccount.id]: mockNonEvmAccount, + }, + selectedAccount: mockNonEvmAccount.id, + }, + }, + }); + + const result = accountsController.getSelectedAccount('eip155:1'); + + expect(result).toStrictEqual(setLastSelectedAsAny(mockNewerEvmAccount)); + }); + + it('should return the last selected evm account by default', () => { + const { accountsController } = setupAccountsController({ + initialState: { + internalAccounts: { + accounts: { [mockAccount.id]: mockAccount }, + selectedAccount: mockAccount.id, + }, + }, + }); + + const result = accountsController.getSelectedAccount(); + + expect(result).toStrictEqual(setLastSelectedAsAny(mockAccount)); + }); + + it('should return the last selected evm account by default even if there are undefined lastSelected', () => { + const mockOlderEvmAccount = createExpectedInternalAccount({ + id: 'mock-id-1', + name: 'mock account 1', + address: 'mock-address-1', + keyringType: KeyringTypes.hd, + lastSelected: undefined, + }); + const mockNewerEvmAccount = createExpectedInternalAccount({ + id: 'mock-id-2', + name: 'mock account 2', + address: 'mock-address-2', + keyringType: KeyringTypes.hd, + lastSelected: 22222, + }); + const mockNonEvmAccount = createExpectedInternalAccount({ + id: 'mock-non-evm', + name: 'non-evm', + address: '36jxQb2bkX32PEdnQ3m2iLefDssNeMmXtb', + keyringType: KeyringTypes.snap, + type: BtcAccountType.P2wpkh, + }); + + const { accountsController } = setupAccountsController({ + initialState: { + internalAccounts: { + accounts: { + [mockNewerEvmAccount.id]: mockNewerEvmAccount, + [mockOlderEvmAccount.id]: mockOlderEvmAccount, + [mockNonEvmAccount.id]: mockNonEvmAccount, + }, + selectedAccount: mockNonEvmAccount.id, + }, + }, + }); + + const result = accountsController.getSelectedAccount('eip155:1'); + + expect(result).toStrictEqual(setLastSelectedAsAny(mockNewerEvmAccount)); + }); + + it("should throw error if there aren't any evm accounts", () => { + const mockNonEvmAccount = createExpectedInternalAccount({ + id: 'mock-non-evm', + name: 'non-evm', + address: '36jxQb2bkX32PEdnQ3m2iLefDssNeMmXtb', + keyringType: KeyringTypes.snap, + type: BtcAccountType.P2wpkh, + }); + + const { accountsController } = setupAccountsController({ + initialState: { + internalAccounts: { + accounts: { + [mockNonEvmAccount.id]: mockNonEvmAccount, + }, + selectedAccount: mockNonEvmAccount.id, + }, + }, + }); + + expect(() => accountsController.getSelectedAccount('eip155:1')).toThrow( + 'AccountsController: No evm accounts', + ); + }); + }); + describe('listAccounts', () => { it('should return a list of accounts', () => { const { accountsController } = setupAccountsController({ @@ -1790,7 +1963,7 @@ describe('AccountsController', () => { accountsController.state.internalAccounts.selectedAccount, ).toStrictEqual(mockNonEvmAccount.id); - expect(messengerSpy.mock.calls).toBe(2); // state change and then selectedAccountChange + expect(messengerSpy.mock.calls).toHaveLength(2); // state change and then selectedAccountChange expect(messengerSpy).not.toHaveBeenCalledWith( 'AccountsController:selectedEvmAccountChange', diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index 91bfdde7ee1..eaf401a168c 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -20,7 +20,7 @@ import type { SnapStateChange, } from '@metamask/snaps-controllers'; import type { SnapId } from '@metamask/snaps-sdk'; -import type { Snap } from '@metamask/snaps-utils'; +import type { Caip2ChainId, Snap } from '@metamask/snaps-utils'; import type { Keyring, Json } from '@metamask/utils'; import type { Draft } from 'immer'; @@ -247,12 +247,44 @@ export class AccountsController extends BaseController< } /** - * Returns the selected internal account. + * Returns the selected evm internal account by default unless the namespace is not eip155 * + * @param chainId - Caip2 Id of the account * @returns The selected internal account. */ - getSelectedAccount(): InternalAccount { - return this.getAccountExpect(this.state.internalAccounts.selectedAccount); + getSelectedAccount(chainId: Caip2ChainId = 'eip155:*'): InternalAccount { + // TODO: have CAIP2 addresses within InternalAccount + const selectedAccount = this.getAccountExpect( + this.state.internalAccounts.selectedAccount, + ); + + if ( + !chainId.startsWith('eip155:') || + (chainId.startsWith('eip155:') && + selectedAccount.type.startsWith('eip155:')) + ) { + return selectedAccount; + } + + const accounts = this.listAccounts().filter((account) => + account.type.startsWith('eip155:'), + ); + let lastSelectedEvmAccount = accounts[0]; + for (let i = 1; i < accounts.length; i++) { + if ( + (accounts[i].metadata.lastSelected ?? 0) > + (selectedAccount.metadata.lastSelected ?? 0) + ) { + lastSelectedEvmAccount = accounts[i]; + } + } + + if (!lastSelectedEvmAccount) { + // !Should never reach this. + throw new Error('AccountsController: No evm accounts'); + } + + return lastSelectedEvmAccount; } /** @@ -809,12 +841,12 @@ export class AccountsController extends BaseController< accounts: { ...oldState.internalAccounts.accounts, [newAccount.id]: { - ...newAccount, - metadata: { - ...newAccount.metadata, - name: accountName, - importTime: Date.now(), - lastSelected: Date.now(), + ...newAccount, + metadata: { + ...newAccount.metadata, + name: accountName, + importTime: Date.now(), + lastSelected: Date.now(), }, }, }, From 61068cf1d033fb94c13472b1769e36cbb78d22e1 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Tue, 21 May 2024 11:58:26 +0800 Subject: [PATCH 04/24] feat: add mocks --- .../src/tests/mocks.test.ts | 77 ++++++++++++++++++ .../accounts-controller/src/tests/mocks.ts | 79 +++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 packages/accounts-controller/src/tests/mocks.test.ts create mode 100644 packages/accounts-controller/src/tests/mocks.ts diff --git a/packages/accounts-controller/src/tests/mocks.test.ts b/packages/accounts-controller/src/tests/mocks.test.ts new file mode 100644 index 00000000000..d631ca8aae4 --- /dev/null +++ b/packages/accounts-controller/src/tests/mocks.test.ts @@ -0,0 +1,77 @@ +import { BtcAccountType, EthAccountType } from '@metamask/keyring-api'; + +import { createMockInternalAccount } from './mocks'; + +describe('createMockInternalAccount', () => { + it('should create a mock internal account', () => { + const account = createMockInternalAccount(); + expect(account).toStrictEqual({ + id: expect.any(String), + address: expect.any(String), + type: expect.any(String), + options: expect.any(Object), + methods: expect.any(Array), + metadata: { + name: expect.any(String), + keyring: { type: expect.any(String) }, + importTime: expect.any(Number), + lastSelected: expect.any(Number), + snap: undefined, + }, + }); + }); + + it('should create a mock internal account with custom values', () => { + const customSnap = { + id: '1', + enabled: true, + name: 'Snap 1', + }; + const account = createMockInternalAccount({ + id: '1', + address: '0x123', + type: EthAccountType.Erc4337, + name: 'Custom Account', + snap: customSnap, + }); + expect(account).toStrictEqual({ + id: '1', + address: '0x123', + type: EthAccountType.Erc4337, + options: expect.any(Object), + methods: expect.any(Array), + metadata: { + name: 'Custom Account', + keyring: { type: expect.any(String) }, + importTime: expect.any(Number), + lastSelected: expect.any(Number), + snap: customSnap, + }, + }); + }); + + it('should create a nonevm account', () => { + const account = createMockInternalAccount({ type: BtcAccountType.P2wpkh }); + expect(account).toStrictEqual({ + id: expect.any(String), + address: expect.any(String), + type: BtcAccountType.P2wpkh, + options: expect.any(Object), + methods: expect.any(Array), + metadata: { + name: expect.any(String), + keyring: { type: expect.any(String) }, + importTime: expect.any(Number), + lastSelected: expect.any(Number), + snap: undefined, + }, + }); + }); + + it('will throw if an unknown account type was passed', () => { + // @ts-expect-error testing unknown account type + expect(() => createMockInternalAccount({ type: 'unknown' })).toThrow( + 'Unknown account type: unknown', + ); + }); +}); diff --git a/packages/accounts-controller/src/tests/mocks.ts b/packages/accounts-controller/src/tests/mocks.ts new file mode 100644 index 00000000000..c96fbcfd699 --- /dev/null +++ b/packages/accounts-controller/src/tests/mocks.ts @@ -0,0 +1,79 @@ +import type { + InternalAccount, + InternalAccountType, +} from '@metamask/keyring-api'; +import { + BtcAccountType, + BtcMethod, + EthAccountType, + EthErc4337Method, + EthMethod, +} from '@metamask/keyring-api'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import { v4 } from 'uuid'; + +export const createMockInternalAccount = ({ + id = v4(), + address = '0x2990079bcdee240329a520d2444386fc119da21a', + type = EthAccountType.Eoa, + name = 'Account 1', + keyringType = KeyringTypes.hd, + snap, + importTime = Date.now(), + lastSelected = Date.now(), +}: { + id?: string; + address?: string; + type?: InternalAccountType; + name?: string; + keyringType?: KeyringTypes; + snap?: { + id: string; + enabled: boolean; + name: string; + }; + importTime?: number; + lastSelected?: number; +} = {}): InternalAccount => { + let methods; + + switch (type) { + case EthAccountType.Eoa: + methods = [ + EthMethod.PersonalSign, + EthMethod.Sign, + EthMethod.SignTransaction, + EthMethod.SignTypedDataV1, + EthMethod.SignTypedDataV3, + EthMethod.SignTypedDataV4, + ]; + break; + case EthAccountType.Erc4337: + methods = [ + EthErc4337Method.PatchUserOperation, + EthErc4337Method.PrepareUserOperation, + EthErc4337Method.SignUserOperation, + ]; + break; + case BtcAccountType.P2wpkh: + methods = [BtcMethod.SendMany]; + break; + default: + throw new Error(`Unknown account type: ${type as string}`); + } + + return { + id, + address, + options: {}, + methods, + type, + metadata: { + name, + keyring: { type: keyringType }, + importTime, + lastSelected, + snap: snap && snap, + }, + } as InternalAccount; +}; From 9cfa922ccf0fcec02ca48be3b97f8429ed29ee32 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Tue, 21 May 2024 11:59:01 +0800 Subject: [PATCH 05/24] fix: update btc address in test --- .../src/AccountsController.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index 98ecd1588b5..ebb07b3bd4a 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -1663,11 +1663,11 @@ describe('AccountsController', () => { expect(result).toStrictEqual(setLastSelectedAsAny(mockAccount)); }); - it('should return the selected account if chainId is non evm', () => { + it('should return the selected account if chainId is non-evm', () => { const mockNonEvmAccount = createExpectedInternalAccount({ id: 'mock-non-evm', name: 'non-evm', - address: '36jxQb2bkX32PEdnQ3m2iLefDssNeMmXtb', + address: 'bc1qzqc2aqlw8nwa0a05ehjkk7dgt8308ac7kzw9a6', keyringType: KeyringTypes.snap, type: BtcAccountType.P2wpkh, }); @@ -1689,7 +1689,7 @@ describe('AccountsController', () => { expect(result).toStrictEqual(mockNonEvmAccount); }); - it('should return the last selected evm account if the selected account is non evm', () => { + it('should return the last selected evm account if the selected account is non-evm', () => { const mockOlderEvmAccount = createExpectedInternalAccount({ id: 'mock-id-1', name: 'mock account 1', @@ -1707,7 +1707,7 @@ describe('AccountsController', () => { const mockNonEvmAccount = createExpectedInternalAccount({ id: 'mock-non-evm', name: 'non-evm', - address: '36jxQb2bkX32PEdnQ3m2iLefDssNeMmXtb', + address: 'bc1qzqc2aqlw8nwa0a05ehjkk7dgt8308ac7kzw9a6', keyringType: KeyringTypes.snap, type: BtcAccountType.P2wpkh, }); @@ -1763,7 +1763,7 @@ describe('AccountsController', () => { const mockNonEvmAccount = createExpectedInternalAccount({ id: 'mock-non-evm', name: 'non-evm', - address: '36jxQb2bkX32PEdnQ3m2iLefDssNeMmXtb', + address: 'bc1qzqc2aqlw8nwa0a05ehjkk7dgt8308ac7kzw9a6', keyringType: KeyringTypes.snap, type: BtcAccountType.P2wpkh, }); @@ -1790,7 +1790,7 @@ describe('AccountsController', () => { const mockNonEvmAccount = createExpectedInternalAccount({ id: 'mock-non-evm', name: 'non-evm', - address: '36jxQb2bkX32PEdnQ3m2iLefDssNeMmXtb', + address: 'bc1qzqc2aqlw8nwa0a05ehjkk7dgt8308ac7kzw9a6', keyringType: KeyringTypes.snap, type: BtcAccountType.P2wpkh, }); @@ -1935,11 +1935,11 @@ describe('AccountsController', () => { ).toStrictEqual(mockAccount2.id); }); - it('should not emit setSelectedEvmAccountChange if the account is non evm', () => { + it('should not emit setSelectedEvmAccountChange if the account is non-evm', () => { const mockNonEvmAccount = createExpectedInternalAccount({ id: 'mock-non-evm', name: 'non-evm', - address: '36jxQb2bkX32PEdnQ3m2iLefDssNeMmXtb', + address: 'bc1qzqc2aqlw8nwa0a05ehjkk7dgt8308ac7kzw9a6', keyringType: KeyringTypes.snap, type: BtcAccountType.P2wpkh, }); From 4e8f8a2c206ce117b1be960ced4faaba9310f0ca Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Tue, 21 May 2024 15:49:45 +0800 Subject: [PATCH 06/24] chore: add keyring-api to assets-controller --- packages/transaction-controller/package.json | 1 + yarn.lock | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json index c1d57668ac2..3b7d02ca3a6 100644 --- a/packages/transaction-controller/package.json +++ b/packages/transaction-controller/package.json @@ -52,6 +52,7 @@ "@metamask/controller-utils": "^9.1.0", "@metamask/eth-query": "^4.0.0", "@metamask/gas-fee-controller": "^15.1.2", + "@metamask/keyring-api": "6.1.1", "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/network-controller": "^18.1.1", "@metamask/rpc-errors": "^6.2.1", diff --git a/yarn.lock b/yarn.lock index 897d04266fc..3bbcaa973f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2354,7 +2354,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-api@npm:^6.1.1": +"@metamask/keyring-api@npm:6.1.1, @metamask/keyring-api@npm:^6.1.1": version: 6.1.1 resolution: "@metamask/keyring-api@npm:6.1.1" dependencies: @@ -2995,6 +2995,7 @@ __metadata: "@metamask/eth-query": ^4.0.0 "@metamask/ethjs-provider-http": ^0.3.0 "@metamask/gas-fee-controller": ^15.1.2 + "@metamask/keyring-api": 6.1.1 "@metamask/metamask-eth-abis": ^3.1.1 "@metamask/network-controller": ^18.1.1 "@metamask/rpc-errors": ^6.2.1 From 115fadf133bea0b6ff26a7149b433f92c45d6e3f Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Tue, 21 May 2024 15:50:33 +0800 Subject: [PATCH 07/24] fix: update getSelectedAccount tests --- .../src/AccountsController.test.ts | 118 ++++++------------ .../src/AccountsController.ts | 26 +++- packages/accounts-controller/src/index.ts | 2 + 3 files changed, 57 insertions(+), 89 deletions(-) diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index ebb07b3bd4a..ef5edcf6c62 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -13,6 +13,7 @@ import { import { KeyringTypes } from '@metamask/keyring-controller'; import type { SnapControllerState } from '@metamask/snaps-controllers'; import { SnapStatus } from '@metamask/snaps-utils'; +import type { CaipChainId } from '@metamask/utils'; import * as uuid from 'uuid'; import type { V4Options } from 'uuid'; @@ -24,6 +25,7 @@ import type { AllowedEvents, } from './AccountsController'; import { AccountsController } from './AccountsController'; +import { createMockInternalAccount } from './tests/mocks'; import { getUUIDOptionsFromAddressOfNormalAccount, keyringTypeToName, @@ -1648,22 +1650,6 @@ describe('AccountsController', () => { }); describe('getSelectedAccount', () => { - it('should return the evm account by default', () => { - const { accountsController } = setupAccountsController({ - initialState: { - internalAccounts: { - accounts: { [mockAccount.id]: mockAccount }, - selectedAccount: mockAccount.id, - }, - }, - }); - - const result = accountsController.getSelectedAccount(); - - expect(result).toStrictEqual(setLastSelectedAsAny(mockAccount)); - }); - - it('should return the selected account if chainId is non-evm', () => { const mockNonEvmAccount = createExpectedInternalAccount({ id: 'mock-non-evm', name: 'non-evm', @@ -1672,24 +1658,6 @@ describe('AccountsController', () => { type: BtcAccountType.P2wpkh, }); - const { accountsController } = setupAccountsController({ - initialState: { - internalAccounts: { - accounts: { - [mockAccount.id]: mockAccount, - [mockNonEvmAccount.id]: mockNonEvmAccount, - }, - selectedAccount: mockNonEvmAccount.id, - }, - }, - }); - - const result = accountsController.getSelectedAccount('bip122:1'); - - expect(result).toStrictEqual(mockNonEvmAccount); - }); - - it('should return the last selected evm account if the selected account is non-evm', () => { const mockOlderEvmAccount = createExpectedInternalAccount({ id: 'mock-id-1', name: 'mock account 1', @@ -1704,14 +1672,17 @@ describe('AccountsController', () => { keyringType: KeyringTypes.hd, lastSelected: 22222, }); - const mockNonEvmAccount = createExpectedInternalAccount({ - id: 'mock-non-evm', - name: 'non-evm', - address: 'bc1qzqc2aqlw8nwa0a05ehjkk7dgt8308ac7kzw9a6', - keyringType: KeyringTypes.snap, - type: BtcAccountType.P2wpkh, - }); + it.each([ + [undefined, mockNewerEvmAccount, mockNewerEvmAccount], + [undefined, mockNonEvmAccount, mockNewerEvmAccount], + ['eip155:*', mockNonEvmAccount, mockNewerEvmAccount], + ['eip155:1', mockNonEvmAccount, mockNewerEvmAccount], + ['bip122:1', mockNewerEvmAccount, mockNewerEvmAccount], // nonevm chain ids should always return the selectedAccount + ['bip122:1', mockNonEvmAccount, mockNonEvmAccount], // nonevm chain ids should always return the selectedAccount + ])( + "chainId %s with selectedAccount '%s' should return %s", + (chainId, currentSelectedAccount, expected) => { const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { @@ -1720,71 +1691,52 @@ describe('AccountsController', () => { [mockNewerEvmAccount.id]: mockNewerEvmAccount, [mockNonEvmAccount.id]: mockNonEvmAccount, }, - selectedAccount: mockNonEvmAccount.id, + selectedAccount: currentSelectedAccount.id, }, }, }); - const result = accountsController.getSelectedAccount('eip155:1'); - - expect(result).toStrictEqual(setLastSelectedAsAny(mockNewerEvmAccount)); - }); + expect( + accountsController.getSelectedAccount(chainId as CaipChainId), + ).toStrictEqual(expected); + }, + ); - it('should return the last selected evm account by default', () => { + it("should throw error if there aren't any evm accounts", () => { const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { - accounts: { [mockAccount.id]: mockAccount }, - selectedAccount: mockAccount.id, + accounts: { + [mockNonEvmAccount.id]: mockNonEvmAccount, + }, + selectedAccount: mockNonEvmAccount.id, }, }, }); - const result = accountsController.getSelectedAccount(); - - expect(result).toStrictEqual(setLastSelectedAsAny(mockAccount)); + expect(() => accountsController.getSelectedAccount('eip155:1')).toThrow( + 'AccountsController: No evm accounts', + ); }); - it('should return the last selected evm account by default even if there are undefined lastSelected', () => { - const mockOlderEvmAccount = createExpectedInternalAccount({ - id: 'mock-id-1', - name: 'mock account 1', - address: 'mock-address-1', - keyringType: KeyringTypes.hd, - lastSelected: undefined, - }); - const mockNewerEvmAccount = createExpectedInternalAccount({ - id: 'mock-id-2', - name: 'mock account 2', - address: 'mock-address-2', - keyringType: KeyringTypes.hd, - lastSelected: 22222, - }); - const mockNonEvmAccount = createExpectedInternalAccount({ - id: 'mock-non-evm', - name: 'non-evm', - address: 'bc1qzqc2aqlw8nwa0a05ehjkk7dgt8308ac7kzw9a6', - keyringType: KeyringTypes.snap, - type: BtcAccountType.P2wpkh, - }); - + it('should throw if an invalid caip2 chain id was passed', () => { const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { - accounts: { - [mockNewerEvmAccount.id]: mockNewerEvmAccount, - [mockOlderEvmAccount.id]: mockOlderEvmAccount, - [mockNonEvmAccount.id]: mockNonEvmAccount, - }, - selectedAccount: mockNonEvmAccount.id, + accounts: { [mockAccount.id]: mockAccount }, + selectedAccount: mockAccount.id, }, }, }); - const result = accountsController.getSelectedAccount('eip155:1'); + const invalidCaip2 = 'ethereum'; - expect(result).toStrictEqual(setLastSelectedAsAny(mockNewerEvmAccount)); + // @ts-expect-error testing invalid caip2 + expect(() => accountsController.getSelectedAccount(invalidCaip2)).toThrow( + `Invalid CAIP2 id ${invalidCaip2}`, + ); }); + }); it("should throw error if there aren't any evm accounts", () => { const mockNonEvmAccount = createExpectedInternalAccount({ diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index eaf401a168c..5768abfd464 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -21,7 +21,12 @@ import type { } from '@metamask/snaps-controllers'; import type { SnapId } from '@metamask/snaps-sdk'; import type { Caip2ChainId, Snap } from '@metamask/snaps-utils'; -import type { Keyring, Json } from '@metamask/utils'; +import { + type Keyring, + type Json, + type CaipChainId, + isCaipChainId, +} from '@metamask/utils'; import type { Draft } from 'immer'; import { @@ -253,6 +258,10 @@ export class AccountsController extends BaseController< * @returns The selected internal account. */ getSelectedAccount(chainId: Caip2ChainId = 'eip155:*'): InternalAccount { + if (!isCaipChainId(chainId) && chainId !== 'eip155:*') { + throw new Error(`Invalid CAIP2 id ${String(chainId)}`); + } + // TODO: have CAIP2 addresses within InternalAccount const selectedAccount = this.getAccountExpect( this.state.internalAccounts.selectedAccount, @@ -270,14 +279,19 @@ export class AccountsController extends BaseController< account.type.startsWith('eip155:'), ); let lastSelectedEvmAccount = accounts[0]; - for (let i = 1; i < accounts.length; i++) { + lastSelectedEvmAccount = accounts.reduce((prevAccount, currentAccount) => { if ( - (accounts[i].metadata.lastSelected ?? 0) > - (selectedAccount.metadata.lastSelected ?? 0) + // When the account is added, lastSelected will be set + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + currentAccount.metadata.lastSelected! > + // When the account is added, lastSelected will be set + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + prevAccount.metadata.lastSelected! ) { - lastSelectedEvmAccount = accounts[i]; + return currentAccount; } - } + return prevAccount; + }, accounts[0]); if (!lastSelectedEvmAccount) { // !Should never reach this. diff --git a/packages/accounts-controller/src/index.ts b/packages/accounts-controller/src/index.ts index 274efa5d5b1..29505118b61 100644 --- a/packages/accounts-controller/src/index.ts +++ b/packages/accounts-controller/src/index.ts @@ -11,8 +11,10 @@ export type { AccountsControllerActions, AccountsControllerChangeEvent, AccountsControllerSelectedAccountChangeEvent, + AccountsControllerSelectedEvmAccountChangeEvent, AccountsControllerEvents, AccountsControllerMessenger, } from './AccountsController'; export { AccountsController } from './AccountsController'; export { keyringTypeToName, getUUIDFromAddressOfNormalAccount } from './utils'; +export { createMockInternalAccount } from './tests/mocks'; From 6927777fd9cf02ef8626082e5afb5fe94788c16b Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Tue, 21 May 2024 15:50:49 +0800 Subject: [PATCH 08/24] feat: add optional arg to listAccounts --- .../src/AccountsController.test.ts | 110 +++++++++--------- .../src/AccountsController.ts | 16 ++- 2 files changed, 71 insertions(+), 55 deletions(-) diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index ef5edcf6c62..0e2cec59a01 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -1650,28 +1650,28 @@ describe('AccountsController', () => { }); describe('getSelectedAccount', () => { - const mockNonEvmAccount = createExpectedInternalAccount({ - id: 'mock-non-evm', - name: 'non-evm', - address: 'bc1qzqc2aqlw8nwa0a05ehjkk7dgt8308ac7kzw9a6', - keyringType: KeyringTypes.snap, - type: BtcAccountType.P2wpkh, - }); + const mockNonEvmAccount = createExpectedInternalAccount({ + id: 'mock-non-evm', + name: 'non-evm', + address: 'bc1qzqc2aqlw8nwa0a05ehjkk7dgt8308ac7kzw9a6', + keyringType: KeyringTypes.snap, + type: BtcAccountType.P2wpkh, + }); - const mockOlderEvmAccount = createExpectedInternalAccount({ - id: 'mock-id-1', - name: 'mock account 1', - address: 'mock-address-1', - keyringType: KeyringTypes.hd, - lastSelected: 11111, - }); - const mockNewerEvmAccount = createExpectedInternalAccount({ - id: 'mock-id-2', - name: 'mock account 2', - address: 'mock-address-2', - keyringType: KeyringTypes.hd, - lastSelected: 22222, - }); + const mockOlderEvmAccount = createExpectedInternalAccount({ + id: 'mock-id-1', + name: 'mock account 1', + address: 'mock-address-1', + keyringType: KeyringTypes.hd, + lastSelected: 11111, + }); + const mockNewerEvmAccount = createExpectedInternalAccount({ + id: 'mock-id-2', + name: 'mock account 2', + address: 'mock-address-2', + keyringType: KeyringTypes.hd, + lastSelected: 22222, + }); it.each([ [undefined, mockNewerEvmAccount, mockNewerEvmAccount], @@ -1683,18 +1683,18 @@ describe('AccountsController', () => { ])( "chainId %s with selectedAccount '%s' should return %s", (chainId, currentSelectedAccount, expected) => { - const { accountsController } = setupAccountsController({ - initialState: { - internalAccounts: { - accounts: { - [mockOlderEvmAccount.id]: mockOlderEvmAccount, - [mockNewerEvmAccount.id]: mockNewerEvmAccount, - [mockNonEvmAccount.id]: mockNonEvmAccount, - }, + const { accountsController } = setupAccountsController({ + initialState: { + internalAccounts: { + accounts: { + [mockOlderEvmAccount.id]: mockOlderEvmAccount, + [mockNewerEvmAccount.id]: mockNewerEvmAccount, + [mockNonEvmAccount.id]: mockNonEvmAccount, + }, selectedAccount: currentSelectedAccount.id, + }, }, - }, - }); + }); expect( accountsController.getSelectedAccount(chainId as CaipChainId), @@ -1738,34 +1738,38 @@ describe('AccountsController', () => { }); }); - it("should throw error if there aren't any evm accounts", () => { - const mockNonEvmAccount = createExpectedInternalAccount({ - id: 'mock-non-evm', - name: 'non-evm', - address: 'bc1qzqc2aqlw8nwa0a05ehjkk7dgt8308ac7kzw9a6', - keyringType: KeyringTypes.snap, - type: BtcAccountType.P2wpkh, - }); + describe('listAccounts', () => { + const nonEvmAccount = createMockInternalAccount({ + id: 'mock-id-non-evm', + address: 'mock-non-evm-address', + type: BtcAccountType.P2wpkh, + keyringType: KeyringTypes.snap, + }); + it.each([ + [undefined, [mockAccount, mockAccount2, nonEvmAccount]], + ['eip155:1', [mockAccount, mockAccount2]], + ['eip155:*', [mockAccount, mockAccount2]], + ['bip122:1', [nonEvmAccount]], + ])(`%s should return %s`, (chainId, expected) => { const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { accounts: { - [mockNonEvmAccount.id]: mockNonEvmAccount, + [mockAccount.id]: mockAccount, + [mockAccount2.id]: mockAccount2, + [nonEvmAccount.id]: nonEvmAccount, }, - selectedAccount: mockNonEvmAccount.id, + selectedAccount: mockAccount.id, }, }, }); - - expect(() => accountsController.getSelectedAccount('eip155:1')).toThrow( - 'AccountsController: No evm accounts', - ); + expect( + accountsController.listAccounts(chainId as CaipChainId), + ).toStrictEqual(expected); }); - }); - describe('listAccounts', () => { - it('should return a list of accounts', () => { + it('should throw if invalid caip2 was passed', () => { const { accountsController } = setupAccountsController({ initialState: { internalAccounts: { @@ -1778,12 +1782,12 @@ describe('AccountsController', () => { }, }); - const result = accountsController.listAccounts(); + const invalidCaip2 = 'ethereum'; - expect(result).toStrictEqual([ - setLastSelectedAsAny(mockAccount as InternalAccount), - setLastSelectedAsAny(mockAccount2 as InternalAccount), - ]); + // @ts-expect-error testing invalid caip2 + expect(() => accountsController.listAccounts(invalidCaip2)).toThrow( + `Invalid CAIP2 id ${invalidCaip2}`, + ); }); }); diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index 5768abfd464..65fcbb4d089 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -211,10 +211,22 @@ export class AccountsController extends BaseController< /** * Returns an array of all internal accounts. * + * @param chainId - The chain ID. * @returns An array of InternalAccount objects. */ - listAccounts(): InternalAccount[] { - return Object.values(this.state.internalAccounts.accounts); + listAccounts(chainId?: CaipChainId): InternalAccount[] { + const accounts = Object.values(this.state.internalAccounts.accounts); + if (!chainId) { + return accounts; + } + + if (!isCaipChainId(chainId) && chainId !== 'eip155:*') { + throw new Error(`Invalid CAIP2 id ${String(chainId)}`); + } + + return accounts.filter((account) => + account.type.startsWith(chainId.split(':')[0]), + ); } /** From 6690e98ce161463e39c27a7975a3a359eb6a5f7c Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Tue, 21 May 2024 15:56:56 +0800 Subject: [PATCH 09/24] feat: update AccountTracker to use account id --- .../src/AccountTrackerController.test.ts | 151 +++++++----------- .../src/AccountTrackerController.ts | 41 +++-- 2 files changed, 86 insertions(+), 106 deletions(-) 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 6c8479dc2b7..0aa25d3dce0 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('eip155:*').map( + (internalAccount) => internalAccount.address, + ), + ); const newAddresses = addresses.filter( (address) => !existing.includes(address), ); @@ -114,9 +120,9 @@ export class AccountTrackerController extends StaticIntervalPollingControllerV1< */ override name = 'AccountTrackerController'; - 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) { From 9445c205136eba7845e2596fa4189633bd8c4c34 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Tue, 21 May 2024 15:57:14 +0800 Subject: [PATCH 10/24] refactor: update TokenBalanceController to use account id --- .../src/TokenBalancesController.test.ts | 81 ++++++++++++------- .../src/TokenBalancesController.ts | 17 ++-- 2 files changed, 65 insertions(+), 33 deletions(-) diff --git a/packages/assets-controllers/src/TokenBalancesController.test.ts b/packages/assets-controllers/src/TokenBalancesController.test.ts index 01d023a8cdf..bbf99c6fbd0 100644 --- a/packages/assets-controllers/src/TokenBalancesController.test.ts +++ b/packages/assets-controllers/src/TokenBalancesController.test.ts @@ -1,3 +1,4 @@ +import { createMockInternalAccount } from '@metamask/accounts-controller'; import { ControllerMessenger } from '@metamask/base-controller'; import { toHex } from '@metamask/controller-utils'; import BN from 'bn.js'; @@ -28,7 +29,7 @@ function getMessenger( ): TokenBalancesControllerMessenger { return controllerMessenger.getRestricted({ name: controllerName, - allowedActions: ['PreferencesController:getState'], + allowedActions: ['AccountsController:getSelectedAccount'], allowedEvents: ['TokensController:stateChange'], }); } @@ -49,8 +50,10 @@ describe('TokenBalancesController', () => { it('should set default state', () => { controllerMessenger.registerActionHandler( - 'PreferencesController:getState', - jest.fn().mockReturnValue({ selectedAddress: '0x1234' }), + 'AccountsController:getSelectedAccount', + jest + .fn() + .mockReturnValue(createMockInternalAccount({ address: '0x1234' })), ); const controller = new TokenBalancesController({ getERC20BalanceOf: jest.fn(), @@ -62,8 +65,10 @@ describe('TokenBalancesController', () => { it('should poll and update balances in the right interval', async () => { controllerMessenger.registerActionHandler( - 'PreferencesController:getState', - jest.fn().mockReturnValue({ selectedAddress: '0x1234' }), + 'AccountsController:getSelectedAccount', + jest + .fn() + .mockReturnValue(createMockInternalAccount({ address: '0x1234' })), ); const updateBalancesSpy = jest.spyOn( TokenBalancesController.prototype, @@ -88,8 +93,10 @@ describe('TokenBalancesController', () => { it('should update balances if enabled', async () => { const address = '0x86fa049857e0209aa7d9e616f7eb3b3b78ecfdb0'; controllerMessenger.registerActionHandler( - 'PreferencesController:getState', - jest.fn().mockReturnValue({ selectedAddress: '0x1234' }), + 'AccountsController:getSelectedAccount', + jest + .fn() + .mockReturnValue(createMockInternalAccount({ address: '0x1234' })), ); const controller = new TokenBalancesController({ disabled: false, @@ -109,8 +116,10 @@ describe('TokenBalancesController', () => { it('should not update balances if disabled', async () => { const address = '0x86fa049857e0209aa7d9e616f7eb3b3b78ecfdb0'; controllerMessenger.registerActionHandler( - 'PreferencesController:getState', - jest.fn().mockReturnValue({ selectedAddress: '0x1234' }), + 'AccountsController:getSelectedAccount', + jest + .fn() + .mockReturnValue(createMockInternalAccount({ address: '0x1234' })), ); const controller = new TokenBalancesController({ disabled: true, @@ -128,8 +137,10 @@ describe('TokenBalancesController', () => { it('should update balances if controller is manually enabled', async () => { const address = '0x86fa049857e0209aa7d9e616f7eb3b3b78ecfdb0'; controllerMessenger.registerActionHandler( - 'PreferencesController:getState', - jest.fn().mockReturnValue({ selectedAddress: '0x1234' }), + 'AccountsController:getSelectedAccount', + jest + .fn() + .mockReturnValue(createMockInternalAccount({ address: '0x1234' })), ); const controller = new TokenBalancesController({ disabled: true, @@ -154,8 +165,10 @@ describe('TokenBalancesController', () => { it('should not update balances if controller is manually disabled', async () => { const address = '0x86fa049857e0209aa7d9e616f7eb3b3b78ecfdb0'; controllerMessenger.registerActionHandler( - 'PreferencesController:getState', - jest.fn().mockReturnValue({ selectedAddress: '0x1234' }), + 'AccountsController:getSelectedAccount', + jest + .fn() + .mockReturnValue(createMockInternalAccount({ address: '0x1234' })), ); const controller = new TokenBalancesController({ disabled: false, @@ -182,8 +195,10 @@ describe('TokenBalancesController', () => { it('should update balances if tokens change and controller is manually enabled', async () => { const address = '0x86fa049857e0209aa7d9e616f7eb3b3b78ecfdb0'; controllerMessenger.registerActionHandler( - 'PreferencesController:getState', - jest.fn().mockReturnValue({ selectedAddress: '0x1234' }), + 'AccountsController:getSelectedAccount', + jest + .fn() + .mockReturnValue(createMockInternalAccount({ address: '0x1234' })), ); const controller = new TokenBalancesController({ disabled: true, @@ -220,8 +235,10 @@ describe('TokenBalancesController', () => { it('should not update balances if tokens change and controller is manually disabled', async () => { const address = '0x86fa049857e0209aa7d9e616f7eb3b3b78ecfdb0'; controllerMessenger.registerActionHandler( - 'PreferencesController:getState', - jest.fn().mockReturnValue({ selectedAddress: '0x1234' }), + 'AccountsController:getSelectedAccount', + jest + .fn() + .mockReturnValue(createMockInternalAccount({ address: '0x1234' })), ); const controller = new TokenBalancesController({ disabled: false, @@ -259,8 +276,10 @@ describe('TokenBalancesController', () => { it('should clear previous interval', async () => { controllerMessenger.registerActionHandler( - 'PreferencesController:getState', - jest.fn().mockReturnValue({ selectedAddress: '0x1234' }), + 'AccountsController:getSelectedAccount', + jest + .fn() + .mockReturnValue(createMockInternalAccount({ address: '0x1234' })), ); const controller = new TokenBalancesController({ interval: 1337, @@ -289,8 +308,12 @@ describe('TokenBalancesController', () => { }, ]; controllerMessenger.registerActionHandler( - 'PreferencesController:getState', - jest.fn().mockReturnValue({ selectedAddress }), + 'AccountsController:getSelectedAccount', + jest + .fn() + .mockReturnValue( + createMockInternalAccount({ address: selectedAddress }), + ), ); const controller = new TokenBalancesController({ interval: 1337, @@ -324,8 +347,8 @@ describe('TokenBalancesController', () => { ]; controllerMessenger.registerActionHandler( - 'PreferencesController:getState', - jest.fn().mockReturnValue({}), + 'AccountsController:getSelectedAccount', + jest.fn().mockReturnValue(createMockInternalAccount({ address })), ); const controller = new TokenBalancesController({ interval: 1337, @@ -353,8 +376,10 @@ describe('TokenBalancesController', () => { it('should update balances when tokens change', async () => { controllerMessenger.registerActionHandler( - 'PreferencesController:getState', - jest.fn().mockReturnValue({ selectedAddress: '0x1234' }), + 'AccountsController:getSelectedAccount', + jest + .fn() + .mockReturnValue(createMockInternalAccount({ address: '0x1234' })), ); const controller = new TokenBalancesController({ getERC20BalanceOf: jest.fn(), @@ -382,8 +407,10 @@ describe('TokenBalancesController', () => { it('should update token balances when detected tokens are added', async () => { controllerMessenger.registerActionHandler( - 'PreferencesController:getState', - jest.fn().mockReturnValue({ selectedAddress: '0x1234' }), + 'AccountsController:getSelectedAccount', + jest + .fn() + .mockReturnValue(createMockInternalAccount({ address: '0x1234' })), ); const controller = new TokenBalancesController({ interval: 1337, diff --git a/packages/assets-controllers/src/TokenBalancesController.ts b/packages/assets-controllers/src/TokenBalancesController.ts index 1acc2f226cd..6d0eafe728f 100644 --- a/packages/assets-controllers/src/TokenBalancesController.ts +++ b/packages/assets-controllers/src/TokenBalancesController.ts @@ -1,3 +1,4 @@ +import { type AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller'; import { type RestrictedControllerMessenger, type ControllerGetStateAction, @@ -5,7 +6,6 @@ import { BaseController, } from '@metamask/base-controller'; import { safelyExecute, toHex } from '@metamask/controller-utils'; -import type { PreferencesControllerGetStateAction } from '@metamask/preferences-controller'; import type { AssetsContractController } from './AssetsContractController'; import type { Token } from './TokenRatesController'; @@ -56,7 +56,7 @@ export type TokenBalancesControllerGetStateAction = ControllerGetStateAction< export type TokenBalancesControllerActions = TokenBalancesControllerGetStateAction; -export type AllowedActions = PreferencesControllerGetStateAction; +export type AllowedActions = AccountsControllerGetSelectedAccountAction; export type TokenBalancesControllerStateChangeEvent = ControllerStateChangeEvent< @@ -195,16 +195,21 @@ export class TokenBalancesController extends BaseController< if (this.#disabled) { return; } + const selectedInternalAccount = this.messagingSystem.call( + 'AccountsController:getSelectedAccount', + 'eip155:*', + ); const newContractBalances: ContractBalances = {}; for (const token of this.#tokens) { const { address } = token; - const { selectedAddress } = this.messagingSystem.call( - 'PreferencesController:getState', - ); + try { newContractBalances[address] = toHex( - await this.#getERC20BalanceOf(address, selectedAddress), + await this.#getERC20BalanceOf( + address, + selectedInternalAccount.address, + ), ); token.balanceError = null; } catch (error) { From 32ceafa05da5cfd5dc770bdac54454bd5f1d73ec Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Tue, 21 May 2024 15:58:42 +0800 Subject: [PATCH 11/24] refactor: Update TokenDetectionController to use account id --- .../src/TokenDetectionController.test.ts | 420 ++++++++++++------ .../src/TokenDetectionController.ts | 74 +-- 2 files changed, 337 insertions(+), 157 deletions(-) diff --git a/packages/assets-controllers/src/TokenDetectionController.test.ts b/packages/assets-controllers/src/TokenDetectionController.test.ts index b30e755c876..ed4b5cc3778 100644 --- a/packages/assets-controllers/src/TokenDetectionController.test.ts +++ b/packages/assets-controllers/src/TokenDetectionController.test.ts @@ -1,3 +1,4 @@ +import { createMockInternalAccount } from '@metamask/accounts-controller'; import type { AddApprovalRequest } from '@metamask/approval-controller'; import { ControllerMessenger } from '@metamask/base-controller'; import { @@ -141,6 +142,7 @@ function buildTokenDetectionControllerMessenger( return controllerMessenger.getRestricted({ name: controllerName, allowedActions: [ + 'AccountsController:getAccount', 'AccountsController:getSelectedAccount', 'KeyringController:getState', 'NetworkController:getNetworkClientById', @@ -152,7 +154,7 @@ function buildTokenDetectionControllerMessenger( 'PreferencesController:getState', ], allowedEvents: [ - 'AccountsController:selectedAccountChange', + 'AccountsController:selectedEvmAccountChange', 'KeyringController:lock', 'KeyringController:unlock', 'NetworkController:networkDidChange', @@ -163,6 +165,8 @@ function buildTokenDetectionControllerMessenger( } describe('TokenDetectionController', () => { + const defaultSelectedAccount = createMockInternalAccount(); + beforeEach(async () => { nock(TOKEN_END_POINT_API) .get(getTokensPath(ChainId.mainnet)) @@ -200,6 +204,7 @@ describe('TokenDetectionController', () => { await withController( { isKeyringUnlocked: false, + options: { selectedAccountId: defaultSelectedAccount.id }, }, async ({ controller }) => { const mockTokens = sinon.stub(controller, 'detectTokens'); @@ -218,8 +223,12 @@ describe('TokenDetectionController', () => { await withController( { isKeyringUnlocked: false, + options: { + selectedAccountId: defaultSelectedAccount.id, + }, }, - async ({ controller, triggerKeyringUnlock }) => { + async ({ controller, mockGetAccount, triggerKeyringUnlock }) => { + mockGetAccount(defaultSelectedAccount); const mockTokens = sinon.stub(controller, 'detectTokens'); await controller.start(); @@ -252,16 +261,24 @@ describe('TokenDetectionController', () => { }); it('should poll and detect tokens on interval while on supported networks', async () => { - await withController(async ({ controller }) => { - const mockTokens = sinon.stub(controller, 'detectTokens'); - controller.setIntervalLength(10); + await withController( + { + options: { + selectedAccountId: defaultSelectedAccount.id, + }, + }, + async ({ controller, mockGetAccount }) => { + mockGetAccount(defaultSelectedAccount); + const mockTokens = sinon.stub(controller, 'detectTokens'); + controller.setIntervalLength(10); - await controller.start(); + await controller.start(); - expect(mockTokens.calledOnce).toBe(true); - await advanceTime({ clock, duration: 15 }); - expect(mockTokens.calledTwice).toBe(true); - }); + expect(mockTokens.calledOnce).toBe(true); + await advanceTime({ clock, duration: 15 }); + expect(mockTokens.calledTwice).toBe(true); + }, + ); }); it('should not autodetect while not on supported networks', async () => { @@ -272,9 +289,11 @@ describe('TokenDetectionController', () => { { options: { getBalancesInSingleCall: mockGetBalancesInSingleCall, + selectedAccountId: defaultSelectedAccount.id, }, }, - async ({ controller, mockNetworkState }) => { + async ({ controller, mockGetAccount, mockNetworkState }) => { + mockGetAccount(defaultSelectedAccount); mockNetworkState({ ...defaultNetworkState, selectedNetworkClientId: NetworkType.goerli, @@ -290,15 +309,23 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); await withController( { options: { getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, - async ({ controller, mockTokenListGetState, callActionSpy }) => { + async ({ + controller, + mockGetAccount, + mockTokenListGetState, + callActionSpy, + }) => { + mockGetAccount(selectedAccount); mockTokenListGetState({ ...getDefaultTokenListState(), tokensChainsCache: { @@ -326,7 +353,7 @@ describe('TokenDetectionController', () => { [sampleTokenA], { chainId: ChainId.mainnet, - selectedAddress, + selectedAddress: selectedAccount.address, }, ); }, @@ -337,21 +364,25 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); await withController( { options: { getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, async ({ controller, + mockGetAccount, mockTokenListGetState, mockNetworkState, mockGetNetworkClientById, callActionSpy, }) => { + mockGetAccount(selectedAccount); mockNetworkState({ ...defaultNetworkState, selectedNetworkClientId: 'polygon', @@ -390,7 +421,7 @@ describe('TokenDetectionController', () => { [sampleTokenA], { chainId: '0x89', - selectedAddress, + selectedAddress: selectedAccount.address, }, ); }, @@ -402,17 +433,25 @@ describe('TokenDetectionController', () => { [sampleTokenA.address]: new BN(1), [sampleTokenB.address]: new BN(1), }); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); const interval = 100; await withController( { options: { getBalancesInSingleCall: mockGetBalancesInSingleCall, interval, - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, - async ({ controller, mockTokenListGetState, callActionSpy }) => { + async ({ + controller, + mockGetAccount, + mockTokenListGetState, + callActionSpy, + }) => { + mockGetAccount(selectedAccount); const tokenListState = { ...getDefaultTokenListState(), tokensChainsCache: { @@ -452,7 +491,7 @@ describe('TokenDetectionController', () => { [sampleTokenA, sampleTokenB], { chainId: ChainId.mainnet, - selectedAddress, + selectedAddress: selectedAccount.address, }, ); }, @@ -463,20 +502,24 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); await withController( { options: { getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, async ({ controller, + mockGetAccount, mockTokensGetState, mockTokenListGetState, callActionSpy, }) => { + mockGetAccount(selectedAccount); mockTokensGetState({ ...getDefaultTokensState(), ignoredTokens: [sampleTokenA.address], @@ -518,9 +561,16 @@ describe('TokenDetectionController', () => { { options: { getBalancesInSingleCall: mockGetBalancesInSingleCall, + selectedAccountId: defaultSelectedAccount.id, }, }, - async ({ controller, mockTokenListGetState, callActionSpy }) => { + async ({ + controller, + mockGetAccount, + mockTokenListGetState, + callActionSpy, + }) => { + mockGetAccount(defaultSelectedAccount); mockTokenListGetState({ ...getDefaultTokenListState(), tokensChainsCache: { @@ -566,23 +616,27 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const firstSelectedAddress = - '0x0000000000000000000000000000000000000001'; - const secondSelectedAddress = - '0x0000000000000000000000000000000000000002'; + const firstSelectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); + const secondSelectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000002', + }); await withController( { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress: firstSelectedAddress, + selectedAccountId: firstSelectedAccount.id, }, }, async ({ + mockGetAccount, mockTokenListGetState, triggerSelectedAccountChange, callActionSpy, }) => { + mockGetAccount(firstSelectedAccount); mockTokenListGetState({ ...getDefaultTokenListState(), tokensChainsCache: { @@ -603,9 +657,8 @@ describe('TokenDetectionController', () => { }, }); - triggerSelectedAccountChange({ - address: secondSelectedAddress, - } as InternalAccount); + mockGetAccount(secondSelectedAccount); + triggerSelectedAccountChange(secondSelectedAccount); await advanceTime({ clock, duration: 1 }); expect(callActionSpy).toHaveBeenCalledWith( @@ -613,7 +666,7 @@ describe('TokenDetectionController', () => { [sampleTokenA], { chainId: ChainId.mainnet, - selectedAddress: secondSelectedAddress, + selectedAddress: secondSelectedAccount.address, }, ); }, @@ -624,13 +677,15 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); await withController( { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, async ({ @@ -659,7 +714,7 @@ describe('TokenDetectionController', () => { }); triggerSelectedAccountChange({ - address: selectedAddress, + address: selectedAccount.address, } as InternalAccount); await advanceTime({ clock, duration: 1 }); @@ -675,16 +730,18 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const firstSelectedAddress = - '0x0000000000000000000000000000000000000001'; - const secondSelectedAddress = - '0x0000000000000000000000000000000000000002'; + const firstSelectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); + const secondSelectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000002', + }); await withController( { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress: firstSelectedAddress, + selectedAccountId: firstSelectedAccount.id, }, isKeyringUnlocked: false, }, @@ -714,7 +771,7 @@ describe('TokenDetectionController', () => { }); triggerSelectedAccountChange({ - address: secondSelectedAddress, + address: secondSelectedAccount.address, } as InternalAccount); await advanceTime({ clock, duration: 1 }); @@ -732,16 +789,18 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const firstSelectedAddress = - '0x0000000000000000000000000000000000000001'; - const secondSelectedAddress = - '0x0000000000000000000000000000000000000002'; + const firstSelectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); + const secondSelectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000002', + }); await withController( { options: { disabled: true, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress: firstSelectedAddress, + selectedAccountId: firstSelectedAccount.id, }, }, async ({ @@ -770,7 +829,7 @@ describe('TokenDetectionController', () => { }); triggerSelectedAccountChange({ - address: secondSelectedAddress, + address: secondSelectedAccount.address, } as InternalAccount); await advanceTime({ clock, duration: 1 }); @@ -798,23 +857,28 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const firstSelectedAddress = - '0x0000000000000000000000000000000000000001'; - const secondSelectedAddress = - '0x0000000000000000000000000000000000000002'; + const firstSelectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); + const secondSelectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000002', + }); await withController( { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress: firstSelectedAddress, + selectedAccountId: firstSelectedAccount.id, }, }, async ({ + mockGetAccount, mockTokenListGetState, triggerPreferencesStateChange, + triggerSelectedAccountChange, callActionSpy, }) => { + mockGetAccount(firstSelectedAccount); mockTokenListGetState({ ...getDefaultTokenListState(), tokensChainsCache: { @@ -837,17 +901,18 @@ describe('TokenDetectionController', () => { triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress: secondSelectedAddress, useTokenDetection: true, }); + mockGetAccount(secondSelectedAccount); + triggerSelectedAccountChange(secondSelectedAccount); await advanceTime({ clock, duration: 1 }); - expect(callActionSpy).toHaveBeenCalledWith( + expect(callActionSpy).toHaveBeenLastCalledWith( 'TokensController:addDetectedTokens', [sampleTokenA], { chainId: ChainId.mainnet, - selectedAddress: secondSelectedAddress, + selectedAddress: secondSelectedAccount.address, }, ); }, @@ -858,20 +923,24 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); await withController( { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, async ({ + mockGetAccount, mockTokenListGetState, triggerPreferencesStateChange, callActionSpy, }) => { + mockGetAccount(selectedAccount); mockTokenListGetState({ ...getDefaultTokenListState(), tokensChainsCache: { @@ -894,14 +963,12 @@ describe('TokenDetectionController', () => { triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useTokenDetection: false, }); await advanceTime({ clock, duration: 1 }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useTokenDetection: true, }); await advanceTime({ clock, duration: 1 }); @@ -911,7 +978,7 @@ describe('TokenDetectionController', () => { [sampleTokenA], { chainId: ChainId.mainnet, - selectedAddress, + selectedAddress: selectedAccount.address, }, ); }, @@ -922,23 +989,28 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const firstSelectedAddress = - '0x0000000000000000000000000000000000000001'; - const secondSelectedAddress = - '0x0000000000000000000000000000000000000002'; + const firstSelectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); + const secondSelectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000002', + }); await withController( { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress: firstSelectedAddress, + selectedAccountId: firstSelectedAccount.id, }, }, async ({ + mockGetAccount, mockTokenListGetState, + triggerSelectedAccountChange, triggerPreferencesStateChange, callActionSpy, }) => { + mockGetAccount(firstSelectedAccount); mockTokenListGetState({ ...getDefaultTokenListState(), tokenList: { @@ -956,9 +1028,10 @@ describe('TokenDetectionController', () => { triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress: secondSelectedAddress, useTokenDetection: false, }); + mockGetAccount(secondSelectedAccount); + triggerSelectedAccountChange(secondSelectedAccount); await advanceTime({ clock, duration: 1 }); expect(callActionSpy).not.toHaveBeenCalledWith( @@ -972,20 +1045,24 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); await withController( { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, async ({ + mockGetAccount, mockTokenListGetState, triggerPreferencesStateChange, callActionSpy, }) => { + mockGetAccount(selectedAccount); mockTokenListGetState({ ...getDefaultTokenListState(), tokenList: { @@ -1003,7 +1080,6 @@ describe('TokenDetectionController', () => { triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useTokenDetection: true, }); await advanceTime({ clock, duration: 1 }); @@ -1020,24 +1096,29 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const firstSelectedAddress = - '0x0000000000000000000000000000000000000001'; - const secondSelectedAddress = - '0x0000000000000000000000000000000000000002'; + const firstSelectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); + const secondSelectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000002', + }); await withController( { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress: firstSelectedAddress, + selectedAccountId: firstSelectedAccount.id, }, isKeyringUnlocked: false, }, async ({ + mockGetAccount, mockTokenListGetState, triggerPreferencesStateChange, + triggerSelectedAccountChange, callActionSpy, }) => { + mockGetAccount(firstSelectedAccount); mockTokenListGetState({ ...getDefaultTokenListState(), tokenList: { @@ -1055,9 +1136,10 @@ describe('TokenDetectionController', () => { triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress: secondSelectedAddress, useTokenDetection: true, }); + mockGetAccount(secondSelectedAccount); + triggerSelectedAccountChange(secondSelectedAccount); await advanceTime({ clock, duration: 1 }); expect(callActionSpy).not.toHaveBeenCalledWith( @@ -1071,21 +1153,25 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); await withController( { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress, + selectedAccountId: selectedAccount.id, }, isKeyringUnlocked: false, }, async ({ + mockGetAccount, mockTokenListGetState, triggerPreferencesStateChange, callActionSpy, }) => { + mockGetAccount(selectedAccount); mockTokenListGetState({ ...getDefaultTokenListState(), tokenList: { @@ -1103,14 +1189,12 @@ describe('TokenDetectionController', () => { triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useTokenDetection: false, }); await advanceTime({ clock, duration: 1 }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useTokenDetection: true, }); await advanceTime({ clock, duration: 1 }); @@ -1129,23 +1213,28 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const firstSelectedAddress = - '0x0000000000000000000000000000000000000001'; - const secondSelectedAddress = - '0x0000000000000000000000000000000000000002'; + const firstSelectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); + const secondSelectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000002', + }); await withController( { options: { disabled: true, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress: firstSelectedAddress, + selectedAccountId: firstSelectedAccount.id, }, }, async ({ + mockGetAccount, mockTokenListGetState, triggerPreferencesStateChange, + triggerSelectedAccountChange, callActionSpy, }) => { + mockGetAccount(firstSelectedAccount); mockTokenListGetState({ ...getDefaultTokenListState(), tokenList: { @@ -1163,9 +1252,10 @@ describe('TokenDetectionController', () => { triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress: secondSelectedAddress, useTokenDetection: true, }); + mockGetAccount(secondSelectedAccount); + triggerSelectedAccountChange(secondSelectedAccount); await advanceTime({ clock, duration: 1 }); expect(callActionSpy).not.toHaveBeenCalledWith( @@ -1179,20 +1269,24 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); await withController( { options: { disabled: true, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, async ({ + mockGetAccount, mockTokenListGetState, triggerPreferencesStateChange, callActionSpy, }) => { + mockGetAccount(selectedAccount); mockTokenListGetState({ ...getDefaultTokenListState(), tokenList: { @@ -1210,14 +1304,12 @@ describe('TokenDetectionController', () => { triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useTokenDetection: false, }); await advanceTime({ clock, duration: 1 }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useTokenDetection: true, }); await advanceTime({ clock, duration: 1 }); @@ -1246,20 +1338,24 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); await withController( { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, async ({ + mockGetAccount, mockTokenListGetState, callActionSpy, triggerNetworkDidChange, }) => { + mockGetAccount(selectedAccount); mockTokenListGetState({ ...getDefaultTokenListState(), tokensChainsCache: { @@ -1291,7 +1387,7 @@ describe('TokenDetectionController', () => { [sampleTokenA], { chainId: '0x89', - selectedAddress, + selectedAddress: selectedAccount.address, }, ); }, @@ -1302,20 +1398,24 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); await withController( { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, async ({ + mockGetAccount, mockTokenListGetState, callActionSpy, triggerNetworkDidChange, }) => { + mockGetAccount(selectedAccount); mockTokenListGetState({ ...getDefaultTokenListState(), tokensChainsCache: { @@ -1353,20 +1453,24 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); await withController( { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, async ({ + mockGetAccount, mockTokenListGetState, callActionSpy, triggerNetworkDidChange, }) => { + mockGetAccount(selectedAccount); mockTokenListGetState({ ...getDefaultTokenListState(), tokenList: { @@ -1400,21 +1504,25 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); await withController( { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress, + selectedAccountId: selectedAccount.id, }, isKeyringUnlocked: false, }, async ({ + mockGetAccount, mockTokenListGetState, callActionSpy, triggerNetworkDidChange, }) => { + mockGetAccount(selectedAccount); mockTokenListGetState({ ...getDefaultTokenListState(), tokenList: { @@ -1450,20 +1558,24 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); await withController( { options: { disabled: true, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, async ({ + mockGetAccount, mockTokenListGetState, callActionSpy, triggerNetworkDidChange, }) => { + mockGetAccount(selectedAccount); mockTokenListGetState({ ...getDefaultTokenListState(), tokenList: { @@ -1509,20 +1621,24 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); await withController( { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, async ({ + mockGetAccount, mockTokenListGetState, callActionSpy, triggerTokenListStateChange, }) => { + mockGetAccount(selectedAccount); const tokenList = { [sampleTokenA.address]: { name: sampleTokenA.name, @@ -1554,7 +1670,7 @@ describe('TokenDetectionController', () => { [sampleTokenA], { chainId: ChainId.mainnet, - selectedAddress, + selectedAddress: selectedAccount.address, }, ); }, @@ -1565,20 +1681,24 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); await withController( { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, async ({ + mockGetAccount, mockTokenListGetState, callActionSpy, triggerTokenListStateChange, }) => { + mockGetAccount(selectedAccount); const tokenListState = { ...getDefaultTokenListState(), tokenList: {}, @@ -1600,21 +1720,25 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); await withController( { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress, + selectedAccountId: selectedAccount.id, }, isKeyringUnlocked: false, }, async ({ + mockGetAccount, mockTokenListGetState, callActionSpy, triggerTokenListStateChange, }) => { + mockGetAccount(selectedAccount); const tokenListState = { ...getDefaultTokenListState(), tokenList: { @@ -1648,20 +1772,24 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); await withController( { options: { disabled: true, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, async ({ + mockGetAccount, mockTokenListGetState, callActionSpy, triggerTokenListStateChange, }) => { + mockGetAccount(selectedAccount); const tokenListState = { ...getDefaultTokenListState(), tokenList: { @@ -1704,13 +1832,15 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); await withController( { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, async ({ controller, mockTokenListGetState }) => { @@ -1770,13 +1900,15 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); await withController( { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, async ({ @@ -1784,7 +1916,9 @@ describe('TokenDetectionController', () => { mockNetworkState, triggerPreferencesStateChange, callActionSpy, + mockGetAccount, }) => { + mockGetAccount(selectedAccount); mockNetworkState({ ...defaultNetworkState, selectedNetworkClientId: NetworkType.goerli, @@ -1795,7 +1929,7 @@ describe('TokenDetectionController', () => { }); await controller.detectTokens({ networkClientId: NetworkType.goerli, - selectedAddress, + selectedAddress: selectedAccount.address, }); expect(callActionSpy).not.toHaveBeenCalledWith( 'TokensController:addDetectedTokens', @@ -1814,27 +1948,31 @@ describe('TokenDetectionController', () => { {}, ), ); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); await withController( { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, async ({ controller, + mockGetAccount, triggerPreferencesStateChange, callActionSpy, }) => { + mockGetAccount(selectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), useTokenDetection: false, }); await controller.detectTokens({ networkClientId: NetworkType.mainnet, - selectedAddress, + selectedAddress: selectedAccount.address, }); expect(callActionSpy).toHaveBeenLastCalledWith( 'TokensController:addDetectedTokens', @@ -1847,7 +1985,7 @@ describe('TokenDetectionController', () => { }; }), { - selectedAddress, + selectedAddress: selectedAccount.address, chainId: ChainId.mainnet, }, ); @@ -1859,16 +1997,24 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); await withController( { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, - async ({ controller, mockTokenListGetState, callActionSpy }) => { + async ({ + controller, + mockGetAccount, + mockTokenListGetState, + callActionSpy, + }) => { + mockGetAccount(selectedAccount); mockTokenListGetState({ ...getDefaultTokenListState(), tokensChainsCache: { @@ -1891,7 +2037,7 @@ describe('TokenDetectionController', () => { await controller.detectTokens({ networkClientId: NetworkType.mainnet, - selectedAddress, + selectedAddress: selectedAccount.address, }); expect(callActionSpy).toHaveBeenCalledWith( @@ -1899,7 +2045,7 @@ describe('TokenDetectionController', () => { [sampleTokenA], { chainId: ChainId.mainnet, - selectedAddress, + selectedAddress: selectedAccount.address, }, ); }, @@ -1910,7 +2056,9 @@ describe('TokenDetectionController', () => { const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ [sampleTokenA.address]: new BN(1), }); - const selectedAddress = '0x0000000000000000000000000000000000000001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); const mockTrackMetaMetricsEvent = jest.fn(); await withController( @@ -1919,10 +2067,11 @@ describe('TokenDetectionController', () => { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, trackMetaMetricsEvent: mockTrackMetaMetricsEvent, - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, - async ({ controller, mockTokenListGetState }) => { + async ({ controller, mockGetAccount, mockTokenListGetState }) => { + mockGetAccount(selectedAccount); mockTokenListGetState({ ...getDefaultTokenListState(), tokensChainsCache: { @@ -1945,7 +2094,7 @@ describe('TokenDetectionController', () => { await controller.detectTokens({ networkClientId: NetworkType.mainnet, - selectedAddress, + selectedAddress: selectedAccount.address, }); expect(mockTrackMetaMetricsEvent).toHaveBeenCalledWith({ @@ -1977,6 +2126,7 @@ function getTokensPath(chainId: Hex) { type WithControllerCallback = ({ controller, + mockGetAccount, mockGetSelectedAccount, mockKeyringGetState, mockTokensGetState, @@ -1994,6 +2144,7 @@ type WithControllerCallback = ({ triggerNetworkDidChange, }: { controller: TokenDetectionController; + mockGetAccount: (internalAccount: InternalAccount) => void; mockGetSelectedAccount: (address: string) => void; mockKeyringGetState: (state: KeyringControllerState) => void; mockTokensGetState: (state: TokensState) => void; @@ -2044,6 +2195,12 @@ async function withController( const controllerMessenger = messenger ?? new ControllerMessenger(); + const mockGetAccount = jest.fn(); + controllerMessenger.registerActionHandler( + 'AccountsController:getAccount', + mockGetAccount, + ); + const mockGetSelectedAccount = jest.fn(); controllerMessenger.registerActionHandler( 'AccountsController:getSelectedAccount', @@ -2127,6 +2284,9 @@ async function withController( try { return await fn({ controller, + mockGetAccount: (internalAccount: InternalAccount) => { + mockGetAccount.mockReturnValue(internalAccount); + }, mockGetSelectedAccount: (address: string) => { mockGetSelectedAccount.mockReturnValue({ address } as InternalAccount); }, @@ -2182,7 +2342,7 @@ async function withController( }, triggerSelectedAccountChange: (account: InternalAccount) => { controllerMessenger.publish( - 'AccountsController:selectedAccountChange', + 'AccountsController:selectedEvmAccountChange', account, ); }, diff --git a/packages/assets-controllers/src/TokenDetectionController.ts b/packages/assets-controllers/src/TokenDetectionController.ts index bbebdca4807..756a3fc0dc6 100644 --- a/packages/assets-controllers/src/TokenDetectionController.ts +++ b/packages/assets-controllers/src/TokenDetectionController.ts @@ -1,6 +1,7 @@ import type { AccountsControllerGetSelectedAccountAction, - AccountsControllerSelectedAccountChangeEvent, + AccountsControllerGetAccountAction, + AccountsControllerSelectedEvmAccountChangeEvent, } from '@metamask/accounts-controller'; import type { RestrictedControllerMessenger, @@ -105,6 +106,7 @@ export type TokenDetectionControllerActions = export type AllowedActions = | AccountsControllerGetSelectedAccountAction + | AccountsControllerGetAccountAction | NetworkControllerGetNetworkClientByIdAction | NetworkControllerGetNetworkConfigurationByNetworkClientId | NetworkControllerGetStateAction @@ -121,7 +123,7 @@ export type TokenDetectionControllerEvents = TokenDetectionControllerStateChangeEvent; export type AllowedEvents = - | AccountsControllerSelectedAccountChangeEvent + | AccountsControllerSelectedEvmAccountChangeEvent | NetworkControllerNetworkDidChangeEvent | TokenListStateChange | KeyringControllerLockEvent @@ -153,7 +155,7 @@ export class TokenDetectionController extends StaticIntervalPollingController< > { #intervalId?: ReturnType; - #selectedAddress: string; + #selectedAccountId: string; #networkClientId: NetworkClientId; @@ -186,19 +188,19 @@ export class TokenDetectionController extends StaticIntervalPollingController< * @param options.messenger - The controller messaging system. * @param options.disabled - If set to true, all network requests are blocked. * @param options.interval - Polling interval used to fetch new token rates - * @param options.selectedAddress - Vault selected address + * @param options.selectedAccountId - Vault selected address * @param options.getBalancesInSingleCall - Gets the balances of a list of tokens for the given address. * @param options.trackMetaMetricsEvent - Sets options for MetaMetrics event tracking. */ constructor({ - selectedAddress, + selectedAccountId, interval = DEFAULT_INTERVAL, disabled = true, getBalancesInSingleCall, trackMetaMetricsEvent, messenger, }: { - selectedAddress?: string; + selectedAccountId?: string; interval?: number; disabled?: boolean; getBalancesInSingleCall: AssetsContractController['getBalancesInSingleCall']; @@ -223,10 +225,15 @@ export class TokenDetectionController extends StaticIntervalPollingController< this.#disabled = disabled; this.setIntervalLength(interval); - this.#selectedAddress = - selectedAddress ?? - this.messagingSystem.call('AccountsController:getSelectedAccount') - .address; + if (selectedAccountId) { + this.#selectedAccountId = selectedAccountId; + } else { + const selectedInternalAccount = this.messagingSystem.call( + 'AccountsController:getSelectedAccount', + 'eip155:*', + ); + this.#selectedAccountId = selectedInternalAccount.id; + } const { chainId, networkClientId } = this.#getCorrectChainIdAndNetworkClientId(); @@ -277,32 +284,33 @@ export class TokenDetectionController extends StaticIntervalPollingController< this.messagingSystem.subscribe( 'PreferencesController:stateChange', - async ({ selectedAddress: newSelectedAddress, useTokenDetection }) => { - const isSelectedAddressChanged = - this.#selectedAddress !== newSelectedAddress; + async ({ useTokenDetection }) => { + const selectedAccount = this.messagingSystem.call( + 'AccountsController:getSelectedAccount', + 'eip155:*', + ); const isDetectionChangedFromPreferences = this.#isDetectionEnabledFromPreferences !== useTokenDetection; - this.#selectedAddress = newSelectedAddress; this.#isDetectionEnabledFromPreferences = useTokenDetection; - if (isSelectedAddressChanged || isDetectionChangedFromPreferences) { + if (isDetectionChangedFromPreferences) { await this.#restartTokenDetection({ - selectedAddress: this.#selectedAddress, + selectedAccountId: selectedAccount.id, }); } }, ); this.messagingSystem.subscribe( - 'AccountsController:selectedAccountChange', - async ({ address: newSelectedAddress }) => { - const isSelectedAddressChanged = - this.#selectedAddress !== newSelectedAddress; - if (isSelectedAddressChanged) { - this.#selectedAddress = newSelectedAddress; + 'AccountsController:selectedEvmAccountChange', + async (internalAccount) => { + const didSelectedAccountIdChanged = + this.#selectedAccountId !== internalAccount.id; + if (didSelectedAccountIdChanged) { + this.#selectedAccountId = internalAccount.id; await this.#restartTokenDetection({ - selectedAddress: this.#selectedAddress, + selectedAccountId: this.#selectedAccountId, }); } }, @@ -436,16 +444,23 @@ export class TokenDetectionController extends StaticIntervalPollingController< * in case of address change or user session initialization. * * @param options - Options for restart token detection. - * @param options.selectedAddress - the selectedAddress against which to detect for token balances + * @param options.selectedAccountId - the id of the InternalAccount against which to detect for token balances * @param options.networkClientId - The ID of the network client to use. */ async #restartTokenDetection({ - selectedAddress, + selectedAccountId, networkClientId, }: { - selectedAddress?: string; + selectedAccountId?: string; networkClientId?: NetworkClientId; } = {}): Promise { + const internalAccount = this.messagingSystem.call( + 'AccountsController:getAccount', + selectedAccountId ?? this.#selectedAccountId, + ); + + const selectedAddress = internalAccount?.address || ''; + await this.detectTokens({ networkClientId, selectedAddress, @@ -472,8 +487,13 @@ export class TokenDetectionController extends StaticIntervalPollingController< return; } + const selectedInternalAccount = this.messagingSystem.call( + 'AccountsController:getAccount', + this.#selectedAccountId, + ); + const addressAgainstWhichToDetect = - selectedAddress ?? this.#selectedAddress; + selectedAddress ?? selectedInternalAccount?.address ?? ''; const { chainId, networkClientId: selectedNetworkClientId } = this.#getCorrectChainIdAndNetworkClientId(networkClientId); const chainIdAgainstWhichToDetect = chainId; From a11976de727b03549a8860a80fab372f061b0a98 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Tue, 21 May 2024 15:59:08 +0800 Subject: [PATCH 12/24] refactor: Update TokenRatesController to use account id --- .../src/TokenRatesController.test.ts | 559 ++++++++++++------ .../src/TokenRatesController.ts | 40 +- 2 files changed, 393 insertions(+), 206 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index a82a195ff7a..a8f79ae23d3 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -1,10 +1,11 @@ +import { createMockInternalAccount } from '@metamask/accounts-controller'; import { NetworksTicker, toChecksumHexAddress, toHex, } from '@metamask/controller-utils'; +import type { InternalAccount } from '@metamask/keyring-api'; import type { NetworkState } from '@metamask/network-controller'; -import type { PreferencesState } from '@metamask/preferences-controller'; import type { Hex } from '@metamask/utils'; import { add0x } from '@metamask/utils'; import nock from 'nock'; @@ -25,7 +26,9 @@ import type { } from './TokenRatesController'; import type { TokensState } from './TokensController'; -const defaultSelectedAddress = '0x0000000000000000000000000000000000000001'; +const defaultMockInternalAccount = createMockInternalAccount({ + address: '0xA', +}); const mockTokenAddress = '0x0000000000000000000000000000000000000010'; describe('TokenRatesController', () => { @@ -47,10 +50,11 @@ describe('TokenRatesController', () => { it('should set default state', () => { const controller = new TokenRatesController({ getNetworkClientById: jest.fn(), + getInternalAccount: jest.fn(), chainId: '0x1', ticker: NetworksTicker.mainnet, - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), + selectedAccountId: defaultMockInternalAccount.id, + onSelectedAccountChange: jest.fn(), onTokensStateChange: jest.fn(), onNetworkStateChange: jest.fn(), tokenPricesService: buildMockTokenPricesService(), @@ -64,10 +68,11 @@ describe('TokenRatesController', () => { it('should initialize with the default config', () => { const controller = new TokenRatesController({ getNetworkClientById: jest.fn(), + getInternalAccount: jest.fn(), chainId: '0x1', ticker: NetworksTicker.mainnet, - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), + selectedAccountId: defaultMockInternalAccount.id, + onSelectedAccountChange: jest.fn(), onTokensStateChange: jest.fn(), onNetworkStateChange: jest.fn(), tokenPricesService: buildMockTokenPricesService(), @@ -80,7 +85,7 @@ describe('TokenRatesController', () => { disabled: false, nativeCurrency: NetworksTicker.mainnet, chainId: '0x1', - selectedAddress: defaultSelectedAddress, + selectedAccountId: defaultMockInternalAccount.id, }); }); @@ -89,10 +94,11 @@ describe('TokenRatesController', () => { new TokenRatesController({ interval: 100, getNetworkClientById: jest.fn(), + getInternalAccount: jest.fn(), chainId: '0x1', ticker: NetworksTicker.mainnet, - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), + selectedAccountId: defaultMockInternalAccount.id, + onSelectedAccountChange: jest.fn(), onTokensStateChange: jest.fn(), onNetworkStateChange: jest.fn(), tokenPricesService: buildMockTokenPricesService(), @@ -118,18 +124,22 @@ describe('TokenRatesController', () => { describe('when legacy polling is active', () => { it('should update exchange rates when any of the addresses in the "all tokens" collection change', async () => { const chainId = '0xC'; - const selectedAddress = '0xA'; + const selectedAccount = defaultMockInternalAccount; const tokenAddresses = ['0xE1', '0xE2']; + const mockGetInternalAccount = jest + .fn() + .mockReturnValue(selectedAccount); await withController( { options: { chainId, - selectedAddress, + selectedAccountId: selectedAccount.id, + getInternalAccount: mockGetInternalAccount, }, config: { allTokens: { [chainId]: { - [selectedAddress]: [ + [selectedAccount.address]: [ { address: tokenAddresses[0], decimals: 0, @@ -152,7 +162,7 @@ describe('TokenRatesController', () => { controllerEvents.tokensStateChange({ allTokens: { [chainId]: { - [selectedAddress]: [ + [selectedAccount.address]: [ { address: tokenAddresses[1], decimals: 0, @@ -173,19 +183,23 @@ describe('TokenRatesController', () => { it('should update exchange rates when any of the addresses in the "all detected tokens" collection change', async () => { const chainId = '0xC'; - const selectedAddress = '0xA'; + const selectedAccount = createMockInternalAccount({ address: '0xA' }); const tokenAddresses = ['0xE1', '0xE2']; + const mockGetInternalAccount = jest + .fn() + .mockReturnValue(selectedAccount); await withController( { options: { chainId, - selectedAddress, + selectedAccountId: selectedAccount.id, + getInternalAccount: mockGetInternalAccount, }, config: { allTokens: {}, allDetectedTokens: { [chainId]: { - [selectedAddress]: [ + [selectedAccount.address]: [ { address: tokenAddresses[0], decimals: 0, @@ -208,7 +222,7 @@ describe('TokenRatesController', () => { allTokens: {}, allDetectedTokens: { [chainId]: { - [selectedAddress]: [ + [selectedAccount.address]: [ { address: tokenAddresses[1], decimals: 0, @@ -228,11 +242,14 @@ describe('TokenRatesController', () => { it('should not update exchange rates if both the "all tokens" or "all detected tokens" are exactly the same', async () => { const chainId = '0xC'; - const selectedAddress = '0xA'; + const selectedAccount = createMockInternalAccount({ address: '0xA' }); + const mockGetInternalAccount = jest + .fn() + .mockReturnValue(selectedAccount); const tokensState = { allTokens: { [chainId]: { - [selectedAddress]: [ + [selectedAccount.address]: [ { address: mockTokenAddress, decimals: 0, @@ -248,7 +265,8 @@ describe('TokenRatesController', () => { { options: { chainId, - selectedAddress, + selectedAccountId: selectedAccount.id, + getInternalAccount: mockGetInternalAccount, }, config: tokensState, }, @@ -269,10 +287,10 @@ describe('TokenRatesController', () => { it('should not update exchange rates if all of the tokens in "all tokens" just move to "all detected tokens"', async () => { const chainId = '0xC'; - const selectedAddress = '0xA'; + const selectedAccount = createMockInternalAccount({ address: '0xA' }); const tokens = { [chainId]: { - [selectedAddress]: [ + [selectedAccount.address]: [ { address: mockTokenAddress, decimals: 0, @@ -286,11 +304,14 @@ describe('TokenRatesController', () => { { options: { chainId, - selectedAddress, + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), }, config: { allTokens: tokens, allDetectedTokens: {}, + selectedAccountId: selectedAccount.id, }, }, async ({ controller, controllerEvents }) => { @@ -313,17 +334,21 @@ describe('TokenRatesController', () => { it('should not update exchange rates if a new token is added to "all detected tokens" but is already present in "all tokens"', async () => { const chainId = '0xC'; - const selectedAddress = '0xA'; + const selectedAccount = createMockInternalAccount({ address: '0xA' }); + const mockGetInternalAccount = jest + .fn() + .mockReturnValue(selectedAccount); await withController( { options: { chainId, - selectedAddress, + selectedAccountId: selectedAccount.id, + getInternalAccount: mockGetInternalAccount, }, config: { allTokens: { [chainId]: { - [selectedAddress]: [ + [selectedAccount.address]: [ { address: mockTokenAddress, decimals: 0, @@ -346,7 +371,7 @@ describe('TokenRatesController', () => { controllerEvents.tokensStateChange({ allTokens: { [chainId]: { - [selectedAddress]: [ + [selectedAccount.address]: [ { address: mockTokenAddress, decimals: 0, @@ -358,7 +383,7 @@ describe('TokenRatesController', () => { }, allDetectedTokens: { [chainId]: { - [selectedAddress]: [ + [selectedAccount.address]: [ { address: mockTokenAddress, decimals: 0, @@ -378,18 +403,22 @@ describe('TokenRatesController', () => { it('should not update exchange rates if a new token is added to "all tokens" but is already present in "all detected tokens"', async () => { const chainId = '0xC'; - const selectedAddress = '0xA'; + const selectedAccount = createMockInternalAccount({ address: '0xA' }); + const mockGetInternalAccount = jest + .fn() + .mockReturnValue(selectedAccount); await withController( { options: { chainId, - selectedAddress, + selectedAccountId: selectedAccount.id, + getInternalAccount: mockGetInternalAccount, }, config: { allTokens: {}, allDetectedTokens: { [chainId]: { - [selectedAddress]: [ + [selectedAccount.address]: [ { address: mockTokenAddress, decimals: 0, @@ -411,7 +440,7 @@ describe('TokenRatesController', () => { controllerEvents.tokensStateChange({ allTokens: { [chainId]: { - [selectedAddress]: [ + [selectedAccount.address]: [ { address: mockTokenAddress, decimals: 0, @@ -423,7 +452,7 @@ describe('TokenRatesController', () => { }, allDetectedTokens: { [chainId]: { - [selectedAddress]: [ + [selectedAccount.address]: [ { address: mockTokenAddress, decimals: 0, @@ -443,18 +472,22 @@ describe('TokenRatesController', () => { it('should not update exchange rates if none of the addresses in "all tokens" or "all detected tokens" change, even if other parts of the token change', async () => { const chainId = '0xC'; - const selectedAddress = '0xA'; + const selectedAccount = createMockInternalAccount({ address: '0xA' }); + const mockGetInternalAccount = jest + .fn() + .mockReturnValue(selectedAccount); await withController( { options: { chainId, - selectedAddress, + selectedAccountId: selectedAccount.id, + getInternalAccount: mockGetInternalAccount, }, config: { allTokens: {}, allDetectedTokens: { [chainId]: { - [selectedAddress]: [ + [selectedAccount.address]: [ { address: mockTokenAddress, decimals: 3, @@ -477,7 +510,7 @@ describe('TokenRatesController', () => { allTokens: {}, allDetectedTokens: { [chainId]: { - [selectedAddress]: [ + [selectedAccount.address]: [ { address: mockTokenAddress, decimals: 7, @@ -497,18 +530,24 @@ describe('TokenRatesController', () => { it('should not update exchange rates if none of the addresses in "all tokens" or "all detected tokens" change, when normalized to checksum addresses', async () => { const chainId = '0xC'; - const selectedAddress = '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; + const selectedAccount = createMockInternalAccount({ + address: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + }); + const mockGetInternalAccount = jest + .fn() + .mockReturnValue(selectedAccount); await withController( { options: { chainId, - selectedAddress, + selectedAccountId: selectedAccount.id, + getInternalAccount: mockGetInternalAccount, }, config: { allTokens: {}, allDetectedTokens: { [chainId]: { - [selectedAddress]: [ + [selectedAccount.address]: [ { address: '0x0EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE2', decimals: 3, @@ -531,7 +570,7 @@ describe('TokenRatesController', () => { allTokens: {}, allDetectedTokens: { [chainId]: { - [selectedAddress]: [ + [selectedAccount.address]: [ { address: '0x0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2', decimals: 7, @@ -551,18 +590,22 @@ describe('TokenRatesController', () => { it('should not update exchange rates if any of the addresses in "all tokens" or "all detected tokens" merely change order', async () => { const chainId = '0xC'; - const selectedAddress = '0xA'; + const selectedAccount = createMockInternalAccount({ address: '0xA' }); + const mockGetInternalAccount = jest + .fn() + .mockReturnValue(selectedAccount); await withController( { options: { chainId, - selectedAddress, + selectedAccountId: selectedAccount.id, + getInternalAccount: mockGetInternalAccount, }, config: { allTokens: {}, allDetectedTokens: { [chainId]: { - [selectedAddress]: [ + [selectedAccount.address]: [ { address: '0xE1', decimals: 0, @@ -591,7 +634,7 @@ describe('TokenRatesController', () => { allTokens: {}, allDetectedTokens: { [chainId]: { - [selectedAddress]: [ + [selectedAccount.address]: [ { address: '0xE2', decimals: 0, @@ -619,18 +662,22 @@ describe('TokenRatesController', () => { describe('when legacy polling is inactive', () => { it('should not update exchange rates when any of the addresses in the "all tokens" collection change', async () => { const chainId = '0xC'; - const selectedAddress = '0xA'; + const selectedAccount = createMockInternalAccount({ address: '0xA' }); + const mockGetInternalAccount = jest + .fn() + .mockReturnValue(selectedAccount); const tokenAddresses = ['0xE1', '0xE2']; await withController( { options: { chainId, - selectedAddress, + selectedAccountId: selectedAccount.id, + getInternalAccount: mockGetInternalAccount, }, config: { allTokens: { [chainId]: { - [selectedAddress]: [ + [selectedAccount.address]: [ { address: tokenAddresses[0], decimals: 0, @@ -652,7 +699,7 @@ describe('TokenRatesController', () => { controllerEvents.tokensStateChange({ allTokens: { [chainId]: { - [selectedAddress]: [ + [selectedAccount.address]: [ { address: tokenAddresses[1], decimals: 0, @@ -672,19 +719,23 @@ describe('TokenRatesController', () => { it('should not update exchange rates when any of the addresses in the "all detected tokens" collection change', async () => { const chainId = '0xC'; - const selectedAddress = '0xA'; + const selectedAccount = createMockInternalAccount({ address: '0xA' }); + const mockGetInternalAccount = jest + .fn() + .mockReturnValue(selectedAccount); const tokenAddresses = ['0xE1', '0xE2']; await withController( { options: { chainId, - selectedAddress, + selectedAccountId: selectedAccount.id, + getInternalAccount: mockGetInternalAccount, }, config: { allTokens: {}, allDetectedTokens: { [chainId]: { - [selectedAddress]: [ + [selectedAccount.address]: [ { address: tokenAddresses[0], decimals: 0, @@ -706,7 +757,7 @@ describe('TokenRatesController', () => { allTokens: {}, allDetectedTokens: { [chainId]: { - [selectedAddress]: [ + [selectedAccount.address]: [ { address: tokenAddresses[1], decimals: 0, @@ -749,10 +800,11 @@ describe('TokenRatesController', () => { const controller = new TokenRatesController({ interval: 100, getNetworkClientById: jest.fn(), + getInternalAccount: jest.fn(), chainId: toHex(1337), ticker: 'TEST', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), + selectedAccountId: defaultMockInternalAccount.id, + onSelectedAccountChange: jest.fn(), onTokensStateChange: jest.fn(), onNetworkStateChange, tokenPricesService: buildMockTokenPricesService(), @@ -782,10 +834,11 @@ describe('TokenRatesController', () => { const controller = new TokenRatesController({ interval: 100, getNetworkClientById: jest.fn(), + getInternalAccount: jest.fn(), chainId: toHex(1337), ticker: 'TEST', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), + selectedAccountId: defaultMockInternalAccount.id, + onSelectedAccountChange: jest.fn(), onTokensStateChange: jest.fn(), onNetworkStateChange, tokenPricesService: buildMockTokenPricesService(), @@ -815,10 +868,11 @@ describe('TokenRatesController', () => { const controller = new TokenRatesController({ interval: 100, getNetworkClientById: jest.fn(), + getInternalAccount: jest.fn(), chainId: toHex(1337), ticker: 'TEST', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), + selectedAccountId: defaultMockInternalAccount.id, + onSelectedAccountChange: jest.fn(), onTokensStateChange: jest.fn(), onNetworkStateChange, tokenPricesService: buildMockTokenPricesService(), @@ -846,10 +900,11 @@ describe('TokenRatesController', () => { const controller = new TokenRatesController({ interval: 100, getNetworkClientById: jest.fn(), + getInternalAccount: jest.fn(), chainId: toHex(1337), ticker: 'TEST', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), + selectedAccountId: defaultMockInternalAccount.id, + onSelectedAccountChange: jest.fn(), onTokensStateChange: jest.fn(), onNetworkStateChange, tokenPricesService: buildMockTokenPricesService(), @@ -877,10 +932,11 @@ describe('TokenRatesController', () => { const controller = new TokenRatesController({ interval: 100, getNetworkClientById: jest.fn(), + getInternalAccount: jest.fn(), chainId: toHex(1337), ticker: 'TEST', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), + selectedAccountId: defaultMockInternalAccount.id, + onSelectedAccountChange: jest.fn(), onTokensStateChange: jest.fn(), onNetworkStateChange, tokenPricesService: buildMockTokenPricesService(), @@ -912,10 +968,11 @@ describe('TokenRatesController', () => { const controller = new TokenRatesController({ interval: 100, getNetworkClientById: jest.fn(), + getInternalAccount: jest.fn(), chainId: toHex(1337), ticker: 'TEST', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), + selectedAccountId: defaultMockInternalAccount.id, + onSelectedAccountChange: jest.fn(), onTokensStateChange: jest.fn(), onNetworkStateChange, tokenPricesService: buildMockTokenPricesService(), @@ -944,10 +1001,11 @@ describe('TokenRatesController', () => { const controller = new TokenRatesController({ interval: 100, getNetworkClientById: jest.fn(), + getInternalAccount: jest.fn(), chainId: toHex(1337), ticker: 'TEST', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), + selectedAccountId: defaultMockInternalAccount.id, + onSelectedAccountChange: jest.fn(), onTokensStateChange: jest.fn(), onNetworkStateChange, tokenPricesService: buildMockTokenPricesService(), @@ -976,10 +1034,11 @@ describe('TokenRatesController', () => { const controller = new TokenRatesController({ interval: 100, getNetworkClientById: jest.fn(), + getInternalAccount: jest.fn(), chainId: toHex(1337), ticker: 'TEST', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), + selectedAccountId: defaultMockInternalAccount.id, + onSelectedAccountChange: jest.fn(), onTokensStateChange: jest.fn(), onNetworkStateChange, tokenPricesService: buildMockTokenPricesService(), @@ -1006,10 +1065,11 @@ describe('TokenRatesController', () => { const controller = new TokenRatesController({ interval: 100, getNetworkClientById: jest.fn(), + getInternalAccount: jest.fn(), chainId: toHex(1337), ticker: 'TEST', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), + selectedAccountId: defaultMockInternalAccount.id, + onSelectedAccountChange: jest.fn(), onTokensStateChange: jest.fn(), onNetworkStateChange, tokenPricesService: buildMockTokenPricesService(), @@ -1026,37 +1086,41 @@ describe('TokenRatesController', () => { }); }); - describe('PreferencesController::stateChange', () => { + describe('onSelectedAccountChange', () => { let clock: sinon.SinonFakeTimers; - beforeEach(() => { clock = useFakeTimers({ now: Date.now() }); }); - afterEach(() => { clock.restore(); }); describe('when polling is active', () => { it('should update exchange rates when selected address changes', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let preferencesStateChangeListener: (state: any) => Promise; - const onPreferencesStateChange = jest + const alternateSelectedAddress = + '0x0000000000000000000000000000000000000002'; + const alternativeAccount = createMockInternalAccount({ + address: alternateSelectedAddress, + }); + + let selectedAccountChangeListener: ( + interalAccount: InternalAccount, + ) => Promise; + const onSelectedAccountChange = jest .fn() .mockImplementation((listener) => { - preferencesStateChangeListener = listener; + selectedAccountChangeListener = listener; }); - const alternateSelectedAddress = - '0x0000000000000000000000000000000000000002'; + const controller = new TokenRatesController( { interval: 100, getNetworkClientById: jest.fn(), + getInternalAccount: jest.fn(), chainId: '0x1', ticker: NetworksTicker.mainnet, - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange, + selectedAccountId: defaultMockInternalAccount.id, + onSelectedAccountChange, onTokensStateChange: jest.fn(), onNetworkStateChange: jest.fn(), tokenPricesService: buildMockTokenPricesService(), @@ -1078,30 +1142,31 @@ describe('TokenRatesController', () => { .mockResolvedValue(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await preferencesStateChangeListener!({ - selectedAddress: alternateSelectedAddress, - }); + await selectedAccountChangeListener!(alternativeAccount); - expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); + expect(updateExchangeRatesSpy).toHaveBeenCalled(); }); it('should not update exchange rates when preferences state changes without selected address changing', async () => { // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let preferencesStateChangeListener: (state: any) => Promise; - const onPreferencesStateChange = jest + + let selectedAccountChangeListener: ( + interalAccount: InternalAccount, + ) => Promise; + const onSelectedAccountChange = jest .fn() .mockImplementation((listener) => { - preferencesStateChangeListener = listener; + selectedAccountChangeListener = listener; }); const controller = new TokenRatesController( { interval: 100, + getInternalAccount: jest.fn(), getNetworkClientById: jest.fn(), chainId: '0x1', ticker: NetworksTicker.mainnet, - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange, + selectedAccountId: defaultMockInternalAccount.id, + onSelectedAccountChange, onTokensStateChange: jest.fn(), onNetworkStateChange: jest.fn(), tokenPricesService: buildMockTokenPricesService(), @@ -1109,7 +1174,7 @@ describe('TokenRatesController', () => { { allTokens: { '0x1': { - [defaultSelectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: '0x02', decimals: 0, symbol: '', aggregators: [] }, { address: '0x03', decimals: 0, symbol: '', aggregators: [] }, ], @@ -1123,10 +1188,7 @@ describe('TokenRatesController', () => { .mockResolvedValue(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await preferencesStateChangeListener!({ - selectedAddress: defaultSelectedAddress, - exampleConfig: 'exampleValue', - }); + await selectedAccountChangeListener!(defaultMockInternalAccount); expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); }); @@ -1134,24 +1196,29 @@ describe('TokenRatesController', () => { describe('when polling is inactive', () => { it('should not update exchange rates when selected address changes', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let preferencesStateChangeListener: (state: any) => Promise; - const onPreferencesStateChange = jest + const alternateSelectedAddress = + '0x0000000000000000000000000000000000000002'; + const alternateAccount = createMockInternalAccount({ + address: alternateSelectedAddress, + }); + let selectedAccountChangeListener: ( + interalAccount: InternalAccount, + ) => Promise; + const onSelectedAccountChange = jest .fn() .mockImplementation((listener) => { - preferencesStateChangeListener = listener; + selectedAccountChangeListener = listener; }); - const alternateSelectedAddress = - '0x0000000000000000000000000000000000000002'; + const controller = new TokenRatesController( { interval: 100, + getInternalAccount: jest.fn(), getNetworkClientById: jest.fn(), chainId: '0x1', ticker: NetworksTicker.mainnet, - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange, + selectedAccountId: defaultMockInternalAccount.id, + onSelectedAccountChange, onTokensStateChange: jest.fn(), onNetworkStateChange: jest.fn(), tokenPricesService: buildMockTokenPricesService(), @@ -1159,7 +1226,7 @@ describe('TokenRatesController', () => { { allTokens: { '0x1': { - [alternateSelectedAddress]: [ + [alternateAccount.address]: [ { address: '0x02', decimals: 0, symbol: '', aggregators: [] }, { address: '0x03', decimals: 0, symbol: '', aggregators: [] }, ], @@ -1172,9 +1239,7 @@ describe('TokenRatesController', () => { .mockResolvedValue(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await preferencesStateChangeListener!({ - selectedAddress: alternateSelectedAddress, - }); + await selectedAccountChangeListener!(alternateAccount); expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); }); @@ -1201,10 +1266,13 @@ describe('TokenRatesController', () => { { interval, getNetworkClientById: jest.fn(), + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), chainId: '0x1', ticker: NetworksTicker.mainnet, - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), + selectedAccountId: defaultMockInternalAccount.id, + onSelectedAccountChange: jest.fn(), onTokensStateChange: jest.fn(), onNetworkStateChange: jest.fn(), tokenPricesService, @@ -1212,7 +1280,7 @@ describe('TokenRatesController', () => { { allTokens: { '0x1': { - [defaultSelectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: mockTokenAddress, decimals: 0, @@ -1245,10 +1313,13 @@ describe('TokenRatesController', () => { { interval, getNetworkClientById: jest.fn(), + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), chainId: '0x1', ticker: NetworksTicker.mainnet, - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), + selectedAccountId: defaultMockInternalAccount.id, + onSelectedAccountChange: jest.fn(), onTokensStateChange: jest.fn(), onNetworkStateChange: jest.fn(), tokenPricesService, @@ -1256,7 +1327,7 @@ describe('TokenRatesController', () => { { allTokens: { '0x1': { - [defaultSelectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: mockTokenAddress, decimals: 0, @@ -1300,8 +1371,8 @@ describe('TokenRatesController', () => { interval, chainId: '0x2', ticker: 'ticker', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), + selectedAccountId: defaultMockInternalAccount.id, + onSelectedAccountChange: jest.fn(), onTokensStateChange: jest.fn(), onNetworkStateChange: jest.fn(), getNetworkClientById: jest.fn().mockReturnValue({ @@ -1310,12 +1381,15 @@ describe('TokenRatesController', () => { ticker: NetworksTicker.mainnet, }, }), + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), tokenPricesService, }, { allTokens: { '0x1': { - [defaultSelectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: mockTokenAddress, decimals: 0, @@ -1352,8 +1426,8 @@ describe('TokenRatesController', () => { { chainId: '0x2', ticker: 'ticker', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), + selectedAccountId: defaultMockInternalAccount.id, + onSelectedAccountChange: jest.fn(), onTokensStateChange: jest.fn(), onNetworkStateChange: jest.fn(), getNetworkClientById: jest.fn().mockReturnValue({ @@ -1362,12 +1436,15 @@ describe('TokenRatesController', () => { ticker: NetworksTicker.mainnet, }, }), + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), tokenPricesService, }, { allTokens: { '0x1': { - [defaultSelectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: '0x02', decimals: 0, @@ -1417,8 +1494,8 @@ describe('TokenRatesController', () => { { chainId: '0x2', ticker: 'ticker', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), + selectedAccountId: defaultMockInternalAccount.id, + onSelectedAccountChange: jest.fn(), onTokensStateChange: jest.fn(), onNetworkStateChange: jest.fn(), getNetworkClientById: jest.fn().mockReturnValue({ @@ -1427,12 +1504,15 @@ describe('TokenRatesController', () => { ticker: 'LOL', }, }), + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), tokenPricesService, }, { allTokens: { '0x1': { - [defaultSelectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: '0x02', decimals: 0, @@ -1483,8 +1563,8 @@ describe('TokenRatesController', () => { { chainId: '0x2', ticker: 'ETH', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), + selectedAccountId: defaultMockInternalAccount.id, + onSelectedAccountChange: jest.fn(), onTokensStateChange: jest.fn(), onNetworkStateChange: jest.fn(), getNetworkClientById: jest.fn().mockReturnValue({ @@ -1493,12 +1573,15 @@ describe('TokenRatesController', () => { ticker: 'LOL', }, }), + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), tokenPricesService, }, { allTokens: { '0x1': { - [defaultSelectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: '0x02', decimals: 0, @@ -1544,8 +1627,8 @@ describe('TokenRatesController', () => { interval, chainId: '0x2', ticker: 'ticker', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), + selectedAccountId: defaultMockInternalAccount.id, + onSelectedAccountChange: jest.fn(), onTokensStateChange: jest.fn(), onNetworkStateChange: jest.fn(), getNetworkClientById: jest.fn().mockReturnValue({ @@ -1554,12 +1637,15 @@ describe('TokenRatesController', () => { ticker: NetworksTicker.mainnet, }, }), + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), tokenPricesService, }, { allTokens: { '0x1': { - [defaultSelectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: mockTokenAddress, decimals: 0, @@ -1598,14 +1684,24 @@ describe('TokenRatesController', () => { ])('%s', (method) => { it('does not update state when disabled', async () => { await withController( - { config: { disabled: true } }, + { + options: { + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), + }, + config: { + disabled: true, + selectedAccountId: defaultMockInternalAccount.id, + }, + }, async ({ controller, controllerEvents }) => { const tokenAddress = '0x0000000000000000000000000000000000000001'; await callUpdateExchangeRatesMethod({ allTokens: { [toHex(1)]: { - [controller.config.selectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: tokenAddress, decimals: 18, @@ -1631,47 +1727,59 @@ describe('TokenRatesController', () => { }); it('does not update state if there are no tokens for the given chain and address', async () => { - await withController(async ({ controller, controllerEvents }) => { - const tokenAddress = '0x0000000000000000000000000000000000000001'; - const differentAccount = '0x1000000000000000000000000000000000000000'; + await withController( + { + options: { + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), + }, + config: { + selectedAccountId: defaultMockInternalAccount.id, + }, + }, + async ({ controller, controllerEvents }) => { + const tokenAddress = '0x0000000000000000000000000000000000000001'; + const differentAccount = '0x1000000000000000000000000000000000000000'; - await callUpdateExchangeRatesMethod({ - allTokens: { - // These tokens are for the right chain but wrong account - [toHex(1)]: { - [differentAccount]: [ - { - address: tokenAddress, - decimals: 18, - symbol: 'TST', - aggregators: [], - }, - ], - }, - // These tokens are for the right account but wrong chain - [toHex(2)]: { - [controller.config.selectedAddress]: [ - { - address: tokenAddress, - decimals: 18, - symbol: 'TST', - aggregators: [], - }, - ], + await callUpdateExchangeRatesMethod({ + allTokens: { + // These tokens are for the right chain but wrong account + [toHex(1)]: { + [differentAccount]: [ + { + address: tokenAddress, + decimals: 18, + symbol: 'TST', + aggregators: [], + }, + ], + }, + // These tokens are for the right account but wrong chain + [toHex(2)]: { + [defaultMockInternalAccount.address]: [ + { + address: tokenAddress, + decimals: 18, + symbol: 'TST', + aggregators: [], + }, + ], + }, }, - }, - chainId: toHex(1), - controller, - controllerEvents, - method, - nativeCurrency: 'ETH', - }); + chainId: toHex(1), + controller, + controllerEvents, + method, + nativeCurrency: 'ETH', + }); - expect(controller.state.contractExchangeRates).toStrictEqual({}); - expect(controller.state.contractExchangeRatesByChainId).toStrictEqual( - {}, - ); - }); + expect(controller.state.contractExchangeRates).toStrictEqual({}); + expect(controller.state.contractExchangeRatesByChainId).toStrictEqual( + {}, + ); + }, + ); }); it('does not update state if the price update fails', async () => { @@ -1681,7 +1789,17 @@ describe('TokenRatesController', () => { .mockRejectedValue(new Error('Failed to fetch')), }); await withController( - { options: { tokenPricesService } }, + { + options: { + tokenPricesService, + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), + }, + config: { + selectedAccountId: defaultMockInternalAccount.id, + }, + }, async ({ controller, controllerEvents }) => { const tokenAddress = '0x0000000000000000000000000000000000000001'; @@ -1690,7 +1808,7 @@ describe('TokenRatesController', () => { await callUpdateExchangeRatesMethod({ allTokens: { [toHex(1)]: { - [controller.config.selectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: tokenAddress, decimals: 18, @@ -1736,13 +1854,19 @@ describe('TokenRatesController', () => { options: { ticker, tokenPricesService, + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), + }, + config: { + selectedAccountId: defaultMockInternalAccount.id, }, }, async ({ controller, controllerEvents }) => { await callUpdateExchangeRatesMethod({ allTokens: { [chainId]: { - [controller.config.selectedAddress]: tokens, + [defaultMockInternalAccount.address]: tokens, }, }, chainId, @@ -1791,12 +1915,22 @@ describe('TokenRatesController', () => { }), }); await withController( - { options: { tokenPricesService } }, + { + options: { + tokenPricesService, + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), + }, + config: { + selectedAccountId: defaultMockInternalAccount.id, + }, + }, async ({ controller, controllerEvents }) => { await callUpdateExchangeRatesMethod({ allTokens: { [toHex(1)]: { - [controller.config.selectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: tokenAddresses[0], decimals: 18, @@ -1860,12 +1994,20 @@ describe('TokenRatesController', () => { }), }); await withController( - { options: { tokenPricesService } }, + { + options: { + tokenPricesService, + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), + }, + config: { selectedAccountId: defaultMockInternalAccount.id }, + }, async ({ controller, controllerEvents }) => { await callUpdateExchangeRatesMethod({ allTokens: { [toHex(2)]: { - [controller.config.selectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: tokenAddresses[0], decimals: 18, @@ -1940,12 +2082,22 @@ describe('TokenRatesController', () => { .reply(200, { UNSUPPORTED: 0.5 }); // .5 eth to 1 matic await withController( - { options: { tokenPricesService } }, + { + options: { + tokenPricesService, + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), + }, + config: { + selectedAccountId: defaultMockInternalAccount.id, + }, + }, async ({ controller, controllerEvents }) => { await callUpdateExchangeRatesMethod({ allTokens: { [toHex(137)]: { - [controller.config.selectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: tokenAddresses[0], decimals: 18, @@ -2020,13 +2172,19 @@ describe('TokenRatesController', () => { options: { ticker, tokenPricesService, + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), + }, + config: { + selectedAccountId: defaultMockInternalAccount.id, }, }, async ({ controller, controllerEvents }) => { await callUpdateExchangeRatesMethod({ allTokens: { [chainId]: { - [controller.config.selectedAddress]: tokens, + [defaultMockInternalAccount.address]: tokens, }, }, chainId, @@ -2080,12 +2238,22 @@ describe('TokenRatesController', () => { ) as unknown as AbstractTokenPricesService['validateChainIdSupported'], }); await withController( - { options: { tokenPricesService } }, + { + options: { + tokenPricesService, + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), + }, + config: { + selectedAccountId: defaultMockInternalAccount.id, + }, + }, async ({ controller, controllerEvents }) => { await callUpdateExchangeRatesMethod({ allTokens: { [toHex(999)]: { - [controller.config.selectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: tokenAddresses[0], decimals: 18, @@ -2149,13 +2317,23 @@ describe('TokenRatesController', () => { fetchTokenPrices: fetchTokenPricesMock, }); await withController( - { options: { tokenPricesService } }, + { + options: { + tokenPricesService, + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), + }, + config: { + selectedAccountId: defaultMockInternalAccount.id, + }, + }, async ({ controller, controllerEvents }) => { const updateExchangeRates = async () => await callUpdateExchangeRatesMethod({ allTokens: { [toHex(1)]: { - [controller.config.selectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: tokenAddresses[0], decimals: 18, @@ -2208,8 +2386,8 @@ describe('TokenRatesController', () => { */ type ControllerEvents = { networkStateChange: (state: NetworkState) => void; - preferencesStateChange: (state: PreferencesState) => void; tokensStateChange: (state: TokensState) => void; + seletedAccountChange: (internalAccount: InternalAccount) => void; }; /** @@ -2267,13 +2445,14 @@ async function withController( onNetworkStateChange: (listener) => { controllerEvents.networkStateChange = listener; }, - onPreferencesStateChange: (listener) => { - controllerEvents.preferencesStateChange = listener; + onSelectedAccountChange: (listener) => { + controllerEvents.seletedAccountChange = listener; }, onTokensStateChange: (listener) => { controllerEvents.tokensStateChange = listener; }, - selectedAddress: defaultSelectedAddress, + getInternalAccount: jest.fn(), + selectedAccountId: defaultMockInternalAccount.id, ticker: NetworksTicker.mainnet, tokenPricesService: buildMockTokenPricesService(), ...options, diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 2863bb7b96c..36f8e294e0d 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -5,13 +5,13 @@ import { FALL_BACK_VS_CURRENCY, toHex, } from '@metamask/controller-utils'; +import { type InternalAccount } from '@metamask/keyring-api'; import type { NetworkClientId, NetworkController, NetworkState, } from '@metamask/network-controller'; import { StaticIntervalPollingControllerV1 } from '@metamask/polling-controller'; -import type { PreferencesState } from '@metamask/preferences-controller'; import { createDeferredPromise, type Hex } from '@metamask/utils'; import { isEqual } from 'lodash'; @@ -60,7 +60,7 @@ export interface TokenRatesConfig extends BaseConfig { interval: number; nativeCurrency: string; chainId: Hex; - selectedAddress: string; + selectedAccountId: string; allTokens: { [chainId: Hex]: { [key: string]: Token[] } }; allDetectedTokens: { [chainId: Hex]: { [key: string]: Token[] } }; threshold: number; @@ -156,6 +156,8 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< private readonly getNetworkClientById: NetworkController['getNetworkClientById']; + private readonly getInternalAccount: (accountId: string) => InternalAccount; + /** * Creates a TokenRatesController instance. * @@ -165,8 +167,9 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< * @param options.getNetworkClientById - Gets the network client with the given id from the NetworkController. * @param options.chainId - The chain ID of the current network. * @param options.ticker - The ticker for the current network. - * @param options.selectedAddress - The current selected address. - * @param options.onPreferencesStateChange - Allows subscribing to preference controller state changes. + * @param options.getInternalAccount - A callback to get an InternalAccount by id. + * @param options.selectedAccountId - The current selected address. + * @param options.onSelectedAccountChange - Allows subscribing to changes of selected account. * @param options.onTokensStateChange - Allows subscribing to token controller state changes. * @param options.onNetworkStateChange - Allows subscribing to network state changes. * @param options.tokenPricesService - An object in charge of retrieving token prices. @@ -180,8 +183,9 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< getNetworkClientById, chainId: initialChainId, ticker: initialTicker, - selectedAddress: initialSelectedAddress, - onPreferencesStateChange, + selectedAccountId, + getInternalAccount, + onSelectedAccountChange, onTokensStateChange, onNetworkStateChange, tokenPricesService, @@ -191,9 +195,10 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< getNetworkClientById: NetworkController['getNetworkClientById']; chainId: Hex; ticker: string; - selectedAddress: string; - onPreferencesStateChange: ( - listener: (preferencesState: PreferencesState) => void, + selectedAccountId: string; + getInternalAccount: (accountId: string) => InternalAccount; + onSelectedAccountChange: ( + listener: (internalAccount: InternalAccount) => void, ) => void; onTokensStateChange: ( listener: (tokensState: TokensState) => void, @@ -213,7 +218,7 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< disabled: false, nativeCurrency: initialTicker, chainId: initialChainId, - selectedAddress: initialSelectedAddress, + selectedAccountId, allTokens: {}, // TODO: initialize these correctly, maybe as part of BaseControllerV2 migration allDetectedTokens: {}, }; @@ -225,15 +230,16 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< this.initialize(); this.setIntervalLength(interval); this.getNetworkClientById = getNetworkClientById; + this.getInternalAccount = getInternalAccount; this.#tokenPricesService = tokenPricesService; if (config?.disabled) { this.configure({ disabled: true }, false, false); } - onPreferencesStateChange(async ({ selectedAddress }) => { - if (this.config.selectedAddress !== selectedAddress) { - this.configure({ selectedAddress }); + onSelectedAccountChange(async (internalAccount) => { + if (this.config.selectedAccountId !== internalAccount.id) { + this.configure({ selectedAccountId: internalAccount.id }); if (this.#pollState === PollState.Active) { await this.updateExchangeRates(); } @@ -276,10 +282,11 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< * @returns The list of tokens addresses for the current chain */ #getTokenAddresses(chainId: Hex): Hex[] { - const { allTokens, allDetectedTokens } = this.config; - const tokens = allTokens[chainId]?.[this.config.selectedAddress] || []; + const { allTokens, allDetectedTokens, selectedAccountId } = this.config; + const internalAccount = this.getInternalAccount(selectedAccountId); + const tokens = allTokens[chainId]?.[internalAccount.address] || []; const detectedTokens = - allDetectedTokens[chainId]?.[this.config.selectedAddress] || []; + allDetectedTokens[chainId]?.[internalAccount.address] || []; return [ ...new Set( @@ -334,6 +341,7 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< */ async updateExchangeRates() { const { chainId, nativeCurrency } = this.config; + await this.updateExchangeRatesByChainId({ chainId, nativeCurrency, From 449bc00ccb9ce107e728bb635c0208a4c5267ff2 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Tue, 21 May 2024 15:59:43 +0800 Subject: [PATCH 13/24] refactor: update TokensController to use account id --- .../src/TokensController.test.ts | 262 +++++++++++------- .../src/TokensController.ts | 94 +++++-- 2 files changed, 232 insertions(+), 124 deletions(-) diff --git a/packages/assets-controllers/src/TokensController.test.ts b/packages/assets-controllers/src/TokensController.test.ts index 307c7d78233..27faf2212db 100644 --- a/packages/assets-controllers/src/TokensController.test.ts +++ b/packages/assets-controllers/src/TokensController.test.ts @@ -1,4 +1,6 @@ import { Contract } from '@ethersproject/contracts'; +import type { AccountsController } from '@metamask/accounts-controller'; +import { createMockInternalAccount } from '@metamask/accounts-controller'; import type { ApprovalStateChange } from '@metamask/approval-controller'; import { ApprovalController, @@ -15,14 +17,13 @@ import { toHex, NetworksTicker, } from '@metamask/controller-utils'; +import type { InternalAccount } from '@metamask/keyring-api'; import type { NetworkClientConfiguration, NetworkClientId, ProviderConfig, } from '@metamask/network-controller'; import { defaultState as defaultNetworkState } from '@metamask/network-controller'; -import type { PreferencesState } from '@metamask/preferences-controller'; -import { getDefaultPreferencesState } from '@metamask/preferences-controller'; import nock from 'nock'; import * as sinon from 'sinon'; import { v1 as uuidV1 } from 'uuid'; @@ -69,6 +70,10 @@ const GOERLI = { ticker: NetworksTicker.goerli, }; +const defaultMockInternalAccount = createMockInternalAccount({ + address: '0x1', +}); + describe('TokensController', () => { beforeEach(() => { uuidV1Mock.mockReturnValue('9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'); @@ -279,32 +284,34 @@ describe('TokensController', () => { it('should add token by selected address', async () => { await withController( - async ({ controller, triggerPreferencesStateChange }) => { + async ({ + controller, + triggerSelectedAccountChange, + getAccountHandler, + }) => { ContractMock.mockReturnValue( buildMockEthersERC721Contract({ supportsInterface: false }), ); const firstAddress = '0x123'; + const firstAccount = createMockInternalAccount({ + address: firstAddress, + }); const secondAddress = '0x321'; - - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: firstAddress, + const secondAccount = createMockInternalAccount({ + address: secondAddress, }); + + getAccountHandler.mockReturnValue(firstAccount); + triggerSelectedAccountChange(firstAccount); await controller.addToken({ address: '0x01', symbol: 'bar', decimals: 2, }); - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: secondAddress, - }); + triggerSelectedAccountChange(secondAccount); expect(controller.state.tokens).toHaveLength(0); - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: firstAddress, - }); + triggerSelectedAccountChange(firstAccount); expect(controller.state.tokens[0]).toStrictEqual({ address: '0x01', decimals: 2, @@ -354,7 +361,7 @@ describe('TokensController', () => { }), }, }, - async ({ controller }) => { + async ({ controller, getAccountHandler }) => { ContractMock.mockReturnValue( buildMockEthersERC721Contract({ supportsInterface: false }), ); @@ -424,25 +431,32 @@ describe('TokensController', () => { it('should remove token by selected address', async () => { await withController( - async ({ controller, triggerPreferencesStateChange }) => { + async ({ + controller, + triggerSelectedAccountChange, + getAccountHandler, + }) => { ContractMock.mockReturnValue( buildMockEthersERC721Contract({ supportsInterface: false }), ); const firstAddress = '0x123'; + const firstAccount = createMockInternalAccount({ + address: firstAddress, + }); const secondAddress = '0x321'; - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: firstAddress, + const secondAccount = createMockInternalAccount({ + address: secondAddress, }); + + getAccountHandler.mockReturnValue(firstAccount); + triggerSelectedAccountChange(firstAccount); await controller.addToken({ address: '0x02', symbol: 'baz', decimals: 2, }); - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: secondAddress, - }); + getAccountHandler.mockReturnValue(secondAccount); + triggerSelectedAccountChange(secondAccount); await controller.addToken({ address: '0x01', symbol: 'bar', @@ -452,10 +466,7 @@ describe('TokensController', () => { controller.ignoreTokens(['0x01']); expect(controller.state.tokens).toHaveLength(0); - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: firstAddress, - }); + triggerSelectedAccountChange(firstAccount); expect(controller.state.tokens[0]).toStrictEqual({ address: '0x02', decimals: 2, @@ -539,14 +550,16 @@ describe('TokensController', () => { await withController( async ({ controller, - triggerPreferencesStateChange, + triggerSelectedAccountChange, changeNetwork, + getAccountHandler, }) => { const selectedAddress = '0x0001'; - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress, + const selectedAccount = createMockInternalAccount({ + address: selectedAddress, }); + getAccountHandler.mockReturnValue(selectedAccount); + triggerSelectedAccountChange(selectedAccount); changeNetwork(SEPOLIA); await controller.addToken({ address: '0x01', @@ -586,14 +599,16 @@ describe('TokensController', () => { await withController( async ({ controller, - triggerPreferencesStateChange, + triggerSelectedAccountChange, changeNetwork, + getAccountHandler, }) => { const selectedAddress = '0x0001'; - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress, + const selectedAccount = createMockInternalAccount({ + address: selectedAddress, }); + getAccountHandler.mockReturnValue(selectedAccount); + triggerSelectedAccountChange(selectedAccount); changeNetwork(SEPOLIA); await controller.addToken({ address: '0x01', @@ -623,15 +638,20 @@ describe('TokensController', () => { await withController( async ({ controller, - triggerPreferencesStateChange, + triggerSelectedAccountChange, changeNetwork, + getAccountHandler, }) => { const selectedAddress1 = '0x0001'; + const selectedAccount1 = createMockInternalAccount({ + address: selectedAddress1, + }); const selectedAddress2 = '0x0002'; - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: selectedAddress1, + const selectedAccount2 = createMockInternalAccount({ + address: selectedAddress2, }); + getAccountHandler.mockReturnValue(selectedAccount1); + triggerSelectedAccountChange(selectedAccount1); changeNetwork(SEPOLIA); await controller.addToken({ address: '0x01', @@ -655,10 +675,8 @@ describe('TokensController', () => { controller.ignoreTokens(['0x02']); expect(controller.state.ignoredTokens).toStrictEqual(['0x02']); - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: selectedAddress2, - }); + getAccountHandler.mockReturnValue(selectedAccount2); + triggerSelectedAccountChange(selectedAccount2); expect(controller.state.ignoredTokens).toHaveLength(0); await controller.addToken({ @@ -811,7 +829,8 @@ describe('TokensController', () => { describe('addToken method', () => { it('should add isERC721 = true when token is an NFT and is in our contract-metadata repo', async () => { - await withController(async ({ controller }) => { + await withController(async ({ controller, getAccountHandler }) => { + getAccountHandler.mockReturnValue(defaultMockInternalAccount); const contractAddresses = Object.keys(contractMaps); const erc721ContractAddresses = contractAddresses.filter( (contractAddress) => contractMaps[contractAddress].erc721 === true, @@ -833,7 +852,8 @@ describe('TokensController', () => { }); it('should add isERC721 = true when the token is an NFT but not in our contract-metadata repo', async () => { - await withController(async ({ controller }) => { + await withController(async ({ controller, getAccountHandler }) => { + getAccountHandler.mockReturnValue(defaultMockInternalAccount); ContractMock.mockReturnValue( buildMockEthersERC721Contract({ supportsInterface: true }), ); @@ -861,7 +881,8 @@ describe('TokensController', () => { }); it('should add isERC721 = false to token object already in state when token is not an NFT and in our contract-metadata repo', async () => { - await withController(async ({ controller }) => { + await withController(async ({ controller, getAccountHandler }) => { + getAccountHandler.mockReturnValue(defaultMockInternalAccount); const contractAddresses = Object.keys(contractMaps); const erc20ContractAddresses = contractAddresses.filter( (contractAddress) => contractMaps[contractAddress].erc20 === true, @@ -883,7 +904,8 @@ describe('TokensController', () => { }); it('should add isERC721 = false when the token is not an NFT and not in our contract-metadata repo', async () => { - await withController(async ({ controller }) => { + await withController(async ({ controller, getAccountHandler }) => { + getAccountHandler.mockReturnValue(defaultMockInternalAccount); ContractMock.mockReturnValue( buildMockEthersERC721Contract({ supportsInterface: false }), ); @@ -911,21 +933,24 @@ describe('TokensController', () => { }); it('should throw error if switching networks while adding token', async () => { - await withController(async ({ controller, changeNetwork }) => { - const dummyTokenAddress = - '0x514910771AF9Ca656af840dff83E8264EcF986CA'; + await withController( + async ({ controller, changeNetwork, getAccountHandler }) => { + getAccountHandler.mockReturnValue(defaultMockInternalAccount); + const dummyTokenAddress = + '0x514910771AF9Ca656af840dff83E8264EcF986CA'; - const addTokenPromise = controller.addToken({ - address: dummyTokenAddress, - symbol: 'LINK', - decimals: 18, - }); - changeNetwork(GOERLI); + const addTokenPromise = controller.addToken({ + address: dummyTokenAddress, + symbol: 'LINK', + decimals: 18, + }); + changeNetwork(GOERLI); - await expect(addTokenPromise).rejects.toThrow( - 'TokensController Error: Switched networks while adding token', - ); - }); + await expect(addTokenPromise).rejects.toThrow( + 'TokensController Error: Switched networks while adding token', + ); + }, + ); }); }); @@ -1005,7 +1030,8 @@ describe('TokensController', () => { async ({ controller, changeNetwork, - triggerPreferencesStateChange, + triggerSelectedAccountChange, + getAccountHandler, }) => { ContractMock.mockReturnValue( buildMockEthersERC721Contract({ supportsInterface: false }), @@ -1014,11 +1040,11 @@ describe('TokensController', () => { // The currently configured chain + address const CONFIGURED_CHAIN = SEPOLIA; const CONFIGURED_ADDRESS = '0xConfiguredAddress'; - changeNetwork(CONFIGURED_CHAIN); - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: CONFIGURED_ADDRESS, + const configuredAccount = createMockInternalAccount({ + address: CONFIGURED_ADDRESS, }); + changeNetwork(CONFIGURED_CHAIN); + triggerSelectedAccountChange(configuredAccount); // A different chain + address const OTHER_CHAIN = '0xOtherChainId'; @@ -1042,6 +1068,8 @@ describe('TokensController', () => { detectedTokenOtherAccount, ] = generateTokens(3); + getAccountHandler.mockReturnValue(configuredAccount); + // Run twice to ensure idempotency for (let i = 0; i < 2; i++) { // Add and detect some tokens on the configured chain + account @@ -1170,6 +1198,9 @@ describe('TokensController', () => { describe('_getNewAllTokensState method', () => { it('should nest newTokens under chain ID and selected address when provided with newTokens as input', async () => { const dummySelectedAddress = '0x1'; + const dummySelectedAccount = createMockInternalAccount({ + address: dummySelectedAddress, + }); const dummyTokens: Token[] = [ { address: '0x01', @@ -1185,7 +1216,7 @@ describe('TokensController', () => { options: { chainId: ChainId.mainnet, config: { - selectedAddress: dummySelectedAddress, + selectedAccountId: dummySelectedAccount.id, }, }, }, @@ -1195,7 +1226,9 @@ describe('TokensController', () => { }); expect( - processedTokens.newAllTokens[ChainId.mainnet][dummySelectedAddress], + processedTokens.newAllTokens[ChainId.mainnet][ + dummySelectedAccount.address + ], ).toStrictEqual(dummyTokens); }, ); @@ -1203,6 +1236,9 @@ describe('TokensController', () => { it('should nest detectedTokens under chain ID and selected address when provided with detectedTokens as input', async () => { const dummySelectedAddress = '0x1'; + const dummySelectedAccount = createMockInternalAccount({ + address: dummySelectedAddress, + }); const dummyTokens: Token[] = [ { address: '0x01', @@ -1218,7 +1254,7 @@ describe('TokensController', () => { options: { chainId: ChainId.mainnet, config: { - selectedAddress: dummySelectedAddress, + selectedAccountId: dummySelectedAccount.id, }, }, }, @@ -1238,6 +1274,9 @@ describe('TokensController', () => { it('should nest ignoredTokens under chain ID and selected address when provided with ignoredTokens as input', async () => { const dummySelectedAddress = '0x1'; + const dummySelectedAccount = createMockInternalAccount({ + address: dummySelectedAddress, + }); const dummyIgnoredTokens = ['0x01']; await withController( @@ -1245,7 +1284,7 @@ describe('TokensController', () => { options: { chainId: ChainId.mainnet, config: { - selectedAddress: dummySelectedAddress, + selectedAccountId: dummySelectedAccount.id, }, }, }, @@ -1697,7 +1736,6 @@ describe('TokensController', () => { buildMockEthersERC721Contract({ supportsInterface: false }), ); uuidV1Mock.mockReturnValue(requestId); - await controller.watchAsset({ asset, type: 'ERC20' }); expect(controller.state.tokens).toHaveLength(1); @@ -1848,7 +1886,6 @@ describe('TokensController', () => { buildMockEthersERC721Contract({ supportsInterface: false }), ); uuidV1Mock.mockReturnValue(requestId); - await expect( controller.watchAsset({ asset, type: 'ERC20' }), ).rejects.toThrow(errorMessage); @@ -1968,14 +2005,20 @@ describe('TokensController', () => { describe('when PreferencesController:stateChange is published', () => { it('should update tokens list when set address changes', async () => { await withController( - async ({ controller, triggerPreferencesStateChange }) => { + async ({ + controller, + triggerSelectedAccountChange, + getAccountHandler, + }) => { ContractMock.mockReturnValue( buildMockEthersERC721Contract({ supportsInterface: false }), ); - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: '0x1', + const selectedAccount = createMockInternalAccount({ address: '0x1' }); + const selectedAccount2 = createMockInternalAccount({ + address: '0x2', }); + getAccountHandler.mockReturnValue(selectedAccount); + triggerSelectedAccountChange(selectedAccount); await controller.addToken({ address: '0x01', symbol: 'A', @@ -1986,10 +2029,8 @@ describe('TokensController', () => { symbol: 'B', decimals: 5, }); - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: '0x2', - }); + getAccountHandler.mockReturnValue(selectedAccount2); + triggerSelectedAccountChange(selectedAccount2); expect(controller.state.tokens).toStrictEqual([]); await controller.addToken({ @@ -1997,10 +2038,7 @@ describe('TokensController', () => { symbol: 'C', decimals: 6, }); - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: '0x1', - }); + triggerSelectedAccountChange(selectedAccount); expect(controller.state.tokens).toStrictEqual([ { address: '0x01', @@ -2024,10 +2062,7 @@ describe('TokensController', () => { }, ]); - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: '0x2', - }); + triggerSelectedAccountChange(selectedAccount2); expect(controller.state.tokens).toStrictEqual([ { address: '0x03', @@ -2136,6 +2171,9 @@ describe('TokensController', () => { describe('Clearing nested lists', () => { it('should clear nest allTokens under chain ID and selected address when an added token is ignored', async () => { const selectedAddress = '0x1'; + const selectedAccount = createMockInternalAccount({ + address: selectedAddress, + }); const tokenAddress = '0x01'; const dummyTokens = [ { @@ -2152,11 +2190,12 @@ describe('TokensController', () => { options: { chainId: ChainId.mainnet, config: { - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, }, - async ({ controller }) => { + async ({ controller, getAccountHandler }) => { + getAccountHandler.mockReturnValue(selectedAccount); await controller.addTokens(dummyTokens); controller.ignoreTokens([tokenAddress]); @@ -2169,6 +2208,9 @@ describe('TokensController', () => { it('should clear nest allIgnoredTokens under chain ID and selected address when an ignored token is re-added', async () => { const selectedAddress = '0x1'; + const selectedAccount = createMockInternalAccount({ + address: selectedAddress, + }); const tokenAddress = '0x01'; const dummyTokens = [ { @@ -2185,11 +2227,12 @@ describe('TokensController', () => { options: { chainId: ChainId.mainnet, config: { - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, }, - async ({ controller }) => { + async ({ controller, getAccountHandler }) => { + getAccountHandler.mockReturnValue(selectedAccount); await controller.addTokens(dummyTokens); controller.ignoreTokens([tokenAddress]); await controller.addTokens(dummyTokens); @@ -2203,6 +2246,9 @@ describe('TokensController', () => { it('should clear nest allDetectedTokens under chain ID and selected address when an detected token is added to tokens list', async () => { const selectedAddress = '0x1'; + const selectedAccount = createMockInternalAccount({ + address: selectedAddress, + }); const tokenAddress = '0x01'; const dummyTokens = [ { @@ -2219,11 +2265,12 @@ describe('TokensController', () => { options: { chainId: ChainId.mainnet, config: { - selectedAddress, + selectedAccountId: selectedAccount.id, }, }, }, - async ({ controller }) => { + async ({ controller, getAccountHandler }) => { + getAccountHandler.mockReturnValue(selectedAccount); await controller.addDetectedTokens(dummyTokens); await controller.addTokens(dummyTokens); @@ -2299,13 +2346,14 @@ type WithControllerCallback = ({ changeNetwork, messenger, approvalController, - triggerPreferencesStateChange, + triggerSelectedAccountChange, }: { controller: TokensController; changeNetwork: (providerConfig: ProviderConfig) => void; messenger: UnrestrictedMessenger; approvalController: ApprovalController; - triggerPreferencesStateChange: (state: PreferencesState) => void; + triggerSelectedAccountChange: (internalAccount: InternalAccount) => void; + getAccountHandler: jest.Mock>; }) => Promise | ReturnValue; type WithControllerArgs = @@ -2359,17 +2407,18 @@ async function withController( allowedActions: [ 'ApprovalController:addRequest', 'NetworkController:getNetworkClientById', + 'AccountsController:getAccount', ], allowedEvents: [ 'NetworkController:networkDidChange', - 'PreferencesController:stateChange', + 'AccountsController:selectedEvmAccountChange', 'TokenListController:stateChange', ], }); const controller = new TokensController({ chainId: ChainId.mainnet, config: { - selectedAddress: '0x1', + selectedAccountId: defaultMockInternalAccount.id, // The tests assume that this is set, but they shouldn't make that // assumption. But we have to do this due to a bug in TokensController // where the provider can possibly be `undefined` if `networkClientId` is @@ -2380,10 +2429,20 @@ async function withController( ...options, }); - const triggerPreferencesStateChange = (state: PreferencesState) => { - messenger.publish('PreferencesController:stateChange', state, []); + const triggerSelectedAccountChange = (internalAccount: InternalAccount) => { + messenger.publish( + 'AccountsController:selectedEvmAccountChange', + internalAccount, + ); }; + const getAccountHandler = jest.fn(); + + messenger.registerActionHandler( + `AccountsController:getAccount`, + getAccountHandler.mockReturnValue(defaultMockInternalAccount), + ); + const changeNetwork = (providerConfig: ProviderConfig) => { messenger.publish('NetworkController:networkDidChange', { ...defaultNetworkState, @@ -2404,7 +2463,8 @@ async function withController( changeNetwork, messenger, approvalController, - triggerPreferencesStateChange, + triggerSelectedAccountChange, + getAccountHandler, }); } diff --git a/packages/assets-controllers/src/TokensController.ts b/packages/assets-controllers/src/TokensController.ts index a96de9cd6c4..95384320dc2 100644 --- a/packages/assets-controllers/src/TokensController.ts +++ b/packages/assets-controllers/src/TokensController.ts @@ -1,5 +1,9 @@ import { Contract } from '@ethersproject/contracts'; import { Web3Provider } from '@ethersproject/providers'; +import type { + AccountsControllerGetAccountAction, + AccountsControllerSelectedEvmAccountChangeEvent, +} from '@metamask/accounts-controller'; import type { AddApprovalRequest } from '@metamask/approval-controller'; import type { BaseConfig, @@ -26,7 +30,6 @@ import type { NetworkControllerNetworkDidChangeEvent, Provider, } from '@metamask/network-controller'; -import type { PreferencesControllerStateChangeEvent } from '@metamask/preferences-controller'; import { rpcErrors } from '@metamask/rpc-errors'; import type { Hex } from '@metamask/utils'; import { Mutex } from 'async-mutex'; @@ -52,13 +55,13 @@ import type { Token } from './TokenRatesController'; * @type TokensConfig * * Tokens controller configuration - * @property selectedAddress - Vault selected address + * @property selectedAccountId - Vault selected address */ // This interface was created before this ESLint rule was added. // Convert to a `type` in a future major version. // eslint-disable-next-line @typescript-eslint/consistent-type-definitions export interface TokensConfig extends BaseConfig { - selectedAddress: string; + selectedAccountId: string; chainId: Hex; provider: Provider | undefined; } @@ -126,7 +129,8 @@ export type TokensControllerAddDetectedTokensAction = { */ export type AllowedActions = | AddApprovalRequest - | NetworkControllerGetNetworkClientByIdAction; + | NetworkControllerGetNetworkClientByIdAction + | AccountsControllerGetAccountAction; // TODO: Once `TokensController` is upgraded to V2, rewrite this type using the `ControllerStateChangeEvent` type, which constrains `TokensState` as `Record`. export type TokensControllerStateChangeEvent = { @@ -138,8 +142,8 @@ export type TokensControllerEvents = TokensControllerStateChangeEvent; export type AllowedEvents = | NetworkControllerNetworkDidChangeEvent - | PreferencesControllerStateChangeEvent - | TokenListStateChange; + | TokenListStateChange + | AccountsControllerSelectedEvmAccountChangeEvent; /** * The messenger of the {@link TokensController}. @@ -236,7 +240,7 @@ export class TokensController extends BaseControllerV1< super(config, state); this.defaultConfig = { - selectedAddress: '', + selectedAccountId: '', chainId: initialChainId, provider: undefined, ...config, @@ -258,15 +262,17 @@ export class TokensController extends BaseControllerV1< ); this.messagingSystem.subscribe( - 'PreferencesController:stateChange', - ({ selectedAddress }) => { + 'AccountsController:selectedEvmAccountChange', + (internalAccount) => { + this.configure({ selectedAccountId: internalAccount.id }); const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state; const { chainId } = this.config; - this.configure({ selectedAddress }); this.update({ - tokens: allTokens[chainId]?.[selectedAddress] ?? [], - ignoredTokens: allIgnoredTokens[chainId]?.[selectedAddress] ?? [], - detectedTokens: allDetectedTokens[chainId]?.[selectedAddress] ?? [], + tokens: allTokens[chainId]?.[internalAccount.address] ?? [], + ignoredTokens: + allIgnoredTokens[chainId]?.[internalAccount.address] ?? [], + detectedTokens: + allDetectedTokens[chainId]?.[internalAccount.address] ?? [], }); }, ); @@ -275,7 +281,15 @@ export class TokensController extends BaseControllerV1< 'NetworkController:networkDidChange', ({ providerConfig }) => { const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state; - const { selectedAddress } = this.config; + const { selectedAccountId } = this.config; + const selectedInternalAccount = this.messagingSystem.call( + 'AccountsController:getAccount', + selectedAccountId, + ); + + // Previously selectedAddress could be an empty string. This is to preserve the behaviour + const selectedAddress = selectedInternalAccount?.address || ''; + const { chainId } = providerConfig; this.abortController.abort(); this.abortController = new AbortController(); @@ -329,7 +343,7 @@ export class TokensController extends BaseControllerV1< interactingAddress?: string; networkClientId?: NetworkClientId; }): Promise { - const { chainId, selectedAddress } = this.config; + const { chainId, selectedAccountId } = this.config; const releaseLock = await this.mutex.acquire(); const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state; let currentChainId = chainId; @@ -340,8 +354,15 @@ export class TokensController extends BaseControllerV1< ).configuration.chainId; } - const accountAddress = interactingAddress || selectedAddress; - const isInteractingWithWalletAccount = accountAddress === selectedAddress; + const internalAccount = this.messagingSystem.call( + 'AccountsController:getAccount', + selectedAccountId, + ); + + // Previously selectedAddress could be an empty string. This is to preserve the behaviour + const accountAddress = interactingAddress || internalAccount?.address || ''; + const isInteractingWithWalletAccount = + accountAddress === internalAccount?.address; try { address = toChecksumHexAddress(address); @@ -548,10 +569,17 @@ export class TokensController extends BaseControllerV1< ) { const releaseLock = await this.mutex.acquire(); + const internalAccount = this.messagingSystem.call( + 'AccountsController:getAccount', + this.config.selectedAccountId, + ); + // Get existing tokens for the chain + account const chainId = detectionDetails?.chainId ?? this.config.chainId; + + // Previously selectedAddress could be an empty string. This is to preserve the behaviour const accountAddress = - detectionDetails?.selectedAddress ?? this.config.selectedAddress; + detectionDetails?.selectedAddress ?? internalAccount?.address ?? ''; const { allTokens, allDetectedTokens, allIgnoredTokens } = this.state; let newTokens = [...(allTokens?.[chainId]?.[accountAddress] ?? [])]; @@ -618,9 +646,17 @@ export class TokensController extends BaseControllerV1< // We may be detecting tokens on a different chain/account pair than are currently configured. // Re-point `tokens` and `detectedTokens` to keep them referencing the current chain/account. - const { chainId: currentChain, selectedAddress: currentAddress } = + const { chainId: currentChain, selectedAccountId: currentAccountId } = this.config; + const currentInternalAccount = this.messagingSystem.call( + 'AccountsController:getAccount', + currentAccountId, + ); + + // Previously selectedAddress could be an empty string. This is to preserve the behaviour + const currentAddress = currentInternalAccount?.address || ''; + newTokens = newAllTokens?.[currentChain]?.[currentAddress] || []; newDetectedTokens = newAllDetectedTokens?.[currentChain]?.[currentAddress] || []; @@ -774,6 +810,12 @@ export class TokensController extends BaseControllerV1< throw rpcErrors.invalidParams(`Invalid address "${asset.address}"`); } + // Validate if account is an evm account + const selectedAccount = this.messagingSystem.call( + 'AccountsController:getAccount', + this.config.selectedAccountId, + ); + // Validate contract if (await this._detectIsERC721(asset.address, networkClientId)) { @@ -864,7 +906,8 @@ export class TokensController extends BaseControllerV1< id: this._generateRandomId(), time: Date.now(), type, - interactingAddress: interactingAddress || this.config.selectedAddress, + // Previously selectedAddress could be an empty string. This is to preserve the behaviour + interactingAddress: interactingAddress || selectedAccount?.address || '', }; await this._requestApproval(suggestedAssetMeta); @@ -908,9 +951,14 @@ export class TokensController extends BaseControllerV1< interactingChainId, } = params; const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state; - const { chainId, selectedAddress } = this.config; - - const userAddressToAddTokens = interactingAddress ?? selectedAddress; + const { chainId, selectedAccountId } = this.config; + const selectedInternalAccount = this.messagingSystem.call( + 'AccountsController:getAccount', + selectedAccountId, + ); + // Previously selectedAddress could be an empty string. This is to preserve the behaviour + const userAddressToAddTokens = + interactingAddress ?? selectedInternalAccount?.address ?? ''; const chainIdToAddTokens = interactingChainId ?? chainId; let newAllTokens = allTokens; From ecae96fd4e9c1e38eda05fed72bbcc4a1f864bfe Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Tue, 21 May 2024 16:00:28 +0800 Subject: [PATCH 14/24] refactor: update TransactionController and IncomingTransactionHelper to use account id --- .../src/TransactionController.test.ts | 17 ++++++++++++++++- .../src/TransactionController.ts | 19 +++++++++++-------- .../helpers/IncomingTransactionHelper.test.ts | 19 +++++++++++++++++-- .../src/helpers/IncomingTransactionHelper.ts | 19 ++++++++++++------- 4 files changed, 56 insertions(+), 18 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 0253086fd08..335cd01ed44 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -16,6 +16,7 @@ import { } from '@metamask/controller-utils'; import EthQuery from '@metamask/eth-query'; import HttpProvider from '@metamask/ethjs-provider-http'; +import { EthAccountType } from '@metamask/keyring-api'; import type { BlockTracker, NetworkController, @@ -434,6 +435,20 @@ const MOCK_CUSTOM_NETWORK: MockNetwork = { }; const ACCOUNT_MOCK = '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207'; +const INTERNAL_ACCOUNT_MOCK = { + id: '58def058-d35f-49a1-a7ab-e2580565f6f5', + address: ACCOUNT_MOCK, + type: EthAccountType.Eoa, + options: {}, + methods: [], + metadata: { + name: 'Account 1', + keyring: { type: 'HD Key Tree' }, + importTime: 1631619180000, + lastSelected: 1631619180000, + }, +}; + const ACCOUNT_2_MOCK = '0x08f137f335ea1b8f193b8f6ea92561a60d23a211'; const NONCE_MOCK = 12; const ACTION_ID_MOCK = '123456'; @@ -582,7 +597,7 @@ describe('TransactionController', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any getNetworkClientRegistry: () => ({} as any), getPermittedAccounts: async () => [ACCOUNT_MOCK], - getSelectedAddress: () => ACCOUNT_MOCK, + getSelectedAccount: () => INTERNAL_ACCOUNT_MOCK, isMultichainEnabled: false, hooks: {}, onNetworkStateChange: network.subscribe, diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index c56bda6fefb..de00a917b18 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -37,7 +37,7 @@ import type { } from '@metamask/network-controller'; import { NetworkClientType } from '@metamask/network-controller'; import { errorCodes, rpcErrors, providerErrors } from '@metamask/rpc-errors'; -import type { Hex } from '@metamask/utils'; +import type { CaipChainId, Hex } from '@metamask/utils'; import { add0x } from '@metamask/utils'; import { Mutex } from 'async-mutex'; import { MethodRegistry } from 'eth-method-registry'; @@ -122,6 +122,7 @@ import { validateTransactionOrigin, validateTxParams, } from './utils/validation'; +import { InternalAccount } from '@metamask/keyring-api'; /** * Metadata for the TransactionController state, describing how to "anonymize" @@ -297,7 +298,7 @@ export type TransactionControllerOptions = { getNetworkState: () => NetworkState; getPermittedAccounts: (origin?: string) => Promise; getSavedGasFees?: (chainId: Hex) => SavedGasFees | undefined; - getSelectedAddress: () => string; + getSelectedAccount: () => InternalAccount; incomingTransactions?: IncomingTransactionOptions; isMultichainEnabled: boolean; isSimulationEnabled?: () => boolean; @@ -605,7 +606,9 @@ export class TransactionController extends BaseController< private readonly getPermittedAccounts: (origin?: string) => Promise; - private readonly getSelectedAddress: () => string; + private readonly getSelectedAccount: ( + chainId: CaipChainId, + ) => InternalAccount; private readonly getExternalPendingTransactions: ( address: string, @@ -724,7 +727,7 @@ export class TransactionController extends BaseController< * @param options.getNetworkState - Gets the state of the network controller. * @param options.getPermittedAccounts - Get accounts that a given origin has permissions for. * @param options.getSavedGasFees - Gets the saved gas fee config. - * @param options.getSelectedAddress - Gets the address of the currently selected account. + * @param options.getSelectedAccount - Gets the address of the currently selected account. * @param options.incomingTransactions - Configuration options for incoming transaction support. * @param options.isMultichainEnabled - Enable multichain support. * @param options.isSimulationEnabled - Whether new transactions will be automatically simulated. @@ -752,7 +755,7 @@ export class TransactionController extends BaseController< getNetworkState, getPermittedAccounts, getSavedGasFees, - getSelectedAddress, + getSelectedAccount, incomingTransactions = {}, isMultichainEnabled = false, isSimulationEnabled, @@ -793,7 +796,7 @@ export class TransactionController extends BaseController< this.getGasFeeEstimates = getGasFeeEstimates || (() => Promise.resolve({} as GasFeeState)); this.getPermittedAccounts = getPermittedAccounts; - this.getSelectedAddress = getSelectedAddress; + this.getSelectedAccount = getSelectedAccount; this.getExternalPendingTransactions = getExternalPendingTransactions ?? (() => []); this.securityProviderRequest = securityProviderRequest; @@ -1026,7 +1029,7 @@ export class TransactionController extends BaseController< if (origin) { await validateTransactionOrigin( await this.getPermittedAccounts(origin), - this.getSelectedAddress(), + this.getSelectedAccount('eip:155:*').address, txParams.from, origin, ); @@ -3419,7 +3422,7 @@ export class TransactionController extends BaseController< }): IncomingTransactionHelper { const incomingTransactionHelper = new IncomingTransactionHelper({ blockTracker, - getCurrentAccount: this.getSelectedAddress, + getCurrentAccount: this.getSelectedAccount, getLastFetchedBlockNumbers: () => this.state.lastFetchedBlockNumbers, getChainId: chainId ? () => chainId : this.getChainId.bind(this), isEnabled: this.#incomingTransactionOptions.isEnabled, diff --git a/packages/transaction-controller/src/helpers/IncomingTransactionHelper.test.ts b/packages/transaction-controller/src/helpers/IncomingTransactionHelper.test.ts index 49b39c4effc..6e65f7de1c3 100644 --- a/packages/transaction-controller/src/helpers/IncomingTransactionHelper.test.ts +++ b/packages/transaction-controller/src/helpers/IncomingTransactionHelper.test.ts @@ -32,7 +32,21 @@ const BLOCK_TRACKER_MOCK = { const CONTROLLER_ARGS_MOCK = { blockTracker: BLOCK_TRACKER_MOCK, - getCurrentAccount: () => ADDRESS_MOCK, + getCurrentAccount: () => { + return { + id: '58def058-d35f-49a1-a7ab-e2580565f6f5', + address: ADDRESS_MOCK, + type: 'eip155:eoa' as const, + options: {}, + methods: [], + metadata: { + name: 'Account 1', + keyring: { type: 'HD Key Tree' }, + importTime: 1631619180000, + lastSelected: 1631619180000, + }, + }; + }, getLastFetchedBlockNumbers: () => ({}), getChainId: () => CHAIN_ID_MOCK, remoteTransactionSource: {} as RemoteTransactionSource, @@ -546,7 +560,8 @@ describe('IncomingTransactionHelper', () => { remoteTransactionSource: createRemoteTransactionSourceMock([ TRANSACTION_MOCK_2, ]), - getCurrentAccount: () => undefined as unknown as string, + // @ts-expect-error testing undefined + getCurrentAccount: () => undefined, }); const { blockNumberListener } = await emitBlockTrackerLatestEvent( diff --git a/packages/transaction-controller/src/helpers/IncomingTransactionHelper.ts b/packages/transaction-controller/src/helpers/IncomingTransactionHelper.ts index c6600b48931..b96a777f12d 100644 --- a/packages/transaction-controller/src/helpers/IncomingTransactionHelper.ts +++ b/packages/transaction-controller/src/helpers/IncomingTransactionHelper.ts @@ -1,5 +1,6 @@ +import type { InternalAccount } from '@metamask/keyring-api'; import type { BlockTracker } from '@metamask/network-controller'; -import type { Hex } from '@metamask/utils'; +import type { CaipChainId, Hex } from '@metamask/utils'; import { Mutex } from 'async-mutex'; import EventEmitter from 'events'; @@ -7,6 +8,7 @@ import { incomingTransactionsLogger as log } from '../logger'; import type { RemoteTransactionSource, TransactionMeta } from '../types'; const RECENT_HISTORY_BLOCK_RANGE = 10; +const EVM_WILDCARD_CHAIN_ID = 'eip155:*'; // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -35,7 +37,7 @@ export class IncomingTransactionHelper { #blockTracker: BlockTracker; - #getCurrentAccount: () => string; + #getCurrentAccount: (chainId: CaipChainId) => InternalAccount; #getLastFetchedBlockNumbers: () => Record; @@ -72,7 +74,7 @@ export class IncomingTransactionHelper { updateTransactions, }: { blockTracker: BlockTracker; - getCurrentAccount: () => string; + getCurrentAccount: (chainId: CaipChainId) => InternalAccount; getLastFetchedBlockNumbers: () => Record; getLocalTransactions?: () => TransactionMeta[]; getChainId: () => Hex; @@ -144,7 +146,7 @@ export class IncomingTransactionHelper { this.#remoteTransactionSource.getLastBlockVariations?.() ?? []; const fromBlock = this.#getFromBlock(latestBlockNumber); - const address = this.#getCurrentAccount(); + const account = this.#getCurrentAccount(EVM_WILDCARD_CHAIN_ID); const currentChainId = this.#getChainId(); let remoteTransactions = []; @@ -152,7 +154,7 @@ export class IncomingTransactionHelper { try { remoteTransactions = await this.#remoteTransactionSource.fetchTransactions({ - address, + address: account.address, currentChainId, fromBlock, limit: this.#transactionLimit, @@ -165,7 +167,8 @@ export class IncomingTransactionHelper { } if (!this.#updateTransactions) { remoteTransactions = remoteTransactions.filter( - (tx) => tx.txParams.to?.toLowerCase() === address.toLowerCase(), + (tx) => + tx.txParams.to?.toLowerCase() === account.address.toLowerCase(), ); } @@ -301,7 +304,9 @@ export class IncomingTransactionHelper { #getBlockNumberKey(additionalKeys: string[]): string { const currentChainId = this.#getChainId(); - const currentAccount = this.#getCurrentAccount()?.toLowerCase(); + const currentAccount = this.#getCurrentAccount( + EVM_WILDCARD_CHAIN_ID, + )?.address.toLowerCase(); return [currentChainId, currentAccount, ...additionalKeys].join('#'); } From f541860b976e8e3e094962db153956f3dd1bd05a Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Tue, 21 May 2024 16:28:32 +0800 Subject: [PATCH 15/24] refactor: update nft controller to use account id --- .../src/NftController.test.ts | 566 ++++++++++++------ .../assets-controllers/src/NftController.ts | 213 +++++-- .../src/NftDetectionController.test.ts | 388 +++++++++--- .../src/NftDetectionController.ts | 76 ++- 4 files changed, 870 insertions(+), 373 deletions(-) diff --git a/packages/assets-controllers/src/NftController.test.ts b/packages/assets-controllers/src/NftController.test.ts index 2100dee04e4..05a99f75f04 100644 --- a/packages/assets-controllers/src/NftController.test.ts +++ b/packages/assets-controllers/src/NftController.test.ts @@ -1,4 +1,10 @@ import type { Network } from '@ethersproject/providers'; +import type { + AccountsControllerGetAccountAction, + AccountsControllerGetSelectedAccountAction, + AccountsControllerSelectedEvmAccountChangeEvent, +} from '@metamask/accounts-controller'; +import { createMockInternalAccount } from '@metamask/accounts-controller/src/tests/mocks'; import type { AddApprovalRequest, ApprovalStateChange, @@ -17,6 +23,7 @@ import { NetworksTicker, NFT_API_BASE_URL, } from '@metamask/controller-utils'; +import type { InternalAccount } from '@metamask/keyring-api'; import type { NetworkState, ProviderConfig, @@ -48,6 +55,11 @@ const ERC721_DEPRESSIONIST_ADDRESS = '0x18E8E76aeB9E2d9FA2A2b88DD9CF3C8ED45c3660'; const ERC721_DEPRESSIONIST_ID = '36'; const OWNER_ADDRESS = '0x5a3CA5cD63807Ce5e4d7841AB32Ce6B6d9BbBa2D'; +const OWNER_ID = '54d1e7bc-1dce-4220-a15f-2f454bae7869'; +const OWNER_ACCOUNT = createMockInternalAccount({ + id: OWNER_ID, + address: OWNER_ADDRESS, +}); const SECOND_OWNER_ADDRESS = '0x500017171kasdfbou081'; const DEPRESSIONIST_CID_V1 = @@ -70,8 +82,13 @@ const GOERLI = { ticker: NetworksTicker.goerli, }; -type ApprovalActions = AddApprovalRequest; -type ApprovalEvents = ApprovalStateChange; +type ApprovalActions = + | AddApprovalRequest + | AccountsControllerGetAccountAction + | AccountsControllerGetSelectedAccountAction; +type ApprovalEvents = + | ApprovalStateChange + | AccountsControllerSelectedEvmAccountChangeEvent; const controllerName = 'NftController' as const; @@ -122,6 +139,20 @@ function setupController( const messenger = new ControllerMessenger(); + const getInternalAccountMock = jest.fn().mockReturnValue(OWNER_ACCOUNT); + + messenger.registerActionHandler( + 'AccountsController:getAccount', + getInternalAccountMock, + ); + + const getSelectedAccountMock = jest.fn().mockReturnValue(OWNER_ACCOUNT); + + messenger.registerActionHandler( + 'AccountsController:getSelectedAccount', + getSelectedAccountMock, + ); + const approvalControllerMessenger = messenger.getRestricted({ name: 'ApprovalController', allowedActions: [], @@ -162,11 +193,19 @@ function setupController( const nftControllerMessenger = messenger.getRestricted< typeof controllerName, - ApprovalActions['type'] + ApprovalActions['type'], + Extract< + ApprovalEvents, + AccountsControllerSelectedEvmAccountChangeEvent + >['type'] >({ name: controllerName, - allowedActions: ['ApprovalController:addRequest'], - allowedEvents: [], + allowedActions: [ + 'ApprovalController:addRequest', + 'AccountsController:getSelectedAccount', + 'AccountsController:getAccount', + ], + allowedEvents: ['AccountsController:selectedEvmAccountChange'], }); const preferencesStateChangeListeners: ((state: PreferencesState) => void)[] = @@ -197,15 +236,28 @@ function setupController( triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, - selectedAddress: OWNER_ADDRESS, }); + const triggerSelectedAccountChange = ( + internalAccount: InternalAccount, + ): void => { + messenger.publish( + 'AccountsController:selectedEvmAccountChange', + internalAccount, + ); + }; + + triggerSelectedAccountChange(OWNER_ACCOUNT); + return { nftController, changeNetwork, messenger, approvalController, triggerPreferencesStateChange, + triggerSelectedAccountChange, + getInternalAccountMock, + getSelectedAccountMock, }; } @@ -392,12 +444,17 @@ describe('NftController', () => { getERC721OwnerOf: jest.fn().mockImplementation(() => '0x12345abcefg'), }); - const callActionSpy = jest.spyOn(messenger, 'call').mockResolvedValue({}); + const callActionSpy = jest.spyOn(messenger, 'call'); await expect(() => nftController.watchNft(ERC721_NFT, ERC721, 'https://test-dapp.com'), ).rejects.toThrow('Suggested NFT is not owned by the selected account'); - expect(callActionSpy).toHaveBeenCalledTimes(0); + // First call is getInternalAccount. Second call is the approval request. + expect(callActionSpy).not.toHaveBeenNthCalledWith( + 2, + 'ApprovalController:addRequest', + expect.any(Object), + ); }); it('should error if the call to isNftOwner fail', async function () { @@ -420,12 +477,13 @@ describe('NftController', () => { getERC1155BalanceOf: jest.fn().mockImplementation(() => new BN(0)), }); - const callActionSpy = jest.spyOn(messenger, 'call').mockResolvedValue({}); + const callActionSpy = jest.spyOn(messenger, 'call'); await expect(() => nftController.watchNft(ERC1155_NFT, ERC1155, 'https://test-dapp.com'), ).rejects.toThrow('Suggested NFT is not owned by the selected account'); - expect(callActionSpy).toHaveBeenCalledTimes(0); + // First call is to get InternalAccount + expect(callActionSpy).toHaveBeenCalledTimes(1); }); it('should handle ERC721 type and add pending request to ApprovalController with the OpenSea API disabled and IPFS gateway enabled', async function () { @@ -459,11 +517,17 @@ describe('NftController', () => { (v4 as jest.Mock).mockImplementationOnce(() => requestId); - const callActionSpy = jest.spyOn(messenger, 'call').mockResolvedValue({}); + const callActionSpy = jest + .spyOn(messenger, 'call') + .mockReturnValueOnce(OWNER_ACCOUNT) + .mockResolvedValueOnce({}) + .mockReturnValueOnce(OWNER_ACCOUNT); await nftController.watchNft(ERC721_NFT, ERC721, 'https://test-dapp.com'); - expect(callActionSpy).toHaveBeenCalledTimes(1); - expect(callActionSpy).toHaveBeenCalledWith( + // First call is getInternalAccount. Second call is the approval request. + expect(callActionSpy).toHaveBeenCalledTimes(3); + expect(callActionSpy).toHaveBeenNthCalledWith( + 2, 'ApprovalController:addRequest', { id: requestId, @@ -518,11 +582,17 @@ describe('NftController', () => { (v4 as jest.Mock).mockImplementationOnce(() => requestId); - const callActionSpy = jest.spyOn(messenger, 'call').mockResolvedValue({}); + const callActionSpy = jest + .spyOn(messenger, 'call') + .mockReturnValueOnce(OWNER_ACCOUNT) + .mockResolvedValueOnce({}) + .mockReturnValueOnce(OWNER_ACCOUNT); await nftController.watchNft(ERC721_NFT, ERC721, 'https://test-dapp.com'); - expect(callActionSpy).toHaveBeenCalledTimes(1); - expect(callActionSpy).toHaveBeenCalledWith( + // First call is getInternalAccount. Second call is the approval request. + expect(callActionSpy).toHaveBeenCalledTimes(3); + expect(callActionSpy).toHaveBeenNthCalledWith( + 2, 'ApprovalController:addRequest', { id: requestId, @@ -577,11 +647,17 @@ describe('NftController', () => { (v4 as jest.Mock).mockImplementationOnce(() => requestId); - const callActionSpy = jest.spyOn(messenger, 'call').mockResolvedValue({}); + const callActionSpy = jest + .spyOn(messenger, 'call') + .mockReturnValueOnce(OWNER_ACCOUNT) + .mockResolvedValueOnce({}) + .mockReturnValueOnce(OWNER_ACCOUNT); await nftController.watchNft(ERC721_NFT, ERC721, 'https://test-dapp.com'); - expect(callActionSpy).toHaveBeenCalledTimes(1); - expect(callActionSpy).toHaveBeenCalledWith( + // First call is getInternalAccount. Second call is the approval request. + expect(callActionSpy).toHaveBeenCalledTimes(3); + expect(callActionSpy).toHaveBeenNthCalledWith( + 2, 'ApprovalController:addRequest', { id: requestId, @@ -636,11 +712,17 @@ describe('NftController', () => { (v4 as jest.Mock).mockImplementationOnce(() => requestId); - const callActionSpy = jest.spyOn(messenger, 'call').mockResolvedValue({}); + const callActionSpy = jest + .spyOn(messenger, 'call') + .mockReturnValueOnce(OWNER_ACCOUNT) + .mockResolvedValueOnce({}) + .mockReturnValueOnce(OWNER_ACCOUNT); await nftController.watchNft(ERC721_NFT, ERC721, 'https://test-dapp.com'); - expect(callActionSpy).toHaveBeenCalledTimes(1); - expect(callActionSpy).toHaveBeenCalledWith( + // First call is getInternalAccount. Second call is the approval request. + expect(callActionSpy).toHaveBeenCalledTimes(3); + expect(callActionSpy).toHaveBeenNthCalledWith( + 2, 'ApprovalController:addRequest', { id: requestId, @@ -698,15 +780,21 @@ describe('NftController', () => { (v4 as jest.Mock).mockImplementationOnce(() => requestId); - const callActionSpy = jest.spyOn(messenger, 'call').mockResolvedValue({}); + const callActionSpy = jest + .spyOn(messenger, 'call') + .mockReturnValueOnce(OWNER_ACCOUNT) + .mockResolvedValueOnce({}) + .mockReturnValueOnce(OWNER_ACCOUNT); await nftController.watchNft( ERC1155_NFT, ERC1155, 'https://etherscan.io', ); - expect(callActionSpy).toHaveBeenCalledTimes(1); - expect(callActionSpy).toHaveBeenCalledWith( + // First call is getInternalAccount. Second call is the approval request. + expect(callActionSpy).toHaveBeenCalledTimes(3); + expect(callActionSpy).toHaveBeenNthCalledWith( + 2, 'ApprovalController:addRequest', { id: requestId, @@ -756,7 +844,6 @@ describe('NftController', () => { ...getDefaultPreferencesState(), isIpfsGatewayEnabled: true, openSeaEnabled: true, - selectedAddress: OWNER_ADDRESS, }); const requestId = 'approval-request-id-1'; @@ -764,15 +851,21 @@ describe('NftController', () => { (v4 as jest.Mock).mockImplementationOnce(() => requestId); - const callActionSpy = jest.spyOn(messenger, 'call').mockResolvedValue({}); + const callActionSpy = jest + .spyOn(messenger, 'call') + .mockReturnValueOnce(OWNER_ACCOUNT) + .mockResolvedValueOnce({}) + .mockReturnValue(OWNER_ACCOUNT); await nftController.watchNft( ERC1155_NFT, ERC1155, 'https://etherscan.io', ); - expect(callActionSpy).toHaveBeenCalledTimes(1); - expect(callActionSpy).toHaveBeenCalledWith( + // First call is getInternalAccount. Second call is the approval request. + expect(callActionSpy).toHaveBeenCalledTimes(3); + expect(callActionSpy).toHaveBeenNthCalledWith( + 2, 'ApprovalController:addRequest', { id: requestId, @@ -1022,7 +1115,7 @@ describe('NftController', () => { getERC721AssetName: jest.fn().mockResolvedValue('Name'), }); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft('0x01', '1', { nftMetadata: { name: 'name', @@ -1038,7 +1131,7 @@ describe('NftController', () => { }); expect( - nftController.state.allNfts[selectedAddress][chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual({ address: '0x01', description: 'description', @@ -1055,7 +1148,7 @@ describe('NftController', () => { }); expect( - nftController.state.allNftContracts[selectedAddress][chainId][0], + nftController.state.allNftContracts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual({ address: '0x01', logo: 'url', @@ -1117,33 +1210,44 @@ describe('NftController', () => { }); it('should add NFT by selected address', async () => { - const { nftController, triggerPreferencesStateChange } = - setupController(); + const { + nftController, + triggerPreferencesStateChange, + triggerSelectedAccountChange, + getInternalAccountMock, + } = setupController(); const { chainId } = nftController.config; const firstAddress = '0x123'; + const firstAccount = createMockInternalAccount({ address: firstAddress }); const secondAddress = '0x321'; + const secondAccount = createMockInternalAccount({ + address: secondAddress, + }); sinon // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any .stub(nftController, 'getNftInformation' as any) .returns({ name: 'name', image: 'url', description: 'description' }); + getInternalAccountMock.mockReturnValue(firstAccount); + triggerSelectedAccountChange(firstAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, - selectedAddress: firstAddress, }); await nftController.addNft('0x01', '1234'); + getInternalAccountMock.mockReturnValue(secondAccount); + triggerSelectedAccountChange(secondAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, - selectedAddress: secondAddress, }); await nftController.addNft('0x02', '4321'); + getInternalAccountMock.mockReturnValue(firstAccount); + triggerSelectedAccountChange(firstAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, - selectedAddress: firstAddress, }); expect( nftController.state.allNfts[firstAddress][chainId][0], @@ -1160,7 +1264,7 @@ describe('NftController', () => { it('should update NFT if image is different', async () => { const { nftController } = setupController(); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft('0x01', '1', { nftMetadata: { @@ -1173,7 +1277,7 @@ describe('NftController', () => { }); expect( - nftController.state.allNfts[selectedAddress][chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual({ address: '0x01', description: 'description', @@ -1196,7 +1300,7 @@ describe('NftController', () => { }); expect( - nftController.state.allNfts[selectedAddress][chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual({ address: '0x01', description: 'description', @@ -1211,7 +1315,7 @@ describe('NftController', () => { it('should not duplicate NFT nor NFT contract if already added', async () => { const { nftController } = setupController(); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft('0x01', '1', { nftMetadata: { name: 'name', @@ -1233,11 +1337,11 @@ describe('NftController', () => { }); expect( - nftController.state.allNfts[selectedAddress][chainId], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId], ).toHaveLength(1); expect( - nftController.state.allNftContracts[selectedAddress][chainId], + nftController.state.allNftContracts[OWNER_ACCOUNT.address][chainId], ).toHaveLength(1); }); @@ -1251,10 +1355,10 @@ describe('NftController', () => { .mockRejectedValue(new Error('Not an ERC1155 contract')), }); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft('0x01', '1'); expect( - nftController.state.allNfts[selectedAddress][chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual({ address: '0x01', description: 'Description', @@ -1305,12 +1409,12 @@ describe('NftController', () => { description: 'Kudos Description (directly from tokenURI)', }); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft(ERC721_KUDOSADDRESS, ERC721_KUDOS_TOKEN_ID); expect( - nftController.state.allNfts[selectedAddress][chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual({ address: ERC721_KUDOSADDRESS, image: 'Kudos Image (directly from tokenURI)', @@ -1325,7 +1429,7 @@ describe('NftController', () => { }); expect( - nftController.state.allNftContracts[selectedAddress][chainId][0], + nftController.state.allNftContracts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual({ address: ERC721_KUDOSADDRESS, name: 'KudosToken', @@ -1356,11 +1460,11 @@ describe('NftController', () => { image: 'image (directly from tokenURI)', animation_url: null, }); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft(ERC1155_NFT_ADDRESS, ERC1155_NFT_ID); expect( - nftController.state.allNfts[selectedAddress][chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual({ address: ERC1155_NFT_ADDRESS, image: 'image (directly from tokenURI)', @@ -1395,7 +1499,7 @@ describe('NftController', () => { name: 'Kudos Name (directly from tokenURI)', description: 'Kudos Description (directly from tokenURI)', }); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; sinon // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -1405,7 +1509,7 @@ describe('NftController', () => { await nftController.addNft(ERC721_KUDOSADDRESS, ERC721_KUDOS_TOKEN_ID); expect( - nftController.state.allNfts[selectedAddress][chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual({ address: ERC721_KUDOSADDRESS, image: 'Kudos Image (directly from tokenURI)', @@ -1420,7 +1524,7 @@ describe('NftController', () => { }); expect( - nftController.state.allNftContracts[selectedAddress][chainId][0], + nftController.state.allNftContracts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual({ address: ERC721_KUDOSADDRESS, name: 'KudosToken', @@ -1431,7 +1535,6 @@ describe('NftController', () => { it('should add NFT by provider type', async () => { const { nftController, changeNetwork } = setupController(); - const { selectedAddress } = nftController.config; sinon // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -1444,11 +1547,15 @@ describe('NftController', () => { changeNetwork(SEPOLIA); expect( - nftController.state.allNfts[selectedAddress]?.[ChainId[GOERLI.type]], + nftController.state.allNfts[OWNER_ACCOUNT.address]?.[ + ChainId[GOERLI.type] + ], ).toBeUndefined(); expect( - nftController.state.allNfts[selectedAddress][ChainId[SEPOLIA.type]][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][ + ChainId[SEPOLIA.type] + ][0], ).toStrictEqual({ address: '0x01', description: 'description', @@ -1465,7 +1572,7 @@ describe('NftController', () => { const { nftController } = setupController({ onNftAdded: mockOnNftAdded, }); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any sinon.stub(nftController, 'getNftContractInformation' as any).returns({ @@ -1490,7 +1597,7 @@ describe('NftController', () => { await nftController.addNft('0x01234abcdefg', '1234'); expect(nftController.state.allNftContracts).toStrictEqual({ - [selectedAddress]: { + [OWNER_ACCOUNT.address]: { [chainId]: [ { address: '0x01234abcdefg', @@ -1500,7 +1607,7 @@ describe('NftController', () => { }); expect(nftController.state.allNfts).toStrictEqual({ - [selectedAddress]: { + [OWNER_ACCOUNT.address]: { [chainId]: [ { address: '0x01234abcdefg', @@ -1640,31 +1747,31 @@ describe('NftController', () => { image_url: 'Kudos logo (from proxy API)', }); */ - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft( '0x6EbeAf8e8E946F0716E6533A6f2cefc83f60e8Ab', '123', { - userAddress: selectedAddress, + userAddress: OWNER_ACCOUNT.address, source: Source.Detected, }, ); expect( - nftController.state.allNfts[selectedAddress]?.[chainId], + nftController.state.allNfts[OWNER_ACCOUNT.address]?.[chainId], ).toBeUndefined(); expect( - nftController.state.allNftContracts[selectedAddress]?.[chainId], + nftController.state.allNftContracts[OWNER_ACCOUNT.address]?.[chainId], ).toBeUndefined(); await nftController.addNft(ERC721_KUDOSADDRESS, ERC721_KUDOS_TOKEN_ID, { - userAddress: selectedAddress, + userAddress: OWNER_ACCOUNT.address, source: Source.Detected, }); expect( - nftController.state.allNfts[selectedAddress][chainId], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId], ).toStrictEqual([ { address: ERC721_KUDOSADDRESS, @@ -1685,7 +1792,7 @@ describe('NftController', () => { ]); expect( - nftController.state.allNftContracts[selectedAddress][chainId], + nftController.state.allNftContracts[OWNER_ACCOUNT.address][chainId], ).toStrictEqual([ { address: ERC721_KUDOSADDRESS, @@ -1721,18 +1828,16 @@ describe('NftController', () => { ) .replyWithError(new Error('Failed to fetch')); - const { selectedAddress } = nftController.config; - await nftController.addNft( '0x6EbeAf8e8E946F0716E6533A6f2cefc83f60e8Ab', '123', { - userAddress: selectedAddress, + userAddress: OWNER_ACCOUNT.address, source: Source.Detected, }, ); await nftController.addNft(ERC721_KUDOSADDRESS, ERC721_KUDOS_TOKEN_ID, { - userAddress: selectedAddress, + userAddress: OWNER_ACCOUNT.address, source: Source.Detected, }); @@ -1743,7 +1848,7 @@ describe('NftController', () => { it('should not add duplicate NFTs to the ignoredNfts list', async () => { const { nftController } = setupController(); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft('0x01', '1', { nftMetadata: { @@ -1764,13 +1869,13 @@ describe('NftController', () => { }); expect( - nftController.state.allNfts[selectedAddress][chainId], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId], ).toHaveLength(2); expect(nftController.state.ignoredNfts).toHaveLength(0); nftController.removeAndIgnoreNft('0x01', '1'); expect( - nftController.state.allNfts[selectedAddress][chainId], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId], ).toHaveLength(1); expect(nftController.state.ignoredNfts).toHaveLength(1); @@ -1784,13 +1889,13 @@ describe('NftController', () => { }); expect( - nftController.state.allNfts[selectedAddress][chainId], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId], ).toHaveLength(2); expect(nftController.state.ignoredNfts).toHaveLength(1); nftController.removeAndIgnoreNft('0x01', '1'); expect( - nftController.state.allNfts[selectedAddress][chainId], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId], ).toHaveLength(1); expect(nftController.state.ignoredNfts).toHaveLength(1); }); @@ -1816,7 +1921,7 @@ describe('NftController', () => { nftController.configure({ ipfsGateway: IPFS_DEFAULT_GATEWAY_URL, }); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft( ERC721_DEPRESSIONIST_ADDRESS, @@ -1824,7 +1929,7 @@ describe('NftController', () => { ); expect( - nftController.state.allNftContracts[selectedAddress][chainId][0], + nftController.state.allNftContracts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual({ address: ERC721_DEPRESSIONIST_ADDRESS, name: "Maltjik.jpg's Depressionists", @@ -1832,7 +1937,7 @@ describe('NftController', () => { schemaName: 'ERC721', }); expect( - nftController.state.allNfts[selectedAddress][chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual({ address: ERC721_DEPRESSIONIST_ADDRESS, tokenId: '36', @@ -1855,12 +1960,12 @@ describe('NftController', () => { ) .replyWithError(new Error('Failed to fetch')); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft(ERC721_NFT_ADDRESS, ERC721_NFT_ID); expect( - nftController.state.allNfts[selectedAddress][chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual({ address: ERC721_NFT_ADDRESS, image: null, @@ -2105,10 +2210,22 @@ describe('NftController', () => { describe('addNftVerifyOwnership', () => { it('should verify ownership by selected address and add NFT', async () => { - const { nftController, triggerPreferencesStateChange } = - setupController(); + const { + nftController, + getInternalAccountMock, + triggerPreferencesStateChange, + triggerSelectedAccountChange, + } = setupController(); const firstAddress = '0x123'; + const firstAccount = createMockInternalAccount({ + address: firstAddress, + id: '22c022b5-309c-45e4-a82d-64bb11fc0e74', + }); const secondAddress = '0x321'; + const secondAccount = createMockInternalAccount({ + address: secondAddress, + id: 'f9a42417-6071-4b51-8ecd-f7b14abd8851', + }); const { chainId } = nftController.config; // TODO: Replace `any` with type @@ -2120,25 +2237,28 @@ describe('NftController', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any .stub(nftController, 'getNftInformation' as any) .returns({ name: 'name', image: 'url', description: 'description' }); + getInternalAccountMock.mockReturnValue(firstAccount); + triggerSelectedAccountChange(firstAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, - selectedAddress: firstAddress, }); await nftController.addNftVerifyOwnership('0x01', '1234'); + getInternalAccountMock.mockReturnValue(secondAccount); + triggerSelectedAccountChange(secondAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, - selectedAddress: secondAddress, }); await nftController.addNftVerifyOwnership('0x02', '4321'); + getInternalAccountMock.mockReturnValue(firstAccount); + triggerSelectedAccountChange(firstAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, - selectedAddress: firstAddress, }); expect( - nftController.state.allNfts[firstAddress][chainId][0], + nftController.state.allNfts[firstAccount.address][chainId][0], ).toStrictEqual({ address: '0x01', description: 'description', @@ -2151,16 +2271,25 @@ describe('NftController', () => { }); it('should throw an error if selected address is not owner of input NFT', async () => { - const { nftController, triggerPreferencesStateChange } = - setupController(); + const { + nftController, + getInternalAccountMock, + triggerPreferencesStateChange, + triggerSelectedAccountChange, + } = setupController(); // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any sinon.stub(nftController, 'isNftOwner' as any).returns(false); const firstAddress = '0x123'; + const firstAccount = createMockInternalAccount({ + address: firstAddress, + id: '22c022b5-309c-45e4-a82d-64bb11fc0e74', + }); + getInternalAccountMock.mockReturnValue(firstAccount); + triggerSelectedAccountChange(firstAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, - selectedAddress: firstAddress, }); const result = async () => await nftController.addNftVerifyOwnership('0x01', '1234'); @@ -2169,11 +2298,23 @@ describe('NftController', () => { }); it('should verify ownership by selected address and add NFT by the correct chainId when passed networkClientId', async () => { - const { nftController, triggerPreferencesStateChange } = - setupController(); + const { + nftController, + getInternalAccountMock, + triggerSelectedAccountChange, + triggerPreferencesStateChange, + } = setupController(); const firstAddress = '0x123'; + const firstAccount = createMockInternalAccount({ + address: firstAddress, + id: '22c022b5-309c-45e4-a82d-64bb11fc0e74', + }); const secondAddress = '0x321'; + const secondAccount = createMockInternalAccount({ + address: secondAddress, + id: 'f9a42417-6071-4b51-8ecd-f7b14abd8851', + }); // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -2184,18 +2325,20 @@ describe('NftController', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any .stub(nftController, 'getNftInformation' as any) .returns({ name: 'name', image: 'url', description: 'description' }); + getInternalAccountMock.mockReturnValue(firstAccount); + triggerSelectedAccountChange(firstAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, - selectedAddress: firstAddress, }); await nftController.addNftVerifyOwnership('0x01', '1234', { networkClientId: 'sepolia', }); + getInternalAccountMock.mockReturnValue(secondAccount); + triggerSelectedAccountChange(secondAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, - selectedAddress: secondAddress, }); await nftController.addNftVerifyOwnership('0x02', '4321', { networkClientId: 'goerli', @@ -2284,7 +2427,7 @@ describe('NftController', () => { describe('removeNft', () => { it('should remove NFT and NFT contract', async () => { const { nftController } = setupController(); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft('0x01', '1', { nftMetadata: { @@ -2296,17 +2439,17 @@ describe('NftController', () => { }); nftController.removeNft('0x01', '1'); expect( - nftController.state.allNfts[selectedAddress][chainId], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId], ).toHaveLength(0); expect( - nftController.state.allNftContracts[selectedAddress][chainId], + nftController.state.allNftContracts[OWNER_ACCOUNT.address][chainId], ).toHaveLength(0); }); it('should not remove NFT contract if NFT still exists', async () => { const { nftController } = setupController(); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft('0x01', '1', { nftMetadata: { @@ -2327,17 +2470,21 @@ describe('NftController', () => { }); nftController.removeNft('0x01', '1'); expect( - nftController.state.allNfts[selectedAddress][chainId], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId], ).toHaveLength(1); expect( - nftController.state.allNftContracts[selectedAddress][chainId], + nftController.state.allNftContracts[OWNER_ACCOUNT.address][chainId], ).toHaveLength(1); }); it('should remove NFT by selected address', async () => { - const { nftController, triggerPreferencesStateChange } = - setupController(); + const { + nftController, + getInternalAccountMock, + triggerSelectedAccountChange, + triggerPreferencesStateChange, + } = setupController(); const { chainId } = nftController.config; sinon // TODO: Replace `any` with type @@ -2345,27 +2492,38 @@ describe('NftController', () => { .stub(nftController, 'getNftInformation' as any) .returns({ name: 'name', image: 'url', description: 'description' }); const firstAddress = '0x123'; + const firstAccount = createMockInternalAccount({ + address: firstAddress, + id: '22c022b5-309c-45e4-a82d-64bb11fc0e74', + }); const secondAddress = '0x321'; + const secondAccount = createMockInternalAccount({ + address: secondAddress, + id: 'f9a42417-6071-4b51-8ecd-f7b14abd8851', + }); + getInternalAccountMock.mockReturnValue(firstAccount); + triggerSelectedAccountChange(firstAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, - selectedAddress: firstAddress, }); await nftController.addNft('0x02', '4321'); + getInternalAccountMock.mockReturnValue(secondAccount); + triggerSelectedAccountChange(secondAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, - selectedAddress: secondAddress, }); await nftController.addNft('0x01', '1234'); nftController.removeNft('0x01', '1234'); expect(nftController.state.allNfts[secondAddress][chainId]).toHaveLength( 0, ); + getInternalAccountMock.mockReturnValue(firstAccount); + triggerSelectedAccountChange(firstAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, - selectedAddress: firstAddress, }); expect( nftController.state.allNfts[firstAddress][chainId][0], @@ -2382,7 +2540,6 @@ describe('NftController', () => { it('should remove NFT by provider type', async () => { const { nftController, changeNetwork } = setupController(); - const { selectedAddress } = nftController.config; sinon // TODO: Replace `any` with type @@ -2395,13 +2552,13 @@ describe('NftController', () => { await nftController.addNft('0x01', '1234'); nftController.removeNft('0x01', '1234'); expect( - nftController.state.allNfts[selectedAddress][GOERLI.chainId], + nftController.state.allNfts[OWNER_ACCOUNT.address][GOERLI.chainId], ).toHaveLength(0); changeNetwork(SEPOLIA); expect( - nftController.state.allNfts[selectedAddress][SEPOLIA.chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][SEPOLIA.chainId][0], ).toStrictEqual({ address: '0x02', description: 'description', @@ -2414,17 +2571,31 @@ describe('NftController', () => { }); it('should remove correct NFT and NFT contract when passed networkClientId and userAddress in options', async () => { - const { nftController, changeNetwork, triggerPreferencesStateChange } = - setupController(); + const { + nftController, + changeNetwork, + triggerPreferencesStateChange, + triggerSelectedAccountChange, + getInternalAccountMock, + } = setupController(); const userAddress1 = '0x123'; + const userAccount1 = createMockInternalAccount({ + address: userAddress1, + id: '5fd59cae-95d3-4a1d-ba97-657c8f83c300', + }); const userAddress2 = '0x321'; + const userAccount2 = createMockInternalAccount({ + address: userAddress2, + id: '9ea40063-a95c-4f79-a4b6-0c065549245e', + }); changeNetwork(SEPOLIA); + getInternalAccountMock.mockReturnValue(userAccount1); + triggerSelectedAccountChange(userAccount1); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, - selectedAddress: userAddress1, }); await nftController.addNft('0x01', '1', { @@ -2450,10 +2621,11 @@ describe('NftController', () => { }); changeNetwork(GOERLI); + getInternalAccountMock.mockReturnValue(userAccount2); + triggerSelectedAccountChange(userAccount2); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, - selectedAddress: userAddress2, }); // now remove the nft after changing to a different network and account from the one where it was added @@ -2474,7 +2646,7 @@ describe('NftController', () => { it('should be able to clear the ignoredNfts list', async () => { const { nftController } = setupController(); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft('0x02', '1', { nftMetadata: { @@ -2486,15 +2658,15 @@ describe('NftController', () => { }, }); - expect(nftController.state.allNfts[selectedAddress][chainId]).toHaveLength( - 1, - ); + expect( + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId], + ).toHaveLength(1); expect(nftController.state.ignoredNfts).toHaveLength(0); nftController.removeAndIgnoreNft('0x02', '1'); - expect(nftController.state.allNfts[selectedAddress][chainId]).toHaveLength( - 0, - ); + expect( + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId], + ).toHaveLength(0); expect(nftController.state.ignoredNfts).toHaveLength(1); nftController.clearIgnoredNfts(); @@ -2633,12 +2805,12 @@ describe('NftController', () => { .stub(nftController, 'getNftURIAndStandard' as any) .returns(['ipfs://*', ERC1155]); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft(ERC1155_NFT_ADDRESS, ERC1155_NFT_ID); expect( - nftController.state.allNfts[selectedAddress][chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual({ address: ERC1155_NFT_ADDRESS, name: null, @@ -2656,7 +2828,7 @@ describe('NftController', () => { describe('updateNftFavoriteStatus', () => { it('should not set NFT as favorite if nft not found', async () => { const { nftController } = setupController(); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft( ERC721_DEPRESSIONIST_ADDRESS, ERC721_DEPRESSIONIST_ID, @@ -2670,7 +2842,7 @@ describe('NftController', () => { ); expect( - nftController.state.allNfts[selectedAddress][chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual( expect.objectContaining({ address: ERC721_DEPRESSIONIST_ADDRESS, @@ -2681,7 +2853,7 @@ describe('NftController', () => { }); it('should set NFT as favorite', async () => { const { nftController } = setupController(); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft( ERC721_DEPRESSIONIST_ADDRESS, ERC721_DEPRESSIONIST_ID, @@ -2695,7 +2867,7 @@ describe('NftController', () => { ); expect( - nftController.state.allNfts[selectedAddress][chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual( expect.objectContaining({ address: ERC721_DEPRESSIONIST_ADDRESS, @@ -2707,7 +2879,7 @@ describe('NftController', () => { it('should set NFT as favorite and then unset it', async () => { const { nftController } = setupController(); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft( ERC721_DEPRESSIONIST_ADDRESS, ERC721_DEPRESSIONIST_ID, @@ -2721,7 +2893,7 @@ describe('NftController', () => { ); expect( - nftController.state.allNfts[selectedAddress][chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual( expect.objectContaining({ address: ERC721_DEPRESSIONIST_ADDRESS, @@ -2737,7 +2909,7 @@ describe('NftController', () => { ); expect( - nftController.state.allNfts[selectedAddress][chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual( expect.objectContaining({ address: ERC721_DEPRESSIONIST_ADDRESS, @@ -2749,7 +2921,7 @@ describe('NftController', () => { it('should keep the favorite status as true after updating metadata', async () => { const { nftController } = setupController(); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft( ERC721_DEPRESSIONIST_ADDRESS, ERC721_DEPRESSIONIST_ID, @@ -2763,7 +2935,7 @@ describe('NftController', () => { ); expect( - nftController.state.allNfts[selectedAddress][chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual( expect.objectContaining({ address: ERC721_DEPRESSIONIST_ADDRESS, @@ -2786,7 +2958,7 @@ describe('NftController', () => { ); expect( - nftController.state.allNfts[selectedAddress][chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual( expect.objectContaining({ image: 'new_image', @@ -2800,13 +2972,13 @@ describe('NftController', () => { ); expect( - nftController.state.allNfts[selectedAddress][chainId], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId], ).toHaveLength(1); }); it('should keep the favorite status as false after updating metadata', async () => { const { nftController } = setupController(); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft( ERC721_DEPRESSIONIST_ADDRESS, ERC721_DEPRESSIONIST_ID, @@ -2814,7 +2986,7 @@ describe('NftController', () => { ); expect( - nftController.state.allNfts[selectedAddress][chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual( expect.objectContaining({ address: ERC721_DEPRESSIONIST_ADDRESS, @@ -2837,7 +3009,7 @@ describe('NftController', () => { ); expect( - nftController.state.allNfts[selectedAddress][chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual( expect.objectContaining({ image: 'new_image', @@ -2851,22 +3023,36 @@ describe('NftController', () => { ); expect( - nftController.state.allNfts[selectedAddress][chainId], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId], ).toHaveLength(1); }); it('should set NFT as favorite when passed networkClientId and userAddress in options', async () => { - const { nftController, triggerPreferencesStateChange, changeNetwork } = - setupController(); + const { + nftController, + triggerPreferencesStateChange, + changeNetwork, + triggerSelectedAccountChange, + getInternalAccountMock, + } = setupController(); const userAddress1 = '0x123'; + const userAccount1 = createMockInternalAccount({ + address: userAddress1, + id: '0a2a9a41-2b35-4863-8f36-baceec4e9686', + }); const userAddress2 = '0x321'; + const userAccount2 = createMockInternalAccount({ + address: userAddress2, + id: '09b239a4-c229-4a2b-9739-1cb4b9dea7b9', + }); changeNetwork(SEPOLIA); + getInternalAccountMock.mockReturnValue(userAccount1); + triggerSelectedAccountChange(userAccount1); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, - selectedAddress: userAddress1, }); await nftController.addNft( @@ -2876,7 +3062,7 @@ describe('NftController', () => { ); expect( - nftController.state.allNfts[userAddress1][SEPOLIA.chainId][0], + nftController.state.allNfts[userAccount1.address][SEPOLIA.chainId][0], ).toStrictEqual( expect.objectContaining({ address: ERC721_DEPRESSIONIST_ADDRESS, @@ -2886,10 +3072,11 @@ describe('NftController', () => { ); changeNetwork(GOERLI); + getInternalAccountMock.mockReturnValue(userAccount2); + triggerSelectedAccountChange(userAccount2); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, - selectedAddress: userAddress2, }); // now favorite the nft after changing to a different account from the one where it was added @@ -2899,12 +3086,12 @@ describe('NftController', () => { true, { networkClientId: SEPOLIA.type, - userAddress: userAddress1, + userAddress: userAccount1.address, }, ); expect( - nftController.state.allNfts[userAddress1][SEPOLIA.chainId][0], + nftController.state.allNfts[userAccount1.address][SEPOLIA.chainId][0], ).toStrictEqual( expect.objectContaining({ address: ERC721_DEPRESSIONIST_ADDRESS, @@ -2923,7 +3110,7 @@ describe('NftController', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any sinon.stub(nftController, 'isNftOwner' as any).returns(false); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft('0x02', '1', { nftMetadata: { name: 'name', @@ -2935,13 +3122,13 @@ describe('NftController', () => { }); expect( - nftController.state.allNfts[selectedAddress][chainId][0] + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0] .isCurrentlyOwned, ).toBe(true); await nftController.checkAndUpdateAllNftsOwnershipStatus(); expect( - nftController.state.allNfts[selectedAddress][chainId][0] + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0] .isCurrentlyOwned, ).toBe(false); }); @@ -2952,7 +3139,7 @@ describe('NftController', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any sinon.stub(nftController, 'isNftOwner' as any).returns(true); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft('0x02', '1', { nftMetadata: { name: 'name', @@ -2964,13 +3151,13 @@ describe('NftController', () => { }); expect( - nftController.state.allNfts[selectedAddress][chainId][0] + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0] .isCurrentlyOwned, ).toBe(true); await nftController.checkAndUpdateAllNftsOwnershipStatus(); expect( - nftController.state.allNfts[selectedAddress][chainId][0] + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0] .isCurrentlyOwned, ).toBe(true); }); @@ -2983,7 +3170,7 @@ describe('NftController', () => { .stub(nftController, 'isNftOwner' as any) .throws(new Error('Unable to verify ownership')); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft('0x02', '1', { nftMetadata: { name: 'name', @@ -2995,13 +3182,13 @@ describe('NftController', () => { }); expect( - nftController.state.allNfts[selectedAddress][chainId][0] + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0] .isCurrentlyOwned, ).toBe(true); await nftController.checkAndUpdateAllNftsOwnershipStatus(); expect( - nftController.state.allNfts[selectedAddress][chainId][0] + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0] .isCurrentlyOwned, ).toBe(true); }); @@ -3013,11 +3200,10 @@ describe('NftController', () => { triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, - selectedAddress: OWNER_ADDRESS, }); changeNetwork(SEPOLIA); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; await nftController.addNft('0x02', '1', { nftMetadata: { name: 'name', @@ -3029,7 +3215,7 @@ describe('NftController', () => { }); expect( - nftController.state.allNfts[selectedAddress][chainId][0] + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0] .isCurrentlyOwned, ).toBe(true); @@ -3040,7 +3226,6 @@ describe('NftController', () => { triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, - selectedAddress: SECOND_OWNER_ADDRESS, }); changeNetwork(GOERLI); @@ -3057,9 +3242,9 @@ describe('NftController', () => { }); describe('checkAndUpdateSingleNftOwnershipStatus', () => { - it('should check whether the passed NFT is still owned by the the current selectedAddress/chainId combination and update its isCurrentlyOwned property in state if batch is false and isNftOwner returns false', async () => { + it('should check whether the passed NFT is still owned by the the current address of the selected account/chainId combination and update its isCurrentlyOwned property in state if batch is false and isNftOwner returns false', async () => { const { nftController } = setupController(); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; const nft = { address: '0x02', tokenId: '1', @@ -3075,7 +3260,7 @@ describe('NftController', () => { }); expect( - nftController.state.allNfts[selectedAddress][chainId][0] + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0] .isCurrentlyOwned, ).toBe(true); @@ -3086,14 +3271,14 @@ describe('NftController', () => { await nftController.checkAndUpdateSingleNftOwnershipStatus(nft, false); expect( - nftController.state.allNfts[selectedAddress][chainId][0] + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0] .isCurrentlyOwned, ).toBe(false); }); it('should check whether the passed NFT is still owned by the the current selectedAddress/chainId combination and return the updated NFT object without updating state if batch is true', async () => { const { nftController } = setupController(); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; const nft = { address: '0x02', tokenId: '1', @@ -3109,7 +3294,7 @@ describe('NftController', () => { }); expect( - nftController.state.allNfts[selectedAddress][chainId][0] + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0] .isCurrentlyOwned, ).toBe(true); @@ -3121,11 +3306,11 @@ describe('NftController', () => { await nftController.checkAndUpdateSingleNftOwnershipStatus(nft, true); expect( - nftController.state.allNfts[selectedAddress][chainId][0] + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0] .isCurrentlyOwned, ).toBe(true); - expect(updatedNft.isCurrentlyOwned).toBe(false); + expect(updatedNft?.isCurrentlyOwned).toBe(false); }); it('should check whether the passed NFT is still owned by the the selectedAddress/chainId combination passed in the accountParams argument and update its isCurrentlyOwned property in state, when the currently configured selectedAddress/chainId are different from those passed', async () => { @@ -3139,7 +3324,7 @@ describe('NftController', () => { }); changeNetwork(SEPOLIA); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; const nft = { address: '0x02', tokenId: '1', @@ -3155,7 +3340,7 @@ describe('NftController', () => { }); expect( - nftController.state.allNfts[selectedAddress][chainId][0] + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0] .isCurrentlyOwned, ).toBe(true); @@ -3192,7 +3377,7 @@ describe('NftController', () => { }); changeNetwork(SEPOLIA); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; const nft = { address: '0x02', tokenId: '1', @@ -3208,7 +3393,7 @@ describe('NftController', () => { }); expect( - nftController.state.allNfts[selectedAddress][chainId][0] + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0] .isCurrentlyOwned, ).toBe(true); @@ -3257,14 +3442,14 @@ describe('NftController', () => { favorite: false, }; const { nftController } = setupController(); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; it('should return null if the NFT does not exist in the state', async () => { expect( nftController.findNftByAddressAndTokenId( mockNft.address, mockNft.tokenId, - selectedAddress, + OWNER_ACCOUNT.address, chainId, ), ).toBeNull(); @@ -3272,14 +3457,14 @@ describe('NftController', () => { it('should return the NFT by the address and tokenId', () => { nftController.state.allNfts = { - [selectedAddress]: { [chainId]: [mockNft] }, + [OWNER_ACCOUNT.address]: { [chainId]: [mockNft] }, }; expect( nftController.findNftByAddressAndTokenId( mockNft.address, mockNft.tokenId, - selectedAddress, + OWNER_ACCOUNT.address, chainId, ), ).toStrictEqual({ nft: mockNft, index: 0 }); @@ -3311,11 +3496,11 @@ describe('NftController', () => { transactionId: mockTransactionId, }; - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; it('should update the NFT if the NFT exist', async () => { nftController.state.allNfts = { - [selectedAddress]: { [chainId]: [mockNft] }, + [OWNER_ACCOUNT.address]: { [chainId]: [mockNft] }, }; nftController.updateNft( @@ -3323,12 +3508,12 @@ describe('NftController', () => { { transactionId: mockTransactionId, }, - selectedAddress, + OWNER_ACCOUNT.address, chainId, ); expect( - nftController.state.allNfts[selectedAddress][chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0], ).toStrictEqual(expectedMockNft); }); @@ -3339,7 +3524,7 @@ describe('NftController', () => { { transactionId: mockTransactionId, }, - selectedAddress, + OWNER_ACCOUNT.address, chainId, ), ).toBeUndefined(); @@ -3363,13 +3548,13 @@ describe('NftController', () => { transactionId: mockTransactionId, }; - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; it('should not update any NFT state and should return false when passed a transaction id that does not match that of any NFT', async () => { expect( nftController.resetNftTransactionStatusByTransactionId( nonExistTransactionId, - selectedAddress, + OWNER_ACCOUNT.address, chainId, ), ).toBe(false); @@ -3377,23 +3562,25 @@ describe('NftController', () => { it('should set the transaction id of an NFT in state to undefined, and return true when it has successfully updated this state', async () => { nftController.state.allNfts = { - [selectedAddress]: { [chainId]: [mockNft] }, + [OWNER_ACCOUNT.address]: { [chainId]: [mockNft] }, }; expect( - nftController.state.allNfts[selectedAddress][chainId][0].transactionId, + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0] + .transactionId, ).toBe(mockTransactionId); expect( nftController.resetNftTransactionStatusByTransactionId( mockTransactionId, - selectedAddress, + OWNER_ACCOUNT.address, chainId, ), ).toBe(true); expect( - nftController.state.allNfts[selectedAddress][chainId][0].transactionId, + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0] + .transactionId, ).toBeUndefined(); }); }); @@ -3401,7 +3588,6 @@ describe('NftController', () => { describe('updateNftMetadata', () => { it('should update Nft metadata successfully', async () => { const { nftController } = setupController(); - const { selectedAddress } = nftController.config; const spy = jest.spyOn(nftController, 'updateNft'); const testNetworkClientId = 'sepolia'; await nftController.addNft('0xtest', '3', { @@ -3437,7 +3623,7 @@ describe('NftController', () => { expect(spy).toHaveBeenCalledTimes(1); expect( - nftController.state.allNfts[selectedAddress][SEPOLIA.chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][SEPOLIA.chainId][0], ).toStrictEqual({ address: '0xtest', description: 'description pudgy', @@ -3453,7 +3639,6 @@ describe('NftController', () => { it('should not update metadata when state nft and fetched nft are the same', async () => { const { nftController } = setupController(); - const { selectedAddress } = nftController.config; const spy = jest.spyOn(nftController, 'updateNft'); const testNetworkClientId = 'sepolia'; await nftController.addNft('0xtest', '3', { @@ -3493,7 +3678,7 @@ describe('NftController', () => { expect(spy).toHaveBeenCalledTimes(0); expect( - nftController.state.allNfts[selectedAddress][SEPOLIA.chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][SEPOLIA.chainId][0], ).toStrictEqual({ address: '0xtest', description: 'description', @@ -3508,7 +3693,6 @@ describe('NftController', () => { it('should trigger update metadata when state nft and fetched nft are not the same', async () => { const { nftController } = setupController(); - const { selectedAddress } = nftController.config; const spy = jest.spyOn(nftController, 'updateNft'); const testNetworkClientId = 'sepolia'; await nftController.addNft('0xtest', '3', { @@ -3548,7 +3732,7 @@ describe('NftController', () => { expect(spy).toHaveBeenCalledTimes(1); expect( - nftController.state.allNfts[selectedAddress][SEPOLIA.chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][SEPOLIA.chainId][0], ).toStrictEqual({ address: '0xtest', description: 'description', @@ -3563,7 +3747,6 @@ describe('NftController', () => { it('should not update metadata when calls to fetch metadata fail', async () => { const { nftController } = setupController(); - const { selectedAddress } = nftController.config; const spy = jest.spyOn(nftController, 'updateNft'); const testNetworkClientId = 'sepolia'; await nftController.addNft('0xtest', '3', { @@ -3599,7 +3782,7 @@ describe('NftController', () => { expect(spy).toHaveBeenCalledTimes(0); expect( - nftController.state.allNfts[selectedAddress][SEPOLIA.chainId][0], + nftController.state.allNfts[OWNER_ACCOUNT.address][SEPOLIA.chainId][0], ).toStrictEqual({ address: '0xtest', description: '', @@ -3614,7 +3797,6 @@ describe('NftController', () => { it('should update metadata when some calls to fetch metadata succeed', async () => { const { nftController } = setupController(); - const { selectedAddress } = nftController.config; const spy = jest.spyOn(nftController, 'updateNft'); const testNetworkClientId = 'sepolia'; // Add nfts @@ -3705,7 +3887,7 @@ describe('NftController', () => { expect(spy).toHaveBeenCalledTimes(2); expect( - nftController.state.allNfts[selectedAddress][SEPOLIA.chainId], + nftController.state.allNfts[OWNER_ACCOUNT.address][SEPOLIA.chainId], ).toStrictEqual([ { address: '0xtest1', diff --git a/packages/assets-controllers/src/NftController.ts b/packages/assets-controllers/src/NftController.ts index 580f1a7c39d..34354f2421c 100644 --- a/packages/assets-controllers/src/NftController.ts +++ b/packages/assets-controllers/src/NftController.ts @@ -1,4 +1,9 @@ import { isAddress } from '@ethersproject/address'; +import { + type AccountsControllerSelectedEvmAccountChangeEvent, + type AccountsControllerGetAccountAction, + type AccountsControllerGetSelectedAccountAction, +} from '@metamask/accounts-controller'; import type { AddApprovalRequest } from '@metamask/approval-controller'; import type { BaseConfig, @@ -18,6 +23,7 @@ import { ApprovalType, NFT_API_BASE_URL, } from '@metamask/controller-utils'; +import { type InternalAccount } from '@metamask/keyring-api'; import type { NetworkClientId, NetworkController, @@ -169,13 +175,13 @@ export interface NftMetadata { * @type NftConfig * * NFT controller configuration - * @property selectedAddress - Vault selected address + * @property selectedAccountId - Vault selected account */ // This interface was created before this ESLint rule was added. // Convert to a `type` in a future major version. // eslint-disable-next-line @typescript-eslint/consistent-type-definitions export interface NftConfig extends BaseConfig { - selectedAddress: string; + selectedAccountId: string; chainId: Hex; ipfsGateway: string; openSeaEnabled: boolean; @@ -221,7 +227,12 @@ const controllerName = 'NftController'; /** * The external actions available to the {@link NftController}. */ -type AllowedActions = AddApprovalRequest; +type AllowedActions = + | AddApprovalRequest + | AccountsControllerGetAccountAction + | AccountsControllerGetSelectedAccountAction; + +type AllowedEvents = AccountsControllerSelectedEvmAccountChangeEvent; /** * The messenger of the {@link NftController}. @@ -229,9 +240,9 @@ type AllowedActions = AddApprovalRequest; export type NftControllerMessenger = RestrictedControllerMessenger< typeof controllerName, AllowedActions, - never, + AllowedEvents, AllowedActions['type'], - never + AllowedEvents['type'] >; export const getDefaultNftState = (): NftState => { @@ -1019,7 +1030,7 @@ export class NftController extends BaseControllerV1 { ) { super(config, state); this.defaultConfig = { - selectedAddress: '', + selectedAccountId: '', chainId: initialChainId, ipfsGateway: IPFS_DEFAULT_GATEWAY_URL, openSeaEnabled: false, @@ -1039,18 +1050,24 @@ export class NftController extends BaseControllerV1 { this.onNftAdded = onNftAdded; this.messagingSystem = messenger; + this.messagingSystem.subscribe( + 'AccountsController:selectedEvmAccountChange', + (newSelectedAccount: InternalAccount) => { + this.configure({ selectedAccountId: newSelectedAccount.id }); + }, + ); + onPreferencesStateChange( - async ({ - selectedAddress, - ipfsGateway, - openSeaEnabled, - isIpfsGatewayEnabled, - }) => { + async ({ ipfsGateway, openSeaEnabled, isIpfsGatewayEnabled }) => { + const newSelectedAccount = this.messagingSystem.call( + 'AccountsController:getSelectedAccount', + ); + this.configure({ - selectedAddress, ipfsGateway, openSeaEnabled, isIpfsGatewayEnabled, + selectedAccountId: newSelectedAccount.id, }); const needsUpdateNftMetadata = @@ -1059,7 +1076,7 @@ export class NftController extends BaseControllerV1 { if (needsUpdateNftMetadata) { const { chainId } = this.config; const nfts: Nft[] = - this.state.allNfts[selectedAddress]?.[chainId] ?? []; + this.state.allNfts[newSelectedAccount.address]?.[chainId] ?? []; // filter only nfts const nftsToUpdate = nfts.filter( (singleNft) => @@ -1068,7 +1085,7 @@ export class NftController extends BaseControllerV1 { if (nftsToUpdate.length !== 0) { await this.updateNftMetadata({ nfts: nftsToUpdate, - userAddress: selectedAddress, + userAddress: newSelectedAccount.address, }); } } @@ -1167,14 +1184,21 @@ export class NftController extends BaseControllerV1 { origin: string, { networkClientId, - userAddress = this.config.selectedAddress, + userAddress, }: { networkClientId?: NetworkClientId; userAddress?: string; - } = { - userAddress: this.config.selectedAddress, - }, + } = {}, ) { + const { selectedAccountId } = this.config; + const selectedAccount = this.messagingSystem.call( + 'AccountsController:getAccount', + selectedAccountId, + ); + + // + userAddress = userAddress || selectedAccount?.address || ''; + await this.validateWatchNft(asset, type, userAddress); const nftMetadata = await this.getNftInformation( @@ -1290,17 +1314,22 @@ export class NftController extends BaseControllerV1 { address: string, tokenId: string, { - userAddress = this.config.selectedAddress, + userAddress, networkClientId, source, }: { userAddress?: string; networkClientId?: NetworkClientId; source?: Source; - } = { - userAddress: this.config.selectedAddress, - }, + } = {}, ) { + const { selectedAccountId } = this.config; + const selectedAccount = this.messagingSystem.call( + 'AccountsController:getAccount', + selectedAccountId, + ); + userAddress = userAddress ?? selectedAccount?.address ?? ''; + if ( !(await this.isNftOwner(userAddress, address, tokenId, { networkClientId, @@ -1332,7 +1361,7 @@ export class NftController extends BaseControllerV1 { tokenId: string, { nftMetadata, - userAddress = this.config.selectedAddress, + userAddress, source = Source.Custom, networkClientId, }: { @@ -1340,8 +1369,16 @@ export class NftController extends BaseControllerV1 { userAddress?: string; source?: Source; networkClientId?: NetworkClientId; - } = { userAddress: this.config.selectedAddress }, + } = {}, ) { + const { selectedAccountId } = this.config; + const selectedAccount = this.messagingSystem.call( + 'AccountsController:getAccount', + selectedAccountId, + ); + // Previously selectedAddress could be an empty string. This is to preserve the behaviour + userAddress = userAddress ?? selectedAccount?.address ?? ''; + tokenAddress = toChecksumHexAddress(tokenAddress); const chainId = this.getCorrectChainId({ networkClientId }); @@ -1388,13 +1425,21 @@ export class NftController extends BaseControllerV1 { */ async updateNftMetadata({ nfts, - userAddress = this.config.selectedAddress, + userAddress, networkClientId, }: { nfts: Nft[]; userAddress?: string; networkClientId?: NetworkClientId; }) { + const userAccount = this.messagingSystem.call( + 'AccountsController:getAccount', + this.config.selectedAccountId, + ); + + // Previously selectedAddress could be an empty string. This is to preserve the behaviour + const addressToSearch = userAddress || userAccount?.address || ''; + const chainId = this.getCorrectChainId({ networkClientId }); const nftsWithChecksumAdr = nfts.map((nft) => { @@ -1423,7 +1468,7 @@ export class NftController extends BaseControllerV1 { // We want to avoid updating the state if the state and fetched nft info are the same const nftsWithDifferentMetadata: PromiseFulfilledResult[] = []; const { allNfts } = this.state; - const stateNfts = allNfts[userAddress]?.[chainId] || []; + const stateNfts = allNfts[addressToSearch]?.[chainId] || []; successfulNewFetchedNfts.forEach((singleNft) => { const existingEntry: Nft | undefined = stateNfts.find( @@ -1450,7 +1495,7 @@ export class NftController extends BaseControllerV1 { this.updateNft( elm.value.nft, elm.value.newMetadata, - userAddress, + addressToSearch, chainId, ), ); @@ -1471,22 +1516,34 @@ export class NftController extends BaseControllerV1 { tokenId: string, { networkClientId, - userAddress = this.config.selectedAddress, - }: { networkClientId?: NetworkClientId; userAddress?: string } = { - userAddress: this.config.selectedAddress, - }, + userAddress, + }: { networkClientId?: NetworkClientId; userAddress?: string } = {}, ) { + const { selectedAccountId } = this.config; + const userAccount = this.messagingSystem.call( + 'AccountsController:getAccount', + selectedAccountId, + ); + + // Previously selectedAddress could be an empty string. This is to preserve the behaviour + const addressToSearch = userAddress || userAccount?.address || ''; const chainId = this.getCorrectChainId({ networkClientId }); address = toChecksumHexAddress(address); - this.removeIndividualNft(address, tokenId, { chainId, userAddress }); + this.removeIndividualNft(address, tokenId, { + chainId, + userAddress: addressToSearch, + }); const { allNfts } = this.state; - const nfts = allNfts[userAddress]?.[chainId] || []; + const nfts = allNfts[addressToSearch]?.[chainId] || []; const remainingNft = nfts.find( (nft) => nft.address.toLowerCase() === address.toLowerCase(), ); if (!remainingNft) { - this.removeNftContract(address, { chainId, userAddress }); + this.removeNftContract(address, { + chainId, + userAddress: addressToSearch, + }); } } @@ -1504,24 +1561,34 @@ export class NftController extends BaseControllerV1 { tokenId: string, { networkClientId, - userAddress = this.config.selectedAddress, - }: { networkClientId?: NetworkClientId; userAddress?: string } = { - userAddress: this.config.selectedAddress, - }, + userAddress, + }: { networkClientId?: NetworkClientId; userAddress?: string } = {}, ) { + const { selectedAccountId } = this.config; + const selectedAccount = this.messagingSystem.call( + 'AccountsController:getAccount', + selectedAccountId, + ); + + // Previously selectedAddress could be an empty string. This is to preserve the behaviour + const addressToSearch = userAddress || selectedAccount?.address || ''; + const chainId = this.getCorrectChainId({ networkClientId }); address = toChecksumHexAddress(address); this.removeAndIgnoreIndividualNft(address, tokenId, { chainId, - userAddress, + userAddress: addressToSearch, }); const { allNfts } = this.state; - const nfts = allNfts[userAddress]?.[chainId] || []; + const nfts = allNfts[addressToSearch]?.[chainId] || []; const remainingNft = nfts.find( (nft) => nft.address.toLowerCase() === address.toLowerCase(), ); if (!remainingNft) { - this.removeNftContract(address, { chainId, userAddress }); + this.removeNftContract(address, { + chainId, + userAddress: addressToSearch, + }); } } @@ -1547,17 +1614,23 @@ export class NftController extends BaseControllerV1 { nft: Nft, batch: boolean, { - userAddress = this.config.selectedAddress, + userAddress, networkClientId, - }: { networkClientId?: NetworkClientId; userAddress?: string } = { - userAddress: this.config.selectedAddress, - }, + }: { networkClientId?: NetworkClientId; userAddress?: string } = {}, ) { + const { selectedAccountId } = this.config; + const selectedAccount = this.messagingSystem.call( + 'AccountsController:getAccount', + selectedAccountId, + ); + // Previously selectedAddress could be an empty string. This is to preserve the behaviour + const addressToSearch = userAddress || selectedAccount?.address || ''; + const chainId = this.getCorrectChainId({ networkClientId }); const { address, tokenId } = nft; let isOwned = nft.isCurrentlyOwned; try { - isOwned = await this.isNftOwner(userAddress, address, tokenId, { + isOwned = await this.isNftOwner(addressToSearch, address, tokenId, { networkClientId, }); } catch { @@ -1574,7 +1647,7 @@ export class NftController extends BaseControllerV1 { // if this is not part of a batched update we update this one NFT in state const { allNfts } = this.state; - const nfts = allNfts[userAddress]?.[chainId] || []; + const nfts = allNfts[addressToSearch]?.[chainId] || []; const nftToUpdate = nfts.find( (item) => item.tokenId === tokenId && @@ -1583,7 +1656,7 @@ export class NftController extends BaseControllerV1 { if (nftToUpdate) { nftToUpdate.isCurrentlyOwned = isOwned; this.updateNestedNftState(nfts, ALL_NFTS_STATE_KEY, { - userAddress, + userAddress: addressToSearch, chainId, }); } @@ -1597,17 +1670,21 @@ export class NftController extends BaseControllerV1 { * @param options.networkClientId - The networkClientId that can be used to identify the network client to use for this request. * @param options.userAddress - The address of the account where the NFT ownership status is checked/updated. */ - async checkAndUpdateAllNftsOwnershipStatus( - { - networkClientId, - userAddress = this.config.selectedAddress, - }: { networkClientId?: NetworkClientId; userAddress?: string } = { - userAddress: this.config.selectedAddress, - }, - ) { + async checkAndUpdateAllNftsOwnershipStatus({ + networkClientId, + userAddress, + }: { networkClientId?: NetworkClientId; userAddress?: string } = {}) { + const { selectedAccountId } = this.config; + const selectedAccount = this.messagingSystem.call( + 'AccountsController:getAccount', + selectedAccountId, + ); + // Previously selectedAddress could be an empty string. This is to preserve the behaviour + const addressToSearch = userAddress || selectedAccount?.address || ''; + const chainId = this.getCorrectChainId({ networkClientId }); const { allNfts } = this.state; - const nfts = allNfts[userAddress]?.[chainId] || []; + const nfts = allNfts[addressToSearch]?.[chainId] || []; const updatedNfts = await Promise.all( nfts.map(async (nft) => { return ( @@ -1620,7 +1697,7 @@ export class NftController extends BaseControllerV1 { ); this.updateNestedNftState(updatedNfts, ALL_NFTS_STATE_KEY, { - userAddress, + userAddress: addressToSearch, chainId, }); } @@ -1641,17 +1718,23 @@ export class NftController extends BaseControllerV1 { favorite: boolean, { networkClientId, - userAddress = this.config.selectedAddress, + userAddress, }: { networkClientId?: NetworkClientId; userAddress?: string; - } = { - userAddress: this.config.selectedAddress, - }, + } = {}, ) { + const { selectedAccountId } = this.config; + const selectedAccount = this.messagingSystem.call( + 'AccountsController:getAccount', + selectedAccountId, + ); + // Previously selectedAddress could be an empty string. This is to preserve the behaviour + const addressToSearch = userAddress || selectedAccount?.address || ''; + const chainId = this.getCorrectChainId({ networkClientId }); const { allNfts } = this.state; - const nfts = allNfts[userAddress]?.[chainId] || []; + const nfts = allNfts[addressToSearch]?.[chainId] || []; const index: number = nfts.findIndex( (nft) => nft.address === address && nft.tokenId === tokenId, ); @@ -1670,7 +1753,7 @@ export class NftController extends BaseControllerV1 { this.updateNestedNftState(nfts, ALL_NFTS_STATE_KEY, { chainId, - userAddress, + userAddress: addressToSearch, }); } diff --git a/packages/assets-controllers/src/NftDetectionController.test.ts b/packages/assets-controllers/src/NftDetectionController.test.ts index 9f06ee94215..e3032a677ac 100644 --- a/packages/assets-controllers/src/NftDetectionController.test.ts +++ b/packages/assets-controllers/src/NftDetectionController.test.ts @@ -1,4 +1,6 @@ +import { createMockInternalAccount } from '@metamask/accounts-controller'; import { NFT_API_BASE_URL, ChainId, toHex } from '@metamask/controller-utils'; +import type { InternalAccount } from '@metamask/keyring-api'; import { NetworkClientType } from '@metamask/network-controller'; import type { NetworkClient } from '@metamask/network-controller'; import { @@ -23,6 +25,7 @@ const DEFAULT_INTERVAL = 180000; describe('NftDetectionController', () => { let clock: sinon.SinonFakeTimers; + const defaultSelectedAccount = createMockInternalAccount(); beforeEach(async () => { clock = sinon.useFakeTimers(); @@ -277,15 +280,27 @@ describe('NftDetectionController', () => { expect(controller.config).toStrictEqual({ interval: DEFAULT_INTERVAL, chainId: toHex(1), - selectedAddress: '', + selectedAccountId: '', disabled: true, }); }); }); it('should poll and detect NFTs on interval while on mainnet', async () => { + const mockGetInternalAccount = jest + .fn() + .mockReturnValue(defaultSelectedAccount); + const mockGetSelectedAccount = jest + .fn() + .mockReturnValue(defaultSelectedAccount); await withController( - { config: { interval: 10 } }, + { + config: { interval: 10, selectedAccountId: defaultSelectedAccount.id }, + options: { + getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, + }, + }, async ({ controller, triggerPreferencesStateChange }) => { const mockNfts = sinon.stub(controller, 'detectNfts'); triggerPreferencesStateChange({ @@ -311,51 +326,60 @@ describe('NftDetectionController', () => { }); it('should poll and detect NFTs by networkClientId on interval while on mainnet', async () => { - await withController(async ({ controller }) => { - const spy = jest - .spyOn(controller, 'detectNfts') - .mockImplementation(() => { - return Promise.resolve(); - }); + const mockGetInternalAccount = jest + .fn() + .mockReturnValue(defaultSelectedAccount); + await withController( + { + config: { selectedAccountId: defaultSelectedAccount.id }, + options: { getInternalAccount: mockGetInternalAccount }, + }, + async ({ controller }) => { + const spy = jest + .spyOn(controller, 'detectNfts') + .mockImplementation(() => { + return Promise.resolve(); + }); - controller.startPollingByNetworkClientId('mainnet', { - address: '0x1', - }); + controller.startPollingByNetworkClientId('mainnet', { + address: '0x1', + }); - await advanceTime({ clock, duration: 0 }); - expect(spy.mock.calls).toHaveLength(1); - await advanceTime({ - clock, - duration: DEFAULT_INTERVAL / 2, - }); - expect(spy.mock.calls).toHaveLength(1); - await advanceTime({ - clock, - duration: DEFAULT_INTERVAL / 2, - }); - expect(spy.mock.calls).toHaveLength(2); - await advanceTime({ clock, duration: DEFAULT_INTERVAL }); - expect(spy.mock.calls).toMatchObject([ - [ - { - networkClientId: 'mainnet', - userAddress: '0x1', - }, - ], - [ - { - networkClientId: 'mainnet', - userAddress: '0x1', - }, - ], - [ - { - networkClientId: 'mainnet', - userAddress: '0x1', - }, - ], - ]); - }); + await advanceTime({ clock, duration: 0 }); + expect(spy.mock.calls).toHaveLength(1); + await advanceTime({ + clock, + duration: DEFAULT_INTERVAL / 2, + }); + expect(spy.mock.calls).toHaveLength(1); + await advanceTime({ + clock, + duration: DEFAULT_INTERVAL / 2, + }); + expect(spy.mock.calls).toHaveLength(2); + await advanceTime({ clock, duration: DEFAULT_INTERVAL }); + expect(spy.mock.calls).toMatchObject([ + [ + { + networkClientId: 'mainnet', + userAddress: '0x1', + }, + ], + [ + { + networkClientId: 'mainnet', + userAddress: '0x1', + }, + ], + [ + { + networkClientId: 'mainnet', + userAddress: '0x1', + }, + ], + ]); + }, + ); }); it('should detect mainnet correctly', async () => { @@ -380,13 +404,27 @@ describe('NftDetectionController', () => { it('should detect and add NFTs correctly when blockaid result is not included in response', async () => { const mockAddNft = jest.fn(); + const mockGetInternalAccount = jest.fn(); + const mockGetSelectedAccount = jest.fn(); await withController( - { options: { addNft: mockAddNft } }, - async ({ controller, triggerPreferencesStateChange }) => { - const selectedAddress = '0x1'; + { + options: { + addNft: mockAddNft, + getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, + }, + }, + async ({ + controller, + triggerPreferencesStateChange, + triggerSelectedAccountChange, + }) => { + const selectedAccount = createMockInternalAccount({ address: '0x1' }); + mockGetSelectedAccount.mockReturnValue(selectedAccount); + mockGetInternalAccount.mockReturnValue(selectedAccount); + triggerSelectedAccountChange(selectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useNftDetection: true, }); // Wait for detect call triggered by preferences state change to settle @@ -409,7 +447,7 @@ describe('NftDetectionController', () => { standard: 'ERC721', imageOriginal: 'imageOriginal/2574.png', }, - userAddress: selectedAddress, + userAddress: selectedAccount.address, source: Source.Detected, networkClientId: undefined, }, @@ -420,13 +458,30 @@ describe('NftDetectionController', () => { it('should detect and add NFTs correctly when blockaid result is in response', async () => { const mockAddNft = jest.fn(); + const mockGetInternalAccount = jest.fn(); + const mockGetSelectedAccount = jest.fn(); await withController( - { options: { addNft: mockAddNft } }, - async ({ controller, triggerPreferencesStateChange }) => { - const selectedAddress = '0x123'; + { + config: { selectedAccountId: defaultSelectedAccount.id }, + options: { + addNft: mockAddNft, + getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, + }, + }, + async ({ + controller, + triggerPreferencesStateChange, + triggerSelectedAccountChange, + }) => { + const updatedSelectedAccount = createMockInternalAccount({ + address: '0x123', + }); + mockGetInternalAccount.mockReturnValue(updatedSelectedAccount); + triggerSelectedAccountChange(updatedSelectedAccount); + mockGetSelectedAccount.mockReturnValue(updatedSelectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useNftDetection: true, }); // Wait for detect call triggered by preferences state change to settle @@ -447,7 +502,7 @@ describe('NftDetectionController', () => { standard: 'ERC721', imageOriginal: 'imageOriginal/2574.png', }, - userAddress: selectedAddress, + userAddress: updatedSelectedAccount.address, source: Source.Detected, networkClientId: undefined, }); @@ -459,7 +514,7 @@ describe('NftDetectionController', () => { standard: 'ERC721', imageOriginal: 'imageOriginal/2575.png', }, - userAddress: selectedAddress, + userAddress: updatedSelectedAccount.address, source: Source.Detected, networkClientId: undefined, }); @@ -469,13 +524,29 @@ describe('NftDetectionController', () => { it('should detect and add NFTs and filter them correctly', async () => { const mockAddNft = jest.fn(); + const mockGetInternalAccount = jest.fn(); + const mockGetSelectedAccount = jest.fn(); await withController( - { options: { addNft: mockAddNft } }, - async ({ controller, triggerPreferencesStateChange }) => { - const selectedAddress = '0x12345'; + { + options: { + addNft: mockAddNft, + getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, + }, + }, + async ({ + controller, + triggerPreferencesStateChange, + triggerSelectedAccountChange, + }) => { + const updatedSelectedAccount = createMockInternalAccount({ + address: '0x12345', + }); + mockGetInternalAccount.mockReturnValue(updatedSelectedAccount); + triggerSelectedAccountChange(updatedSelectedAccount); + mockGetSelectedAccount.mockReturnValue(updatedSelectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useNftDetection: true, }); // Wait for detect call triggered by preferences state change to settle @@ -502,7 +573,7 @@ describe('NftDetectionController', () => { standard: 'ERC721', imageOriginal: 'imageOriginal/1.png', }, - userAddress: selectedAddress, + userAddress: updatedSelectedAccount.address, source: Source.Detected, networkClientId: undefined, }, @@ -519,7 +590,7 @@ describe('NftDetectionController', () => { standard: 'ERC721', imageOriginal: 'imageOriginal/2.png', }, - userAddress: selectedAddress, + userAddress: updatedSelectedAccount.address, source: Source.Detected, networkClientId: undefined, }, @@ -530,13 +601,29 @@ describe('NftDetectionController', () => { it('should detect and add NFTs by networkClientId correctly', async () => { const mockAddNft = jest.fn(); + const mockGetInternalAccount = jest.fn(); + const mockGetSelectedAccount = jest.fn(); await withController( - { options: { addNft: mockAddNft } }, - async ({ controller, triggerPreferencesStateChange }) => { - const selectedAddress = '0x1'; + { + options: { + addNft: mockAddNft, + getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, + }, + }, + async ({ + controller, + triggerPreferencesStateChange, + triggerSelectedAccountChange, + }) => { + const updatedSelectedAccount = createMockInternalAccount({ + address: '0x1', + }); + mockGetInternalAccount.mockReturnValue(updatedSelectedAccount); + triggerSelectedAccountChange(updatedSelectedAccount); + mockGetSelectedAccount.mockReturnValue(updatedSelectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useNftDetection: true, }); // Wait for detect call triggered by preferences state change to settle @@ -573,6 +660,8 @@ describe('NftDetectionController', () => { it('should not autodetect NFTs that exist in the ignoreList', async () => { const mockAddNft = jest.fn(); + const mockGetInternalAccount = jest.fn(); + const mockGetSelectedAccount = jest.fn(); const mockGetNftState = jest.fn().mockImplementation(() => { return { ...getDefaultNftState(), @@ -588,12 +677,25 @@ describe('NftDetectionController', () => { }; }); await withController( - { options: { addNft: mockAddNft, getNftState: mockGetNftState } }, - async ({ controller, triggerPreferencesStateChange }) => { - const selectedAddress = '0x9'; + { + options: { + addNft: mockAddNft, + getNftState: mockGetNftState, + getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, + }, + }, + async ({ + controller, + triggerPreferencesStateChange, + triggerSelectedAccountChange, + }) => { + const selectedAccount = createMockInternalAccount({ address: '0x9' }); + mockGetInternalAccount.mockReturnValue(selectedAccount); + mockGetSelectedAccount.mockReturnValue(selectedAccount); + triggerSelectedAccountChange(selectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useNftDetection: true, }); // Wait for detect call triggered by preferences state change to settle @@ -612,19 +714,22 @@ describe('NftDetectionController', () => { it('should not detect and add NFTs if there is no selectedAddress', async () => { const mockAddNft = jest.fn(); + const mockGetInternalAccount = jest.fn().mockReturnValue(null); + const mockGetSelectedAccount = jest.fn().mockReturnValue({ address: '' }); await withController( - { options: { addNft: mockAddNft } }, + { + options: { + addNft: mockAddNft, + getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, + }, + }, async ({ controller, triggerPreferencesStateChange }) => { - const selectedAddress = ''; // Emtpy selected address triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useNftDetection: true, // auto-detect is enabled so it proceeds to check userAddress }); - // confirm that default selected address is an empty string - expect(controller.config.selectedAddress).toBe(''); - await controller.detectNfts(); expect(mockAddNft).not.toHaveBeenCalled(); @@ -657,8 +762,17 @@ describe('NftDetectionController', () => { }); it('should not detectNfts when disabled is false and useNftDetection is true', async () => { + const mockGetSelectedAccount = jest + .fn() + .mockReturnValue(defaultSelectedAccount); await withController( - { config: { interval: 10 }, options: { disabled: false } }, + { + config: { interval: 10, selectedAccountId: defaultSelectedAccount.id }, + options: { + disabled: false, + getSelectedAccount: mockGetSelectedAccount, + }, + }, async ({ controller, triggerPreferencesStateChange }) => { const mockNfts = sinon.stub(controller, 'detectNfts'); triggerPreferencesStateChange({ @@ -685,13 +799,27 @@ describe('NftDetectionController', () => { it('should not detect and add NFTs if preferences controller useNftDetection is set to false', async () => { const mockAddNft = jest.fn(); + const mockGetInternalAccount = jest.fn(); + const mockGetSelectedAccount = jest.fn(); await withController( - { options: { addNft: mockAddNft } }, - async ({ controller, triggerPreferencesStateChange }) => { - const selectedAddress = '0x9'; + { + options: { + addNft: mockAddNft, + getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, + }, + }, + async ({ + controller, + triggerPreferencesStateChange, + triggerSelectedAccountChange, + }) => { + const selectedAccount = createMockInternalAccount({ address: '0x9' }); + mockGetInternalAccount.mockReturnValue(selectedAccount); + mockGetSelectedAccount.mockReturnValue(selectedAccount); + triggerSelectedAccountChange(selectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useNftDetection: false, }); // Wait for detect call triggered by preferences state change to settle @@ -709,9 +837,9 @@ describe('NftDetectionController', () => { }); it('should do nothing when the request to Nft API fails', async () => { - const selectedAddress = '0x3'; + const selectedAccount = createMockInternalAccount({ address: '0x3' }); nock(NFT_API_BASE_URL) - .get(`/users/${selectedAddress}/tokens`) + .get(`/users/${selectedAccount.address}/tokens`) .query({ continuation: '', limit: '50', @@ -721,12 +849,25 @@ describe('NftDetectionController', () => { .replyWithError(new Error('Failed to fetch')) .persist(); const mockAddNft = jest.fn(); + const mockGetInternalAccount = jest.fn().mockReturnValue(selectedAccount); + const mockGetSelectedAccount = jest.fn().mockReturnValue(selectedAccount); await withController( - { options: { addNft: mockAddNft } }, - async ({ controller, triggerPreferencesStateChange }) => { + { + options: { + addNft: mockAddNft, + getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, + }, + }, + async ({ + controller, + triggerPreferencesStateChange, + triggerSelectedAccountChange, + }) => { + mockGetInternalAccount.mockReturnValue(selectedAccount); + triggerSelectedAccountChange(selectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useNftDetection: true, }); // Wait for detect call triggered by preferences state change to settle @@ -744,12 +885,24 @@ describe('NftDetectionController', () => { }); it('should rethrow error when Nft APi server fails with error other than fetch failure', async () => { - const selectedAddress = '0x4'; + const selectedAccount = createMockInternalAccount({ address: '0x4' }); + const mockGetInternalAccount = jest.fn().mockReturnValue(selectedAccount); + const mockGetSelectedAccount = jest.fn().mockReturnValue(selectedAccount); await withController( - async ({ controller, triggerPreferencesStateChange }) => { + { + options: { + getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, + }, + }, + async ({ + controller, + triggerPreferencesStateChange, + triggerSelectedAccountChange, + }) => { // This mock is for the initial detect call after preferences change nock(NFT_API_BASE_URL) - .get(`/users/${selectedAddress}/tokens`) + .get(`/users/${selectedAccount.address}/tokens`) .query({ continuation: '', limit: '50', @@ -759,9 +912,9 @@ describe('NftDetectionController', () => { .reply(200, { tokens: [], }); + triggerSelectedAccountChange(selectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useNftDetection: true, }); // Wait for detect call triggered by preferences state change to settle @@ -771,7 +924,7 @@ describe('NftDetectionController', () => { }); // This mock is for the call under test nock(NFT_API_BASE_URL) - .get(`/users/${selectedAddress}/tokens`) + .get(`/users/${selectedAccount.address}/tokens`) .query({ continuation: '', limit: '50', @@ -789,13 +942,29 @@ describe('NftDetectionController', () => { it('should rethrow error when attempt to add NFT fails', async () => { const mockAddNft = jest.fn(); + const mockGetInternalAccount = jest.fn(); + const mockGetSelectedAccount = jest.fn(); await withController( - { options: { addNft: mockAddNft } }, - async ({ controller, triggerPreferencesStateChange }) => { - const selectedAddress = '0x1'; + { + options: { + addNft: mockAddNft, + getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, + }, + }, + async ({ + controller, + triggerPreferencesStateChange, + triggerSelectedAccountChange, + }) => { + const selectedAccount = createMockInternalAccount({ + address: '0x1', + }); + mockGetInternalAccount.mockReturnValue(selectedAccount); + mockGetSelectedAccount.mockReturnValue(selectedAccount); + triggerSelectedAccountChange(selectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useNftDetection: true, }); // Wait for detect call triggered by preferences state change to settle @@ -814,8 +983,20 @@ describe('NftDetectionController', () => { }); it('should only re-detect when relevant settings change', async () => { + const mockGetInternalAccount = jest + .fn() + .mockReturnValue(defaultSelectedAccount); + const mockGetSelectedAccount = jest + .fn() + .mockReturnValue(defaultSelectedAccount); await withController( - {}, + { + config: { selectedAccountId: defaultSelectedAccount.id }, + options: { + getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, + }, + }, async ({ controller, triggerPreferencesStateChange }) => { const detectNfts = sinon.stub(controller, 'detectNfts'); @@ -848,6 +1029,7 @@ type WithControllerCallback = ({ controller: NftDetectionController; triggerNftStateChange: (state: NftState) => void; triggerPreferencesStateChange: (state: PreferencesState) => void; + triggerSelectedAccountChange: (account: InternalAccount) => void; }) => Promise | ReturnValue; type WithControllerOptions = { @@ -888,6 +1070,8 @@ async function withController( const nftStateChangeListeners: ((state: NftState) => void)[] = []; const preferencesStateChangeListeners: ((state: PreferencesState) => void)[] = []; + const selectedAccountChangeListener: ((account: InternalAccount) => void)[] = + []; const controller = new NftDetectionController( { chainId: ChainId.mainnet, @@ -897,14 +1081,19 @@ async function withController( onPreferencesStateChange: (listener) => { preferencesStateChangeListeners.push(listener); }, + onSelectedAccountChange: (listener) => { + selectedAccountChangeListener.push(listener); + }, onNetworkStateChange: jest.fn(), getOpenSeaApiKey: jest.fn(), addNft: jest.fn(), getNftApi: jest.fn(), getNetworkClientById, + getInternalAccount: jest.fn(), + getSelectedAccount: jest.fn(), getNftState: getDefaultNftState, disabled: true, - selectedAddress: '', + selectedAccountId: '', ...options, }, config, @@ -922,6 +1111,11 @@ async function withController( listener(state); } }, + triggerSelectedAccountChange: (account: InternalAccount) => { + for (const listener of selectedAccountChangeListener) { + listener(account); + } + }, }); } finally { controller.stop(); diff --git a/packages/assets-controllers/src/NftDetectionController.ts b/packages/assets-controllers/src/NftDetectionController.ts index 488d468d861..dfd58e5e894 100644 --- a/packages/assets-controllers/src/NftDetectionController.ts +++ b/packages/assets-controllers/src/NftDetectionController.ts @@ -1,3 +1,4 @@ +import type { AccountsController } from '@metamask/accounts-controller'; import type { BaseConfig, BaseState } from '@metamask/base-controller'; import { fetchWithErrorHandling, @@ -5,6 +6,7 @@ import { ChainId, NFT_API_BASE_URL, } from '@metamask/controller-utils'; +import type { InternalAccount } from '@metamask/keyring-api'; import type { NetworkClientId, NetworkController, @@ -138,7 +140,7 @@ export interface ApiNftCreator { * NftDetection configuration * @property interval - Polling interval used to fetch new token rates * @property chainId - Current chain ID - * @property selectedAddress - Vault selected address + * @property selectedAccountId - Vault selected account id */ // This interface was created before this ESLint rule was added. // Convert to a `type` in a future major version. @@ -146,7 +148,7 @@ export interface ApiNftCreator { export interface NftDetectionConfig extends BaseConfig { interval: number; chainId: Hex; - selectedAddress: string; + selectedAccountId: string; } export type ReservoirResponse = { @@ -417,6 +419,10 @@ export class NftDetectionController extends StaticIntervalPollingControllerV1< private readonly getNetworkClientById: NetworkController['getNetworkClientById']; + private readonly getInternalAccount: AccountsController['getAccount']; + + private readonly getSelectedAccount: AccountsController['getSelectedAccount']; + /** * Creates an NftDetectionController instance. * @@ -425,13 +431,16 @@ export class NftDetectionController extends StaticIntervalPollingControllerV1< * @param options.onNftsStateChange - Allows subscribing to assets controller state changes. * @param options.onPreferencesStateChange - Allows subscribing to preferences controller state changes. * @param options.onNetworkStateChange - Allows subscribing to network controller state changes. + * @param options.onSelectedAccountChange - Allows subscribing to the change of account from the accoutns controller. * @param options.getOpenSeaApiKey - Gets the OpenSea API key, if one is set. * @param options.addNft - Add an NFT. * @param options.getNftApi - Gets the URL to fetch an NFT from OpenSea. * @param options.getNftState - Gets the current state of the Assets controller. * @param options.disabled - Represents previous value of useNftDetection. Used to detect changes of useNftDetection. Default value is true. - * @param options.selectedAddress - Represents current selected address. + * @param options.selectedAccountId - Represents current selected account id. * @param options.getNetworkClientById - Gets the network client by ID, from the NetworkController. + * @param options.getInternalAccount - Gets the internal account from the AccountsController. + * @param options.getSelectedAccount - Gets the selected account from the AccountsController. * @param config - Initial options used to configure this controller. * @param state - Initial state to set on this controller. */ @@ -441,12 +450,15 @@ export class NftDetectionController extends StaticIntervalPollingControllerV1< getNetworkClientById, onPreferencesStateChange, onNetworkStateChange, + onSelectedAccountChange, getOpenSeaApiKey, addNft, getNftApi, getNftState, + getInternalAccount, + getSelectedAccount, disabled: initialDisabled, - selectedAddress: initialSelectedAddress, + selectedAccountId: initialSelectedAccountId, }: { chainId: Hex; getNetworkClientById: NetworkController['getNetworkClientById']; @@ -457,12 +469,17 @@ export class NftDetectionController extends StaticIntervalPollingControllerV1< onNetworkStateChange: ( listener: (networkState: NetworkState) => void, ) => void; + onSelectedAccountChange: ( + listener: (internalAccount: InternalAccount) => void, + ) => void; getOpenSeaApiKey: () => string | undefined; addNft: NftController['addNft']; getNftApi: NftController['getNftApi']; getNftState: () => NftState; + getInternalAccount: (accountId: string) => InternalAccount; + getSelectedAccount: () => InternalAccount; disabled: boolean; - selectedAddress: string; + selectedAccountId: string; }, config?: Partial, state?: Partial, @@ -471,21 +488,39 @@ export class NftDetectionController extends StaticIntervalPollingControllerV1< this.defaultConfig = { interval: DEFAULT_INTERVAL, chainId: initialChainId, - selectedAddress: initialSelectedAddress, + selectedAccountId: initialSelectedAccountId, disabled: initialDisabled, }; this.initialize(); this.getNftState = getNftState; this.getNetworkClientById = getNetworkClientById; - onPreferencesStateChange(({ selectedAddress, useNftDetection }) => { - const { selectedAddress: previouslySelectedAddress, disabled } = + this.getInternalAccount = getInternalAccount; + this.getSelectedAccount = getSelectedAccount; + + onSelectedAccountChange((internalAccount) => { + this.configure({ selectedAccountId: internalAccount.id }); + + const { selectedAccountId, disabled } = this.config; + if (!disabled || selectedAccountId !== internalAccount.id) { + this.start(); + } else { + this.stop(); + } + }); + + onPreferencesStateChange(({ useNftDetection }) => { + const { selectedAccountId: previousSelectedAccountId, disabled } = this.config; + const newSelectedAccount = this.getSelectedAccount('eip155:*'); if ( - selectedAddress !== previouslySelectedAddress || + newSelectedAccount.id !== previousSelectedAccountId || !useNftDetection !== disabled ) { - this.configure({ selectedAddress, disabled: !useNftDetection }); + this.configure({ + disabled: !useNftDetection, + selectedAccountId: newSelectedAccount.id, + }); if (useNftDetection) { this.start(); } else { @@ -569,15 +604,18 @@ export class NftDetectionController extends StaticIntervalPollingControllerV1< * @param options.networkClientId - The network client ID to detect NFTs on. * @param options.userAddress - The address to detect NFTs for. */ - async detectNfts( - { - networkClientId, - userAddress, - }: { - networkClientId?: NetworkClientId; - userAddress: string; - } = { userAddress: this.config.selectedAddress }, - ) { + async detectNfts({ + networkClientId, + userAddress, + }: { + networkClientId?: NetworkClientId; + userAddress?: string; + } = {}) { + const { selectedAccountId } = this.config; + const internalAccount = this.getInternalAccount(selectedAccountId); + + userAddress = userAddress || internalAccount?.address; + /* istanbul ignore if */ if (!this.isMainnet() || this.disabled) { return; From 98a3348dceba508003268a1cd8c7557ac5e9c615 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Tue, 21 May 2024 17:48:41 +0800 Subject: [PATCH 16/24] fix: transaction controller integration test --- .../src/TransactionController.ts | 2 +- .../TransactionControllerIntegration.test.ts | 89 ++++++++++++++++--- 2 files changed, 79 insertions(+), 12 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index de00a917b18..9af09141270 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -25,6 +25,7 @@ import type { FetchGasFeeEstimateOptions, GasFeeState, } from '@metamask/gas-fee-controller'; +import type { InternalAccount } from '@metamask/keyring-api'; import type { BlockTracker, NetworkClientId, @@ -122,7 +123,6 @@ import { validateTransactionOrigin, validateTxParams, } from './utils/validation'; -import { InternalAccount } from '@metamask/keyring-api'; /** * Metadata for the TransactionController state, describing how to "anonymize" diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index 979f88c4525..91349913c88 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -11,6 +11,8 @@ import { InfuraNetworkType, NetworkType, } from '@metamask/controller-utils'; +import type { InternalAccount } from '@metamask/keyring-api'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { NetworkController, NetworkClientType, @@ -25,6 +27,7 @@ import assert from 'assert'; import nock from 'nock'; import type { SinonFakeTimers } from 'sinon'; import { useFakeTimers } from 'sinon'; +import { v4 } from 'uuid'; import { advanceTime } from '../../../tests/helpers'; import { mockNetwork } from '../../../tests/mock-network'; @@ -64,7 +67,46 @@ type UnrestrictedControllerMessenger = ControllerMessenger< | TransactionControllerEvents >; +const createMockInternalAccount = ({ + id = v4(), + address = '0x2990079bcdee240329a520d2444386fc119da21a', + name = 'Account 1', + importTime = Date.now(), + lastSelected = Date.now(), +}: { + id?: string; + address?: string; + name?: string; + importTime?: number; + lastSelected?: number; +} = {}): InternalAccount => { + return { + id, + address, + options: {}, + methods: [ + EthMethod.PersonalSign, + EthMethod.Sign, + EthMethod.SignTransaction, + EthMethod.SignTypedDataV1, + EthMethod.SignTypedDataV3, + EthMethod.SignTypedDataV4, + ], + type: EthAccountType.Eoa, + metadata: { + name, + keyring: { type: 'HD Key Tree' }, + importTime, + lastSelected, + }, + } as InternalAccount; +}; + const ACCOUNT_MOCK = '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207'; +const INTERNAL_ACCOUNT_MOCK = createMockInternalAccount({ + address: ACCOUNT_MOCK, +}); + const ACCOUNT_2_MOCK = '0x08f137f335ea1b8f193b8f6ea92561a60d23a211'; const ACCOUNT_3_MOCK = '0xe688b84b23f322a994a53dbf8e15fa82cdb71127'; const infuraProjectId = 'fake-infura-project-id'; @@ -167,7 +209,8 @@ const setupController = async ( getNetworkClientRegistry: networkController.getNetworkClientRegistry.bind(networkController), getPermittedAccounts: async () => [ACCOUNT_MOCK], - getSelectedAddress: () => '0xdeadbeef', + getSelectedAccount: () => + createMockInternalAccount({ address: '0xdeadbeef' }), hooks: {}, isMultichainEnabled: false, messenger, @@ -802,7 +845,7 @@ describe('TransactionController Integration', () => { await setupController({ isMultichainEnabled: true, getPermittedAccounts: async () => [ACCOUNT_MOCK], - getSelectedAddress: () => ACCOUNT_MOCK, + getSelectedAccount: () => INTERNAL_ACCOUNT_MOCK, }); const otherNetworkClientIdOnGoerli = await networkController.upsertNetworkConfiguration( @@ -883,7 +926,7 @@ describe('TransactionController Integration', () => { await setupController({ isMultichainEnabled: true, getPermittedAccounts: async () => [ACCOUNT_MOCK], - getSelectedAddress: () => ACCOUNT_MOCK, + getSelectedAccount: () => INTERNAL_ACCOUNT_MOCK, }); const addTx1 = await transactionController.addTransaction( @@ -1140,10 +1183,13 @@ describe('TransactionController Integration', () => { }); const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to; + const selectedAccountMock = createMockInternalAccount({ + address: selectedAddress, + }); const { networkController, transactionController } = await setupController({ - getSelectedAddress: () => selectedAddress, + getSelectedAccount: () => selectedAccountMock, isMultichainEnabled: true, }); @@ -1209,6 +1255,9 @@ describe('TransactionController Integration', () => { it('should start the global incoming transaction helper when no networkClientIds provided', async () => { const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to; + const selectedAccountMock = createMockInternalAccount({ + address: selectedAddress, + }); mockNetwork({ networkClientConfiguration: buildInfuraNetworkClientConfiguration( @@ -1226,7 +1275,7 @@ describe('TransactionController Integration', () => { .reply(200, ETHERSCAN_TRANSACTION_RESPONSE_MOCK); const { transactionController } = await setupController({ - getSelectedAddress: () => selectedAddress, + getSelectedAccount: () => selectedAccountMock, }); transactionController.startIncomingTransactionPolling(); @@ -1314,10 +1363,13 @@ describe('TransactionController Integration', () => { }); const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to; + const selectedAccountMock = createMockInternalAccount({ + address: selectedAddress, + }); const { networkController, transactionController } = await setupController({ - getSelectedAddress: () => selectedAddress, + getSelectedAccount: () => selectedAccountMock, isMultichainEnabled: true, }); @@ -1410,10 +1462,13 @@ describe('TransactionController Integration', () => { describe('stopIncomingTransactionPolling', () => { it('should not poll for new incoming transactions for the given networkClientId', async () => { const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to; + const selectedAccountMock = createMockInternalAccount({ + address: selectedAddress, + }); const { networkController, transactionController } = await setupController({ - getSelectedAddress: () => selectedAddress, + getSelectedAccount: () => selectedAccountMock, }); const networkClients = networkController.getNetworkClientRegistry(); @@ -1454,9 +1509,12 @@ describe('TransactionController Integration', () => { it('should stop the global incoming transaction helper when no networkClientIds provided', async () => { const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to; + const selectedAccountMock = createMockInternalAccount({ + address: selectedAddress, + }); const { transactionController } = await setupController({ - getSelectedAddress: () => selectedAddress, + getSelectedAccount: () => selectedAccountMock, }); mockNetwork({ @@ -1490,10 +1548,13 @@ describe('TransactionController Integration', () => { describe('stopAllIncomingTransactionPolling', () => { it('should not poll for incoming transactions on any network client', async () => { const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to; + const selectedAccountMock = createMockInternalAccount({ + address: selectedAddress, + }); const { networkController, transactionController } = await setupController({ - getSelectedAddress: () => selectedAddress, + getSelectedAccount: () => selectedAccountMock, }); const networkClients = networkController.getNetworkClientRegistry(); @@ -1534,10 +1595,13 @@ describe('TransactionController Integration', () => { describe('updateIncomingTransactions', () => { it('should add incoming transactions to state with the correct chainId for the given networkClientId without waiting for the next block', async () => { const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to; + const selectedAccountMock = createMockInternalAccount({ + address: selectedAddress, + }); const { networkController, transactionController } = await setupController({ - getSelectedAddress: () => selectedAddress, + getSelectedAccount: () => selectedAccountMock, isMultichainEnabled: true, }); @@ -1600,9 +1664,12 @@ describe('TransactionController Integration', () => { it('should update the incoming transactions for the gloablly selected network when no networkClientIds provided', async () => { const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to; + const selectedAccountMock = createMockInternalAccount({ + address: selectedAddress, + }); const { transactionController } = await setupController({ - getSelectedAddress: () => selectedAddress, + getSelectedAccount: () => selectedAccountMock, }); mockNetwork({ From fb1848683ca9b8b5f449a666449d460b5164fa5b Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Tue, 21 May 2024 17:50:01 +0800 Subject: [PATCH 17/24] fix: lint remove duplicate test --- .../src/AccountsController.test.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index 0e2cec59a01..f9912b010da 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -1852,24 +1852,6 @@ describe('AccountsController', () => { }); }); - describe('getSelectedAccount', () => { - it('should return the selected account', () => { - const { accountsController } = setupAccountsController({ - initialState: { - internalAccounts: { - accounts: { [mockAccount.id]: mockAccount }, - selectedAccount: mockAccount.id, - }, - }, - }); - const result = accountsController.getAccountExpect(mockAccount.id); - - expect(result).toStrictEqual( - setLastSelectedAsAny(mockAccount as InternalAccount), - ); - }); - }); - describe('setSelectedAccount', () => { it('should set the selected account', () => { const { accountsController } = setupAccountsController({ From 29137ce71d9f4618fa7a018abce22a3f2956f1d1 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Tue, 21 May 2024 17:50:33 +0800 Subject: [PATCH 18/24] fix: tokens controller unused var --- packages/assets-controllers/src/TokensController.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/assets-controllers/src/TokensController.test.ts b/packages/assets-controllers/src/TokensController.test.ts index 27faf2212db..efb447bc3fa 100644 --- a/packages/assets-controllers/src/TokensController.test.ts +++ b/packages/assets-controllers/src/TokensController.test.ts @@ -361,7 +361,7 @@ describe('TokensController', () => { }), }, }, - async ({ controller, getAccountHandler }) => { + async ({ controller }) => { ContractMock.mockReturnValue( buildMockEthersERC721Contract({ supportsInterface: false }), ); From b2ae223876787a4021e08b581364182a414d87fc Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Tue, 21 May 2024 20:15:41 +0800 Subject: [PATCH 19/24] fix: lint in type --- packages/assets-controllers/src/TokensController.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/assets-controllers/src/TokensController.test.ts b/packages/assets-controllers/src/TokensController.test.ts index efb447bc3fa..1ee581f6d6a 100644 --- a/packages/assets-controllers/src/TokensController.test.ts +++ b/packages/assets-controllers/src/TokensController.test.ts @@ -2353,7 +2353,10 @@ type WithControllerCallback = ({ messenger: UnrestrictedMessenger; approvalController: ApprovalController; triggerSelectedAccountChange: (internalAccount: InternalAccount) => void; - getAccountHandler: jest.Mock>; + getAccountHandler: jest.Mock< + ReturnType, + Parameters + >; }) => Promise | ReturnValue; type WithControllerArgs = From 4351ebe2725ec5465b5e255433f28c86f9352db7 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Mon, 27 May 2024 16:02:47 +0800 Subject: [PATCH 20/24] fix: lint --- .../src/NftController.test.ts | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/packages/assets-controllers/src/NftController.test.ts b/packages/assets-controllers/src/NftController.test.ts index 2defde3dc44..39d40143fda 100644 --- a/packages/assets-controllers/src/NftController.test.ts +++ b/packages/assets-controllers/src/NftController.test.ts @@ -203,33 +203,6 @@ function setupController({ showApprovalRequest: jest.fn(), }); - const mockGetNetworkClientById = jest - .fn() - .mockImplementation((networkClientId) => { - switch (networkClientId) { - case 'sepolia': - return { - configuration: { - chainId: SEPOLIA.chainId, - }, - }; - case 'goerli': - return { - configuration: { - chainId: GOERLI.chainId, - }, - }; - case 'customNetworkClientId-1': - return { - configuration: { - chainId: '0xa', - }, - }; - default: - throw new Error('Invalid network client id'); - } - }); - const nftControllerMessenger = messenger.getRestricted< typeof controllerName, ApprovalActions['type'], From e01842de6415093acc94babc97c4424502a5db9a Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Fri, 31 May 2024 10:42:29 +0200 Subject: [PATCH 21/24] chore: bump keyring-api to 6.3.1 --- packages/accounts-controller/package.json | 2 +- packages/assets-controllers/package.json | 8 +- .../src/BalancesController.ts | 186 ++++++++++++++++++ packages/chain-controller/package.json | 2 +- packages/keyring-controller/package.json | 2 +- yarn.lock | 30 ++- 6 files changed, 220 insertions(+), 10 deletions(-) create mode 100644 packages/assets-controllers/src/BalancesController.ts diff --git a/packages/accounts-controller/package.json b/packages/accounts-controller/package.json index 3c59326eb2e..947838b0187 100644 --- a/packages/accounts-controller/package.json +++ b/packages/accounts-controller/package.json @@ -44,7 +44,7 @@ "@ethereumjs/util": "^8.1.0", "@metamask/base-controller": "^5.0.2", "@metamask/eth-snap-keyring": "^4.1.1", - "@metamask/keyring-api": "^6.1.1", + "@metamask/keyring-api": "^6.3.1", "@metamask/snaps-sdk": "^4.2.0", "@metamask/snaps-utils": "^7.4.0", "@metamask/utils": "^8.3.0", diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index 62fa534e3b5..6a607c5a711 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": "^9.1.0", "@metamask/eth-query": "^4.0.0", + "@metamask/keyring-api": "^6.3.1", "@metamask/keyring-controller": "^16.0.0", "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/network-controller": "^18.1.1", "@metamask/polling-controller": "^6.0.2", "@metamask/preferences-controller": "^11.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.1.1", "@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": "^6.0.0", "@metamask/keyring-controller": "^16.0.0", "@metamask/network-controller": "^18.0.0", - "@metamask/preferences-controller": "^11.0.0" + "@metamask/preferences-controller": "^11.0.0", + "@metamask/snaps-controllers": "^8.1.1" }, "engines": { "node": ">=16.0.0" diff --git a/packages/assets-controllers/src/BalancesController.ts b/packages/assets-controllers/src/BalancesController.ts new file mode 100644 index 00000000000..78ebd096b2e --- /dev/null +++ b/packages/assets-controllers/src/BalancesController.ts @@ -0,0 +1,186 @@ +import { type AccountsControllerGetAccountAction } from '@metamask/accounts-controller'; +import { + BaseController, + type ControllerGetStateAction, + type ControllerStateChangeEvent, + type RestrictedControllerMessenger, +} from '@metamask/base-controller'; +import { + KeyringRpcMethod, + type Balance, + type CaipAssetType, +} from '@metamask/keyring-api'; +import type { HandleSnapRequest } from '@metamask/snaps-controllers'; +import type { SnapId } from '@metamask/snaps-sdk'; +import { HandlerType } from '@metamask/snaps-utils'; +import type { Draft } from 'immer'; +import { v4 as uuid } from 'uuid'; + +const controllerName = 'BalancesController'; + +/** + * State used by the {@link BalancesController} to cache account balances. + */ +export type BalancesControllerState = { + balances: { + [account: string]: { + [asset: string]: { + amount: string; + unit: string; + }; + }; + }; +}; + +/** + * Default state of the {@link BalancesController}. + */ +const defaultState: BalancesControllerState = { balances: {} }; + +/** + * Returns the state of the {@link BalancesController}. + */ +export type GetBalancesControllerState = ControllerGetStateAction< + typeof controllerName, + BalancesControllerState +>; + +/** + * Returns the balances of an account. + */ +export type GetBalances = { + type: `${typeof controllerName}:getBalances`; + handler: BalancesController['getBalances']; +}; + +/** + * Event emitted when the state of the {@link BalancesController} changes. + */ +export type BalancesControllerStateChange = ControllerStateChangeEvent< + typeof controllerName, + BalancesControllerState +>; + +/** + * Actions exposed by the {@link BalancesController}. + */ +export type BalancesControllerActions = + | GetBalancesControllerState + | GetBalances; + +/** + * Events emitted by {@link BalancesController}. + */ +export type BalancesControllerEvents = BalancesControllerStateChange; + +/** + * Actions that this controller is allowed to call. + */ +export type AllowedActions = + | HandleSnapRequest + | AccountsControllerGetAccountAction; + +/** + * Messenger type for the BalancesController. + */ +export type BalancesControllerMessenger = RestrictedControllerMessenger< + typeof controllerName, + BalancesControllerActions | AllowedActions, + BalancesControllerEvents, + AllowedActions['type'], + never +>; + +/** + * {@link BalancesController}'s metadata. + * + * This allows us to choose if fields of the state should be persisted or not + * using the `persist` flag; and if they can be sent to Sentry or not, using + * the `anonymous` flag. + */ +const balancesControllerMetadata = { + balances: { + persist: true, + anonymous: false, + }, +}; + +/** + * The BalancesController is responsible for fetching and caching account + * balances. + */ +export class BalancesController extends BaseController< + typeof controllerName, + BalancesControllerState, + BalancesControllerMessenger +> { + constructor({ + messenger, + state, + }: { + messenger: BalancesControllerMessenger; + state: BalancesControllerState; + }) { + super({ + messenger, + name: controllerName, + metadata: balancesControllerMetadata, + state: { + ...defaultState, + ...state, + }, + }); + } + + /** + * Get the balances for an account. + * + * @param accountId - ID of the account to get balances for. + * @param assetTypes - Array of asset types to get balances for. + * @returns A map of asset types to balances. + */ + async getBalances( + accountId: string, + assetTypes: CaipAssetType[], + ): Promise> { + console.log('!!! Getting balances for account', accountId); + console.log('!!! Assets:', assetTypes); + + const account = this.messagingSystem.call( + 'AccountsController:getAccount', + accountId, + ); + if (!account) { + return {}; + } + + const snapId = account.metadata.snap?.id; + if (!snapId) { + return {}; + } + + const balances = (await this.messagingSystem.call( + 'SnapController:handleRequest', + { + snapId: snapId as SnapId, + origin: 'metamask', + handler: HandlerType.OnRpcRequest, + request: { + jsonrpc: '2.0', + id: uuid(), + method: KeyringRpcMethod.GetAccountBalances, + params: { + id: account.id, + assets: assetTypes, + }, + }, + }, + )) as Record; + + this.update((state: Draft) => { + state.balances[accountId] = balances; + }); + + return balances; + } +} diff --git a/packages/chain-controller/package.json b/packages/chain-controller/package.json index 115469530ec..a0af138bbfb 100644 --- a/packages/chain-controller/package.json +++ b/packages/chain-controller/package.json @@ -43,7 +43,7 @@ "dependencies": { "@metamask/base-controller": "^5.0.2", "@metamask/chain-api": "^0.0.1", - "@metamask/keyring-api": "^6.1.1", + "@metamask/keyring-api": "^6.3.1", "@metamask/snaps-controllers": "^8.1.1", "@metamask/snaps-sdk": "^4.2.0", "@metamask/snaps-utils": "^7.4.0", diff --git a/packages/keyring-controller/package.json b/packages/keyring-controller/package.json index fba60a48747..4e3d33692cd 100644 --- a/packages/keyring-controller/package.json +++ b/packages/keyring-controller/package.json @@ -48,7 +48,7 @@ "@metamask/eth-hd-keyring": "^7.0.1", "@metamask/eth-sig-util": "^7.0.1", "@metamask/eth-simple-keyring": "^6.0.1", - "@metamask/keyring-api": "^6.1.1", + "@metamask/keyring-api": "^6.3.1", "@metamask/message-manager": "^8.0.2", "@metamask/utils": "^8.3.0", "async-mutex": "^0.2.6", diff --git a/yarn.lock b/yarn.lock index f224633a493..5efaa948a9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1617,7 +1617,7 @@ __metadata: "@metamask/auto-changelog": ^3.4.4 "@metamask/base-controller": ^5.0.2 "@metamask/eth-snap-keyring": ^4.1.1 - "@metamask/keyring-api": ^6.1.1 + "@metamask/keyring-api": ^6.3.1 "@metamask/keyring-controller": ^16.0.0 "@metamask/snaps-controllers": ^8.1.1 "@metamask/snaps-sdk": ^4.2.0 @@ -1723,13 +1723,15 @@ __metadata: "@metamask/controller-utils": ^9.1.0 "@metamask/eth-query": ^4.0.0 "@metamask/ethjs-provider-http": ^0.3.0 - "@metamask/keyring-api": ^6.1.1 + "@metamask/keyring-api": ^6.3.1 "@metamask/keyring-controller": ^16.0.0 "@metamask/metamask-eth-abis": ^3.1.1 "@metamask/network-controller": ^18.1.1 "@metamask/polling-controller": ^6.0.2 "@metamask/preferences-controller": ^11.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 @@ -1740,6 +1742,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 @@ -1758,6 +1761,7 @@ __metadata: "@metamask/keyring-controller": ^16.0.0 "@metamask/network-controller": ^18.0.0 "@metamask/preferences-controller": ^11.0.0 + "@metamask/snaps-controllers": ^8.1.1 languageName: unknown linkType: soft @@ -1854,7 +1858,7 @@ __metadata: "@metamask/auto-changelog": ^3.4.4 "@metamask/base-controller": ^5.0.2 "@metamask/chain-api": ^0.0.1 - "@metamask/keyring-api": ^6.1.1 + "@metamask/keyring-api": ^6.3.1 "@metamask/snaps-controllers": ^8.1.1 "@metamask/snaps-sdk": ^4.2.0 "@metamask/snaps-utils": ^7.4.0 @@ -2404,6 +2408,22 @@ __metadata: languageName: node linkType: hard +"@metamask/keyring-api@npm:^6.3.1": + version: 6.3.1 + resolution: "@metamask/keyring-api@npm:6.3.1" + dependencies: + "@metamask/snaps-sdk": ^4.2.0 + "@metamask/utils": ^8.4.0 + "@types/uuid": ^9.0.8 + bech32: ^2.0.0 + superstruct: ^1.0.3 + uuid: ^9.0.1 + peerDependencies: + "@metamask/providers": ">=15 <17" + checksum: d9b5164b8fbd583dd641c917153ef9264cfb236fbe49f77351f495822f1b83b544293be4946cb15eddded31a73c2a58fd8c312a28f97a6929d8498dbf269bd0b + languageName: node + linkType: hard + "@metamask/keyring-controller@^16.0.0, @metamask/keyring-controller@workspace:packages/keyring-controller": version: 0.0.0-use.local resolution: "@metamask/keyring-controller@workspace:packages/keyring-controller" @@ -2420,7 +2440,7 @@ __metadata: "@metamask/eth-hd-keyring": ^7.0.1 "@metamask/eth-sig-util": ^7.0.1 "@metamask/eth-simple-keyring": ^6.0.1 - "@metamask/keyring-api": ^6.1.1 + "@metamask/keyring-api": ^6.3.1 "@metamask/message-manager": ^8.0.2 "@metamask/scure-bip39": ^2.1.1 "@metamask/utils": ^8.3.0 @@ -3881,7 +3901,7 @@ __metadata: languageName: node linkType: hard -"@types/uuid@npm:^9.0.1": +"@types/uuid@npm:^9.0.1, @types/uuid@npm:^9.0.8": version: 9.0.8 resolution: "@types/uuid@npm:9.0.8" checksum: b8c60b7ba8250356b5088302583d1704a4e1a13558d143c549c408bf8920535602ffc12394ede77f8a8083511b023704bc66d1345792714002bfa261b17c5275 From b8208ed10d36641cb0c5f131f73328599b3f67e6 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Mon, 3 Jun 2024 21:54:17 +0800 Subject: [PATCH 22/24] fix: mock --- packages/accounts-controller/src/tests/mocks.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/accounts-controller/src/tests/mocks.ts b/packages/accounts-controller/src/tests/mocks.ts index 59a9892a1a7..daebd1fbc34 100644 --- a/packages/accounts-controller/src/tests/mocks.ts +++ b/packages/accounts-controller/src/tests/mocks.ts @@ -6,7 +6,6 @@ import { BtcAccountType, BtcMethod, EthAccountType, - EthErc4337Method, EthMethod, } from '@metamask/keyring-api'; import { KeyringTypes } from '@metamask/keyring-controller'; @@ -50,9 +49,9 @@ export const createMockInternalAccount = ({ break; case EthAccountType.Erc4337: methods = [ - EthErc4337Method.PatchUserOperation, - EthErc4337Method.PrepareUserOperation, - EthErc4337Method.SignUserOperation, + EthMethod.PatchUserOperation, + EthMethod.PrepareUserOperation, + EthMethod.SignUserOperation, ]; break; case BtcAccountType.P2wpkh: From 782bd36a60d2a86654a9a19c1e8f38a7bbe1d71d Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Tue, 4 Jun 2024 19:40:07 +0800 Subject: [PATCH 23/24] fix: remove mock export --- packages/accounts-controller/src/index.ts | 1 - .../assets-controllers/src/AccountTrackerController.test.ts | 2 +- packages/assets-controllers/src/NftController.test.ts | 2 +- packages/assets-controllers/src/NftDetectionController.test.ts | 2 +- packages/assets-controllers/src/TokenBalancesController.test.ts | 2 +- .../assets-controllers/src/TokenDetectionController.test.ts | 2 +- packages/assets-controllers/src/TokenRatesController.test.ts | 2 +- packages/assets-controllers/src/TokensController.test.ts | 2 +- 8 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/accounts-controller/src/index.ts b/packages/accounts-controller/src/index.ts index 29505118b61..c07b5b03f26 100644 --- a/packages/accounts-controller/src/index.ts +++ b/packages/accounts-controller/src/index.ts @@ -17,4 +17,3 @@ export type { } from './AccountsController'; export { AccountsController } from './AccountsController'; export { keyringTypeToName, getUUIDFromAddressOfNormalAccount } from './utils'; -export { createMockInternalAccount } from './tests/mocks'; diff --git a/packages/assets-controllers/src/AccountTrackerController.test.ts b/packages/assets-controllers/src/AccountTrackerController.test.ts index 3a952053a57..93212d287fe 100644 --- a/packages/assets-controllers/src/AccountTrackerController.test.ts +++ b/packages/assets-controllers/src/AccountTrackerController.test.ts @@ -1,10 +1,10 @@ -import { createMockInternalAccount } from '@metamask/accounts-controller'; import { query } from '@metamask/controller-utils'; import HttpProvider from '@metamask/ethjs-provider-http'; import type { InternalAccount } from '@metamask/keyring-api'; import * as sinon from 'sinon'; import { advanceTime } from '../../../tests/helpers'; +import { createMockInternalAccount } from '../../accounts-controller/src/tests/mocks'; import { AccountTrackerController } from './AccountTrackerController'; jest.mock('@metamask/controller-utils', () => { diff --git a/packages/assets-controllers/src/NftController.test.ts b/packages/assets-controllers/src/NftController.test.ts index fa2b3af32a4..80853834c33 100644 --- a/packages/assets-controllers/src/NftController.test.ts +++ b/packages/assets-controllers/src/NftController.test.ts @@ -4,7 +4,6 @@ import type { AccountsControllerGetSelectedAccountAction, AccountsControllerSelectedEvmAccountChangeEvent, } from '@metamask/accounts-controller'; -import { createMockInternalAccount } from '@metamask/accounts-controller/src/tests/mocks'; import type { AddApprovalRequest, ApprovalStateChange, @@ -43,6 +42,7 @@ import nock from 'nock'; import * as sinon from 'sinon'; import { v4 } from 'uuid'; +import { createMockInternalAccount } from '../../accounts-controller/src/tests/mocks'; import type { ExtractAvailableAction, ExtractAvailableEvent, diff --git a/packages/assets-controllers/src/NftDetectionController.test.ts b/packages/assets-controllers/src/NftDetectionController.test.ts index 8b47997ebf8..4a989d5e155 100644 --- a/packages/assets-controllers/src/NftDetectionController.test.ts +++ b/packages/assets-controllers/src/NftDetectionController.test.ts @@ -1,5 +1,4 @@ import type { AccountsController } from '@metamask/accounts-controller'; -import { createMockInternalAccount } from '@metamask/accounts-controller'; import { ControllerMessenger } from '@metamask/base-controller'; import { NFT_API_BASE_URL, ChainId } from '@metamask/controller-utils'; import { @@ -22,6 +21,7 @@ import * as sinon from 'sinon'; import { FakeBlockTracker } from '../../../tests/fake-block-tracker'; import { FakeProvider } from '../../../tests/fake-provider'; import { advanceTime } from '../../../tests/helpers'; +import { createMockInternalAccount } from '../../accounts-controller/src/tests/mocks'; import { buildCustomNetworkClientConfiguration, buildMockGetNetworkClientById, diff --git a/packages/assets-controllers/src/TokenBalancesController.test.ts b/packages/assets-controllers/src/TokenBalancesController.test.ts index 7f43caf2782..5ac19788a47 100644 --- a/packages/assets-controllers/src/TokenBalancesController.test.ts +++ b/packages/assets-controllers/src/TokenBalancesController.test.ts @@ -1,9 +1,9 @@ -import { createMockInternalAccount } from '@metamask/accounts-controller'; import { ControllerMessenger } from '@metamask/base-controller'; import { toHex } from '@metamask/controller-utils'; import BN from 'bn.js'; import { flushPromises } from '../../../tests/helpers'; +import { createMockInternalAccount } from '../../accounts-controller/src/tests/mocks'; import type { AllowedActions, AllowedEvents, diff --git a/packages/assets-controllers/src/TokenDetectionController.test.ts b/packages/assets-controllers/src/TokenDetectionController.test.ts index 6f054c2b589..a21c05ff462 100644 --- a/packages/assets-controllers/src/TokenDetectionController.test.ts +++ b/packages/assets-controllers/src/TokenDetectionController.test.ts @@ -1,4 +1,3 @@ -import { createMockInternalAccount } from '@metamask/accounts-controller'; import type { AddApprovalRequest } from '@metamask/approval-controller'; import { ControllerMessenger } from '@metamask/base-controller'; import { @@ -28,6 +27,7 @@ import nock from 'nock'; import * as sinon from 'sinon'; import { advanceTime } from '../../../tests/helpers'; +import { createMockInternalAccount } from '../../accounts-controller/src/tests/mocks'; import { formatAggregatorNames } from './assetsUtil'; import { TOKEN_END_POINT_API } from './token-service'; import type { diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index 14b65ac96ff..3e934b54c8f 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -1,4 +1,3 @@ -import { createMockInternalAccount } from '@metamask/accounts-controller'; import { ChainId, InfuraNetworkType, @@ -20,6 +19,7 @@ import nock from 'nock'; import { useFakeTimers } from 'sinon'; import { advanceTime, flushPromises } from '../../../tests/helpers'; +import { createMockInternalAccount } from '../../accounts-controller/src/tests/mocks'; import { buildCustomNetworkClientConfiguration, buildMockGetNetworkClientById, diff --git a/packages/assets-controllers/src/TokensController.test.ts b/packages/assets-controllers/src/TokensController.test.ts index c9e259a9a8c..21fd9198fa1 100644 --- a/packages/assets-controllers/src/TokensController.test.ts +++ b/packages/assets-controllers/src/TokensController.test.ts @@ -1,6 +1,5 @@ import { Contract } from '@ethersproject/contracts'; import type { AccountsController } from '@metamask/accounts-controller'; -import { createMockInternalAccount } from '@metamask/accounts-controller'; import type { ApprovalStateChange } from '@metamask/approval-controller'; import { ApprovalController, @@ -29,6 +28,7 @@ import * as sinon from 'sinon'; import { v1 as uuidV1 } from 'uuid'; import { FakeProvider } from '../../../tests/fake-provider'; +import { createMockInternalAccount } from '../../accounts-controller/src/tests/mocks'; import type { ExtractAvailableAction, ExtractAvailableEvent, From 97a639a8199d668a9d6c92b3f6bbe98c611a2f6d Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Tue, 4 Jun 2024 19:44:44 +0800 Subject: [PATCH 24/24] chore: fix yarn lock --- packages/assets-controllers/package.json | 3 +- packages/transaction-controller/package.json | 2 +- yarn.lock | 39 +++----------------- 3 files changed, 7 insertions(+), 37 deletions(-) diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index d92352f1c90..509ecd5d4ad 100644 --- a/packages/assets-controllers/package.json +++ b/packages/assets-controllers/package.json @@ -53,7 +53,7 @@ "@metamask/contract-metadata": "^2.4.0", "@metamask/controller-utils": "^11.0.0", "@metamask/eth-query": "^4.0.0", - "@metamask/keyring-api": "^6.3.1", + "@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", @@ -76,7 +76,6 @@ "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", diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json index 0f988494691..a83eb445be2 100644 --- a/packages/transaction-controller/package.json +++ b/packages/transaction-controller/package.json @@ -52,7 +52,7 @@ "@metamask/controller-utils": "^11.0.0", "@metamask/eth-query": "^4.0.0", "@metamask/gas-fee-controller": "^17.0.0", - "@metamask/keyring-api": "6.1.1", + "@metamask/keyring-api": "6.4.0", "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/network-controller": "^19.0.0", "@metamask/nonce-tracker": "^5.0.0", diff --git a/yarn.lock b/yarn.lock index 4deb8ce847a..ffc7ddedb27 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1,3 +1,6 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + __metadata: version: 6 cacheKey: 8 @@ -2464,39 +2467,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-api@npm:6.1.1, @metamask/keyring-api@npm:^6.1.1": - version: 6.1.1 - resolution: "@metamask/keyring-api@npm:6.1.1" - dependencies: - "@metamask/snaps-sdk": ^4.2.0 - "@metamask/utils": ^8.4.0 - "@types/uuid": ^9.0.8 - bech32: ^2.0.0 - superstruct: ^1.0.3 - uuid: ^9.0.1 - peerDependencies: - "@metamask/providers": ">=15 <18" - checksum: 7845ed5fa73db3165703c2142b6062d03ca5fea329b54d28f424dee2bb393edc1f9a015e771289ef7236c31f30355bf2c52ad74bb47cf531c09c5eec66e06b00 - languageName: node - linkType: hard - -"@metamask/keyring-api@npm:^6.3.1": - version: 6.3.1 - resolution: "@metamask/keyring-api@npm:6.3.1" - dependencies: - "@metamask/snaps-sdk": ^4.2.0 - "@metamask/utils": ^8.4.0 - "@types/uuid": ^9.0.8 - bech32: ^2.0.0 - superstruct: ^1.0.3 - uuid: ^9.0.1 - peerDependencies: - "@metamask/providers": ">=15 <17" - checksum: d9b5164b8fbd583dd641c917153ef9264cfb236fbe49f77351f495822f1b83b544293be4946cb15eddded31a73c2a58fd8c312a28f97a6929d8498dbf269bd0b - languageName: node - linkType: hard - -"@metamask/keyring-api@npm:^6.3.1, @metamask/keyring-api@npm:^6.4.0": +"@metamask/keyring-api@npm:6.4.0, @metamask/keyring-api@npm:^6.3.1, @metamask/keyring-api@npm:^6.4.0": version: 6.4.0 resolution: "@metamask/keyring-api@npm:6.4.0" dependencies: @@ -3182,7 +3153,7 @@ __metadata: "@metamask/eth-query": ^4.0.0 "@metamask/ethjs-provider-http": ^0.3.0 "@metamask/gas-fee-controller": ^17.0.0 - "@metamask/keyring-api": 6.1.1 + "@metamask/keyring-api": 6.4.0 "@metamask/metamask-eth-abis": ^3.1.1 "@metamask/network-controller": ^19.0.0 "@metamask/nonce-tracker": ^5.0.0