Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions jest.config.packages.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ module.exports = {
'<rootDir>/../json-rpc-engine/src/v2/index.ts',
],
'^@metamask/utils/node$': require.resolve('@metamask/utils/node'),
'^@metamask/snaps-controllers/node$': ['@metamask/snaps-controllers/node'],
'^@metamask/snaps-execution-environments/node-thread$': [
'@metamask/snaps-execution-environments/node-thread',
],
'^@metamask/post-message-stream/node$': [
'@metamask/post-message-stream/node',
],
'^@metamask/(.+)$': [
'<rootDir>/../$1/src',
// Some @metamask/* packages we are referencing aren't in this monorepo,
Expand Down
14 changes: 13 additions & 1 deletion packages/wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,33 @@
"dependencies": {
"@metamask/accounts-controller": "^37.2.0",
"@metamask/approval-controller": "^9.0.1",
"@metamask/bitcoin-wallet-snap": "^1.10.1",
"@metamask/browser-passworder": "^6.0.0",
"@metamask/connectivity-controller": "^0.2.0",
"@metamask/controller-utils": "^11.20.0",
"@metamask/json-rpc-engine": "^10.2.4",
"@metamask/json-rpc-middleware-stream": "^8.0.8",
"@metamask/keyring-controller": "^25.2.0",
"@metamask/messenger": "^1.1.1",
"@metamask/multichain-account-service": "^8.0.1",
"@metamask/network-controller": "^30.0.1",
"@metamask/object-multiplex": "^2.1.0",
"@metamask/permission-controller": "^12.3.0",
"@metamask/remote-feature-flag-controller": "^4.2.0",
"@metamask/scure-bip39": "^2.1.1",
"@metamask/snaps-controllers": "^20.0.1",
"@metamask/snaps-execution-environments": "^11.0.2",
"@metamask/solana-wallet-snap": "^2.8.0",
"@metamask/transaction-controller": "^64.0.0",
"@metamask/utils": "^11.9.0"
"@metamask/tron-wallet-snap": "^1.25.2",
"@metamask/utils": "^11.9.0",
"readable-stream": "^4.7.0"
},
"devDependencies": {
"@metamask/auto-changelog": "^3.4.4",
"@ts-bridge/cli": "^0.6.4",
"@types/jest": "^29.5.14",
"@types/readable-stream": "^4",
"deepmerge": "^4.2.2",
"dotenv": "^16.4.7",
"jest": "^29.7.0",
Expand Down
12 changes: 6 additions & 6 deletions packages/wallet/src/Wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
DistributionType,
EnvironmentType,
} from '@metamask/remote-feature-flag-controller';
import { enableNetConnect } from 'nock';
import { disableNetConnect, enableNetConnect } from 'nock';

import { importSecretRecoveryPhrase, sendTransaction } from './utilities';
import { Wallet } from './Wallet';
Expand Down Expand Up @@ -34,6 +34,7 @@ async function setupWallet(): Promise<Wallet> {
},
}),
getMetaMetricsId: (): string => 'fake-metrics-id',
ensureOnboardingComplete: () => Promise.resolve(),
},
});

