diff --git a/packages/assets-controllers/jest.config.js b/packages/assets-controllers/jest.config.js index c5034d8960..a226e79eb7 100644 --- a/packages/assets-controllers/jest.config.js +++ b/packages/assets-controllers/jest.config.js @@ -17,10 +17,10 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 90.35, - functions: 96.74, - lines: 97.34, - statements: 97.36, + branches: 91.07, + functions: 97.51, + lines: 98.12, + statements: 98.03, }, }, diff --git a/packages/assets-controllers/src/AssetsContractController.test.ts b/packages/assets-controllers/src/AssetsContractController.test.ts index 8600501e0e..277405bbec 100644 --- a/packages/assets-controllers/src/AssetsContractController.test.ts +++ b/packages/assets-controllers/src/AssetsContractController.test.ts @@ -9,22 +9,26 @@ import { } from '@metamask/controller-utils'; import HttpProvider from '@metamask/ethjs-provider-http'; import type { - NetworkClientId, - NetworkControllerMessenger, Provider, + NetworkClientId, + NetworkControllerActions, + NetworkControllerEvents, } from '@metamask/network-controller'; import { NetworkController, NetworkClientType, } from '@metamask/network-controller'; -import { - getDefaultPreferencesState, - type PreferencesState, -} from '@metamask/preferences-controller'; +import type { PreferencesState } from '@metamask/preferences-controller'; +import { getDefaultPreferencesState } from '@metamask/preferences-controller'; import assert from 'assert'; import { mockNetwork } from '../../../tests/mock-network'; +import type { + ExtractAvailableAction, + ExtractAvailableEvent, +} from '../../base-controller/tests/helpers'; import { buildInfuraNetworkClientConfiguration } from '../../network-controller/tests/helpers'; +import type { AssetsContractControllerMessenger } from './AssetsContractController'; import { AssetsContractController, MISSING_PROVIDER_ERROR, @@ -56,31 +60,37 @@ const TEST_ACCOUNT_PUBLIC_ADDRESS = */ async function setupAssetContractControllers({ options, - useNetworkControllerProvider, + useNetworkControllerProvider = false, infuraProjectId = '341eacb578dd44a1a049cbc5f6fd4035', }: { - options?: Partial[0]>; + options?: Partial< + Omit[0], 'messenger'> + >; useNetworkControllerProvider?: boolean; infuraProjectId?: string; } = {}) { const networkClientConfiguration = { type: NetworkClientType.Infura, - network: 'mainnet', + network: NetworkType.mainnet, infuraProjectId, chainId: BUILT_IN_NETWORKS.mainnet.chainId, ticker: BUILT_IN_NETWORKS.mainnet.ticker, } as const; let provider: Provider; - const messenger: NetworkControllerMessenger = - new ControllerMessenger().getRestricted({ + const controllerMessenger = new ControllerMessenger< + | ExtractAvailableAction + | NetworkControllerActions, + | ExtractAvailableEvent + | NetworkControllerEvents + >(); + const networkController = new NetworkController({ + infuraProjectId, + messenger: controllerMessenger.getRestricted({ name: 'NetworkController', allowedActions: [], allowedEvents: [], - }); - const networkController = new NetworkController({ - infuraProjectId, - messenger, + }), trackMetaMetricsEvent: jest.fn(), }); if (useNetworkControllerProvider) { @@ -94,40 +104,52 @@ async function setupAssetContractControllers({ ); } - const getNetworkClientById = useNetworkControllerProvider - ? networkController.getNetworkClientById.bind(networkController) - : (networkClientId: NetworkClientId) => - ({ + controllerMessenger.unregisterActionHandler( + 'NetworkController:getNetworkClientById', + ); + controllerMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + // @ts-expect-error TODO: remove this annotation once the `Eip1193Provider` class is released + useNetworkControllerProvider + ? networkController.getNetworkClientById.bind(networkController) + : (networkClientId: NetworkClientId) => ({ ...networkController.getNetworkClientById(networkClientId), provider, - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); + }), + ); - const preferencesStateChangeListeners: ((state: PreferencesState) => void)[] = - []; + const assetsContractMessenger = controllerMessenger.getRestricted({ + name: 'AssetsContractController', + allowedActions: [ + 'NetworkController:getNetworkClientById', + 'NetworkController:getNetworkConfigurationByNetworkClientId', + 'NetworkController:getSelectedNetworkClient', + 'NetworkController:getState', + ], + allowedEvents: [ + 'PreferencesController:stateChange', + 'NetworkController:networkDidChange', + ], + }); const assetsContract = new AssetsContractController({ chainId: ChainId.mainnet, - onPreferencesStateChange: (listener) => { - preferencesStateChangeListeners.push(listener); - }, - onNetworkDidChange: (listener) => - messenger.subscribe('NetworkController:networkDidChange', listener), - getNetworkClientById, + messenger: assetsContractMessenger, ...options, }); return { - messenger, + messenger: controllerMessenger, network: networkController, assetsContract, provider, networkClientConfiguration, infuraProjectId, triggerPreferencesStateChange: (state: PreferencesState) => { - for (const listener of preferencesStateChangeListeners) { - listener(state); - } + controllerMessenger.publish( + 'PreferencesController:stateChange', + state, + [], + ); }, }; } @@ -170,10 +192,12 @@ export { setupAssetContractControllers, mockNetworkWithDefaultChainId }; describe('AssetsContractController', () => { it('should set default config', async () => { const { assetsContract, messenger } = await setupAssetContractControllers(); - expect(assetsContract.config).toStrictEqual({ + expect({ + chainId: assetsContract.chainId, + ipfsGateway: assetsContract.ipfsGateway, + }).toStrictEqual({ chainId: SupportedTokenDetectionNetworks.mainnet, ipfsGateway: IPFS_DEFAULT_GATEWAY_URL, - provider: undefined, }); messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); @@ -181,10 +205,12 @@ describe('AssetsContractController', () => { it('should update the ipfsGateWay config value when this value is changed in the preferences controller', async () => { const { assetsContract, messenger, triggerPreferencesStateChange } = await setupAssetContractControllers(); - expect(assetsContract.config).toStrictEqual({ + expect({ + chainId: assetsContract.chainId, + ipfsGateway: assetsContract.ipfsGateway, + }).toStrictEqual({ chainId: SupportedTokenDetectionNetworks.mainnet, ipfsGateway: IPFS_DEFAULT_GATEWAY_URL, - provider: undefined, }); triggerPreferencesStateChange({ @@ -192,28 +218,23 @@ describe('AssetsContractController', () => { ipfsGateway: 'newIPFSGateWay', }); - expect(assetsContract.config).toStrictEqual({ + expect({ + chainId: assetsContract.chainId, + ipfsGateway: assetsContract.ipfsGateway, + }).toStrictEqual({ ipfsGateway: 'newIPFSGateWay', chainId: SupportedTokenDetectionNetworks.mainnet, - provider: undefined, }); messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); - it('should throw when provider property is accessed', async () => { - const { assetsContract, messenger } = await setupAssetContractControllers(); - expect(() => console.log(assetsContract.provider)).toThrow( - 'Property only used for setting', - ); - messenger.clearEventSubscriptions('NetworkController:networkDidChange'); - }); - it('should throw missing provider error when getting ERC-20 token balance when missing provider', async () => { const { assetsContract, messenger } = await setupAssetContractControllers(); - assetsContract.configure({ provider: undefined }); + assetsContract.setProvider(undefined); await expect( - assetsContract.getERC20BalanceOf( + messenger.call( + `AssetsContractController:getERC20BalanceOf`, ERC20_UNI_ADDRESS, TEST_ACCOUNT_PUBLIC_ADDRESS, ), @@ -223,9 +244,12 @@ describe('AssetsContractController', () => { it('should throw missing provider error when getting ERC-20 token decimal when missing provider', async () => { const { assetsContract, messenger } = await setupAssetContractControllers(); - assetsContract.configure({ provider: undefined }); + assetsContract.setProvider(undefined); await expect( - assetsContract.getERC20TokenDecimals(ERC20_UNI_ADDRESS), + messenger.call( + `AssetsContractController:getERC20TokenDecimals`, + ERC20_UNI_ADDRESS, + ), ).rejects.toThrow(MISSING_PROVIDER_ERROR); messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); @@ -233,7 +257,7 @@ describe('AssetsContractController', () => { it('should get balance of ERC-20 token contract correctly', async () => { const { assetsContract, messenger, provider, networkClientConfiguration } = await setupAssetContractControllers(); - assetsContract.configure({ provider }); + assetsContract.setProvider(provider); mockNetworkWithDefaultChainId({ networkClientConfiguration, mocks: [ @@ -271,11 +295,13 @@ describe('AssetsContractController', () => { }, ], }); - const UNIBalance = await assetsContract.getERC20BalanceOf( + const UNIBalance = await messenger.call( + `AssetsContractController:getERC20BalanceOf`, ERC20_UNI_ADDRESS, TEST_ACCOUNT_PUBLIC_ADDRESS, ); - const UNINoBalance = await assetsContract.getERC20BalanceOf( + const UNINoBalance = await messenger.call( + `AssetsContractController:getERC20BalanceOf`, ERC20_UNI_ADDRESS, '0x202637dAAEfbd7f131f90338a4A6c69F6Cd5CE91', ); @@ -287,7 +313,7 @@ describe('AssetsContractController', () => { it('should get ERC-721 NFT tokenId correctly', async () => { const { assetsContract, messenger, provider, networkClientConfiguration } = await setupAssetContractControllers(); - assetsContract.configure({ provider }); + assetsContract.setProvider(provider); mockNetworkWithDefaultChainId({ networkClientConfiguration, mocks: [ @@ -309,7 +335,8 @@ describe('AssetsContractController', () => { }, ], }); - const tokenId = await assetsContract.getERC721NftTokenId( + const tokenId = await messenger.call( + `AssetsContractController:getERC721NftTokenId`, ERC721_GODS_ADDRESS, '0x9a90bd8d1149a88b42a99cf62215ad955d6f498a', 0, @@ -320,9 +347,10 @@ describe('AssetsContractController', () => { it('should throw missing provider error when getting ERC-721 token standard and details when missing provider', async () => { const { assetsContract, messenger } = await setupAssetContractControllers(); - assetsContract.configure({ provider: undefined }); + assetsContract.setProvider(undefined); await expect( - assetsContract.getTokenStandardAndDetails( + messenger.call( + `AssetsContractController:getTokenStandardAndDetails`, ERC20_UNI_ADDRESS, TEST_ACCOUNT_PUBLIC_ADDRESS, ), @@ -333,10 +361,11 @@ describe('AssetsContractController', () => { it('should throw contract standard error when getting ERC-20 token standard and details when provided with invalid ERC-20 address', async () => { const { assetsContract, messenger, provider } = await setupAssetContractControllers(); - assetsContract.configure({ provider }); + assetsContract.setProvider(provider); const error = 'Unable to determine contract standard'; await expect( - assetsContract.getTokenStandardAndDetails( + messenger.call( + `AssetsContractController:getTokenStandardAndDetails`, 'BaDeRc20AdDrEsS', TEST_ACCOUNT_PUBLIC_ADDRESS, ), @@ -347,7 +376,7 @@ describe('AssetsContractController', () => { it('should get ERC-721 token standard and details', async () => { const { assetsContract, messenger, provider, networkClientConfiguration } = await setupAssetContractControllers(); - assetsContract.configure({ provider }); + assetsContract.setProvider(provider); mockNetworkWithDefaultChainId({ networkClientConfiguration, mocks: [ @@ -401,7 +430,8 @@ describe('AssetsContractController', () => { }, ], }); - const standardAndDetails = await assetsContract.getTokenStandardAndDetails( + const standardAndDetails = await messenger.call( + `AssetsContractController:getTokenStandardAndDetails`, ERC721_GODS_ADDRESS, TEST_ACCOUNT_PUBLIC_ADDRESS, ); @@ -412,7 +442,7 @@ describe('AssetsContractController', () => { it('should get ERC-1155 token standard and details', async () => { const { assetsContract, messenger, provider, networkClientConfiguration } = await setupAssetContractControllers(); - assetsContract.configure({ provider }); + assetsContract.setProvider(provider); mockNetworkWithDefaultChainId({ networkClientConfiguration, mocks: [ @@ -482,7 +512,8 @@ describe('AssetsContractController', () => { }, ], }); - const standardAndDetails = await assetsContract.getTokenStandardAndDetails( + const standardAndDetails = await messenger.call( + `AssetsContractController:getTokenStandardAndDetails`, ERC1155_ADDRESS, TEST_ACCOUNT_PUBLIC_ADDRESS, ); @@ -497,7 +528,7 @@ describe('AssetsContractController', () => { it('should get ERC-20 token standard and details', async () => { const { assetsContract, messenger, provider, networkClientConfiguration } = await setupAssetContractControllers(); - assetsContract.configure({ provider }); + assetsContract.setProvider(provider); mockNetworkWithDefaultChainId({ networkClientConfiguration, mocks: [ @@ -583,7 +614,8 @@ describe('AssetsContractController', () => { }, ], }); - const standardAndDetails = await assetsContract.getTokenStandardAndDetails( + const standardAndDetails = await messenger.call( + `AssetsContractController:getTokenStandardAndDetails`, ERC20_UNI_ADDRESS, TEST_ACCOUNT_PUBLIC_ADDRESS, ); @@ -594,7 +626,7 @@ describe('AssetsContractController', () => { it('should get ERC-721 NFT tokenURI correctly', async () => { const { assetsContract, messenger, provider, networkClientConfiguration } = await setupAssetContractControllers(); - assetsContract.configure({ provider }); + assetsContract.setProvider(provider); mockNetworkWithDefaultChainId({ networkClientConfiguration, mocks: [ @@ -632,7 +664,8 @@ describe('AssetsContractController', () => { }, ], }); - const tokenId = await assetsContract.getERC721TokenURI( + const tokenId = await messenger.call( + `AssetsContractController:getERC721TokenURI`, ERC721_GODS_ADDRESS, '0', ); @@ -643,7 +676,7 @@ describe('AssetsContractController', () => { it('should not throw an error when address given does not support NFT Metadata interface', async () => { const { assetsContract, messenger, provider, networkClientConfiguration } = await setupAssetContractControllers(); - assetsContract.configure({ provider }); + assetsContract.setProvider(provider); const errorLogSpy = jest .spyOn(console, 'error') .mockImplementationOnce(() => { @@ -685,7 +718,8 @@ describe('AssetsContractController', () => { }, ], }); - const uri = await assetsContract.getERC721TokenURI( + const uri = await messenger.call( + `AssetsContractController:getERC721TokenURI`, '0x0000000000000000000000000000000000000000', '0', ); @@ -701,7 +735,7 @@ describe('AssetsContractController', () => { it('should get ERC-721 NFT name', async () => { const { assetsContract, messenger, provider, networkClientConfiguration } = await setupAssetContractControllers(); - assetsContract.configure({ provider }); + assetsContract.setProvider(provider); mockNetworkWithDefaultChainId({ networkClientConfiguration, mocks: [ @@ -723,7 +757,10 @@ describe('AssetsContractController', () => { }, ], }); - const name = await assetsContract.getERC721AssetName(ERC721_GODS_ADDRESS); + const name = await messenger.call( + `AssetsContractController:getERC721AssetName`, + ERC721_GODS_ADDRESS, + ); expect(name).toBe('Gods Unchained'); messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); @@ -731,7 +768,7 @@ describe('AssetsContractController', () => { it('should get ERC-721 NFT symbol', async () => { const { assetsContract, messenger, provider, networkClientConfiguration } = await setupAssetContractControllers(); - assetsContract.configure({ provider }); + assetsContract.setProvider(provider); mockNetworkWithDefaultChainId({ networkClientConfiguration, mocks: [ @@ -753,7 +790,8 @@ describe('AssetsContractController', () => { }, ], }); - const symbol = await assetsContract.getERC721AssetSymbol( + const symbol = await messenger.call( + `AssetsContractController:getERC721AssetSymbol`, ERC721_GODS_ADDRESS, ); expect(symbol).toBe('GODS'); @@ -761,9 +799,12 @@ describe('AssetsContractController', () => { }); it('should throw missing provider error when getting ERC-721 NFT symbol when missing provider', async () => { - const { assetsContract, messenger } = await setupAssetContractControllers(); + const { messenger } = await setupAssetContractControllers(); await expect( - assetsContract.getERC721AssetSymbol(ERC721_GODS_ADDRESS), + messenger.call( + `AssetsContractController:getERC721AssetSymbol`, + ERC721_GODS_ADDRESS, + ), ).rejects.toThrow(MISSING_PROVIDER_ERROR); messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); @@ -771,7 +812,7 @@ describe('AssetsContractController', () => { it('should get ERC-20 token decimals', async () => { const { assetsContract, messenger, provider, networkClientConfiguration } = await setupAssetContractControllers(); - assetsContract.configure({ provider }); + assetsContract.setProvider(provider); mockNetworkWithDefaultChainId({ networkClientConfiguration, mocks: [ @@ -793,7 +834,8 @@ describe('AssetsContractController', () => { }, ], }); - const decimals = await assetsContract.getERC20TokenDecimals( + const decimals = await messenger.call( + `AssetsContractController:getERC20TokenDecimals`, ERC20_SAI_ADDRESS, ); expect(Number(decimals)).toBe(18); @@ -803,7 +845,7 @@ describe('AssetsContractController', () => { it('should get ERC-20 token name', async () => { const { assetsContract, messenger, provider, networkClientConfiguration } = await setupAssetContractControllers(); - assetsContract.configure({ provider }); + assetsContract.setProvider(provider); mockNetworkWithDefaultChainId({ networkClientConfiguration, mocks: [ @@ -826,7 +868,10 @@ describe('AssetsContractController', () => { ], }); - const name = await assetsContract.getERC20TokenName(ERC20_DAI_ADDRESS); + const name = await messenger.call( + `AssetsContractController:getERC20TokenName`, + ERC20_DAI_ADDRESS, + ); expect(name).toBe('Dai Stablecoin'); messenger.clearEventSubscriptions('NetworkController:networkDidChange'); @@ -835,7 +880,7 @@ describe('AssetsContractController', () => { it('should get ERC-721 NFT ownership', async () => { const { assetsContract, messenger, provider, networkClientConfiguration } = await setupAssetContractControllers(); - assetsContract.configure({ provider }); + assetsContract.setProvider(provider); mockNetworkWithDefaultChainId({ networkClientConfiguration, mocks: [ @@ -857,7 +902,8 @@ describe('AssetsContractController', () => { }, ], }); - const tokenId = await assetsContract.getERC721OwnerOf( + const tokenId = await messenger.call( + `AssetsContractController:getERC721OwnerOf`, ERC721_GODS_ADDRESS, '148332', ); @@ -866,9 +912,13 @@ describe('AssetsContractController', () => { }); it('should throw missing provider error when getting ERC-721 NFT ownership', async () => { - const { assetsContract, messenger } = await setupAssetContractControllers(); + const { messenger } = await setupAssetContractControllers(); await expect( - assetsContract.getERC721OwnerOf(ERC721_GODS_ADDRESS, '148332'), + messenger.call( + `AssetsContractController:getERC721OwnerOf`, + ERC721_GODS_ADDRESS, + '148332', + ), ).rejects.toThrow(MISSING_PROVIDER_ERROR); messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); @@ -876,7 +926,7 @@ describe('AssetsContractController', () => { it('should get balance of ERC-20 token in a single call on network with token detection support', async () => { const { assetsContract, messenger, provider, networkClientConfiguration } = await setupAssetContractControllers(); - assetsContract.configure({ provider }); + assetsContract.setProvider(provider); mockNetworkWithDefaultChainId({ networkClientConfiguration, mocks: [ @@ -898,7 +948,8 @@ describe('AssetsContractController', () => { }, ], }); - const balances = await assetsContract.getBalancesInSingleCall( + const balances = await messenger.call( + `AssetsContractController:getBalancesInSingleCall`, ERC20_SAI_ADDRESS, [ERC20_SAI_ADDRESS], ); @@ -974,7 +1025,7 @@ describe('AssetsContractController', () => { }, ], }); - const { assetsContract, network, provider } = + const { assetsContract, messenger, provider } = await setupAssetContractControllers({ options: { chainId: ChainId.mainnet, @@ -982,9 +1033,10 @@ describe('AssetsContractController', () => { useNetworkControllerProvider: true, infuraProjectId, }); - assetsContract.configure({ provider }); + assetsContract.setProvider(provider); - const balancesOnMainnet = await assetsContract.getBalancesInSingleCall( + const balancesOnMainnet = await messenger.call( + 'AssetsContractController:getBalancesInSingleCall', ERC20_SAI_ADDRESS, [ERC20_SAI_ADDRESS], ); @@ -992,15 +1044,20 @@ describe('AssetsContractController', () => { [ERC20_SAI_ADDRESS]: BigNumber.from('0x0733ed8ef4c4a0155d09'), }); - await network.setActiveNetwork(InfuraNetworkType['linea-mainnet']); + await messenger.call( + `NetworkController:setActiveNetwork`, + InfuraNetworkType['linea-mainnet'], + ); - const balancesOnLineaMainnet = await assetsContract.getBalancesInSingleCall( + const balancesOnLineaMainnet = await messenger.call( + 'AssetsContractController:getBalancesInSingleCall', ERC20_SAI_ADDRESS, [ERC20_SAI_ADDRESS], ); expect(balancesOnLineaMainnet).toStrictEqual({ [ERC20_SAI_ADDRESS]: BigNumber.from('0xa0155d09733ed8ef4c4'), }); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should not have balance in a single call after switching to network without token detection support', async () => { @@ -1011,7 +1068,7 @@ describe('AssetsContractController', () => { provider, networkClientConfiguration, } = await setupAssetContractControllers(); - assetsContract.configure({ provider }); + assetsContract.setProvider(provider); mockNetworkWithDefaultChainId({ networkClientConfiguration, mocks: [ @@ -1064,7 +1121,8 @@ describe('AssetsContractController', () => { ], }); - const balances = await assetsContract.getBalancesInSingleCall( + const balances = await messenger.call( + `AssetsContractController:getBalancesInSingleCall`, ERC20_SAI_ADDRESS, [ERC20_SAI_ADDRESS], ); @@ -1072,7 +1130,8 @@ describe('AssetsContractController', () => { await network.setActiveNetwork(NetworkType.sepolia); - const noBalances = await assetsContract.getBalancesInSingleCall( + const noBalances = await messenger.call( + `AssetsContractController:getBalancesInSingleCall`, ERC20_SAI_ADDRESS, [ERC20_SAI_ADDRESS], ); @@ -1082,9 +1141,10 @@ describe('AssetsContractController', () => { it('should throw missing provider error when transferring single ERC-1155 when missing provider', async () => { const { assetsContract, messenger } = await setupAssetContractControllers(); - assetsContract.configure({ provider: undefined }); + assetsContract.setProvider(undefined); await expect( - assetsContract.transferSingleERC1155( + messenger.call( + `AssetsContractController:transferSingleERC1155`, ERC1155_ADDRESS, TEST_ACCOUNT_PUBLIC_ADDRESS, TEST_ACCOUNT_PUBLIC_ADDRESS, @@ -1098,7 +1158,7 @@ describe('AssetsContractController', () => { it('should throw when ERC1155 function transferSingle is not defined', async () => { const { assetsContract, messenger, provider, networkClientConfiguration } = await setupAssetContractControllers(); - assetsContract.configure({ provider }); + assetsContract.setProvider(provider); mockNetworkWithDefaultChainId({ networkClientConfiguration, mocks: [ @@ -1121,7 +1181,8 @@ describe('AssetsContractController', () => { ], }); await expect( - assetsContract.transferSingleERC1155( + messenger.call( + `AssetsContractController:transferSingleERC1155`, ERC1155_ADDRESS, '0x0', TEST_ACCOUNT_PUBLIC_ADDRESS, @@ -1135,7 +1196,7 @@ describe('AssetsContractController', () => { it('should get the balance of a ERC-1155 NFT for a given address', async () => { const { assetsContract, messenger, provider, networkClientConfiguration } = await setupAssetContractControllers(); - assetsContract.configure({ provider }); + assetsContract.setProvider(provider); mockNetworkWithDefaultChainId({ networkClientConfiguration, mocks: [ @@ -1157,7 +1218,8 @@ describe('AssetsContractController', () => { }, ], }); - const balance = await assetsContract.getERC1155BalanceOf( + const balance = await messenger.call( + `AssetsContractController:getERC1155BalanceOf`, TEST_ACCOUNT_PUBLIC_ADDRESS, ERC1155_ADDRESS, ERC1155_ID, @@ -1167,9 +1229,10 @@ describe('AssetsContractController', () => { }); it('should throw missing provider error when getting the balance of a ERC-1155 NFT when missing provider', async () => { - const { assetsContract, messenger } = await setupAssetContractControllers(); + const { messenger } = await setupAssetContractControllers(); await expect( - assetsContract.getERC1155BalanceOf( + messenger.call( + `AssetsContractController:getERC1155BalanceOf`, TEST_ACCOUNT_PUBLIC_ADDRESS, ERC1155_ADDRESS, ERC1155_ID, @@ -1181,7 +1244,7 @@ describe('AssetsContractController', () => { it('should get the URI of a ERC-1155 NFT', async () => { const { assetsContract, messenger, provider, networkClientConfiguration } = await setupAssetContractControllers(); - assetsContract.configure({ provider }); + assetsContract.setProvider(provider); mockNetworkWithDefaultChainId({ networkClientConfiguration, mocks: [ @@ -1204,7 +1267,8 @@ describe('AssetsContractController', () => { ], }); const expectedUri = `https://api.opensea.io/api/v1/metadata/${ERC1155_ADDRESS}/0x{id}`; - const uri = await assetsContract.getERC1155TokenURI( + const uri = await messenger.call( + `AssetsContractController:getERC1155TokenURI`, ERC1155_ADDRESS, ERC1155_ID, ); diff --git a/packages/assets-controllers/src/AssetsContractController.ts b/packages/assets-controllers/src/AssetsContractController.ts index 24871c48a6..d3fd83b4d6 100644 --- a/packages/assets-controllers/src/AssetsContractController.ts +++ b/packages/assets-controllers/src/AssetsContractController.ts @@ -1,16 +1,21 @@ import { Contract } from '@ethersproject/contracts'; import { Web3Provider } from '@ethersproject/providers'; -import type { BaseConfig, BaseState } from '@metamask/base-controller'; -import { BaseControllerV1 } from '@metamask/base-controller'; +import type { + ActionConstraint, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; import { IPFS_DEFAULT_GATEWAY_URL } from '@metamask/controller-utils'; import type { NetworkClientId, - NetworkState, - NetworkController, + NetworkControllerGetNetworkClientByIdAction, + NetworkControllerGetNetworkConfigurationByNetworkClientId, + NetworkControllerGetSelectedNetworkClientAction, + NetworkControllerGetStateAction, + NetworkControllerNetworkDidChangeEvent, Provider, } from '@metamask/network-controller'; -import type { PreferencesState } from '@metamask/preferences-controller'; -import type { Hex } from '@metamask/utils'; +import type { PreferencesControllerStateChangeEvent } from '@metamask/preferences-controller'; +import { getKnownPropertyNames, type Hex } from '@metamask/utils'; import type BN from 'bn.js'; import abiSingleCallBalancesContract from 'single-call-balance-checker-abi'; @@ -25,7 +30,7 @@ import { ERC721Standard } from './Standards/NftStandards/ERC721/ERC721Standard'; * @param chainId - ChainID of network * @returns Whether the current network supports token detection */ -export const SINGLE_CALL_BALANCES_ADDRESS_BY_CHAINID: Record = { +export const SINGLE_CALL_BALANCES_ADDRESS_BY_CHAINID = { [SupportedTokenDetectionNetworks.mainnet]: '0xb1f8e55c7f64d203c1400b9d8555d050f94adf39', [SupportedTokenDetectionNetworks.bsc]: @@ -62,125 +67,237 @@ export const SINGLE_CALL_BALANCES_ADDRESS_BY_CHAINID: Record = { '0x6aa75276052d96696134252587894ef5ffa520af', [SupportedTokenDetectionNetworks.moonriver]: '0x6aa75276052d96696134252587894ef5ffa520af', -}; +} as const satisfies Record; export const MISSING_PROVIDER_ERROR = 'AssetsContractController failed to set the provider correctly. A provider must be set for this method to be available'; -/** - * @type AssetsContractConfig - * - * Assets Contract controller configuration - * @property provider - Provider used to create a new web3 instance - */ -// 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 AssetsContractConfig extends BaseConfig { - provider: Provider | undefined; - ipfsGateway: string; - chainId: Hex; -} - /** * @type BalanceMap * * Key value object containing the balance for each tokenAddress * @property [tokenAddress] - Address of the token */ -// 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 BalanceMap { +export type BalanceMap = { [tokenAddress: string]: BN; -} +}; + +/** + * The name of the {@link AssetsContractController} + */ +const name = 'AssetsContractController'; + +/** + * A utility type that derives the public method names of a given messenger consumer class, + * and uses it to generate the class's internal messenger action types. + * @template Controller - A messenger consumer class. + */ +// TODO: Figure out generic constraint and move to base-controller +type ControllerActionsMap = { + [ClassMethod in keyof Controller as Controller[ClassMethod] extends ActionConstraint['handler'] + ? ClassMethod + : never]: { + type: `${typeof name}:${ClassMethod & string}`; + handler: Controller[ClassMethod]; + }; +}; + +type AssetsContractControllerActionsMap = + ControllerActionsMap; + +/** + * The union of all public class method names of {@link AssetsContractController}. + */ +type AssetsContractControllerMethodName = + keyof AssetsContractControllerActionsMap; + +/** + * The union of all internal messenger actions available to the {@link AssetsContractControllerMessenger}. + */ +export type AssetsContractControllerActions = + AssetsContractControllerActionsMap[AssetsContractControllerMethodName]; + +export type AssetsContractControllerGetERC20StandardAction = + AssetsContractControllerActionsMap['getERC20Standard']; + +export type AssetsContractControllerGetERC721StandardAction = + AssetsContractControllerActionsMap['getERC721Standard']; + +export type AssetsContractControllerGetERC1155StandardAction = + AssetsContractControllerActionsMap['getERC1155Standard']; + +export type AssetsContractControllerGetERC20BalanceOfAction = + AssetsContractControllerActionsMap['getERC20BalanceOf']; + +export type AssetsContractControllerGetERC20TokenDecimalsAction = + AssetsContractControllerActionsMap['getERC20TokenDecimals']; + +export type AssetsContractControllerGetERC20TokenNameAction = + AssetsContractControllerActionsMap['getERC20TokenName']; + +export type AssetsContractControllerGetERC721NftTokenIdAction = + AssetsContractControllerActionsMap['getERC721NftTokenId']; + +export type AssetsContractControllerGetERC721TokenURIAction = + AssetsContractControllerActionsMap['getERC721TokenURI']; + +export type AssetsContractControllerGetERC721AssetNameAction = + AssetsContractControllerActionsMap['getERC721AssetName']; + +export type AssetsContractControllerGetERC721AssetSymbolAction = + AssetsContractControllerActionsMap['getERC721AssetSymbol']; + +export type AssetsContractControllerGetERC721OwnerOfAction = + AssetsContractControllerActionsMap['getERC721OwnerOf']; + +export type AssetsContractControllerGetERC1155TokenURIAction = + AssetsContractControllerActionsMap['getERC1155TokenURI']; + +export type AssetsContractControllerGetERC1155BalanceOfAction = + AssetsContractControllerActionsMap['getERC1155BalanceOf']; + +export type AssetsContractControllerTransferSingleERC1155Action = + AssetsContractControllerActionsMap['transferSingleERC1155']; + +export type AssetsContractControllerGetTokenStandardAndDetailsAction = + AssetsContractControllerActionsMap['getTokenStandardAndDetails']; + +export type AssetsContractControllerGetBalancesInSingleCallAction = + AssetsContractControllerActionsMap['getBalancesInSingleCall']; + +/** + * The union of all internal messenger events available to the {@link AssetsContractControllerMessenger}. + */ +export type AssetsContractControllerEvents = never; + +/** + * The union of all external messenger actions that must be allowed by the {@link AssetsContractControllerMessenger}. + */ +export type AllowedActions = + | NetworkControllerGetNetworkClientByIdAction + | NetworkControllerGetNetworkConfigurationByNetworkClientId + | NetworkControllerGetSelectedNetworkClientAction + | NetworkControllerGetStateAction; + +/** + * The union of all external messenger event that must be allowed by the {@link AssetsContractControllerMessenger}. + */ +export type AllowedEvents = + | PreferencesControllerStateChangeEvent + | NetworkControllerNetworkDidChangeEvent; + +/** + * The messenger of the {@link AssetsContractController}. + */ +export type AssetsContractControllerMessenger = RestrictedControllerMessenger< + typeof name, + AssetsContractControllerActions | AllowedActions, + AssetsContractControllerEvents | AllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] +>; /** * Controller that interacts with contracts on mainnet through web3 */ -export class AssetsContractController extends BaseControllerV1< - AssetsContractConfig, - BaseState -> { - private _provider?: Provider; +export class AssetsContractController { + protected messagingSystem: AssetsContractControllerMessenger; - /** - * Name of this controller used during composition - */ - override name = 'AssetsContractController' as const; + #provider: Provider | undefined; - private readonly getNetworkClientById: NetworkController['getNetworkClientById']; + #ipfsGateway: string; + + #chainId: Hex; /** * Creates a AssetsContractController instance. * * @param options - The controller options. + * @param options.messenger - The controller messenger. * @param options.chainId - The chain ID of the current network. - * @param options.onPreferencesStateChange - Allows subscribing to preference controller state changes. - * @param options.onNetworkDidChange - Allows subscribing to network controller networkDidChange events. - * @param options.getNetworkClientById - Gets the network client with the given id from the NetworkController. - * @param config - Initial options used to configure this controller. - * @param state - Initial state to set on this controller. */ - constructor( - { - chainId: initialChainId, - onPreferencesStateChange, - onNetworkDidChange, - getNetworkClientById, - }: { - chainId: Hex; - onPreferencesStateChange: ( - listener: (preferencesState: PreferencesState) => void, - ) => void; - onNetworkDidChange: ( - listener: (networkState: NetworkState) => void, - ) => void; - getNetworkClientById: NetworkController['getNetworkClientById']; - }, - config?: Partial, - state?: Partial, - ) { - super(config, state); - this.defaultConfig = { - provider: undefined, - ipfsGateway: IPFS_DEFAULT_GATEWAY_URL, - chainId: initialChainId, - }; - this.initialize(); - this.getNetworkClientById = getNetworkClientById; - - onPreferencesStateChange(({ ipfsGateway }) => { - this.configure({ ipfsGateway }); - }); - - onNetworkDidChange(({ selectedNetworkClientId }) => { - const selectedNetworkClient = getNetworkClientById( - selectedNetworkClientId, - ); - const { chainId } = selectedNetworkClient.configuration; + constructor({ + messenger, + chainId: initialChainId, + }: { + messenger: AssetsContractControllerMessenger; + chainId: Hex; + }) { + this.messagingSystem = messenger; + this.#provider = undefined; + this.#ipfsGateway = IPFS_DEFAULT_GATEWAY_URL; + this.#chainId = initialChainId; + + this.#registerActionHandlers(); + this.#registerEventSubscriptions(); + } - if (this.config.chainId !== chainId) { - this.configure({ - chainId: selectedNetworkClient.configuration.chainId, - }); - } - }); + // TODO: Expand into base-controller utility function that batch registers action handlers. + #registerActionHandlers() { + const methodsExcludedFromMessenger = [ + 'constructor', + 'messagingSystem', + 'setProvider', + 'provider', + 'ipfsGateway', + 'chainId', + ]; + + getKnownPropertyNames(Object.getPrototypeOf(this)).forEach( + (method) => { + if ( + ((key: keyof this): key is AssetsContractControllerMethodName => + !methodsExcludedFromMessenger.find((e) => e === key) && + typeof this[key] === 'function')(method) + ) { + this.messagingSystem.registerActionHandler( + `${name}:${method}`, + // TODO: Write a generic for-loop implementation that iterates over an input union type in tandem with the input array. + // @ts-expect-error Both assigned argument and assignee parameter are using the entire union type for `method` instead of the type for the current element + this[method].bind(this), + ); + } + }, + ); + } + + #registerEventSubscriptions() { + this.messagingSystem.subscribe( + `PreferencesController:stateChange`, + ({ ipfsGateway }) => { + this.#ipfsGateway = ipfsGateway; + }, + ); + + this.messagingSystem.subscribe( + `NetworkController:networkDidChange`, + ({ selectedNetworkClientId }) => { + const chainId = this.#getCorrectChainId(selectedNetworkClientId); + + if (this.#chainId !== chainId) { + this.#chainId = chainId; + // @ts-expect-error TODO: remove this annotation once the `Eip1193Provider` class is released + this.#provider = this.#getCorrectProvider(); + } + }, + ); } /** * Sets a new provider. * - * TODO: Replace this wth a method. - * - * @property provider - Provider used to create a new underlying Web3 instance + * @param provider - Provider used to create a new underlying Web3 instance */ - set provider(provider: Provider) { - this._provider = provider; + setProvider(provider: Provider | undefined) { + this.#provider = provider; + } + + get ipfsGateway() { + return this.#ipfsGateway; } - get provider() { - throw new Error('Property only used for setting'); + get chainId() { + return this.#chainId; } /** @@ -189,10 +306,14 @@ export class AssetsContractController extends BaseControllerV1< * @param networkClientId - Network Client ID. * @returns Web3Provider instance. */ - getProvider(networkClientId?: NetworkClientId): Web3Provider { + #getCorrectProvider(networkClientId?: NetworkClientId): Web3Provider { const provider = networkClientId - ? this.getNetworkClientById(networkClientId).provider - : this._provider; + ? this.messagingSystem.call( + `NetworkController:getNetworkClientById`, + networkClientId, + ).provider + : this.messagingSystem.call('NetworkController:getSelectedNetworkClient') + ?.provider ?? this.#provider; if (provider === undefined) { throw new Error(MISSING_PROVIDER_ERROR); @@ -207,10 +328,24 @@ export class AssetsContractController extends BaseControllerV1< * @param networkClientId - Network Client ID used to get the provider. * @returns Hex chain ID. */ - getChainId(networkClientId?: NetworkClientId): Hex { - return networkClientId - ? this.getNetworkClientById(networkClientId).configuration.chainId - : this.config.chainId; + #getCorrectChainId(networkClientId?: NetworkClientId): Hex { + if (networkClientId) { + const networkClientConfiguration = this.messagingSystem.call( + 'NetworkController:getNetworkConfigurationByNetworkClientId', + networkClientId, + ); + if (networkClientConfiguration) { + return networkClientConfiguration.chainId; + } + } + const { selectedNetworkClientId } = this.messagingSystem.call( + 'NetworkController:getState', + ); + const networkClient = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + selectedNetworkClientId, + ); + return networkClient.configuration?.chainId ?? this.#chainId; } /** @@ -220,7 +355,7 @@ export class AssetsContractController extends BaseControllerV1< * @returns ERC20Standard instance. */ getERC20Standard(networkClientId?: NetworkClientId): ERC20Standard { - const provider = this.getProvider(networkClientId); + const provider = this.#getCorrectProvider(networkClientId); return new ERC20Standard(provider); } @@ -231,7 +366,7 @@ export class AssetsContractController extends BaseControllerV1< * @returns ERC721Standard instance. */ getERC721Standard(networkClientId?: NetworkClientId): ERC721Standard { - const provider = this.getProvider(networkClientId); + const provider = this.#getCorrectProvider(networkClientId); return new ERC721Standard(provider); } @@ -242,7 +377,7 @@ export class AssetsContractController extends BaseControllerV1< * @returns ERC1155Standard instance. */ getERC1155Standard(networkClientId?: NetworkClientId): ERC1155Standard { - const provider = this.getProvider(networkClientId); + const provider = this.#getCorrectProvider(networkClientId); return new ERC1155Standard(provider); } @@ -302,7 +437,7 @@ export class AssetsContractController extends BaseControllerV1< * @param networkClientId - Network Client ID to fetch the provider with. * @returns Promise resolving to token identifier for the 'index'th asset assigned to 'selectedAddress'. */ - getERC721NftTokenId( + async getERC721NftTokenId( address: string, selectedAddress: string, index: number, @@ -335,9 +470,7 @@ export class AssetsContractController extends BaseControllerV1< balance?: BN | undefined; }> { // Asserts provider is available - this.getProvider(networkClientId); - - const { ipfsGateway } = this.config; + this.#getCorrectProvider(networkClientId); // ERC721 try { @@ -345,7 +478,7 @@ export class AssetsContractController extends BaseControllerV1< return { ...(await erc721Standard.getDetails( tokenAddress, - ipfsGateway, + this.#ipfsGateway, tokenId, )), }; @@ -359,7 +492,7 @@ export class AssetsContractController extends BaseControllerV1< return { ...(await erc1155Standard.getDetails( tokenAddress, - ipfsGateway, + this.#ipfsGateway, tokenId, )), }; @@ -523,9 +656,12 @@ export class AssetsContractController extends BaseControllerV1< tokensToDetect: string[], networkClientId?: NetworkClientId, ) { - const chainId = this.getChainId(networkClientId); - const provider = this.getProvider(networkClientId); - if (!(chainId in SINGLE_CALL_BALANCES_ADDRESS_BY_CHAINID)) { + const chainId = this.#getCorrectChainId(networkClientId); + const provider = this.#getCorrectProvider(networkClientId); + if ( + !((id): id is keyof typeof SINGLE_CALL_BALANCES_ADDRESS_BY_CHAINID => + id in SINGLE_CALL_BALANCES_ADDRESS_BY_CHAINID)(chainId) + ) { // Only fetch balance if contract address exists return {}; } diff --git a/packages/assets-controllers/src/AssetsContractControllerWithNetworkClientId.test.ts b/packages/assets-controllers/src/AssetsContractControllerWithNetworkClientId.test.ts index d064192b68..97760db63f 100644 --- a/packages/assets-controllers/src/AssetsContractControllerWithNetworkClientId.test.ts +++ b/packages/assets-controllers/src/AssetsContractControllerWithNetworkClientId.test.ts @@ -19,30 +19,38 @@ const TEST_ACCOUNT_PUBLIC_ADDRESS = describe('AssetsContractController with NetworkClientId', () => { it('should throw when getting ERC-20 token balance when networkClientId is invalid', async () => { - const { assetsContract, messenger } = await setupAssetContractControllers(); + const { messenger } = await setupAssetContractControllers(); await expect( - assetsContract.getERC20BalanceOf( - ERC20_UNI_ADDRESS, - TEST_ACCOUNT_PUBLIC_ADDRESS, - 'invalidNetworkClientId', - ), - ).rejects.toThrow('No custom network client was found'); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + async () => + await messenger.call( + `AssetsContractController:getERC20BalanceOf`, + ERC20_UNI_ADDRESS, + TEST_ACCOUNT_PUBLIC_ADDRESS, + 'invalidNetworkClientId', + ), + ).rejects.toThrow( + `No custom network client was found with the ID "invalidNetworkClientId".`, + ); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should throw when getting ERC-20 token decimal when networkClientId is invalid', async () => { - const { assetsContract, messenger } = await setupAssetContractControllers(); + const { messenger } = await setupAssetContractControllers(); await expect( - assetsContract.getERC20TokenDecimals( - ERC20_UNI_ADDRESS, - 'invalidNetworkClientId', - ), - ).rejects.toThrow('No custom network client was found'); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + async () => + await messenger.call( + `AssetsContractController:getERC20TokenDecimals`, + ERC20_UNI_ADDRESS, + 'invalidNetworkClientId', + ), + ).rejects.toThrow( + `No custom network client was found with the ID "invalidNetworkClientId".`, + ); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should get balance of ERC-20 token contract correctly', async () => { - const { assetsContract, messenger, networkClientConfiguration } = + const { messenger, networkClientConfiguration } = await setupAssetContractControllers(); mockNetworkWithDefaultChainId({ networkClientConfiguration, @@ -81,23 +89,25 @@ describe('AssetsContractController with NetworkClientId', () => { }, ], }); - const UNIBalance = await assetsContract.getERC20BalanceOf( + const UNIBalance = await messenger.call( + `AssetsContractController:getERC20BalanceOf`, ERC20_UNI_ADDRESS, TEST_ACCOUNT_PUBLIC_ADDRESS, 'mainnet', ); - const UNINoBalance = await assetsContract.getERC20BalanceOf( + const UNINoBalance = await messenger.call( + `AssetsContractController:getERC20BalanceOf`, ERC20_UNI_ADDRESS, '0x202637dAAEfbd7f131f90338a4A6c69F6Cd5CE91', 'mainnet', ); expect(UNIBalance.toString(16)).not.toBe('0'); expect(UNINoBalance.toString(16)).toBe('0'); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should get ERC-721 NFT tokenId correctly', async () => { - const { assetsContract, messenger, networkClientConfiguration } = + const { messenger, networkClientConfiguration } = await setupAssetContractControllers(); mockNetworkWithDefaultChainId({ networkClientConfiguration, @@ -120,45 +130,50 @@ describe('AssetsContractController with NetworkClientId', () => { }, ], }); - const tokenId = await assetsContract.getERC721NftTokenId( + const tokenId = await messenger.call( + `AssetsContractController:getERC721NftTokenId`, ERC721_GODS_ADDRESS, '0x9a90bd8d1149a88b42a99cf62215ad955d6f498a', 0, 'mainnet', ); expect(tokenId).not.toBe(0); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should throw error when getting ERC-721 token standard and details when networkClientId is invalid', async () => { - const { assetsContract, messenger } = await setupAssetContractControllers(); + const { messenger } = await setupAssetContractControllers(); await expect( - assetsContract.getTokenStandardAndDetails( - ERC20_UNI_ADDRESS, - TEST_ACCOUNT_PUBLIC_ADDRESS, - undefined, - 'invalidNetworkClientId', - ), + async () => + await messenger.call( + `AssetsContractController:getTokenStandardAndDetails`, + ERC20_UNI_ADDRESS, + TEST_ACCOUNT_PUBLIC_ADDRESS, + undefined, + 'invalidNetworkClientId', + ), ).rejects.toThrow('No custom network client was found'); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should throw contract standard error when getting ERC-20 token standard and details when provided with invalid ERC-20 address', async () => { - const { assetsContract, messenger } = await setupAssetContractControllers(); + const { messenger } = await setupAssetContractControllers(); const error = 'Unable to determine contract standard'; await expect( - assetsContract.getTokenStandardAndDetails( - 'BaDeRc20AdDrEsS', - TEST_ACCOUNT_PUBLIC_ADDRESS, - undefined, - 'mainnet', - ), + async () => + await messenger.call( + `AssetsContractController:getTokenStandardAndDetails`, + 'BaDeRc20AdDrEsS', + TEST_ACCOUNT_PUBLIC_ADDRESS, + undefined, + 'mainnet', + ), ).rejects.toThrow(error); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should get ERC-721 token standard and details', async () => { - const { assetsContract, messenger, networkClientConfiguration } = + const { messenger, networkClientConfiguration } = await setupAssetContractControllers(); mockNetworkWithDefaultChainId({ networkClientConfiguration, @@ -213,18 +228,19 @@ describe('AssetsContractController with NetworkClientId', () => { }, ], }); - const standardAndDetails = await assetsContract.getTokenStandardAndDetails( + const standardAndDetails = await messenger.call( + `AssetsContractController:getTokenStandardAndDetails`, ERC721_GODS_ADDRESS, TEST_ACCOUNT_PUBLIC_ADDRESS, undefined, 'mainnet', ); expect(standardAndDetails.standard).toBe('ERC721'); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should get ERC-1155 token standard and details', async () => { - const { assetsContract, messenger, networkClientConfiguration } = + const { messenger, networkClientConfiguration } = await setupAssetContractControllers(); mockNetworkWithDefaultChainId({ networkClientConfiguration, @@ -263,18 +279,19 @@ describe('AssetsContractController with NetworkClientId', () => { }, ], }); - const standardAndDetails = await assetsContract.getTokenStandardAndDetails( + const standardAndDetails = await messenger.call( + `AssetsContractController:getTokenStandardAndDetails`, ERC1155_ADDRESS, TEST_ACCOUNT_PUBLIC_ADDRESS, undefined, 'mainnet', ); expect(standardAndDetails.standard).toBe('ERC1155'); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should get ERC-20 token standard and details', async () => { - const { assetsContract, messenger, networkClientConfiguration } = + const { messenger, networkClientConfiguration } = await setupAssetContractControllers(); mockNetworkWithDefaultChainId({ networkClientConfiguration, @@ -361,18 +378,19 @@ describe('AssetsContractController with NetworkClientId', () => { }, ], }); - const standardAndDetails = await assetsContract.getTokenStandardAndDetails( + const standardAndDetails = await messenger.call( + `AssetsContractController:getTokenStandardAndDetails`, ERC20_UNI_ADDRESS, TEST_ACCOUNT_PUBLIC_ADDRESS, undefined, 'mainnet', ); expect(standardAndDetails.standard).toBe('ERC20'); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should get ERC-721 NFT tokenURI correctly', async () => { - const { assetsContract, messenger, networkClientConfiguration } = + const { messenger, networkClientConfiguration } = await setupAssetContractControllers(); mockNetworkWithDefaultChainId({ networkClientConfiguration, @@ -411,17 +429,18 @@ describe('AssetsContractController with NetworkClientId', () => { }, ], }); - const tokenId = await assetsContract.getERC721TokenURI( + const tokenId = await messenger.call( + `AssetsContractController:getERC721TokenURI`, ERC721_GODS_ADDRESS, '0', 'mainnet', ); expect(tokenId).toBe('https://api.godsunchained.com/card/0'); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should not throw an error when address given is does not support NFT Metadata interface', async () => { - const { assetsContract, messenger, networkClientConfiguration } = + const { messenger, networkClientConfiguration } = await setupAssetContractControllers(); mockNetworkWithDefaultChainId({ networkClientConfiguration, @@ -464,7 +483,8 @@ describe('AssetsContractController with NetworkClientId', () => { .mockImplementationOnce(() => { /**/ }); - const uri = await assetsContract.getERC721TokenURI( + const uri = await messenger.call( + `AssetsContractController:getERC721TokenURI`, '0x0000000000000000000000000000000000000000', '0', 'mainnet', @@ -474,11 +494,11 @@ describe('AssetsContractController with NetworkClientId', () => { expect(errorLogSpy.mock.calls).toContainEqual([ 'Contract does not support ERC721 metadata interface.', ]); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should get ERC-721 NFT name', async () => { - const { assetsContract, messenger, networkClientConfiguration } = + const { messenger, networkClientConfiguration } = await setupAssetContractControllers(); mockNetworkWithDefaultChainId({ networkClientConfiguration, @@ -501,16 +521,17 @@ describe('AssetsContractController with NetworkClientId', () => { }, ], }); - const name = await assetsContract.getERC721AssetName( + const name = await messenger.call( + `AssetsContractController:getERC721AssetName`, ERC721_GODS_ADDRESS, 'mainnet', ); expect(name).toBe('Gods Unchained'); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should get ERC-721 NFT symbol', async () => { - const { assetsContract, messenger, networkClientConfiguration } = + const { messenger, networkClientConfiguration } = await setupAssetContractControllers(); mockNetworkWithDefaultChainId({ networkClientConfiguration, @@ -533,27 +554,30 @@ describe('AssetsContractController with NetworkClientId', () => { }, ], }); - const symbol = await assetsContract.getERC721AssetSymbol( + const symbol = await messenger.call( + `AssetsContractController:getERC721AssetSymbol`, ERC721_GODS_ADDRESS, 'mainnet', ); expect(symbol).toBe('GODS'); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should throw error when getting ERC-721 NFT symbol when networkClientId is invalid', async () => { - const { assetsContract, messenger } = await setupAssetContractControllers(); + const { messenger } = await setupAssetContractControllers(); await expect( - assetsContract.getERC721AssetSymbol( - ERC721_GODS_ADDRESS, - 'invalidNetworkClientId', - ), + async () => + await messenger.call( + `AssetsContractController:getERC721AssetSymbol`, + ERC721_GODS_ADDRESS, + 'invalidNetworkClientId', + ), ).rejects.toThrow('No custom network client was found'); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should get ERC-20 token decimals', async () => { - const { assetsContract, messenger, networkClientConfiguration } = + const { messenger, networkClientConfiguration } = await setupAssetContractControllers(); mockNetworkWithDefaultChainId({ networkClientConfiguration, @@ -576,16 +600,17 @@ describe('AssetsContractController with NetworkClientId', () => { }, ], }); - const decimals = await assetsContract.getERC20TokenDecimals( + const decimals = await messenger.call( + `AssetsContractController:getERC20TokenDecimals`, ERC20_SAI_ADDRESS, 'mainnet', ); expect(Number(decimals)).toBe(18); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should get ERC-20 token name', async () => { - const { assetsContract, messenger, networkClientConfiguration } = + const { messenger, networkClientConfiguration } = await setupAssetContractControllers(); mockNetworkWithDefaultChainId({ networkClientConfiguration, @@ -609,17 +634,18 @@ describe('AssetsContractController with NetworkClientId', () => { ], }); - const name = await assetsContract.getERC20TokenName( + const name = await messenger.call( + `AssetsContractController:getERC20TokenName`, ERC20_DAI_ADDRESS, 'mainnet', ); expect(name).toBe('Dai Stablecoin'); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should get ERC-721 NFT ownership', async () => { - const { assetsContract, messenger, networkClientConfiguration } = + const { messenger, networkClientConfiguration } = await setupAssetContractControllers(); mockNetworkWithDefaultChainId({ networkClientConfiguration, @@ -642,29 +668,32 @@ describe('AssetsContractController with NetworkClientId', () => { }, ], }); - const tokenId = await assetsContract.getERC721OwnerOf( + const tokenId = await messenger.call( + `AssetsContractController:getERC721OwnerOf`, ERC721_GODS_ADDRESS, '148332', 'mainnet', ); expect(tokenId).not.toBe(''); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should throw error when getting ERC-721 NFT ownership using networkClientId that is invalid', async () => { - const { assetsContract, messenger } = await setupAssetContractControllers(); + const { messenger } = await setupAssetContractControllers(); await expect( - assetsContract.getERC721OwnerOf( - ERC721_GODS_ADDRESS, - '148332', - 'invalidNetworkClientId', - ), + async () => + await messenger.call( + `AssetsContractController:getERC721OwnerOf`, + ERC721_GODS_ADDRESS, + '148332', + 'invalidNetworkClientId', + ), ).rejects.toThrow('No custom network client was found'); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should get balance of ERC-20 token in a single call on network with token detection support', async () => { - const { assetsContract, messenger, networkClientConfiguration } = + const { messenger, networkClientConfiguration } = await setupAssetContractControllers(); mockNetworkWithDefaultChainId({ networkClientConfiguration, @@ -687,17 +716,18 @@ describe('AssetsContractController with NetworkClientId', () => { }, ], }); - const balances = await assetsContract.getBalancesInSingleCall( + const balances = await messenger.call( + `AssetsContractController:getBalancesInSingleCall`, ERC20_SAI_ADDRESS, [ERC20_SAI_ADDRESS], 'mainnet', ); expect(balances[ERC20_SAI_ADDRESS]).toBeDefined(); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should not have balance in a single call after switching to network without token detection support', async () => { - const { assetsContract, messenger, networkClientConfiguration } = + const { messenger, networkClientConfiguration } = await setupAssetContractControllers(); mockNetworkWithDefaultChainId({ networkClientConfiguration, @@ -751,39 +781,43 @@ describe('AssetsContractController with NetworkClientId', () => { ], }); - const balances = await assetsContract.getBalancesInSingleCall( + const balances = await messenger.call( + `AssetsContractController:getBalancesInSingleCall`, ERC20_SAI_ADDRESS, [ERC20_SAI_ADDRESS], 'mainnet', ); expect(balances[ERC20_SAI_ADDRESS]).toBeDefined(); - const noBalances = await assetsContract.getBalancesInSingleCall( + const noBalances = await messenger.call( + `AssetsContractController:getBalancesInSingleCall`, ERC20_SAI_ADDRESS, [ERC20_SAI_ADDRESS], 'sepolia', ); expect(noBalances).toStrictEqual({}); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should throw error when transferring single ERC-1155 when networkClientId is invalid', async () => { - const { assetsContract, messenger } = await setupAssetContractControllers(); + const { messenger } = await setupAssetContractControllers(); await expect( - assetsContract.transferSingleERC1155( - ERC1155_ADDRESS, - TEST_ACCOUNT_PUBLIC_ADDRESS, - TEST_ACCOUNT_PUBLIC_ADDRESS, - ERC1155_ID, - '1', - 'invalidNetworkClientId', - ), + async () => + await messenger.call( + `AssetsContractController:transferSingleERC1155`, + ERC1155_ADDRESS, + TEST_ACCOUNT_PUBLIC_ADDRESS, + TEST_ACCOUNT_PUBLIC_ADDRESS, + ERC1155_ID, + '1', + 'invalidNetworkClientId', + ), ).rejects.toThrow('No custom network client was found'); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should get the balance of a ERC-1155 NFT for a given address', async () => { - const { assetsContract, messenger, networkClientConfiguration } = + const { messenger, networkClientConfiguration } = await setupAssetContractControllers(); mockNetworkWithDefaultChainId({ networkClientConfiguration, @@ -806,31 +840,34 @@ describe('AssetsContractController with NetworkClientId', () => { }, ], }); - const balance = await assetsContract.getERC1155BalanceOf( + const balance = await messenger.call( + `AssetsContractController:getERC1155BalanceOf`, TEST_ACCOUNT_PUBLIC_ADDRESS, ERC1155_ADDRESS, ERC1155_ID, 'mainnet', ); expect(Number(balance)).toBeGreaterThan(0); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should throw error when getting the balance of a ERC-1155 NFT when networkClientId is invalid', async () => { - const { assetsContract, messenger } = await setupAssetContractControllers(); + const { messenger } = await setupAssetContractControllers(); await expect( - assetsContract.getERC1155BalanceOf( - TEST_ACCOUNT_PUBLIC_ADDRESS, - ERC1155_ADDRESS, - ERC1155_ID, - 'invalidNetworkClientId', - ), + async () => + await messenger.call( + `AssetsContractController:getERC1155BalanceOf`, + TEST_ACCOUNT_PUBLIC_ADDRESS, + ERC1155_ADDRESS, + ERC1155_ID, + 'invalidNetworkClientId', + ), ).rejects.toThrow('No custom network client was found'); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); it('should get the URI of a ERC-1155 NFT', async () => { - const { assetsContract, messenger, networkClientConfiguration } = + const { messenger, networkClientConfiguration } = await setupAssetContractControllers(); mockNetworkWithDefaultChainId({ networkClientConfiguration, @@ -854,12 +891,13 @@ describe('AssetsContractController with NetworkClientId', () => { ], }); const expectedUri = `https://api.opensea.io/api/v1/metadata/${ERC1155_ADDRESS}/0x{id}`; - const uri = await assetsContract.getERC1155TokenURI( + const uri = await messenger.call( + `AssetsContractController:getERC1155TokenURI`, ERC1155_ADDRESS, ERC1155_ID, 'mainnet', ); expect(uri.toLowerCase()).toStrictEqual(expectedUri); - messenger.clearEventSubscriptions('NetworkController:stateChange'); + messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); }); diff --git a/packages/assets-controllers/src/NftController.test.ts b/packages/assets-controllers/src/NftController.test.ts index dcc528315e..2c2f286df0 100644 --- a/packages/assets-controllers/src/NftController.test.ts +++ b/packages/assets-controllers/src/NftController.test.ts @@ -3,13 +3,8 @@ import type { AccountsControllerGetAccountAction, AccountsControllerGetSelectedAccountAction, AccountsControllerSelectedAccountChangeEvent, - AccountsControllerSelectedEvmAccountChangeEvent, } from '@metamask/accounts-controller'; -import type { - AddApprovalRequest, - ApprovalStateChange, - ApprovalControllerMessenger, -} from '@metamask/approval-controller'; +import type { ApprovalControllerMessenger } from '@metamask/approval-controller'; import { ApprovalController } from '@metamask/approval-controller'; import { ControllerMessenger } from '@metamask/base-controller'; import { @@ -29,11 +24,8 @@ import type { InternalAccount } from '@metamask/keyring-api'; import type { NetworkClientConfiguration, NetworkClientId, - NetworkControllerGetNetworkClientByIdAction, - NetworkControllerNetworkDidChangeEvent, } from '@metamask/network-controller'; import { defaultState as defaultNetworkState } from '@metamask/network-controller'; -import type { PreferencesControllerStateChangeEvent } from '@metamask/preferences-controller'; import { getDefaultPreferencesState, type PreferencesState, @@ -52,18 +44,24 @@ import { buildCustomNetworkClientConfiguration, buildMockGetNetworkClientById, } from '../../network-controller/tests/helpers'; +import type { + AssetsContractControllerGetERC1155BalanceOfAction, + AssetsContractControllerGetERC1155TokenURIAction, + AssetsContractControllerGetERC721AssetNameAction, + AssetsContractControllerGetERC721AssetSymbolAction, + AssetsContractControllerGetERC721OwnerOfAction, + AssetsContractControllerGetERC721TokenURIAction, +} from './AssetsContractController'; import { getFormattedIpfsUrl } from './assetsUtil'; import { Source } from './constants'; import type { Nft, NftControllerState, NftControllerMessenger, + AllowedActions as NftControllerAllowedActions, + AllowedEvents as NftControllerAllowedEvents, } from './NftController'; -import { - NftController, - type AllowedActions, - type AllowedEvents, -} from './NftController'; +import { NftController } from './NftController'; const CRYPTOPUNK_ADDRESS = '0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB'; const ERC721_KUDOSADDRESS = '0x2aEa4Add166EBf38b63d09a75dE1a7b94Aa24163'; @@ -98,17 +96,6 @@ const GOERLI = { ticker: NetworksTicker.goerli, }; -type ApprovalActions = - | AddApprovalRequest - | AccountsControllerGetAccountAction - | AccountsControllerGetSelectedAccountAction - | NetworkControllerGetNetworkClientByIdAction; -type ApprovalEvents = - | ApprovalStateChange - | PreferencesControllerStateChangeEvent - | NetworkControllerNetworkDidChangeEvent - | AccountsControllerSelectedEvmAccountChangeEvent; - const controllerName = 'NftController' as const; // Mock out detectNetwork function for cleaner tests, Ethers calls this a bunch of times because the Web3Provider is paranoid. @@ -142,18 +129,74 @@ jest.mock('uuid', () => { * * @param args - Arguments to this function. * @param args.options - Controller options. + * @param args.getERC721AssetName - Used to construct mock versions of the + * `AssetsContractController:getERC721AssetName` action. + * @param args.getERC721AssetSymbol - Used to construct mock versions of the + * `AssetsContractController:getERC721AssetSymbol` action. + * @param args.getERC721TokenURI - Used to construct mock versions of the + * `AssetsContractController:getERC721TokenURI` action. + * @param args.getERC721OwnerOf - Used to construct mock versions of the + * `AssetsContractController:getERC721OwnerOf` action. + * @param args.getERC1155BalanceOf - Used to construct mock versions of the + * `AssetsContractController:getERC1155BalanceOf` action. + * @param args.getERC1155TokenURI - Used to construct mock versions of the + * `AssetsContractController:getERC1155TokenURI` action. * @param args.mockNetworkClientConfigurationsByNetworkClientId - Used to construct * mock versions of network clients and ultimately mock the * `NetworkController:getNetworkClientById` action. + * @param args.getAccount - Used to construct mock versions of the + * `AccountsController:getAccount` action. + * @param args.getSelectedAccount - Used to construct mock versions of the + * `AccountsController:getSelectedAccount` action. * @param args.defaultSelectedAccount - The default selected account to use in * @returns A collection of test controllers and mocks. */ function setupController({ options = {}, + getERC721AssetName, + getERC721AssetSymbol, + getERC721TokenURI, + getERC721OwnerOf, + getERC1155BalanceOf, + getERC1155TokenURI, + getAccount, + getSelectedAccount, mockNetworkClientConfigurationsByNetworkClientId = {}, defaultSelectedAccount = OWNER_ACCOUNT, }: { options?: Partial[0]>; + getERC721AssetName?: jest.Mock< + ReturnType, + Parameters + >; + getERC721AssetSymbol?: jest.Mock< + ReturnType, + Parameters + >; + getERC721TokenURI?: jest.Mock< + ReturnType, + Parameters + >; + getERC721OwnerOf?: jest.Mock< + ReturnType, + Parameters + >; + getERC1155BalanceOf?: jest.Mock< + ReturnType, + Parameters + >; + getERC1155TokenURI?: jest.Mock< + ReturnType, + Parameters + >; + getAccount?: jest.Mock< + ReturnType, + Parameters | [null] + >; + getSelectedAccount?: jest.Mock< + ReturnType, + Parameters + >; mockNetworkClientConfigurationsByNetworkClientId?: Record< NetworkClientId, NetworkClientConfiguration @@ -162,10 +205,10 @@ function setupController({ } = {}) { const messenger = new ControllerMessenger< | ExtractAvailableAction - | AllowedActions + | NftControllerAllowedActions | ExtractAvailableAction, | ExtractAvailableEvent - | AllowedEvents + | NftControllerAllowedEvents | ExtractAvailableEvent | AccountsControllerSelectedAccountChangeEvent >(); @@ -178,24 +221,86 @@ function setupController({ getNetworkClientById, ); - const mockGetAccount = jest - .fn() - .mockReturnValue(defaultSelectedAccount ?? OWNER_ACCOUNT); - + const mockGetAccount = + getAccount ?? jest.fn().mockReturnValue(defaultSelectedAccount); messenger.registerActionHandler( 'AccountsController:getAccount', mockGetAccount, ); - const mockGetSelectedAccount = jest - .fn() - .mockReturnValue(defaultSelectedAccount ?? OWNER_ACCOUNT); - + const mockGetSelectedAccount = + getSelectedAccount ?? jest.fn().mockReturnValue(defaultSelectedAccount); messenger.registerActionHandler( 'AccountsController:getSelectedAccount', mockGetSelectedAccount, ); + const mockGetERC721AssetName = + getERC721AssetName ?? + jest.fn< + ReturnType, + Parameters + >(); + messenger.registerActionHandler( + 'AssetsContractController:getERC721AssetName', + mockGetERC721AssetName, + ); + + const mockGetERC721AssetSymbol = + getERC721AssetSymbol ?? + jest.fn< + ReturnType, + Parameters + >(); + messenger.registerActionHandler( + 'AssetsContractController:getERC721AssetSymbol', + mockGetERC721AssetSymbol, + ); + + const mockGetERC721TokenURI = + getERC721TokenURI ?? + jest.fn< + ReturnType, + Parameters + >(); + messenger.registerActionHandler( + 'AssetsContractController:getERC721TokenURI', + mockGetERC721TokenURI, + ); + + const mockGetERC721OwnerOf = + getERC721OwnerOf ?? + jest.fn< + ReturnType, + Parameters + >(); + messenger.registerActionHandler( + 'AssetsContractController:getERC721OwnerOf', + mockGetERC721OwnerOf, + ); + + const mockGetERC1155BalanceOf = + getERC1155BalanceOf ?? + jest.fn< + ReturnType, + Parameters + >(); + messenger.registerActionHandler( + 'AssetsContractController:getERC1155BalanceOf', + mockGetERC1155BalanceOf, + ); + + const mockGetERC1155TokenURI = + getERC1155TokenURI ?? + jest.fn< + ReturnType, + Parameters + >(); + messenger.registerActionHandler( + 'AssetsContractController:getERC1155TokenURI', + mockGetERC1155TokenURI, + ); + const approvalControllerMessenger = messenger.getRestricted({ name: 'ApprovalController', allowedActions: [], @@ -207,25 +312,21 @@ function setupController({ showApprovalRequest: jest.fn(), }); - const nftControllerMessenger = messenger.getRestricted< - typeof controllerName, - ApprovalActions['type'], - Extract< - ApprovalEvents, - | PreferencesControllerStateChangeEvent - | AccountsControllerSelectedEvmAccountChangeEvent - | NetworkControllerNetworkDidChangeEvent - >['type'] - >({ + const nftControllerMessenger = messenger.getRestricted({ name: controllerName, allowedActions: [ 'ApprovalController:addRequest', 'AccountsController:getSelectedAccount', 'AccountsController:getAccount', 'NetworkController:getNetworkClientById', + 'AssetsContractController:getERC721AssetName', + 'AssetsContractController:getERC721AssetSymbol', + 'AssetsContractController:getERC721TokenURI', + 'AssetsContractController:getERC721OwnerOf', + 'AssetsContractController:getERC1155BalanceOf', + 'AssetsContractController:getERC1155TokenURI', ], allowedEvents: [ - // @ts-expect-error - Adding this for test 'AccountsController:selectedAccountChange', 'AccountsController:selectedEvmAccountChange', 'PreferencesController:stateChange', @@ -235,13 +336,8 @@ function setupController({ const nftController = new NftController({ chainId: ChainId.mainnet, - getERC721AssetName: jest.fn(), - getERC721AssetSymbol: jest.fn(), - getERC721TokenURI: jest.fn(), - getERC721OwnerOf: jest.fn(), - getERC1155BalanceOf: jest.fn(), - getERC1155TokenURI: jest.fn(), onNftAdded: jest.fn(), + // @ts-expect-error - Added incompatible event `AccountsController:selectedAccountChange` to allowlist for testing purposes messenger: nftControllerMessenger, ...options, }); @@ -286,6 +382,12 @@ function setupController({ triggerSelectedAccountChange, mockGetAccount, mockGetSelectedAccount, + mockGetERC1155BalanceOf, + mockGetERC1155TokenURI, + mockGetERC721AssetName, + mockGetERC721AssetSymbol, + mockGetERC721OwnerOf, + mockGetERC721TokenURI, }; } @@ -407,12 +509,10 @@ describe('NftController', () => { }), ); const { nftController } = setupController({ - options: { - getERC721TokenURI: jest - .fn() - .mockImplementation(() => 'https://testtokenuri.com'), - getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), - }, + getERC721TokenURI: jest + .fn() + .mockImplementation(() => 'https://testtokenuri.com'), + getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -484,9 +584,7 @@ describe('NftController', () => { it('should error if the user does not own the suggested ERC721 NFT', async function () { const { nftController, messenger } = setupController({ - options: { - getERC721OwnerOf: jest.fn().mockImplementation(() => '0x12345abcefg'), - }, + getERC721OwnerOf: jest.fn().mockImplementation(() => '0x12345abcefg'), }); const callActionSpy = jest.spyOn(messenger, 'call'); @@ -519,9 +617,7 @@ describe('NftController', () => { it('should error if the user does not own the suggested ERC1155 NFT', async function () { const { nftController, messenger } = setupController({ - options: { - getERC1155BalanceOf: jest.fn().mockImplementation(() => new BN(0)), - }, + getERC1155BalanceOf: jest.fn().mockImplementation(() => new BN(0)), }); const callActionSpy = jest.spyOn(messenger, 'call'); @@ -530,7 +626,11 @@ describe('NftController', () => { nftController.watchNft(ERC1155_NFT, ERC1155, 'https://test-dapp.com'), ).rejects.toThrow('Suggested NFT is not owned by the selected account'); // First call is to get InternalAccount - expect(callActionSpy).toHaveBeenCalledTimes(1); + expect(callActionSpy).toHaveBeenNthCalledWith( + 1, + 'AccountsController:getAccount', + expect.any(String), + ); }); it('should handle ERC721 type and add pending request to ApprovalController with the OpenSea API disabled and IPFS gateway enabled', async function () { @@ -550,12 +650,13 @@ describe('NftController', () => { triggerPreferencesStateChange, triggerSelectedAccountChange, } = setupController({ - options: { - getERC721TokenURI: jest - .fn() - .mockImplementation(() => 'https://testtokenuri.com'), - getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), - }, + getAccount: jest.fn().mockReturnValue(OWNER_ACCOUNT), + getERC721OwnerOf: jest.fn().mockResolvedValue(OWNER_ADDRESS), + getERC721TokenURI: jest + .fn() + .mockResolvedValue('https://testtokenuri.com'), + getERC721AssetName: jest.fn().mockResolvedValue('testERC721Name'), + getERC721AssetSymbol: jest.fn().mockResolvedValue('testERC721Symbol'), }); triggerSelectedAccountChange(OWNER_ACCOUNT); triggerPreferencesStateChange({ @@ -572,15 +673,25 @@ describe('NftController', () => { const callActionSpy = jest .spyOn(messenger, 'call') + // 1. `AccountsController:getAccount` .mockReturnValueOnce(OWNER_ACCOUNT) + // 2. `AssetsContractController:getERC721OwnerOf` + .mockResolvedValueOnce(OWNER_ADDRESS) + // 3. `AssetsContractController:getERC721TokenURI` + .mockResolvedValueOnce('https://testtokenuri.com') + // 4. `ApprovalController:addRequest` .mockResolvedValueOnce({}) - .mockReturnValueOnce(OWNER_ACCOUNT); + // 5. `AccountsController:getAccount` + .mockReturnValueOnce(OWNER_ACCOUNT) + // 6. `AssetsContractController:getERC721AssetName` + .mockResolvedValueOnce('testERC721Name') + // 7. `AssetsContractController:getERC721AssetSymbol` + .mockResolvedValueOnce('testERC721Symbol'); await nftController.watchNft(ERC721_NFT, ERC721, 'https://test-dapp.com'); - // First call is getInternalAccount. Second call is the approval request. - expect(callActionSpy).toHaveBeenCalledTimes(3); + expect(callActionSpy).toHaveBeenCalledTimes(7); expect(callActionSpy).toHaveBeenNthCalledWith( - 2, + 4, 'ApprovalController:addRequest', { id: requestId, @@ -621,12 +732,13 @@ describe('NftController', () => { triggerPreferencesStateChange, triggerSelectedAccountChange, } = setupController({ - options: { - getERC721TokenURI: jest - .fn() - .mockImplementation(() => 'https://testtokenuri.com'), - getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), - }, + getAccount: jest.fn().mockReturnValue(OWNER_ACCOUNT), + getERC721OwnerOf: jest.fn().mockResolvedValue(OWNER_ADDRESS), + getERC721TokenURI: jest + .fn() + .mockResolvedValue('https://testtokenuri.com'), + getERC721AssetName: jest.fn().mockResolvedValue('testERC721Name'), + getERC721AssetSymbol: jest.fn().mockResolvedValue('testERC721Symbol'), }); triggerSelectedAccountChange(OWNER_ACCOUNT); triggerPreferencesStateChange({ @@ -643,15 +755,25 @@ describe('NftController', () => { const callActionSpy = jest .spyOn(messenger, 'call') + // 1. `AccountsController:getAccount` .mockReturnValueOnce(OWNER_ACCOUNT) + // 2. `AssetsContractController:getERC721OwnerOf` + .mockResolvedValueOnce(OWNER_ADDRESS) + // 3. `AssetsContractController:getERC721TokenURI` + .mockResolvedValueOnce('https://testtokenuri.com') + // 4. `ApprovalController:addRequest` .mockResolvedValueOnce({}) - .mockReturnValueOnce(OWNER_ACCOUNT); + // 5. `AccountsController:getAccount` + .mockReturnValueOnce(OWNER_ACCOUNT) + // 6. `AssetsContractController:getERC721AssetName` + .mockResolvedValueOnce('testERC721Name') + // 7. `AssetsContractController:getERC721AssetSymbol` + .mockResolvedValueOnce('testERC721Symbol'); await nftController.watchNft(ERC721_NFT, ERC721, 'https://test-dapp.com'); - // First call is getInternalAccount. Second call is the approval request. - expect(callActionSpy).toHaveBeenCalledTimes(3); + expect(callActionSpy).toHaveBeenCalledTimes(7); expect(callActionSpy).toHaveBeenNthCalledWith( - 2, + 4, 'ApprovalController:addRequest', { id: requestId, @@ -692,12 +814,13 @@ describe('NftController', () => { triggerPreferencesStateChange, triggerSelectedAccountChange, } = setupController({ - options: { - getERC721TokenURI: jest - .fn() - .mockImplementation(() => 'ipfs://testtokenuri.com'), - getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), - }, + getAccount: jest.fn().mockReturnValue(OWNER_ACCOUNT), + getERC721OwnerOf: jest.fn().mockResolvedValue(OWNER_ADDRESS), + getERC721TokenURI: jest + .fn() + .mockResolvedValue('https://testtokenuri.com'), + getERC721AssetName: jest.fn().mockResolvedValue('testERC721Name'), + getERC721AssetSymbol: jest.fn().mockResolvedValue('testERC721Symbol'), }); triggerSelectedAccountChange(OWNER_ACCOUNT); triggerPreferencesStateChange({ @@ -714,15 +837,25 @@ describe('NftController', () => { const callActionSpy = jest .spyOn(messenger, 'call') + // 1. `AccountsController:getAccount` .mockReturnValueOnce(OWNER_ACCOUNT) + // 2. `AssetsContractController:getERC721OwnerOf` + .mockResolvedValueOnce(OWNER_ADDRESS) + // 3. `AssetsContractController:getERC721TokenURI` + .mockResolvedValueOnce('https://testtokenuri.com') + // 4. `ApprovalController:addRequest` .mockResolvedValueOnce({}) - .mockReturnValueOnce(OWNER_ACCOUNT); + // 5. `AccountsController:getAccount` + .mockReturnValueOnce(OWNER_ACCOUNT) + // 6. `AssetsContractController:getERC721AssetName` + .mockResolvedValueOnce('testERC721Name') + // 7. `AssetsContractController:getERC721AssetSymbol` + .mockResolvedValueOnce('testERC721Symbol'); await nftController.watchNft(ERC721_NFT, ERC721, 'https://test-dapp.com'); - // First call is getInternalAccount. Second call is the approval request. - expect(callActionSpy).toHaveBeenCalledTimes(3); + expect(callActionSpy).toHaveBeenCalledTimes(7); expect(callActionSpy).toHaveBeenNthCalledWith( - 2, + 4, 'ApprovalController:addRequest', { id: requestId, @@ -763,12 +896,13 @@ describe('NftController', () => { triggerPreferencesStateChange, triggerSelectedAccountChange, } = setupController({ - options: { - getERC721TokenURI: jest - .fn() - .mockImplementation(() => 'ipfs://testtokenuri.com'), - getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), - }, + getAccount: jest.fn().mockReturnValue(OWNER_ACCOUNT), + getERC721OwnerOf: jest.fn().mockResolvedValue(OWNER_ADDRESS), + getERC721TokenURI: jest + .fn() + .mockResolvedValue('https://testtokenuri.com'), + getERC721AssetName: jest.fn().mockResolvedValue('testERC721Name'), + getERC721AssetSymbol: jest.fn().mockResolvedValue('testERC721Symbol'), }); triggerSelectedAccountChange(OWNER_ACCOUNT); @@ -786,15 +920,25 @@ describe('NftController', () => { const callActionSpy = jest .spyOn(messenger, 'call') + // 1. `AccountsController:getAccount` .mockReturnValueOnce(OWNER_ACCOUNT) + // 2. `AssetsContractController:getERC721OwnerOf` + .mockResolvedValueOnce(OWNER_ADDRESS) + // 3. `AssetsContractController:getERC721TokenURI` + .mockResolvedValueOnce('https://testtokenuri.com') + // 4. `ApprovalController:addRequest` .mockResolvedValueOnce({}) - .mockReturnValueOnce(OWNER_ACCOUNT); + // 5. `AccountsController:getAccount` + .mockReturnValueOnce(OWNER_ACCOUNT) + // 6. `AssetsContractController:getERC721AssetName` + .mockResolvedValueOnce('testERC721Name') + // 7. `AssetsContractController:getERC721AssetSymbol` + .mockResolvedValueOnce('testERC721Symbol'); await nftController.watchNft(ERC721_NFT, ERC721, 'https://test-dapp.com'); - // First call is getInternalAccount. Second call is the approval request. - expect(callActionSpy).toHaveBeenCalledTimes(3); + expect(callActionSpy).toHaveBeenCalledTimes(7); expect(callActionSpy).toHaveBeenNthCalledWith( - 2, + 4, 'ApprovalController:addRequest', { id: requestId, @@ -805,9 +949,9 @@ describe('NftController', () => { interactingAddress: OWNER_ADDRESS, asset: { ...ERC721_NFT, - description: null, - image: null, - name: null, + description: 'testERC721Description', + image: 'testERC721Image', + name: 'testERC721Name', standard: ERC721, }, }, @@ -836,15 +980,17 @@ describe('NftController', () => { triggerPreferencesStateChange, triggerSelectedAccountChange, } = setupController({ - options: { - getERC721TokenURI: jest - .fn() - .mockRejectedValue(new Error('Not an ERC721 contract')), - getERC1155TokenURI: jest - .fn() - .mockImplementation(() => 'https://testtokenuri.com'), - getERC1155BalanceOf: jest.fn().mockImplementation(() => new BN(1)), - }, + getAccount: jest.fn().mockReturnValue(OWNER_ACCOUNT), + getERC721OwnerOf: jest + .fn() + .mockRejectedValue(new Error('Not an ERC721 contract')), + getERC1155BalanceOf: jest.fn().mockResolvedValue(new BN(1)), + getERC721TokenURI: jest + .fn() + .mockRejectedValue(new Error('Not an ERC721 contract')), + getERC1155TokenURI: jest + .fn() + .mockResolvedValue('https://testtokenuri.com'), }); triggerSelectedAccountChange(OWNER_ACCOUNT); @@ -861,19 +1007,33 @@ describe('NftController', () => { const callActionSpy = jest .spyOn(messenger, 'call') + // 1. `AccountsController:getAccount` .mockReturnValueOnce(OWNER_ACCOUNT) + // 2. `AssetsContractController:getERC721OwnerOf` + .mockRejectedValueOnce(new Error('Not an ERC721 contract')) + // 3. `AssetsContractController:getERC1155BalanceOf` + .mockResolvedValueOnce(new BN(1)) + // 4. `AssetsContractController:getERC721TokenURI` + .mockRejectedValueOnce(new Error('Not an ERC721 contract')) + // 5. `AssetsContractController:getERC1155TokenURI` + .mockResolvedValueOnce('https://testtokenuri.com') + // 6. `ApprovalController:addRequest` .mockResolvedValueOnce({}) - .mockReturnValueOnce(OWNER_ACCOUNT); + // 7. `AccountsController:getAccount` + .mockReturnValueOnce(OWNER_ACCOUNT) + // 8. `AssetsContractController:getERC721AssetName` + .mockRejectedValueOnce(new Error('Not an ERC721 contract')) + // 9. `AssetsContractController:getERC721AssetSymbol` + .mockRejectedValueOnce(new Error('Not an ERC721 contract')); await nftController.watchNft( ERC1155_NFT, ERC1155, 'https://etherscan.io', ); - // First call is getInternalAccount. Second call is the approval request. - expect(callActionSpy).toHaveBeenCalledTimes(3); + expect(callActionSpy).toHaveBeenCalledTimes(9); expect(callActionSpy).toHaveBeenNthCalledWith( - 2, + 6, 'ApprovalController:addRequest', { id: requestId, @@ -911,15 +1071,17 @@ describe('NftController', () => { const { nftController, messenger, triggerPreferencesStateChange } = setupController({ - options: { - getERC721TokenURI: jest - .fn() - .mockRejectedValue(new Error('Not an ERC721 contract')), - getERC1155TokenURI: jest - .fn() - .mockImplementation(() => 'https://testtokenuri.com'), - getERC1155BalanceOf: jest.fn().mockImplementation(() => new BN(1)), - }, + getAccount: jest.fn().mockReturnValue(OWNER_ACCOUNT), + getERC721OwnerOf: jest + .fn() + .mockRejectedValue(new Error('Not an ERC721 contract')), + getERC1155BalanceOf: jest.fn().mockResolvedValue(new BN(1)), + getERC721TokenURI: jest + .fn() + .mockRejectedValue(new Error('Not an ERC721 contract')), + getERC1155TokenURI: jest + .fn() + .mockResolvedValue('https://testtokenuri.com'), }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), @@ -934,19 +1096,34 @@ describe('NftController', () => { const callActionSpy = jest .spyOn(messenger, 'call') + // 1. `AccountsController:getAccount` .mockReturnValueOnce(OWNER_ACCOUNT) + // 2. `AssetsContractController:getERC721OwnerOf` + .mockRejectedValueOnce(new Error('Not an ERC721 contract')) + // 3. `AssetsContractController:getERC1155BalanceOf` + .mockResolvedValueOnce(new BN(1)) + // 4. `AssetsContractController:getERC721TokenURI` + .mockRejectedValueOnce(new Error('Not an ERC721 contract')) + // 5. `AssetsContractController:getERC1155TokenURI` + .mockResolvedValueOnce('https://testtokenuri.com') + // 6. `ApprovalController:addRequest` .mockResolvedValueOnce({}) - .mockReturnValue(OWNER_ACCOUNT); + // 7. `AccountsController:getAccount` + .mockReturnValueOnce(OWNER_ACCOUNT) + // 8. `AssetsContractController:getERC721AssetName` + .mockRejectedValueOnce(new Error('Not an ERC721 contract')) + // 9. `AssetsContractController:getERC721AssetSymbol` + .mockRejectedValueOnce(new Error('Not an ERC721 contract')); await nftController.watchNft( ERC1155_NFT, ERC1155, 'https://etherscan.io', ); - // First call is getInternalAccount. Second call is the approval request. - expect(callActionSpy).toHaveBeenCalledTimes(3); + + expect(callActionSpy).toHaveBeenCalledTimes(9); expect(callActionSpy).toHaveBeenNthCalledWith( - 2, + 6, 'ApprovalController:addRequest', { id: requestId, @@ -990,20 +1167,12 @@ describe('NftController', () => { triggerPreferencesStateChange, triggerSelectedAccountChange, } = setupController({ - options: { - getERC721OwnerOf: jest - .fn() - .mockImplementation(() => SECOND_OWNER_ADDRESS), - getERC721TokenURI: jest - .fn() - .mockImplementation(() => 'https://testtokenuri.com'), - getERC721AssetName: jest - .fn() - .mockImplementation(() => 'testERC721Name'), - getERC721AssetSymbol: jest - .fn() - .mockImplementation(() => 'testERC721Symbol'), - }, + getERC721OwnerOf: jest.fn().mockResolvedValue(SECOND_OWNER_ADDRESS), + getERC721TokenURI: jest + .fn() + .mockResolvedValue('https://testtokenuri.com'), + getERC721AssetName: jest.fn().mockResolvedValue('testERC721Name'), + getERC721AssetSymbol: jest.fn().mockResolvedValue('testERC721Symbol'), }); const requestId = 'approval-request-id-1'; @@ -1096,18 +1265,12 @@ describe('NftController', () => { triggerSelectedAccountChange, changeNetwork, } = setupController({ - options: { - getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), - getERC721TokenURI: jest - .fn() - .mockImplementation(() => 'https://testtokenuri.com'), - getERC721AssetName: jest - .fn() - .mockImplementation(() => 'testERC721Name'), - getERC721AssetSymbol: jest - .fn() - .mockImplementation(() => 'testERC721Symbol'), - }, + getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + getERC721TokenURI: jest + .fn() + .mockResolvedValue('https://testtokenuri.com'), + getERC721AssetName: jest.fn().mockResolvedValue('testERC721Name'), + getERC721AssetSymbol: jest.fn().mockResolvedValue('testERC721Symbol'), }); const requestId = 'approval-request-id-1'; @@ -1203,12 +1366,16 @@ describe('NftController', () => { const requestId = 'approval-request-id-1'; (v4 as jest.Mock).mockImplementationOnce(() => requestId); - const result = nftController.watchNft( - ERC721_NFT, - ERC721, - 'https://test-dapp.com', - ); - await expect(result).rejects.toThrow( + // Awaiting `expect` as recommended by eslint results in this test stalling and timing out. + // eslint-disable-next-line @typescript-eslint/no-floating-promises, jest/valid-expect + expect( + async () => + await nftController.watchNft( + ERC721_NFT, + ERC721, + 'https://test-dapp.com', + ), + ).rejects.toThrow( "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.", ); }); @@ -1219,8 +1386,8 @@ describe('NftController', () => { const { nftController } = setupController({ options: { chainId: ChainId.mainnet, - getERC721AssetName: jest.fn().mockResolvedValue('Name'), }, + getERC721AssetName: jest.fn().mockResolvedValue('Name'), }); await nftController.addNft('0x01', '1', { @@ -1333,10 +1500,8 @@ describe('NftController', () => { triggerSelectedAccountChange, mockGetAccount, } = setupController({ - options: { - getERC721TokenURI: mockGetERC721TokenURI, - getERC1155TokenURI: mockGetERC1155TokenURI, - }, + getERC721TokenURI: mockGetERC721TokenURI, + getERC1155TokenURI: mockGetERC1155TokenURI, }); const firstAddress = '0x123'; const firstAccount = createMockInternalAccount({ address: firstAddress }); @@ -1387,7 +1552,6 @@ describe('NftController', () => { it('should update NFT if image is different', async () => { const { nftController } = setupController({ - options: {}, defaultSelectedAccount: OWNER_ACCOUNT, }); @@ -1583,7 +1747,6 @@ describe('NftController', () => { it('should not duplicate NFT nor NFT contract if already added', async () => { const { nftController } = setupController({ - options: {}, defaultSelectedAccount: OWNER_ACCOUNT, }); @@ -1620,14 +1783,12 @@ describe('NftController', () => { it('should add NFT and get information from NFT-API', async () => { const { nftController } = setupController({ - options: { - getERC721TokenURI: jest - .fn() - .mockRejectedValue(new Error('Not an ERC721 contract')), - getERC1155TokenURI: jest - .fn() - .mockRejectedValue(new Error('Not an ERC1155 contract')), - }, + getERC721TokenURI: jest + .fn() + .mockRejectedValue(new Error('Not an ERC721 contract')), + getERC1155TokenURI: jest + .fn() + .mockRejectedValue(new Error('Not an ERC1155 contract')), defaultSelectedAccount: OWNER_ACCOUNT, }); @@ -1700,15 +1861,13 @@ describe('NftController', () => { it('should add NFT erc721 and aggregate NFT data from both contract and NFT-API even if call to Get Collections fails', async () => { const { nftController } = setupController({ - options: { - getERC721AssetName: jest.fn().mockResolvedValue('KudosToken'), - getERC721AssetSymbol: jest.fn().mockResolvedValue('KDO'), - getERC721TokenURI: jest - .fn() - .mockResolvedValue( - 'https://ipfs.gitcoin.co:443/api/v0/cat/QmPmt6EAaioN78ECnW5oCL8v2YvVSpoBjLCjrXhhsAvoov', - ), - }, + getERC721AssetName: jest.fn().mockResolvedValue('KudosToken'), + getERC721AssetSymbol: jest.fn().mockResolvedValue('KDO'), + getERC721TokenURI: jest + .fn() + .mockResolvedValue( + 'https://ipfs.gitcoin.co:443/api/v0/cat/QmPmt6EAaioN78ECnW5oCL8v2YvVSpoBjLCjrXhhsAvoov', + ), defaultSelectedAccount: OWNER_ACCOUNT, }); nock(NFT_API_BASE_URL) @@ -1782,15 +1941,13 @@ describe('NftController', () => { }); it('should add NFT erc721 and aggregate NFT data from both contract and NFT-API when call to Get Collections succeeds', async () => { const { nftController } = setupController({ - options: { - getERC721AssetName: jest.fn().mockResolvedValue('KudosToken'), - getERC721AssetSymbol: jest.fn().mockResolvedValue('KDO'), - getERC721TokenURI: jest - .fn() - .mockResolvedValue( - 'https://ipfs.gitcoin.co:443/api/v0/cat/QmPmt6EAaioN78ECnW5oCL8v2YvVSpoBjLCjrXhhsAvoov', - ), - }, + getERC721AssetName: jest.fn().mockResolvedValue('KudosToken'), + getERC721AssetSymbol: jest.fn().mockResolvedValue('KDO'), + getERC721TokenURI: jest + .fn() + .mockResolvedValue( + 'https://ipfs.gitcoin.co:443/api/v0/cat/QmPmt6EAaioN78ECnW5oCL8v2YvVSpoBjLCjrXhhsAvoov', + ), defaultSelectedAccount: OWNER_ACCOUNT, }); nock(NFT_API_BASE_URL) @@ -1874,16 +2031,14 @@ describe('NftController', () => { it('should add NFT erc1155 and get NFT information from contract when NFT API call fail', async () => { const { nftController } = setupController({ - options: { - getERC721TokenURI: jest - .fn() - .mockRejectedValue(new Error('Not a 721 contract')), - getERC1155TokenURI: jest - .fn() - .mockResolvedValue( - 'https://api.opensea.io/api/v1/metadata/0x495f947276749Ce646f68AC8c248420045cb7b5e/0x{id}', - ), - }, + getERC721TokenURI: jest + .fn() + .mockRejectedValue(new Error('Not a 721 contract')), + getERC1155TokenURI: jest + .fn() + .mockResolvedValue( + 'https://api.opensea.io/api/v1/metadata/0x495f947276749Ce646f68AC8c248420045cb7b5e/0x{id}', + ), defaultSelectedAccount: OWNER_ACCOUNT, }); nock('https://api.opensea.io') @@ -1922,18 +2077,16 @@ describe('NftController', () => { it('should add NFT erc721 and get NFT information only from contract', async () => { const { nftController } = setupController({ - options: { - getERC721AssetName: jest.fn().mockResolvedValue('KudosToken'), - getERC721AssetSymbol: jest.fn().mockResolvedValue('KDO'), - getERC721TokenURI: jest.fn().mockImplementation((tokenAddress) => { - switch (tokenAddress) { - case ERC721_KUDOSADDRESS: - return 'https://ipfs.gitcoin.co:443/api/v0/cat/QmPmt6EAaioN78ECnW5oCL8v2YvVSpoBjLCjrXhhsAvoov'; - default: - throw new Error('Not an ERC721 token'); - } - }), - }, + getERC721AssetName: jest.fn().mockResolvedValue('KudosToken'), + getERC721AssetSymbol: jest.fn().mockResolvedValue('KDO'), + getERC721TokenURI: jest.fn().mockImplementation((tokenAddress) => { + switch (tokenAddress) { + case ERC721_KUDOSADDRESS: + return 'https://ipfs.gitcoin.co:443/api/v0/cat/QmPmt6EAaioN78ECnW5oCL8v2YvVSpoBjLCjrXhhsAvoov'; + default: + throw new Error('Not an ERC721 token'); + } + }), defaultSelectedAccount: OWNER_ACCOUNT, }); nock('https://ipfs.gitcoin.co:443') @@ -1983,11 +2136,9 @@ describe('NftController', () => { const testTokenUriEncoded = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaW5ZTWluIG1l'; const { nftController } = setupController({ - options: { - getERC721AssetName: jest.fn().mockResolvedValue('KudosToken'), - getERC721AssetSymbol: jest.fn().mockResolvedValue('KDO'), - getERC721TokenURI: jest.fn().mockResolvedValue(testTokenUriEncoded), - }, + getERC721AssetName: jest.fn().mockResolvedValue('KudosToken'), + getERC721AssetSymbol: jest.fn().mockResolvedValue('KDO'), + getERC721TokenURI: jest.fn().mockResolvedValue(testTokenUriEncoded), defaultSelectedAccount: OWNER_ACCOUNT, }); await nftController.addNft(ERC721_KUDOSADDRESS, ERC721_KUDOS_TOKEN_ID); @@ -2011,9 +2162,7 @@ describe('NftController', () => { const tokenURI = 'https://url/'; const mockGetERC721TokenURI = jest.fn().mockResolvedValue(tokenURI); const { nftController, changeNetwork } = setupController({ - options: { - getERC721TokenURI: mockGetERC721TokenURI, - }, + getERC721TokenURI: mockGetERC721TokenURI, defaultSelectedAccount: OWNER_ACCOUNT, }); nock('https://url').get('/').reply(200, { @@ -2059,10 +2208,10 @@ describe('NftController', () => { const { nftController } = setupController({ options: { onNftAdded: mockOnNftAdded, - getERC721AssetSymbol: mockGetERC721AssetSymbol, - getERC721AssetName: mockGetERC721AssetName, - getERC721TokenURI: mockGetERC721TokenURI, }, + getERC721AssetSymbol: mockGetERC721AssetSymbol, + getERC721AssetName: mockGetERC721AssetName, + getERC721TokenURI: mockGetERC721TokenURI, defaultSelectedAccount: OWNER_ACCOUNT, }); @@ -2121,10 +2270,10 @@ describe('NftController', () => { const { nftController, changeNetwork } = setupController({ options: { onNftAdded: mockOnNftAdded, - getERC721AssetSymbol: mockGetERC721AssetSymbol, - getERC721AssetName: mockGetERC721AssetName, - getERC721TokenURI: mockGetERC721TokenURI, }, + getERC721AssetSymbol: mockGetERC721AssetSymbol, + getERC721AssetName: mockGetERC721AssetName, + getERC721TokenURI: mockGetERC721TokenURI, }); nock('https://url').get('/').reply(200, { name: 'name', @@ -2181,13 +2330,13 @@ describe('NftController', () => { const { nftController } = setupController({ options: { onNftAdded: mockOnNftAdded, - getERC721AssetName: jest - .fn() - .mockRejectedValue(new Error('Failed to fetch')), - getERC721AssetSymbol: jest - .fn() - .mockRejectedValue(new Error('Failed to fetch')), }, + getERC721AssetName: jest + .fn() + .mockRejectedValue(new Error('Failed to fetch')), + getERC721AssetSymbol: jest + .fn() + .mockRejectedValue(new Error('Failed to fetch')), defaultSelectedAccount: OWNER_ACCOUNT, }); nock(NFT_API_BASE_URL) @@ -2296,13 +2445,13 @@ describe('NftController', () => { const { nftController } = setupController({ options: { onNftAdded: mockOnNftAdded, - getERC721AssetName: jest - .fn() - .mockRejectedValue(new Error('Failed to fetch')), - getERC721AssetSymbol: jest - .fn() - .mockRejectedValue(new Error('Failed to fetch')), }, + getERC721AssetName: jest + .fn() + .mockRejectedValue(new Error('Failed to fetch')), + getERC721AssetSymbol: jest + .fn() + .mockRejectedValue(new Error('Failed to fetch')), defaultSelectedAccount: OWNER_ACCOUNT, }); nock(NFT_API_BASE_URL) @@ -2418,13 +2567,13 @@ describe('NftController', () => { const { nftController } = setupController({ options: { onNftAdded: mockOnNftAdded, - getERC721AssetName: jest - .fn() - .mockRejectedValue(new Error('Failed to fetch')), - getERC721AssetSymbol: jest - .fn() - .mockRejectedValue(new Error('Failed to fetch')), }, + getERC721AssetName: jest + .fn() + .mockRejectedValue(new Error('Failed to fetch')), + getERC721AssetSymbol: jest + .fn() + .mockRejectedValue(new Error('Failed to fetch')), defaultSelectedAccount: OWNER_ACCOUNT, }); nock(NFT_API_BASE_URL) @@ -2452,7 +2601,6 @@ describe('NftController', () => { it('should not add duplicate NFTs to the ignoredNfts list', async () => { const { nftController } = setupController({ - options: {}, defaultSelectedAccount: OWNER_ACCOUNT, }); @@ -2509,23 +2657,21 @@ describe('NftController', () => { it('should add NFT with metadata hosted in IPFS', async () => { const { nftController, triggerPreferencesStateChange, mockGetAccount } = setupController({ - options: { - getERC721AssetName: jest - .fn() - .mockResolvedValue("Maltjik.jpg's Depressionists"), - getERC721AssetSymbol: jest.fn().mockResolvedValue('DPNS'), - getERC721TokenURI: jest.fn().mockImplementation((tokenAddress) => { - switch (tokenAddress) { - case ERC721_DEPRESSIONIST_ADDRESS: - return `ipfs://${DEPRESSIONIST_CID_V1}`; - default: - throw new Error('Not an ERC721 token'); - } - }), - getERC1155TokenURI: jest - .fn() - .mockRejectedValue(new Error('Not an ERC1155 token')), - }, + getERC721AssetName: jest + .fn() + .mockResolvedValue("Maltjik.jpg's Depressionists"), + getERC721AssetSymbol: jest.fn().mockResolvedValue('DPNS'), + getERC721TokenURI: jest.fn().mockImplementation((tokenAddress) => { + switch (tokenAddress) { + case ERC721_DEPRESSIONIST_ADDRESS: + return `ipfs://${DEPRESSIONIST_CID_V1}`; + default: + throw new Error('Not an ERC721 token'); + } + }), + getERC1155TokenURI: jest + .fn() + .mockRejectedValue(new Error('Not an ERC1155 token')), }); mockGetAccount.mockReturnValue(OWNER_ACCOUNT); triggerPreferencesStateChange({ @@ -2624,26 +2770,24 @@ describe('NftController', () => { ); const { nftController } = setupController({ - options: { - getERC721TokenURI: jest.fn().mockImplementation((tokenAddress) => { - switch (tokenAddress) { - case '0x01': - return 'https://testtokenuri-1.com'; - case '0x02': - return 'https://testtokenuri-2.com'; - default: - throw new Error('Not an ERC721 token'); - } - }), - getERC1155TokenURI: jest.fn().mockImplementation((tokenAddress) => { - switch (tokenAddress) { - case '0x03': - return 'https://testtokenuri-3.com'; - default: - throw new Error('Not an ERC1155 token'); - } - }), - }, + getERC721TokenURI: jest.fn().mockImplementation((tokenAddress) => { + switch (tokenAddress) { + case '0x01': + return 'https://testtokenuri-1.com'; + case '0x02': + return 'https://testtokenuri-2.com'; + default: + throw new Error('Not an ERC721 token'); + } + }), + getERC1155TokenURI: jest.fn().mockImplementation((tokenAddress) => { + switch (tokenAddress) { + case '0x03': + return 'https://testtokenuri-3.com'; + default: + throw new Error('Not an ERC1155 token'); + } + }), mockNetworkClientConfigurationsByNetworkClientId: { 'customNetworkClientId-1': buildCustomNetworkClientConfiguration({ chainId: '0xa', @@ -2744,26 +2888,24 @@ describe('NftController', () => { ); const { nftController, changeNetwork } = setupController({ - options: { - getERC721TokenURI: jest.fn().mockImplementation((tokenAddress) => { - switch (tokenAddress) { - case '0x01': - return 'https://testtokenuri-1.com'; - case '0x02': - return 'https://testtokenuri-2.com'; - default: - throw new Error('Not an ERC721 token'); - } - }), - getERC1155TokenURI: jest.fn().mockImplementation((tokenAddress) => { - switch (tokenAddress) { - case '0x03': - return 'https://testtokenuri-3.com'; - default: - throw new Error('Not an ERC1155 token'); - } - }), - }, + getERC721TokenURI: jest.fn().mockImplementation((tokenAddress) => { + switch (tokenAddress) { + case '0x01': + return 'https://testtokenuri-1.com'; + case '0x02': + return 'https://testtokenuri-2.com'; + default: + throw new Error('Not an ERC721 token'); + } + }), + getERC1155TokenURI: jest.fn().mockImplementation((tokenAddress) => { + switch (tokenAddress) { + case '0x03': + return 'https://testtokenuri-3.com'; + default: + throw new Error('Not an ERC1155 token'); + } + }), }); await nftController.addNft('0x01', '1234', { @@ -2830,8 +2972,8 @@ describe('NftController', () => { const { nftController, mockGetAccount } = setupController({ options: { chainId: ChainId.mainnet, - getERC721AssetName: jest.fn().mockResolvedValue('Name'), }, + getERC721AssetName: jest.fn().mockResolvedValue('Name'), }); mockGetAccount.mockReturnValue(null); @@ -2865,9 +3007,7 @@ describe('NftController', () => { triggerPreferencesStateChange, triggerSelectedAccountChange, } = setupController({ - options: { - getERC721TokenURI: mockGetERC721TokenURI, - }, + getERC721TokenURI: mockGetERC721TokenURI, }); const firstAddress = '0x123'; const firstAccount = createMockInternalAccount({ @@ -2955,9 +3095,7 @@ describe('NftController', () => { mockGetAccount, triggerSelectedAccountChange, } = setupController({ - options: { - getERC721TokenURI: mockGetERC721TokenURI, - }, + getERC721TokenURI: mockGetERC721TokenURI, }); const firstAddress = '0x123'; @@ -3037,9 +3175,7 @@ describe('NftController', () => { triggerPreferencesStateChange, triggerSelectedAccountChange, } = setupController({ - options: { - getERC721TokenURI: mockGetERC721TokenURI, - }, + getERC721TokenURI: mockGetERC721TokenURI, }); // Ensure that the currently selected address is not the same as either of the userAddresses triggerSelectedAccountChange(OWNER_ACCOUNT); @@ -3102,7 +3238,6 @@ describe('NftController', () => { describe('removeNft', () => { it('should remove NFT and NFT contract', async () => { const { nftController } = setupController({ - options: {}, defaultSelectedAccount: OWNER_ACCOUNT, }); @@ -3167,9 +3302,7 @@ describe('NftController', () => { mockGetAccount, triggerSelectedAccountChange, } = setupController({ - options: { - getERC721TokenURI: mockGetERC721TokenURI, - }, + getERC721TokenURI: mockGetERC721TokenURI, }); nock('https://url').get('/').reply(200, { name: 'name', @@ -3227,9 +3360,7 @@ describe('NftController', () => { const tokenURI = 'https://url/'; const mockGetERC721TokenURI = jest.fn().mockResolvedValue(tokenURI); const { nftController, changeNetwork } = setupController({ - options: { - getERC721TokenURI: mockGetERC721TokenURI, - }, + getERC721TokenURI: mockGetERC721TokenURI, defaultSelectedAccount: OWNER_ACCOUNT, }); @@ -3340,7 +3471,6 @@ describe('NftController', () => { it('should be able to clear the ignoredNfts list', async () => { const { nftController } = setupController({ - options: {}, defaultSelectedAccount: OWNER_ACCOUNT, }); @@ -3376,10 +3506,8 @@ describe('NftController', () => { .fn() .mockRejectedValue(new Error('ERC1155 error')); const { nftController } = setupController({ - options: { - getERC721OwnerOf: mockGetERC721OwnerOf, - getERC1155BalanceOf: mockGetERC1155BalanceOf, - }, + getERC721OwnerOf: mockGetERC721OwnerOf, + getERC1155BalanceOf: mockGetERC1155BalanceOf, }); const isOwner = await nftController.isNftOwner( @@ -3397,10 +3525,8 @@ describe('NftController', () => { .fn() .mockRejectedValue(new Error('ERC1155 error')); const { nftController } = setupController({ - options: { - getERC721OwnerOf: mockGetERC721OwnerOf, - getERC1155BalanceOf: mockGetERC1155BalanceOf, - }, + getERC721OwnerOf: mockGetERC721OwnerOf, + getERC1155BalanceOf: mockGetERC1155BalanceOf, }); const isOwner = await nftController.isNftOwner( @@ -3417,10 +3543,8 @@ describe('NftController', () => { .fn() .mockRejectedValue(new Error('ERC1155 error')); const { nftController } = setupController({ - options: { - getERC721OwnerOf: mockGetERC721OwnerOf, - getERC1155BalanceOf: mockGetERC1155BalanceOf, - }, + getERC721OwnerOf: mockGetERC721OwnerOf, + getERC1155BalanceOf: mockGetERC1155BalanceOf, }); const isOwner = await nftController.isNftOwner( @@ -3437,10 +3561,8 @@ describe('NftController', () => { .mockRejectedValue(new Error('ERC721 error')); const mockGetERC1155BalanceOf = jest.fn().mockResolvedValue(new BN(1)); const { nftController } = setupController({ - options: { - getERC721OwnerOf: mockGetERC721OwnerOf, - getERC1155BalanceOf: mockGetERC1155BalanceOf, - }, + getERC721OwnerOf: mockGetERC721OwnerOf, + getERC1155BalanceOf: mockGetERC1155BalanceOf, }); const isOwner = await nftController.isNftOwner( @@ -3457,10 +3579,8 @@ describe('NftController', () => { .mockRejectedValue(new Error('ERC721 error')); const mockGetERC1155BalanceOf = jest.fn().mockResolvedValue(new BN(0)); const { nftController } = setupController({ - options: { - getERC721OwnerOf: mockGetERC721OwnerOf, - getERC1155BalanceOf: mockGetERC1155BalanceOf, - }, + getERC721OwnerOf: mockGetERC721OwnerOf, + getERC1155BalanceOf: mockGetERC1155BalanceOf, }); const isOwner = await nftController.isNftOwner( @@ -3480,10 +3600,8 @@ describe('NftController', () => { .fn() .mockRejectedValue(new Error('ERC1155 error')); const { nftController } = setupController({ - options: { - getERC721OwnerOf: mockGetERC721OwnerOf, - getERC1155BalanceOf: mockGetERC1155BalanceOf, - }, + getERC721OwnerOf: mockGetERC721OwnerOf, + getERC1155BalanceOf: mockGetERC1155BalanceOf, }); const error = "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."; @@ -3503,10 +3621,8 @@ describe('NftController', () => { triggerPreferencesStateChange, triggerSelectedAccountChange, } = setupController({ - options: { - getERC721TokenURI: jest.fn().mockRejectedValue(''), - getERC1155TokenURI: jest.fn().mockResolvedValue('ipfs://*'), - }, + getERC721TokenURI: jest.fn().mockRejectedValue(''), + getERC1155TokenURI: jest.fn().mockResolvedValue('ipfs://*'), defaultSelectedAccount: OWNER_ACCOUNT, }); triggerSelectedAccountChange(OWNER_ACCOUNT); @@ -3537,7 +3653,6 @@ describe('NftController', () => { describe('updateNftFavoriteStatus', () => { it('should not set NFT as favorite if nft not found', async () => { const { nftController } = setupController({ - options: {}, defaultSelectedAccount: OWNER_ACCOUNT, }); @@ -3565,7 +3680,6 @@ describe('NftController', () => { }); it('should set NFT as favorite', async () => { const { nftController } = setupController({ - options: {}, defaultSelectedAccount: OWNER_ACCOUNT, }); @@ -3594,7 +3708,6 @@ describe('NftController', () => { it('should set NFT as favorite and then unset it', async () => { const { nftController } = setupController({ - options: {}, defaultSelectedAccount: OWNER_ACCOUNT, }); @@ -3639,7 +3752,6 @@ describe('NftController', () => { it('should keep the favorite status as true after updating metadata', async () => { const { nftController } = setupController({ - options: {}, defaultSelectedAccount: OWNER_ACCOUNT, }); @@ -3699,7 +3811,6 @@ describe('NftController', () => { it('should keep the favorite status as false after updating metadata', async () => { const { nftController } = setupController({ - options: {}, defaultSelectedAccount: OWNER_ACCOUNT, }); @@ -3830,7 +3941,6 @@ describe('NftController', () => { describe('checkAndUpdateAllNftsOwnershipStatus', () => { it('should check whether NFTs for the current selectedAccount/chainId combination are still owned by the selectedAccount and update the isCurrentlyOwned value to false when NFT is not still owned', async () => { const { nftController } = setupController({ - options: {}, defaultSelectedAccount: OWNER_ACCOUNT, }); jest.spyOn(nftController, 'isNftOwner').mockResolvedValue(false); @@ -3859,7 +3969,6 @@ describe('NftController', () => { it('should check whether NFTs for the current selectedAccount/chainId combination are still owned by the selectedAccount and leave/set the isCurrentlyOwned value to true when NFT is still owned', async () => { const { nftController } = setupController({ - options: {}, defaultSelectedAccount: OWNER_ACCOUNT, }); jest.spyOn(nftController, 'isNftOwner').mockResolvedValue(true); @@ -3888,7 +3997,6 @@ describe('NftController', () => { it('should check whether NFTs for the current selectedAccount/chainId combination are still owned by the selectedAccount and leave the isCurrentlyOwned value as is when NFT ownership check fails', async () => { const { nftController } = setupController({ - options: {}, defaultSelectedAccount: OWNER_ACCOUNT, }); jest @@ -3967,9 +4075,7 @@ describe('NftController', () => { }); it('should handle default case where selectedAccount is not set', async () => { - const { nftController, mockGetAccount } = setupController({ - options: {}, - }); + const { nftController, mockGetAccount } = setupController({}); mockGetAccount.mockReturnValue(null); jest.spyOn(nftController, 'isNftOwner').mockResolvedValue(false); @@ -3993,7 +4099,6 @@ describe('NftController', () => { describe('checkAndUpdateSingleNftOwnershipStatus', () => { it('should check whether the passed NFT is still owned by the the current selectedAccount/chainId combination and update its isCurrentlyOwned property in state if batch is false and isNftOwner returns false', async () => { const { nftController } = setupController({ - options: {}, defaultSelectedAccount: OWNER_ACCOUNT, }); @@ -4028,7 +4133,6 @@ describe('NftController', () => { 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({ - options: {}, defaultSelectedAccount: OWNER_ACCOUNT, }); @@ -4203,7 +4307,6 @@ describe('NftController', () => { it('should return null if the NFT does not exist in the state', async () => { const { nftController } = setupController({ - options: {}, defaultSelectedAccount: OWNER_ACCOUNT, }); @@ -4291,7 +4394,6 @@ describe('NftController', () => { it('should return undefined if the NFT does not exist', () => { const { nftController } = setupController({ - options: {}, defaultSelectedAccount: OWNER_ACCOUNT, }); @@ -4325,7 +4427,6 @@ describe('NftController', () => { 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 () => { const { nftController } = setupController({ - options: {}, defaultSelectedAccount: OWNER_ACCOUNT, }); @@ -4374,9 +4475,7 @@ describe('NftController', () => { const tokenURI = 'https://api.pudgypenguins.io/lil/4'; const mockGetERC721TokenURI = jest.fn().mockResolvedValue(tokenURI); const { nftController, mockGetAccount } = setupController({ - options: { - getERC721TokenURI: mockGetERC721TokenURI, - }, + getERC721TokenURI: mockGetERC721TokenURI, defaultSelectedAccount: OWNER_ACCOUNT, }); const spy = jest.spyOn(nftController, 'updateNft'); @@ -4431,9 +4530,7 @@ describe('NftController', () => { const tokenURI = 'https://url/'; const mockGetERC721TokenURI = jest.fn().mockResolvedValue(tokenURI); const { nftController, mockGetAccount } = setupController({ - options: { - getERC721TokenURI: mockGetERC721TokenURI, - }, + getERC721TokenURI: mockGetERC721TokenURI, defaultSelectedAccount: OWNER_ACCOUNT, }); const updateNftSpy = jest.spyOn(nftController, 'updateNft'); @@ -4497,9 +4594,7 @@ describe('NftController', () => { const tokenURI = 'https://url/'; const mockGetERC721TokenURI = jest.fn().mockResolvedValue(tokenURI); const { nftController, mockGetAccount } = setupController({ - options: { - getERC721TokenURI: mockGetERC721TokenURI, - }, + getERC721TokenURI: mockGetERC721TokenURI, defaultSelectedAccount: OWNER_ACCOUNT, }); const spy = jest.spyOn(nftController, 'updateNft'); @@ -4595,9 +4690,7 @@ describe('NftController', () => { changeNetwork, triggerSelectedAccountChange, } = setupController({ - options: { - getERC721TokenURI: mockGetERC721TokenURI, - }, + getERC721TokenURI: mockGetERC721TokenURI, }); changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); const spy = jest.spyOn(nftController, 'updateNftMetadata'); @@ -4645,9 +4738,7 @@ describe('NftController', () => { changeNetwork, triggerSelectedAccountChange, } = setupController({ - options: { - getERC721TokenURI: mockGetERC721TokenURI, - }, + getERC721TokenURI: mockGetERC721TokenURI, }); changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); const spy = jest.spyOn(nftController, 'updateNftMetadata'); @@ -4691,9 +4782,7 @@ describe('NftController', () => { const tokenURI = 'https://api.pudgypenguins.io/lil/4'; const mockGetERC721TokenURI = jest.fn().mockResolvedValue(tokenURI); const { nftController, triggerPreferencesStateChange } = setupController({ - options: { - getERC721TokenURI: mockGetERC721TokenURI, - }, + getERC721TokenURI: mockGetERC721TokenURI, }); const selectedAddress = OWNER_ADDRESS; const spy = jest.spyOn(nftController, 'updateNft'); @@ -4798,8 +4887,8 @@ describe('NftController', () => { const { nftController, messenger } = setupController({ options: { openSeaEnabled: true, - getERC721TokenURI: mockGetERC721TokenURI, }, + getERC721TokenURI: mockGetERC721TokenURI, }); const updateNftMetadataSpy = jest.spyOn(nftController, 'updateNftMetadata'); messenger.publish( diff --git a/packages/assets-controllers/src/NftController.ts b/packages/assets-controllers/src/NftController.ts index 9b197b5855..ffa2b040d0 100644 --- a/packages/assets-controllers/src/NftController.ts +++ b/packages/assets-controllers/src/NftController.ts @@ -44,7 +44,14 @@ import { Mutex } from 'async-mutex'; import BN from 'bn.js'; import { v4 as random } from 'uuid'; -import type { AssetsContractController } from './AssetsContractController'; +import type { + AssetsContractControllerGetERC1155BalanceOfAction, + AssetsContractControllerGetERC1155TokenURIAction, + AssetsContractControllerGetERC721AssetNameAction, + AssetsContractControllerGetERC721AssetSymbolAction, + AssetsContractControllerGetERC721OwnerOfAction, + AssetsContractControllerGetERC721TokenURIAction, +} from './AssetsContractController'; import { compareNftMetadata, getFormattedIpfsUrl, @@ -230,7 +237,13 @@ export type AllowedActions = | AddApprovalRequest | AccountsControllerGetAccountAction | AccountsControllerGetSelectedAccountAction - | NetworkControllerGetNetworkClientByIdAction; + | NetworkControllerGetNetworkClientByIdAction + | AssetsContractControllerGetERC721AssetNameAction + | AssetsContractControllerGetERC721AssetSymbolAction + | AssetsContractControllerGetERC721TokenURIAction + | AssetsContractControllerGetERC721OwnerOfAction + | AssetsContractControllerGetERC1155BalanceOfAction + | AssetsContractControllerGetERC1155TokenURIAction; export type AllowedEvents = | PreferencesControllerStateChangeEvent @@ -288,18 +301,6 @@ export class NftController extends BaseController< #isIpfsGatewayEnabled: boolean; - readonly #getERC721AssetName: AssetsContractController['getERC721AssetName']; - - readonly #getERC721AssetSymbol: AssetsContractController['getERC721AssetSymbol']; - - readonly #getERC721TokenURI: AssetsContractController['getERC721TokenURI']; - - readonly #getERC721OwnerOf: AssetsContractController['getERC721OwnerOf']; - - readonly #getERC1155BalanceOf: AssetsContractController['getERC1155BalanceOf']; - - readonly #getERC1155TokenURI: AssetsContractController['getERC1155TokenURI']; - readonly #onNftAdded?: (data: { address: string; symbol: string | undefined; @@ -317,12 +318,6 @@ export class NftController extends BaseController< * @param options.openSeaEnabled - Controls whether the OpenSea API is used. * @param options.useIpfsSubdomains - Controls whether IPFS subdomains are used. * @param options.isIpfsGatewayEnabled - Controls whether IPFS is enabled or not. - * @param options.getERC721AssetName - Gets the name of the asset at the given address. - * @param options.getERC721AssetSymbol - Gets the symbol of the asset at the given address. - * @param options.getERC721TokenURI - Gets the URI of the ERC721 token at the given address, with the given ID. - * @param options.getERC721OwnerOf - Get the owner of a ERC-721 NFT. - * @param options.getERC1155BalanceOf - Gets balance of a ERC-1155 NFT. - * @param options.getERC1155TokenURI - Gets the URI of the ERC1155 token at the given address, with the given ID. * @param options.onNftAdded - Callback that is called when an NFT is added. Currently used pass data * for tracking the NFT added event. * @param options.messenger - The controller messenger. @@ -334,12 +329,6 @@ export class NftController extends BaseController< openSeaEnabled = false, useIpfsSubdomains = true, isIpfsGatewayEnabled = true, - getERC721AssetName, - getERC721AssetSymbol, - getERC721TokenURI, - getERC721OwnerOf, - getERC1155BalanceOf, - getERC1155TokenURI, onNftAdded, messenger, state = {}, @@ -349,12 +338,6 @@ export class NftController extends BaseController< openSeaEnabled?: boolean; useIpfsSubdomains?: boolean; isIpfsGatewayEnabled?: boolean; - getERC721AssetName: AssetsContractController['getERC721AssetName']; - getERC721AssetSymbol: AssetsContractController['getERC721AssetSymbol']; - getERC721TokenURI: AssetsContractController['getERC721TokenURI']; - getERC721OwnerOf: AssetsContractController['getERC721OwnerOf']; - getERC1155BalanceOf: AssetsContractController['getERC1155BalanceOf']; - getERC1155TokenURI: AssetsContractController['getERC1155TokenURI']; onNftAdded?: (data: { address: string; symbol: string | undefined; @@ -383,13 +366,6 @@ export class NftController extends BaseController< this.#openSeaEnabled = openSeaEnabled; this.#useIpfsSubdomains = useIpfsSubdomains; this.#isIpfsGatewayEnabled = isIpfsGatewayEnabled; - - this.#getERC721AssetName = getERC721AssetName; - this.#getERC721AssetSymbol = getERC721AssetSymbol; - this.#getERC721TokenURI = getERC721TokenURI; - this.#getERC721OwnerOf = getERC721OwnerOf; - this.#getERC1155BalanceOf = getERC1155BalanceOf; - this.#getERC1155TokenURI = getERC1155TokenURI; this.#onNftAdded = onNftAdded; this.messagingSystem.subscribe( @@ -735,7 +711,8 @@ export class NftController extends BaseController< ): Promise<[string, string]> { // try ERC721 uri try { - const uri = await this.#getERC721TokenURI( + const uri = await this.messagingSystem.call( + 'AssetsContractController:getERC721TokenURI', contractAddress, tokenId, networkClientId, @@ -747,7 +724,8 @@ export class NftController extends BaseController< // try ERC1155 uri try { - const tokenURI = await this.#getERC1155TokenURI( + const tokenURI = await this.messagingSystem.call( + 'AssetsContractController:getERC1155TokenURI', contractAddress, tokenId, networkClientId, @@ -832,8 +810,16 @@ export class NftController extends BaseController< Pick > { const [name, symbol] = await Promise.all([ - this.#getERC721AssetName(contractAddress, networkClientId), - this.#getERC721AssetSymbol(contractAddress, networkClientId), + this.messagingSystem.call( + 'AssetsContractController:getERC721AssetName', + contractAddress, + networkClientId, + ), + this.messagingSystem.call( + 'AssetsContractController:getERC721AssetSymbol', + contractAddress, + networkClientId, + ), ]); return { @@ -1413,7 +1399,8 @@ export class NftController extends BaseController< ): Promise { // Checks the ownership for ERC-721. try { - const owner = await this.#getERC721OwnerOf( + const owner = await this.messagingSystem.call( + 'AssetsContractController:getERC721OwnerOf', nftAddress, tokenId, networkClientId, @@ -1426,7 +1413,8 @@ export class NftController extends BaseController< // Checks the ownership for ERC-1155. try { - const balance = await this.#getERC1155BalanceOf( + const balance = await this.messagingSystem.call( + 'AssetsContractController:getERC1155BalanceOf', ownerAddress, nftAddress, tokenId, diff --git a/packages/assets-controllers/src/TokenBalancesController.test.ts b/packages/assets-controllers/src/TokenBalancesController.test.ts index 49390c64fe..e57d13c77f 100644 --- a/packages/assets-controllers/src/TokenBalancesController.test.ts +++ b/packages/assets-controllers/src/TokenBalancesController.test.ts @@ -8,6 +8,8 @@ import { createMockInternalAccount } from '../../accounts-controller/src/tests/m import type { AllowedActions, AllowedEvents, + TokenBalancesControllerActions, + TokenBalancesControllerEvents, TokenBalancesControllerMessenger, } from './TokenBalancesController'; import { TokenBalancesController } from './TokenBalancesController'; @@ -27,13 +29,16 @@ const controllerName = 'TokenBalancesController'; */ function getMessenger( controllerMessenger = new ControllerMessenger< - AllowedActions, - AllowedEvents + TokenBalancesControllerActions | AllowedActions, + TokenBalancesControllerEvents | AllowedEvents >(), ): TokenBalancesControllerMessenger { return controllerMessenger.getRestricted({ name: controllerName, - allowedActions: ['AccountsController:getSelectedAccount'], + allowedActions: [ + 'AccountsController:getSelectedAccount', + 'AssetsContractController:getERC20BalanceOf', + ], allowedEvents: ['TokensController:stateChange'], }); } @@ -55,8 +60,8 @@ const setupController = ({ triggerTokensStateChange: (state: TokensControllerState) => Promise; } => { const controllerMessenger = new ControllerMessenger< - AllowedActions, - AllowedEvents + TokenBalancesControllerActions | AllowedActions, + TokenBalancesControllerEvents | AllowedEvents >(); const messenger = getMessenger(controllerMessenger); @@ -67,9 +72,12 @@ const setupController = ({ 'AccountsController:getSelectedAccount', mockSelectedAccount, ); + controllerMessenger.registerActionHandler( + 'AssetsContractController:getERC20BalanceOf', + mockGetERC20BalanceOf, + ); const controller = new TokenBalancesController({ - getERC20BalanceOf: mockGetERC20BalanceOf, messenger, ...config, }); @@ -114,7 +122,6 @@ describe('TokenBalancesController', () => { new TokenBalancesController({ interval: 10, - getERC20BalanceOf: jest.fn(), messenger: getMessenger(new ControllerMessenger()), }); await flushPromises(); diff --git a/packages/assets-controllers/src/TokenBalancesController.ts b/packages/assets-controllers/src/TokenBalancesController.ts index a1b58a4340..e80db70d5f 100644 --- a/packages/assets-controllers/src/TokenBalancesController.ts +++ b/packages/assets-controllers/src/TokenBalancesController.ts @@ -1,13 +1,13 @@ -import { type AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller'; -import { - type RestrictedControllerMessenger, - type ControllerGetStateAction, - type ControllerStateChangeEvent, - BaseController, +import type { AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller'; +import type { + RestrictedControllerMessenger, + ControllerGetStateAction, + ControllerStateChangeEvent, } from '@metamask/base-controller'; +import { BaseController } from '@metamask/base-controller'; import { safelyExecute, toHex } from '@metamask/controller-utils'; -import type { AssetsContractController } from './AssetsContractController'; +import type { AssetsContractControllerGetERC20BalanceOfAction } from './AssetsContractController'; import type { Token } from './TokenRatesController'; import type { TokensControllerStateChangeEvent } from './TokensController'; @@ -24,13 +24,11 @@ const metadata = { * @property interval - Polling interval used to fetch new token balances. * @property tokens - List of tokens to track balances for. * @property disabled - If set to true, all tracked tokens contract balances updates are blocked. - * @property getERC20BalanceOf - Gets the balance of the given account at the given contract address. */ type TokenBalancesControllerOptions = { interval?: number; tokens?: Token[]; disabled?: boolean; - getERC20BalanceOf: AssetsContractController['getERC20BalanceOf']; messenger: TokenBalancesControllerMessenger; state?: Partial; }; @@ -56,7 +54,9 @@ export type TokenBalancesControllerGetStateAction = ControllerGetStateAction< export type TokenBalancesControllerActions = TokenBalancesControllerGetStateAction; -export type AllowedActions = AccountsControllerGetSelectedAccountAction; +export type AllowedActions = + | AccountsControllerGetSelectedAccountAction + | AssetsContractControllerGetERC20BalanceOfAction; export type TokenBalancesControllerStateChangeEvent = ControllerStateChangeEvent< @@ -99,8 +99,6 @@ export class TokenBalancesController extends BaseController< > { #handle?: ReturnType; - #getERC20BalanceOf: AssetsContractController['getERC20BalanceOf']; - #interval: number; #tokens: Token[]; @@ -114,7 +112,6 @@ export class TokenBalancesController extends BaseController< * @param options.interval - Polling interval used to fetch new token balances. * @param options.tokens - List of tokens to track balances for. * @param options.disabled - If set to true, all tracked tokens contract balances updates are blocked. - * @param options.getERC20BalanceOf - Gets the balance of the given account at the given contract address. * @param options.state - Initial state to set on this controller. * @param options.messenger - The controller restricted messenger. */ @@ -122,7 +119,6 @@ export class TokenBalancesController extends BaseController< interval = DEFAULT_INTERVAL, tokens = [], disabled = false, - getERC20BalanceOf, messenger, state = {}, }: TokenBalancesControllerOptions) { @@ -150,8 +146,6 @@ export class TokenBalancesController extends BaseController< }, ); - this.#getERC20BalanceOf = getERC20BalanceOf; - // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.poll(); @@ -209,7 +203,8 @@ export class TokenBalancesController extends BaseController< for (const token of this.#tokens) { const { address } = token; try { - const balance = await this.#getERC20BalanceOf( + const balance = await this.messagingSystem.call( + 'AssetsContractController:getERC20BalanceOf', address, selectedInternalAccount.address, ); diff --git a/packages/assets-controllers/src/index.ts b/packages/assets-controllers/src/index.ts index a2e680fad3..52293d4cb4 100644 --- a/packages/assets-controllers/src/index.ts +++ b/packages/assets-controllers/src/index.ts @@ -8,7 +8,32 @@ export type { AccountTrackerControllerEvents, } from './AccountTrackerController'; export { AccountTrackerController } from './AccountTrackerController'; -export * from './AssetsContractController'; +export type { + AssetsContractControllerActions, + AssetsContractControllerEvents, + AssetsContractControllerGetERC20StandardAction, + AssetsContractControllerGetERC721StandardAction, + AssetsContractControllerGetERC1155StandardAction, + AssetsContractControllerGetERC20BalanceOfAction, + AssetsContractControllerGetERC20TokenDecimalsAction, + AssetsContractControllerGetERC20TokenNameAction, + AssetsContractControllerGetERC721NftTokenIdAction, + AssetsContractControllerGetERC721TokenURIAction, + AssetsContractControllerGetERC721AssetNameAction, + AssetsContractControllerGetERC721AssetSymbolAction, + AssetsContractControllerGetERC721OwnerOfAction, + AssetsContractControllerGetERC1155TokenURIAction, + AssetsContractControllerGetERC1155BalanceOfAction, + AssetsContractControllerTransferSingleERC1155Action, + AssetsContractControllerGetTokenStandardAndDetailsAction, + AssetsContractControllerGetBalancesInSingleCallAction, + AssetsContractControllerMessenger, + BalanceMap, +} from './AssetsContractController'; +export { + SINGLE_CALL_BALANCES_ADDRESS_BY_CHAINID, + AssetsContractController, +} from './AssetsContractController'; export * from './CurrencyRateController'; export type { NftControllerState,