diff --git a/packages/transaction-controller/jest.config.js b/packages/transaction-controller/jest.config.js index fe9ca89faba..ac5f2ff4e1a 100644 --- a/packages/transaction-controller/jest.config.js +++ b/packages/transaction-controller/jest.config.js @@ -18,7 +18,7 @@ module.exports = merge(baseConfig, { coverageThreshold: { global: { branches: 81.29, - functions: 95.14, + functions: 94.49, lines: 95.24, statements: 95.34, }, diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 2c2263971e1..1f4649a8087 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -1528,4 +1528,56 @@ describe('TransactionController', () => { }, ); }); + + describe('initApprovals', () => { + it('creates approvals for all unapproved transaction', async () => { + const transaction = { + from: ACCOUNT_MOCK, + id: 'mocked', + networkID: '5', + status: TransactionStatus.unapproved, + transactionHash: '1337', + }; + const controller = newController(); + controller.state.transactions.push(transaction as any); + controller.state.transactions.push({ + ...transaction, + id: 'mocked1', + transactionHash: '1338', + } as any); + + controller.initApprovals(); + + expect(delayMessengerMock.call).toHaveBeenCalledTimes(2); + expect(delayMessengerMock.call).toHaveBeenCalledWith( + 'ApprovalController:addRequest', + { + expectsResult: true, + id: 'mocked', + origin: 'metamask', + requestData: { txId: 'mocked' }, + type: 'transaction', + }, + false, + ); + expect(delayMessengerMock.call).toHaveBeenCalledWith( + 'ApprovalController:addRequest', + { + expectsResult: true, + id: 'mocked1', + origin: 'metamask', + requestData: { txId: 'mocked1' }, + type: 'transaction', + }, + false, + ); + }); + + it('does not create any approval when there is no unapproved transaction', async () => { + const controller = newController(); + controller.initApprovals(); + + expect(delayMessengerMock.call).not.toHaveBeenCalled(); + }); + }); }); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 4c089ffeec8..2fe455791b1 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -30,6 +30,7 @@ import type { NetworkState, Provider, } from '@metamask/network-controller'; +import type { Hex } from '@metamask/utils'; import { Mutex } from 'async-mutex'; import MethodRegistry from 'eth-method-registry'; import { errorCodes, ethErrors } from 'eth-rpc-errors'; @@ -53,6 +54,7 @@ import { validateGasValues, validateMinimumIncrease, ESTIMATE_GAS_ERROR, + transactionMatchesNetwork, } from './utils'; export const HARDFORK = Hardfork.London; @@ -350,7 +352,7 @@ export class TransactionController extends BaseController< origin?: string; } = {}, ): Promise { - const { providerConfig, networkId } = this.getNetworkState(); + const { chainId, networkId } = this.getChainAndNetworkId(); const { transactions } = this.state; transaction = normalizeTransaction(transaction); validateTransaction(transaction); @@ -358,7 +360,7 @@ export class TransactionController extends BaseController< const transactionMeta: TransactionMeta = { id: random(), networkID: networkId ?? undefined, - chainId: providerConfig.chainId, + chainId, origin, status: TransactionStatus.unapproved as TransactionStatus.unapproved, time: Date.now(), @@ -393,6 +395,27 @@ export class TransactionController extends BaseController< }); } + /** + * Creates approvals for all unapproved transactions persisted. + */ + initApprovals() { + const { networkId, chainId } = this.getChainAndNetworkId(); + const unapprovedTxs = this.state.transactions.filter( + (transaction) => + transaction.status === TransactionStatus.unapproved && + transactionMatchesNetwork(transaction, chainId, networkId), + ); + + for (const txMeta of unapprovedTxs) { + this.processApproval(txMeta, { + shouldShowRequest: false, + }).catch((error) => { + /* istanbul ignore next */ + console.error('Error during persisted transaction approval', error); + }); + } + } + /** * `@ethereumjs/tx` uses `@ethereumjs/common` as a configuration tool for * specifying which chain, network, hardfork and EIPs to support for @@ -741,9 +764,8 @@ export class TransactionController extends BaseController< */ async queryTransactionStatuses() { const { transactions } = this.state; - const { providerConfig, networkId: currentNetworkID } = - this.getNetworkState(); - const { chainId: currentChainId } = providerConfig; + const { chainId: currentChainId, networkId: currentNetworkID } = + this.getChainAndNetworkId(); let gotUpdates = false; await safelyExecute(() => Promise.all( @@ -804,9 +826,8 @@ export class TransactionController extends BaseController< this.update({ transactions: [] }); return; } - const { providerConfig, networkId: currentNetworkID } = - this.getNetworkState(); - const { chainId: currentChainId } = providerConfig; + const { chainId: currentChainId, networkId: currentNetworkID } = + this.getChainAndNetworkId(); const newTransactions = this.state.transactions.filter( ({ networkID, chainId, transaction }) => { // Using fallback to networkID only when there is no chainId present. Should be removed when networkID is completely removed. @@ -865,12 +886,15 @@ export class TransactionController extends BaseController< private async processApproval( transactionMeta: TransactionMeta, + { shouldShowRequest = true } = {}, ): Promise { const transactionId = transactionMeta.id; let resultCallbacks: AcceptResultCallbacks | undefined; try { - const acceptResult = await this.requestApproval(transactionMeta); + const acceptResult = await this.requestApproval(transactionMeta, { + shouldShowRequest, + }); resultCallbacks = acceptResult.resultCallbacks; const { meta, isCompleted } = this.isTransactionCompleted(transactionId); @@ -936,8 +960,7 @@ export class TransactionController extends BaseController< private async approveTransaction(transactionID: string) { const { transactions } = this.state; const releaseLock = await this.mutex.acquire(); - const { providerConfig } = this.getNetworkState(); - const { chainId } = providerConfig; + const { chainId } = this.getChainAndNetworkId(); const index = transactions.findIndex(({ id }) => transactionID === id); const transactionMeta = transactions[index]; const { @@ -1201,7 +1224,10 @@ export class TransactionController extends BaseController< return Number(txReceipt.status) === 0; } - private async requestApproval(txMeta: TransactionMeta): Promise { + private async requestApproval( + txMeta: TransactionMeta, + { shouldShowRequest }: { shouldShowRequest: boolean }, + ): Promise { const id = this.getApprovalId(txMeta); const { origin } = txMeta; const type = ApprovalType.Transaction; @@ -1216,7 +1242,7 @@ export class TransactionController extends BaseController< requestData, expectsResult: true, }, - true, + shouldShowRequest, )) as Promise; } @@ -1243,6 +1269,16 @@ export class TransactionController extends BaseController< return { meta: transaction, isCompleted }; } + + private getChainAndNetworkId(): { + networkId: string | null; + chainId: Hex; + } { + const { networkId, providerConfig } = this.getNetworkState(); + const chainId = providerConfig?.chainId; + + return { networkId, chainId }; + } } export default TransactionController; diff --git a/packages/transaction-controller/src/utils.test.ts b/packages/transaction-controller/src/utils.test.ts index 90467229a77..884a0d5f78b 100644 --- a/packages/transaction-controller/src/utils.test.ts +++ b/packages/transaction-controller/src/utils.test.ts @@ -7,7 +7,10 @@ import type { import type { Transaction, TransactionMeta } from './types'; import { TransactionStatus } from './types'; import * as util from './utils'; -import { getAndFormatTransactionsForNonceTracker } from './utils'; +import { + getAndFormatTransactionsForNonceTracker, + transactionMatchesNetwork, +} from './utils'; const MAX_FEE_PER_GAS = 'maxFeePerGas'; const MAX_PRIORITY_FEE_PER_GAS = 'maxPriorityFeePerGas'; @@ -306,4 +309,75 @@ describe('utils', () => { expect(result).toStrictEqual(expectedResult); }); }); + + describe('transactionMatchesNetwork', () => { + const transaction: TransactionMeta = { + chainId: '0x1', + networkID: '1', + id: '1', + time: 123456, + transaction: { + from: '0x123', + gas: '0x100', + value: '0x200', + nonce: '0x1', + }, + status: TransactionStatus.unapproved, + }; + it('returns true if chainId matches', () => { + const chainId = '0x1'; + const networkId = '1'; + expect(transactionMatchesNetwork(transaction, chainId, networkId)).toBe( + true, + ); + }); + + it('returns false if chainId does not match', () => { + const chainId = '0x1'; + const networkId = '1'; + expect( + transactionMatchesNetwork( + { ...transaction, chainId: '0x2' }, + chainId, + networkId, + ), + ).toBe(false); + }); + + it('returns true if networkID matches', () => { + const chainId = '0x1'; + const networkId = '1'; + expect( + transactionMatchesNetwork( + { ...transaction, chainId: undefined }, + chainId, + networkId, + ), + ).toBe(true); + }); + + it('returns false if networkID does not match', () => { + const chainId = '0x1'; + const networkId = '1'; + expect( + transactionMatchesNetwork( + { ...transaction, networkID: '2', chainId: undefined }, + chainId, + networkId, + ), + ).toBe(false); + }); + + it('returns true if chainId and networkID are undefined', () => { + const chainId = '0x2'; + const networkId = '1'; + expect( + transactionMatchesNetwork( + { ...transaction, chainId: undefined, networkID: undefined }, + chainId, + networkId, + ), + ).toBe(false); + }); + }); }); diff --git a/packages/transaction-controller/src/utils.ts b/packages/transaction-controller/src/utils.ts index 5be8a1a38c2..b7831dc5969 100644 --- a/packages/transaction-controller/src/utils.ts +++ b/packages/transaction-controller/src/utils.ts @@ -2,6 +2,7 @@ import { convertHexToDecimal, isValidHexAddress, } from '@metamask/controller-utils'; +import type { Hex } from '@metamask/utils'; import { addHexPrefix, isHexString } from 'ethereumjs-util'; import type { Transaction as NonceTrackerTransaction } from 'nonce-tracker/dist/NonceTracker'; @@ -207,3 +208,26 @@ export function getAndFormatTransactionsForNonceTracker( }; }); } + +/** + * Checks whether a given transaction matches the specified network or chain ID. + * This function is used to determine if a transaction is relevant to the current network or chain. + * + * @param transaction - The transaction metadata to check. + * @param chainId - The chain ID of the current network. + * @param networkId - The network ID of the current network. + * @returns A boolean value indicating whether the transaction matches the current network or chain ID. + */ +export function transactionMatchesNetwork( + transaction: TransactionMeta, + chainId: Hex, + networkId: string | null, +) { + if (transaction.chainId) { + return transaction.chainId === chainId; + } + if (transaction.networkID) { + return transaction.networkID === networkId; + } + return false; +}