From 9d908c5438a466206fdee1c159d326df18b2e0bb Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:12:15 -0700 Subject: [PATCH 1/6] Add dotenv, tsconfig references, and test cleanup for wallet package - Add dotenv to load INFURA_PROJECT_KEY from .env in tests - Add .env.example with placeholder key - Add missing tsconfig references for all repo-local imports - Add Wallet.destroy() to clean up controller instances - Use fake timers and proper afterEach cleanup to prevent open handles Co-Authored-By: Claude Opus 4.6 --- .gitignore | 7 +++++- packages/wallet/.env.example | 2 ++ packages/wallet/jest.config.js | 3 +++ packages/wallet/package.json | 1 + packages/wallet/src/Wallet.test.ts | 35 +++++++++++++++++++++++------ packages/wallet/src/Wallet.ts | 16 +++++++++++++ packages/wallet/tests/setup.ts | 4 ++++ packages/wallet/tsconfig.build.json | 23 ++++++++++++++++++- packages/wallet/tsconfig.json | 23 ++++++++++++++++++- yarn.lock | 8 +++++++ 10 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 packages/wallet/.env.example create mode 100644 packages/wallet/tests/setup.ts diff --git a/.gitignore b/.gitignore index 2f1de082398..78e1324684b 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,9 @@ scripts/coverage packages/*/*.tsbuildinfo # AI -.sisyphus/ \ No newline at end of file +.sisyphus/ + +# Wallet +.claude/ +.env +!.env.example diff --git a/packages/wallet/.env.example b/packages/wallet/.env.example new file mode 100644 index 00000000000..ba9556adb02 --- /dev/null +++ b/packages/wallet/.env.example @@ -0,0 +1,2 @@ +INFURA_PROJECT_KEY= + diff --git a/packages/wallet/jest.config.js b/packages/wallet/jest.config.js index ca084133399..7bf7e6462fc 100644 --- a/packages/wallet/jest.config.js +++ b/packages/wallet/jest.config.js @@ -14,6 +14,9 @@ module.exports = merge(baseConfig, { // The display name when running multiple projects displayName, + // Load dotenv before tests + setupFiles: [path.resolve(__dirname, 'tests/setup.ts')], + // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { diff --git a/packages/wallet/package.json b/packages/wallet/package.json index 0ad514fb7c7..05bb75582c4 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -61,6 +61,7 @@ "@ts-bridge/cli": "^0.6.4", "@types/jest": "^29.5.14", "deepmerge": "^4.2.2", + "dotenv": "^16.4.7", "jest": "^29.7.0", "ts-jest": "^29.2.5", "tsx": "^4.20.5", diff --git a/packages/wallet/src/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts index b73b5ec7238..7818081223c 100644 --- a/packages/wallet/src/Wallet.test.ts +++ b/packages/wallet/src/Wallet.test.ts @@ -1,4 +1,4 @@ -import { enableNetConnect } from 'nock'; +import nock, { enableNetConnect } from 'nock'; import { importSecretRecoveryPhrase, sendTransaction } from './utilities'; import { Wallet } from './Wallet'; @@ -8,9 +8,15 @@ const TEST_PHRASE = const TEST_PASSWORD = 'testpass'; async function setupWallet() { + if (!process.env.INFURA_PROJECT_KEY) { + throw new Error( + 'INFURA_PROJECT_KEY is not set. Copy .env.example to .env and fill in your key.', + ); + } + const wallet = new Wallet({ options: { - infuraProjectId: 'infura-project-id', + infuraProjectId: process.env.INFURA_PROJECT_KEY, clientVersion: '1.0.0', showApprovalRequest: () => undefined, }, @@ -22,8 +28,22 @@ async function setupWallet() { } describe('Wallet', () => { + let wallet: Wallet; + + beforeEach(() => { + jest.useFakeTimers({ doNotFake: ['nextTick', 'queueMicrotask'] }); + }); + + afterEach(async () => { + await wallet?.destroy(); + nock.cleanAll(); + nock.enableNetConnect(); + jest.useRealTimers(); + }); + it('can unlock and populate accounts', async () => { - const { messenger } = await setupWallet(); + wallet = await setupWallet(); + const { messenger } = wallet; expect( messenger @@ -35,7 +55,7 @@ describe('Wallet', () => { it('signs transactions', async () => { enableNetConnect(); - const wallet = await setupWallet(); + wallet = await setupWallet(); const addresses = wallet.messenger .call('AccountsController:listAccounts') @@ -47,7 +67,7 @@ describe('Wallet', () => { { networkClientId: 'sepolia' }, ); - const hash = await result; + const hash = await jest.advanceTimersByTimeAsync(60_000).then(() => result); expect(hash).toStrictEqual(expect.any(String)); expect(transactionMeta).toStrictEqual( @@ -61,10 +81,11 @@ describe('Wallet', () => { }), }), ); - }); + }, 30_000); it('exposes state', async () => { - const { state } = await setupWallet(); + wallet = await setupWallet(); + const { state } = wallet; expect(state.KeyringController).toStrictEqual({ isUnlocked: true, diff --git a/packages/wallet/src/Wallet.ts b/packages/wallet/src/Wallet.ts index 3b378f81803..2c563c883e7 100644 --- a/packages/wallet/src/Wallet.ts +++ b/packages/wallet/src/Wallet.ts @@ -31,4 +31,20 @@ export class Wallet { {}, ); } + + async destroy(): Promise { + await Promise.all( + Object.values(this.#instances).map((instance) => { + if ( + instance !== null && + typeof instance === 'object' && + 'destroy' in instance && + typeof instance.destroy === 'function' + ) { + return instance.destroy(); + } + return undefined; + }), + ); + } } diff --git a/packages/wallet/tests/setup.ts b/packages/wallet/tests/setup.ts new file mode 100644 index 00000000000..5f142bdf046 --- /dev/null +++ b/packages/wallet/tests/setup.ts @@ -0,0 +1,4 @@ +import dotenv from 'dotenv'; +import path from 'path'; + +dotenv.config({ path: path.resolve(__dirname, '../.env') }); diff --git a/packages/wallet/tsconfig.build.json b/packages/wallet/tsconfig.build.json index 2657e084ed0..a5e012287d5 100644 --- a/packages/wallet/tsconfig.build.json +++ b/packages/wallet/tsconfig.build.json @@ -7,10 +7,31 @@ }, "references": [ { - "path": "../messenger/tsconfig.build.json" + "path": "../accounts-controller/tsconfig.build.json" + }, + { + "path": "../approval-controller/tsconfig.build.json" + }, + { + "path": "../connectivity-controller/tsconfig.build.json" + }, + { + "path": "../controller-utils/tsconfig.build.json" }, { "path": "../keyring-controller/tsconfig.build.json" + }, + { + "path": "../messenger/tsconfig.build.json" + }, + { + "path": "../network-controller/tsconfig.build.json" + }, + { + "path": "../remote-feature-flag-controller/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 cdbf9854ac4..bbe38f95d3e 100644 --- a/packages/wallet/tsconfig.json +++ b/packages/wallet/tsconfig.json @@ -5,10 +5,31 @@ }, "references": [ { - "path": "../messenger/tsconfig.json" + "path": "../accounts-controller/tsconfig.json" + }, + { + "path": "../approval-controller/tsconfig.json" + }, + { + "path": "../connectivity-controller/tsconfig.json" + }, + { + "path": "../controller-utils/tsconfig.json" }, { "path": "../keyring-controller/tsconfig.json" + }, + { + "path": "../messenger/tsconfig.json" + }, + { + "path": "../network-controller/tsconfig.json" + }, + { + "path": "../remote-feature-flag-controller/tsconfig.json" + }, + { + "path": "../transaction-controller/tsconfig.json" } ], "include": ["../../types", "./src"] diff --git a/yarn.lock b/yarn.lock index bc3452ae7ae..c0b255d57b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5710,6 +5710,7 @@ __metadata: "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" deepmerge: "npm:^4.2.2" + dotenv: "npm:^16.4.7" jest: "npm:^29.7.0" ts-jest: "npm:^29.2.5" tsx: "npm:^4.20.5" @@ -8553,6 +8554,13 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:^16.4.7": + version: 16.6.1 + resolution: "dotenv@npm:16.6.1" + checksum: 10/1d1897144344447ffe62aa1a6d664f4cd2e0784e0aff787eeeec1940ded32f8e4b5b506d665134fc87157baa086fce07ec6383970a2b6d2e7985beaed6a4cc14 + languageName: node + linkType: hard + "dunder-proto@npm:^1.0.1": version: 1.0.1 resolution: "dunder-proto@npm:1.0.1" From c4b0728c7c57787b6a497d4ad990b8829ec29291 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 8 Apr 2026 16:31:41 -0700 Subject: [PATCH 2/6] Fix wallet package build errors - Use method syntax in InitializationConfiguration for bivariant parameter checking, allowing heterogeneous configs in a single array - Change InstanceState fallback from null to unknown for type compatibility - Widen TransactionController messenger type to include init-time actions - Add missing RemoteFeatureFlagController constructor args via WalletOptions - Use type guards and specific casts instead of any where possible - Default RootMessenger type params to any for unconstrained call() usage Co-Authored-By: Claude Opus 4.6 --- packages/wallet/src/Wallet.test.ts | 15 +++++ packages/wallet/src/Wallet.ts | 7 ++- .../src/initialization/initialization.ts | 2 +- .../remote-feature-flag-controller.ts | 2 + .../instances/transaction-controller.ts | 60 ++++++++++++++----- packages/wallet/src/initialization/types.ts | 18 +++--- packages/wallet/src/types.ts | 9 ++- packages/wallet/src/utilities.ts | 7 ++- 8 files changed, 86 insertions(+), 34 deletions(-) diff --git a/packages/wallet/src/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts index 7818081223c..74bf874ec6d 100644 --- a/packages/wallet/src/Wallet.test.ts +++ b/packages/wallet/src/Wallet.test.ts @@ -1,3 +1,9 @@ +import { + ClientConfigApiService, + ClientType, + DistributionType, + EnvironmentType, +} from '@metamask/remote-feature-flag-controller'; import nock, { enableNetConnect } from 'nock'; import { importSecretRecoveryPhrase, sendTransaction } from './utilities'; @@ -19,6 +25,15 @@ async function setupWallet() { infuraProjectId: process.env.INFURA_PROJECT_KEY, clientVersion: '1.0.0', showApprovalRequest: () => undefined, + clientConfigApiService: new ClientConfigApiService({ + fetch: globalThis.fetch, + config: { + client: ClientType.Extension, + distribution: DistributionType.Main, + environment: EnvironmentType.Production, + }, + }), + getMetaMetricsId: () => 'fake-metrics-id', }, }); diff --git a/packages/wallet/src/Wallet.ts b/packages/wallet/src/Wallet.ts index 2c563c883e7..40fb73b0689 100644 --- a/packages/wallet/src/Wallet.ts +++ b/packages/wallet/src/Wallet.ts @@ -25,7 +25,12 @@ export class Wallet { get state(): Record { return Object.entries(this.#instances).reduce>( (accumulator, [key, instance]) => { - accumulator[key] = instance.state ?? null; + accumulator[key] = + instance !== null && + typeof instance === 'object' && + 'state' in instance + ? instance.state + : null; return accumulator; }, {}, diff --git a/packages/wallet/src/initialization/initialization.ts b/packages/wallet/src/initialization/initialization.ts index abcf6034ef8..5201d8ae145 100644 --- a/packages/wallet/src/initialization/initialization.ts +++ b/packages/wallet/src/initialization/initialization.ts @@ -30,7 +30,7 @@ export function initialize({ ), ); - const instances = {}; + const instances: Record = {}; for (const config of configurationEntries) { const { name } = config; diff --git a/packages/wallet/src/initialization/instances/remote-feature-flag-controller.ts b/packages/wallet/src/initialization/instances/remote-feature-flag-controller.ts index 56885f4466a..4b5aaf3ca76 100644 --- a/packages/wallet/src/initialization/instances/remote-feature-flag-controller.ts +++ b/packages/wallet/src/initialization/instances/remote-feature-flag-controller.ts @@ -17,6 +17,8 @@ export const remoteFeatureFlagController: InitializationConfiguration< state, messenger, clientVersion: options.clientVersion, + clientConfigApiService: options.clientConfigApiService, + getMetaMetricsId: options.getMetaMetricsId, }); return { diff --git a/packages/wallet/src/initialization/instances/transaction-controller.ts b/packages/wallet/src/initialization/instances/transaction-controller.ts index c2b89afce53..cfd6a7b4d3f 100644 --- a/packages/wallet/src/initialization/instances/transaction-controller.ts +++ b/packages/wallet/src/initialization/instances/transaction-controller.ts @@ -1,8 +1,15 @@ +import type { KeyringControllerSignTransactionAction } from '@metamask/keyring-controller'; import { Messenger, MessengerActions, MessengerEvents, } from '@metamask/messenger'; +import type { + NetworkControllerGetEIP1559CompatibilityAction, + NetworkControllerGetNetworkClientRegistryAction, + NetworkControllerGetStateAction, +} from '@metamask/network-controller'; +import type { TransactionControllerOptions } from '@metamask/transaction-controller'; import { TransactionController, TransactionControllerMessenger, @@ -10,33 +17,54 @@ import { import { InitializationConfiguration } from '../types'; -type AllowedActions = MessengerActions; +type InitActions = + | NetworkControllerGetNetworkClientRegistryAction + | NetworkControllerGetEIP1559CompatibilityAction + | NetworkControllerGetStateAction + | KeyringControllerSignTransactionAction; + +type AllowedActions = + | MessengerActions + | InitActions; type AllowedEvents = MessengerEvents; +type WalletTransactionControllerMessenger = Messenger< + 'TransactionController', + AllowedActions, + AllowedEvents +>; + export const transactionController: InitializationConfiguration< TransactionController, - TransactionControllerMessenger + WalletTransactionControllerMessenger > = { name: 'TransactionController', init: ({ state, messenger }) => { // TODO: Add the rest of the arguments. const instance = new TransactionController({ state, - messenger, - getNetworkClientRegistry: messenger.call.bind( - messenger, - 'NetworkController:getNetworkClientRegistry', - ), - getCurrentNetworkEIP1559Compatibility: messenger.call.bind( - messenger, - 'NetworkController:getEIP1559Compatibility', - ), - getNetworkState: messenger.call.bind( - messenger, - 'NetworkController:getState', - ), - sign: messenger.call.bind(messenger, 'KeyringController:signTransaction'), + messenger: messenger as unknown as TransactionControllerMessenger, + disableHistory: true, + disableSendFlowHistory: true, + disableSwaps: true, + hooks: {}, + getNetworkClientRegistry: () => + messenger.call('NetworkController:getNetworkClientRegistry'), + getCurrentNetworkEIP1559Compatibility: () => + messenger.call( + 'NetworkController:getEIP1559Compatibility', + ) as Promise, + getNetworkState: () => messenger.call('NetworkController:getState'), + sign: ( + ...args: Parameters> + ) => + messenger.call( + 'KeyringController:signTransaction', + ...args, + ) as unknown as ReturnType< + NonNullable + >, }); return { diff --git a/packages/wallet/src/initialization/types.ts b/packages/wallet/src/initialization/types.ts index a04826d9765..12307d099b8 100644 --- a/packages/wallet/src/initialization/types.ts +++ b/packages/wallet/src/initialization/types.ts @@ -2,7 +2,7 @@ import { RootMessenger, WalletOptions } from '../types'; export type InstanceState = Instance extends { state: unknown } ? Instance['state'] - : null; + : unknown; export type InitFunctionArguments = { state: InstanceState; @@ -10,16 +10,12 @@ export type InitFunctionArguments = { options: WalletOptions; }; -export type InitFunction = ( - args: InitFunctionArguments, -) => { instance: Instance }; - -export type MessengerInitFunction = ( - parent: RootMessenger, -) => NarrowedMessenger; - +// Method syntax provides bivariant parameter checking, which is needed to +// collect heterogeneous InitializationConfiguration values in a single array. export type InitializationConfiguration = { name: string; - init: InitFunction; - messenger: MessengerInitFunction; + init(args: InitFunctionArguments): { + instance: Instance; + }; + messenger(parent: RootMessenger): InstanceMessenger; }; diff --git a/packages/wallet/src/types.ts b/packages/wallet/src/types.ts index 865163da5da..590a1461298 100644 --- a/packages/wallet/src/types.ts +++ b/packages/wallet/src/types.ts @@ -3,14 +3,19 @@ import { EventConstraint, Messenger, } from '@metamask/messenger'; +import type { ClientConfigApiService } from '@metamask/remote-feature-flag-controller'; export type RootMessenger< - AllowedActions extends ActionConstraint = ActionConstraint, - AllowedEvents extends EventConstraint = EventConstraint, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + AllowedActions extends ActionConstraint = any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + AllowedEvents extends EventConstraint = any, > = Messenger<'Root', AllowedActions, AllowedEvents>; export type WalletOptions = { infuraProjectId: string; clientVersion: string; showApprovalRequest: () => void; + clientConfigApiService: ClientConfigApiService; + getMetaMetricsId: () => string; }; diff --git a/packages/wallet/src/utilities.ts b/packages/wallet/src/utilities.ts index 936d01e5965..54a6b5d4d13 100644 --- a/packages/wallet/src/utilities.ts +++ b/packages/wallet/src/utilities.ts @@ -1,7 +1,8 @@ // TODO: Determine if these should be available directly on Wallet. import { wordlist } from '@metamask/scure-bip39/dist/wordlists/english'; -import { +import type { AddTransactionOptions, + TransactionMeta, TransactionParams, } from '@metamask/transaction-controller'; @@ -60,11 +61,11 @@ export async function sendTransaction( transaction: TransactionParams, options: AddTransactionOptions, ) { - const { transactionMeta, result } = await wallet.messenger.call( + const { transactionMeta, result } = (await wallet.messenger.call( 'TransactionController:addTransaction', transaction, options, - ); + )) as { transactionMeta: TransactionMeta; result: Promise }; const approvalId = transactionMeta.id; From 8626c7c5c994c54f87e269de333ec138dd991707 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 8 Apr 2026 16:34:44 -0700 Subject: [PATCH 3/6] Add missing dependencies to wallet package Co-Authored-By: Claude Opus 4.6 --- packages/wallet/package.json | 7 +++++++ yarn.lock | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/packages/wallet/package.json b/packages/wallet/package.json index 05bb75582c4..c00cb2635f4 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -49,11 +49,17 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { + "@metamask/accounts-controller": "^37.2.0", + "@metamask/approval-controller": "^9.0.1", "@metamask/browser-passworder": "^6.0.0", + "@metamask/connectivity-controller": "^0.2.0", "@metamask/controller-utils": "^11.20.0", "@metamask/keyring-controller": "^25.2.0", "@metamask/messenger": "^1.1.1", + "@metamask/network-controller": "^30.0.1", + "@metamask/remote-feature-flag-controller": "^4.2.0", "@metamask/scure-bip39": "^2.1.1", + "@metamask/transaction-controller": "^64.0.0", "@metamask/utils": "^11.11.0" }, "devDependencies": { @@ -63,6 +69,7 @@ "deepmerge": "^4.2.2", "dotenv": "^16.4.7", "jest": "^29.7.0", + "nock": "^13.3.1", "ts-jest": "^29.2.5", "tsx": "^4.20.5", "typedoc": "^0.25.13", diff --git a/yarn.lock b/yarn.lock index c0b255d57b7..a3dbc5c393b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5700,18 +5700,25 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/wallet@workspace:packages/wallet" dependencies: + "@metamask/accounts-controller": "npm:^37.2.0" + "@metamask/approval-controller": "npm:^9.0.1" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/browser-passworder": "npm:^6.0.0" + "@metamask/connectivity-controller": "npm:^0.2.0" "@metamask/controller-utils": "npm:^11.20.0" "@metamask/keyring-controller": "npm:^25.2.0" "@metamask/messenger": "npm:^1.1.1" + "@metamask/network-controller": "npm:^30.0.1" + "@metamask/remote-feature-flag-controller": "npm:^4.2.0" "@metamask/scure-bip39": "npm:^2.1.1" + "@metamask/transaction-controller": "npm:^64.0.0" "@metamask/utils": "npm:^11.11.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" deepmerge: "npm:^4.2.2" dotenv: "npm:^16.4.7" jest: "npm:^29.7.0" + nock: "npm:^13.3.1" ts-jest: "npm:^29.2.5" tsx: "npm:^4.20.5" typedoc: "npm:^0.25.13" From b36ee54086a11cc0761b655c29ace6c5086ef45f Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:04:44 -0700 Subject: [PATCH 4/6] Add bindMessengerAction helper Replaces inline arrow function wrappers with bindMessengerAction, a typed helper around messenger.call.bind that restores per-action type inference lost by Function.prototype.bind. Also incorporates upstream refinements to initialization types and Wallet state accessor. Co-Authored-By: Claude Opus 4.6 --- packages/wallet/src/Wallet.ts | 14 +++---- .../src/initialization/initialization.ts | 6 +-- .../instances/transaction-controller.ts | 42 +++++++++++-------- packages/wallet/src/initialization/types.ts | 36 ++++++++++++++++ 4 files changed, 68 insertions(+), 30 deletions(-) diff --git a/packages/wallet/src/Wallet.ts b/packages/wallet/src/Wallet.ts index 40fb73b0689..47211f3121d 100644 --- a/packages/wallet/src/Wallet.ts +++ b/packages/wallet/src/Wallet.ts @@ -1,4 +1,5 @@ import { Messenger } from '@metamask/messenger'; +import { isObject } from '@metamask/utils'; import type { Json } from '@metamask/utils'; import { initialize } from './initialization'; @@ -12,7 +13,7 @@ export type WalletConstructorArgs = { export class Wallet { public messenger: RootMessenger; - readonly #instances; + readonly #instances: Record>; constructor({ state = {}, options }: WalletConstructorArgs) { this.messenger = new Messenger({ @@ -24,14 +25,9 @@ export class Wallet { get state(): Record { return Object.entries(this.#instances).reduce>( - (accumulator, [key, instance]) => { - accumulator[key] = - instance !== null && - typeof instance === 'object' && - 'state' in instance - ? instance.state - : null; - return accumulator; + (totalState, [name, instance]) => { + totalState[name] = instance.state ?? null; + return totalState; }, {}, ); diff --git a/packages/wallet/src/initialization/initialization.ts b/packages/wallet/src/initialization/initialization.ts index 5201d8ae145..70ff75db8ab 100644 --- a/packages/wallet/src/initialization/initialization.ts +++ b/packages/wallet/src/initialization/initialization.ts @@ -19,7 +19,7 @@ export function initialize({ messenger, initializationConfigurations = [], options, -}: InitializeArgs) { +}: InitializeArgs): Record> { const overriddenConfiguration = initializationConfigurations.map( (config) => config.name, ); @@ -30,7 +30,7 @@ export function initialize({ ), ); - const instances: Record = {}; + const instances: Record> = {}; for (const config of configurationEntries) { const { name } = config; @@ -45,7 +45,7 @@ export function initialize({ options, }); - instances[name] = instance; + instances[name] = instance as Record; } return instances; diff --git a/packages/wallet/src/initialization/instances/transaction-controller.ts b/packages/wallet/src/initialization/instances/transaction-controller.ts index cfd6a7b4d3f..3768e5d195c 100644 --- a/packages/wallet/src/initialization/instances/transaction-controller.ts +++ b/packages/wallet/src/initialization/instances/transaction-controller.ts @@ -15,7 +15,7 @@ import { TransactionControllerMessenger, } from '@metamask/transaction-controller'; -import { InitializationConfiguration } from '../types'; +import { bindMessengerAction, InitializationConfiguration } from '../types'; type InitActions = | NetworkControllerGetNetworkClientRegistryAction @@ -47,24 +47,30 @@ export const transactionController: InitializationConfiguration< messenger: messenger as unknown as TransactionControllerMessenger, disableHistory: true, disableSendFlowHistory: true, - disableSwaps: true, + disableSwaps: false, hooks: {}, - getNetworkClientRegistry: () => - messenger.call('NetworkController:getNetworkClientRegistry'), - getCurrentNetworkEIP1559Compatibility: () => - messenger.call( - 'NetworkController:getEIP1559Compatibility', - ) as Promise, - getNetworkState: () => messenger.call('NetworkController:getState'), - sign: ( - ...args: Parameters> - ) => - messenger.call( - 'KeyringController:signTransaction', - ...args, - ) as unknown as ReturnType< - NonNullable - >, + getNetworkClientRegistry: bindMessengerAction( + messenger, + 'NetworkController:getNetworkClientRegistry', + ), + getCurrentNetworkEIP1559Compatibility: bindMessengerAction( + messenger, + 'NetworkController:getEIP1559Compatibility', + ) as () => Promise, + getNetworkState: bindMessengerAction( + messenger, + 'NetworkController:getState', + ), + // KeyringController.signTransaction is typed as returning + // Promise (a plain data object), but the actual keyring + // implementations return the full TypedTransaction class instance. + // TransactionController expects Promise here. The + // cast bridges a stale return-type declaration in KeyringController, + // not a real runtime mismatch. + sign: bindMessengerAction( + messenger, + 'KeyringController:signTransaction', + ) as unknown as TransactionControllerOptions['sign'], }); return { diff --git a/packages/wallet/src/initialization/types.ts b/packages/wallet/src/initialization/types.ts index 12307d099b8..c024f8e6ac9 100644 --- a/packages/wallet/src/initialization/types.ts +++ b/packages/wallet/src/initialization/types.ts @@ -1,3 +1,12 @@ +import type { + ActionConstraint, + EventConstraint, + ExtractActionParameters, + ExtractActionResponse, + Messenger, + MessengerActions, +} from '@metamask/messenger'; + import { RootMessenger, WalletOptions } from '../types'; export type InstanceState = Instance extends { state: unknown } @@ -10,6 +19,33 @@ export type InitFunctionArguments = { options: WalletOptions; }; +/** + * Typed wrapper around `messenger.call.bind(messenger, actionType)`. + * + * TypeScript's `Function.prototype.bind` loses generic inference on + * `Messenger.call`, so the bound function's parameters and return type + * collapse to a union of every action. This helper restores the correct + * per-action types via an explicit cast that is safe because `bind` + * preserves the runtime behavior exactly. + * + * @param messenger - The messenger instance. + * @param actionType - The action to bind. + * @returns A function that calls the action with the correct types. + */ +export function bindMessengerAction< + Msgr extends Messenger, + ActionType extends MessengerActions['type'], +>( + messenger: Msgr, + actionType: ActionType, +): ( + ...args: ExtractActionParameters, ActionType> +) => ExtractActionResponse, ActionType> { + return messenger.call.bind(messenger, actionType) as ( + ...args: ExtractActionParameters, ActionType> + ) => ExtractActionResponse, ActionType>; +} + // Method syntax provides bivariant parameter checking, which is needed to // collect heterogeneous InitializationConfiguration values in a single array. export type InitializationConfiguration = { From 36691cc1ae9a46700acb3e74506647c6b5e78238 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:27:18 -0700 Subject: [PATCH 5/6] Add explicit return types and fix lint issues in wallet package Co-Authored-By: Claude Sonnet 4.6 --- packages/wallet/jest.config.js | 2 +- packages/wallet/src/Wallet.test.ts | 12 ++-- packages/wallet/src/Wallet.ts | 8 +-- .../instances/keyring-controller.ts | 36 +++++++----- .../instances/network-controller.ts | 57 ++++++++++--------- packages/wallet/src/utilities.ts | 6 +- packages/wallet/test/setup.ts | 4 ++ packages/wallet/tests/setup.ts | 4 -- packages/wallet/tsconfig.json | 2 +- 9 files changed, 69 insertions(+), 62 deletions(-) create mode 100644 packages/wallet/test/setup.ts delete mode 100644 packages/wallet/tests/setup.ts diff --git a/packages/wallet/jest.config.js b/packages/wallet/jest.config.js index 7bf7e6462fc..e0b6d9792e8 100644 --- a/packages/wallet/jest.config.js +++ b/packages/wallet/jest.config.js @@ -15,7 +15,7 @@ module.exports = merge(baseConfig, { displayName, // Load dotenv before tests - setupFiles: [path.resolve(__dirname, 'tests/setup.ts')], + setupFiles: [path.resolve(__dirname, 'test/setup.ts')], // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { diff --git a/packages/wallet/src/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts index 74bf874ec6d..7e8fbca3128 100644 --- a/packages/wallet/src/Wallet.test.ts +++ b/packages/wallet/src/Wallet.test.ts @@ -4,7 +4,7 @@ import { DistributionType, EnvironmentType, } from '@metamask/remote-feature-flag-controller'; -import nock, { enableNetConnect } from 'nock'; +import { cleanAll, enableNetConnect } from 'nock'; import { importSecretRecoveryPhrase, sendTransaction } from './utilities'; import { Wallet } from './Wallet'; @@ -13,7 +13,7 @@ const TEST_PHRASE = 'test test test test test test test test test test test ball'; const TEST_PASSWORD = 'testpass'; -async function setupWallet() { +async function setupWallet(): Promise { if (!process.env.INFURA_PROJECT_KEY) { throw new Error( 'INFURA_PROJECT_KEY is not set. Copy .env.example to .env and fill in your key.', @@ -24,7 +24,7 @@ async function setupWallet() { options: { infuraProjectId: process.env.INFURA_PROJECT_KEY, clientVersion: '1.0.0', - showApprovalRequest: () => undefined, + showApprovalRequest: (): undefined => undefined, clientConfigApiService: new ClientConfigApiService({ fetch: globalThis.fetch, config: { @@ -33,7 +33,7 @@ async function setupWallet() { environment: EnvironmentType.Production, }, }), - getMetaMetricsId: () => 'fake-metrics-id', + getMetaMetricsId: (): string => 'fake-metrics-id', }, }); @@ -51,8 +51,8 @@ describe('Wallet', () => { afterEach(async () => { await wallet?.destroy(); - nock.cleanAll(); - nock.enableNetConnect(); + cleanAll(); + enableNetConnect(); jest.useRealTimers(); }); diff --git a/packages/wallet/src/Wallet.ts b/packages/wallet/src/Wallet.ts index 47211f3121d..9ddb9dc4b70 100644 --- a/packages/wallet/src/Wallet.ts +++ b/packages/wallet/src/Wallet.ts @@ -1,5 +1,4 @@ import { Messenger } from '@metamask/messenger'; -import { isObject } from '@metamask/utils'; import type { Json } from '@metamask/utils'; import { initialize } from './initialization'; @@ -36,12 +35,7 @@ export class Wallet { async destroy(): Promise { await Promise.all( Object.values(this.#instances).map((instance) => { - if ( - instance !== null && - typeof instance === 'object' && - 'destroy' in instance && - typeof instance.destroy === 'function' - ) { + if (typeof instance.destroy === 'function') { return instance.destroy(); } return undefined; diff --git a/packages/wallet/src/initialization/instances/keyring-controller.ts b/packages/wallet/src/initialization/instances/keyring-controller.ts index 8348de7e1be..7f3a356864c 100644 --- a/packages/wallet/src/initialization/instances/keyring-controller.ts +++ b/packages/wallet/src/initialization/instances/keyring-controller.ts @@ -1,3 +1,8 @@ +import type { + DetailedEncryptionResult, + EncryptionKey, + KeyDerivationOptions, +} from '@metamask/browser-passworder'; import { encrypt, encryptWithDetail, @@ -10,9 +15,8 @@ import { importKey, exportKey, generateSalt, - EncryptionKey, - KeyDerivationOptions, } from '@metamask/browser-passworder'; +import type { Encryptor } from '@metamask/keyring-controller'; import { KeyringController, KeyringControllerMessenger, @@ -35,7 +39,7 @@ const encryptFactory = data: unknown, key?: EncryptionKey | CryptoKey, salt?: string, - ) => + ): Promise => encrypt(password, data, key, salt, { algorithm: 'PBKDF2', params: { @@ -52,7 +56,11 @@ const encryptFactory = */ const encryptWithDetailFactory = (iterations: number) => - async (password: string, object: unknown, salt?: string) => + async ( + password: string, + object: unknown, + salt?: string, + ): Promise => encryptWithDetail(password, object, salt, { algorithm: 'PBKDF2', params: { @@ -77,7 +85,7 @@ const keyFromPasswordFactory = salt: string, exportable?: boolean, opts?: KeyDerivationOptions, - ) => + ): Promise => keyFromPassword( password, salt, @@ -97,13 +105,15 @@ const keyFromPasswordFactory = * @param iterations - The number of iterations to use for the PBKDF2 algorithm. * @returns A function that checks if the vault was encrypted with the given number of iterations. */ -const isVaultUpdatedFactory = (iterations: number) => (vault: string) => - isVaultUpdated(vault, { - algorithm: 'PBKDF2', - params: { - iterations, - }, - }); +const isVaultUpdatedFactory = + (iterations: number) => + (vault: string): boolean => + isVaultUpdated(vault, { + algorithm: 'PBKDF2', + params: { + iterations, + }, + }); /** * A factory function that returns an encryptor with the given number of iterations. @@ -114,7 +124,7 @@ const isVaultUpdatedFactory = (iterations: number) => (vault: string) => * @param iterations - The number of iterations to use for the PBKDF2 algorithm. * @returns An encryptor set with the given number of iterations. */ -const encryptorFactory = (iterations: number) => ({ +const encryptorFactory = (iterations: number): Encryptor => ({ encrypt: encryptFactory(iterations), encryptWithKey, encryptWithDetail: encryptWithDetailFactory(iterations), diff --git a/packages/wallet/src/initialization/instances/network-controller.ts b/packages/wallet/src/initialization/instances/network-controller.ts index 47d762955a7..aa23185d83c 100644 --- a/packages/wallet/src/initialization/instances/network-controller.ts +++ b/packages/wallet/src/initialization/instances/network-controller.ts @@ -5,6 +5,7 @@ import { MessengerActions, MessengerEvents, } from '@metamask/messenger'; +import type { NetworkControllerOptions } from '@metamask/network-controller'; import { NetworkController, NetworkControllerMessenger, @@ -24,36 +25,38 @@ export const networkController: InitializationConfiguration< name: 'NetworkController', init: ({ state, messenger, options }) => { // TODO: This was gutted to simplify implementation for now. - const getRpcServiceOptions = () => { - const maxRetries = DEFAULT_MAX_RETRIES; + const getRpcServiceOptions: NetworkControllerOptions['getRpcServiceOptions'] = + () => { + const maxRetries = DEFAULT_MAX_RETRIES; - const isOffline = (): boolean => { - const connectivityState = messenger.call( - 'ConnectivityController:getState', - ); - return ( - connectivityState.connectivityStatus === CONNECTIVITY_STATUSES.Offline - ); - }; + const isOffline = (): boolean => { + const connectivityState = messenger.call( + 'ConnectivityController:getState', + ); + return ( + connectivityState.connectivityStatus === + CONNECTIVITY_STATUSES.Offline + ); + }; - return { - fetch: globalThis.fetch.bind(globalThis), - btoa: globalThis.btoa.bind(globalThis), - isOffline, - policyOptions: { - // Ensure that the "cooldown" period after breaking the circuit is short. - circuitBreakDuration: inMilliseconds(30, Duration.Second), - maxRetries, - // Ensure that if the endpoint continually responds with errors, we - // break the circuit relatively fast (but not prematurely). - // - // Note that the circuit will break much faster if the errors are - // retriable (e.g. 503) than if not (e.g. 500), so we attempt to strike - // a balance here. - maxConsecutiveFailures: (maxRetries + 1) * 3, - }, + return { + fetch: globalThis.fetch.bind(globalThis), + btoa: globalThis.btoa.bind(globalThis), + isOffline, + policyOptions: { + // Ensure that the "cooldown" period after breaking the circuit is short. + circuitBreakDuration: inMilliseconds(30, Duration.Second), + maxRetries, + // Ensure that if the endpoint continually responds with errors, we + // break the circuit relatively fast (but not prematurely). + // + // Note that the circuit will break much faster if the errors are + // retriable (e.g. 503) than if not (e.g. 500), so we attempt to strike + // a balance here. + maxConsecutiveFailures: (maxRetries + 1) * 3, + }, + }; }; - }; // TODO: Add the rest of the arguments. const instance = new NetworkController({ diff --git a/packages/wallet/src/utilities.ts b/packages/wallet/src/utilities.ts index 54a6b5d4d13..3d5de362765 100644 --- a/packages/wallet/src/utilities.ts +++ b/packages/wallet/src/utilities.ts @@ -19,7 +19,7 @@ export async function importSecretRecoveryPhrase( wallet: Wallet, password: string, phrase: string, -) { +): Promise { const indices = phrase.split(' ').map((word) => wordlist.indexOf(word)); const mnemonic = new Uint8Array(new Uint16Array(indices).buffer); @@ -40,7 +40,7 @@ export async function importSecretRecoveryPhrase( export async function createSecretRecoveryPhrase( wallet: Wallet, password: string, -) { +): Promise { // TODO: This should use the new MultichainAccountService. await wallet.messenger.call( 'KeyringController:createNewVaultAndKeychain', @@ -60,7 +60,7 @@ export async function sendTransaction( wallet: Wallet, transaction: TransactionParams, options: AddTransactionOptions, -) { +): Promise<{ transactionMeta: TransactionMeta; result: Promise }> { const { transactionMeta, result } = (await wallet.messenger.call( 'TransactionController:addTransaction', transaction, diff --git a/packages/wallet/test/setup.ts b/packages/wallet/test/setup.ts new file mode 100644 index 00000000000..192571b40bc --- /dev/null +++ b/packages/wallet/test/setup.ts @@ -0,0 +1,4 @@ +import { config } from 'dotenv'; +import path from 'path'; + +config({ path: path.resolve(__dirname, '../.env') }); diff --git a/packages/wallet/tests/setup.ts b/packages/wallet/tests/setup.ts deleted file mode 100644 index 5f142bdf046..00000000000 --- a/packages/wallet/tests/setup.ts +++ /dev/null @@ -1,4 +0,0 @@ -import dotenv from 'dotenv'; -import path from 'path'; - -dotenv.config({ path: path.resolve(__dirname, '../.env') }); diff --git a/packages/wallet/tsconfig.json b/packages/wallet/tsconfig.json index bbe38f95d3e..8f0b0c57883 100644 --- a/packages/wallet/tsconfig.json +++ b/packages/wallet/tsconfig.json @@ -32,5 +32,5 @@ "path": "../transaction-controller/tsconfig.json" } ], - "include": ["../../types", "./src"] + "include": ["../../types", "./src", "./test"] } From e91bbe2a126e9b0f90138815139e7dcb1101fb85 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Fri, 10 Apr 2026 11:28:09 -0700 Subject: [PATCH 6/6] refactor: First batch of lint fixes --- packages/wallet/src/Wallet.test.ts | 6 +++--- packages/wallet/src/initialization/types.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/wallet/src/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts index 7e8fbca3128..ba8387ed270 100644 --- a/packages/wallet/src/Wallet.test.ts +++ b/packages/wallet/src/Wallet.test.ts @@ -4,7 +4,7 @@ import { DistributionType, EnvironmentType, } from '@metamask/remote-feature-flag-controller'; -import { cleanAll, enableNetConnect } from 'nock'; +import { enableNetConnect } from 'nock'; import { importSecretRecoveryPhrase, sendTransaction } from './utilities'; import { Wallet } from './Wallet'; @@ -51,7 +51,6 @@ describe('Wallet', () => { afterEach(async () => { await wallet?.destroy(); - cleanAll(); enableNetConnect(); jest.useRealTimers(); }); @@ -82,6 +81,7 @@ describe('Wallet', () => { { networkClientId: 'sepolia' }, ); + // Advance timers by an arbitrary value to trigger downstream timer logic. const hash = await jest.advanceTimersByTimeAsync(60_000).then(() => result); expect(hash).toStrictEqual(expect.any(String)); @@ -96,7 +96,7 @@ describe('Wallet', () => { }), }), ); - }, 30_000); + }, 10_000); it('exposes state', async () => { wallet = await setupWallet(); diff --git a/packages/wallet/src/initialization/types.ts b/packages/wallet/src/initialization/types.ts index c024f8e6ac9..a08b1934c99 100644 --- a/packages/wallet/src/initialization/types.ts +++ b/packages/wallet/src/initialization/types.ts @@ -46,10 +46,10 @@ export function bindMessengerAction< ) => ExtractActionResponse, ActionType>; } -// Method syntax provides bivariant parameter checking, which is needed to -// collect heterogeneous InitializationConfiguration values in a single array. export type InitializationConfiguration = { name: string; + // This is a method as opposed to function property in order to collect + // heterogeneous InitializationConfiguration values in a single array. init(args: InitFunctionArguments): { instance: Instance; };