diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2b66123bd13..049a35be340 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -146,6 +146,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 1b8c734bbbb..80a1a9a2251 100644 --- a/README.md +++ b/README.md @@ -626,6 +626,7 @@ linkStyle default opacity:0.5 wallet --> network_controller; wallet --> remote_feature_flag_controller; wallet --> storage_service; + wallet --> transaction_controller; wallet_cli --> base_controller; wallet_cli --> remote_feature_flag_controller; wallet_cli --> storage_service; diff --git a/packages/wallet/CHANGELOG.md b/packages/wallet/CHANGELOG.md index ea64a689723..ef4d9193332 100644 --- a/packages/wallet/CHANGELOG.md +++ b/packages/wallet/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **BREAKING:** Wire `TransactionController` into the default wallet initialization ([#8975](https://github.com/MetaMask/core/pull/8975)) + ## [6.0.0] ### Added diff --git a/packages/wallet/package.json b/packages/wallet/package.json index 28454851761..aab11a0b692 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -66,6 +66,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.2.2", "@metamask/utils": "^11.11.0" }, "devDependencies": { diff --git a/packages/wallet/src/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts index 2c39a1544ee..d3c67ba3ef3 100644 --- a/packages/wallet/src/Wallet.test.ts +++ b/packages/wallet/src/Wallet.test.ts @@ -423,6 +423,16 @@ describe('Wallet', () => { }); }); + describe('TransactionController', () => { + it('is wired and exposes its state on the wallet messenger', async () => { + const wallet = await setupWallet(); + + expect( + wallet.messenger.call('TransactionController:getState'), + ).toStrictEqual(expect.objectContaining({ transactions: [] })); + }); + }); + describe('RemoteFeatureFlagController', () => { it('is wired and exposes its state on the wallet messenger', async () => { const wallet = await setupWallet(); diff --git a/packages/wallet/src/initialization/instances/index.ts b/packages/wallet/src/initialization/instances/index.ts index ad8d554b678..a15db956032 100644 --- a/packages/wallet/src/initialization/instances/index.ts +++ b/packages/wallet/src/initialization/instances/index.ts @@ -6,3 +6,4 @@ export { keyringController } from './keyring-controller/keyring-controller'; export { networkController } from './network-controller/network-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/transaction-controller.test.ts b/packages/wallet/src/initialization/instances/transaction-controller/transaction-controller.test.ts new file mode 100644 index 00000000000..1e88b0e52d2 --- /dev/null +++ b/packages/wallet/src/initialization/instances/transaction-controller/transaction-controller.test.ts @@ -0,0 +1,144 @@ +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({ + 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(), + }, + networkController: { + infuraProjectId: 'test-infura-project-id', + }, + 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 00000000000..6ee57a27e60 --- /dev/null +++ b/packages/wallet/src/initialization/instances/transaction-controller/transaction-controller.ts @@ -0,0 +1,57 @@ +import { Messenger } from '@metamask/messenger'; +import type { TransactionControllerMessenger } from '@metamask/transaction-controller'; +import { TransactionController } from '@metamask/transaction-controller'; + +import type { InitializationConfiguration } from '../../types'; + +export type { TransactionControllerInstanceOptions } from './types'; + +export const transactionController: InitializationConfiguration< + TransactionController, + TransactionControllerMessenger +> = { + name: 'TransactionController', + init: ({ state, messenger, options }) => { + const { disableSwaps = false, ...rest } = options; + + return new TransactionController({ + ...rest, + disableSwaps, + messenger, + state, + }); + }, + getMessenger: (parent) => { + const messenger: TransactionControllerMessenger = new Messenger({ + namespace: 'TransactionController', + parent, + }); + + parent.delegate({ + messenger, + 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', + ], + events: [ + 'AccountActivityService:transactionUpdated', + // TODO: Replace with `NetworkController:stateChanged` once TransactionController migrates. + // eslint-disable-next-line no-restricted-syntax + 'NetworkController:stateChange', + ], + }); + + 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 00000000000..d9e8eb5198e --- /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, + 'messenger' | 'state' +> & { disableSwaps?: boolean }; diff --git a/packages/wallet/src/types.ts b/packages/wallet/src/types.ts index 6a66903dde0..1f330e9bafd 100644 --- a/packages/wallet/src/types.ts +++ b/packages/wallet/src/types.ts @@ -11,6 +11,7 @@ import type { KeyringControllerInstanceOptions } from './initialization/instance import type { NetworkControllerInstanceOptions } from './initialization/instances/network-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 type { InitializationConfiguration } from './initialization/types'; export type WalletOptions = { @@ -30,4 +31,5 @@ export type InstanceSpecificOptions = { networkController: NetworkControllerInstanceOptions; remoteFeatureFlagController: RemoteFeatureFlagControllerInstanceOptions; storageService: StorageServiceInstanceOptions; + transactionController?: TransactionControllerInstanceOptions; }; diff --git a/packages/wallet/tsconfig.build.json b/packages/wallet/tsconfig.build.json index bfa1aa578bf..cc15d40bc18 100644 --- a/packages/wallet/tsconfig.build.json +++ b/packages/wallet/tsconfig.build.json @@ -16,7 +16,8 @@ { "path": "../messenger/tsconfig.build.json" }, { "path": "../network-controller/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 9d52f9fb407..24995af5a41 100644 --- a/packages/wallet/tsconfig.json +++ b/packages/wallet/tsconfig.json @@ -14,7 +14,8 @@ { "path": "../messenger/tsconfig.json" }, { "path": "../network-controller/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 9e514a392fc..260f003b9aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9037,6 +9037,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.2.2" "@metamask/utils": "npm:^11.11.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14"