From 0eff976d8d0c9e56b4e5a2153a1ebc7017f87403 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Fri, 26 Apr 2024 10:00:00 +0800 Subject: [PATCH 01/23] feat: add mocks and evm check to accounts controller --- packages/accounts-controller/src/index.ts | 6 +- .../accounts-controller/src/tests/mocks.ts | 59 +++++++++++++++++++ packages/accounts-controller/src/utils.ts | 13 ++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 packages/accounts-controller/src/tests/mocks.ts diff --git a/packages/accounts-controller/src/index.ts b/packages/accounts-controller/src/index.ts index 274efa5d5b1..d49ff52ab05 100644 --- a/packages/accounts-controller/src/index.ts +++ b/packages/accounts-controller/src/index.ts @@ -15,4 +15,8 @@ export type { AccountsControllerMessenger, } from './AccountsController'; export { AccountsController } from './AccountsController'; -export { keyringTypeToName, getUUIDFromAddressOfNormalAccount } from './utils'; +export { + isEVMAccount, + keyringTypeToName, + getUUIDFromAddressOfNormalAccount, +} from './utils'; diff --git a/packages/accounts-controller/src/tests/mocks.ts b/packages/accounts-controller/src/tests/mocks.ts new file mode 100644 index 00000000000..3d743803d3b --- /dev/null +++ b/packages/accounts-controller/src/tests/mocks.ts @@ -0,0 +1,59 @@ +import type { InternalAccount } from '@metamask/keyring-api'; +import { EthAccountType, 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?: EthAccountType; + name?: string; + keyringType?: KeyringTypes; + snap?: { + id: string; + enabled: boolean; + name: string; + }; + importTime?: number; + lastSelected?: number; +} = {}): InternalAccount => { + const methods = + type === EthAccountType.Eoa + ? [ + EthMethod.PersonalSign, + EthMethod.Sign, + EthMethod.SignTransaction, + EthMethod.SignTypedDataV1, + EthMethod.SignTypedDataV3, + EthMethod.SignTypedDataV4, + ] + : [ + EthMethod.PatchUserOperation, + EthMethod.PrepareUserOperation, + EthMethod.SignUserOperation, + ]; + + return { + id, + address, + options: {}, + methods, + type: EthAccountType.Eoa, + metadata: { + name, + keyring: { type: keyringType }, + importTime, + lastSelected, + snap: snap && snap, + }, + }; +}; diff --git a/packages/accounts-controller/src/utils.ts b/packages/accounts-controller/src/utils.ts index b3e7cbd639d..0668c18c387 100644 --- a/packages/accounts-controller/src/utils.ts +++ b/packages/accounts-controller/src/utils.ts @@ -1,4 +1,6 @@ import { toBuffer } from '@ethereumjs/util'; +import type { InternalAccount } from '@metamask/keyring-api'; +import { EthAccountType } from '@metamask/keyring-api'; import { isCustodyKeyring, KeyringTypes } from '@metamask/keyring-controller'; import { sha256 } from 'ethereum-cryptography/sha256'; import type { V4Options } from 'uuid'; @@ -68,3 +70,14 @@ export function getUUIDOptionsFromAddressOfNormalAccount( export function getUUIDFromAddressOfNormalAccount(address: string): string { return uuid(getUUIDOptionsFromAddressOfNormalAccount(address)); } + +/** + * Checks if the given internal account is an EVM account. + * @param internalAccount - The internal account to check. + * @returns True if the internal account is an EVM account, false otherwise. + */ +export function isEVMAccount(internalAccount: InternalAccount): boolean { + return [EthAccountType.Eoa, EthAccountType.Erc4337].includes( + internalAccount?.type as EthAccountType, + ); +} From 83250ef7aafdbde5cee7d2c66bf2bc2913b6c243 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Fri, 26 Apr 2024 20:29:52 +0800 Subject: [PATCH 02/23] feat: add mocks and util function for non evm --- .../src/tests/mocks.test.ts | 52 +++++++++++++++++++ .../accounts-controller/src/tests/mocks.ts | 2 +- .../accounts-controller/src/utils.test.ts | 18 +++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 packages/accounts-controller/src/tests/mocks.test.ts create mode 100644 packages/accounts-controller/src/utils.test.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..b59b7a51a98 --- /dev/null +++ b/packages/accounts-controller/src/tests/mocks.test.ts @@ -0,0 +1,52 @@ +import { 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, + }, + }); + }); +}); diff --git a/packages/accounts-controller/src/tests/mocks.ts b/packages/accounts-controller/src/tests/mocks.ts index 3d743803d3b..05a435fe74e 100644 --- a/packages/accounts-controller/src/tests/mocks.ts +++ b/packages/accounts-controller/src/tests/mocks.ts @@ -47,7 +47,7 @@ export const createMockInternalAccount = ({ address, options: {}, methods, - type: EthAccountType.Eoa, + type, metadata: { name, keyring: { type: keyringType }, diff --git a/packages/accounts-controller/src/utils.test.ts b/packages/accounts-controller/src/utils.test.ts new file mode 100644 index 00000000000..010f8848491 --- /dev/null +++ b/packages/accounts-controller/src/utils.test.ts @@ -0,0 +1,18 @@ +import { EthAccountType } from '@metamask/keyring-api'; + +import { createMockInternalAccount } from './tests/mocks'; +import { isEVMAccount } from './utils'; + +describe('isEVMAccount', () => { + it.each([ + [EthAccountType.Eoa, true], + [EthAccountType.Erc4337, true], + ['bip122', false], + ])('%s should return %s', (accountType, expected) => { + expect( + isEVMAccount( + createMockInternalAccount({ type: accountType as EthAccountType }), + ), + ).toBe(expected); + }); +}); From fafd322a6d701d07cb6f2c071129619611227438 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Fri, 26 Apr 2024 20:30:23 +0800 Subject: [PATCH 03/23] fix: update NftDetectionController to use selectedInternalId instead of selectedAddress --- .../src/NftDetectionController.test.ts | 397 +++++++++++++----- .../src/NftDetectionController.ts | 82 ++-- 2 files changed, 359 insertions(+), 120 deletions(-) diff --git a/packages/assets-controllers/src/NftDetectionController.test.ts b/packages/assets-controllers/src/NftDetectionController.test.ts index 9f06ee94215..28ab48bf462 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/src/tests/mocks'; 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(); @@ -264,7 +267,6 @@ describe('NftDetectionController', () => { }, ], }); - console.log(nock.activeMocks()); }); afterEach(() => { @@ -277,15 +279,21 @@ 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); await withController( - { config: { interval: 10 } }, + { + config: { interval: 10, selectedAccountId: defaultSelectedAccount.id }, + options: { getInternalAccount: mockGetInternalAccount }, + }, async ({ controller, triggerPreferencesStateChange }) => { const mockNfts = sinon.stub(controller, 'detectNfts'); triggerPreferencesStateChange({ @@ -311,51 +319,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 +397,24 @@ 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(); await withController( - { options: { addNft: mockAddNft } }, - async ({ controller, triggerPreferencesStateChange }) => { - const selectedAddress = '0x1'; + { + options: { + addNft: mockAddNft, + getInternalAccount: mockGetInternalAccount, + }, + }, + async ({ + controller, + triggerPreferencesStateChange, + triggerSelectedAccountChange, + }) => { + const selectedAccount = createMockInternalAccount({ address: '0x1' }); + mockGetInternalAccount.mockReturnValue(selectedAccount); + triggerSelectedAccountChange(selectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useNftDetection: true, }); // Wait for detect call triggered by preferences state change to settle @@ -409,7 +437,7 @@ describe('NftDetectionController', () => { standard: 'ERC721', imageOriginal: 'imageOriginal/2574.png', }, - userAddress: selectedAddress, + userAddress: selectedAccount.address, source: Source.Detected, networkClientId: undefined, }, @@ -420,13 +448,29 @@ describe('NftDetectionController', () => { it('should detect and add NFTs correctly when blockaid result is in response', async () => { const mockAddNft = jest.fn(); + const mockGetInternalAccount = jest.fn(); await withController( - { options: { addNft: mockAddNft } }, - async ({ controller, triggerPreferencesStateChange }) => { - const selectedAddress = '0x123'; + { + config: { selectedAccountId: defaultSelectedAccount.id }, + options: { + addNft: mockAddNft, + getInternalAccount: mockGetInternalAccount, + }, + }, + async ({ + controller, + triggerPreferencesStateChange, + triggerSelectedAccountChange, + }) => { + const updatedSelectedAccount = createMockInternalAccount({ + address: '0x123', + }); + mockGetInternalAccount + .mockReturnValueOnce(defaultSelectedAccount) + .mockReturnValue(updatedSelectedAccount); + triggerSelectedAccountChange(updatedSelectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useNftDetection: true, }); // Wait for detect call triggered by preferences state change to settle @@ -447,7 +491,7 @@ describe('NftDetectionController', () => { standard: 'ERC721', imageOriginal: 'imageOriginal/2574.png', }, - userAddress: selectedAddress, + userAddress: updatedSelectedAccount.address, source: Source.Detected, networkClientId: undefined, }); @@ -459,7 +503,7 @@ describe('NftDetectionController', () => { standard: 'ERC721', imageOriginal: 'imageOriginal/2575.png', }, - userAddress: selectedAddress, + userAddress: updatedSelectedAccount.address, source: Source.Detected, networkClientId: undefined, }); @@ -469,13 +513,26 @@ describe('NftDetectionController', () => { it('should detect and add NFTs and filter them correctly', async () => { const mockAddNft = jest.fn(); + const mockGetInternalAccount = jest.fn(); await withController( - { options: { addNft: mockAddNft } }, - async ({ controller, triggerPreferencesStateChange }) => { - const selectedAddress = '0x12345'; + { + options: { + addNft: mockAddNft, + getInternalAccount: mockGetInternalAccount, + }, + }, + async ({ + controller, + triggerPreferencesStateChange, + triggerSelectedAccountChange, + }) => { + const updatedSelectedAccount = createMockInternalAccount({ + address: '0x12345', + }); + mockGetInternalAccount.mockReturnValue(updatedSelectedAccount); + triggerSelectedAccountChange(updatedSelectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useNftDetection: true, }); // Wait for detect call triggered by preferences state change to settle @@ -502,7 +559,7 @@ describe('NftDetectionController', () => { standard: 'ERC721', imageOriginal: 'imageOriginal/1.png', }, - userAddress: selectedAddress, + userAddress: updatedSelectedAccount.address, source: Source.Detected, networkClientId: undefined, }, @@ -519,7 +576,7 @@ describe('NftDetectionController', () => { standard: 'ERC721', imageOriginal: 'imageOriginal/2.png', }, - userAddress: selectedAddress, + userAddress: updatedSelectedAccount.address, source: Source.Detected, networkClientId: undefined, }, @@ -530,13 +587,26 @@ describe('NftDetectionController', () => { it('should detect and add NFTs by networkClientId correctly', async () => { const mockAddNft = jest.fn(); + const mockGetInternalAccount = jest.fn(); await withController( - { options: { addNft: mockAddNft } }, - async ({ controller, triggerPreferencesStateChange }) => { - const selectedAddress = '0x1'; + { + options: { + addNft: mockAddNft, + getInternalAccount: mockGetInternalAccount, + }, + }, + async ({ + controller, + triggerPreferencesStateChange, + triggerSelectedAccountChange, + }) => { + const updatedSelectedAccount = createMockInternalAccount({ + address: '0x1', + }); + mockGetInternalAccount.mockReturnValue(updatedSelectedAccount); + triggerSelectedAccountChange(updatedSelectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useNftDetection: true, }); // Wait for detect call triggered by preferences state change to settle @@ -573,6 +643,7 @@ describe('NftDetectionController', () => { it('should not autodetect NFTs that exist in the ignoreList', async () => { const mockAddNft = jest.fn(); + const mockGetInternalAccount = jest.fn(); const mockGetNftState = jest.fn().mockImplementation(() => { return { ...getDefaultNftState(), @@ -588,12 +659,23 @@ describe('NftDetectionController', () => { }; }); await withController( - { options: { addNft: mockAddNft, getNftState: mockGetNftState } }, - async ({ controller, triggerPreferencesStateChange }) => { - const selectedAddress = '0x9'; + { + options: { + addNft: mockAddNft, + getNftState: mockGetNftState, + getInternalAccount: mockGetInternalAccount, + }, + }, + async ({ + controller, + triggerPreferencesStateChange, + triggerSelectedAccountChange, + }) => { + const selectedAccount = createMockInternalAccount({ address: '0x9' }); + mockGetInternalAccount.mockReturnValue(selectedAccount); + triggerSelectedAccountChange(selectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useNftDetection: true, }); // Wait for detect call triggered by preferences state change to settle @@ -613,17 +695,43 @@ describe('NftDetectionController', () => { it('should not detect and add NFTs if there is no selectedAddress', async () => { const mockAddNft = jest.fn(); await withController( - { options: { addNft: mockAddNft } }, + { config: { selectedAccountId: '' }, options: { addNft: mockAddNft } }, + async ({ controller, triggerPreferencesStateChange }) => { + triggerPreferencesStateChange({ + ...getDefaultPreferencesState(), + useNftDetection: true, // auto-detect is enabled so it proceeds to check userAddress + }); + + // confirm that default selected address is an empty string + expect(controller.config.selectedAccountId).toBe(''); + + await controller.detectNfts(); + + expect(mockAddNft).not.toHaveBeenCalled(); + }, + ); + }); + + it('should not detect and add NFTs if the account is a nonevm account', async () => { + const mockAddNft = jest.fn(); + // @ts-expect-error create a nonevm account + const nonEvmAccount = createMockInternalAccount({ type: 'bitcoin' }); + const mockGetInternalAccount = jest.fn().mockReturnValue(nonEvmAccount); + await withController( + { + options: { + addNft: mockAddNft, + getInternalAccount: mockGetInternalAccount, + }, + }, 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(''); + expect(controller.config.selectedAccountId).toBe(''); await controller.detectNfts(); @@ -685,13 +793,24 @@ 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(); await withController( - { options: { addNft: mockAddNft } }, - async ({ controller, triggerPreferencesStateChange }) => { - const selectedAddress = '0x9'; + { + options: { + addNft: mockAddNft, + getInternalAccount: mockGetInternalAccount, + }, + }, + async ({ + controller, + triggerPreferencesStateChange, + triggerSelectedAccountChange, + }) => { + const selectedAccount = createMockInternalAccount({ address: '0x9' }); + mockGetInternalAccount.mockReturnValue(selectedAccount); + triggerSelectedAccountChange(selectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useNftDetection: false, }); // Wait for detect call triggered by preferences state change to settle @@ -709,9 +828,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 +840,23 @@ describe('NftDetectionController', () => { .replyWithError(new Error('Failed to fetch')) .persist(); const mockAddNft = jest.fn(); + const mockGetInternalAccount = jest.fn().mockReturnValue(selectedAccount); await withController( - { options: { addNft: mockAddNft } }, - async ({ controller, triggerPreferencesStateChange }) => { + { + options: { + addNft: mockAddNft, + getInternalAccount: mockGetInternalAccount, + }, + }, + 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 +874,18 @@ 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); await withController( - async ({ controller, triggerPreferencesStateChange }) => { + { options: { getInternalAccount: mockGetInternalAccount } }, + 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 +895,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 +907,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 +925,26 @@ describe('NftDetectionController', () => { it('should rethrow error when attempt to add NFT fails', async () => { const mockAddNft = jest.fn(); + const mockGetInternalAccount = jest.fn(); await withController( - { options: { addNft: mockAddNft } }, - async ({ controller, triggerPreferencesStateChange }) => { - const selectedAddress = '0x1'; + { + options: { + addNft: mockAddNft, + getInternalAccount: mockGetInternalAccount, + }, + }, + async ({ + controller, + triggerPreferencesStateChange, + triggerSelectedAccountChange, + }) => { + const selectedAccount = createMockInternalAccount({ + address: '0x1', + }); + mockGetInternalAccount.mockReturnValue(selectedAccount); + triggerSelectedAccountChange(selectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useNftDetection: true, }); // Wait for detect call triggered by preferences state change to settle @@ -814,8 +963,14 @@ describe('NftDetectionController', () => { }); it('should only re-detect when relevant settings change', async () => { + const mockGetInternalAccount = jest + .fn() + .mockReturnValue(defaultSelectedAccount); await withController( - {}, + { + config: { selectedAccountId: defaultSelectedAccount.id }, + options: { getInternalAccount: mockGetInternalAccount }, + }, async ({ controller, triggerPreferencesStateChange }) => { const detectNfts = sinon.stub(controller, 'detectNfts'); @@ -840,6 +995,46 @@ describe('NftDetectionController', () => { }, ); }); + + it('should only re-detect when the selected account changes to a evm account', async () => { + const mockGetInternalAccount = jest.fn(); + await withController( + { + config: { + selectedAccountId: defaultSelectedAccount.id, + disabled: false, + }, + options: { getInternalAccount: mockGetInternalAccount }, + }, + async ({ controller, triggerSelectedAccountChange }) => { + const detectNfts = sinon.stub(controller, 'detectNfts'); + + // Same accounts shouldn't trigger detections + mockGetInternalAccount.mockReturnValueOnce(defaultSelectedAccount); + triggerSelectedAccountChange(defaultSelectedAccount); + await advanceTime({ clock, duration: 1 }); + expect(detectNfts.callCount).toBe(0); + + // Repeated account changes should only trigger 1 detection + for (let i = 0; i < 5; i++) { + const newAccount = createMockInternalAccount(); + mockGetInternalAccount.mockReturnValue(newAccount); + triggerSelectedAccountChange(newAccount); + } + await advanceTime({ clock, duration: 1 }); + expect(detectNfts.callCount).toBe(5); + detectNfts.resetHistory(); + + // Irrelevant account changes shouldn't trigger a detection + // @ts-expect-error create a nonevm account + const nonevmAccount = createMockInternalAccount({ type: 'bitcoin' }); + mockGetInternalAccount.mockReturnValue(nonevmAccount); + triggerSelectedAccountChange(nonevmAccount); + await advanceTime({ clock, duration: 1 }); + expect(detectNfts.callCount).toBe(0); + }, + ); + }); }); type WithControllerCallback = ({ @@ -848,6 +1043,7 @@ type WithControllerCallback = ({ controller: NftDetectionController; triggerNftStateChange: (state: NftState) => void; triggerPreferencesStateChange: (state: PreferencesState) => void; + triggerSelectedAccountChange: (account: InternalAccount) => void; }) => Promise | ReturnValue; type WithControllerOptions = { @@ -888,6 +1084,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 +1095,18 @@ 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(), getNftState: getDefaultNftState, disabled: true, - selectedAddress: '', + selectedAccountId: '', ...options, }, config, @@ -922,6 +1124,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..5f72d3def2c 100644 --- a/packages/assets-controllers/src/NftDetectionController.ts +++ b/packages/assets-controllers/src/NftDetectionController.ts @@ -1,3 +1,4 @@ +import { isEVMAccount } 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,8 @@ export class NftDetectionController extends StaticIntervalPollingControllerV1< private readonly getNetworkClientById: NetworkController['getNetworkClientById']; + private readonly getInternalAccount: (accountId: string) => InternalAccount; + /** * Creates an NftDetectionController instance. * @@ -425,13 +429,15 @@ 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 config - Initial options used to configure this controller. * @param state - Initial state to set on this controller. */ @@ -441,12 +447,14 @@ export class NftDetectionController extends StaticIntervalPollingControllerV1< getNetworkClientById, onPreferencesStateChange, onNetworkStateChange, + onSelectedAccountChange, getOpenSeaApiKey, addNft, getNftApi, getNftState, + getInternalAccount, disabled: initialDisabled, - selectedAddress: initialSelectedAddress, + selectedAccountId: initialSelectedAccountId, }: { chainId: Hex; getNetworkClientById: NetworkController['getNetworkClientById']; @@ -457,12 +465,16 @@ 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; disabled: boolean; - selectedAddress: string; + selectedAccountId: string; }, config?: Partial, state?: Partial, @@ -471,22 +483,36 @@ 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.config; - - if ( - selectedAddress !== previouslySelectedAddress || - !useNftDetection !== disabled - ) { - this.configure({ selectedAddress, disabled: !useNftDetection }); - if (useNftDetection) { + this.getInternalAccount = getInternalAccount; + onSelectedAccountChange((internalAccount) => { + if (!isEVMAccount(internalAccount)) { + this.configure({ selectedAccountId: '' }); + this.stop(); + return; + } + const { selectedAccountId, disabled } = this.config; + if (!disabled && selectedAccountId !== internalAccount.id) { + this.configure({ selectedAccountId: internalAccount.id }); + this.start(); + } else { + this.stop(); + } + }); + + onPreferencesStateChange(({ useNftDetection }) => { + const { disabled, selectedAccountId } = this.config; + const selectedInternalAccount = + this.getInternalAccount(selectedAccountId); + + if (!useNftDetection !== disabled) { + this.configure({ disabled: !useNftDetection }); + if (useNftDetection && isEVMAccount(selectedInternalAccount)) { this.start(); } else { this.stop(); @@ -569,15 +595,21 @@ 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 selectedInternalAccount = this.getInternalAccount(selectedAccountId); + if (!isEVMAccount(selectedInternalAccount)) { + return; + } + + userAddress = userAddress || selectedInternalAccount.address; + /* istanbul ignore if */ if (!this.isMainnet() || this.disabled) { return; From 61ace68cfa8f9de5d8d2fff3f9419e5712809619 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Fri, 26 Apr 2024 21:12:26 +0800 Subject: [PATCH 04/23] fix: update NftController to use selectedAccountId instead of selectedAddress --- .../src/NftController.test.ts | 777 +++++++++++++----- .../assets-controllers/src/NftController.ts | 233 ++++-- 2 files changed, 755 insertions(+), 255 deletions(-) diff --git a/packages/assets-controllers/src/NftController.test.ts b/packages/assets-controllers/src/NftController.test.ts index 4342f801439..d469d89e8ea 100644 --- a/packages/assets-controllers/src/NftController.test.ts +++ b/packages/assets-controllers/src/NftController.test.ts @@ -1,4 +1,9 @@ import type { Network } from '@ethersproject/providers'; +import type { + AccountsControllerGetAccountAction, + AccountsControllerSelectedAccountChangeEvent, +} from '@metamask/accounts-controller'; +import { createMockInternalAccount } from '@metamask/accounts-controller/src/tests/mocks'; import type { AddApprovalRequest, ApprovalStateChange, @@ -17,6 +22,7 @@ import { NetworksTicker, NFT_API_BASE_URL, } from '@metamask/controller-utils'; +import type { InternalAccount } from '@metamask/keyring-api'; import type { NetworkState, ProviderConfig, @@ -48,6 +54,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 +81,10 @@ const GOERLI = { ticker: NetworksTicker.goerli, }; -type ApprovalActions = AddApprovalRequest; -type ApprovalEvents = ApprovalStateChange; +type ApprovalActions = AddApprovalRequest | AccountsControllerGetAccountAction; +type ApprovalEvents = + | ApprovalStateChange + | AccountsControllerSelectedAccountChangeEvent; const controllerName = 'NftController' as const; @@ -122,6 +135,13 @@ function setupController( const messenger = new ControllerMessenger(); + const getInternalAccountMock = jest.fn().mockReturnValue(OWNER_ACCOUNT); + + messenger.registerActionHandler( + 'AccountsController:getAccount', + getInternalAccountMock, + ); + const approvalControllerMessenger = messenger.getRestricted({ name: 'ApprovalController', allowedActions: [], @@ -162,11 +182,18 @@ function setupController( const nftControllerMessenger = messenger.getRestricted< typeof controllerName, - ApprovalActions['type'] + ApprovalActions['type'], + Extract< + ApprovalEvents, + AccountsControllerSelectedAccountChangeEvent + >['type'] >({ name: controllerName, - allowedActions: ['ApprovalController:addRequest'], - allowedEvents: [], + allowedActions: [ + 'ApprovalController:addRequest', + 'AccountsController:getAccount', + ], + allowedEvents: ['AccountsController:selectedAccountChange'], }); const preferencesStateChangeListeners: ((state: PreferencesState) => void)[] = @@ -197,15 +224,27 @@ function setupController( triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, - selectedAddress: OWNER_ADDRESS, }); + const triggerSelectedAccountChange = ( + internalAccount: InternalAccount, + ): void => { + messenger.publish( + 'AccountsController:selectedAccountChange', + internalAccount, + ); + }; + + triggerSelectedAccountChange(OWNER_ACCOUNT); + return { nftController, changeNetwork, messenger, approvalController, triggerPreferencesStateChange, + triggerSelectedAccountChange, + getInternalAccountMock, }; } @@ -392,12 +431,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 +464,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 +504,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 +569,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 +634,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 +699,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 +767,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 +831,6 @@ describe('NftController', () => { ...getDefaultPreferencesState(), isIpfsGatewayEnabled: true, openSeaEnabled: true, - selectedAddress: OWNER_ADDRESS, }); const requestId = 'approval-request-id-1'; @@ -764,15 +838,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, @@ -1014,6 +1094,24 @@ describe('NftController', () => { "Unable to verify ownership. Possibly because the standard is not supported or the user's currently selected network does not match the chain of the asset in question.", ); }); + + it('should return if the selected account is a non evm account', async () => { + // @ts-expect-error creating a non evm account + const nonEvmAccount = createMockInternalAccount({ type: 'bip122' }); + const { + nftController, + triggerSelectedAccountChange, + getInternalAccountMock, + } = setupController(); + + getInternalAccountMock.mockReturnValue(nonEvmAccount); + triggerSelectedAccountChange(nonEvmAccount); + + const addNftSpy = jest.spyOn(nftController, 'addNft'); + await nftController.watchNft(ERC721_NFT, ERC721, 'https://test-dapp.com'); + + expect(addNftSpy).not.toHaveBeenCalled(); + }); }); describe('addNft', () => { @@ -1022,7 +1120,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 +1136,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 +1153,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 +1215,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 +1269,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 +1282,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 +1305,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 +1320,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 +1342,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 +1360,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 +1414,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 +1434,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 +1465,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 +1504,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 +1514,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 +1529,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 +1540,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 +1552,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 +1577,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 +1602,7 @@ describe('NftController', () => { await nftController.addNft('0x01234abcdefg', '1234'); expect(nftController.state.allNftContracts).toStrictEqual({ - [selectedAddress]: { + [OWNER_ACCOUNT.address]: { [chainId]: [ { address: '0x01234abcdefg', @@ -1500,7 +1612,7 @@ describe('NftController', () => { }); expect(nftController.state.allNfts).toStrictEqual({ - [selectedAddress]: { + [OWNER_ACCOUNT.address]: { [chainId]: [ { address: '0x01234abcdefg', @@ -1640,31 +1752,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 +1797,7 @@ describe('NftController', () => { ]); expect( - nftController.state.allNftContracts[selectedAddress][chainId], + nftController.state.allNftContracts[OWNER_ACCOUNT.address][chainId], ).toStrictEqual([ { address: ERC721_KUDOSADDRESS, @@ -1721,18 +1833,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 +1853,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 +1874,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 +1894,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 +1926,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 +1934,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 +1942,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 +1965,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, @@ -2101,14 +2211,42 @@ describe('NftController', () => { }, ]); }); + + it('should return if the selected account is a non evm account', async () => { + // @ts-expect-error creating a non evm account + const nonEvmAccount = createMockInternalAccount({ type: 'bip122' }); + const { + nftController, + triggerSelectedAccountChange, + getInternalAccountMock, + } = setupController(); + + getInternalAccountMock.mockReturnValue(nonEvmAccount); + triggerSelectedAccountChange(nonEvmAccount); + + await nftController.addNft('0x01', '1234'); + expect(nftController.state.allNfts).toStrictEqual({}); + }); }); 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 +2258,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 +2292,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 +2319,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 +2346,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', @@ -2279,12 +2443,29 @@ describe('NftController', () => { isCurrentlyOwned: true, }); }); + + it('should return if the selected account is a non evm account', async () => { + // @ts-expect-error creating a non evm account + const nonEvmAccount = createMockInternalAccount({ type: 'bip122' }); + const { + nftController, + triggerSelectedAccountChange, + getInternalAccountMock, + } = setupController(); + + getInternalAccountMock.mockReturnValue(nonEvmAccount); + triggerSelectedAccountChange(nonEvmAccount); + + const isNftOwnerSpy = jest.spyOn(nftController, 'isNftOwner'); + await nftController.addNftVerifyOwnership('0x01', '1234'); + expect(isNftOwnerSpy).not.toHaveBeenCalled(); + }); }); 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 +2477,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 +2508,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 +2530,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 +2578,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 +2590,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 +2609,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 +2659,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 @@ -2470,11 +2680,34 @@ describe('NftController', () => { nftController.state.allNftContracts[userAddress1][SEPOLIA.chainId], ).toHaveLength(0); }); + + it('should return if the selected account is a non evm account', async () => { + // @ts-expect-error creating a non evm account + const nonEvmAccount = createMockInternalAccount({ type: 'bip122' }); + const { nftController, getInternalAccountMock } = setupController(); + + getInternalAccountMock.mockReturnValue(OWNER_ACCOUNT); + await nftController.addNft('0x01', '1', { + nftMetadata: { + name: 'name', + image: 'image', + description: 'description', + standard: 'standard', + }, + }); + + getInternalAccountMock.mockReturnValue(nonEvmAccount); + nftController.removeNft('0x01', '1'); + + expect( + Object.values(nftController.state.allNfts[OWNER_ACCOUNT.address]), + ).toHaveLength(1); + }); }); 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 +2719,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 +2866,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 +2889,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 +2903,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 +2914,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 +2928,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 +2940,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 +2954,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 +2970,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 +2982,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 +2996,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 +3019,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 +3033,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 +3047,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 +3070,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 +3084,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 +3123,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 +3133,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 +3147,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, @@ -2913,6 +3161,21 @@ describe('NftController', () => { }), ); }); + + it('should return if the selected account is a non evm account', async () => { + // @ts-expect-error creating a non evm account + const nonEvmAccount = createMockInternalAccount({ type: 'bip122' }); + const { nftController, getInternalAccountMock } = setupController(); + + const updateSpy = jest.spyOn(nftController, 'update'); + getInternalAccountMock.mockReturnValue(nonEvmAccount); + nftController.updateNftFavoriteStatus( + ERC721_DEPRESSIONIST_ADDRESS, + ERC721_DEPRESSIONIST_ID, + true, + ); + expect(updateSpy).not.toHaveBeenCalled(); + }); }); describe('checkAndUpdateNftsOwnershipStatus', () => { @@ -2923,7 +3186,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 +3198,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 +3215,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 +3227,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 +3246,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 +3258,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 +3276,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 +3291,7 @@ describe('NftController', () => { }); expect( - nftController.state.allNfts[selectedAddress][chainId][0] + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0] .isCurrentlyOwned, ).toBe(true); @@ -3040,7 +3302,6 @@ describe('NftController', () => { triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, - selectedAddress: SECOND_OWNER_ADDRESS, }); changeNetwork(GOERLI); @@ -3054,12 +3315,30 @@ describe('NftController', () => { .isCurrentlyOwned, ).toBe(false); }); + + it('should return if the selected account is a non evm account', async () => { + // @ts-expect-error creating a non evm account + const nonEvmAccount = createMockInternalAccount({ type: 'bip122' }); + const { nftController, getInternalAccountMock } = setupController(); + + const checkAndUpdateSingleNftOwnershipStatusSpy = jest.spyOn( + nftController, + 'checkAndUpdateSingleNftOwnershipStatus', + ); + getInternalAccountMock.mockReturnValue(nonEvmAccount); + await nftController.checkAndUpdateAllNftsOwnershipStatus({ + networkClientId: 'sepolia', + }); + expect( + checkAndUpdateSingleNftOwnershipStatusSpy, + ).not.toHaveBeenCalled(); + }); }); 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 +3354,7 @@ describe('NftController', () => { }); expect( - nftController.state.allNfts[selectedAddress][chainId][0] + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0] .isCurrentlyOwned, ).toBe(true); @@ -3086,14 +3365,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 +3388,7 @@ describe('NftController', () => { }); expect( - nftController.state.allNfts[selectedAddress][chainId][0] + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0] .isCurrentlyOwned, ).toBe(true); @@ -3121,11 +3400,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 +3418,7 @@ describe('NftController', () => { }); changeNetwork(SEPOLIA); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; const nft = { address: '0x02', tokenId: '1', @@ -3155,7 +3434,7 @@ describe('NftController', () => { }); expect( - nftController.state.allNfts[selectedAddress][chainId][0] + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0] .isCurrentlyOwned, ).toBe(true); @@ -3192,7 +3471,7 @@ describe('NftController', () => { }); changeNetwork(SEPOLIA); - const { selectedAddress, chainId } = nftController.config; + const { chainId } = nftController.config; const nft = { address: '0x02', tokenId: '1', @@ -3208,7 +3487,7 @@ describe('NftController', () => { }); expect( - nftController.state.allNfts[selectedAddress][chainId][0] + nftController.state.allNfts[OWNER_ACCOUNT.address][chainId][0] .isCurrentlyOwned, ).toBe(true); @@ -3244,6 +3523,34 @@ describe('NftController', () => { ).toBe(false); }); }); + + it('should throw error if the selected account is a non evm account', async () => { + // @ts-expect-error creating a non evm account + const nonEvmAccount = createMockInternalAccount({ type: 'bip122' }); + const { nftController, getInternalAccountMock } = setupController(); + + const nft = { + address: '0x02', + tokenId: '1', + name: 'name', + image: 'image', + description: 'description', + standard: 'standard', + favorite: false, + }; + + getInternalAccountMock.mockReturnValue(nonEvmAccount); + await expect( + async () => + await nftController.checkAndUpdateSingleNftOwnershipStatus( + nft, + false, + { + networkClientId: SEPOLIA.type, + }, + ), + ).rejects.toThrow('Non EVM Account selected'); + }); }); describe('findNftByAddressAndTokenId', () => { @@ -3257,14 +3564,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 +3579,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 +3618,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 +3630,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 +3646,7 @@ describe('NftController', () => { { transactionId: mockTransactionId, }, - selectedAddress, + OWNER_ACCOUNT.address, chainId, ), ).toBeUndefined(); @@ -3363,13 +3670,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 +3684,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 +3710,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 +3745,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 +3761,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', { @@ -3489,7 +3796,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: '', @@ -3504,7 +3811,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 @@ -3595,7 +3901,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', @@ -3744,5 +4050,100 @@ describe('NftController', () => { expect(spy).toHaveBeenCalledTimes(1); }); + + it('should return if the selected account is a non evm account', async () => { + // @ts-expect-error creating a non evm account + const nonEvmAccount = createMockInternalAccount({ type: 'bip122' }); + const { nftController, getInternalAccountMock } = setupController(); + + getInternalAccountMock.mockReturnValue(OWNER_ACCOUNT); + + await nftController.addNft('0xtest', '1', { + nftMetadata: { + name: '', + description: '', + image: '', + standard: 'ERC721', + }, + userAddress: OWNER_ADDRESS, + }); + + sinon + .stub(nftController, 'getNftInformation' as keyof typeof nftController) + .onFirstCall() + .returns({ + name: 'name pudgy 1', + image: 'url pudgy 1', + description: 'description pudgy 2', + }) + .onSecondCall() + .returns({ + name: 'name pudgy 2', + image: 'url pudgy 2', + description: 'description pudgy 2', + }) + .onThirdCall() + .rejects(new Error('Error')); + + const testInputNfts: Nft[] = [ + { + address: '0xtest1', + description: null, + favorite: false, + image: null, + isCurrentlyOwned: true, + name: null, + standard: 'ERC721', + tokenId: '1', + }, + { + address: '0xtest2', + description: null, + favorite: false, + image: null, + isCurrentlyOwned: true, + name: null, + standard: 'ERC721', + tokenId: '2', + }, + { + address: '0xtest3', + description: null, + favorite: false, + image: null, + isCurrentlyOwned: true, + name: null, + standard: 'ERC721', + tokenId: '3', + }, + ]; + + getInternalAccountMock.mockReturnValue(nonEvmAccount); + + const updateNftSpy = jest.spyOn(nftController, 'updateNft'); + + await nftController.updateNftMetadata({ + nfts: testInputNfts, + }); + + expect(updateNftSpy).not.toHaveBeenCalled(); + }); + }); + + describe('removeAndIgnoreNft', () => { + it('should return if the account is non evm', async () => { + // @ts-expect-error creating a non evm account + const nonEvmAccount = createMockInternalAccount({ type: 'bip122' }); + const { nftController, getInternalAccountMock } = setupController(); + + getInternalAccountMock.mockReturnValue(nonEvmAccount); + + // should not update state + const updateSpy = jest.spyOn(nftController, 'update'); + + nftController.removeAndIgnoreNft('0xtest', '1'); + + expect(updateSpy).not.toHaveBeenCalled(); + }); }); }); diff --git a/packages/assets-controllers/src/NftController.ts b/packages/assets-controllers/src/NftController.ts index 9061b8f1809..2c8a1366063 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 AccountsControllerSelectedAccountChangeEvent, + type AccountsControllerGetAccountAction, + isEVMAccount, +} 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,9 @@ const controllerName = 'NftController'; /** * The external actions available to the {@link NftController}. */ -type AllowedActions = AddApprovalRequest; +type AllowedActions = AddApprovalRequest | AccountsControllerGetAccountAction; + +type AllowedEvents = AccountsControllerSelectedAccountChangeEvent; /** * The messenger of the {@link NftController}. @@ -229,9 +237,9 @@ type AllowedActions = AddApprovalRequest; export type NftControllerMessenger = RestrictedControllerMessenger< typeof controllerName, AllowedActions, - never, + AllowedEvents, AllowedActions['type'], - never + AllowedEvents['type'] >; export const getDefaultNftState = (): NftState => { @@ -259,7 +267,7 @@ export class NftController extends BaseControllerV1 { * * @param newCollection - the modified piece of state to update in the controller's store * @param baseStateKey - The root key in the store to update. - * @param passedConfig - An object containing the selectedAddress and chainId that are passed through the auto-detection flow. + * @param passedConfig - An object containing the selectedAccountId and chainId that are passed through the auto-detection flow. * @param passedConfig.userAddress - the address passed through the NFT detection flow to ensure assets are stored to the correct account * @param passedConfig.chainId - the chainId passed through the NFT detection flow to ensure assets are stored to the correct account */ @@ -1019,7 +1027,7 @@ export class NftController extends BaseControllerV1 { ) { super(config, state); this.defaultConfig = { - selectedAddress: '', + selectedAccountId: '', chainId: initialChainId, ipfsGateway: IPFS_DEFAULT_GATEWAY_URL, openSeaEnabled: false, @@ -1039,37 +1047,49 @@ export class NftController extends BaseControllerV1 { this.onNftAdded = onNftAdded; this.messagingSystem = messenger; + this.messagingSystem.subscribe( + 'AccountsController:selectedAccountChange', + (newSelectedAccount: InternalAccount) => { + if (!isEVMAccount(newSelectedAccount)) { + return; + } + this.configure({ selectedAccountId: newSelectedAccount.id }); + }, + ); + onPreferencesStateChange( - async ({ - selectedAddress, - ipfsGateway, - openSeaEnabled, - isIpfsGatewayEnabled, - }) => { + async ({ ipfsGateway, openSeaEnabled, isIpfsGatewayEnabled }) => { this.configure({ - selectedAddress, ipfsGateway, openSeaEnabled, isIpfsGatewayEnabled, }); - const needsUpdateNftMetadata = - (isIpfsGatewayEnabled && ipfsGateway !== '') || openSeaEnabled; - - if (needsUpdateNftMetadata) { - const { chainId } = this.config; - const nfts: Nft[] = - this.state.allNfts[selectedAddress]?.[chainId] ?? []; - // filter only nfts - const nftsToUpdate = nfts.filter( - (singleNft) => - !singleNft.name && !singleNft.description && !singleNft.image, - ); - if (nftsToUpdate.length !== 0) { - await this.updateNftMetadata({ - nfts: nftsToUpdate, - userAddress: selectedAddress, - }); + const { selectedAccountId } = this.config; + const selectedAccount = this.messagingSystem.call( + 'AccountsController:getAccount', + selectedAccountId, + ); + + if (selectedAccount && isEVMAccount(selectedAccount)) { + const needsUpdateNftMetadata = + (isIpfsGatewayEnabled && ipfsGateway !== '') || openSeaEnabled; + + if (needsUpdateNftMetadata) { + const { chainId } = this.config; + const nfts: Nft[] = + this.state.allNfts[selectedAccount.address]?.[chainId] ?? []; + // filter only nfts + const nftsToUpdate = nfts.filter( + (singleNft) => + !singleNft.name && !singleNft.description && !singleNft.image, + ); + if (nftsToUpdate.length !== 0) { + await this.updateNftMetadata({ + nfts: nftsToUpdate, + userAddress: selectedAccount.address, + }); + } } } }, @@ -1167,14 +1187,23 @@ 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, + ); + if (!selectedAccount || !isEVMAccount(selectedAccount)) { + return; + } + + userAddress = userAddress || selectedAccount.address; + await this.validateWatchNft(asset, type, userAddress); const nftMetadata = await this.getNftInformation( @@ -1290,17 +1319,25 @@ 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, + ); + if (!selectedAccount || !isEVMAccount(selectedAccount)) { + return; + } + userAddress = userAddress ?? selectedAccount.address; + if ( !(await this.isNftOwner(userAddress, address, tokenId, { networkClientId, @@ -1332,7 +1369,7 @@ export class NftController extends BaseControllerV1 { tokenId: string, { nftMetadata, - userAddress = this.config.selectedAddress, + userAddress, source = Source.Custom, networkClientId, }: { @@ -1340,8 +1377,18 @@ 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, + ); + if (!selectedAccount || !isEVMAccount(selectedAccount)) { + return; + } + userAddress = userAddress ?? selectedAccount.address; + tokenAddress = toChecksumHexAddress(tokenAddress); const chainId = this.getCorrectChainId({ networkClientId }); @@ -1388,13 +1435,23 @@ export class NftController extends BaseControllerV1 { */ async updateNftMetadata({ nfts, - userAddress = this.config.selectedAddress, + userAddress, networkClientId, }: { nfts: Nft[]; userAddress?: string; networkClientId?: NetworkClientId; }) { + const selectedAccount = this.messagingSystem.call( + 'AccountsController:getAccount', + this.config.selectedAccountId, + ); + if (!selectedAccount || !isEVMAccount(selectedAccount)) { + return; + } + + const addressToSearch = userAddress || selectedAccount.address; + const chainId = this.getCorrectChainId({ networkClientId }); const nftsWithChecksumAdr = nfts.map((nft) => { @@ -1426,7 +1483,7 @@ export class NftController extends BaseControllerV1 { this.updateNft( elm.value.nft, elm.value.newMetadata, - userAddress, + addressToSearch, chainId, ), ); @@ -1446,11 +1503,19 @@ 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, + ); + if (!selectedAccount || !isEVMAccount(selectedAccount)) { + return; + } + + userAddress = userAddress || selectedAccount.address; const chainId = this.getCorrectChainId({ networkClientId }); address = toChecksumHexAddress(address); this.removeIndividualNft(address, tokenId, { chainId, userAddress }); @@ -1479,11 +1544,20 @@ 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, + ); + if (!selectedAccount || !isEVMAccount(selectedAccount)) { + return; + } + + userAddress = userAddress || selectedAccount.address; + const chainId = this.getCorrectChainId({ networkClientId }); address = toChecksumHexAddress(address); this.removeAndIgnoreIndividualNft(address, tokenId, { @@ -1522,12 +1596,21 @@ 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, + ); + if (!selectedAccount || !isEVMAccount(selectedAccount)) { + throw new Error('Non EVM Account selected'); + } + + userAddress = userAddress || selectedAccount.address; + const chainId = this.getCorrectChainId({ networkClientId }); const { address, tokenId } = nft; let isOwned = nft.isCurrentlyOwned; @@ -1572,14 +1655,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, + ); + if (!selectedAccount || !isEVMAccount(selectedAccount)) { + return; + } + + userAddress = userAddress || selectedAccount.address; + const chainId = this.getCorrectChainId({ networkClientId }); const { allNfts } = this.state; const nfts = allNfts[userAddress]?.[chainId] || []; @@ -1616,14 +1706,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, + ); + if (!selectedAccount || !isEVMAccount(selectedAccount)) { + return; + } + + userAddress = userAddress || selectedAccount.address; + const chainId = this.getCorrectChainId({ networkClientId }); const { allNfts } = this.state; const nfts = allNfts[userAddress]?.[chainId] || []; From c866813f403e9de06a4cda8338704c7ae91a3939 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Mon, 29 Apr 2024 13:08:48 +0800 Subject: [PATCH 05/23] fix: use selectedAccount action --- .../src/NftController.test.ts | 15 ++++++++++- .../assets-controllers/src/NftController.ts | 26 +++++++++---------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/packages/assets-controllers/src/NftController.test.ts b/packages/assets-controllers/src/NftController.test.ts index d469d89e8ea..34db9d59dae 100644 --- a/packages/assets-controllers/src/NftController.test.ts +++ b/packages/assets-controllers/src/NftController.test.ts @@ -1,6 +1,7 @@ import type { Network } from '@ethersproject/providers'; import type { AccountsControllerGetAccountAction, + AccountsControllerGetSelectedAccountAction, AccountsControllerSelectedAccountChangeEvent, } from '@metamask/accounts-controller'; import { createMockInternalAccount } from '@metamask/accounts-controller/src/tests/mocks'; @@ -81,7 +82,10 @@ const GOERLI = { ticker: NetworksTicker.goerli, }; -type ApprovalActions = AddApprovalRequest | AccountsControllerGetAccountAction; +type ApprovalActions = + | AddApprovalRequest + | AccountsControllerGetAccountAction + | AccountsControllerGetSelectedAccountAction; type ApprovalEvents = | ApprovalStateChange | AccountsControllerSelectedAccountChangeEvent; @@ -142,6 +146,13 @@ function setupController( getInternalAccountMock, ); + const getSelectedAccountMock = jest.fn().mockReturnValue(OWNER_ACCOUNT); + + messenger.registerActionHandler( + 'AccountsController:getSelectedAccount', + getSelectedAccountMock, + ); + const approvalControllerMessenger = messenger.getRestricted({ name: 'ApprovalController', allowedActions: [], @@ -191,6 +202,7 @@ function setupController( name: controllerName, allowedActions: [ 'ApprovalController:addRequest', + 'AccountsController:getSelectedAccount', 'AccountsController:getAccount', ], allowedEvents: ['AccountsController:selectedAccountChange'], @@ -245,6 +257,7 @@ function setupController( triggerPreferencesStateChange, triggerSelectedAccountChange, getInternalAccountMock, + getSelectedAccountMock, }; } diff --git a/packages/assets-controllers/src/NftController.ts b/packages/assets-controllers/src/NftController.ts index 2c8a1366063..350afa2a89e 100644 --- a/packages/assets-controllers/src/NftController.ts +++ b/packages/assets-controllers/src/NftController.ts @@ -2,6 +2,7 @@ import { isAddress } from '@ethersproject/address'; import { type AccountsControllerSelectedAccountChangeEvent, type AccountsControllerGetAccountAction, + type AccountsControllerGetSelectedAccountAction, isEVMAccount, } from '@metamask/accounts-controller'; import type { AddApprovalRequest } from '@metamask/approval-controller'; @@ -227,7 +228,10 @@ const controllerName = 'NftController'; /** * The external actions available to the {@link NftController}. */ -type AllowedActions = AddApprovalRequest | AccountsControllerGetAccountAction; +type AllowedActions = + | AddApprovalRequest + | AccountsControllerGetAccountAction + | AccountsControllerGetSelectedAccountAction; type AllowedEvents = AccountsControllerSelectedAccountChangeEvent; @@ -1050,35 +1054,31 @@ export class NftController extends BaseControllerV1 { this.messagingSystem.subscribe( 'AccountsController:selectedAccountChange', (newSelectedAccount: InternalAccount) => { - if (!isEVMAccount(newSelectedAccount)) { - return; - } this.configure({ selectedAccountId: newSelectedAccount.id }); }, ); onPreferencesStateChange( async ({ ipfsGateway, openSeaEnabled, isIpfsGatewayEnabled }) => { + const newSelectedAccount = this.messagingSystem.call( + 'AccountsController:getSelectedAccount', + ); + this.configure({ ipfsGateway, openSeaEnabled, isIpfsGatewayEnabled, + selectedAccountId: newSelectedAccount.id, }); - const { selectedAccountId } = this.config; - const selectedAccount = this.messagingSystem.call( - 'AccountsController:getAccount', - selectedAccountId, - ); - - if (selectedAccount && isEVMAccount(selectedAccount)) { + if (isEVMAccount(newSelectedAccount)) { const needsUpdateNftMetadata = (isIpfsGatewayEnabled && ipfsGateway !== '') || openSeaEnabled; if (needsUpdateNftMetadata) { const { chainId } = this.config; const nfts: Nft[] = - this.state.allNfts[selectedAccount.address]?.[chainId] ?? []; + this.state.allNfts[newSelectedAccount.address]?.[chainId] ?? []; // filter only nfts const nftsToUpdate = nfts.filter( (singleNft) => @@ -1087,7 +1087,7 @@ export class NftController extends BaseControllerV1 { if (nftsToUpdate.length !== 0) { await this.updateNftMetadata({ nfts: nftsToUpdate, - userAddress: selectedAccount.address, + userAddress: newSelectedAccount.address, }); } } From 6ee9a6274b7f3872baf0d560e14a0989890f0334 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Mon, 29 Apr 2024 13:27:35 +0800 Subject: [PATCH 06/23] fix: listeners in NftDetectionController --- .../src/NftDetectionController.test.ts | 128 ++++++++---------- .../src/NftDetectionController.ts | 37 +++-- 2 files changed, 81 insertions(+), 84 deletions(-) diff --git a/packages/assets-controllers/src/NftDetectionController.test.ts b/packages/assets-controllers/src/NftDetectionController.test.ts index 28ab48bf462..402868614e1 100644 --- a/packages/assets-controllers/src/NftDetectionController.test.ts +++ b/packages/assets-controllers/src/NftDetectionController.test.ts @@ -289,10 +289,16 @@ describe('NftDetectionController', () => { const mockGetInternalAccount = jest .fn() .mockReturnValue(defaultSelectedAccount); + const mockGetSelectedAccount = jest + .fn() + .mockReturnValue(defaultSelectedAccount); await withController( { config: { interval: 10, selectedAccountId: defaultSelectedAccount.id }, - options: { getInternalAccount: mockGetInternalAccount }, + options: { + getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, + }, }, async ({ controller, triggerPreferencesStateChange }) => { const mockNfts = sinon.stub(controller, 'detectNfts'); @@ -398,11 +404,13 @@ 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, getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, }, }, async ({ @@ -411,6 +419,7 @@ describe('NftDetectionController', () => { triggerSelectedAccountChange, }) => { const selectedAccount = createMockInternalAccount({ address: '0x1' }); + mockGetSelectedAccount.mockReturnValue(selectedAccount); mockGetInternalAccount.mockReturnValue(selectedAccount); triggerSelectedAccountChange(selectedAccount); triggerPreferencesStateChange({ @@ -449,12 +458,14 @@ 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( { config: { selectedAccountId: defaultSelectedAccount.id }, options: { addNft: mockAddNft, getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, }, }, async ({ @@ -465,10 +476,9 @@ describe('NftDetectionController', () => { const updatedSelectedAccount = createMockInternalAccount({ address: '0x123', }); - mockGetInternalAccount - .mockReturnValueOnce(defaultSelectedAccount) - .mockReturnValue(updatedSelectedAccount); + mockGetInternalAccount.mockReturnValue(updatedSelectedAccount); triggerSelectedAccountChange(updatedSelectedAccount); + mockGetSelectedAccount.mockReturnValue(updatedSelectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), useNftDetection: true, @@ -514,11 +524,13 @@ 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, getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, }, }, async ({ @@ -531,6 +543,7 @@ describe('NftDetectionController', () => { }); mockGetInternalAccount.mockReturnValue(updatedSelectedAccount); triggerSelectedAccountChange(updatedSelectedAccount); + mockGetSelectedAccount.mockReturnValue(updatedSelectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), useNftDetection: true, @@ -588,11 +601,13 @@ 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, getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, }, }, async ({ @@ -605,6 +620,7 @@ describe('NftDetectionController', () => { }); mockGetInternalAccount.mockReturnValue(updatedSelectedAccount); triggerSelectedAccountChange(updatedSelectedAccount); + mockGetSelectedAccount.mockReturnValue(updatedSelectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), useNftDetection: true, @@ -644,6 +660,7 @@ 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(), @@ -664,6 +681,7 @@ describe('NftDetectionController', () => { addNft: mockAddNft, getNftState: mockGetNftState, getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, }, }, async ({ @@ -673,6 +691,7 @@ describe('NftDetectionController', () => { }) => { const selectedAccount = createMockInternalAccount({ address: '0x9' }); mockGetInternalAccount.mockReturnValue(selectedAccount); + mockGetSelectedAccount.mockReturnValue(selectedAccount); triggerSelectedAccountChange(selectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), @@ -692,36 +711,18 @@ describe('NftDetectionController', () => { ); }); - it('should not detect and add NFTs if there is no selectedAddress', async () => { - const mockAddNft = jest.fn(); - await withController( - { config: { selectedAccountId: '' }, options: { addNft: mockAddNft } }, - async ({ controller, triggerPreferencesStateChange }) => { - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - useNftDetection: true, // auto-detect is enabled so it proceeds to check userAddress - }); - - // confirm that default selected address is an empty string - expect(controller.config.selectedAccountId).toBe(''); - - await controller.detectNfts(); - - expect(mockAddNft).not.toHaveBeenCalled(); - }, - ); - }); - it('should not detect and add NFTs if the account is a nonevm account', async () => { const mockAddNft = jest.fn(); // @ts-expect-error create a nonevm account const nonEvmAccount = createMockInternalAccount({ type: 'bitcoin' }); const mockGetInternalAccount = jest.fn().mockReturnValue(nonEvmAccount); + const mockGetSelectedAccount = jest.fn().mockReturnValue(nonEvmAccount); await withController( { options: { addNft: mockAddNft, getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, }, }, async ({ controller, triggerPreferencesStateChange }) => { @@ -730,9 +731,6 @@ describe('NftDetectionController', () => { useNftDetection: true, // auto-detect is enabled so it proceeds to check userAddress }); - // confirm that default selected address is an empty string - expect(controller.config.selectedAccountId).toBe(''); - await controller.detectNfts(); expect(mockAddNft).not.toHaveBeenCalled(); @@ -765,8 +763,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({ @@ -794,11 +801,13 @@ 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, getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, }, }, async ({ @@ -808,6 +817,7 @@ describe('NftDetectionController', () => { }) => { const selectedAccount = createMockInternalAccount({ address: '0x9' }); mockGetInternalAccount.mockReturnValue(selectedAccount); + mockGetSelectedAccount.mockReturnValue(selectedAccount); triggerSelectedAccountChange(selectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), @@ -841,11 +851,13 @@ describe('NftDetectionController', () => { .persist(); const mockAddNft = jest.fn(); const mockGetInternalAccount = jest.fn().mockReturnValue(selectedAccount); + const mockGetSelectedAccount = jest.fn().mockReturnValue(selectedAccount); await withController( { options: { addNft: mockAddNft, getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, }, }, async ({ @@ -876,8 +888,14 @@ describe('NftDetectionController', () => { it('should rethrow error when Nft APi server fails with error other than fetch failure', async () => { const selectedAccount = createMockInternalAccount({ address: '0x4' }); const mockGetInternalAccount = jest.fn().mockReturnValue(selectedAccount); + const mockGetSelectedAccount = jest.fn().mockReturnValue(selectedAccount); await withController( - { options: { getInternalAccount: mockGetInternalAccount } }, + { + options: { + getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, + }, + }, async ({ controller, triggerPreferencesStateChange, @@ -926,11 +944,13 @@ 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, getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, }, }, async ({ @@ -942,6 +962,7 @@ describe('NftDetectionController', () => { address: '0x1', }); mockGetInternalAccount.mockReturnValue(selectedAccount); + mockGetSelectedAccount.mockReturnValue(selectedAccount); triggerSelectedAccountChange(selectedAccount); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), @@ -966,10 +987,16 @@ describe('NftDetectionController', () => { const mockGetInternalAccount = jest .fn() .mockReturnValue(defaultSelectedAccount); + const mockGetSelectedAccount = jest + .fn() + .mockReturnValue(defaultSelectedAccount); await withController( { config: { selectedAccountId: defaultSelectedAccount.id }, - options: { getInternalAccount: mockGetInternalAccount }, + options: { + getInternalAccount: mockGetInternalAccount, + getSelectedAccount: mockGetSelectedAccount, + }, }, async ({ controller, triggerPreferencesStateChange }) => { const detectNfts = sinon.stub(controller, 'detectNfts'); @@ -995,46 +1022,6 @@ describe('NftDetectionController', () => { }, ); }); - - it('should only re-detect when the selected account changes to a evm account', async () => { - const mockGetInternalAccount = jest.fn(); - await withController( - { - config: { - selectedAccountId: defaultSelectedAccount.id, - disabled: false, - }, - options: { getInternalAccount: mockGetInternalAccount }, - }, - async ({ controller, triggerSelectedAccountChange }) => { - const detectNfts = sinon.stub(controller, 'detectNfts'); - - // Same accounts shouldn't trigger detections - mockGetInternalAccount.mockReturnValueOnce(defaultSelectedAccount); - triggerSelectedAccountChange(defaultSelectedAccount); - await advanceTime({ clock, duration: 1 }); - expect(detectNfts.callCount).toBe(0); - - // Repeated account changes should only trigger 1 detection - for (let i = 0; i < 5; i++) { - const newAccount = createMockInternalAccount(); - mockGetInternalAccount.mockReturnValue(newAccount); - triggerSelectedAccountChange(newAccount); - } - await advanceTime({ clock, duration: 1 }); - expect(detectNfts.callCount).toBe(5); - detectNfts.resetHistory(); - - // Irrelevant account changes shouldn't trigger a detection - // @ts-expect-error create a nonevm account - const nonevmAccount = createMockInternalAccount({ type: 'bitcoin' }); - mockGetInternalAccount.mockReturnValue(nonevmAccount); - triggerSelectedAccountChange(nonevmAccount); - await advanceTime({ clock, duration: 1 }); - expect(detectNfts.callCount).toBe(0); - }, - ); - }); }); type WithControllerCallback = ({ @@ -1104,6 +1091,7 @@ async function withController( getNftApi: jest.fn(), getNetworkClientById, getInternalAccount: jest.fn(), + getSelectedAccount: jest.fn(), getNftState: getDefaultNftState, disabled: true, selectedAccountId: '', diff --git a/packages/assets-controllers/src/NftDetectionController.ts b/packages/assets-controllers/src/NftDetectionController.ts index 5f72d3def2c..11ef451a0dc 100644 --- a/packages/assets-controllers/src/NftDetectionController.ts +++ b/packages/assets-controllers/src/NftDetectionController.ts @@ -421,6 +421,8 @@ export class NftDetectionController extends StaticIntervalPollingControllerV1< private readonly getInternalAccount: (accountId: string) => InternalAccount; + private readonly getSelectedAccount: () => InternalAccount; + /** * Creates an NftDetectionController instance. * @@ -438,6 +440,7 @@ export class NftDetectionController extends StaticIntervalPollingControllerV1< * @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. */ @@ -453,6 +456,7 @@ export class NftDetectionController extends StaticIntervalPollingControllerV1< getNftApi, getNftState, getInternalAccount, + getSelectedAccount, disabled: initialDisabled, selectedAccountId: initialSelectedAccountId, }: { @@ -473,6 +477,7 @@ export class NftDetectionController extends StaticIntervalPollingControllerV1< getNftApi: NftController['getNftApi']; getNftState: () => NftState; getInternalAccount: (accountId: string) => InternalAccount; + getSelectedAccount: () => InternalAccount; disabled: boolean; selectedAccountId: string; }, @@ -490,15 +495,13 @@ export class NftDetectionController extends StaticIntervalPollingControllerV1< this.getNftState = getNftState; this.getNetworkClientById = getNetworkClientById; this.getInternalAccount = getInternalAccount; + this.getSelectedAccount = getSelectedAccount; + onSelectedAccountChange((internalAccount) => { - if (!isEVMAccount(internalAccount)) { - this.configure({ selectedAccountId: '' }); - this.stop(); - return; - } + this.configure({ selectedAccountId: internalAccount.id }); + const { selectedAccountId, disabled } = this.config; - if (!disabled && selectedAccountId !== internalAccount.id) { - this.configure({ selectedAccountId: internalAccount.id }); + if (!disabled || selectedAccountId !== internalAccount.id) { this.start(); } else { this.stop(); @@ -506,13 +509,19 @@ export class NftDetectionController extends StaticIntervalPollingControllerV1< }); onPreferencesStateChange(({ useNftDetection }) => { - const { disabled, selectedAccountId } = this.config; - const selectedInternalAccount = - this.getInternalAccount(selectedAccountId); - - if (!useNftDetection !== disabled) { - this.configure({ disabled: !useNftDetection }); - if (useNftDetection && isEVMAccount(selectedInternalAccount)) { + const { selectedAccountId: previousSelectedAccountId, disabled } = + this.config; + const newSelectedAccount = this.getSelectedAccount(); + + if ( + newSelectedAccount.id !== previousSelectedAccountId || + !useNftDetection !== disabled + ) { + this.configure({ + disabled: !useNftDetection, + selectedAccountId: newSelectedAccount.id, + }); + if (useNftDetection) { this.start(); } else { this.stop(); From 4c30c69664bc788d5a6f7fc45b495cb4589b1f25 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Fri, 26 Apr 2024 10:00:00 +0800 Subject: [PATCH 07/23] feat: add mocks and evm check to accounts controller --- packages/accounts-controller/src/index.ts | 6 +- .../accounts-controller/src/tests/mocks.ts | 59 +++++++++++++++++++ packages/accounts-controller/src/utils.ts | 13 ++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 packages/accounts-controller/src/tests/mocks.ts diff --git a/packages/accounts-controller/src/index.ts b/packages/accounts-controller/src/index.ts index 274efa5d5b1..d49ff52ab05 100644 --- a/packages/accounts-controller/src/index.ts +++ b/packages/accounts-controller/src/index.ts @@ -15,4 +15,8 @@ export type { AccountsControllerMessenger, } from './AccountsController'; export { AccountsController } from './AccountsController'; -export { keyringTypeToName, getUUIDFromAddressOfNormalAccount } from './utils'; +export { + isEVMAccount, + keyringTypeToName, + getUUIDFromAddressOfNormalAccount, +} from './utils'; diff --git a/packages/accounts-controller/src/tests/mocks.ts b/packages/accounts-controller/src/tests/mocks.ts new file mode 100644 index 00000000000..3d743803d3b --- /dev/null +++ b/packages/accounts-controller/src/tests/mocks.ts @@ -0,0 +1,59 @@ +import type { InternalAccount } from '@metamask/keyring-api'; +import { EthAccountType, 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?: EthAccountType; + name?: string; + keyringType?: KeyringTypes; + snap?: { + id: string; + enabled: boolean; + name: string; + }; + importTime?: number; + lastSelected?: number; +} = {}): InternalAccount => { + const methods = + type === EthAccountType.Eoa + ? [ + EthMethod.PersonalSign, + EthMethod.Sign, + EthMethod.SignTransaction, + EthMethod.SignTypedDataV1, + EthMethod.SignTypedDataV3, + EthMethod.SignTypedDataV4, + ] + : [ + EthMethod.PatchUserOperation, + EthMethod.PrepareUserOperation, + EthMethod.SignUserOperation, + ]; + + return { + id, + address, + options: {}, + methods, + type: EthAccountType.Eoa, + metadata: { + name, + keyring: { type: keyringType }, + importTime, + lastSelected, + snap: snap && snap, + }, + }; +}; diff --git a/packages/accounts-controller/src/utils.ts b/packages/accounts-controller/src/utils.ts index b3e7cbd639d..0668c18c387 100644 --- a/packages/accounts-controller/src/utils.ts +++ b/packages/accounts-controller/src/utils.ts @@ -1,4 +1,6 @@ import { toBuffer } from '@ethereumjs/util'; +import type { InternalAccount } from '@metamask/keyring-api'; +import { EthAccountType } from '@metamask/keyring-api'; import { isCustodyKeyring, KeyringTypes } from '@metamask/keyring-controller'; import { sha256 } from 'ethereum-cryptography/sha256'; import type { V4Options } from 'uuid'; @@ -68,3 +70,14 @@ export function getUUIDOptionsFromAddressOfNormalAccount( export function getUUIDFromAddressOfNormalAccount(address: string): string { return uuid(getUUIDOptionsFromAddressOfNormalAccount(address)); } + +/** + * Checks if the given internal account is an EVM account. + * @param internalAccount - The internal account to check. + * @returns True if the internal account is an EVM account, false otherwise. + */ +export function isEVMAccount(internalAccount: InternalAccount): boolean { + return [EthAccountType.Eoa, EthAccountType.Erc4337].includes( + internalAccount?.type as EthAccountType, + ); +} From c0cde1b370c3c8e1f3fb9d327bf85f8ea0a47f86 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Fri, 26 Apr 2024 20:29:52 +0800 Subject: [PATCH 08/23] feat: add mocks and util function for non evm --- .../src/tests/mocks.test.ts | 52 +++++++++++++++++++ .../accounts-controller/src/tests/mocks.ts | 2 +- .../accounts-controller/src/utils.test.ts | 18 +++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 packages/accounts-controller/src/tests/mocks.test.ts create mode 100644 packages/accounts-controller/src/utils.test.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..b59b7a51a98 --- /dev/null +++ b/packages/accounts-controller/src/tests/mocks.test.ts @@ -0,0 +1,52 @@ +import { 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, + }, + }); + }); +}); diff --git a/packages/accounts-controller/src/tests/mocks.ts b/packages/accounts-controller/src/tests/mocks.ts index 3d743803d3b..05a435fe74e 100644 --- a/packages/accounts-controller/src/tests/mocks.ts +++ b/packages/accounts-controller/src/tests/mocks.ts @@ -47,7 +47,7 @@ export const createMockInternalAccount = ({ address, options: {}, methods, - type: EthAccountType.Eoa, + type, metadata: { name, keyring: { type: keyringType }, diff --git a/packages/accounts-controller/src/utils.test.ts b/packages/accounts-controller/src/utils.test.ts new file mode 100644 index 00000000000..010f8848491 --- /dev/null +++ b/packages/accounts-controller/src/utils.test.ts @@ -0,0 +1,18 @@ +import { EthAccountType } from '@metamask/keyring-api'; + +import { createMockInternalAccount } from './tests/mocks'; +import { isEVMAccount } from './utils'; + +describe('isEVMAccount', () => { + it.each([ + [EthAccountType.Eoa, true], + [EthAccountType.Erc4337, true], + ['bip122', false], + ])('%s should return %s', (accountType, expected) => { + expect( + isEVMAccount( + createMockInternalAccount({ type: accountType as EthAccountType }), + ), + ).toBe(expected); + }); +}); From a9ec2a02ccdffd3ba78cfb3c32abcb95c6dd16e5 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Mon, 29 Apr 2024 14:21:43 +0800 Subject: [PATCH 09/23] fix: remove getIdentites and getSelectedAddress from AccountTrackerControlelr and use InternalAccount --- .../src/AccountTrackerController.test.ts | 151 +++++++----------- .../src/AccountTrackerController.ts | 45 ++++-- packages/transaction-controller/package.json | 3 + .../tsconfig.build.json | 1 + packages/transaction-controller/tsconfig.json | 1 + yarn.lock | 3 + 6 files changed, 98 insertions(+), 106 deletions(-) diff --git a/packages/assets-controllers/src/AccountTrackerController.test.ts b/packages/assets-controllers/src/AccountTrackerController.test.ts index 7f32c4ad550..8b2884670b1 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/src/tests/mocks'; 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..4ddcb6dcae0 100644 --- a/packages/assets-controllers/src/AccountTrackerController.ts +++ b/packages/assets-controllers/src/AccountTrackerController.ts @@ -1,7 +1,9 @@ +import { isEVMAccount } from '@metamask/accounts-controller'; import type { BaseConfig, BaseState } from '@metamask/base-controller'; import { query, safelyExecuteWithTimeout } from '@metamask/controller-utils'; import EthQuery from '@metamask/eth-query'; import type { Provider } from '@metamask/eth-query'; +import type { InternalAccount } from '@metamask/keyring-api'; import type { NetworkClientId, NetworkController, @@ -79,7 +81,11 @@ export class AccountTrackerController extends StaticIntervalPollingControllerV1< }); } - const addresses = Object.keys(this.getIdentities()); + const addresses = Object.values( + this.getInternalAccounts().map( + (internalAccount) => internalAccount.address, + ), + ); const newAddresses = addresses.filter( (address) => !existing.includes(address), ); @@ -114,9 +120,9 @@ export class AccountTrackerController extends StaticIntervalPollingControllerV1< */ override name = 'AccountTrackerController'; - private readonly getIdentities: () => PreferencesState['identities']; + private readonly getInternalAccounts: () => InternalAccount[]; - private readonly getSelectedAddress: () => PreferencesState['selectedAddress']; + private readonly getSelectedAccount: () => InternalAccount; 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: () => InternalAccount[]; + getSelectedAccount: () => InternalAccount; 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,11 @@ export class AccountTrackerController extends StaticIntervalPollingControllerV1< * @param networkClientId - Optional networkClientId to fetch a network client with */ refresh = async (networkClientId?: NetworkClientId) => { + const selectedAccount = this.getSelectedAccount(); + if (!selectedAccount || !isEVMAccount(selectedAccount)) { + return; + } + const releaseLock = await this.refreshMutex.acquire(); try { const { chainId, ethQuery } = @@ -264,7 +275,7 @@ export class AccountTrackerController extends StaticIntervalPollingControllerV1< const accountsToUpdate = isMultiAccountBalancesEnabled ? Object.keys(accounts) - : [this.getSelectedAddress()]; + : [selectedAccount.address]; const accountsForChain = { ...accountsByChainId[chainId] }; for (const address of accountsToUpdate) { diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json index 03dabe4846f..1f3d5283f9e 100644 --- a/packages/transaction-controller/package.json +++ b/packages/transaction-controller/package.json @@ -47,6 +47,7 @@ "@ethersproject/abi": "^5.7.0", "@ethersproject/contracts": "^5.7.0", "@ethersproject/providers": "^5.7.0", + "@metamask/accounts-controller": "^13.0.0", "@metamask/approval-controller": "^6.0.1", "@metamask/base-controller": "^5.0.1", "@metamask/controller-utils": "^9.1.0", @@ -68,6 +69,7 @@ "@babel/runtime": "^7.23.9", "@metamask/auto-changelog": "^3.4.4", "@metamask/ethjs-provider-http": "^0.3.0", + "@metamask/keyring-api": "^6.0.0", "@types/bn.js": "^5.1.5", "@types/jest": "^27.4.1", "@types/node": "^16.18.54", @@ -83,6 +85,7 @@ }, "peerDependencies": { "@babel/runtime": "^7.23.9", + "@metamask/accounts-controller": "^13.0.0", "@metamask/approval-controller": "^6.0.0", "@metamask/gas-fee-controller": "^15.0.0", "@metamask/network-controller": "^18.0.0" diff --git a/packages/transaction-controller/tsconfig.build.json b/packages/transaction-controller/tsconfig.build.json index 9a78dab46ae..648111f91b0 100644 --- a/packages/transaction-controller/tsconfig.build.json +++ b/packages/transaction-controller/tsconfig.build.json @@ -6,6 +6,7 @@ "rootDir": "./src" }, "references": [ + { "path": "../accounts-controller/tsconfig.build.json" }, { "path": "../approval-controller/tsconfig.build.json" }, { "path": "../base-controller/tsconfig.build.json" }, { "path": "../controller-utils/tsconfig.build.json" }, diff --git a/packages/transaction-controller/tsconfig.json b/packages/transaction-controller/tsconfig.json index 52940e55927..bf67c437107 100644 --- a/packages/transaction-controller/tsconfig.json +++ b/packages/transaction-controller/tsconfig.json @@ -5,6 +5,7 @@ "target": "ES2022" }, "references": [ + { "path": "../accounts-controller" }, { "path": "../approval-controller" }, { "path": "../base-controller" }, { "path": "../controller-utils" }, diff --git a/yarn.lock b/yarn.lock index 983c4ed71ac..5432544b655 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3014,6 +3014,7 @@ __metadata: "@ethersproject/abi": ^5.7.0 "@ethersproject/contracts": ^5.7.0 "@ethersproject/providers": ^5.7.0 + "@metamask/accounts-controller": ^13.0.0 "@metamask/approval-controller": ^6.0.1 "@metamask/auto-changelog": ^3.4.4 "@metamask/base-controller": ^5.0.1 @@ -3021,6 +3022,7 @@ __metadata: "@metamask/eth-query": ^4.0.0 "@metamask/ethjs-provider-http": ^0.3.0 "@metamask/gas-fee-controller": ^15.1.0 + "@metamask/keyring-api": ^6.0.0 "@metamask/metamask-eth-abis": ^3.1.1 "@metamask/network-controller": ^18.1.0 "@metamask/rpc-errors": ^6.2.1 @@ -3046,6 +3048,7 @@ __metadata: uuid: ^8.3.2 peerDependencies: "@babel/runtime": ^7.23.9 + "@metamask/accounts-controller": ^13.0.0 "@metamask/approval-controller": ^6.0.0 "@metamask/gas-fee-controller": ^15.0.0 "@metamask/network-controller": ^18.0.0 From 23aeaa0e857465fa1a2f9c54d48c866de4d2b8b0 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Wed, 24 Apr 2024 23:23:27 +0800 Subject: [PATCH 10/23] fix: update selectedAddress args in token controllers --- .../src/AccountsController.ts | 13 ++- packages/accounts-controller/src/index.ts | 7 +- packages/accounts-controller/src/utils.ts | 13 +++ .../src/TokenDetectionController.ts | 83 ++++++++++++------- .../src/TokenRatesController.ts | 43 ++++++---- .../src/TokensController.ts | 67 +++++++++++---- 6 files changed, 162 insertions(+), 64 deletions(-) diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index 993e54aceac..b3e94417248 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -75,6 +75,11 @@ export type AccountsControllerGetAccountAction = { handler: AccountsController['getAccount']; }; +export type AccountsControllerGetAccountExpectAction = { + type: `${typeof controllerName}:getAccountExpect`; + handler: AccountsController['getAccountExpect']; +}; + export type AllowedActions = | KeyringControllerGetKeyringForAccountAction | KeyringControllerGetKeyringsByTypeAction @@ -88,7 +93,8 @@ export type AccountsControllerActions = | AccountsControllerUpdateAccountsAction | AccountsControllerGetAccountByAddressAction | AccountsControllerGetSelectedAccountAction - | AccountsControllerGetAccountAction; + | AccountsControllerGetAccountAction + | AccountsControllerGetAccountExpectAction; export type AccountsControllerChangeEvent = ControllerStateChangeEvent< typeof controllerName, @@ -840,5 +846,10 @@ export class AccountsController extends BaseController< `AccountsController:getAccount`, this.getAccount.bind(this), ); + + this.messagingSystem.registerActionHandler( + `AccountsController:getAccountExpect`, + this.getAccountExpect.bind(this), + ); } } diff --git a/packages/accounts-controller/src/index.ts b/packages/accounts-controller/src/index.ts index 274efa5d5b1..e1f26ad5171 100644 --- a/packages/accounts-controller/src/index.ts +++ b/packages/accounts-controller/src/index.ts @@ -8,6 +8,7 @@ export type { AccountsControllerGetSelectedAccountAction, AccountsControllerGetAccountByAddressAction, AccountsControllerGetAccountAction, + AccountsControllerGetAccountExpectAction, AccountsControllerActions, AccountsControllerChangeEvent, AccountsControllerSelectedAccountChangeEvent, @@ -15,4 +16,8 @@ export type { AccountsControllerMessenger, } from './AccountsController'; export { AccountsController } from './AccountsController'; -export { keyringTypeToName, getUUIDFromAddressOfNormalAccount } from './utils'; +export { + isEVMAccount, + keyringTypeToName, + getUUIDFromAddressOfNormalAccount, +} from './utils'; diff --git a/packages/accounts-controller/src/utils.ts b/packages/accounts-controller/src/utils.ts index b3e7cbd639d..5bd7454aa53 100644 --- a/packages/accounts-controller/src/utils.ts +++ b/packages/accounts-controller/src/utils.ts @@ -1,4 +1,6 @@ import { toBuffer } from '@ethereumjs/util'; +import type { InternalAccount } from '@metamask/keyring-api'; +import { EthAccountType } from '@metamask/keyring-api'; import { isCustodyKeyring, KeyringTypes } from '@metamask/keyring-controller'; import { sha256 } from 'ethereum-cryptography/sha256'; import type { V4Options } from 'uuid'; @@ -68,3 +70,14 @@ export function getUUIDOptionsFromAddressOfNormalAccount( export function getUUIDFromAddressOfNormalAccount(address: string): string { return uuid(getUUIDOptionsFromAddressOfNormalAccount(address)); } + +/** + * Checks if the given internal account is an EVM account. + * @param internalAccount - The internal account to check. + * @returns True if the internal account is an EVM account, false otherwise. + */ +export function isEVMAccount(internalAccount: InternalAccount): boolean { + return [EthAccountType.Eoa, EthAccountType.Erc4337].includes( + internalAccount.type as EthAccountType, + ); +} diff --git a/packages/assets-controllers/src/TokenDetectionController.ts b/packages/assets-controllers/src/TokenDetectionController.ts index bbebdca4807..6786b30609b 100644 --- a/packages/assets-controllers/src/TokenDetectionController.ts +++ b/packages/assets-controllers/src/TokenDetectionController.ts @@ -1,6 +1,8 @@ -import type { - AccountsControllerGetSelectedAccountAction, - AccountsControllerSelectedAccountChangeEvent, +import { + type AccountsControllerGetSelectedAccountAction, + type AccountsControllerGetAccountExpectAction, + type AccountsControllerSelectedAccountChangeEvent, + isEVMAccount, } from '@metamask/accounts-controller'; import type { RestrictedControllerMessenger, @@ -105,6 +107,7 @@ export type TokenDetectionControllerActions = export type AllowedActions = | AccountsControllerGetSelectedAccountAction + | AccountsControllerGetAccountExpectAction | NetworkControllerGetNetworkClientByIdAction | NetworkControllerGetNetworkConfigurationByNetworkClientId | NetworkControllerGetStateAction @@ -153,7 +156,7 @@ export class TokenDetectionController extends StaticIntervalPollingController< > { #intervalId?: ReturnType; - #selectedAddress: string; + #selectedAccountId: string; #networkClientId: NetworkClientId; @@ -186,19 +189,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 +226,18 @@ 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', + ); + // return the first evm internal account. + if (isEVMAccount(selectedInternalAccount)) { + this.#selectedAccountId = selectedInternalAccount.id; + } + this.#selectedAccountId = ''; + } const { chainId, networkClientId } = this.#getCorrectChainIdAndNetworkClientId(); @@ -277,18 +288,15 @@ export class TokenDetectionController extends StaticIntervalPollingController< this.messagingSystem.subscribe( 'PreferencesController:stateChange', - async ({ selectedAddress: newSelectedAddress, useTokenDetection }) => { - const isSelectedAddressChanged = - this.#selectedAddress !== newSelectedAddress; + async ({ useTokenDetection }) => { const isDetectionChangedFromPreferences = this.#isDetectionEnabledFromPreferences !== useTokenDetection; - this.#selectedAddress = newSelectedAddress; this.#isDetectionEnabledFromPreferences = useTokenDetection; - if (isSelectedAddressChanged || isDetectionChangedFromPreferences) { + if (isDetectionChangedFromPreferences) { await this.#restartTokenDetection({ - selectedAddress: this.#selectedAddress, + selectedAccountId: this.#selectedAccountId, }); } }, @@ -296,13 +304,16 @@ export class TokenDetectionController extends StaticIntervalPollingController< this.messagingSystem.subscribe( 'AccountsController:selectedAccountChange', - async ({ address: newSelectedAddress }) => { - const isSelectedAddressChanged = - this.#selectedAddress !== newSelectedAddress; - if (isSelectedAddressChanged) { - this.#selectedAddress = newSelectedAddress; + async (internalAccount) => { + if (!isEVMAccount(internalAccount)) { + return; + } + const didSelectedAccountIdChanged = + this.#selectedAccountId !== internalAccount.id; + if (didSelectedAccountIdChanged) { + this.#selectedAccountId = internalAccount.id; await this.#restartTokenDetection({ - selectedAddress: this.#selectedAddress, + selectedAccountId: this.#selectedAccountId, }); } }, @@ -436,19 +447,27 @@ 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:getAccountExpect', + selectedAccountId ?? this.#selectedAccountId, + ); + if (!isEVMAccount(internalAccount)) { + return; + } + await this.detectTokens({ networkClientId, - selectedAddress, + selectedAddress: internalAccount.address, }); this.setIntervalLength(DEFAULT_INTERVAL); } @@ -472,8 +491,16 @@ export class TokenDetectionController extends StaticIntervalPollingController< return; } + const selectedInternalAccount = this.messagingSystem.call( + 'AccountsController:getAccountExpect', + this.#selectedAccountId, + ); + if (!isEVMAccount(selectedInternalAccount)) { + return; + } + const addressAgainstWhichToDetect = - selectedAddress ?? this.#selectedAddress; + selectedAddress ?? selectedInternalAccount.address; const { chainId, networkClientId: selectedNetworkClientId } = this.#getCorrectChainIdAndNetworkClientId(networkClientId); const chainIdAgainstWhichToDetect = chainId; diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 2863bb7b96c..e284ca4f9ae 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -1,3 +1,4 @@ +import { isEVMAccount } from '@metamask/accounts-controller'; import type { BaseConfig, BaseState } from '@metamask/base-controller'; import { safelyExecute, @@ -5,13 +6,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 +61,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 +157,8 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< private readonly getNetworkClientById: NetworkController['getNetworkClientById']; + private readonly getInternalAccount: (accountId: string) => InternalAccount; + /** * Creates a TokenRatesController instance. * @@ -165,8 +168,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 +184,9 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< getNetworkClientById, chainId: initialChainId, ticker: initialTicker, - selectedAddress: initialSelectedAddress, - onPreferencesStateChange, + selectedAccountId, + getInternalAccount, + onSelectedAccountChange, onTokensStateChange, onNetworkStateChange, tokenPricesService, @@ -191,9 +196,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 +219,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 +231,19 @@ 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 (!isEVMAccount(internalAccount)) { + return; + } + if (this.config.selectedAccountId !== internalAccount.id) { + this.configure({ selectedAccountId: internalAccount.id }); if (this.#pollState === PollState.Active) { await this.updateExchangeRates(); } @@ -276,10 +286,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( diff --git a/packages/assets-controllers/src/TokensController.ts b/packages/assets-controllers/src/TokensController.ts index a96de9cd6c4..369f4b53a94 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 { + AccountsControllerGetAccountExpectAction, + AccountsControllerSelectedAccountChangeEvent, +} 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'; @@ -47,18 +50,19 @@ import type { TokenListToken, } from './TokenListController'; import type { Token } from './TokenRatesController'; +import { isEVMAccoount } from '@metamask/accounts-controller/src/utils'; /** * @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 +130,8 @@ export type TokensControllerAddDetectedTokensAction = { */ export type AllowedActions = | AddApprovalRequest - | NetworkControllerGetNetworkClientByIdAction; + | NetworkControllerGetNetworkClientByIdAction + | AccountsControllerGetAccountExpectAction; // TODO: Once `TokensController` is upgraded to V2, rewrite this type using the `ControllerStateChangeEvent` type, which constrains `TokensState` as `Record`. export type TokensControllerStateChangeEvent = { @@ -138,8 +143,8 @@ export type TokensControllerEvents = TokensControllerStateChangeEvent; export type AllowedEvents = | NetworkControllerNetworkDidChangeEvent - | PreferencesControllerStateChangeEvent - | TokenListStateChange; + | TokenListStateChange + | AccountsControllerSelectedAccountChangeEvent; /** * The messenger of the {@link TokensController}. @@ -236,7 +241,7 @@ export class TokensController extends BaseControllerV1< super(config, state); this.defaultConfig = { - selectedAddress: '', + selectedAccountId: '', chainId: initialChainId, provider: undefined, ...config, @@ -258,15 +263,20 @@ export class TokensController extends BaseControllerV1< ); this.messagingSystem.subscribe( - 'PreferencesController:stateChange', - ({ selectedAddress }) => { + 'AccountsController:selectedAccountChange', + (internalAccount) => { + if (!isEVMAccoount(internalAccount)) { + return; + } const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state; const { chainId } = this.config; - this.configure({ selectedAddress }); + this.configure({ selectedAccountId: internalAccount.id }); 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 +285,11 @@ export class TokensController extends BaseControllerV1< 'NetworkController:networkDidChange', ({ providerConfig }) => { const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state; - const { selectedAddress } = this.config; + const { selectedAccountId } = this.config; + const { address: selectedAddress } = this.messagingSystem.call( + 'AccountsController:getAccountExpect', + selectedAccountId, + ); 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,17 @@ export class TokensController extends BaseControllerV1< ).configuration.chainId; } - const accountAddress = interactingAddress || selectedAddress; - const isInteractingWithWalletAccount = accountAddress === selectedAddress; + const internalAccount = this.messagingSystem.call( + 'AccountsController:getAccountExpect', + selectedAccountId, + ); + if (!isEVMAccoount(internalAccount)) { + return []; + } + + const accountAddress = interactingAddress || internalAccount.address; + const isInteractingWithWalletAccount = + accountAddress === internalAccount.address; try { address = toChecksumHexAddress(address); @@ -548,10 +571,18 @@ export class TokensController extends BaseControllerV1< ) { const releaseLock = await this.mutex.acquire(); + const internalAccount = this.messagingSystem.call( + 'AccountsController:getAccountExpect', + this.config.selectedAccountId, + ); + if (!isEVMAccoount(internalAccount)) { + return []; + } + // Get existing tokens for the chain + account const chainId = detectionDetails?.chainId ?? this.config.chainId; const accountAddress = - detectionDetails?.selectedAddress ?? this.config.selectedAddress; + detectionDetails?.selectedAddress ?? internalAccount.address; const { allTokens, allDetectedTokens, allIgnoredTokens } = this.state; let newTokens = [...(allTokens?.[chainId]?.[accountAddress] ?? [])]; From 42133c0bbb893fc25ace0691f8f49fac4fa0b3f6 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Thu, 25 Apr 2024 16:53:06 +0800 Subject: [PATCH 11/23] fix: udpate TokenBalancesController to use InternalAccount instead of selectedAddress --- .../accounts-controller/src/tests/mocks.ts | 59 ++++++++++++++ .../src/TokenBalancesController.test.ts | 81 ++++++++++++------- .../src/TokenBalancesController.ts | 19 +++-- 3 files changed, 127 insertions(+), 32 deletions(-) create mode 100644 packages/accounts-controller/src/tests/mocks.ts diff --git a/packages/accounts-controller/src/tests/mocks.ts b/packages/accounts-controller/src/tests/mocks.ts new file mode 100644 index 00000000000..3d743803d3b --- /dev/null +++ b/packages/accounts-controller/src/tests/mocks.ts @@ -0,0 +1,59 @@ +import type { InternalAccount } from '@metamask/keyring-api'; +import { EthAccountType, 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?: EthAccountType; + name?: string; + keyringType?: KeyringTypes; + snap?: { + id: string; + enabled: boolean; + name: string; + }; + importTime?: number; + lastSelected?: number; +} = {}): InternalAccount => { + const methods = + type === EthAccountType.Eoa + ? [ + EthMethod.PersonalSign, + EthMethod.Sign, + EthMethod.SignTransaction, + EthMethod.SignTypedDataV1, + EthMethod.SignTypedDataV3, + EthMethod.SignTypedDataV4, + ] + : [ + EthMethod.PatchUserOperation, + EthMethod.PrepareUserOperation, + EthMethod.SignUserOperation, + ]; + + return { + id, + address, + options: {}, + methods, + type: EthAccountType.Eoa, + metadata: { + name, + keyring: { type: keyringType }, + importTime, + lastSelected, + snap: snap && snap, + }, + }; +}; diff --git a/packages/assets-controllers/src/TokenBalancesController.test.ts b/packages/assets-controllers/src/TokenBalancesController.test.ts index 01d023a8cdf..74bfe8caba9 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/src/tests/mocks'; 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..f86b88e5fb0 100644 --- a/packages/assets-controllers/src/TokenBalancesController.ts +++ b/packages/assets-controllers/src/TokenBalancesController.ts @@ -1,3 +1,7 @@ +import { + isEVMAccount, + type AccountsControllerGetSelectedAccountAction, +} from '@metamask/accounts-controller'; import { type RestrictedControllerMessenger, type ControllerGetStateAction, @@ -5,7 +9,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 +59,7 @@ export type TokenBalancesControllerGetStateAction = ControllerGetStateAction< export type TokenBalancesControllerActions = TokenBalancesControllerGetStateAction; -export type AllowedActions = PreferencesControllerGetStateAction; +export type AllowedActions = AccountsControllerGetSelectedAccountAction; export type TokenBalancesControllerStateChangeEvent = ControllerStateChangeEvent< @@ -199,12 +202,18 @@ export class TokenBalancesController extends BaseController< const newContractBalances: ContractBalances = {}; for (const token of this.#tokens) { const { address } = token; - const { selectedAddress } = this.messagingSystem.call( - 'PreferencesController:getState', + const selectedInternalAccount = this.messagingSystem.call( + 'AccountsController:getSelectedAccount', ); + if (!isEVMAccount(selectedInternalAccount)) { + return; + } try { newContractBalances[address] = toHex( - await this.#getERC20BalanceOf(address, selectedAddress), + await this.#getERC20BalanceOf( + address, + selectedInternalAccount.address, + ), ); token.balanceError = null; } catch (error) { From e291bdb2f3836d302e39544d828a60358d14f443 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Thu, 25 Apr 2024 17:51:17 +0800 Subject: [PATCH 12/23] fix: update TokenBalances test to reach 100% coverage --- .../accounts-controller/src/tests/mocks.ts | 2 +- .../src/TokenBalancesController.test.ts | 18 ++++++++++++++++++ .../src/TokenBalancesController.ts | 13 +++++++------ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/packages/accounts-controller/src/tests/mocks.ts b/packages/accounts-controller/src/tests/mocks.ts index 3d743803d3b..05a435fe74e 100644 --- a/packages/accounts-controller/src/tests/mocks.ts +++ b/packages/accounts-controller/src/tests/mocks.ts @@ -47,7 +47,7 @@ export const createMockInternalAccount = ({ address, options: {}, methods, - type: EthAccountType.Eoa, + type, metadata: { name, keyring: { type: keyringType }, diff --git a/packages/assets-controllers/src/TokenBalancesController.test.ts b/packages/assets-controllers/src/TokenBalancesController.test.ts index 74bfe8caba9..1853735a812 100644 --- a/packages/assets-controllers/src/TokenBalancesController.test.ts +++ b/packages/assets-controllers/src/TokenBalancesController.test.ts @@ -440,4 +440,22 @@ describe('TokenBalancesController', () => { '0x02': toHex(new BN(1)), }); }); + + it('should not update balances if the account is a non-evm account', async () => { + const spyGetERC20BalanceOf = jest.fn(); + controllerMessenger.registerActionHandler( + 'AccountsController:getSelectedAccount', + jest.fn().mockReturnValue( + // @ts-expect-error bitcoin type is not available in, using a mock instead + createMockInternalAccount({ address: '0x1234', type: 'bitcoin' }), + ), + ); + const controller = new TokenBalancesController({ + interval: 1337, + getERC20BalanceOf: spyGetERC20BalanceOf, + messenger, + }); + await controller.updateBalances(); + expect(spyGetERC20BalanceOf).not.toHaveBeenCalled(); + }); }); diff --git a/packages/assets-controllers/src/TokenBalancesController.ts b/packages/assets-controllers/src/TokenBalancesController.ts index f86b88e5fb0..d024c037678 100644 --- a/packages/assets-controllers/src/TokenBalancesController.ts +++ b/packages/assets-controllers/src/TokenBalancesController.ts @@ -198,16 +198,17 @@ export class TokenBalancesController extends BaseController< if (this.#disabled) { return; } + const selectedInternalAccount = this.messagingSystem.call( + 'AccountsController:getSelectedAccount', + ); + if (!isEVMAccount(selectedInternalAccount)) { + return; + } const newContractBalances: ContractBalances = {}; for (const token of this.#tokens) { const { address } = token; - const selectedInternalAccount = this.messagingSystem.call( - 'AccountsController:getSelectedAccount', - ); - if (!isEVMAccount(selectedInternalAccount)) { - return; - } + try { newContractBalances[address] = toHex( await this.#getERC20BalanceOf( From 9b8a1eb92dadc2cb3f82edda66841d0dbd86f4c5 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Fri, 26 Apr 2024 09:54:26 +0800 Subject: [PATCH 13/23] fix: update token controllers --- packages/accounts-controller/src/utils.ts | 2 +- .../src/TokenDetectionController.test.ts | 555 +++++++++++++---- .../src/TokenDetectionController.ts | 13 +- .../src/TokenRatesController.test.ts | 577 ++++++++++++------ .../src/TokenRatesController.ts | 5 +- .../src/TokensController.test.ts | 396 ++++++++---- .../src/TokensController.ts | 83 ++- 7 files changed, 1170 insertions(+), 461 deletions(-) diff --git a/packages/accounts-controller/src/utils.ts b/packages/accounts-controller/src/utils.ts index 5bd7454aa53..0668c18c387 100644 --- a/packages/accounts-controller/src/utils.ts +++ b/packages/accounts-controller/src/utils.ts @@ -78,6 +78,6 @@ export function getUUIDFromAddressOfNormalAccount(address: string): string { */ export function isEVMAccount(internalAccount: InternalAccount): boolean { return [EthAccountType.Eoa, EthAccountType.Erc4337].includes( - internalAccount.type as EthAccountType, + internalAccount?.type as EthAccountType, ); } diff --git a/packages/assets-controllers/src/TokenDetectionController.test.ts b/packages/assets-controllers/src/TokenDetectionController.test.ts index b30e755c876..2e522db01c6 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/src/tests/mocks'; 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', @@ -163,6 +165,8 @@ function buildTokenDetectionControllerMessenger( } describe('TokenDetectionController', () => { + const defaultSelectedAccount = createMockInternalAccount(); + beforeEach(async () => { nock(TOKEN_END_POINT_API) .get(getTokensPath(ChainId.mainnet)) @@ -186,6 +190,48 @@ describe('TokenDetectionController', () => { sinon.restore(); }); + describe('constructor', () => { + describe('selectedAccountId', () => { + it('should get the selectedAccount from accounts controller if not defined in constructor arguments', async () => { + await withController( + { + options: {}, + }, + async ({ mockGetAccount, callActionSpy }) => { + mockGetAccount(defaultSelectedAccount); + expect(callActionSpy).toHaveBeenCalledWith( + 'AccountsController:getSelectedAccount', + ); + }, + ); + }); + + it('should if not start non evm account was passed and selectedAccountId defined in constructor arguments', async () => { + const nonEvmAccount = createMockInternalAccount({ + // @ts-expect-error mocking a bitcoin account + type: 'bitcoin', + }); + await withController( + { + options: {}, + }, + async ({ controller, mockGetAccount, callActionSpy }) => { + mockGetAccount(nonEvmAccount); + expect(callActionSpy).toHaveBeenCalledWith( + 'AccountsController:getSelectedAccount', + ); + + await controller.start(); + + expect(callActionSpy).not.toHaveBeenCalledWith( + 'TokensController:addDetectedTokens', + ); + }, + ); + }); + }); + }); + describe('start', () => { let clock: sinon.SinonFakeTimers; beforeEach(() => { @@ -200,6 +246,7 @@ describe('TokenDetectionController', () => { await withController( { isKeyringUnlocked: false, + options: { selectedAccountId: defaultSelectedAccount.id }, }, async ({ controller }) => { const mockTokens = sinon.stub(controller, 'detectTokens'); @@ -218,8 +265,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(); @@ -236,6 +287,7 @@ describe('TokenDetectionController', () => { await withController( { isKeyringUnlocked: true, + options: { selectedAccountId: defaultSelectedAccount.id }, }, async ({ controller, triggerKeyringLock }) => { const mockTokens = sinon.stub(controller, 'detectTokens'); @@ -252,16 +304,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 +332,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 +352,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 +396,7 @@ describe('TokenDetectionController', () => { [sampleTokenA], { chainId: ChainId.mainnet, - selectedAddress, + selectedAddress: selectedAccount.address, }, ); }, @@ -337,21 +407,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 +464,7 @@ describe('TokenDetectionController', () => { [sampleTokenA], { chainId: '0x89', - selectedAddress, + selectedAddress: selectedAccount.address, }, ); }, @@ -402,17 +476,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 +534,7 @@ describe('TokenDetectionController', () => { [sampleTokenA, sampleTokenB], { chainId: ChainId.mainnet, - selectedAddress, + selectedAddress: selectedAccount.address, }, ); }, @@ -463,20 +545,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 +604,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 +659,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 +700,8 @@ describe('TokenDetectionController', () => { }, }); - triggerSelectedAccountChange({ - address: secondSelectedAddress, - } as InternalAccount); + mockGetAccount(secondSelectedAccount); + triggerSelectedAccountChange(secondSelectedAccount); await advanceTime({ clock, duration: 1 }); expect(callActionSpy).toHaveBeenCalledWith( @@ -613,7 +709,7 @@ describe('TokenDetectionController', () => { [sampleTokenA], { chainId: ChainId.mainnet, - selectedAddress: secondSelectedAddress, + selectedAddress: secondSelectedAccount.address, }, ); }, @@ -624,13 +720,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 +757,7 @@ describe('TokenDetectionController', () => { }); triggerSelectedAccountChange({ - address: selectedAddress, + address: selectedAccount.address, } as InternalAccount); await advanceTime({ clock, duration: 1 }); @@ -675,16 +773,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 +814,7 @@ describe('TokenDetectionController', () => { }); triggerSelectedAccountChange({ - address: secondSelectedAddress, + address: secondSelectedAccount.address, } as InternalAccount); await advanceTime({ clock, duration: 1 }); @@ -732,16 +832,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 +872,7 @@ describe('TokenDetectionController', () => { }); triggerSelectedAccountChange({ - address: secondSelectedAddress, + address: secondSelectedAccount.address, } as InternalAccount); await advanceTime({ clock, duration: 1 }); @@ -781,6 +883,58 @@ describe('TokenDetectionController', () => { ); }); }); + + it('should not restart detection for nonevm accounts', async () => { + const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ + [sampleTokenA.address]: new BN(1), + }); + const nonEvmAccount = createMockInternalAccount({ + // @ts-expect-error mocking a bitcoin account + type: 'bitcoin', + }); + + await withController( + { + options: { + disabled: false, + getBalancesInSingleCall: mockGetBalancesInSingleCall, + selectedAccountId: defaultSelectedAccount.id, + }, + }, + async ({ + callActionSpy, + mockGetAccount, + mockTokenListGetState, + triggerSelectedAccountChange, + }) => { + mockGetAccount(defaultSelectedAccount); + mockTokenListGetState({ + ...getDefaultTokenListState(), + tokensChainsCache: { + '0x1': { + timestamp: 0, + data: { + [sampleTokenA.address]: { + name: sampleTokenA.name, + symbol: sampleTokenA.symbol, + decimals: sampleTokenA.decimals, + address: sampleTokenA.address, + occurrences: 1, + aggregators: sampleTokenA.aggregators, + iconUrl: sampleTokenA.image, + }, + }, + }, + }, + }); + mockGetAccount(nonEvmAccount); + triggerSelectedAccountChange(nonEvmAccount); + expect(callActionSpy).not.toHaveBeenCalledWith( + 'TokensController:addDetectedTokens', + ); + }, + ); + }); }); describe('PreferencesController:stateChange', () => { @@ -798,23 +952,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 +996,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 +1018,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 +1058,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 +1073,7 @@ describe('TokenDetectionController', () => { [sampleTokenA], { chainId: ChainId.mainnet, - selectedAddress, + selectedAddress: selectedAccount.address, }, ); }, @@ -922,23 +1084,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 +1123,10 @@ describe('TokenDetectionController', () => { triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress: secondSelectedAddress, useTokenDetection: false, }); + mockGetAccount(secondSelectedAccount); + triggerSelectedAccountChange(secondSelectedAccount); await advanceTime({ clock, duration: 1 }); expect(callActionSpy).not.toHaveBeenCalledWith( @@ -972,20 +1140,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 +1175,6 @@ describe('TokenDetectionController', () => { triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress, useTokenDetection: true, }); await advanceTime({ clock, duration: 1 }); @@ -1020,24 +1191,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 +1231,10 @@ describe('TokenDetectionController', () => { triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress: secondSelectedAddress, useTokenDetection: true, }); + mockGetAccount(secondSelectedAccount); + triggerSelectedAccountChange(secondSelectedAccount); await advanceTime({ clock, duration: 1 }); expect(callActionSpy).not.toHaveBeenCalledWith( @@ -1071,21 +1248,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 +1284,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 +1308,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 +1347,10 @@ describe('TokenDetectionController', () => { triggerPreferencesStateChange({ ...getDefaultPreferencesState(), - selectedAddress: secondSelectedAddress, useTokenDetection: true, }); + mockGetAccount(secondSelectedAccount); + triggerSelectedAccountChange(secondSelectedAccount); await advanceTime({ clock, duration: 1 }); expect(callActionSpy).not.toHaveBeenCalledWith( @@ -1179,20 +1364,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 +1399,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 +1433,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 +1482,7 @@ describe('TokenDetectionController', () => { [sampleTokenA], { chainId: '0x89', - selectedAddress, + selectedAddress: selectedAccount.address, }, ); }, @@ -1302,20 +1493,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 +1548,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 +1599,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 +1653,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 +1716,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 +1765,7 @@ describe('TokenDetectionController', () => { [sampleTokenA], { chainId: ChainId.mainnet, - selectedAddress, + selectedAddress: selectedAccount.address, }, ); }, @@ -1565,20 +1776,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 +1815,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 +1867,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 +1927,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 +1995,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 +2011,9 @@ describe('TokenDetectionController', () => { mockNetworkState, triggerPreferencesStateChange, callActionSpy, + mockGetAccount, }) => { + mockGetAccount(selectedAccount); mockNetworkState({ ...defaultNetworkState, selectedNetworkClientId: NetworkType.goerli, @@ -1795,7 +2024,7 @@ describe('TokenDetectionController', () => { }); await controller.detectTokens({ networkClientId: NetworkType.goerli, - selectedAddress, + selectedAddress: selectedAccount.address, }); expect(callActionSpy).not.toHaveBeenCalledWith( 'TokensController:addDetectedTokens', @@ -1814,27 +2043,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 +2080,7 @@ describe('TokenDetectionController', () => { }; }), { - selectedAddress, + selectedAddress: selectedAccount.address, chainId: ChainId.mainnet, }, ); @@ -1859,16 +2092,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 +2132,7 @@ describe('TokenDetectionController', () => { await controller.detectTokens({ networkClientId: NetworkType.mainnet, - selectedAddress, + selectedAddress: selectedAccount.address, }); expect(callActionSpy).toHaveBeenCalledWith( @@ -1899,7 +2140,7 @@ describe('TokenDetectionController', () => { [sampleTokenA], { chainId: ChainId.mainnet, - selectedAddress, + selectedAddress: selectedAccount.address, }, ); }, @@ -1910,7 +2151,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 +2162,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 +2189,7 @@ describe('TokenDetectionController', () => { await controller.detectTokens({ networkClientId: NetworkType.mainnet, - selectedAddress, + selectedAddress: selectedAccount.address, }); expect(mockTrackMetaMetricsEvent).toHaveBeenCalledWith({ @@ -1960,6 +2204,50 @@ describe('TokenDetectionController', () => { }, ); }); + + it('should not detect tokens internal account returned from getAccountExpect is nonevm', async () => { + const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ + [sampleTokenA.address]: new BN(1), + }); + const nonEvmAccount = createMockInternalAccount({ + address: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', + // @ts-expect-error non-evm account + type: 'bitcoin', + }); + await withController( + { + options: { + disabled: false, + getBalancesInSingleCall: mockGetBalancesInSingleCall, + selectedAccountId: nonEvmAccount.id, + }, + }, + async ({ + controller, + mockNetworkState, + triggerPreferencesStateChange, + callActionSpy, + mockGetAccount, + }) => { + mockGetAccount(nonEvmAccount); + mockNetworkState({ + ...defaultNetworkState, + selectedNetworkClientId: NetworkType.goerli, + }); + triggerPreferencesStateChange({ + ...getDefaultPreferencesState(), + useTokenDetection: false, + }); + await controller.detectTokens({ + networkClientId: NetworkType.goerli, + selectedAddress: nonEvmAccount.address, + }); + expect(callActionSpy).not.toHaveBeenCalledWith( + 'TokensController:addDetectedTokens', + ); + }, + ); + }); }); }); @@ -1977,6 +2265,7 @@ function getTokensPath(chainId: Hex) { type WithControllerCallback = ({ controller, + mockGetAccount, mockGetSelectedAccount, mockKeyringGetState, mockTokensGetState, @@ -1994,6 +2283,7 @@ type WithControllerCallback = ({ triggerNetworkDidChange, }: { controller: TokenDetectionController; + mockGetAccount: (internalAccount: InternalAccount) => void; mockGetSelectedAccount: (address: string) => void; mockKeyringGetState: (state: KeyringControllerState) => void; mockTokensGetState: (state: TokensState) => void; @@ -2044,6 +2334,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 +2423,9 @@ async function withController( try { return await fn({ controller, + mockGetAccount: (internalAccount: InternalAccount) => { + mockGetAccount.mockReturnValue(internalAccount); + }, mockGetSelectedAccount: (address: string) => { mockGetSelectedAccount.mockReturnValue({ address } as InternalAccount); }, diff --git a/packages/assets-controllers/src/TokenDetectionController.ts b/packages/assets-controllers/src/TokenDetectionController.ts index 6786b30609b..f37f33036a6 100644 --- a/packages/assets-controllers/src/TokenDetectionController.ts +++ b/packages/assets-controllers/src/TokenDetectionController.ts @@ -1,6 +1,6 @@ import { type AccountsControllerGetSelectedAccountAction, - type AccountsControllerGetAccountExpectAction, + type AccountsControllerGetAccountAction, type AccountsControllerSelectedAccountChangeEvent, isEVMAccount, } from '@metamask/accounts-controller'; @@ -107,7 +107,7 @@ export type TokenDetectionControllerActions = export type AllowedActions = | AccountsControllerGetSelectedAccountAction - | AccountsControllerGetAccountExpectAction + | AccountsControllerGetAccountAction | NetworkControllerGetNetworkClientByIdAction | NetworkControllerGetNetworkConfigurationByNetworkClientId | NetworkControllerGetStateAction @@ -232,7 +232,6 @@ export class TokenDetectionController extends StaticIntervalPollingController< const selectedInternalAccount = this.messagingSystem.call( 'AccountsController:getSelectedAccount', ); - // return the first evm internal account. if (isEVMAccount(selectedInternalAccount)) { this.#selectedAccountId = selectedInternalAccount.id; } @@ -458,10 +457,10 @@ export class TokenDetectionController extends StaticIntervalPollingController< networkClientId?: NetworkClientId; } = {}): Promise { const internalAccount = this.messagingSystem.call( - 'AccountsController:getAccountExpect', + 'AccountsController:getAccount', selectedAccountId ?? this.#selectedAccountId, ); - if (!isEVMAccount(internalAccount)) { + if (!internalAccount || !isEVMAccount(internalAccount)) { return; } @@ -492,10 +491,10 @@ export class TokenDetectionController extends StaticIntervalPollingController< } const selectedInternalAccount = this.messagingSystem.call( - 'AccountsController:getAccountExpect', + 'AccountsController:getAccount', this.#selectedAccountId, ); - if (!isEVMAccount(selectedInternalAccount)) { + if (!selectedInternalAccount || !isEVMAccount(selectedInternalAccount)) { return; } diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index a82a195ff7a..fa2e5753836 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/src/tests/mocks'; 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(), @@ -1064,7 +1128,7 @@ describe('TokenRatesController', () => { { allTokens: { '0x1': { - [alternateSelectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: '0x02', decimals: 0, symbol: '', aggregators: [] }, { address: '0x03', decimals: 0, symbol: '', aggregators: [] }, ], @@ -1078,30 +1142,37 @@ 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 () => { + 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 alternateSelectedAccount = createMockInternalAccount({ + address: alternateSelectedAddress, + }); + 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 +1180,7 @@ describe('TokenRatesController', () => { { allTokens: { '0x1': { - [defaultSelectedAddress]: [ + [alternateSelectedAccount.address]: [ { address: '0x02', decimals: 0, symbol: '', aggregators: [] }, { address: '0x03', decimals: 0, symbol: '', aggregators: [] }, ], @@ -1117,53 +1188,47 @@ describe('TokenRatesController', () => { }, }, ); - await controller.start(); const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') .mockResolvedValue(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await preferencesStateChangeListener!({ - selectedAddress: defaultSelectedAddress, - exampleConfig: 'exampleValue', - }); + await selectedAccountChangeListener!(alternateSelectedAccount); expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); }); }); - 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 + describe('non evm chain', () => { + it('should not update exchange rates when its a non evm account', async () => { + const nonEvmAccount = createMockInternalAccount({ + // @ts-expect-error testing bitcoin + type: 'bitcoin', + }); + 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(), }, { allTokens: { - '0x1': { - [alternateSelectedAddress]: [ - { address: '0x02', decimals: 0, symbol: '', aggregators: [] }, - { address: '0x03', decimals: 0, symbol: '', aggregators: [] }, - ], - }, + '0x1': {}, }, }, ); @@ -1172,9 +1237,7 @@ describe('TokenRatesController', () => { .mockResolvedValue(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await preferencesStateChangeListener!({ - selectedAddress: alternateSelectedAddress, - }); + await selectedAccountChangeListener!(nonEvmAccount); expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); }); @@ -1201,10 +1264,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 +1278,7 @@ describe('TokenRatesController', () => { { allTokens: { '0x1': { - [defaultSelectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: mockTokenAddress, decimals: 0, @@ -1245,10 +1311,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 +1325,7 @@ describe('TokenRatesController', () => { { allTokens: { '0x1': { - [defaultSelectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: mockTokenAddress, decimals: 0, @@ -1300,8 +1369,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 +1379,15 @@ describe('TokenRatesController', () => { ticker: NetworksTicker.mainnet, }, }), + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), tokenPricesService, }, { allTokens: { '0x1': { - [defaultSelectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: mockTokenAddress, decimals: 0, @@ -1352,8 +1424,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 +1434,15 @@ describe('TokenRatesController', () => { ticker: NetworksTicker.mainnet, }, }), + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), tokenPricesService, }, { allTokens: { '0x1': { - [defaultSelectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: '0x02', decimals: 0, @@ -1417,8 +1492,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 +1502,15 @@ describe('TokenRatesController', () => { ticker: 'LOL', }, }), + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), tokenPricesService, }, { allTokens: { '0x1': { - [defaultSelectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: '0x02', decimals: 0, @@ -1483,8 +1561,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 +1571,15 @@ describe('TokenRatesController', () => { ticker: 'LOL', }, }), + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), tokenPricesService, }, { allTokens: { '0x1': { - [defaultSelectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: '0x02', decimals: 0, @@ -1544,8 +1625,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 +1635,15 @@ describe('TokenRatesController', () => { ticker: NetworksTicker.mainnet, }, }), + getInternalAccount: jest + .fn() + .mockReturnValue(defaultMockInternalAccount), tokenPricesService, }, { allTokens: { '0x1': { - [defaultSelectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: mockTokenAddress, decimals: 0, @@ -1598,14 +1682,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 +1725,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 +1787,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 +1806,7 @@ describe('TokenRatesController', () => { await callUpdateExchangeRatesMethod({ allTokens: { [toHex(1)]: { - [controller.config.selectedAddress]: [ + [defaultMockInternalAccount.address]: [ { address: tokenAddress, decimals: 18, @@ -1736,13 +1852,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 +1913,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 +1992,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 +2080,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 +2170,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 +2236,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 +2315,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 +2384,8 @@ describe('TokenRatesController', () => { */ type ControllerEvents = { networkStateChange: (state: NetworkState) => void; - preferencesStateChange: (state: PreferencesState) => void; tokensStateChange: (state: TokensState) => void; + seletedAccountChange: (internalAccount: InternalAccount) => void; }; /** @@ -2267,13 +2443,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 e284ca4f9ae..5393ac04660 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -288,7 +288,10 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< #getTokenAddresses(chainId: Hex): Hex[] { const { allTokens, allDetectedTokens, selectedAccountId } = this.config; const internalAccount = this.getInternalAccount(selectedAccountId); - const tokens = allTokens[chainId]?.[internalAccount.address]; + if (!internalAccount || !isEVMAccount(internalAccount)) { + return []; + } + const tokens = allTokens[chainId]?.[internalAccount.address] || []; const detectedTokens = allDetectedTokens[chainId]?.[internalAccount.address] || []; diff --git a/packages/assets-controllers/src/TokensController.test.ts b/packages/assets-controllers/src/TokensController.test.ts index 20ea57ee28d..abc0c7c8e05 100644 --- a/packages/assets-controllers/src/TokensController.test.ts +++ b/packages/assets-controllers/src/TokensController.test.ts @@ -1,3 +1,4 @@ +import type { AccountsController } from '@metamask/accounts-controller'; import type { ApprovalControllerEvents } from '@metamask/approval-controller'; import { ApprovalController, @@ -15,6 +16,7 @@ import { convertHexToDecimal, toHex, } from '@metamask/controller-utils'; +import type { InternalAccount } from '@metamask/keyring-api'; import type { BlockTrackerProxy, NetworkController, @@ -25,13 +27,12 @@ import { defaultState as defaultNetworkState, NetworkClientType, } 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 { FakeBlockTracker } from '../../../tests/fake-block-tracker'; import { FakeProvider } from '../../../tests/fake-provider'; +import { createMockInternalAccount } from '../../accounts-controller/src/tests/mocks'; import { ERC20Standard } from './Standards/ERC20Standard'; import { ERC1155Standard } from './Standards/NftStandards/ERC1155/ERC1155Standard'; import { TOKEN_END_POINT_API } from './token-service'; @@ -98,6 +99,11 @@ describe('TokensController', () => { Parameters >; + let getAccountHander: jest.Mock< + ReturnType, + Parameters + >; + const changeNetwork = (providerConfig: ProviderConfig) => { messenger.publish(`NetworkController:networkDidChange`, { ...defaultNetworkState, @@ -105,14 +111,21 @@ describe('TokensController', () => { }); }; - const triggerPreferencesStateChange = (state: PreferencesState) => { - messenger.publish('PreferencesController:stateChange', state, []); + const triggerSelectedAccountChange = ( + newInternalAccount: InternalAccount, + ) => { + messenger.publish( + 'AccountsController:selectedAccountChange', + newInternalAccount, + ); }; const fakeProvider = new FakeProvider(); beforeEach(async () => { - const defaultSelectedAddress = '0x1'; + const defaultSelectedAccount = createMockInternalAccount({ + address: '0x1', + }); messenger = new ControllerMessenger(); approvalControllerMessenger = messenger.getRestricted({ @@ -130,17 +143,18 @@ describe('TokensController', () => { allowedActions: [ 'ApprovalController:addRequest', 'NetworkController:getNetworkClientById', + 'AccountsController:getAccount', ], allowedEvents: [ 'NetworkController:networkDidChange', - 'PreferencesController:stateChange', 'TokenListController:stateChange', + 'AccountsController:selectedAccountChange', ], }); tokensController = new TokensController({ chainId: ChainId.mainnet, config: { - selectedAddress: defaultSelectedAddress, + selectedAccountId: defaultSelectedAccount.id, provider: fakeProvider, }, messenger: tokensControllerMessenger, @@ -161,6 +175,12 @@ describe('TokensController', () => { >, ), ); + + getAccountHander = jest.fn(); + messenger.registerActionHandler( + `AccountsController:getAccount`, + getAccountHander.mockReturnValue(defaultSelectedAccount), + ); }); afterEach(() => { @@ -360,27 +380,25 @@ describe('TokensController', () => { it('should add token by selected address', async () => { const stub = stubCreateEthers(tokensController, () => false); - const firstAddress = '0x123'; - const secondAddress = '0x321'; - - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: firstAddress, + const firstInternalAccount = createMockInternalAccount({ + address: '0x123', }); + const secondInternalAccount = createMockInternalAccount({ + address: '0x321', + }); + getAccountHander.mockReturnValueOnce(firstInternalAccount); + triggerSelectedAccountChange(firstInternalAccount); + await tokensController.addToken({ address: '0x01', symbol: 'bar', decimals: 2, }); - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: secondAddress, - }); + getAccountHander.mockReturnValueOnce(secondInternalAccount); + triggerSelectedAccountChange(secondInternalAccount); + expect(tokensController.state.tokens).toHaveLength(0); - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: firstAddress, - }); + triggerSelectedAccountChange(firstInternalAccount); expect(tokensController.state.tokens[0]).toStrictEqual({ address: '0x01', decimals: 2, @@ -490,21 +508,21 @@ describe('TokensController', () => { it('should remove token by selected address', async () => { const stub = stubCreateEthers(tokensController, () => false); - const firstAddress = '0x123'; - const secondAddress = '0x321'; - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: firstAddress, + const firstInternalAccount = createMockInternalAccount({ + address: '0x123', + }); + const secondInternalAccount = createMockInternalAccount({ + address: '0x321', }); + getAccountHander.mockReturnValueOnce(firstInternalAccount); + triggerSelectedAccountChange(firstInternalAccount); await tokensController.addToken({ address: '0x02', symbol: 'baz', decimals: 2, }); - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: secondAddress, - }); + getAccountHander.mockReturnValueOnce(secondInternalAccount); + triggerSelectedAccountChange(secondInternalAccount); await tokensController.addToken({ address: '0x01', symbol: 'bar', @@ -512,10 +530,8 @@ describe('TokensController', () => { }); tokensController.ignoreTokens(['0x01']); expect(tokensController.state.tokens).toHaveLength(0); - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: firstAddress, - }); + getAccountHander.mockReturnValueOnce(firstInternalAccount); + triggerSelectedAccountChange(firstInternalAccount); expect(tokensController.state.tokens[0]).toStrictEqual({ address: '0x02', decimals: 2, @@ -561,14 +577,13 @@ describe('TokensController', () => { }); describe('ignoredTokens', () => { - const defaultSelectedAddress = '0x0001'; + const selectedAccount = createMockInternalAccount({ + address: '0x0001', + }); let createEthersStub: sinon.SinonStub; beforeEach(() => { - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: defaultSelectedAddress, - }); + triggerSelectedAccountChange(selectedAccount); changeNetwork(SEPOLIA); createEthersStub = stubCreateEthers(tokensController, () => false); @@ -604,11 +619,8 @@ describe('TokensController', () => { }); it('should remove a token from the ignoredTokens/allIgnoredTokens lists if re-added as part of a bulk addTokens add', async () => { - const selectedAddress = '0x0001'; - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress, - }); + getAccountHander.mockReturnValue(selectedAccount); + triggerSelectedAccountChange(selectedAccount); changeNetwork(SEPOLIA); await tokensController.addToken({ address: '0x01', @@ -635,12 +647,13 @@ describe('TokensController', () => { expect(tokensController.state.ignoredTokens).toHaveLength(1); expect(tokensController.state.allIgnoredTokens).toStrictEqual({ [SEPOLIA.chainId]: { - [selectedAddress]: ['0xFAa'], + [selectedAccount.address]: ['0xFAa'], }, }); }); it('should be able to clear the ignoredToken list', async () => { + getAccountHander.mockReturnValue(selectedAccount); await tokensController.addToken({ address: '0x01', symbol: 'bar', @@ -651,7 +664,7 @@ describe('TokensController', () => { expect(tokensController.state.tokens).toHaveLength(0); expect(tokensController.state.allIgnoredTokens).toStrictEqual({ [SEPOLIA.chainId]: { - [defaultSelectedAddress]: ['0x01'], + [selectedAccount.address]: ['0x01'], }, }); tokensController.clearIgnoredTokens(); @@ -663,12 +676,16 @@ describe('TokensController', () => { it('should ignore tokens by [chainID][accountAddress]', async () => { const selectedAddress1 = '0x0001'; + const selectedInternalAccount1 = createMockInternalAccount({ + address: selectedAddress1, + }); const selectedAddress2 = '0x0002'; - - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: selectedAddress1, + const selectedInternalAccount2 = createMockInternalAccount({ + address: selectedAddress2, }); + + getAccountHander.mockReturnValue(selectedInternalAccount1); // addToken call + triggerSelectedAccountChange(selectedInternalAccount1); changeNetwork(SEPOLIA); await tokensController.addToken({ @@ -692,10 +709,8 @@ describe('TokensController', () => { tokensController.ignoreTokens(['0x02']); expect(tokensController.state.ignoredTokens).toStrictEqual(['0x02']); - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: selectedAddress2, - }); + getAccountHander.mockReturnValue(selectedInternalAccount2); + triggerSelectedAccountChange(selectedInternalAccount2); expect(tokensController.state.ignoredTokens).toHaveLength(0); await tokensController.addToken({ address: '0x03', @@ -707,11 +722,11 @@ describe('TokensController', () => { expect(tokensController.state.allIgnoredTokens).toStrictEqual({ [SEPOLIA.chainId]: { - [selectedAddress1]: ['0x01'], + [selectedInternalAccount1.address]: ['0x01'], }, [GOERLI.chainId]: { - [selectedAddress1]: ['0x02'], - [selectedAddress2]: ['0x03'], + [selectedInternalAccount1.address]: ['0x02'], + [selectedInternalAccount2.address]: ['0x03'], }, }); }); @@ -995,11 +1010,12 @@ describe('TokensController', () => { // The currently configured chain + address const CONFIGURED_CHAIN = SEPOLIA; const CONFIGURED_ADDRESS = '0xConfiguredAddress'; - changeNetwork(CONFIGURED_CHAIN); - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: CONFIGURED_ADDRESS, + const CONFIGURED_ACCOUNT = createMockInternalAccount({ + address: CONFIGURED_ADDRESS, }); + changeNetwork(CONFIGURED_CHAIN); + getAccountHander.mockReturnValue(CONFIGURED_ACCOUNT); + triggerSelectedAccountChange(CONFIGURED_ACCOUNT); // A different chain + address const OTHER_CHAIN = '0xOtherChainId'; @@ -1063,6 +1079,31 @@ describe('TokensController', () => { stub.restore(); }); + + it('should return if the account is non evm', async () => { + const dummyDetectedToken: Token = { + address: '0x01', + symbol: 'barA', + decimals: 2, + aggregators: [], + image: undefined, + isERC721: false, + name: undefined, + }; + + const nonEvmAccount = createMockInternalAccount({ + // @ts-expect-error non evm account + type: 'bitcoin', + }); + + getAccountHander.mockReturnValue(nonEvmAccount); + const spyUpdate = jest.spyOn(tokensController, 'update'); + + await tokensController.addDetectedTokens([dummyDetectedToken]); + + expect(tokensController.state.detectedTokens).toStrictEqual([]); + expect(spyUpdate).not.toHaveBeenCalled(); + }); }); describe('addTokens method', function () { @@ -1141,10 +1182,35 @@ describe('TokensController', () => { 'networkClientId1', ); }); + + it('should return an empty array if its a nonevm account', async () => { + const dummyToken = { + address: '0x01', + symbol: 'barA', + decimals: 2, + aggregators: [], + image: undefined, + name: undefined, + }; + const nonEvmAccount = createMockInternalAccount({ + // @ts-expect-error non evm account + type: 'bitcoin', + }); + + getAccountHander.mockReturnValue(nonEvmAccount); + const spyUpdate = jest.spyOn(tokensController, 'update'); + await tokensController.addToken(dummyToken); + + expect(tokensController.state.tokens).toStrictEqual([]); + expect(spyUpdate).not.toHaveBeenCalled(); + }); }); describe('_getNewAllTokensState method', () => { const dummySelectedAddress = '0x1'; + const dummySelectedAccount = createMockInternalAccount({ + address: dummySelectedAddress, + }); const dummyTokens: Token[] = [ { address: '0x01', @@ -1157,20 +1223,22 @@ describe('TokensController', () => { it('should nest newTokens under chain ID and selected address when provided with newTokens as input', () => { tokensController.configure({ - selectedAddress: dummySelectedAddress, + selectedAccountId: dummySelectedAccount.id, chainId: ChainId.mainnet, }); const processedTokens = tokensController._getNewAllTokensState({ newTokens: dummyTokens, }); expect( - processedTokens.newAllTokens[ChainId.mainnet][dummySelectedAddress], + processedTokens.newAllTokens[ChainId.mainnet][ + dummySelectedAccount.address + ], ).toStrictEqual(dummyTokens); }); it('should nest detectedTokens under chain ID and selected address when provided with detectedTokens as input', () => { tokensController.configure({ - selectedAddress: dummySelectedAddress, + selectedAccountId: dummySelectedAccount.id, chainId: ChainId.mainnet, }); const processedTokens = tokensController._getNewAllTokensState({ @@ -1178,14 +1246,14 @@ describe('TokensController', () => { }); expect( processedTokens.newAllDetectedTokens[ChainId.mainnet][ - dummySelectedAddress + dummySelectedAccount.address ], ).toStrictEqual(dummyTokens); }); it('should nest ignoredTokens under chain ID and selected address when provided with ignoredTokens as input', () => { tokensController.configure({ - selectedAddress: dummySelectedAddress, + selectedAccountId: dummySelectedAccount.id, chainId: ChainId.mainnet, }); const dummyIgnoredTokens = [dummyTokens[0].address]; @@ -1201,6 +1269,9 @@ describe('TokensController', () => { }); describe('watchAsset', function () { + const defaultSelectedAccount = createMockInternalAccount({ + address: '0x1', + }); // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any let asset: any, type: any; @@ -1250,6 +1321,7 @@ describe('TokensController', () => { 'contractSupportsBase1155Interface', ) .mockImplementation(() => isERC1155); + getAccountHander.mockReturnValue(defaultSelectedAccount); }); afterEach(() => { @@ -1390,7 +1462,11 @@ describe('TokensController', () => { it('should use symbols/decimals from contract, and allow them to be optional in the request', async function () { mockContract([asset]); - jest.spyOn(messenger, 'call').mockResolvedValue(undefined); + jest + .spyOn(messenger, 'call') + .mockReturnValueOnce(defaultSelectedAccount) + .mockResolvedValueOnce(undefined) + .mockReturnValueOnce(defaultSelectedAccount); // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any const reqAsset: any = { @@ -1409,7 +1485,11 @@ describe('TokensController', () => { }); it('should use symbols/decimals from request, and allow them to be optional in the contract', async function () { - jest.spyOn(messenger, 'call').mockResolvedValue(undefined); + jest + .spyOn(messenger, 'call') + .mockReturnValueOnce(defaultSelectedAccount) + .mockResolvedValueOnce(undefined) + .mockReturnValueOnce(defaultSelectedAccount); // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any const reqAsset: any = { ...asset, symbol: 'MYSYMBOL', decimals: 13 }; @@ -1446,7 +1526,11 @@ describe('TokensController', () => { it('should perform case insensitive validation of symbols', async function () { asset.symbol = 'ABC'; mockContract([asset, asset]); - jest.spyOn(messenger, 'call').mockResolvedValue(undefined); + jest + .spyOn(messenger, 'call') + .mockReturnValueOnce(defaultSelectedAccount) + .mockResolvedValueOnce(undefined) + .mockReturnValueOnce(defaultSelectedAccount); await tokensController.watchAsset({ asset: { ...asset, symbol: 'abc' }, @@ -1462,7 +1546,11 @@ describe('TokensController', () => { }); it('should be lenient when accepting string vs integer for decimals', async () => { - jest.spyOn(messenger, 'call').mockResolvedValue(undefined); + jest + .spyOn(messenger, 'call') + .mockReturnValueOnce(defaultSelectedAccount) + .mockResolvedValueOnce(undefined) + .mockReturnValue(defaultSelectedAccount); for (const decimals of [6, '6']) { asset.decimals = decimals; mockContract([asset]); @@ -1487,8 +1575,9 @@ describe('TokensController', () => { const callActionSpy = jest .spyOn(messenger, 'call') - .mockResolvedValue(undefined); - + .mockReturnValueOnce(defaultSelectedAccount) + .mockResolvedValueOnce(undefined) + .mockReturnValue(defaultSelectedAccount); await tokensController.watchAsset({ asset, type }); expect(tokensController.state.tokens).toHaveLength(1); @@ -1499,8 +1588,9 @@ describe('TokensController', () => { ...asset, }, ]); - expect(callActionSpy).toHaveBeenCalledTimes(1); - expect(callActionSpy).toHaveBeenCalledWith( + expect(callActionSpy).toHaveBeenCalledTimes(4); + expect(callActionSpy).toHaveBeenNthCalledWith( + 2, 'ApprovalController:addRequest', { id: requestId, @@ -1525,7 +1615,9 @@ describe('TokensController', () => { const callActionSpy = jest .spyOn(messenger, 'call') - .mockResolvedValue(undefined); + .mockReturnValueOnce(defaultSelectedAccount) + .mockResolvedValueOnce(undefined) + .mockReturnValue(defaultSelectedAccount); await tokensController.watchAsset({ asset, type, interactingAddress }); @@ -1543,8 +1635,9 @@ describe('TokensController', () => { ...asset, }, ]); - expect(callActionSpy).toHaveBeenCalledTimes(1); - expect(callActionSpy).toHaveBeenCalledWith( + expect(callActionSpy).toHaveBeenCalledTimes(4); + expect(callActionSpy).toHaveBeenNthCalledWith( + 2, 'ApprovalController:addRequest', { id: requestId, @@ -1630,6 +1723,7 @@ describe('TokensController', () => { const errorMessage = 'Mock Error Message'; const callActionSpy = jest .spyOn(messenger, 'call') + .mockReturnValueOnce(defaultSelectedAccount) .mockRejectedValue(new Error(errorMessage)); await expect( @@ -1638,8 +1732,9 @@ describe('TokensController', () => { expect(tokensController.state.tokens).toHaveLength(0); expect(tokensController.state.tokens).toStrictEqual([]); - expect(callActionSpy).toHaveBeenCalledTimes(1); - expect(callActionSpy).toHaveBeenCalledWith( + expect(callActionSpy).toHaveBeenCalledTimes(2); + expect(callActionSpy).toHaveBeenNthCalledWith( + 2, 'ApprovalController:addRequest', { id: requestId, @@ -1728,13 +1823,13 @@ describe('TokensController', () => { }); }); - describe('onPreferencesStateChange', function () { - it('should update tokens list when set address changes', async function () { + describe('onSelectedAccountChange', () => { + it('should update tokens list when set address changes', async () => { + const mockAccount1 = createMockInternalAccount({ address: '0x1' }); + const mockAccount2 = createMockInternalAccount({ address: '0x2' }); const stub = stubCreateEthers(tokensController, () => false); - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: '0x1', - }); + getAccountHander.mockReturnValue(mockAccount1); + triggerSelectedAccountChange(mockAccount1); await tokensController.addToken({ address: '0x01', symbol: 'A', @@ -1745,20 +1840,16 @@ describe('TokensController', () => { symbol: 'B', decimals: 5, }); - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: '0x2', - }); + getAccountHander.mockReturnValue(mockAccount2); + triggerSelectedAccountChange(mockAccount2); expect(tokensController.state.tokens).toStrictEqual([]); await tokensController.addToken({ address: '0x03', symbol: 'C', decimals: 6, }); - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: '0x1', - }); + getAccountHander.mockReturnValue(mockAccount1); + triggerSelectedAccountChange(mockAccount1); expect(tokensController.state.tokens).toStrictEqual([ { address: '0x01', @@ -1781,10 +1872,8 @@ describe('TokensController', () => { name: undefined, }, ]); - triggerPreferencesStateChange({ - ...getDefaultPreferencesState(), - selectedAddress: '0x2', - }); + getAccountHander.mockReturnValue(mockAccount2); + triggerSelectedAccountChange(mockAccount2); expect(tokensController.state.tokens).toStrictEqual([ { address: '0x03', @@ -1797,9 +1886,85 @@ describe('TokensController', () => { name: undefined, }, ]); - stub.restore(); }); + + it('should not update selectedTokenId if the selected account is not an EVM account', async () => { + const mockEvmAccount = createMockInternalAccount(); + triggerSelectedAccountChange(mockEvmAccount); + const mockNonEvmAccount = createMockInternalAccount({ + // @ts-expect-error testing a not evm type + type: 'bitcoin', + }); + triggerSelectedAccountChange(mockNonEvmAccount); + expect(tokensController.config.selectedAccountId).toBe(mockEvmAccount.id); + }); + }); + + describe('Non evm check', () => { + it('_getNewAllTokensStatem should not update if the account is not an EVM account', async () => { + const dummySelectedAddress = '0x1'; + const dummySelectedAccount = createMockInternalAccount({ + address: dummySelectedAddress, + // @ts-expect-error testing a not evm type + type: 'bitcoin', + }); + tokensController.configure({ + selectedAccountId: dummySelectedAccount.id, + chainId: ChainId.mainnet, + }); + const processedTokens = tokensController._getNewAllTokensState({ + newTokens: [], + }); + expect(processedTokens.newAllTokens).toStrictEqual({}); + }); + + it('watchAsset should not update if the account is not an EVM account', async () => { + const nonEvmAccount = createMockInternalAccount({ + // @ts-expect-error testing a not evm type + type: 'bitcoin', + }); + getAccountHander.mockReturnValue(nonEvmAccount); + const type = ERC20; + const asset = { + address: '0x000000000000000000000000000000000000dEaD', + decimals: 12, + symbol: 'SES', + image: 'image', + name: undefined, + }; + await expect( + async () => + await tokensController.watchAsset({ + asset, + type, + }), + ).rejects.toThrow('Account is not an EVM account'); + }); + + it('addDetectedTokens should not update if the account is not an EVM account', async () => { + const nonEvmAccount = createMockInternalAccount({ + // @ts-expect-error testing a not evm type + type: 'bitcoin', + }); + getAccountHander.mockReturnValue(nonEvmAccount); + const spyUpdate = jest.spyOn(tokensController, 'update'); + await tokensController.addDetectedTokens([]); + expect(spyUpdate).not.toHaveBeenCalled(); + }); + + it('addDetectedTokens should not update if the account is not an EVM account on second check', async () => { + const nonEvmAccount = createMockInternalAccount({ + // @ts-expect-error testing a not evm type + type: 'bitcoin', + }); + getAccountHander + .mockReturnValueOnce(createMockInternalAccount()) + .mockReturnValue(nonEvmAccount); + const spyUpdate = jest.spyOn(tokensController, 'update'); + await tokensController.addDetectedTokens([]); + expect(spyUpdate).not.toHaveBeenCalled(); + }); }); describe('onNetworkDidChange', function () { @@ -1890,6 +2055,24 @@ describe('TokensController', () => { stub.restore(); }); + + it("should not update if selectedAccountId is ''", async () => { + const updateSpy = jest.spyOn(tokensController, 'update'); + getAccountHander.mockReturnValue(undefined); + changeNetwork(SEPOLIA); + expect(updateSpy).not.toHaveBeenCalled(); + }); + + it('should not update if its a non evm account that is selected', async () => { + const nonEvmAccount = createMockInternalAccount({ + // @ts-expect-error testing a not evm type + type: 'bitcoin', + }); + const updateSpy = jest.spyOn(tokensController, 'update'); + getAccountHander.mockReturnValue(nonEvmAccount); + changeNetwork(SEPOLIA); + expect(updateSpy).not.toHaveBeenCalled(); + }); }); describe('Clearing nested lists', function () { @@ -1903,23 +2086,28 @@ describe('TokensController', () => { }, ]; const selectedAddress = '0x1'; + const selectedInternalAccount = createMockInternalAccount({ + address: selectedAddress, + }); const tokenAddress = '0x01'; it('should clear nest allTokens under chain ID and selected address when an added token is ignored', async () => { tokensController.configure({ - selectedAddress, + selectedAccountId: selectedInternalAccount.id, chainId: ChainId.mainnet, }); await tokensController.addTokens(dummyTokens); tokensController.ignoreTokens(['0x01']); expect( - tokensController.state.allTokens[ChainId.mainnet][selectedAddress], + tokensController.state.allTokens[ChainId.mainnet][ + selectedInternalAccount.address + ], ).toStrictEqual([]); }); it('should clear nest allIgnoredTokens under chain ID and selected address when an ignored token is re-added', async () => { tokensController.configure({ - selectedAddress, + selectedAccountId: selectedInternalAccount.id, chainId: ChainId.mainnet, }); await tokensController.addTokens(dummyTokens); @@ -1928,14 +2116,14 @@ describe('TokensController', () => { expect( tokensController.state.allIgnoredTokens[ChainId.mainnet][ - selectedAddress + selectedInternalAccount.address ], ).toStrictEqual([]); }); it('should clear nest allDetectedTokens under chain ID and selected address when an detected token is added to tokens list', async () => { tokensController.configure({ - selectedAddress, + selectedAccountId: selectedInternalAccount.id, chainId: ChainId.mainnet, }); await tokensController.addDetectedTokens(dummyTokens); @@ -1943,7 +2131,7 @@ describe('TokensController', () => { expect( tokensController.state.allDetectedTokens[ChainId.mainnet][ - selectedAddress + selectedInternalAccount.address ], ).toStrictEqual([]); }); diff --git a/packages/assets-controllers/src/TokensController.ts b/packages/assets-controllers/src/TokensController.ts index 369f4b53a94..cecfb384502 100644 --- a/packages/assets-controllers/src/TokensController.ts +++ b/packages/assets-controllers/src/TokensController.ts @@ -1,9 +1,10 @@ import { Contract } from '@ethersproject/contracts'; import { Web3Provider } from '@ethersproject/providers'; import type { - AccountsControllerGetAccountExpectAction, + AccountsControllerGetAccountAction, AccountsControllerSelectedAccountChangeEvent, } from '@metamask/accounts-controller'; +import { isEVMAccount } from '@metamask/accounts-controller/src/utils'; import type { AddApprovalRequest } from '@metamask/approval-controller'; import type { BaseConfig, @@ -50,7 +51,6 @@ import type { TokenListToken, } from './TokenListController'; import type { Token } from './TokenRatesController'; -import { isEVMAccoount } from '@metamask/accounts-controller/src/utils'; /** * @type TokensConfig @@ -131,7 +131,7 @@ export type TokensControllerAddDetectedTokensAction = { export type AllowedActions = | AddApprovalRequest | NetworkControllerGetNetworkClientByIdAction - | AccountsControllerGetAccountExpectAction; + | AccountsControllerGetAccountAction; // TODO: Once `TokensController` is upgraded to V2, rewrite this type using the `ControllerStateChangeEvent` type, which constrains `TokensState` as `Record`. export type TokensControllerStateChangeEvent = { @@ -265,7 +265,7 @@ export class TokensController extends BaseControllerV1< this.messagingSystem.subscribe( 'AccountsController:selectedAccountChange', (internalAccount) => { - if (!isEVMAccoount(internalAccount)) { + if (!isEVMAccount(internalAccount)) { return; } const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state; @@ -286,18 +286,26 @@ export class TokensController extends BaseControllerV1< ({ providerConfig }) => { const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state; const { selectedAccountId } = this.config; - const { address: selectedAddress } = this.messagingSystem.call( - 'AccountsController:getAccountExpect', + const selectedInternalAccount = this.messagingSystem.call( + 'AccountsController:getAccount', selectedAccountId, ); + if ( + !selectedInternalAccount || + !isEVMAccount(selectedInternalAccount) + ) { + return; + } const { chainId } = providerConfig; this.abortController.abort(); this.abortController = new AbortController(); this.configure({ chainId }); this.update({ - tokens: allTokens[chainId]?.[selectedAddress] || [], - ignoredTokens: allIgnoredTokens[chainId]?.[selectedAddress] || [], - detectedTokens: allDetectedTokens[chainId]?.[selectedAddress] || [], + tokens: allTokens[chainId]?.[selectedInternalAccount.address] || [], + ignoredTokens: + allIgnoredTokens[chainId]?.[selectedInternalAccount.address] || [], + detectedTokens: + allDetectedTokens[chainId]?.[selectedInternalAccount.address] || [], }); }, ); @@ -355,10 +363,10 @@ export class TokensController extends BaseControllerV1< } const internalAccount = this.messagingSystem.call( - 'AccountsController:getAccountExpect', + 'AccountsController:getAccount', selectedAccountId, ); - if (!isEVMAccoount(internalAccount)) { + if (!internalAccount || !isEVMAccount(internalAccount)) { return []; } @@ -572,11 +580,12 @@ export class TokensController extends BaseControllerV1< const releaseLock = await this.mutex.acquire(); const internalAccount = this.messagingSystem.call( - 'AccountsController:getAccountExpect', + 'AccountsController:getAccount', this.config.selectedAccountId, ); - if (!isEVMAccoount(internalAccount)) { - return []; + if (!internalAccount || !isEVMAccount(internalAccount)) { + releaseLock(); + return; } // Get existing tokens for the chain + account @@ -649,12 +658,25 @@ 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; - newTokens = newAllTokens?.[currentChain]?.[currentAddress] || []; + const currentInternalAccount = this.messagingSystem.call( + 'AccountsController:getAccount', + currentAccountId, + ); + + if (!currentInternalAccount || !isEVMAccount(currentInternalAccount)) { + releaseLock(); + return; + } + + newTokens = + newAllTokens?.[currentChain]?.[currentInternalAccount.address] || []; newDetectedTokens = - newAllDetectedTokens?.[currentChain]?.[currentAddress] || []; + newAllDetectedTokens?.[currentChain]?.[ + currentInternalAccount.address + ] || []; this.update({ tokens: newTokens, @@ -805,6 +827,15 @@ 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, + ); + if (!selectedAccount || !isEVMAccount(selectedAccount)) { + throw new Error(`Account is not an EVM account`); + } + // Validate contract if (await this._detectIsERC721(asset.address, networkClientId)) { @@ -895,7 +926,7 @@ export class TokensController extends BaseControllerV1< id: this._generateRandomId(), time: Date.now(), type, - interactingAddress: interactingAddress || this.config.selectedAddress, + interactingAddress: interactingAddress || selectedAccount.address, }; await this._requestApproval(suggestedAssetMeta); @@ -939,9 +970,21 @@ export class TokensController extends BaseControllerV1< interactingChainId, } = params; const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state; - const { chainId, selectedAddress } = this.config; + const { chainId, selectedAccountId } = this.config; + const selectedInternalAccount = this.messagingSystem.call( + 'AccountsController:getAccount', + selectedAccountId, + ); + if (!selectedInternalAccount || !isEVMAccount(selectedInternalAccount)) { + return { + newAllTokens: allTokens, + newAllIgnoredTokens: allIgnoredTokens, + newAllDetectedTokens: allDetectedTokens, + }; + } - const userAddressToAddTokens = interactingAddress ?? selectedAddress; + const userAddressToAddTokens = + interactingAddress ?? selectedInternalAccount.address; const chainIdToAddTokens = interactingChainId ?? chainId; let newAllTokens = allTokens; From 6c0595d2c30912e6b29c1d2c92f7dcf35c207532 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Mon, 29 Apr 2024 16:24:47 +0800 Subject: [PATCH 14/23] feat: add tests to util and mocks --- .../src/tests/mocks.test.ts | 52 +++++++++++++++++++ .../accounts-controller/src/utils.test.ts | 18 +++++++ 2 files changed, 70 insertions(+) create mode 100644 packages/accounts-controller/src/tests/mocks.test.ts create mode 100644 packages/accounts-controller/src/utils.test.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..b59b7a51a98 --- /dev/null +++ b/packages/accounts-controller/src/tests/mocks.test.ts @@ -0,0 +1,52 @@ +import { 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, + }, + }); + }); +}); diff --git a/packages/accounts-controller/src/utils.test.ts b/packages/accounts-controller/src/utils.test.ts new file mode 100644 index 00000000000..010f8848491 --- /dev/null +++ b/packages/accounts-controller/src/utils.test.ts @@ -0,0 +1,18 @@ +import { EthAccountType } from '@metamask/keyring-api'; + +import { createMockInternalAccount } from './tests/mocks'; +import { isEVMAccount } from './utils'; + +describe('isEVMAccount', () => { + it.each([ + [EthAccountType.Eoa, true], + [EthAccountType.Erc4337, true], + ['bip122', false], + ])('%s should return %s', (accountType, expected) => { + expect( + isEVMAccount( + createMockInternalAccount({ type: accountType as EthAccountType }), + ), + ).toBe(expected); + }); +}); From cb17e99bb2439146439224fa175b469319121d58 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Mon, 29 Apr 2024 21:01:33 +0800 Subject: [PATCH 15/23] fix: remove nonevm check during selectedAccountChange --- .../src/TokenDetectionController.ts | 13 +++++-------- .../assets-controllers/src/TokenRatesController.ts | 10 ++++++---- .../src/TokensController.test.ts | 11 ----------- .../assets-controllers/src/TokensController.ts | 14 ++++++++------ 4 files changed, 19 insertions(+), 29 deletions(-) diff --git a/packages/assets-controllers/src/TokenDetectionController.ts b/packages/assets-controllers/src/TokenDetectionController.ts index f37f33036a6..6f909db58a4 100644 --- a/packages/assets-controllers/src/TokenDetectionController.ts +++ b/packages/assets-controllers/src/TokenDetectionController.ts @@ -232,10 +232,7 @@ export class TokenDetectionController extends StaticIntervalPollingController< const selectedInternalAccount = this.messagingSystem.call( 'AccountsController:getSelectedAccount', ); - if (isEVMAccount(selectedInternalAccount)) { - this.#selectedAccountId = selectedInternalAccount.id; - } - this.#selectedAccountId = ''; + this.#selectedAccountId = selectedInternalAccount.id; } const { chainId, networkClientId } = @@ -288,6 +285,9 @@ export class TokenDetectionController extends StaticIntervalPollingController< this.messagingSystem.subscribe( 'PreferencesController:stateChange', async ({ useTokenDetection }) => { + const selectedAccount = this.messagingSystem.call( + 'AccountsController:getSelectedAccount', + ); const isDetectionChangedFromPreferences = this.#isDetectionEnabledFromPreferences !== useTokenDetection; @@ -295,7 +295,7 @@ export class TokenDetectionController extends StaticIntervalPollingController< if (isDetectionChangedFromPreferences) { await this.#restartTokenDetection({ - selectedAccountId: this.#selectedAccountId, + selectedAccountId: selectedAccount.id, }); } }, @@ -304,9 +304,6 @@ export class TokenDetectionController extends StaticIntervalPollingController< this.messagingSystem.subscribe( 'AccountsController:selectedAccountChange', async (internalAccount) => { - if (!isEVMAccount(internalAccount)) { - return; - } const didSelectedAccountIdChanged = this.#selectedAccountId !== internalAccount.id; if (didSelectedAccountIdChanged) { diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 5393ac04660..a88a06e68ae 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -239,9 +239,6 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< } onSelectedAccountChange(async (internalAccount) => { - if (!isEVMAccount(internalAccount)) { - return; - } if (this.config.selectedAccountId !== internalAccount.id) { this.configure({ selectedAccountId: internalAccount.id }); if (this.#pollState === PollState.Active) { @@ -347,7 +344,12 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< * Updates exchange rates for all tokens. */ async updateExchangeRates() { - const { chainId, nativeCurrency } = this.config; + const { chainId, nativeCurrency, selectedAccountId } = this.config; + const selectedAccount = this.getInternalAccount(selectedAccountId); + if (!selectedAccount || !isEVMAccount(selectedAccount)) { + return; + } + await this.updateExchangeRatesByChainId({ chainId, nativeCurrency, diff --git a/packages/assets-controllers/src/TokensController.test.ts b/packages/assets-controllers/src/TokensController.test.ts index abc0c7c8e05..daa1d77e6a1 100644 --- a/packages/assets-controllers/src/TokensController.test.ts +++ b/packages/assets-controllers/src/TokensController.test.ts @@ -1888,17 +1888,6 @@ describe('TokensController', () => { ]); stub.restore(); }); - - it('should not update selectedTokenId if the selected account is not an EVM account', async () => { - const mockEvmAccount = createMockInternalAccount(); - triggerSelectedAccountChange(mockEvmAccount); - const mockNonEvmAccount = createMockInternalAccount({ - // @ts-expect-error testing a not evm type - type: 'bitcoin', - }); - triggerSelectedAccountChange(mockNonEvmAccount); - expect(tokensController.config.selectedAccountId).toBe(mockEvmAccount.id); - }); }); describe('Non evm check', () => { diff --git a/packages/assets-controllers/src/TokensController.ts b/packages/assets-controllers/src/TokensController.ts index cecfb384502..ec40ec33332 100644 --- a/packages/assets-controllers/src/TokensController.ts +++ b/packages/assets-controllers/src/TokensController.ts @@ -4,7 +4,7 @@ import type { AccountsControllerGetAccountAction, AccountsControllerSelectedAccountChangeEvent, } from '@metamask/accounts-controller'; -import { isEVMAccount } from '@metamask/accounts-controller/src/utils'; +import { isEVMAccount } from '@metamask/accounts-controller'; import type { AddApprovalRequest } from '@metamask/approval-controller'; import type { BaseConfig, @@ -265,12 +265,12 @@ export class TokensController extends BaseControllerV1< this.messagingSystem.subscribe( 'AccountsController:selectedAccountChange', (internalAccount) => { + this.configure({ selectedAccountId: internalAccount.id }); if (!isEVMAccount(internalAccount)) { return; } const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state; const { chainId } = this.config; - this.configure({ selectedAccountId: internalAccount.id }); this.update({ tokens: allTokens[chainId]?.[internalAccount.address] ?? [], ignoredTokens: @@ -290,16 +290,17 @@ export class TokensController extends BaseControllerV1< 'AccountsController:getAccount', selectedAccountId, ); + + const { chainId } = providerConfig; + this.abortController.abort(); + this.abortController = new AbortController(); + this.configure({ chainId }); if ( !selectedInternalAccount || !isEVMAccount(selectedInternalAccount) ) { return; } - const { chainId } = providerConfig; - this.abortController.abort(); - this.abortController = new AbortController(); - this.configure({ chainId }); this.update({ tokens: allTokens[chainId]?.[selectedInternalAccount.address] || [], ignoredTokens: @@ -367,6 +368,7 @@ export class TokensController extends BaseControllerV1< selectedAccountId, ); if (!internalAccount || !isEVMAccount(internalAccount)) { + releaseLock(); return []; } From f5f6c111998a79ca9787b28712212ffc2680c22f Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Fri, 26 Apr 2024 10:00:00 +0800 Subject: [PATCH 16/23] feat: add mocks and evm check to accounts controller --- packages/accounts-controller/src/index.ts | 6 +- .../accounts-controller/src/tests/mocks.ts | 59 +++++++++++++++++++ packages/accounts-controller/src/utils.ts | 13 ++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 packages/accounts-controller/src/tests/mocks.ts diff --git a/packages/accounts-controller/src/index.ts b/packages/accounts-controller/src/index.ts index 274efa5d5b1..d49ff52ab05 100644 --- a/packages/accounts-controller/src/index.ts +++ b/packages/accounts-controller/src/index.ts @@ -15,4 +15,8 @@ export type { AccountsControllerMessenger, } from './AccountsController'; export { AccountsController } from './AccountsController'; -export { keyringTypeToName, getUUIDFromAddressOfNormalAccount } from './utils'; +export { + isEVMAccount, + keyringTypeToName, + getUUIDFromAddressOfNormalAccount, +} from './utils'; diff --git a/packages/accounts-controller/src/tests/mocks.ts b/packages/accounts-controller/src/tests/mocks.ts new file mode 100644 index 00000000000..3d743803d3b --- /dev/null +++ b/packages/accounts-controller/src/tests/mocks.ts @@ -0,0 +1,59 @@ +import type { InternalAccount } from '@metamask/keyring-api'; +import { EthAccountType, 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?: EthAccountType; + name?: string; + keyringType?: KeyringTypes; + snap?: { + id: string; + enabled: boolean; + name: string; + }; + importTime?: number; + lastSelected?: number; +} = {}): InternalAccount => { + const methods = + type === EthAccountType.Eoa + ? [ + EthMethod.PersonalSign, + EthMethod.Sign, + EthMethod.SignTransaction, + EthMethod.SignTypedDataV1, + EthMethod.SignTypedDataV3, + EthMethod.SignTypedDataV4, + ] + : [ + EthMethod.PatchUserOperation, + EthMethod.PrepareUserOperation, + EthMethod.SignUserOperation, + ]; + + return { + id, + address, + options: {}, + methods, + type: EthAccountType.Eoa, + metadata: { + name, + keyring: { type: keyringType }, + importTime, + lastSelected, + snap: snap && snap, + }, + }; +}; diff --git a/packages/accounts-controller/src/utils.ts b/packages/accounts-controller/src/utils.ts index b3e7cbd639d..0668c18c387 100644 --- a/packages/accounts-controller/src/utils.ts +++ b/packages/accounts-controller/src/utils.ts @@ -1,4 +1,6 @@ import { toBuffer } from '@ethereumjs/util'; +import type { InternalAccount } from '@metamask/keyring-api'; +import { EthAccountType } from '@metamask/keyring-api'; import { isCustodyKeyring, KeyringTypes } from '@metamask/keyring-controller'; import { sha256 } from 'ethereum-cryptography/sha256'; import type { V4Options } from 'uuid'; @@ -68,3 +70,14 @@ export function getUUIDOptionsFromAddressOfNormalAccount( export function getUUIDFromAddressOfNormalAccount(address: string): string { return uuid(getUUIDOptionsFromAddressOfNormalAccount(address)); } + +/** + * Checks if the given internal account is an EVM account. + * @param internalAccount - The internal account to check. + * @returns True if the internal account is an EVM account, false otherwise. + */ +export function isEVMAccount(internalAccount: InternalAccount): boolean { + return [EthAccountType.Eoa, EthAccountType.Erc4337].includes( + internalAccount?.type as EthAccountType, + ); +} From 5967095182f64df1c039c0b295eb5680559ef1ab Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Fri, 26 Apr 2024 20:29:52 +0800 Subject: [PATCH 17/23] feat: add mocks and util function for non evm --- .../src/tests/mocks.test.ts | 52 +++++++++++++++++++ .../accounts-controller/src/tests/mocks.ts | 2 +- .../accounts-controller/src/utils.test.ts | 18 +++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 packages/accounts-controller/src/tests/mocks.test.ts create mode 100644 packages/accounts-controller/src/utils.test.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..b59b7a51a98 --- /dev/null +++ b/packages/accounts-controller/src/tests/mocks.test.ts @@ -0,0 +1,52 @@ +import { 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, + }, + }); + }); +}); diff --git a/packages/accounts-controller/src/tests/mocks.ts b/packages/accounts-controller/src/tests/mocks.ts index 3d743803d3b..05a435fe74e 100644 --- a/packages/accounts-controller/src/tests/mocks.ts +++ b/packages/accounts-controller/src/tests/mocks.ts @@ -47,7 +47,7 @@ export const createMockInternalAccount = ({ address, options: {}, methods, - type: EthAccountType.Eoa, + type, metadata: { name, keyring: { type: keyringType }, diff --git a/packages/accounts-controller/src/utils.test.ts b/packages/accounts-controller/src/utils.test.ts new file mode 100644 index 00000000000..010f8848491 --- /dev/null +++ b/packages/accounts-controller/src/utils.test.ts @@ -0,0 +1,18 @@ +import { EthAccountType } from '@metamask/keyring-api'; + +import { createMockInternalAccount } from './tests/mocks'; +import { isEVMAccount } from './utils'; + +describe('isEVMAccount', () => { + it.each([ + [EthAccountType.Eoa, true], + [EthAccountType.Erc4337, true], + ['bip122', false], + ])('%s should return %s', (accountType, expected) => { + expect( + isEVMAccount( + createMockInternalAccount({ type: accountType as EthAccountType }), + ), + ).toBe(expected); + }); +}); From 0e5df333379fa93bb3613148ab0d9afba79834ec Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Thu, 2 May 2024 12:46:40 +0800 Subject: [PATCH 18/23] fix: imports --- packages/accounts-controller/src/index.ts | 8 ++--- packages/transaction-controller/package.json | 1 + .../src/TransactionController.test.ts | 17 ++++++++++- .../src/TransactionController.ts | 23 ++++++++++----- .../helpers/IncomingTransactionHelper.test.ts | 20 +++++++++++-- .../src/helpers/IncomingTransactionHelper.ts | 29 ++++++++++++++----- yarn.lock | 1 + 7 files changed, 75 insertions(+), 24 deletions(-) diff --git a/packages/accounts-controller/src/index.ts b/packages/accounts-controller/src/index.ts index d49ff52ab05..37525d43b2b 100644 --- a/packages/accounts-controller/src/index.ts +++ b/packages/accounts-controller/src/index.ts @@ -15,8 +15,6 @@ export type { AccountsControllerMessenger, } from './AccountsController'; export { AccountsController } from './AccountsController'; -export { - isEVMAccount, - keyringTypeToName, - getUUIDFromAddressOfNormalAccount, -} from './utils'; +export { keyringTypeToName, getUUIDFromAddressOfNormalAccount } from './utils'; +export { isEVMAccount } from './utils'; +export { createMockInternalAccount } from './tests/mocks'; diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json index 87915a0e0a3..9b9ff3bf7ef 100644 --- a/packages/transaction-controller/package.json +++ b/packages/transaction-controller/package.json @@ -68,6 +68,7 @@ "@babel/runtime": "^7.23.9", "@metamask/auto-changelog": "^3.4.4", "@metamask/ethjs-provider-http": "^0.3.0", + "@metamask/keyring-api": "^6.0.0", "@types/bn.js": "^5.1.5", "@types/jest": "^27.4.1", "@types/node": "^16.18.54", diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 251acace2c7..adbc0cc587e 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, @@ -552,7 +553,21 @@ describe('TransactionController', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any getNetworkClientRegistry: () => ({} as any), getPermittedAccounts: async () => [ACCOUNT_MOCK], - getSelectedAddress: () => ACCOUNT_MOCK, + getSelectedAccount: () => { + return { + 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, + }, + }; + }, isMultichainEnabled: false, hooks: {}, onNetworkStateChange: network.subscribe, diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 7e76843f0d1..7f943b6d90a 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -22,6 +22,7 @@ import { } from '@metamask/controller-utils'; import EthQuery from '@metamask/eth-query'; import type { GasFeeState } from '@metamask/gas-fee-controller'; +import type { InternalAccount } from '@metamask/keyring-api'; import type { BlockTracker, NetworkClientId, @@ -289,7 +290,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; @@ -594,7 +595,7 @@ export class TransactionController extends BaseController< private readonly getPermittedAccounts: (origin?: string) => Promise; - private readonly getSelectedAddress: () => string; + private readonly getSelectedAccount: () => InternalAccount; private readonly getExternalPendingTransactions: ( address: string, @@ -711,7 +712,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. @@ -738,7 +739,7 @@ export class TransactionController extends BaseController< getNetworkState, getPermittedAccounts, getSavedGasFees, - getSelectedAddress, + getSelectedAccount, incomingTransactions = {}, isMultichainEnabled = false, isSimulationEnabled, @@ -778,7 +779,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; @@ -986,6 +987,14 @@ export class TransactionController extends BaseController< ): Promise { log('Adding transaction', txParams); + const selectedAccount = this.getSelectedAccount(); + if ( + selectedAccount.type !== 'eip155:eoa' && + selectedAccount.type !== 'eip155:erc4337' + ) { + throw new Error('Selected account is not an EVM account'); + } + txParams = normalizeTransactionParams(txParams); if ( networkClientId && @@ -1005,7 +1014,7 @@ export class TransactionController extends BaseController< if (origin) { await validateTransactionOrigin( await this.getPermittedAccounts(origin), - this.getSelectedAddress(), + selectedAccount.address, txParams.from, origin, ); @@ -3353,7 +3362,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..8dbd1dfa22b 100644 --- a/packages/transaction-controller/src/helpers/IncomingTransactionHelper.test.ts +++ b/packages/transaction-controller/src/helpers/IncomingTransactionHelper.test.ts @@ -1,6 +1,5 @@ /* eslint-disable jest/prefer-spy-on */ /* eslint-disable jsdoc/require-jsdoc */ - import type { BlockTracker } from '@metamask/network-controller'; import { @@ -32,7 +31,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 +559,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..c80a07e32a0 100644 --- a/packages/transaction-controller/src/helpers/IncomingTransactionHelper.ts +++ b/packages/transaction-controller/src/helpers/IncomingTransactionHelper.ts @@ -1,3 +1,4 @@ +import type { InternalAccount } from '@metamask/keyring-api'; import type { BlockTracker } from '@metamask/network-controller'; import type { Hex } from '@metamask/utils'; import { Mutex } from 'async-mutex'; @@ -35,7 +36,7 @@ export class IncomingTransactionHelper { #blockTracker: BlockTracker; - #getCurrentAccount: () => string; + #getCurrentAccount: () => InternalAccount; #getLastFetchedBlockNumbers: () => Record; @@ -72,7 +73,7 @@ export class IncomingTransactionHelper { updateTransactions, }: { blockTracker: BlockTracker; - getCurrentAccount: () => string; + getCurrentAccount: () => InternalAccount; getLastFetchedBlockNumbers: () => Record; getLocalTransactions?: () => TransactionMeta[]; getChainId: () => Hex; @@ -142,9 +143,15 @@ export class IncomingTransactionHelper { const additionalLastFetchedKeys = this.#remoteTransactionSource.getLastBlockVariations?.() ?? []; - + const currentAccount = this.#getCurrentAccount(); + if ( + currentAccount.type !== 'eip155:eoa' && + currentAccount.type !== 'eip155:erc4337' + ) { + return; + } const fromBlock = this.#getFromBlock(latestBlockNumber); - const address = this.#getCurrentAccount(); + const currentChainId = this.#getChainId(); let remoteTransactions = []; @@ -152,7 +159,7 @@ export class IncomingTransactionHelper { try { remoteTransactions = await this.#remoteTransactionSource.fetchTransactions({ - address, + address: currentAccount.address, currentChainId, fromBlock, limit: this.#transactionLimit, @@ -165,7 +172,9 @@ export class IncomingTransactionHelper { } if (!this.#updateTransactions) { remoteTransactions = remoteTransactions.filter( - (tx) => tx.txParams.to?.toLowerCase() === address.toLowerCase(), + (tx) => + tx.txParams.to?.toLowerCase() === + currentAccount.address.toLowerCase(), ); } @@ -301,9 +310,13 @@ export class IncomingTransactionHelper { #getBlockNumberKey(additionalKeys: string[]): string { const currentChainId = this.#getChainId(); - const currentAccount = this.#getCurrentAccount()?.toLowerCase(); + const currentAccount = this.#getCurrentAccount(); - return [currentChainId, currentAccount, ...additionalKeys].join('#'); + return [ + currentChainId, + currentAccount.address.toLowerCase(), + ...additionalKeys, + ].join('#'); } #canStart(): boolean { diff --git a/yarn.lock b/yarn.lock index 10d2c11431a..929ded218e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3021,6 +3021,7 @@ __metadata: "@metamask/eth-query": ^4.0.0 "@metamask/ethjs-provider-http": ^0.3.0 "@metamask/gas-fee-controller": ^15.1.0 + "@metamask/keyring-api": ^6.0.0 "@metamask/metamask-eth-abis": ^3.1.1 "@metamask/network-controller": ^18.1.0 "@metamask/rpc-errors": ^6.2.1 From a7089c535dcc83533baf8b748e4ab6e87a3a4a0c Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Thu, 2 May 2024 15:44:24 +0800 Subject: [PATCH 19/23] fix: peer dep version --- packages/transaction-controller/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json index 1301022d46e..855de3dd316 100644 --- a/packages/transaction-controller/package.json +++ b/packages/transaction-controller/package.json @@ -85,7 +85,7 @@ }, "peerDependencies": { "@babel/runtime": "^7.23.9", - "@metamask/accounts-controller": "^13.0.0", + "@metamask/accounts-controller": "^14.0.0", "@metamask/approval-controller": "^6.0.0", "@metamask/gas-fee-controller": "^15.0.0", "@metamask/network-controller": "^18.0.0" From 8cee6a625d05d62c1b7638cef1f795e378188600 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Mon, 6 May 2024 21:00:45 +0800 Subject: [PATCH 20/23] fix: update to keyring-api 6.1 --- packages/accounts-controller/package.json | 4 +- packages/accounts-controller/src/index.ts | 1 - .../accounts-controller/src/tests/mocks.ts | 58 ++++-- packages/accounts-controller/src/utils.ts | 11 -- packages/assets-controllers/package.json | 2 +- .../src/AccountTrackerController.ts | 12 +- .../assets-controllers/src/NftController.ts | 54 ++++-- .../src/NftDetectionController.ts | 11 +- .../src/TokenBalancesController.ts | 11 +- .../src/TokenDetectionController.ts | 13 +- .../src/TokenRatesController.ts | 17 +- .../src/TokensController.ts | 32 +++- yarn.lock | 165 +++++++++++++++++- 13 files changed, 310 insertions(+), 81 deletions(-) diff --git a/packages/accounts-controller/package.json b/packages/accounts-controller/package.json index 936ef181cb3..2c5e33f9656 100644 --- a/packages/accounts-controller/package.json +++ b/packages/accounts-controller/package.json @@ -43,8 +43,8 @@ "dependencies": { "@ethereumjs/util": "^8.1.0", "@metamask/base-controller": "^5.0.2", - "@metamask/eth-snap-keyring": "^4.0.0", - "@metamask/keyring-api": "^6.0.0", + "@metamask/eth-snap-keyring": "^4.1.0", + "@metamask/keyring-api": "^6.1.0", "@metamask/snaps-sdk": "^4.0.1", "@metamask/snaps-utils": "^7.1.0", "@metamask/utils": "^8.3.0", diff --git a/packages/accounts-controller/src/index.ts b/packages/accounts-controller/src/index.ts index b7acde597c3..ef584dae53d 100644 --- a/packages/accounts-controller/src/index.ts +++ b/packages/accounts-controller/src/index.ts @@ -17,5 +17,4 @@ export type { } from './AccountsController'; export { AccountsController } from './AccountsController'; export { keyringTypeToName, getUUIDFromAddressOfNormalAccount } from './utils'; -export { isEVMAccount } from './utils'; export { createMockInternalAccount } from './tests/mocks'; diff --git a/packages/accounts-controller/src/tests/mocks.ts b/packages/accounts-controller/src/tests/mocks.ts index 05a435fe74e..8a9590a776e 100644 --- a/packages/accounts-controller/src/tests/mocks.ts +++ b/packages/accounts-controller/src/tests/mocks.ts @@ -1,5 +1,14 @@ -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 { v4 } from 'uuid'; @@ -15,7 +24,7 @@ export const createMockInternalAccount = ({ }: { id?: string; address?: string; - type?: EthAccountType; + type?: InternalAccountType; name?: string; keyringType?: KeyringTypes; snap?: { @@ -26,21 +35,32 @@ export const createMockInternalAccount = ({ importTime?: number; lastSelected?: number; } = {}): InternalAccount => { - const methods = - type === EthAccountType.Eoa - ? [ - EthMethod.PersonalSign, - EthMethod.Sign, - EthMethod.SignTransaction, - EthMethod.SignTypedDataV1, - EthMethod.SignTypedDataV3, - EthMethod.SignTypedDataV4, - ] - : [ - EthMethod.PatchUserOperation, - EthMethod.PrepareUserOperation, - EthMethod.SignUserOperation, - ]; + 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: + methods = [] as EthMethod[]; + } return { id, @@ -55,5 +75,5 @@ export const createMockInternalAccount = ({ lastSelected, snap: snap && snap, }, - }; + } as InternalAccount; }; diff --git a/packages/accounts-controller/src/utils.ts b/packages/accounts-controller/src/utils.ts index 0668c18c387..22dcf32c440 100644 --- a/packages/accounts-controller/src/utils.ts +++ b/packages/accounts-controller/src/utils.ts @@ -70,14 +70,3 @@ export function getUUIDOptionsFromAddressOfNormalAccount( export function getUUIDFromAddressOfNormalAccount(address: string): string { return uuid(getUUIDOptionsFromAddressOfNormalAccount(address)); } - -/** - * Checks if the given internal account is an EVM account. - * @param internalAccount - The internal account to check. - * @returns True if the internal account is an EVM account, false otherwise. - */ -export function isEVMAccount(internalAccount: InternalAccount): boolean { - return [EthAccountType.Eoa, EthAccountType.Erc4337].includes( - internalAccount?.type as EthAccountType, - ); -} diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index 4c739c53b82..bc6d36f84e5 100644 --- a/packages/assets-controllers/package.json +++ b/packages/assets-controllers/package.json @@ -53,6 +53,7 @@ "@metamask/contract-metadata": "^2.4.0", "@metamask/controller-utils": "^9.1.0", "@metamask/eth-query": "^4.0.0", + "@metamask/keyring-api": "^6.1.0", "@metamask/keyring-controller": "^16.0.0", "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/network-controller": "^18.1.0", @@ -73,7 +74,6 @@ "devDependencies": { "@metamask/auto-changelog": "^3.4.4", "@metamask/ethjs-provider-http": "^0.3.0", - "@metamask/keyring-api": "^6.0.0", "@types/jest": "^27.4.1", "@types/lodash": "^4.14.191", "@types/node": "^16.18.54", diff --git a/packages/assets-controllers/src/AccountTrackerController.ts b/packages/assets-controllers/src/AccountTrackerController.ts index 4ddcb6dcae0..a0dc423095f 100644 --- a/packages/assets-controllers/src/AccountTrackerController.ts +++ b/packages/assets-controllers/src/AccountTrackerController.ts @@ -1,9 +1,12 @@ -import { isEVMAccount } 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 { + InternalAccountType, + isEvmAccountType, + type InternalAccount, +} from '@metamask/keyring-api'; import type { NetworkClientId, NetworkController, @@ -260,7 +263,10 @@ export class AccountTrackerController extends StaticIntervalPollingControllerV1< */ refresh = async (networkClientId?: NetworkClientId) => { const selectedAccount = this.getSelectedAccount(); - if (!selectedAccount || !isEVMAccount(selectedAccount)) { + if ( + !selectedAccount || + !isEvmAccountType(selectedAccount.type as InternalAccountType) + ) { return; } diff --git a/packages/assets-controllers/src/NftController.ts b/packages/assets-controllers/src/NftController.ts index 350afa2a89e..8bf8c6c8a50 100644 --- a/packages/assets-controllers/src/NftController.ts +++ b/packages/assets-controllers/src/NftController.ts @@ -3,7 +3,6 @@ import { type AccountsControllerSelectedAccountChangeEvent, type AccountsControllerGetAccountAction, type AccountsControllerGetSelectedAccountAction, - isEVMAccount, } from '@metamask/accounts-controller'; import type { AddApprovalRequest } from '@metamask/approval-controller'; import type { @@ -24,7 +23,11 @@ import { ApprovalType, NFT_API_BASE_URL, } from '@metamask/controller-utils'; -import type { InternalAccount } from '@metamask/keyring-api'; +import { + type InternalAccountType, + type InternalAccount, + isEvmAccountType, +} from '@metamask/keyring-api'; import type { NetworkClientId, NetworkController, @@ -1071,7 +1074,7 @@ export class NftController extends BaseControllerV1 { selectedAccountId: newSelectedAccount.id, }); - if (isEVMAccount(newSelectedAccount)) { + if (isEvmAccountType(newSelectedAccount.type as InternalAccountType)) { const needsUpdateNftMetadata = (isIpfsGatewayEnabled && ipfsGateway !== '') || openSeaEnabled; @@ -1198,7 +1201,10 @@ export class NftController extends BaseControllerV1 { 'AccountsController:getAccount', selectedAccountId, ); - if (!selectedAccount || !isEVMAccount(selectedAccount)) { + if ( + !selectedAccount || + !isEvmAccountType(selectedAccount.type as InternalAccountType) + ) { return; } @@ -1333,7 +1339,10 @@ export class NftController extends BaseControllerV1 { 'AccountsController:getAccount', selectedAccountId, ); - if (!selectedAccount || !isEVMAccount(selectedAccount)) { + if ( + !selectedAccount || + !isEvmAccountType(selectedAccount.type as InternalAccountType) + ) { return; } userAddress = userAddress ?? selectedAccount.address; @@ -1384,7 +1393,10 @@ export class NftController extends BaseControllerV1 { 'AccountsController:getAccount', selectedAccountId, ); - if (!selectedAccount || !isEVMAccount(selectedAccount)) { + if ( + !selectedAccount || + !isEvmAccountType(selectedAccount.type as InternalAccountType) + ) { return; } userAddress = userAddress ?? selectedAccount.address; @@ -1446,7 +1458,10 @@ export class NftController extends BaseControllerV1 { 'AccountsController:getAccount', this.config.selectedAccountId, ); - if (!selectedAccount || !isEVMAccount(selectedAccount)) { + if ( + !selectedAccount || + !isEvmAccountType(selectedAccount.type as InternalAccountType) + ) { return; } @@ -1511,7 +1526,10 @@ export class NftController extends BaseControllerV1 { 'AccountsController:getAccount', selectedAccountId, ); - if (!selectedAccount || !isEVMAccount(selectedAccount)) { + if ( + !selectedAccount || + !isEvmAccountType(selectedAccount.type as InternalAccountType) + ) { return; } @@ -1552,7 +1570,10 @@ export class NftController extends BaseControllerV1 { 'AccountsController:getAccount', selectedAccountId, ); - if (!selectedAccount || !isEVMAccount(selectedAccount)) { + if ( + !selectedAccount || + !isEvmAccountType(selectedAccount.type as InternalAccountType) + ) { return; } @@ -1605,7 +1626,10 @@ export class NftController extends BaseControllerV1 { 'AccountsController:getAccount', selectedAccountId, ); - if (!selectedAccount || !isEVMAccount(selectedAccount)) { + if ( + !selectedAccount || + !isEvmAccountType(selectedAccount.type as InternalAccountType) + ) { throw new Error('Non EVM Account selected'); } @@ -1664,7 +1688,10 @@ export class NftController extends BaseControllerV1 { 'AccountsController:getAccount', selectedAccountId, ); - if (!selectedAccount || !isEVMAccount(selectedAccount)) { + if ( + !selectedAccount || + !isEvmAccountType(selectedAccount.type as InternalAccountType) + ) { return; } @@ -1717,7 +1744,10 @@ export class NftController extends BaseControllerV1 { 'AccountsController:getAccount', selectedAccountId, ); - if (!selectedAccount || !isEVMAccount(selectedAccount)) { + if ( + !selectedAccount || + !isEvmAccountType(selectedAccount.type as InternalAccountType) + ) { return; } diff --git a/packages/assets-controllers/src/NftDetectionController.ts b/packages/assets-controllers/src/NftDetectionController.ts index 11ef451a0dc..982abfa28e8 100644 --- a/packages/assets-controllers/src/NftDetectionController.ts +++ b/packages/assets-controllers/src/NftDetectionController.ts @@ -1,4 +1,3 @@ -import { isEVMAccount } from '@metamask/accounts-controller'; import type { BaseConfig, BaseState } from '@metamask/base-controller'; import { fetchWithErrorHandling, @@ -6,7 +5,11 @@ import { ChainId, NFT_API_BASE_URL, } from '@metamask/controller-utils'; -import type { InternalAccount } from '@metamask/keyring-api'; +import type { + InternalAccount, + InternalAccountType, +} from '@metamask/keyring-api'; +import { isEvmAccountType } from '@metamask/keyring-api'; import type { NetworkClientId, NetworkController, @@ -613,7 +616,9 @@ export class NftDetectionController extends StaticIntervalPollingControllerV1< } = {}) { const { selectedAccountId } = this.config; const selectedInternalAccount = this.getInternalAccount(selectedAccountId); - if (!isEVMAccount(selectedInternalAccount)) { + if ( + !isEvmAccountType(selectedInternalAccount.type as InternalAccountType) + ) { return; } diff --git a/packages/assets-controllers/src/TokenBalancesController.ts b/packages/assets-controllers/src/TokenBalancesController.ts index d024c037678..f65ddd24a9c 100644 --- a/packages/assets-controllers/src/TokenBalancesController.ts +++ b/packages/assets-controllers/src/TokenBalancesController.ts @@ -1,7 +1,4 @@ -import { - isEVMAccount, - type AccountsControllerGetSelectedAccountAction, -} from '@metamask/accounts-controller'; +import { type AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller'; import { type RestrictedControllerMessenger, type ControllerGetStateAction, @@ -9,6 +6,8 @@ import { BaseController, } from '@metamask/base-controller'; import { safelyExecute, toHex } from '@metamask/controller-utils'; +import type { InternalAccountType } from '@metamask/keyring-api'; +import { isEvmAccountType } from '@metamask/keyring-api'; import type { AssetsContractController } from './AssetsContractController'; import type { Token } from './TokenRatesController'; @@ -201,7 +200,9 @@ export class TokenBalancesController extends BaseController< const selectedInternalAccount = this.messagingSystem.call( 'AccountsController:getSelectedAccount', ); - if (!isEVMAccount(selectedInternalAccount)) { + if ( + !isEvmAccountType(selectedInternalAccount.type as InternalAccountType) + ) { return; } diff --git a/packages/assets-controllers/src/TokenDetectionController.ts b/packages/assets-controllers/src/TokenDetectionController.ts index 6f909db58a4..617024a0f3d 100644 --- a/packages/assets-controllers/src/TokenDetectionController.ts +++ b/packages/assets-controllers/src/TokenDetectionController.ts @@ -2,7 +2,6 @@ import { type AccountsControllerGetSelectedAccountAction, type AccountsControllerGetAccountAction, type AccountsControllerSelectedAccountChangeEvent, - isEVMAccount, } from '@metamask/accounts-controller'; import type { RestrictedControllerMessenger, @@ -11,6 +10,8 @@ import type { } from '@metamask/base-controller'; import contractMap from '@metamask/contract-metadata'; import { ChainId, safelyExecute } from '@metamask/controller-utils'; +import type { InternalAccountType } from '@metamask/keyring-api'; +import { isEvmAccountType } from '@metamask/keyring-api'; import type { KeyringControllerGetStateAction, KeyringControllerLockEvent, @@ -457,7 +458,10 @@ export class TokenDetectionController extends StaticIntervalPollingController< 'AccountsController:getAccount', selectedAccountId ?? this.#selectedAccountId, ); - if (!internalAccount || !isEVMAccount(internalAccount)) { + if ( + !internalAccount || + !isEvmAccountType(internalAccount.type as InternalAccountType) + ) { return; } @@ -491,7 +495,10 @@ export class TokenDetectionController extends StaticIntervalPollingController< 'AccountsController:getAccount', this.#selectedAccountId, ); - if (!selectedInternalAccount || !isEVMAccount(selectedInternalAccount)) { + if ( + !selectedInternalAccount || + !isEvmAccountType(selectedInternalAccount.type as InternalAccountType) + ) { return; } diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index a88a06e68ae..1a913685fd7 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -1,4 +1,3 @@ -import { isEVMAccount } from '@metamask/accounts-controller'; import type { BaseConfig, BaseState } from '@metamask/base-controller'; import { safelyExecute, @@ -6,7 +5,11 @@ import { FALL_BACK_VS_CURRENCY, toHex, } from '@metamask/controller-utils'; -import type { InternalAccount } from '@metamask/keyring-api'; +import { + isEvmAccountType, + type InternalAccount, + type InternalAccountType, +} from '@metamask/keyring-api'; import type { NetworkClientId, NetworkController, @@ -285,7 +288,10 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< #getTokenAddresses(chainId: Hex): Hex[] { const { allTokens, allDetectedTokens, selectedAccountId } = this.config; const internalAccount = this.getInternalAccount(selectedAccountId); - if (!internalAccount || !isEVMAccount(internalAccount)) { + if ( + !internalAccount || + !isEvmAccountType(internalAccount.type as InternalAccountType) + ) { return []; } const tokens = allTokens[chainId]?.[internalAccount.address] || []; @@ -346,7 +352,10 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< async updateExchangeRates() { const { chainId, nativeCurrency, selectedAccountId } = this.config; const selectedAccount = this.getInternalAccount(selectedAccountId); - if (!selectedAccount || !isEVMAccount(selectedAccount)) { + if ( + !selectedAccount || + !isEvmAccountType(selectedAccount.type as InternalAccountType) + ) { return; } diff --git a/packages/assets-controllers/src/TokensController.ts b/packages/assets-controllers/src/TokensController.ts index ec40ec33332..528006decea 100644 --- a/packages/assets-controllers/src/TokensController.ts +++ b/packages/assets-controllers/src/TokensController.ts @@ -4,7 +4,6 @@ import type { AccountsControllerGetAccountAction, AccountsControllerSelectedAccountChangeEvent, } from '@metamask/accounts-controller'; -import { isEVMAccount } from '@metamask/accounts-controller'; import type { AddApprovalRequest } from '@metamask/approval-controller'; import type { BaseConfig, @@ -24,6 +23,8 @@ import { isValidHexAddress, safelyExecute, } from '@metamask/controller-utils'; +import type { InternalAccountType } from '@metamask/keyring-api'; +import { isEvmAccountType } from '@metamask/keyring-api'; import { abiERC721 } from '@metamask/metamask-eth-abis'; import type { NetworkClientId, @@ -266,7 +267,7 @@ export class TokensController extends BaseControllerV1< 'AccountsController:selectedAccountChange', (internalAccount) => { this.configure({ selectedAccountId: internalAccount.id }); - if (!isEVMAccount(internalAccount)) { + if (!isEvmAccountType(internalAccount.type as InternalAccountType)) { return; } const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state; @@ -297,7 +298,7 @@ export class TokensController extends BaseControllerV1< this.configure({ chainId }); if ( !selectedInternalAccount || - !isEVMAccount(selectedInternalAccount) + !isEvmAccountType(selectedInternalAccount.type as InternalAccountType) ) { return; } @@ -367,7 +368,10 @@ export class TokensController extends BaseControllerV1< 'AccountsController:getAccount', selectedAccountId, ); - if (!internalAccount || !isEVMAccount(internalAccount)) { + if ( + !internalAccount || + !isEvmAccountType(internalAccount.type as InternalAccountType) + ) { releaseLock(); return []; } @@ -585,7 +589,10 @@ export class TokensController extends BaseControllerV1< 'AccountsController:getAccount', this.config.selectedAccountId, ); - if (!internalAccount || !isEVMAccount(internalAccount)) { + if ( + !internalAccount || + !isEvmAccountType(internalAccount.type as InternalAccountType) + ) { releaseLock(); return; } @@ -668,7 +675,10 @@ export class TokensController extends BaseControllerV1< currentAccountId, ); - if (!currentInternalAccount || !isEVMAccount(currentInternalAccount)) { + if ( + !currentInternalAccount || + !isEvmAccountType(currentInternalAccount.type as InternalAccountType) + ) { releaseLock(); return; } @@ -834,7 +844,10 @@ export class TokensController extends BaseControllerV1< 'AccountsController:getAccount', this.config.selectedAccountId, ); - if (!selectedAccount || !isEVMAccount(selectedAccount)) { + if ( + !selectedAccount || + !isEvmAccountType(selectedAccount.type as InternalAccountType) + ) { throw new Error(`Account is not an EVM account`); } @@ -977,7 +990,10 @@ export class TokensController extends BaseControllerV1< 'AccountsController:getAccount', selectedAccountId, ); - if (!selectedInternalAccount || !isEVMAccount(selectedInternalAccount)) { + if ( + !selectedInternalAccount || + !isEvmAccountType(selectedInternalAccount.type as InternalAccountType) + ) { return { newAllTokens: allTokens, newAllIgnoredTokens: allIgnoredTokens, diff --git a/yarn.lock b/yarn.lock index 30f52313b69..87b2c589e6f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1645,8 +1645,8 @@ __metadata: "@ethereumjs/util": ^8.1.0 "@metamask/auto-changelog": ^3.4.4 "@metamask/base-controller": ^5.0.2 - "@metamask/eth-snap-keyring": ^4.0.0 - "@metamask/keyring-api": ^6.0.0 + "@metamask/eth-snap-keyring": ^4.1.0 + "@metamask/keyring-api": ^6.1.0 "@metamask/keyring-controller": ^16.0.0 "@metamask/snaps-controllers": ^7.0.1 "@metamask/snaps-sdk": ^4.0.1 @@ -1752,7 +1752,7 @@ __metadata: "@metamask/controller-utils": ^9.1.0 "@metamask/eth-query": ^4.0.0 "@metamask/ethjs-provider-http": ^0.3.0 - "@metamask/keyring-api": ^6.0.0 + "@metamask/keyring-api": ^6.1.0 "@metamask/keyring-controller": ^16.0.0 "@metamask/metamask-eth-abis": ^3.1.1 "@metamask/network-controller": ^18.1.0 @@ -2169,21 +2169,21 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-snap-keyring@npm:^4.0.0": - version: 4.0.0 - resolution: "@metamask/eth-snap-keyring@npm:4.0.0" +"@metamask/eth-snap-keyring@npm:^4.1.0": + version: 4.1.0 + resolution: "@metamask/eth-snap-keyring@npm:4.1.0" dependencies: "@ethereumjs/tx": ^4.2.0 "@metamask/eth-sig-util": ^7.0.1 - "@metamask/keyring-api": ^6.0.0 - "@metamask/snaps-controllers": ^7.0.1 + "@metamask/keyring-api": ^6.1.0 + "@metamask/snaps-controllers": ^8.0.0 "@metamask/snaps-sdk": ^4.0.1 "@metamask/snaps-utils": ^7.0.3 "@metamask/utils": ^8.4.0 "@types/uuid": ^9.0.1 superstruct: ^1.0.3 uuid: ^9.0.0 - checksum: 9e38fe022b7d3c1a0178dc549ed24c22039b5d8da4f383a2eb7003de8778e980a28132b2c06f6fc759a83889df7971d64d5f3080a65783314ec5b279b7259e6f + checksum: ab727b7bb43a9ab2097a9993ed5f87f45f4cd414b4f33b19f7e314ceadf93fc000314982fd97d893888c8227d5179bb23d6bb20bfd1df7bef0e94dc5a7b0ad19 languageName: node linkType: hard @@ -2411,6 +2411,22 @@ __metadata: languageName: node linkType: hard +"@metamask/keyring-api@npm:^6.1.0": + version: 6.1.0 + resolution: "@metamask/keyring-api@npm:6.1.0" + dependencies: + "@metamask/snaps-sdk": ^4.0.0 + "@metamask/utils": ^8.3.0 + "@types/uuid": ^9.0.1 + bech32: ^2.0.0 + superstruct: ^1.0.3 + uuid: ^9.0.0 + peerDependencies: + "@metamask/providers": ">=15 <17" + checksum: 493aff0569b2f62a9cbbda82e5b9df709562f5f11d32f5d97224a7d29be1bbd7139e23f1384d9d888e2e90d3858e64ff499c03780b3664d31724a054722b4e0a + 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" @@ -2743,6 +2759,26 @@ __metadata: languageName: node linkType: hard +"@metamask/providers@npm:^16.1.0": + version: 16.1.0 + resolution: "@metamask/providers@npm:16.1.0" + dependencies: + "@metamask/json-rpc-engine": ^8.0.1 + "@metamask/json-rpc-middleware-stream": ^7.0.1 + "@metamask/object-multiplex": ^2.0.0 + "@metamask/rpc-errors": ^6.2.1 + "@metamask/safe-event-emitter": ^3.1.1 + "@metamask/utils": ^8.3.0 + detect-browser: ^5.2.0 + extension-port-stream: ^3.0.0 + fast-deep-equal: ^3.1.3 + is-stream: ^2.0.0 + readable-stream: ^3.6.2 + webextension-polyfill: ^0.10.0 + checksum: 85e40140f342a38112c3d7cee436751a2be4c575cc4f815ab48a73b549abc2d756bf4a10e4b983e91dbd38076601f992531edb6d8d674aebceae32ef7e299275 + languageName: node + linkType: hard + "@metamask/queued-request-controller@workspace:packages/queued-request-controller": version: 0.0.0-use.local resolution: "@metamask/queued-request-controller@workspace:packages/queued-request-controller" @@ -2815,6 +2851,13 @@ __metadata: languageName: node linkType: hard +"@metamask/safe-event-emitter@npm:^3.1.1": + version: 3.1.1 + resolution: "@metamask/safe-event-emitter@npm:3.1.1" + checksum: e24db4d7c20764bfc5b025065f92518c805f0ffb1da4820078b8cff7dcae964c0f354cf053fcb7ac659de015d5ffdf21aae5e8d44e191ee8faa9066855f22653 + languageName: node + linkType: hard + "@metamask/scure-bip39@npm:^2.1.0, @metamask/scure-bip39@npm:^2.1.1": version: 2.1.1 resolution: "@metamask/scure-bip39@npm:2.1.1" @@ -2924,6 +2967,43 @@ __metadata: languageName: node linkType: hard +"@metamask/snaps-controllers@npm:^8.0.0": + version: 8.1.0 + resolution: "@metamask/snaps-controllers@npm:8.1.0" + dependencies: + "@metamask/approval-controller": ^6.0.2 + "@metamask/base-controller": ^5.0.2 + "@metamask/json-rpc-engine": ^8.0.1 + "@metamask/json-rpc-middleware-stream": ^7.0.1 + "@metamask/object-multiplex": ^2.0.0 + "@metamask/permission-controller": ^9.0.2 + "@metamask/phishing-controller": ^9.0.1 + "@metamask/post-message-stream": ^8.0.0 + "@metamask/rpc-errors": ^6.2.1 + "@metamask/snaps-registry": ^3.1.0 + "@metamask/snaps-rpc-methods": ^8.1.0 + "@metamask/snaps-sdk": ^4.1.0 + "@metamask/snaps-utils": ^7.3.0 + "@metamask/utils": ^8.3.0 + "@xstate/fsm": ^2.0.0 + browserify-zlib: ^0.2.0 + concat-stream: ^2.0.0 + fast-deep-equal: ^3.1.3 + get-npm-tarball-url: ^2.0.3 + immer: ^9.0.6 + nanoid: ^3.1.31 + readable-stream: ^3.6.2 + readable-web-to-node-stream: ^3.0.2 + tar-stream: ^3.1.7 + peerDependencies: + "@metamask/snaps-execution-environments": ^6.1.0 + peerDependenciesMeta: + "@metamask/snaps-execution-environments": + optional: true + checksum: 72cfe842bbfbca4231b8357acb8a637d201aed19f9be996be4497370533bd0b6a18f0836329bee07b777769f7db7a9b0779c4332e9bf415b247dc923507eb865 + languageName: node + linkType: hard + "@metamask/snaps-registry@npm:^3.1.0": version: 3.1.0 resolution: "@metamask/snaps-registry@npm:3.1.0" @@ -2952,6 +3032,22 @@ __metadata: languageName: node linkType: hard +"@metamask/snaps-rpc-methods@npm:^8.1.0": + version: 8.1.0 + resolution: "@metamask/snaps-rpc-methods@npm:8.1.0" + dependencies: + "@metamask/key-tree": ^9.0.0 + "@metamask/permission-controller": ^9.0.2 + "@metamask/rpc-errors": ^6.2.1 + "@metamask/snaps-sdk": ^4.1.0 + "@metamask/snaps-utils": ^7.3.0 + "@metamask/utils": ^8.3.0 + "@noble/hashes": ^1.3.1 + superstruct: ^1.0.3 + checksum: 343da447508c1d5a0757640bb6aa3a7b3979294574ce0600f5a011c2918eb1842ae20c93c0967cf49da622dae99af73f6b243fdfbf65046c5f638dc52d04600d + languageName: node + linkType: hard + "@metamask/snaps-sdk@npm:^4.0.0, @metamask/snaps-sdk@npm:^4.0.1": version: 4.0.1 resolution: "@metamask/snaps-sdk@npm:4.0.1" @@ -2966,6 +3062,20 @@ __metadata: languageName: node linkType: hard +"@metamask/snaps-sdk@npm:^4.1.0": + version: 4.1.0 + resolution: "@metamask/snaps-sdk@npm:4.1.0" + dependencies: + "@metamask/key-tree": ^9.0.0 + "@metamask/providers": ^16.1.0 + "@metamask/rpc-errors": ^6.2.1 + "@metamask/utils": ^8.3.0 + fast-xml-parser: ^4.3.4 + superstruct: ^1.0.3 + checksum: b8056b102c996bbe2e6229af6a6118f21c353a513d15a927c9dd67b48c4ca5b5bcf65d5114d8631e9e2662391a574c80d74845ffa25182dbc8fcb2ff09bf7325 + languageName: node + linkType: hard + "@metamask/snaps-utils@npm:^7.0.3, @metamask/snaps-utils@npm:^7.1.0": version: 7.1.0 resolution: "@metamask/snaps-utils@npm:7.1.0" @@ -2996,6 +3106,36 @@ __metadata: languageName: node linkType: hard +"@metamask/snaps-utils@npm:^7.3.0": + version: 7.3.0 + resolution: "@metamask/snaps-utils@npm:7.3.0" + dependencies: + "@babel/core": ^7.23.2 + "@babel/types": ^7.23.0 + "@metamask/base-controller": ^5.0.2 + "@metamask/key-tree": ^9.0.0 + "@metamask/permission-controller": ^9.0.2 + "@metamask/rpc-errors": ^6.2.1 + "@metamask/slip44": ^3.1.0 + "@metamask/snaps-registry": ^3.1.0 + "@metamask/snaps-sdk": ^4.1.0 + "@metamask/utils": ^8.3.0 + "@noble/hashes": ^1.3.1 + "@scure/base": ^1.1.1 + chalk: ^4.1.2 + cron-parser: ^4.5.0 + fast-deep-equal: ^3.1.3 + fast-json-stable-stringify: ^2.1.0 + marked: ^12.0.1 + rfdc: ^1.3.0 + semver: ^7.5.4 + ses: ^1.1.0 + superstruct: ^1.0.3 + validate-npm-package-name: ^5.0.0 + checksum: e58bb4cbcc6c8f17d2bf18995eb75e13af296f3f6c7d97a5681d3d76097450ae49bba9c5ac134e41ae6e01bfb005d0bc4bb1256d421e46b6c56490fa1b9f25ab + languageName: node + linkType: hard + "@metamask/swappable-obj-proxy@npm:^2.2.0": version: 2.2.0 resolution: "@metamask/swappable-obj-proxy@npm:2.2.0" @@ -4562,6 +4702,13 @@ __metadata: languageName: node linkType: hard +"bech32@npm:^2.0.0": + version: 2.0.0 + resolution: "bech32@npm:2.0.0" + checksum: fa15acb270b59aa496734a01f9155677b478987b773bf701f465858bf1606c6a970085babd43d71ce61895f1baa594cb41a2cd1394bd2c6698f03cc2d811300e + languageName: node + linkType: hard + "big-integer@npm:^1.6.44": version: 1.6.51 resolution: "big-integer@npm:1.6.51" From eb5191e9515295a55ca0f54baff35a30bd117618 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Tue, 14 May 2024 17:10:14 +0800 Subject: [PATCH 21/23] chore(deps): bump keyring api version to 6.1.0 --- packages/keyring-controller/package.json | 2 +- packages/transaction-controller/package.json | 2 +- yarn.lock | 126 +------------------ 3 files changed, 8 insertions(+), 122 deletions(-) diff --git a/packages/keyring-controller/package.json b/packages/keyring-controller/package.json index e1818d56b44..d87eb9cf030 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.0.0", + "@metamask/keyring-api": "^6.1.0", "@metamask/message-manager": "^8.0.2", "@metamask/utils": "^8.3.0", "async-mutex": "^0.2.6", diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json index 855de3dd316..f5745a46398 100644 --- a/packages/transaction-controller/package.json +++ b/packages/transaction-controller/package.json @@ -69,7 +69,7 @@ "@babel/runtime": "^7.23.9", "@metamask/auto-changelog": "^3.4.4", "@metamask/ethjs-provider-http": "^0.3.0", - "@metamask/keyring-api": "^6.0.0", + "@metamask/keyring-api": "^6.1.0", "@types/bn.js": "^5.1.5", "@types/jest": "^27.4.1", "@types/node": "^16.18.54", diff --git a/yarn.lock b/yarn.lock index 87b2c589e6f..8a63b8627c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2370,18 +2370,6 @@ __metadata: languageName: unknown linkType: soft -"@metamask/json-rpc-middleware-stream@npm:^6.0.2": - version: 6.0.2 - resolution: "@metamask/json-rpc-middleware-stream@npm:6.0.2" - dependencies: - "@metamask/json-rpc-engine": ^7.3.2 - "@metamask/safe-event-emitter": ^3.0.0 - "@metamask/utils": ^8.3.0 - readable-stream: ^3.6.2 - checksum: e831041b03e9f48f584f4425188f72b58974f95b60429c9fe8b5561da69c6bbfad2f2b2199acdff06ee718967214b65c05604d4f85f3287186619683487f1060 - languageName: node - linkType: hard - "@metamask/key-tree@npm:^9.0.0": version: 9.0.0 resolution: "@metamask/key-tree@npm:9.0.0" @@ -2396,21 +2384,6 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-api@npm:^6.0.0": - version: 6.0.0 - resolution: "@metamask/keyring-api@npm:6.0.0" - dependencies: - "@metamask/snaps-sdk": ^4.0.0 - "@metamask/utils": ^8.3.0 - "@types/uuid": ^9.0.1 - superstruct: ^1.0.3 - uuid: ^9.0.0 - peerDependencies: - "@metamask/providers": ">=15 <17" - checksum: d3d6d5d27945783ef74e8e1b9626d122a07df58a2a62e12c68ff0bda2894c6c0a16d6add40908159fb92dee0b98425c63fc494c9b86ae0d0a0be7dc74389997a - languageName: node - linkType: hard - "@metamask/keyring-api@npm:^6.1.0": version: 6.1.0 resolution: "@metamask/keyring-api@npm:6.1.0" @@ -2443,7 +2416,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.0.0 + "@metamask/keyring-api": ^6.1.0 "@metamask/message-manager": ^8.0.2 "@metamask/scure-bip39": ^2.1.1 "@metamask/utils": ^8.3.0 @@ -2739,26 +2712,6 @@ __metadata: languageName: unknown linkType: soft -"@metamask/providers@npm:^16.0.0": - version: 16.0.0 - resolution: "@metamask/providers@npm:16.0.0" - dependencies: - "@metamask/json-rpc-engine": ^7.3.2 - "@metamask/json-rpc-middleware-stream": ^6.0.2 - "@metamask/object-multiplex": ^2.0.0 - "@metamask/rpc-errors": ^6.2.1 - "@metamask/safe-event-emitter": ^3.0.0 - "@metamask/utils": ^8.3.0 - detect-browser: ^5.2.0 - extension-port-stream: ^3.0.0 - fast-deep-equal: ^3.1.3 - is-stream: ^2.0.0 - readable-stream: ^3.6.2 - webextension-polyfill: ^0.10.0 - checksum: cdc06796111edbf01e9aa8498170f7ffa3c68a4c0f66a629e3b0f7d37ee60eb32d83ee12f285c3d974d971c6af16a3fba531fb5733f5fa9412a18e1d3f648539 - languageName: node - linkType: hard - "@metamask/providers@npm:^16.1.0": version: 16.1.0 resolution: "@metamask/providers@npm:16.1.0" @@ -2844,14 +2797,7 @@ __metadata: languageName: node linkType: hard -"@metamask/safe-event-emitter@npm:^3.0.0": - version: 3.0.0 - resolution: "@metamask/safe-event-emitter@npm:3.0.0" - checksum: 8dc58a76f9f75bf2405931465fc311c68043d851e6b8ebe9f82ae339073a08a83430dba9338f8e3adc4bfc8067607125074bcafa32baee3a5157f42343dc89e5 - languageName: node - linkType: hard - -"@metamask/safe-event-emitter@npm:^3.1.1": +"@metamask/safe-event-emitter@npm:^3.0.0, @metamask/safe-event-emitter@npm:^3.1.1": version: 3.1.1 resolution: "@metamask/safe-event-emitter@npm:3.1.1" checksum: e24db4d7c20764bfc5b025065f92518c805f0ffb1da4820078b8cff7dcae964c0f354cf053fcb7ac659de015d5ffdf21aae5e8d44e191ee8faa9066855f22653 @@ -3016,23 +2962,7 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-rpc-methods@npm:^8.0.0": - version: 8.0.0 - resolution: "@metamask/snaps-rpc-methods@npm:8.0.0" - dependencies: - "@metamask/key-tree": ^9.0.0 - "@metamask/permission-controller": ^9.0.2 - "@metamask/rpc-errors": ^6.2.1 - "@metamask/snaps-sdk": ^4.0.0 - "@metamask/snaps-utils": ^7.1.0 - "@metamask/utils": ^8.3.0 - "@noble/hashes": ^1.3.1 - superstruct: ^1.0.3 - checksum: 219e166b9a1b0653db8c679319ee9022244e697575023dcccbab571faf9411bf88e1139049745a4d7949c1e6a4ae621e20a5e5e4c31f8b68e1f146117f0b8150 - languageName: node - linkType: hard - -"@metamask/snaps-rpc-methods@npm:^8.1.0": +"@metamask/snaps-rpc-methods@npm:^8.0.0, @metamask/snaps-rpc-methods@npm:^8.1.0": version: 8.1.0 resolution: "@metamask/snaps-rpc-methods@npm:8.1.0" dependencies: @@ -3048,21 +2978,7 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-sdk@npm:^4.0.0, @metamask/snaps-sdk@npm:^4.0.1": - version: 4.0.1 - resolution: "@metamask/snaps-sdk@npm:4.0.1" - dependencies: - "@metamask/key-tree": ^9.0.0 - "@metamask/providers": ^16.0.0 - "@metamask/rpc-errors": ^6.2.1 - "@metamask/utils": ^8.3.0 - fast-xml-parser: ^4.3.4 - superstruct: ^1.0.3 - checksum: 21a2985258216c362f521c36ec2e63963268177ac0d811c6bf7409c489209e341f383db891e5051d0510e7a967565ed0fb5825be3232181fa46ccee79642e3d7 - languageName: node - linkType: hard - -"@metamask/snaps-sdk@npm:^4.1.0": +"@metamask/snaps-sdk@npm:^4.0.0, @metamask/snaps-sdk@npm:^4.0.1, @metamask/snaps-sdk@npm:^4.1.0": version: 4.1.0 resolution: "@metamask/snaps-sdk@npm:4.1.0" dependencies: @@ -3076,37 +2992,7 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-utils@npm:^7.0.3, @metamask/snaps-utils@npm:^7.1.0": - version: 7.1.0 - resolution: "@metamask/snaps-utils@npm:7.1.0" - dependencies: - "@babel/core": ^7.23.2 - "@babel/types": ^7.23.0 - "@metamask/base-controller": ^5.0.1 - "@metamask/key-tree": ^9.0.0 - "@metamask/permission-controller": ^9.0.2 - "@metamask/rpc-errors": ^6.2.1 - "@metamask/slip44": ^3.1.0 - "@metamask/snaps-registry": ^3.1.0 - "@metamask/snaps-sdk": ^4.0.0 - "@metamask/utils": ^8.3.0 - "@noble/hashes": ^1.3.1 - "@scure/base": ^1.1.1 - chalk: ^4.1.2 - cron-parser: ^4.5.0 - fast-deep-equal: ^3.1.3 - fast-json-stable-stringify: ^2.1.0 - marked: ^12.0.1 - rfdc: ^1.3.0 - semver: ^7.5.4 - ses: ^1.1.0 - superstruct: ^1.0.3 - validate-npm-package-name: ^5.0.0 - checksum: ecd081596930d8337b9f054e2612f04bfdab1c4d46fb395a8ad8cb7263d0f5ebb6e7a1988168a46a794ca065fe3537959b68cb7b07a80346ff372be160d68601 - languageName: node - linkType: hard - -"@metamask/snaps-utils@npm:^7.3.0": +"@metamask/snaps-utils@npm:^7.0.3, @metamask/snaps-utils@npm:^7.1.0, @metamask/snaps-utils@npm:^7.3.0": version: 7.3.0 resolution: "@metamask/snaps-utils@npm:7.3.0" dependencies: @@ -3162,7 +3048,7 @@ __metadata: "@metamask/eth-query": ^4.0.0 "@metamask/ethjs-provider-http": ^0.3.0 "@metamask/gas-fee-controller": ^15.1.1 - "@metamask/keyring-api": ^6.0.0 + "@metamask/keyring-api": ^6.1.0 "@metamask/metamask-eth-abis": ^3.1.1 "@metamask/network-controller": ^18.1.0 "@metamask/rpc-errors": ^6.2.1 From f916816704199f667d8539b1091892c4a0e6e36a Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Tue, 14 May 2024 17:10:36 +0800 Subject: [PATCH 22/23] fix: type error --- .../src/AccountsController.test.ts | 14 ++-- .../src/AccountsController.ts | 67 ++++++++++++++----- .../src/tests/mocks.test.ts | 27 +++++++- .../accounts-controller/src/tests/mocks.ts | 2 +- .../accounts-controller/src/utils.test.ts | 18 ----- packages/accounts-controller/src/utils.ts | 2 - 6 files changed, 88 insertions(+), 42 deletions(-) delete mode 100644 packages/accounts-controller/src/utils.test.ts diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index e84a87cb215..1344e732fda 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -145,6 +145,8 @@ 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.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({ @@ -154,6 +156,8 @@ function createExpectedInternalAccount({ keyringType, snapId, snapEnabled = true, + importTime, + lastSelected, }: { id: string; name: string; @@ -161,6 +165,8 @@ function createExpectedInternalAccount({ keyringType: string; snapId?: string; snapEnabled?: boolean; + importTime?: number; + lastSelected?: number; }): InternalAccount { const account: InternalAccount = { id, @@ -171,10 +177,8 @@ function createExpectedInternalAccount({ metadata: { name, keyring: { type: keyringType }, - importTime: expect.any(Number), - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - lastSelected: undefined, + importTime: importTime || expect.any(Number), + lastSelected: lastSelected || undefined, }, }; @@ -703,6 +707,8 @@ describe('AccountsController', () => { name: 'Custom Name', address: mockAccount2.address, keyringType: KeyringTypes.hd, + importTime: 1955565967656, + lastSelected: 1955565967656, }); const mockNewKeyringState = { diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index b3e94417248..7da89dee907 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -31,7 +31,7 @@ const controllerName = 'AccountsController'; export type AccountsControllerState = { internalAccounts: { accounts: Record; - selectedAccount: string; // id of the selected account + selectedAccount: string; }; }; @@ -308,7 +308,21 @@ 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; }); } @@ -369,8 +383,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; }); } @@ -381,9 +402,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; }); } } @@ -767,17 +790,29 @@ export class AccountsController extends BaseController< const accountName = `${accountPrefix} ${indexToUse}`; this.update((currentState: Draft) => { - (currentState as AccountsControllerState).internalAccounts.accounts[ - newAccount.id - ] = { - ...newAccount, - metadata: { - ...newAccount.metadata, - name: accountName, - importTime: Date.now(), - lastSelected: Date.now(), + // 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); diff --git a/packages/accounts-controller/src/tests/mocks.test.ts b/packages/accounts-controller/src/tests/mocks.test.ts index b59b7a51a98..d631ca8aae4 100644 --- a/packages/accounts-controller/src/tests/mocks.test.ts +++ b/packages/accounts-controller/src/tests/mocks.test.ts @@ -1,4 +1,4 @@ -import { EthAccountType } from '@metamask/keyring-api'; +import { BtcAccountType, EthAccountType } from '@metamask/keyring-api'; import { createMockInternalAccount } from './mocks'; @@ -49,4 +49,29 @@ describe('createMockInternalAccount', () => { }, }); }); + + 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 index 8a9590a776e..c96fbcfd699 100644 --- a/packages/accounts-controller/src/tests/mocks.ts +++ b/packages/accounts-controller/src/tests/mocks.ts @@ -59,7 +59,7 @@ export const createMockInternalAccount = ({ methods = [BtcMethod.SendMany]; break; default: - methods = [] as EthMethod[]; + throw new Error(`Unknown account type: ${type as string}`); } return { diff --git a/packages/accounts-controller/src/utils.test.ts b/packages/accounts-controller/src/utils.test.ts deleted file mode 100644 index 010f8848491..00000000000 --- a/packages/accounts-controller/src/utils.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { EthAccountType } from '@metamask/keyring-api'; - -import { createMockInternalAccount } from './tests/mocks'; -import { isEVMAccount } from './utils'; - -describe('isEVMAccount', () => { - it.each([ - [EthAccountType.Eoa, true], - [EthAccountType.Erc4337, true], - ['bip122', false], - ])('%s should return %s', (accountType, expected) => { - expect( - isEVMAccount( - createMockInternalAccount({ type: accountType as EthAccountType }), - ), - ).toBe(expected); - }); -}); diff --git a/packages/accounts-controller/src/utils.ts b/packages/accounts-controller/src/utils.ts index 22dcf32c440..b3e7cbd639d 100644 --- a/packages/accounts-controller/src/utils.ts +++ b/packages/accounts-controller/src/utils.ts @@ -1,6 +1,4 @@ import { toBuffer } from '@ethereumjs/util'; -import type { InternalAccount } from '@metamask/keyring-api'; -import { EthAccountType } from '@metamask/keyring-api'; import { isCustodyKeyring, KeyringTypes } from '@metamask/keyring-controller'; import { sha256 } from 'ethereum-cryptography/sha256'; import type { V4Options } from 'uuid'; From a1db0714a7f10656ba1bd126d5dcb783ed07b562 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Tue, 14 May 2024 17:26:43 +0800 Subject: [PATCH 23/23] temp: add skip lib check for snaps sdk type error --- packages/accounts-controller/package.json | 4 ++-- packages/assets-controllers/tsconfig.build.json | 3 ++- yarn.lock | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/accounts-controller/package.json b/packages/accounts-controller/package.json index 2c5e33f9656..c7d6ce4316c 100644 --- a/packages/accounts-controller/package.json +++ b/packages/accounts-controller/package.json @@ -45,8 +45,8 @@ "@metamask/base-controller": "^5.0.2", "@metamask/eth-snap-keyring": "^4.1.0", "@metamask/keyring-api": "^6.1.0", - "@metamask/snaps-sdk": "^4.0.1", - "@metamask/snaps-utils": "^7.1.0", + "@metamask/snaps-sdk": "^4.1.0", + "@metamask/snaps-utils": "^7.3.0", "@metamask/utils": "^8.3.0", "deepmerge": "^4.2.2", "ethereum-cryptography": "^2.1.2", diff --git a/packages/assets-controllers/tsconfig.build.json b/packages/assets-controllers/tsconfig.build.json index 608981bd683..c05f8306ba5 100644 --- a/packages/assets-controllers/tsconfig.build.json +++ b/packages/assets-controllers/tsconfig.build.json @@ -3,7 +3,8 @@ "compilerOptions": { "baseUrl": "./", "outDir": "./dist/types", - "rootDir": "./src" + "rootDir": "./src", + "skipLibCheck": true }, "references": [ { "path": "../accounts-controller/tsconfig.build.json" }, diff --git a/yarn.lock b/yarn.lock index 8a63b8627c3..dc4bfdb1292 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1649,8 +1649,8 @@ __metadata: "@metamask/keyring-api": ^6.1.0 "@metamask/keyring-controller": ^16.0.0 "@metamask/snaps-controllers": ^7.0.1 - "@metamask/snaps-sdk": ^4.0.1 - "@metamask/snaps-utils": ^7.1.0 + "@metamask/snaps-sdk": ^4.1.0 + "@metamask/snaps-utils": ^7.3.0 "@metamask/utils": ^8.3.0 "@types/jest": ^27.4.1 "@types/readable-stream": ^2.3.0