Expand All @@ -46,12 +47,13 @@ describe('Wallet', () => {
let wallet: Wallet;

beforeEach(() => {
jest.useFakeTimers({ doNotFake: ['nextTick', 'queueMicrotask'] });
enableNetConnect();
// jest.useFakeTimers({ doNotFake: ['nextTick', 'queueMicrotask'] });
});

afterEach(async () => {
await wallet?.destroy();
enableNetConnect();
disableNetConnect();
jest.useRealTimers();
});

Expand All @@ -66,9 +68,7 @@ describe('Wallet', () => {
).toStrictEqual(['0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf']);
});

it('signs transactions', async () => {
enableNetConnect();

it.skip('signs transactions', async () => {
wallet = await setupWallet();

const addresses = wallet.messenger
Expand Down
13 changes: 12 additions & 1 deletion packages/wallet/src/Wallet.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Messenger } from '@metamask/messenger';
import type { Json } from '@metamask/utils';
import type { Duplex } from 'stream';

import type {
DefaultActions,
Expand All @@ -9,6 +10,7 @@ import type {
RootMessenger,
} from './initialization';
import { initialize } from './initialization';
import { createProviderRpc } from './json-rpc/createProviderRpc';
import type { WalletOptions } from './types';

export type WalletConstructorArgs = {
Expand All @@ -27,7 +29,12 @@ export class Wallet {
namespace: 'Root',
});

this.#instances = initialize({ state, messenger: this.messenger, options });
this.#instances = initialize({
state,
messenger: this.messenger,
options,
createProviderRpc: this.createProviderRpc.bind(this),
});
}

get state(): DefaultState {
Expand All @@ -40,6 +47,10 @@ export class Wallet {
) as DefaultState;
}

createProviderRpc(stream: Duplex) {
return createProviderRpc(stream);
}

async destroy(): Promise<void> {
await Promise.all(
Object.values(this.#instances).map((instance) => {
Expand Down
14 changes: 12 additions & 2 deletions packages/wallet/src/initialization/initialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function initialize({
messenger,
initializationConfigurations = [],
options,
createProviderRpc,
}: InitializeArgs): DefaultInstances {
const overriddenConfiguration = initializationConfigurations.map(
(config) => config.name,
Expand All @@ -44,10 +45,19 @@ export function initialize({
state: instanceState,
messenger: instanceMessenger,
options,
createProviderRpc,
});

instances[name] = instance as Record<string, unknown>;
instances[name] = instance;
}

return instances as DefaultInstances;
const castInstances = instances as DefaultInstances;

Object.values(castInstances).forEach((instance) => {
if ('init' in instance) {
instance.init().catch(console.error);
}
});

return castInstances;
}
35 changes: 35 additions & 0 deletions packages/wallet/src/initialization/instances/execution-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Messenger } from '@metamask/messenger';
import {
ExecutionService,
ExecutionServiceMessenger,
NodeThreadExecutionService,
} from '@metamask/snaps-controllers/node';
import { Duplex } from 'stream';

import { InitializationConfiguration } from '../types';

export const executionService: InitializationConfiguration<
ExecutionService,
ExecutionServiceMessenger
> = {
name: 'ExecutionService',
init: ({ messenger, createProviderRpc }) => {
function setupSnapProvider(snapId: string, stream: Duplex) {
createProviderRpc(stream);
}

const instance = new NodeThreadExecutionService({
messenger,
setupSnapProvider,
});

return {
instance,
};
},
messenger: (parent) =>
new Messenger<'ExecutionService', never, never, typeof parent>({
namespace: 'ExecutionService',
parent,
}),
};
6 changes: 6 additions & 0 deletions packages/wallet/src/initialization/instances/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
export * from './accounts-controller';
export * from './approval-controller';
export * from './connectivity-controller';
export * from './execution-service';
export * from './storage-service';
export * from './snap-controller';
export * from './keyring-controller';
export * from './multichain-account-service';
export * from './network-controller';
export * from './permission-controller';
export * from './remote-feature-flag-controller';
export * from './subject-metadata-controller';
export * from './transaction-controller';
47 changes: 44 additions & 3 deletions packages/wallet/src/initialization/instances/keyring-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ import {
exportKey,
generateSalt,
} from '@metamask/browser-passworder';
import { SnapKeyring } from '@metamask/eth-snap-keyring';
import type { Encryptor } from '@metamask/keyring-controller';
import {
KeyringController,
KeyringControllerMessenger,
KeyringTypes,
} from '@metamask/keyring-controller';
import { Messenger } from '@metamask/messenger';

Expand Down Expand Up @@ -138,6 +140,25 @@ const encryptorFactory = (iterations: number): Encryptor => ({
generateSalt,
});

const createSnapKeyringBuilder = (messenger: KeyringControllerMessenger) => {
const SnapKeyringBuilder = (() => {
return new SnapKeyring({
messenger,
// callbacks: new SnapKeyringImpl(messenger, helpers),
isAnyAccountTypeAllowed: false,
});
}) as {
(): SnapKeyring;
type: typeof SnapKeyring.type;
state: null;
};

SnapKeyringBuilder.state = null;
SnapKeyringBuilder.type = SnapKeyring.type;

return SnapKeyringBuilder;
};

export const keyringController: InitializationConfiguration<
KeyringController,
KeyringControllerMessenger
Expand All @@ -148,15 +169,35 @@ export const keyringController: InitializationConfiguration<
state,
messenger,
encryptor: encryptorFactory(600_000),
keyringBuilders: [createSnapKeyringBuilder(messenger)],
});

// Ensure the SnapKeyring has been added, this happens in different places in the clients.
messenger.subscribe('KeyringController:unlock', () => {
const [snapKeyring] = instance.getKeyringsByType(KeyringTypes.snap);

if (!snapKeyring) {
instance.addNewKeyring(KeyringTypes.snap).catch(console.error);
}
});

return {
instance,
};
},
messenger: (parent) =>
new Messenger<'KeyringController', never, never, typeof parent>({
messenger: (parent) => {
const controllerMessenger: KeyringControllerMessenger = new Messenger({
namespace: 'KeyringController',
parent,
}),
});

// TODO: We only need to delegate here for the SnapKeyring, decide if we wanna do that
Copy link
Copy Markdown
Member

@rekmarks rekmarks Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this comment.

parent.delegate({
messenger: controllerMessenger,
events: [],
actions: ['SnapController:handleRequest'],
});

return controllerMessenger;
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Messenger } from '@metamask/messenger';
import {
MultichainAccountService,
SOL_ACCOUNT_PROVIDER_NAME,
TRX_ACCOUNT_PROVIDER_NAME,
BTC_ACCOUNT_PROVIDER_NAME,
MultichainAccountServiceMessenger,
} from '@metamask/multichain-account-service';

import { InitializationConfiguration } from '../types';

const snapAccountProviderConfig = {
// READ THIS CAREFULLY:
// We are using 1 to prevent any concurrent `keyring_createAccount` requests. This ensures
// we prevent any desync between Snap's accounts and Metamask's accounts.
maxConcurrency: 1,
// Re-use the default config for the rest:
discovery: {
timeoutMs: 2000,
maxAttempts: 3,
backOffMs: 1000,
},
createAccounts: {
timeoutMs: 3000,
batched: false,
},
resyncAccounts: {
autoRemoveExtraSnapAccounts: false,
},
};

export const multichainAccountService: InitializationConfiguration<
MultichainAccountService,
MultichainAccountServiceMessenger
> = {
name: 'MultichainAccountService',
init: ({ messenger, options }) => {
const instance = new MultichainAccountService({
messenger,
providerConfigs: {
[SOL_ACCOUNT_PROVIDER_NAME]: {
...snapAccountProviderConfig,
createAccounts: {
...snapAccountProviderConfig.createAccounts,
batched: true,
},
},
[BTC_ACCOUNT_PROVIDER_NAME]: snapAccountProviderConfig,
[TRX_ACCOUNT_PROVIDER_NAME]: snapAccountProviderConfig,
},
ensureOnboardingComplete: options.ensureOnboardingComplete,
});

// TODO: Basic Functionality triggers

return {
instance,
};
},
messenger: (parent) => {
const serviceMessenger: MultichainAccountServiceMessenger = new Messenger({
namespace: 'MultichainAccountService',
parent,
});

parent.delegate({
messenger: serviceMessenger,
events: [
'KeyringController:stateChange',
'SnapController:stateChange',
'AccountsController:accountAdded',
'AccountsController:accountRemoved',
],
actions: [
'AccountsController:listMultichainAccounts',
'AccountsController:getAccountByAddress',
'AccountsController:getAccount',
'AccountsController:getAccounts',
'SnapController:getState',
'SnapController:handleRequest',
'KeyringController:getState',
'KeyringController:withKeyring',
'KeyringController:addNewKeyring',
'KeyringController:getKeyringsByType',
'KeyringController:createNewVaultAndKeychain',
'KeyringController:createNewVaultAndRestore',
'NetworkController:getNetworkClientById',
'NetworkController:findNetworkClientIdByChainId',
],
});

return serviceMessenger;
},
};
Loading
Loading