diff --git a/packages/wallet/CHANGELOG.md b/packages/wallet/CHANGELOG.md index 787705fe66..608a618580 100644 --- a/packages/wallet/CHANGELOG.md +++ b/packages/wallet/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **BREAKING:** Wire `TransactionController` into the default wallet initialization ([#8975](https://github.com/MetaMask/core/pull/8975)) - **BREAKING:** Wire `ApprovalController` into the default wallet initialization ([#8953](https://github.com/MetaMask/core/pull/8953)) - The default `Wallet` now constructs an `ApprovalController` and registers its `ApprovalController:*` messenger actions. Consumers that pass their own `messenger` and already wire an `ApprovalController` must remove their own before upgrading, or the duplicate registration will collide. - Adds an `approvalController` slot to `instanceOptions` with `showApprovalRequest` (the callback that surfaces pending approval requests to the user; defaults to a no-op) and `typesExcludedFromRateLimiting` (the approval types exempt from per-origin rate limiting; defaults to a baseline of EVM approval types). Both let consumers (extension, mobile, wallet-cli) inject their platform-specific values. diff --git a/packages/wallet/src/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts index e0ff366c09..f25ed8f849 100644 --- a/packages/wallet/src/Wallet.test.ts +++ b/packages/wallet/src/Wallet.test.ts @@ -8,6 +8,20 @@ import * as initializationModule from './initialization/initialization'; import { importSecretRecoveryPhrase } from './utilities'; import { Wallet } from './Wallet'; +jest.mock('@metamask/transaction-controller', () => ({ + TransactionController: jest + .fn() + .mockImplementation(function (this: { state: unknown }) { + this.state = { + methodData: {}, + transactions: [], + transactionBatches: [], + lastFetchedBlockNumbers: {}, + submitHistory: [], + }; + }), +})); + const TEST_SRP = 'test test test test test test test test test test test ball'; const TEST_PASSWORD = 'testpass'; diff --git a/packages/wallet/src/initialization/instances/index.ts b/packages/wallet/src/initialization/instances/index.ts index aa79320da7..e1ecaf5681 100644 --- a/packages/wallet/src/initialization/instances/index.ts +++ b/packages/wallet/src/initialization/instances/index.ts @@ -1,3 +1,4 @@ export { approvalController } from './approval-controller/approval-controller'; export { keyringController } from './keyring-controller/keyring-controller'; export { storageService } from './storage-service/storage-service'; +export { transactionController } from './transaction-controller/transaction-controller'; diff --git a/packages/wallet/src/initialization/instances/transaction-controller/constants.ts b/packages/wallet/src/initialization/instances/transaction-controller/constants.ts new file mode 100644 index 0000000000..2e28614795 --- /dev/null +++ b/packages/wallet/src/initialization/instances/transaction-controller/constants.ts @@ -0,0 +1,23 @@ +export const TRANSACTION_CONTROLLER_EXTERNAL_ACTIONS = [ + 'AccountsController:getSelectedAccount', + 'AccountsController:getState', + 'ApprovalController:addRequest', + 'GasFeeController:fetchGasFeeEstimates', + 'KeyringController:getState', + 'KeyringController:signEip7702Authorization', + 'KeyringController:signTransaction', + 'NetworkController:findNetworkClientIdByChainId', + 'NetworkController:getEIP1559Compatibility', + 'NetworkController:getNetworkClientById', + 'NetworkController:getNetworkClientRegistry', + 'NetworkController:getState', + 'RemoteFeatureFlagController:getState', +] as const; + +export const TRANSACTION_CONTROLLER_EXTERNAL_EVENTS = [ + 'AccountActivityService:statusChanged', + 'AccountActivityService:transactionUpdated', + 'AccountsController:selectedAccountChange', + 'BackendWebSocketService:connectionStateChanged', + 'NetworkController:stateChange', +] as const; diff --git a/packages/wallet/src/initialization/instances/transaction-controller/transaction-controller.test.ts b/packages/wallet/src/initialization/instances/transaction-controller/transaction-controller.test.ts new file mode 100644 index 0000000000..f32cb9cfbd --- /dev/null +++ b/packages/wallet/src/initialization/instances/transaction-controller/transaction-controller.test.ts @@ -0,0 +1,77 @@ +import { TransactionController } from '@metamask/transaction-controller'; + +import { defaultConfigurations } from '../../defaults'; +import { transactionController } from './transaction-controller'; +import type { TransactionControllerInstanceOptions } from './types'; + +const MOCK_STATE = { + methodData: {}, + transactions: [], + transactionBatches: [], + lastFetchedBlockNumbers: {}, + submitHistory: [], +}; + +jest.mock('@metamask/transaction-controller', () => ({ + TransactionController: jest.fn(), +})); + +function buildOptions( + overrides: Partial = {}, +): TransactionControllerInstanceOptions { + return { + disableSwaps: false, + hooks: {}, + isFirstTimeInteractionEnabled: () => false, + isSimulationEnabled: () => false, + ...overrides, + }; +} + +describe('transactionController', () => { + it('is registered as a default initialization configuration', () => { + expect(Object.values(defaultConfigurations)).toContain( + transactionController, + ); + }); + + it('initializes a TransactionController with the provided state', () => { + jest.mocked(TransactionController).mockImplementation(function (this: { + state: unknown; + }) { + this.state = MOCK_STATE; + } as never); + + transactionController.init({ + state: MOCK_STATE, + // @ts-expect-error Messenger not needed for this assertion. + messenger: {}, + options: buildOptions(), + }); + + expect(TransactionController).toHaveBeenCalledWith( + expect.objectContaining({ state: MOCK_STATE }), + ); + }); + + it('disables incoming transactions', () => { + jest.mocked(TransactionController).mockImplementation(function (this: { + state: unknown; + }) { + this.state = MOCK_STATE; + } as never); + + transactionController.init({ + state: undefined, + // @ts-expect-error Messenger not needed for this assertion. + messenger: {}, + options: buildOptions(), + }); + + const opts = ( + TransactionController as jest.MockedClass + ).mock.calls[0][0]; + + expect(opts.incomingTransactions?.isEnabled?.()).toBe(false); + }); +}); diff --git a/packages/wallet/src/initialization/instances/transaction-controller/transaction-controller.ts b/packages/wallet/src/initialization/instances/transaction-controller/transaction-controller.ts new file mode 100644 index 0000000000..fbb3b904f1 --- /dev/null +++ b/packages/wallet/src/initialization/instances/transaction-controller/transaction-controller.ts @@ -0,0 +1,45 @@ +import { Messenger } from '@metamask/messenger'; +import type { MessengerActions, MessengerEvents } from '@metamask/messenger'; +import type { TransactionControllerMessenger } from '@metamask/transaction-controller'; +import { TransactionController } from '@metamask/transaction-controller'; + +import type { InitializationConfiguration } from '../../types'; +import { + TRANSACTION_CONTROLLER_EXTERNAL_ACTIONS, + TRANSACTION_CONTROLLER_EXTERNAL_EVENTS, +} from './constants'; + +export type { TransactionControllerInstanceOptions } from './types'; + +export const transactionController: InitializationConfiguration< + TransactionController, + TransactionControllerMessenger +> = { + name: 'TransactionController', + init: ({ state, messenger, options }) => + new TransactionController({ + ...options, + incomingTransactions: { isEnabled: () => false }, + messenger, + state, + }), + getMessenger: (parent) => { + const messenger = new Messenger< + 'TransactionController', + MessengerActions, + MessengerEvents, + typeof parent + >({ + namespace: 'TransactionController', + parent, + }); + + parent.delegate({ + messenger, + actions: [...TRANSACTION_CONTROLLER_EXTERNAL_ACTIONS], + events: [...TRANSACTION_CONTROLLER_EXTERNAL_EVENTS], + }); + + return messenger; + }, +}; diff --git a/packages/wallet/src/initialization/instances/transaction-controller/types.ts b/packages/wallet/src/initialization/instances/transaction-controller/types.ts new file mode 100644 index 0000000000..429c48104b --- /dev/null +++ b/packages/wallet/src/initialization/instances/transaction-controller/types.ts @@ -0,0 +1,6 @@ +import type { TransactionControllerOptions } from '@metamask/transaction-controller'; + +export type TransactionControllerInstanceOptions = Omit< + TransactionControllerOptions, + 'incomingTransactions' | 'messenger' | 'state' +>; diff --git a/packages/wallet/src/types.ts b/packages/wallet/src/types.ts index 08c2b63321..317b257f3e 100644 --- a/packages/wallet/src/types.ts +++ b/packages/wallet/src/types.ts @@ -8,6 +8,7 @@ import type { import type { ApprovalControllerInstanceOptions } from './initialization/instances/approval-controller/types'; import type { KeyringControllerInstanceOptions } from './initialization/instances/keyring-controller/types'; import type { StorageServiceInstanceOptions } from './initialization/instances/storage-service/types'; +import type { TransactionControllerInstanceOptions } from './initialization/instances/transaction-controller/types'; import { InitializationConfiguration } from './initialization/types'; export type WalletOptions = { @@ -24,4 +25,5 @@ export type InstanceSpecificOptions = { approvalController?: ApprovalControllerInstanceOptions; keyringController?: KeyringControllerInstanceOptions; storageService: StorageServiceInstanceOptions; + transactionController?: TransactionControllerInstanceOptions; }; diff --git a/packages/wallet/tsconfig.build.json b/packages/wallet/tsconfig.build.json index 68e89ef628..779134c35d 100644 --- a/packages/wallet/tsconfig.build.json +++ b/packages/wallet/tsconfig.build.json @@ -6,12 +6,18 @@ "rootDir": "./src" }, "references": [ + { "path": "../accounts-controller/tsconfig.build.json" }, { "path": "../approval-controller/tsconfig.build.json" }, { "path": "../base-controller/tsconfig.build.json" }, { "path": "../controller-utils/tsconfig.build.json" }, + { "path": "../core-backend/tsconfig.build.json" }, + { "path": "../gas-fee-controller/tsconfig.build.json" }, { "path": "../keyring-controller/tsconfig.build.json" }, { "path": "../messenger/tsconfig.build.json" }, - { "path": "../storage-service/tsconfig.build.json" } + { "path": "../network-controller/tsconfig.build.json" }, + { "path": "../remote-feature-flag-controller/tsconfig.build.json" }, + { "path": "../storage-service/tsconfig.build.json" }, + { "path": "../transaction-controller/tsconfig.build.json" } ], "include": ["../../types", "./src"] }