From 5c48b390bf5d0966a46b74942240f233d83ebc1f Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Mon, 15 Jun 2026 14:12:02 +0100 Subject: [PATCH] feat(wallet): wire transaction controller --- .github/CODEOWNERS | 1 + README.md | 1 + packages/wallet/CHANGELOG.md | 1 + packages/wallet/package.json | 1 + packages/wallet/src/Wallet.test.ts | 32 +++- .../src/initialization/instances/index.ts | 1 + .../transaction-controller/constants.ts | 20 +++ .../transaction-controller.test.ts | 142 ++++++++++++++++++ .../transaction-controller.ts | 52 +++++++ .../instances/transaction-controller/types.ts | 5 + packages/wallet/src/types.ts | 2 + packages/wallet/tsconfig.build.json | 3 +- packages/wallet/tsconfig.json | 3 +- yarn.lock | 1 + 14 files changed, 256 insertions(+), 9 deletions(-) create mode 100644 packages/wallet/src/initialization/instances/transaction-controller/constants.ts create mode 100644 packages/wallet/src/initialization/instances/transaction-controller/transaction-controller.test.ts create mode 100644 packages/wallet/src/initialization/instances/transaction-controller/transaction-controller.ts create mode 100644 packages/wallet/src/initialization/instances/transaction-controller/types.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f5c519f5b9..3866217600 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -140,6 +140,7 @@ /packages/wallet/src/initialization/instances/keyring-controller/ @MetaMask/accounts-engineers @MetaMask/core-platform /packages/wallet/src/initialization/instances/remote-feature-flag-controller/ @MetaMask/extension-platform @MetaMask/mobile-platform @MetaMask/core-platform /packages/wallet/src/initialization/instances/storage-service/ @MetaMask/extension-platform @MetaMask/mobile-platform @MetaMask/core-platform +/packages/wallet/src/initialization/instances/transaction-controller/ @MetaMask/confirmations ## Package Release related /packages/account-tree-controller/package.json @MetaMask/accounts-engineers @MetaMask/core-platform diff --git a/README.md b/README.md index d8c35232e9..39be2f6faa 100644 --- a/README.md +++ b/README.md @@ -608,6 +608,7 @@ linkStyle default opacity:0.5 wallet --> messenger; wallet --> remote_feature_flag_controller; wallet --> storage_service; + wallet --> transaction_controller; wallet_cli --> base_controller; wallet_cli --> wallet; ``` diff --git a/packages/wallet/CHANGELOG.md b/packages/wallet/CHANGELOG.md index 06f7422cec..1d4225f203 100644 --- a/packages/wallet/CHANGELOG.md +++ b/packages/wallet/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING:** Wire `RemoteFeatureFlagController` into the default wallet initialization ([#8969](https://github.com/MetaMask/core/pull/8969)) - The default `Wallet` now constructs a `RemoteFeatureFlagController` and registers its `RemoteFeatureFlagController:*` messenger actions. Consumers that pass their own `messenger` and already wire a `RemoteFeatureFlagController` must remove their own before upgrading, or the duplicate registration will collide. - Adds a required `remoteFeatureFlagController` slot to `instanceOptions`. `clientConfigApiService` is required (each client injects a `ClientConfigApiService` configured for its own client type, distribution, and environment); `getMetaMetricsId`, `clientVersion`, `prevClientVersion`, `fetchInterval`, and `disabled` are optional. `prevClientVersion` lets consumers trigger feature-flag cache invalidation when the client version changes between sessions. +- **BREAKING:** Wire `TransactionController` into the default wallet initialization ([#8975](https://github.com/MetaMask/core/pull/8975)) ### Changed diff --git a/packages/wallet/package.json b/packages/wallet/package.json index ebbdf73c97..808ec8add6 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -64,6 +64,7 @@ "@metamask/remote-feature-flag-controller": "^4.2.2", "@metamask/scure-bip39": "^2.1.1", "@metamask/storage-service": "^1.0.2", + "@metamask/transaction-controller": "^68.0.0", "@metamask/utils": "^11.11.0" }, "devDependencies": { diff --git a/packages/wallet/src/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts index a89602c3fc..223f9b653d 100644 --- a/packages/wallet/src/Wallet.test.ts +++ b/packages/wallet/src/Wallet.test.ts @@ -7,6 +7,7 @@ import { webcrypto } from 'crypto'; import MockEncryptor from '../../keyring-controller/tests/mocks/mockEncryptor'; import * as initializationModule from './initialization/initialization'; import { AlwaysOnlineAdapter } from './initialization/instances/connectivity-controller/always-online-adapter'; +import type { WalletOptions } from './types'; import { importSecretRecoveryPhrase } from './utilities'; import { Wallet } from './Wallet'; @@ -22,8 +23,25 @@ const REMOTE_FEATURE_FLAG_OPTIONS = { }, }; +const TEST_TRANSACTION_CONTROLLER_CONFIGURATION = { + name: 'TransactionController', + getMessenger: (): Messenger => + new Messenger({ namespace: 'TransactionController' }), + init: (): Record => ({}), +}; + +function createWallet(options: WalletOptions): Wallet { + return new Wallet({ + ...options, + initializationConfigurations: [ + ...(options.initializationConfigurations ?? []), + TEST_TRANSACTION_CONTROLLER_CONFIGURATION, + ], + }); +} + async function setupWallet(): Promise { - const wallet = new Wallet({ + const wallet = createWallet({ instanceOptions: { connectivityController: { connectivityAdapter: new AlwaysOnlineAdapter(), @@ -80,7 +98,7 @@ describe('Wallet', () => { }); it('supports passing instance options', async () => { - const wallet = new Wallet({ + const wallet = createWallet({ instanceOptions: { connectivityController: { connectivityAdapter: new AlwaysOnlineAdapter(), @@ -115,7 +133,7 @@ describe('Wallet', () => { class DummyService {} - const wallet = new Wallet({ + const wallet = createWallet({ initializationConfigurations: [ { name: 'KeyringController', @@ -169,7 +187,7 @@ describe('Wallet', () => { NoMeta: { state: {} }, }); - const wallet = new Wallet({ + const wallet = createWallet({ instanceOptions: { connectivityController: { connectivityAdapter: new AlwaysOnlineAdapter(), @@ -244,7 +262,7 @@ describe('Wallet', () => { describe('ConnectivityController', () => { it('reports online connectivity status', () => { - const wallet = new Wallet({ + const wallet = createWallet({ instanceOptions: { connectivityController: { connectivityAdapter: new AlwaysOnlineAdapter(), @@ -276,7 +294,7 @@ describe('Wallet', () => { const vault = '{"data":"iOD5pIcPeRZYQ4WdEMsNYoZ3xBxWBafIU8Cr4nD0X4zBvrOA06tGen3sKQ/ValasXSweLnzH9Fk2frkPYmqeJWBtTNYFwdHPe7P970ThZwreSXN1Sqrx9Ad+YzmIN0y89Yg3KrUodPWaRgIZmgWbfDon6ADPgeEDkX0/GAEYET39O7Rx/gL+rcaTpxnpHPTgHiLbhRHWGsS3z+JVomSqoLAO5XVvrJWenO6R3Nzm62BaJaSPrf/pwstZqhSvxTq8hnQf7aR81hWfwYTxNBVG7TC/dniSQ8K5So6PvUN5nzAqvtzzHT2TagOuxQkX88Zi17P8os21jNmNdA90IGYroD+b/mppyRIgRYWtAUQZH9ji36atEuFupszbg8Qw1iaL3EQyUogC30Cpj9ko5bbqhYgqmFHF0J/kflhPHKuO6d4tgSmhYpTumydQRjxaPnlghIS5YI4W+7p9HVBpb+c6IPUz9y/x3Ngbp+ukJwOnXt2U/eZhXrJzi2z1x/nzPg4fzDJoM7k=","iv":"yrZsyC7dso/q7pQ48YX3vw==","keyMetadata":{"algorithm":"PBKDF2","params":{"iterations":600000}},"salt":"s7nIrMWK1lcZVjfdmES1DBML8Uz4ja2fpm8zUz1lWI0="}'; - const wallet = new Wallet({ + const wallet = createWallet({ state: { KeyringController: { vault, @@ -356,7 +374,7 @@ describe('Wallet', () => { }); it('routes injected instanceOptions through to the controller', async () => { - const wallet = new Wallet({ + const wallet = createWallet({ instanceOptions: { connectivityController: { connectivityAdapter: new AlwaysOnlineAdapter(), diff --git a/packages/wallet/src/initialization/instances/index.ts b/packages/wallet/src/initialization/instances/index.ts index bed6e52b26..389ff9913b 100644 --- a/packages/wallet/src/initialization/instances/index.ts +++ b/packages/wallet/src/initialization/instances/index.ts @@ -4,3 +4,4 @@ export { connectivityController } from './connectivity-controller/connectivity-c export { keyringController } from './keyring-controller/keyring-controller'; export { remoteFeatureFlagController } from './remote-feature-flag-controller/remote-feature-flag-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..8c7da1592d --- /dev/null +++ b/packages/wallet/src/initialization/instances/transaction-controller/constants.ts @@ -0,0 +1,20 @@ +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:transactionUpdated', + '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..baafe4d5dc --- /dev/null +++ b/packages/wallet/src/initialization/instances/transaction-controller/transaction-controller.test.ts @@ -0,0 +1,142 @@ +import { Messenger } from '@metamask/messenger'; +import { InMemoryStorageAdapter } from '@metamask/storage-service'; +import { TransactionController } from '@metamask/transaction-controller'; + +import type { WalletOptions } from '../../../types'; +import { Wallet } from '../../../Wallet'; +import { defaultConfigurations } from '../../defaults'; +import type { + DefaultActions, + DefaultEvents, + RootMessenger, +} from '../../defaults'; +import { AlwaysOnlineAdapter } from '../connectivity-controller/always-online-adapter'; +import { transactionController } from './transaction-controller'; + +const controllers: TransactionController[] = []; +const wallets: Wallet[] = []; + +const REMOTE_FEATURE_FLAG_OPTIONS = { + clientConfigApiService: { + fetchRemoteFeatureFlags: async (): Promise<{ + remoteFeatureFlags: Record; + cacheTimestamp: number; + }> => ({ remoteFeatureFlags: {}, cacheTimestamp: Date.now() }), + }, +}; + +type ActionHandler = (...args: unknown[]) => unknown; + +type AnyMessenger = Messenger; + +describe('transactionController', () => { + afterEach(async () => { + for (const controller of controllers.splice(0)) { + controller.destroy(); + } + + await Promise.all(wallets.splice(0).map((wallet) => wallet.destroy())); + }); + + it('is registered as a default initialization configuration', () => { + expect(Object.values(defaultConfigurations)).toContain( + transactionController, + ); + }); + + it('initializes a TransactionController with default state', () => { + const rootMessenger = getRootMessenger(); + const messenger = transactionController.getMessenger(rootMessenger); + + const instance = transactionController.init({ + state: undefined, + messenger, + options: {}, + }); + controllers.push(instance); + + expect(instance).toBeInstanceOf(TransactionController); + expect(rootMessenger.call('TransactionController:getState')).toStrictEqual({ + methodData: {}, + transactions: [], + transactionBatches: [], + lastFetchedBlockNumbers: {}, + submitHistory: [], + }); + }); + + it('is initialized by the default Wallet configuration', () => { + const wallet = new Wallet({ + messenger: getRootMessenger(), + instanceOptions: getInstanceOptions(), + }); + wallets.push(wallet); + + expect(wallet.getInstance('TransactionController')).toBeInstanceOf( + TransactionController, + ); + }); + + it('forwards the provided state to the controller', () => { + const rootMessenger = getRootMessenger(); + const messenger = transactionController.getMessenger(rootMessenger); + + const instance = transactionController.init({ + state: { + lastFetchedBlockNumbers: { '0x1': 123 }, + }, + messenger, + options: {}, + }); + controllers.push(instance); + + expect(instance.state.lastFetchedBlockNumbers).toStrictEqual({ + '0x1': 123, + }); + }); +}); + +function getRootMessenger(): RootMessenger { + const rootMessenger = new Messenger<'Root', DefaultActions, DefaultEvents>({ + namespace: 'Root', + }); + + registerActionHandler( + rootMessenger, + 'NetworkController', + 'NetworkController:getNetworkClientRegistry', + jest.fn().mockReturnValue({}), + ); + + return rootMessenger; +} + +function getInstanceOptions(): WalletOptions['instanceOptions'] { + return { + connectivityController: { + connectivityAdapter: new AlwaysOnlineAdapter(), + }, + storageService: { + storage: new InMemoryStorageAdapter(), + }, + remoteFeatureFlagController: REMOTE_FEATURE_FLAG_OPTIONS, + }; +} + +function registerActionHandler( + parent: RootMessenger, + namespace: string, + actionType: string, + handler: ActionHandler, +): void { + const messenger = new Messenger({ + namespace, + parent: parent as unknown as AnyMessenger, + }); + + ( + messenger as unknown as { + registerActionHandler(type: string, handler: ActionHandler): void; + } + ).registerActionHandler(actionType, handler); +} 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..428dbf0c62 --- /dev/null +++ b/packages/wallet/src/initialization/instances/transaction-controller/transaction-controller.ts @@ -0,0 +1,52 @@ +import { Messenger } from '@metamask/messenger'; +import type { + TransactionControllerMessenger, + TransactionControllerOptions, +} from '@metamask/transaction-controller'; +import { TransactionController } from '@metamask/transaction-controller'; + +import type { + DefaultActions, + DefaultEvents, + RootMessenger, +} from '../../defaults'; +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 }) => { + return new TransactionController({ + ...options, + messenger, + state, + } as TransactionControllerOptions); + }, + getMessenger: (parent) => { + const messenger = new Messenger< + 'TransactionController', + DefaultActions, + DefaultEvents, + RootMessenger + >({ + namespace: 'TransactionController', + parent, + }); + + parent.delegate({ + messenger, + actions: [...TRANSACTION_CONTROLLER_EXTERNAL_ACTIONS], + events: [...TRANSACTION_CONTROLLER_EXTERNAL_EVENTS], + }); + + return messenger as unknown as TransactionControllerMessenger; + }, +}; 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..54a7865f9f --- /dev/null +++ b/packages/wallet/src/initialization/instances/transaction-controller/types.ts @@ -0,0 +1,5 @@ +import type { TransactionControllerOptions } from '@metamask/transaction-controller'; + +export type TransactionControllerInstanceOptions = Partial< + Omit +>; diff --git a/packages/wallet/src/types.ts b/packages/wallet/src/types.ts index 0bc6c0efaa..683ca51527 100644 --- a/packages/wallet/src/types.ts +++ b/packages/wallet/src/types.ts @@ -10,6 +10,7 @@ import type { ConnectivityControllerInstanceOptions } from './initialization/ins import type { KeyringControllerInstanceOptions } from './initialization/instances/keyring-controller/types'; import type { RemoteFeatureFlagControllerInstanceOptions } from './initialization/instances/remote-feature-flag-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 = { @@ -28,4 +29,5 @@ export type InstanceSpecificOptions = { keyringController?: KeyringControllerInstanceOptions; remoteFeatureFlagController: RemoteFeatureFlagControllerInstanceOptions; storageService: StorageServiceInstanceOptions; + transactionController?: TransactionControllerInstanceOptions; }; diff --git a/packages/wallet/tsconfig.build.json b/packages/wallet/tsconfig.build.json index 317fe28aac..414b0fb0bd 100644 --- a/packages/wallet/tsconfig.build.json +++ b/packages/wallet/tsconfig.build.json @@ -14,7 +14,8 @@ { "path": "../keyring-controller/tsconfig.build.json" }, { "path": "../messenger/tsconfig.build.json" }, { "path": "../remote-feature-flag-controller/tsconfig.build.json" }, - { "path": "../storage-service/tsconfig.build.json" } + { "path": "../storage-service/tsconfig.build.json" }, + { "path": "../transaction-controller/tsconfig.build.json" } ], "include": ["../../types", "./src"] } diff --git a/packages/wallet/tsconfig.json b/packages/wallet/tsconfig.json index abbd559b3e..32fd7c85ce 100644 --- a/packages/wallet/tsconfig.json +++ b/packages/wallet/tsconfig.json @@ -12,7 +12,8 @@ { "path": "../keyring-controller/tsconfig.json" }, { "path": "../messenger/tsconfig.json" }, { "path": "../remote-feature-flag-controller/tsconfig.json" }, - { "path": "../storage-service/tsconfig.json" } + { "path": "../storage-service/tsconfig.json" }, + { "path": "../transaction-controller/tsconfig.json" } ], "include": ["../../types", "./src"] } diff --git a/yarn.lock b/yarn.lock index e3cd61b228..4ef189c287 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8861,6 +8861,7 @@ __metadata: "@metamask/remote-feature-flag-controller": "npm:^4.2.2" "@metamask/scure-bip39": "npm:^2.1.1" "@metamask/storage-service": "npm:^1.0.2" + "@metamask/transaction-controller": "npm:^68.0.0" "@metamask/utils": "npm:^11.11.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14"