diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index ac64324b18..3facaa004f 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -13,6 +13,7 @@ import { toHex, BUILT_IN_NETWORKS, ORIGIN_METAMASK, + InfuraNetworkType, } from '@metamask/controller-utils'; import type { SafeEventEmitterProvider } from '@metamask/eth-json-rpc-provider'; import EthQuery from '@metamask/eth-query'; @@ -21,20 +22,31 @@ import type { InternalAccount } from '@metamask/keyring-api'; import { EthAccountType } from '@metamask/keyring-api'; import type { BlockTracker, - NetworkController, + NetworkClientConfiguration, + NetworkClientId, NetworkState, Provider, } from '@metamask/network-controller'; -import { NetworkClientType, NetworkStatus } from '@metamask/network-controller'; +import { + NetworkClientType, + NetworkStatus, + defaultState as defaultNetworkState, +} from '@metamask/network-controller'; import * as NonceTrackerPackage from '@metamask/nonce-tracker'; import { errorCodes, providerErrors, rpcErrors } from '@metamask/rpc-errors'; +import type { Hex } from '@metamask/utils'; import { createDeferredPromise } from '@metamask/utils'; import assert from 'assert'; import * as uuidModule from 'uuid'; import { FakeBlockTracker } from '../../../tests/fake-block-tracker'; +import { FakeProvider } from '../../../tests/fake-provider'; import { flushPromises } from '../../../tests/helpers'; import { mockNetwork } from '../../../tests/mock-network'; +import { + buildCustomNetworkClientConfiguration, + buildMockGetNetworkClientById, +} from '../../network-controller/tests/helpers'; import { DefaultGasFeeFlow } from './gas-flows/DefaultGasFeeFlow'; import { LineaGasFeeFlow } from './gas-flows/LineaGasFeeFlow'; import { TestGasFeeFlow } from './gas-flows/TestGasFeeFlow'; @@ -85,13 +97,6 @@ type UnrestrictedControllerMessenger = ControllerMessenger< TransactionControllerEvents | AllowedEvents >; -type NetworkClient = ReturnType; - -type NetworkClientConfiguration = Pick< - NetworkClient['configuration'], - 'chainId' ->; - const MOCK_V1_UUID = '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'; jest.mock('@metamask/eth-query'); @@ -301,9 +306,6 @@ function waitForTransactionFinished( const MOCK_PREFERENCES = { state: { selectedAddress: 'foo' } }; const INFURA_PROJECT_ID = '341eacb578dd44a1a049cbc5f6fd4035'; -const GOERLI_PROVIDER = new HttpProvider( - `https://goerli.infura.io/v3/${INFURA_PROJECT_ID}`, -); const MAINNET_PROVIDER = new HttpProvider( `https://mainnet.infura.io/v3/${INFURA_PROJECT_ID}`, ); @@ -312,6 +314,7 @@ const PALM_PROVIDER = new HttpProvider( ); type MockNetwork = { + chainId: Hex; provider: Provider; blockTracker: BlockTracker; state: NetworkState; @@ -319,6 +322,7 @@ type MockNetwork = { }; const MOCK_NETWORK: MockNetwork = { + chainId: ChainId.goerli, provider: MAINNET_PROVIDER, blockTracker: buildMockBlockTracker('0x102833C', MAINNET_PROVIDER), state: { @@ -338,25 +342,9 @@ const MOCK_NETWORK: MockNetwork = { }, subscribe: () => undefined, }; -const MOCK_NETWORK_WITHOUT_CHAIN_ID: MockNetwork = { - provider: GOERLI_PROVIDER, - blockTracker: buildMockBlockTracker('0x102833C', GOERLI_PROVIDER), - state: { - selectedNetworkClientId: NetworkType.goerli, - networksMetadata: { - [NetworkType.goerli]: { - EIPS: { 1559: false }, - status: NetworkStatus.Available, - }, - }, - providerConfig: { - type: NetworkType.goerli, - } as NetworkState['providerConfig'], - networkConfigurations: {}, - }, - subscribe: () => undefined, -}; + const MOCK_MAINNET_NETWORK: MockNetwork = { + chainId: ChainId.mainnet, provider: MAINNET_PROVIDER, blockTracker: buildMockBlockTracker('0x102833C', MAINNET_PROVIDER), state: { @@ -378,6 +366,7 @@ const MOCK_MAINNET_NETWORK: MockNetwork = { }; const MOCK_LINEA_MAINNET_NETWORK: MockNetwork = { + chainId: ChainId['linea-mainnet'], provider: PALM_PROVIDER, blockTracker: buildMockBlockTracker('0xA6EDFC', PALM_PROVIDER), state: { @@ -390,7 +379,7 @@ const MOCK_LINEA_MAINNET_NETWORK: MockNetwork = { }, providerConfig: { type: NetworkType['linea-mainnet'], - chainId: toHex(59144), + chainId: ChainId['linea-mainnet'], ticker: NetworksTicker['linea-mainnet'], }, networkConfigurations: {}, @@ -399,6 +388,7 @@ const MOCK_LINEA_MAINNET_NETWORK: MockNetwork = { }; const MOCK_LINEA_GOERLI_NETWORK: MockNetwork = { + chainId: ChainId['linea-goerli'], provider: PALM_PROVIDER, blockTracker: buildMockBlockTracker('0xA6EDFC', PALM_PROVIDER), state: { @@ -411,7 +401,7 @@ const MOCK_LINEA_GOERLI_NETWORK: MockNetwork = { }, providerConfig: { type: NetworkType['linea-goerli'], - chainId: toHex(59140), + chainId: ChainId['linea-goerli'], ticker: NetworksTicker['linea-goerli'], }, networkConfigurations: {}, @@ -419,27 +409,6 @@ const MOCK_LINEA_GOERLI_NETWORK: MockNetwork = { subscribe: () => undefined, }; -const MOCK_CUSTOM_NETWORK: MockNetwork = { - provider: PALM_PROVIDER, - blockTracker: buildMockBlockTracker('0xA6EDFC', PALM_PROVIDER), - state: { - selectedNetworkClientId: 'uuid-1', - networksMetadata: { - 'uuid-1': { - EIPS: { 1559: false }, - status: NetworkStatus.Available, - }, - }, - providerConfig: { - type: NetworkType.rpc, - chainId: toHex(11297108109), - ticker: 'TEST', - }, - networkConfigurations: {}, - }, - subscribe: () => undefined, -}; - const ACCOUNT_MOCK = '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207'; const INTERNAL_ACCOUNT_MOCK = { id: '58def058-d35f-49a1-a7ab-e2580565f6f5', @@ -458,7 +427,7 @@ const INTERNAL_ACCOUNT_MOCK = { const ACCOUNT_2_MOCK = '0x08f137f335ea1b8f193b8f6ea92561a60d23a211'; const NONCE_MOCK = 12; const ACTION_ID_MOCK = '123456'; -const CHAIN_ID_MOCK = MOCK_NETWORK.state.providerConfig.chainId; +const CHAIN_ID_MOCK = MOCK_NETWORK.chainId; const NETWORK_CLIENT_ID_MOCK = 'networkClientIdMock'; const TRANSACTION_META_MOCK = { @@ -563,30 +532,84 @@ describe('TransactionController', () => { * @param args - The arguments to this function. * @param args.options - TransactionController options. * @param args.network - The mock network to use with the controller. + * @param args.network.blockTracker - The desired block tracker associated + * with the network. + * @param args.network.provider - The desired provider associated with the + * provider. + * @param args.network.state - The desired NetworkController state. + * @param args.network.onNetworkStateChange - The function to subscribe to + * changes in the NetworkController state. * @param args.messengerOptions - Options to build the mock unrestricted * messenger. - * @param args.messengerOptions.addTransactionApprovalRequest - Options to mock - * the `ApprovalController:addRequest` action call for transactions. + * @param args.messengerOptions.addTransactionApprovalRequest - Options to + * mock the `ApprovalController:addRequest` action call for transactions. * @param args.selectedAccount - The selected account to use with the controller. + * @param args.mockNetworkClientConfigurationsByNetworkClientId - Network + * client configurations by network client ID. * @returns The new TransactionController instance. */ function setupController({ options: givenOptions = {}, - network = MOCK_NETWORK, + network = {}, messengerOptions = {}, selectedAccount = INTERNAL_ACCOUNT_MOCK, + mockNetworkClientConfigurationsByNetworkClientId = {}, }: { options?: Partial[0]>; - network?: MockNetwork; + network?: { + blockTracker?: BlockTracker; + provider?: Provider; + state?: Partial; + onNetworkStateChange?: ( + listener: (networkState: NetworkState) => void, + ) => void; + }; messengerOptions?: { addTransactionApprovalRequest?: Parameters< typeof mockAddTransactionApprovalRequest >[1]; }; selectedAccount?: InternalAccount; + mockNetworkClientConfigurationsByNetworkClientId?: Record< + NetworkClientId, + NetworkClientConfiguration + >; } = {}) { + let networkState = { + ...defaultNetworkState, + selectedNetworkClientId: MOCK_NETWORK.state.selectedNetworkClientId, + ...network.state, + }; + const provider = network.provider ?? new FakeProvider(); + const blockTracker = + network.blockTracker ?? new FakeBlockTracker({ provider }); + const onNetworkDidChangeListeners: ((state: NetworkState) => void)[] = []; + const changeNetwork = ({ + selectedNetworkClientId, + }: { + selectedNetworkClientId: NetworkClientId; + }) => { + networkState = { + ...networkState, + selectedNetworkClientId, + }; + onNetworkDidChangeListeners.forEach((listener) => { + listener(networkState); + }); + }; + const onNetworkStateChange = ( + listener: (networkState: NetworkState) => void, + ) => onNetworkDidChangeListeners.push(listener); + const unrestrictedMessenger: UnrestrictedControllerMessenger = new ControllerMessenger(); + const getNetworkClientById = buildMockGetNetworkClientById( + mockNetworkClientConfigurationsByNetworkClientId, + ); + unrestrictedMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + getNetworkClientById, + ); const { addTransactionApprovalRequest = { state: 'pending' } } = messengerOptions; @@ -596,20 +619,20 @@ describe('TransactionController', () => { ); const { messenger: givenRestrictedMessenger, ...otherOptions } = { - blockTracker: network.blockTracker, + blockTracker, disableHistory: false, disableSendFlowHistory: false, disableSwaps: false, getCurrentNetworkEIP1559Compatibility: async () => false, - getNetworkState: () => network.state, + getNetworkState: () => networkState, // TODO: Replace with a real type // eslint-disable-next-line @typescript-eslint/no-explicit-any getNetworkClientRegistry: () => ({} as any), getPermittedAccounts: async () => [ACCOUNT_MOCK], isMultichainEnabled: false, hooks: {}, - onNetworkStateChange: network.subscribe, - provider: network.provider, + onNetworkStateChange, + provider, sign: async (transaction: TypedTransaction) => transaction, transactionHistoryLimit: 40, ...givenOptions, @@ -644,6 +667,7 @@ describe('TransactionController', () => { messenger: unrestrictedMessenger, mockTransactionApprovalRequest, mockGetSelectedAccount, + changeNetwork, }; } @@ -652,17 +676,18 @@ describe('TransactionController', () => { * TransactionController calls as it creates transactions. * * This helper allows the `addRequest` action to be in one of three states: - * approved, rejected, or pending. In the approved state, the promise which the - * action returns is resolved ahead of time, and in the rejected state, the - * promise is rejected ahead of time. Otherwise, in the pending state, the - * promise is unresolved and it is assumed that the test will resolve or reject - * the promise. + * approved, rejected, or pending. In the approved state, the promise which + * the action returns is resolved ahead of time, and in the rejected state, + * the promise is rejected ahead of time. Otherwise, in the pending state, the + * promise is unresolved and it is assumed that the test will resolve or + * reject the promise. * * @param messenger - The unrestricted messenger. - * @param options - Options for the mock. `state` controls the state of the - * promise as outlined above. Note, if the `state` is approved, then its - * `result` may be specified; if the `state` is rejected, then its `error` may - * be specified. + * @param options - An options bag which will be used to create an action + * handler that places the approval request in a certain state. The `state` + * option controls the state of the promise as outlined above: if the `state` + * is approved, then its `result` may be specified; if the `state` is + * rejected, then its `error` may be specified. * @returns An object which contains the aforementioned promise, functions to * manually approve or reject the approval (and therefore the promise), and * finally the mocked version of the action handler itself. @@ -718,7 +743,6 @@ describe('TransactionController', () => { ReturnType, Parameters > = jest.fn().mockReturnValue(promise); - if (options.state === 'approved') { approveTransaction(options.result); } else if (options.state === 'rejected') { @@ -738,22 +762,6 @@ describe('TransactionController', () => { }; } - /** - * Builds a network client that is only used in tests to get a chain ID. - * - * @param networkClient - The properties in the desired network client. - * Only needs to contain `configuration`. - * @param networkClient.configuration - The desired network client - * configuration. Only needs to contain `chainId`> - * @returns The network client. - */ - function buildMockNetworkClient(networkClient: { - configuration: NetworkClientConfiguration; - }): NetworkClient { - // Type assertion: We don't expect anything but the configuration to get used. - return networkClient as unknown as NetworkClient; - } - /** * Wait for a specified number of milliseconds. * @@ -796,18 +804,19 @@ describe('TransactionController', () => { return incomingTransactionHelperMock; }); + pendingTransactionTrackerMock = { + start: jest.fn(), + stop: jest.fn(), + startIfPendingTransactions: jest.fn(), + hub: { + on: jest.fn(), + removeAllListeners: jest.fn(), + }, + onStateChange: jest.fn(), + forceCheckTransaction: jest.fn(), + } as unknown as jest.Mocked; + pendingTransactionTrackerClassMock.mockImplementation(() => { - pendingTransactionTrackerMock = { - start: jest.fn(), - stop: jest.fn(), - startIfPendingTransactions: jest.fn(), - hub: { - on: jest.fn(), - removeAllListeners: jest.fn(), - }, - onStateChange: jest.fn(), - forceCheckTransaction: jest.fn(), - } as unknown as jest.Mocked; return pendingTransactionTrackerMock; }); @@ -938,7 +947,7 @@ describe('TransactionController', () => { transactions: [ { ...TRANSACTION_META_MOCK, - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, status: TransactionStatus.submitted, }, ], @@ -1346,9 +1355,7 @@ describe('TransactionController', () => { expect(updateSwapsTransactionMock).toHaveBeenCalledTimes(1); expect(transactionMeta.txParams.from).toBe(ACCOUNT_MOCK); - expect(transactionMeta.chainId).toBe( - MOCK_NETWORK.state.providerConfig.chainId, - ); + expect(transactionMeta.chainId).toBe(MOCK_NETWORK.chainId); expect(transactionMeta.deviceConfirmedOn).toBe(mockDeviceConfirmedOn); expect(transactionMeta.origin).toBe(mockOrigin); expect(transactionMeta.status).toBe(TransactionStatus.unapproved); @@ -1362,26 +1369,9 @@ describe('TransactionController', () => { describe('networkClientId exists in the MultichainTrackingHelper', () => { it('adds unapproved transaction to state when using networkClientId', async () => { - const { controller, messenger } = setupController({ + const { controller } = setupController({ options: { isMultichainEnabled: true }, }); - messenger.registerActionHandler( - 'NetworkController:getNetworkClientById', - (networkClientId) => { - switch (networkClientId) { - case 'sepolia': - return buildMockNetworkClient({ - configuration: { - chainId: ChainId.sepolia, - }, - }); - default: - throw new Error( - `Unknown network client ID: ${networkClientId}`, - ); - } - }, - ); const sepoliaTxParams: TransactionParams = { chainId: ChainId.sepolia, from: ACCOUNT_MOCK, @@ -1393,7 +1383,7 @@ describe('TransactionController', () => { await controller.addTransaction(sepoliaTxParams, { origin: 'metamask', actionId: ACTION_ID_MOCK, - networkClientId: 'sepolia', + networkClientId: InfuraNetworkType.sepolia, }); const transactionMeta = controller.state.transactions[0]; @@ -1415,23 +1405,6 @@ describe('TransactionController', () => { }, }, }); - messenger.registerActionHandler( - 'NetworkController:getNetworkClientById', - (networkClientId) => { - switch (networkClientId) { - case 'sepolia': - return buildMockNetworkClient({ - configuration: { - chainId: ChainId.sepolia, - }, - }); - default: - throw new Error( - `Unknown network client ID: ${networkClientId}`, - ); - } - }, - ); multichainTrackingHelperMock.has.mockReturnValue(true); const submittedEventListener = jest.fn(); @@ -1449,7 +1422,7 @@ describe('TransactionController', () => { const { result } = await controller.addTransaction(sepoliaTxParams, { origin: 'metamask', actionId: ACTION_ID_MOCK, - networkClientId: 'sepolia', + networkClientId: InfuraNetworkType.sepolia, }); await result; @@ -1547,49 +1520,54 @@ describe('TransactionController', () => { ); }); - it.each([ - ['mainnet', MOCK_MAINNET_NETWORK], - ['custom network', MOCK_CUSTOM_NETWORK], - ])( - 'adds unapproved transaction to state after switching to %s', - async (_networkName, newNetwork) => { - const getNetworkState = jest.fn().mockReturnValue(MOCK_NETWORK.state); - - let networkStateChangeListener: ((state: NetworkState) => void) | null = - null; - - const onNetworkStateChange = ( - listener: (state: NetworkState) => void, - ) => { - networkStateChangeListener = listener; - }; + it('adds unapproved transaction to state after switching to Infura network', async () => { + const { controller, changeNetwork } = setupController({ + network: { + state: { + selectedNetworkClientId: InfuraNetworkType.goerli, + }, + }, + }); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.mainnet }); - const { controller } = setupController({ - options: { getNetworkState, onNetworkStateChange }, - }); + await controller.addTransaction({ + from: ACCOUNT_MOCK, + to: ACCOUNT_MOCK, + }); - // switch from Goerli to Mainnet - getNetworkState.mockReturnValue(newNetwork.state); + expect(controller.state.transactions[0].txParams.from).toBe(ACCOUNT_MOCK); + expect(controller.state.transactions[0].chainId).toBe(ChainId.mainnet); + expect(controller.state.transactions[0].status).toBe( + TransactionStatus.unapproved, + ); + }); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - networkStateChangeListener!(newNetwork.state); + it('adds unapproved transaction to state after switching to custom network', async () => { + const { controller, changeNetwork } = setupController({ + network: { + state: { + selectedNetworkClientId: InfuraNetworkType.goerli, + }, + }, + mockNetworkClientConfigurationsByNetworkClientId: { + 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + chainId: '0x1337', + }), + }, + }); + changeNetwork({ selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD' }); - await controller.addTransaction({ - from: ACCOUNT_MOCK, - to: ACCOUNT_MOCK, - }); + await controller.addTransaction({ + from: ACCOUNT_MOCK, + to: ACCOUNT_MOCK, + }); - expect(controller.state.transactions[0].txParams.from).toBe( - ACCOUNT_MOCK, - ); - expect(controller.state.transactions[0].chainId).toBe( - newNetwork.state.providerConfig.chainId, - ); - expect(controller.state.transactions[0].status).toBe( - TransactionStatus.unapproved, - ); - }, - ); + expect(controller.state.transactions[0].txParams.from).toBe(ACCOUNT_MOCK); + expect(controller.state.transactions[0].chainId).toBe('0x1337'); + expect(controller.state.transactions[0].status).toBe( + TransactionStatus.unapproved, + ); + }); it('throws if address invalid', async () => { const { controller } = setupController(); @@ -1653,15 +1631,19 @@ describe('TransactionController', () => { it('requests approval using the approval controller', async () => { const { controller, messenger } = setupController(); - jest.spyOn(messenger, 'call'); + const messengerCallSpy = jest.spyOn(messenger, 'call'); await controller.addTransaction({ from: ACCOUNT_MOCK, to: ACCOUNT_MOCK, }); - expect(messenger.call).toHaveBeenCalledTimes(1); - expect(messenger.call).toHaveBeenCalledWith( + expect( + messengerCallSpy.mock.calls.filter( + (args) => args[0] === 'ApprovalController:addRequest', + ), + ).toHaveLength(1); + expect(messengerCallSpy).toHaveBeenCalledWith( 'ApprovalController:addRequest', { id: expect.any(String), @@ -1688,7 +1670,9 @@ describe('TransactionController', () => { }, ); - expect(messenger.call).toHaveBeenCalledTimes(0); + expect(messenger.call).not.toHaveBeenCalledWith( + 'ApprovalController:addRequest', + ); }); it('calls security provider with transaction meta and sets response in to securityProviderResponse', async () => { @@ -1742,9 +1726,9 @@ describe('TransactionController', () => { expect(updateGasMock).toHaveBeenCalledTimes(1); expect(updateGasMock).toHaveBeenCalledWith({ ethQuery: expect.any(Object), - chainId: MOCK_NETWORK.state.providerConfig.chainId, - isCustomNetwork: - MOCK_NETWORK.state.providerConfig.type === NetworkType.rpc, + chainId: MOCK_NETWORK.chainId, + // XXX: Is this right? + isCustomNetwork: false, txMeta: expect.any(Object), }); }); @@ -1788,7 +1772,7 @@ describe('TransactionController', () => { expect(getSimulationDataMock).toHaveBeenCalledTimes(1); expect(getSimulationDataMock).toHaveBeenCalledWith({ - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, data: undefined, from: ACCOUNT_MOCK, to: ACCOUNT_MOCK, @@ -2034,7 +2018,18 @@ describe('TransactionController', () => { it('if no chainId defined', async () => { const { controller } = setupController({ - network: MOCK_NETWORK_WITHOUT_CHAIN_ID, + mockNetworkClientConfigurationsByNetworkClientId: { + 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + rpcUrl: 'https://test.network', + ticker: 'TEST', + chainId: undefined, + }), + }, + network: { + state: { + selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', + }, + }, messengerOptions: { addTransactionApprovalRequest: { state: 'approved', @@ -2046,10 +2041,13 @@ describe('TransactionController', () => { }); it('if unexpected status', async () => { - const { controller, messenger } = setupController(); - - jest.spyOn(messenger, 'call').mockImplementationOnce(() => { - throw new Error('Unknown problem'); + const { controller } = setupController({ + messengerOptions: { + addTransactionApprovalRequest: { + state: TransactionStatus.rejected, + error: new Error('Unknown problem'), + }, + }, }); const { result } = await controller.addTransaction({ @@ -2064,10 +2062,13 @@ describe('TransactionController', () => { }); it('if unrecognised error', async () => { - const { controller, messenger } = setupController(); - - jest.spyOn(messenger, 'call').mockImplementationOnce(() => { - throw new Error('TestError'); + const { controller } = setupController({ + messengerOptions: { + addTransactionApprovalRequest: { + state: TransactionStatus.rejected, + error: new Error('TestError'), + }, + }, }); const { result } = await controller.addTransaction({ @@ -2082,12 +2083,8 @@ describe('TransactionController', () => { }); it('if transaction removed', async () => { - const { controller, messenger } = setupController(); - - jest.spyOn(messenger, 'call').mockImplementationOnce(() => { - controller.clearUnapprovedTransactions(); - throw new Error('Unknown problem'); - }); + const { controller, mockTransactionApprovalRequest } = + setupController(); const { result } = await controller.addTransaction({ from: ACCOUNT_MOCK, @@ -2097,6 +2094,9 @@ describe('TransactionController', () => { value: '0x0', }); + controller.clearUnapprovedTransactions(); + mockTransactionApprovalRequest.reject(new Error('Unknown problem')); + await expect(result).rejects.toThrow('Unknown problem'); }); }); @@ -4082,7 +4082,7 @@ describe('TransactionController', () => { const confirmed = { ...TRANSACTION_META_MOCK, id: 'testId1', - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, hash: '0x3', status: TransactionStatus.confirmed, txParams: { ...TRANSACTION_META_MOCK.txParams, nonce: '0x1' }, @@ -4294,7 +4294,7 @@ describe('TransactionController', () => { gas: '0x222', to: ACCOUNT_2_MOCK, value: '0x1', - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, }; await expect( @@ -4324,7 +4324,7 @@ describe('TransactionController', () => { gas: '0x5208', to: ACCOUNT_2_MOCK, value: '0x0', - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, }; // Send the transaction to put it in the process of being signed @@ -4355,7 +4355,7 @@ describe('TransactionController', () => { gas: '0x111', to: ACCOUNT_2_MOCK, value: '0x0', - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, }; const mockTransactionParam2 = { from: ACCOUNT_MOCK, @@ -4363,7 +4363,7 @@ describe('TransactionController', () => { gas: '0x222', to: ACCOUNT_2_MOCK, value: '0x1', - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, }; const result = await controller.approveTransactionsWithSameNonce([ @@ -4394,7 +4394,7 @@ describe('TransactionController', () => { gas: '0x111', to: ACCOUNT_2_MOCK, value: '0x0', - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, }; const mockTransactionParam2 = { from: ACCOUNT_MOCK, @@ -4402,7 +4402,7 @@ describe('TransactionController', () => { gas: '0x222', to: ACCOUNT_2_MOCK, value: '0x1', - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, }; await expect( @@ -4422,7 +4422,7 @@ describe('TransactionController', () => { gas: '0x111', to: ACCOUNT_2_MOCK, value: '0x0', - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, }; const mockTransactionParam2 = { @@ -4431,7 +4431,7 @@ describe('TransactionController', () => { gas: '0x222', to: ACCOUNT_2_MOCK, value: '0x1', - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, }; await controller.approveTransactionsWithSameNonce( @@ -4455,7 +4455,7 @@ describe('TransactionController', () => { gas: '0x111', to: ACCOUNT_2_MOCK, value: '0x0', - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, }; const mockTransactionParam2 = { @@ -4464,7 +4464,7 @@ describe('TransactionController', () => { gas: '0x222', to: ACCOUNT_2_MOCK, value: '0x1', - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, }; await controller.approveTransactionsWithSameNonce([ @@ -5036,13 +5036,17 @@ describe('TransactionController', () => { state: mockedControllerState as any, }, }); - jest.spyOn(messenger, 'call'); + const messengerCallSpy = jest.spyOn(messenger, 'call'); controller.initApprovals(); await flushPromises(); - expect(messenger.call).toHaveBeenCalledTimes(2); - expect(messenger.call).toHaveBeenCalledWith( + expect( + messengerCallSpy.mock.calls.filter( + (args) => args[0] === 'ApprovalController:addRequest', + ), + ).toHaveLength(2); + expect(messengerCallSpy).toHaveBeenCalledWith( 'ApprovalController:addRequest', { expectsResult: true, @@ -5053,7 +5057,7 @@ describe('TransactionController', () => { }, false, ); - expect(messenger.call).toHaveBeenCalledWith( + expect(messengerCallSpy).toHaveBeenCalledWith( 'ApprovalController:addRequest', { expectsResult: true, @@ -5098,16 +5102,17 @@ describe('TransactionController', () => { const mockedErrorMessage = 'mocked error'; - const { controller, messenger } = setupController({ - options: { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - state: mockedControllerState as any, - }, - }); + const { controller, messenger, mockTransactionApprovalRequest } = + setupController({ + options: { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + state: mockedControllerState as any, + }, + }); + const messengerCallSpy = jest.spyOn(messenger, 'call'); // Expect both calls to throw error, one with code property to check if it is handled - jest - .spyOn(messenger, 'call') + mockTransactionApprovalRequest.actionHandlerMock .mockImplementationOnce(() => { // eslint-disable-next-line @typescript-eslint/no-throw-literal throw { message: mockedErrorMessage }; @@ -5130,7 +5135,11 @@ describe('TransactionController', () => { 'Error during persisted transaction approval', new Error(mockedErrorMessage), ); - expect(messenger.call).toHaveBeenCalledTimes(2); + expect( + messengerCallSpy.mock.calls.filter((args) => { + return args[0] === 'ApprovalController:addRequest'; + }), + ).toHaveLength(2); }); it('does not create any approval when there is no unapproved transaction', async () => { @@ -5138,7 +5147,9 @@ describe('TransactionController', () => { jest.spyOn(messenger, 'call'); controller.initApprovals(); await flushPromises(); - expect(messenger.call).not.toHaveBeenCalled(); + expect(messenger.call).not.toHaveBeenCalledWith( + 'ApprovalController:addRequest', + ); }); }); @@ -5304,7 +5315,7 @@ describe('TransactionController', () => { it('returns transactions matching current network', () => { const transactions: TransactionMeta[] = [ { - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, id: 'testId1', status: TransactionStatus.confirmed, time: 1, @@ -5318,7 +5329,7 @@ describe('TransactionController', () => { txParams: { from: '0x2' }, }, { - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, id: 'testId3', status: TransactionStatus.submitted, time: 1, diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index ecbb49a4b3..cb3b677fe9 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -16,10 +16,10 @@ import type { import { BaseController } from '@metamask/base-controller'; import { query, - NetworkType, ApprovalType, ORIGIN_METAMASK, convertHexToDecimal, + isInfuraNetworkType, } from '@metamask/controller-utils'; import EthQuery from '@metamask/eth-query'; import type { @@ -37,11 +37,11 @@ import type { NetworkControllerGetNetworkClientByIdAction, } from '@metamask/network-controller'; import { NetworkClientType } from '@metamask/network-controller'; -import { NonceTracker } from '@metamask/nonce-tracker'; import type { NonceLock, Transaction as NonceTrackerTransaction, } from '@metamask/nonce-tracker'; +import { NonceTracker } from '@metamask/nonce-tracker'; import { errorCodes, rpcErrors, providerErrors } from '@metamask/rpc-errors'; import type { Hex } from '@metamask/utils'; import { add0x } from '@metamask/utils'; @@ -3742,6 +3742,7 @@ export class TransactionController extends BaseController< const finalTransactionMeta = this.getTransaction(transactionId); + /* istanbul ignore if */ if (!finalTransactionMeta) { log( 'Cannot update simulation data as transaction not found', @@ -3823,14 +3824,19 @@ export class TransactionController extends BaseController< } #getGlobalChainId() { - return this.getNetworkState().providerConfig.chainId; + return this.messagingSystem.call( + `NetworkController:getNetworkClientById`, + this.getNetworkState().selectedNetworkClientId, + ).configuration.chainId; } #isCustomNetwork(networkClientId?: NetworkClientId) { const globalNetworkClientId = this.#getGlobalNetworkClientId(); if (!networkClientId || networkClientId === globalNetworkClientId) { - return this.getNetworkState().providerConfig.type === NetworkType.rpc; + return !isInfuraNetworkType( + this.getNetworkState().selectedNetworkClientId, + ); } return (