diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 98d0840d6e1..d1f5c206615 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Uncategorized + +- **BREAKING:** The NftController messenger must now allow the `NetworkController:getNetworkClientById` action ([#XXXX](https://github.com/MetaMask/core/pull/XXXX)) +- NftControllerMessenger now makes use of `selectedNetworkClientId` when responding to changes in NetworkController state to capture the currently selected chain rather than `providerConfig` ([#XXXX](https://github.com/MetaMask/core/pull/XXXX)) + - This should be functionally equivalent, but is being noted anyway. + ## [29.0.0] ### Added diff --git a/packages/assets-controllers/src/NftController.test.ts b/packages/assets-controllers/src/NftController.test.ts index 2100dee04e4..8e2de092d78 100644 --- a/packages/assets-controllers/src/NftController.test.ts +++ b/packages/assets-controllers/src/NftController.test.ts @@ -1,7 +1,7 @@ import type { Network } from '@ethersproject/providers'; import type { - AddApprovalRequest, ApprovalStateChange, + GetApprovalsState, } from '@metamask/approval-controller'; import { ApprovalController } from '@metamask/approval-controller'; import { ControllerMessenger } from '@metamask/base-controller'; @@ -16,10 +16,12 @@ import { ERC20, NetworksTicker, NFT_API_BASE_URL, + InfuraNetworkType, } from '@metamask/controller-utils'; import type { + NetworkClientConfiguration, + NetworkClientId, NetworkState, - ProviderConfig, } from '@metamask/network-controller'; import { defaultState as defaultNetworkState } from '@metamask/network-controller'; import { @@ -31,9 +33,17 @@ import nock from 'nock'; import * as sinon from 'sinon'; import { v4 } from 'uuid'; +import type { + ExtractAvailableAction, + ExtractAvailableEvent, +} from '../../base-controller/tests/helpers'; +import { + buildCustomNetworkClientConfiguration, + buildMockGetNetworkClientById, +} from '../../network-controller/tests/helpers'; import { getFormattedIpfsUrl } from './assetsUtil'; import { Source } from './constants'; -import type { Nft } from './NftController'; +import type { Nft, NftControllerMessenger } from './NftController'; import { NftController } from './NftController'; const CRYPTOPUNK_ADDRESS = '0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB'; @@ -70,9 +80,6 @@ const GOERLI = { ticker: NetworksTicker.goerli, }; -type ApprovalActions = AddApprovalRequest; -type ApprovalEvents = ApprovalStateChange; - const controllerName = 'NftController' as const; // Mock out detectNetwork function for cleaner tests, Ethers calls this a bunch of times because the Web3Provider is paranoid. @@ -104,23 +111,49 @@ jest.mock('uuid', () => { /** * Setup a test controller instance. * - * @param options - Controller options. + * @param args - Arguments to this function. + * @param args.options - Controller options. + * @param args.mockNetworkClientConfigurationsByNetworkClientId - Used to construct + * mock versions of network clients and ultimately mock the + * `NetworkController:getNetworkClientById` action. * @returns A collection of test controllers and mocks. */ -function setupController( - options: Partial[0]> = {}, -) { +function setupController({ + options = {}, + mockNetworkClientConfigurationsByNetworkClientId = {}, +}: { + options?: Partial[0]>; + mockNetworkClientConfigurationsByNetworkClientId?: Record< + NetworkClientId, + NetworkClientConfiguration + >; +} = {}) { const onNetworkDidChangeListeners: ((state: NetworkState) => void)[] = []; - const changeNetwork = (providerConfig: ProviderConfig) => { + const changeNetwork = ({ + selectedNetworkClientId, + }: { + selectedNetworkClientId: NetworkClientId; + }) => { onNetworkDidChangeListeners.forEach((listener) => { listener({ ...defaultNetworkState, - providerConfig, + selectedNetworkClientId, }); }); }; - const messenger = new ControllerMessenger(); + const messenger = new ControllerMessenger< + ExtractAvailableAction | GetApprovalsState, + ExtractAvailableEvent | ApprovalStateChange + >(); + + const getNetworkClientById = buildMockGetNetworkClientById( + mockNetworkClientConfigurationsByNetworkClientId, + ); + messenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + getNetworkClientById, + ); const approvalControllerMessenger = messenger.getRestricted({ name: 'ApprovalController', @@ -133,39 +166,12 @@ function setupController( showApprovalRequest: jest.fn(), }); - const mockGetNetworkClientById = jest - .fn() - .mockImplementation((networkClientId) => { - switch (networkClientId) { - case 'sepolia': - return { - configuration: { - chainId: SEPOLIA.chainId, - }, - }; - case 'goerli': - return { - configuration: { - chainId: GOERLI.chainId, - }, - }; - case 'customNetworkClientId-1': - return { - configuration: { - chainId: '0xa', - }, - }; - default: - throw new Error('Invalid network client id'); - } - }); - - const nftControllerMessenger = messenger.getRestricted< - typeof controllerName, - ApprovalActions['type'] - >({ + const nftControllerMessenger = messenger.getRestricted({ name: controllerName, - allowedActions: ['ApprovalController:addRequest'], + allowedActions: [ + 'ApprovalController:addRequest', + 'NetworkController:getNetworkClientById', + ], allowedEvents: [], }); @@ -184,7 +190,7 @@ function setupController( getERC721OwnerOf: jest.fn(), getERC1155BalanceOf: jest.fn(), getERC1155TokenURI: jest.fn(), - getNetworkClientById: mockGetNetworkClientById, + getNetworkClientById, onNftAdded: jest.fn(), messenger: nftControllerMessenger, ...options, @@ -316,10 +322,12 @@ describe('NftController', () => { }), ); const { nftController } = setupController({ - getERC721TokenURI: jest - .fn() - .mockImplementation(() => 'https://testtokenuri.com'), - getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + options: { + getERC721TokenURI: jest + .fn() + .mockImplementation(() => 'https://testtokenuri.com'), + getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + }, }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -389,7 +397,9 @@ describe('NftController', () => { it('should error if the user does not own the suggested ERC721 NFT', async function () { const { nftController, messenger } = setupController({ - getERC721OwnerOf: jest.fn().mockImplementation(() => '0x12345abcefg'), + options: { + getERC721OwnerOf: jest.fn().mockImplementation(() => '0x12345abcefg'), + }, }); const callActionSpy = jest.spyOn(messenger, 'call').mockResolvedValue({}); @@ -417,7 +427,9 @@ describe('NftController', () => { it('should error if the user does not own the suggested ERC1155 NFT', async function () { const { nftController, messenger } = setupController({ - getERC1155BalanceOf: jest.fn().mockImplementation(() => new BN(0)), + options: { + getERC1155BalanceOf: jest.fn().mockImplementation(() => new BN(0)), + }, }); const callActionSpy = jest.spyOn(messenger, 'call').mockResolvedValue({}); @@ -441,10 +453,12 @@ describe('NftController', () => { ); const { nftController, messenger, triggerPreferencesStateChange } = setupController({ - getERC721TokenURI: jest - .fn() - .mockImplementation(() => 'https://testtokenuri.com'), - getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + options: { + getERC721TokenURI: jest + .fn() + .mockImplementation(() => 'https://testtokenuri.com'), + getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + }, }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), @@ -500,10 +514,12 @@ describe('NftController', () => { ); const { nftController, messenger, triggerPreferencesStateChange } = setupController({ - getERC721TokenURI: jest - .fn() - .mockImplementation(() => 'https://testtokenuri.com'), - getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + options: { + getERC721TokenURI: jest + .fn() + .mockImplementation(() => 'https://testtokenuri.com'), + getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + }, }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), @@ -559,10 +575,12 @@ describe('NftController', () => { ); const { nftController, messenger, triggerPreferencesStateChange } = setupController({ - getERC721TokenURI: jest - .fn() - .mockImplementation(() => 'ipfs://testtokenuri.com'), - getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + options: { + getERC721TokenURI: jest + .fn() + .mockImplementation(() => 'ipfs://testtokenuri.com'), + getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + }, }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), @@ -618,10 +636,12 @@ describe('NftController', () => { ); const { nftController, messenger, triggerPreferencesStateChange } = setupController({ - getERC721TokenURI: jest - .fn() - .mockImplementation(() => 'ipfs://testtokenuri.com'), - getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + options: { + getERC721TokenURI: jest + .fn() + .mockImplementation(() => 'ipfs://testtokenuri.com'), + getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + }, }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), @@ -678,13 +698,15 @@ describe('NftController', () => { const { nftController, messenger, triggerPreferencesStateChange } = setupController({ - getERC721TokenURI: jest - .fn() - .mockRejectedValue(new Error('Not an ERC721 contract')), - getERC1155TokenURI: jest - .fn() - .mockImplementation(() => 'https://testtokenuri.com'), - getERC1155BalanceOf: jest.fn().mockImplementation(() => new BN(1)), + 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)), + }, }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), @@ -744,13 +766,15 @@ describe('NftController', () => { const { nftController, messenger, triggerPreferencesStateChange } = setupController({ - getERC721TokenURI: jest - .fn() - .mockRejectedValue(new Error('Not an ERC721 contract')), - getERC1155TokenURI: jest - .fn() - .mockImplementation(() => 'https://testtokenuri.com'), - getERC1155BalanceOf: jest.fn().mockImplementation(() => new BN(1)), + 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)), + }, }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), @@ -815,18 +839,20 @@ describe('NftController', () => { changeNetwork, triggerPreferencesStateChange, } = setupController({ - getERC721OwnerOf: jest - .fn() - .mockImplementation(() => SECOND_OWNER_ADDRESS), - getERC721TokenURI: jest - .fn() - .mockImplementation(() => 'https://testtokenuri.com'), - getERC721AssetName: jest - .fn() - .mockImplementation(() => 'testERC721Name'), - getERC721AssetSymbol: jest - .fn() - .mockImplementation(() => 'testERC721Symbol'), + 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'), + }, }); const requestId = 'approval-request-id-1'; @@ -858,7 +884,7 @@ describe('NftController', () => { openSeaEnabled: true, selectedAddress: OWNER_ADDRESS, }); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); nftController.watchNft(ERC721_NFT, ERC721, 'https://etherscan.io', { userAddress: SECOND_OWNER_ADDRESS, @@ -911,16 +937,18 @@ describe('NftController', () => { triggerPreferencesStateChange, changeNetwork, } = setupController({ - getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), - getERC721TokenURI: jest - .fn() - .mockImplementation(() => 'https://testtokenuri.com'), - getERC721AssetName: jest - .fn() - .mockImplementation(() => 'testERC721Name'), - getERC721AssetSymbol: jest - .fn() - .mockImplementation(() => 'testERC721Symbol'), + options: { + getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + getERC721TokenURI: jest + .fn() + .mockImplementation(() => 'https://testtokenuri.com'), + getERC721AssetName: jest + .fn() + .mockImplementation(() => 'testERC721Name'), + getERC721AssetSymbol: jest + .fn() + .mockImplementation(() => 'testERC721Symbol'), + }, }); const requestId = 'approval-request-id-1'; @@ -965,7 +993,7 @@ describe('NftController', () => { openSeaEnabled: true, selectedAddress: '0xDifferentAddress', }); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); // now accept the request approvalController.accept(requestId); await acceptedRequest; @@ -1000,7 +1028,7 @@ describe('NftController', () => { // getERC721OwnerOf not mocked // getERC1155BalanceOf not mocked - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); const requestId = 'approval-request-id-1'; (v4 as jest.Mock).mockImplementationOnce(() => requestId); @@ -1019,7 +1047,9 @@ describe('NftController', () => { describe('addNft', () => { it('should add NFT and NFT contract', async () => { const { nftController } = setupController({ - getERC721AssetName: jest.fn().mockResolvedValue('Name'), + options: { + getERC721AssetName: jest.fn().mockResolvedValue('Name'), + }, }); const { selectedAddress, chainId } = nftController.config; @@ -1068,7 +1098,9 @@ describe('NftController', () => { it('should call onNftAdded callback correctly when NFT is manually added', async () => { const mockOnNftAdded = jest.fn(); const { nftController } = setupController({ - onNftAdded: mockOnNftAdded, + options: { + onNftAdded: mockOnNftAdded, + }, }); await nftController.addNft('0x01', '1', { @@ -1092,7 +1124,9 @@ describe('NftController', () => { it('should call onNftAdded callback correctly when NFT is added via detection', async () => { const mockOnNftAdded = jest.fn(); const { nftController } = setupController({ - onNftAdded: mockOnNftAdded, + options: { + onNftAdded: mockOnNftAdded, + }, }); const detectedUserAddress = '0x123'; @@ -1243,12 +1277,14 @@ describe('NftController', () => { it('should add NFT and get information from NFT-API', async () => { const { nftController } = setupController({ - getERC721TokenURI: jest - .fn() - .mockRejectedValue(new Error('Not an ERC721 contract')), - getERC1155TokenURI: jest - .fn() - .mockRejectedValue(new Error('Not an ERC1155 contract')), + options: { + getERC721TokenURI: jest + .fn() + .mockRejectedValue(new Error('Not an ERC721 contract')), + getERC1155TokenURI: jest + .fn() + .mockRejectedValue(new Error('Not an ERC1155 contract')), + }, }); const { selectedAddress, chainId } = nftController.config; @@ -1272,13 +1308,15 @@ describe('NftController', () => { it('should add NFT erc721 and aggregate NFT data from both contract and NFT-API', async () => { const { nftController } = setupController({ - getERC721AssetName: jest.fn().mockResolvedValue('KudosToken'), - getERC721AssetSymbol: jest.fn().mockResolvedValue('KDO'), - getERC721TokenURI: jest - .fn() - .mockResolvedValue( - 'https://ipfs.gitcoin.co:443/api/v0/cat/QmPmt6EAaioN78ECnW5oCL8v2YvVSpoBjLCjrXhhsAvoov', - ), + options: { + getERC721AssetName: jest.fn().mockResolvedValue('KudosToken'), + getERC721AssetSymbol: jest.fn().mockResolvedValue('KDO'), + getERC721TokenURI: jest + .fn() + .mockResolvedValue( + 'https://ipfs.gitcoin.co:443/api/v0/cat/QmPmt6EAaioN78ECnW5oCL8v2YvVSpoBjLCjrXhhsAvoov', + ), + }, }); nock(NFT_API_BASE_URL) .get( @@ -1336,14 +1374,16 @@ describe('NftController', () => { it('should add NFT erc1155 and get NFT information from contract when NFT API call fail', async () => { const { nftController } = setupController({ - getERC721TokenURI: jest - .fn() - .mockRejectedValue(new Error('Not a 721 contract')), - getERC1155TokenURI: jest - .fn() - .mockResolvedValue( - 'https://api.opensea.io/api/v1/metadata/0x495f947276749Ce646f68AC8c248420045cb7b5e/0x{id}', - ), + 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}', + ), + }, }); nock('https://api.opensea.io') .get( @@ -1377,16 +1417,18 @@ describe('NftController', () => { it('should add NFT erc721 and get NFT information only from contract', async () => { const { nftController } = setupController({ - 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'); - } - }), + 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'); + } + }), + }, }); nock('https://ipfs.gitcoin.co:443') .get('/api/v0/cat/QmPmt6EAaioN78ECnW5oCL8v2YvVSpoBjLCjrXhhsAvoov') @@ -1438,10 +1480,10 @@ describe('NftController', () => { .stub(nftController, 'getNftInformation' as any) .returns({ name: 'name', image: 'url', description: 'description' }); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); await nftController.addNft('0x01', '1234'); - changeNetwork(GOERLI); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); expect( nftController.state.allNfts[selectedAddress]?.[ChainId[GOERLI.type]], @@ -1463,7 +1505,9 @@ describe('NftController', () => { it('should add an nft and nftContract to state when all contract information is falsy and the source is left empty (defaults to "custom")', async () => { const mockOnNftAdded = jest.fn(); const { nftController } = setupController({ - onNftAdded: mockOnNftAdded, + options: { + onNftAdded: mockOnNftAdded, + }, }); const { selectedAddress, chainId } = nftController.config; // TODO: Replace `any` with type @@ -1527,9 +1571,11 @@ describe('NftController', () => { it('should add an nft and nftContract to state when all contract information is falsy and the source is "dapp"', async () => { const mockOnNftAdded = jest.fn(); const { nftController, changeNetwork } = setupController({ - onNftAdded: mockOnNftAdded, + options: { + onNftAdded: mockOnNftAdded, + }, }); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -1595,13 +1641,15 @@ describe('NftController', () => { it('should add an nft and nftContract when there is valid contract information and source is "detected"', async () => { const mockOnNftAdded = jest.fn(); const { nftController } = setupController({ - onNftAdded: mockOnNftAdded, - getERC721AssetName: jest - .fn() - .mockRejectedValue(new Error('Failed to fetch')), - getERC721AssetSymbol: jest - .fn() - .mockRejectedValue(new Error('Failed to fetch')), + options: { + onNftAdded: mockOnNftAdded, + getERC721AssetName: jest + .fn() + .mockRejectedValue(new Error('Failed to fetch')), + getERC721AssetSymbol: jest + .fn() + .mockRejectedValue(new Error('Failed to fetch')), + }, }); nock(NFT_API_BASE_URL) .get( @@ -1707,13 +1755,15 @@ describe('NftController', () => { it('should not add an nft and nftContract when there is not valid contract information (or an issue fetching it) and source is "detected"', async () => { const mockOnNftAdded = jest.fn(); const { nftController } = setupController({ - onNftAdded: mockOnNftAdded, - getERC721AssetName: jest - .fn() - .mockRejectedValue(new Error('Failed to fetch')), - getERC721AssetSymbol: jest - .fn() - .mockRejectedValue(new Error('Failed to fetch')), + options: { + onNftAdded: mockOnNftAdded, + getERC721AssetName: jest + .fn() + .mockRejectedValue(new Error('Failed to fetch')), + getERC721AssetSymbol: jest + .fn() + .mockRejectedValue(new Error('Failed to fetch')), + }, }); nock(NFT_API_BASE_URL) .get( @@ -1797,21 +1847,23 @@ describe('NftController', () => { it('should add NFT with metadata hosted in IPFS', async () => { const { nftController } = setupController({ - 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')), + 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')), + }, }); nftController.configure({ ipfsGateway: IPFS_DEFAULT_GATEWAY_URL, @@ -1909,24 +1961,31 @@ describe('NftController', () => { ); const { nftController } = setupController({ - 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'); - } - }), + 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'); + } + }), + }, + mockNetworkClientConfigurationsByNetworkClientId: { + 'customNetworkClientId-1': buildCustomNetworkClientConfiguration({ + chainId: '0xa', + }), + }, }); await nftController.addNft('0x01', '1234', { @@ -2022,36 +2081,38 @@ describe('NftController', () => { ); const { nftController, changeNetwork } = setupController({ - 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'); - } - }), + 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'); + } + }), + }, }); await nftController.addNft('0x01', '1234', { userAddress, }); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); await nftController.addNft('0x02', '4321', { userAddress, }); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); await nftController.addNft('0x03', '5678', { userAddress, @@ -2247,11 +2308,11 @@ describe('NftController', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any .stub(nftController, 'getNftInformation' as any) .returns({ name: 'name', image: 'url', description: 'description' }); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); await nftController.addNftVerifyOwnership('0x01', '1234', { userAddress: firstAddress, }); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); await nftController.addNftVerifyOwnership('0x02', '4321', { userAddress: secondAddress, }); @@ -2389,16 +2450,16 @@ describe('NftController', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any .stub(nftController, 'getNftInformation' as any) .returns({ name: 'name', image: 'url', description: 'description' }); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); await nftController.addNft('0x02', '4321'); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); await nftController.addNft('0x01', '1234'); nftController.removeNft('0x01', '1234'); expect( nftController.state.allNfts[selectedAddress][GOERLI.chainId], ).toHaveLength(0); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); expect( nftController.state.allNfts[selectedAddress][SEPOLIA.chainId][0], @@ -2420,7 +2481,7 @@ describe('NftController', () => { const userAddress1 = '0x123'; const userAddress2 = '0x321'; - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, @@ -2449,7 +2510,7 @@ describe('NftController', () => { isCurrentlyOwned: true, }); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, @@ -2508,8 +2569,10 @@ describe('NftController', () => { .fn() .mockRejectedValue(new Error('ERC1155 error')); const { nftController } = setupController({ - getERC721OwnerOf: mockGetERC721OwnerOf, - getERC1155BalanceOf: mockGetERC1155BalanceOf, + options: { + getERC721OwnerOf: mockGetERC721OwnerOf, + getERC1155BalanceOf: mockGetERC1155BalanceOf, + }, }); const isOwner = await nftController.isNftOwner( @@ -2527,8 +2590,10 @@ describe('NftController', () => { .fn() .mockRejectedValue(new Error('ERC1155 error')); const { nftController } = setupController({ - getERC721OwnerOf: mockGetERC721OwnerOf, - getERC1155BalanceOf: mockGetERC1155BalanceOf, + options: { + getERC721OwnerOf: mockGetERC721OwnerOf, + getERC1155BalanceOf: mockGetERC1155BalanceOf, + }, }); const isOwner = await nftController.isNftOwner( @@ -2545,8 +2610,10 @@ describe('NftController', () => { .fn() .mockRejectedValue(new Error('ERC1155 error')); const { nftController } = setupController({ - getERC721OwnerOf: mockGetERC721OwnerOf, - getERC1155BalanceOf: mockGetERC1155BalanceOf, + options: { + getERC721OwnerOf: mockGetERC721OwnerOf, + getERC1155BalanceOf: mockGetERC1155BalanceOf, + }, }); const isOwner = await nftController.isNftOwner( @@ -2563,8 +2630,10 @@ describe('NftController', () => { .mockRejectedValue(new Error('ERC721 error')); const mockGetERC1155BalanceOf = jest.fn().mockResolvedValue(new BN(1)); const { nftController } = setupController({ - getERC721OwnerOf: mockGetERC721OwnerOf, - getERC1155BalanceOf: mockGetERC1155BalanceOf, + options: { + getERC721OwnerOf: mockGetERC721OwnerOf, + getERC1155BalanceOf: mockGetERC1155BalanceOf, + }, }); const isOwner = await nftController.isNftOwner( @@ -2581,8 +2650,10 @@ describe('NftController', () => { .mockRejectedValue(new Error('ERC721 error')); const mockGetERC1155BalanceOf = jest.fn().mockResolvedValue(new BN(0)); const { nftController } = setupController({ - getERC721OwnerOf: mockGetERC721OwnerOf, - getERC1155BalanceOf: mockGetERC1155BalanceOf, + options: { + getERC721OwnerOf: mockGetERC721OwnerOf, + getERC1155BalanceOf: mockGetERC1155BalanceOf, + }, }); const isOwner = await nftController.isNftOwner( @@ -2602,8 +2673,10 @@ describe('NftController', () => { .fn() .mockRejectedValue(new Error('ERC1155 error')); const { nftController } = setupController({ - getERC721OwnerOf: mockGetERC721OwnerOf, - getERC1155BalanceOf: mockGetERC1155BalanceOf, + options: { + 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."; @@ -2862,7 +2935,7 @@ describe('NftController', () => { const userAddress1 = '0x123'; const userAddress2 = '0x321'; - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, @@ -2885,7 +2958,7 @@ describe('NftController', () => { }), ); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, @@ -3015,7 +3088,7 @@ describe('NftController', () => { openSeaEnabled: true, selectedAddress: OWNER_ADDRESS, }); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); const { selectedAddress, chainId } = nftController.config; await nftController.addNft('0x02', '1', { @@ -3042,7 +3115,7 @@ describe('NftController', () => { openSeaEnabled: true, selectedAddress: SECOND_OWNER_ADDRESS, }); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); await nftController.checkAndUpdateAllNftsOwnershipStatus({ userAddress: OWNER_ADDRESS, @@ -3137,7 +3210,7 @@ describe('NftController', () => { openSeaEnabled: true, selectedAddress: OWNER_ADDRESS, }); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); const { selectedAddress, chainId } = nftController.config; const nft = { @@ -3168,7 +3241,7 @@ describe('NftController', () => { openSeaEnabled: true, selectedAddress: SECOND_OWNER_ADDRESS, }); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); await nftController.checkAndUpdateSingleNftOwnershipStatus(nft, false, { userAddress: OWNER_ADDRESS, @@ -3190,7 +3263,7 @@ describe('NftController', () => { openSeaEnabled: true, selectedAddress: OWNER_ADDRESS, }); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); const { selectedAddress, chainId } = nftController.config; const nft = { @@ -3221,7 +3294,7 @@ describe('NftController', () => { openSeaEnabled: true, selectedAddress: SECOND_OWNER_ADDRESS, }); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); const updatedNft = await nftController.checkAndUpdateSingleNftOwnershipStatus( @@ -3772,7 +3845,7 @@ describe('NftController', () => { it('should trigger calling updateNftMetadata when preferences change - openseaEnabled', async () => { const { nftController, triggerPreferencesStateChange, changeNetwork } = setupController(); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); const spy = jest.spyOn(nftController, 'updateNftMetadata'); const testNetworkClientId = 'sepolia'; @@ -3815,7 +3888,7 @@ describe('NftController', () => { it('should trigger calling updateNftMetadata when preferences change - ipfs enabled', async () => { const { nftController, triggerPreferencesStateChange, changeNetwork } = setupController(); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); const spy = jest.spyOn(nftController, 'updateNftMetadata'); const testNetworkClientId = 'sepolia'; diff --git a/packages/assets-controllers/src/NftController.ts b/packages/assets-controllers/src/NftController.ts index 580f1a7c39d..ff712bf8bf3 100644 --- a/packages/assets-controllers/src/NftController.ts +++ b/packages/assets-controllers/src/NftController.ts @@ -21,6 +21,7 @@ import { import type { NetworkClientId, NetworkController, + NetworkControllerGetNetworkClientByIdAction, NetworkState, } from '@metamask/network-controller'; import type { PreferencesState } from '@metamask/preferences-controller'; @@ -221,7 +222,9 @@ const controllerName = 'NftController'; /** * The external actions available to the {@link NftController}. */ -type AllowedActions = AddApprovalRequest; +type AllowedActions = + | AddApprovalRequest + | NetworkControllerGetNetworkClientByIdAction; /** * The messenger of the {@link NftController}. @@ -1075,8 +1078,12 @@ export class NftController extends BaseControllerV1 { }, ); - onNetworkStateChange(({ providerConfig }) => { - const { chainId } = providerConfig; + onNetworkStateChange(({ selectedNetworkClientId }) => { + const selectedNetworkClient = getNetworkClientById( + selectedNetworkClientId, + ); + const { chainId } = selectedNetworkClient.configuration; + this.configure({ chainId }); }); }