diff --git a/jest.config.packages.js b/jest.config.packages.js index 07d249241ce..c9d42297eb6 100644 --- a/jest.config.packages.js +++ b/jest.config.packages.js @@ -84,6 +84,13 @@ module.exports = { '/../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/(.+)$': [ '/../$1/src', // Some @metamask/* packages we are referencing aren't in this monorepo, diff --git a/packages/wallet/package.json b/packages/wallet/package.json index 0ad8e733fae..d62971c566a 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -56,22 +56,34 @@ "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.3.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": "^6.1.0", "@metamask/foundryup": "^1.0.1", "@ts-bridge/cli": "^0.6.4", "@types/jest": "^29.5.14", + "@types/readable-stream": "^4", "deepmerge": "^4.2.2", "jest": "^29.7.0", "nock": "^13.3.1", diff --git a/packages/wallet/src/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts index 13cc7a899d0..0452c065f00 100644 --- a/packages/wallet/src/Wallet.test.ts +++ b/packages/wallet/src/Wallet.test.ts @@ -5,7 +5,7 @@ import { DistributionType, EnvironmentType, } from '@metamask/remote-feature-flag-controller'; -import { enableNetConnect } from 'nock'; +import { disableNetConnect, enableNetConnect } from 'nock'; import { startAnvil } from '../test/anvil'; import type { AnvilInstance } from '../test/anvil'; @@ -35,6 +35,7 @@ async function setupWallet(): Promise { }, }), getMetaMetricsId: (): string => 'fake-metrics-id', + ensureOnboardingComplete: () => Promise.resolve(), }, }); @@ -47,12 +48,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(); }); @@ -62,8 +64,14 @@ describe('Wallet', () => { expect( messenger - .call('AccountsController:listAccounts') - .map((account) => account.address), + .call('MultichainAccountService:getMultichainAccountWallets') + .flatMap((multichainWallet) => + multichainWallet + .getAccountGroups() + .flatMap((group) => + group.getAccounts().map((account) => account.address), + ), + ), ).toStrictEqual(['0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf']); }); diff --git a/packages/wallet/src/Wallet.ts b/packages/wallet/src/Wallet.ts index 3f60491b496..575c6681e3e 100644 --- a/packages/wallet/src/Wallet.ts +++ b/packages/wallet/src/Wallet.ts @@ -1,5 +1,6 @@ import { Messenger } from '@metamask/messenger'; import type { Json } from '@metamask/utils'; +import type { Duplex } from 'stream'; import type { DefaultActions, @@ -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 = { @@ -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 { @@ -40,6 +47,17 @@ export class Wallet { ) as DefaultState; } + createProviderRpc(args) { + return createProviderRpc({ + messenger: this.messenger, + createPermissionMiddleware: + this.#instances.PermissionController.createPermissionMiddleware.bind( + this.#instances.PermissionController, + ), + ...args, + }); + } + async destroy(): Promise { await Promise.all( Object.values(this.#instances).map((instance) => { diff --git a/packages/wallet/src/initialization/initialization.ts b/packages/wallet/src/initialization/initialization.ts index 2a977c52073..502f68c9a00 100644 --- a/packages/wallet/src/initialization/initialization.ts +++ b/packages/wallet/src/initialization/initialization.ts @@ -20,6 +20,7 @@ export function initialize({ messenger, initializationConfigurations = [], options, + createProviderRpc, }: InitializeArgs): DefaultInstances { const overriddenConfiguration = initializationConfigurations.map( (config) => config.name, @@ -44,10 +45,19 @@ export function initialize({ state: instanceState, messenger: instanceMessenger, options, + createProviderRpc, }); - instances[name] = instance as Record; + 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; } diff --git a/packages/wallet/src/initialization/instances/execution-service.ts b/packages/wallet/src/initialization/instances/execution-service.ts new file mode 100644 index 00000000000..4edb21f8855 --- /dev/null +++ b/packages/wallet/src/initialization/instances/execution-service.ts @@ -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({ origin: snapId, stream }); + } + + const instance = new NodeThreadExecutionService({ + messenger, + setupSnapProvider, + }); + + return { + instance, + }; + }, + messenger: (parent) => + new Messenger<'ExecutionService', never, never, typeof parent>({ + namespace: 'ExecutionService', + parent, + }), +}; diff --git a/packages/wallet/src/initialization/instances/index.ts b/packages/wallet/src/initialization/instances/index.ts index 4f869053f11..e2de77b8d9e 100644 --- a/packages/wallet/src/initialization/instances/index.ts +++ b/packages/wallet/src/initialization/instances/index.ts @@ -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'; diff --git a/packages/wallet/src/initialization/instances/keyring-controller.ts b/packages/wallet/src/initialization/instances/keyring-controller.ts index 7f3a356864c..2d4a8c5dba1 100644 --- a/packages/wallet/src/initialization/instances/keyring-controller.ts +++ b/packages/wallet/src/initialization/instances/keyring-controller.ts @@ -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'; @@ -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 @@ -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 + parent.delegate({ + messenger: controllerMessenger, + events: [], + actions: ['SnapController:handleRequest'], + }); + + return controllerMessenger; + }, }; diff --git a/packages/wallet/src/initialization/instances/multichain-account-service.ts b/packages/wallet/src/initialization/instances/multichain-account-service.ts new file mode 100644 index 00000000000..370c9e0fa9a --- /dev/null +++ b/packages/wallet/src/initialization/instances/multichain-account-service.ts @@ -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; + }, +}; diff --git a/packages/wallet/src/initialization/instances/permission-controller.ts b/packages/wallet/src/initialization/instances/permission-controller.ts new file mode 100644 index 00000000000..022f214cc67 --- /dev/null +++ b/packages/wallet/src/initialization/instances/permission-controller.ts @@ -0,0 +1,53 @@ +import { Messenger } from '@metamask/messenger'; +import { + PermissionController, + PermissionControllerMessenger, +} from '@metamask/permission-controller'; + +import { + getCaveatSpecifications, + getPermissionSpecifications, + unrestrictedMethods, +} from '../../permissions/specifications'; +import { InitializationConfiguration } from '../types'; + +export const permissionController: InitializationConfiguration< + PermissionController, + PermissionControllerMessenger +> = { + name: 'PermissionController', + init: ({ messenger, state }) => { + const instance = new PermissionController({ + messenger, + state, + permissionSpecifications: getPermissionSpecifications({}), + caveatSpecifications: getCaveatSpecifications({}), + unrestrictedMethods, + }); + + return { + instance, + }; + }, + messenger: (parent) => { + const controllerMessenger: PermissionControllerMessenger = new Messenger({ + namespace: 'PermissionController', + parent, + }); + + parent.delegate({ + messenger: controllerMessenger, + actions: [ + 'ApprovalController:addRequest', + 'ApprovalController:hasRequest', + 'ApprovalController:acceptRequest', + 'ApprovalController:rejectRequest', + 'SnapController:getPermittedSnaps', + 'SnapController:installSnaps', + 'SubjectMetadataController:getSubjectMetadata', + ], + }); + + return controllerMessenger; + }, +}; diff --git a/packages/wallet/src/initialization/instances/snap-controller.ts b/packages/wallet/src/initialization/instances/snap-controller.ts new file mode 100644 index 00000000000..22ef7c64c6a --- /dev/null +++ b/packages/wallet/src/initialization/instances/snap-controller.ts @@ -0,0 +1,91 @@ +import BitcoinWalletSnap from '@metamask/bitcoin-wallet-snap/dist/preinstalled-snap.json'; +import { Messenger } from '@metamask/messenger'; +import { + SnapController, + SnapControllerMessenger, + PersistedSnapControllerState, +} from '@metamask/snaps-controllers'; +import SolanaWalletSnap from '@metamask/solana-wallet-snap/dist/preinstalled-snap.json'; +import TronWalletSnap from '@metamask/tron-wallet-snap/dist/preinstalled-snap.json'; + +import { + EndowmentPermissions, + ExcludedSnapEndowments, + ExcludedSnapPermissions, +} from '../../permissions/specifications'; +import { InitializationConfiguration } from '../types'; + +export const snapController: InitializationConfiguration< + SnapController, + SnapControllerMessenger +> = { + name: 'SnapController', + init: ({ messenger, state, options }) => { + const instance = new SnapController({ + messenger, + // Persisted state is different from actual state, consider changing `state` inference type. + state: state as PersistedSnapControllerState, + + environmentEndowmentPermissions: Object.values(EndowmentPermissions), + excludedPermissions: { + ...ExcludedSnapPermissions, + ...ExcludedSnapEndowments, + }, + + ensureOnboardingComplete: options.ensureOnboardingComplete, + preinstalledSnaps: [SolanaWalletSnap, BitcoinWalletSnap, TronWalletSnap], + }); + + return { + instance, + }; + }, + messenger: (parent) => { + const controllerMessenger: SnapControllerMessenger = new Messenger({ + namespace: 'SnapController', + parent, + }); + + parent.delegate({ + messenger: controllerMessenger, + events: [ + 'ExecutionService:unhandledError', + 'ExecutionService:outboundRequest', + 'ExecutionService:outboundResponse', + 'KeyringController:lock', + 'SnapRegistryController:registryUpdated', + ], + actions: [ + 'PermissionController:getEndowments', + 'PermissionController:getPermissions', + 'PermissionController:hasPermission', + 'PermissionController:hasPermissions', + 'PermissionController:revokeAllPermissions', + 'PermissionController:revokePermissions', + 'PermissionController:revokePermissionForAllSubjects', + 'PermissionController:getSubjectNames', + 'PermissionController:updateCaveat', + 'ApprovalController:addRequest', + 'ApprovalController:updateRequestState', + 'PermissionController:grantPermissions', + 'SubjectMetadataController:getSubjectMetadata', + 'SubjectMetadataController:addSubjectMetadata', + 'ExecutionService:executeSnap', + 'ExecutionService:terminateSnap', + 'ExecutionService:handleRpcRequest', + 'SnapRegistryController:get', + 'SnapRegistryController:getMetadata', + 'SnapRegistryController:requestUpdate', + 'SnapRegistryController:resolveVersion', + 'SnapInterfaceController:createInterface', + 'SnapInterfaceController:getInterface', + 'SnapInterfaceController:setInterfaceDisplayed', + 'StorageService:setItem', + 'StorageService:getItem', + 'StorageService:removeItem', + 'StorageService:clear', + ], + }); + return controllerMessenger; + }, +}; diff --git a/packages/wallet/src/initialization/instances/storage-service.ts b/packages/wallet/src/initialization/instances/storage-service.ts new file mode 100644 index 00000000000..de2f1a09ee6 --- /dev/null +++ b/packages/wallet/src/initialization/instances/storage-service.ts @@ -0,0 +1,28 @@ +import { Messenger } from '@metamask/messenger'; +import { + StorageService, + StorageServiceMessenger, +} from '@metamask/storage-service'; + +import { InitializationConfiguration } from '../types'; + +export const storageService: InitializationConfiguration< + StorageService, + StorageServiceMessenger +> = { + name: 'StorageService', + init: ({ messenger }) => { + const instance = new StorageService({ + messenger, + }); + + return { + instance, + }; + }, + messenger: (parent) => + new Messenger<'StorageService', never, never, typeof parent>({ + namespace: 'StorageService', + parent, + }), +}; diff --git a/packages/wallet/src/initialization/instances/subject-metadata-controller.ts b/packages/wallet/src/initialization/instances/subject-metadata-controller.ts new file mode 100644 index 00000000000..3d59a340cf9 --- /dev/null +++ b/packages/wallet/src/initialization/instances/subject-metadata-controller.ts @@ -0,0 +1,39 @@ +import { Messenger } from '@metamask/messenger'; +import { + SubjectMetadataController, + SubjectMetadataControllerMessenger, +} from '@metamask/permission-controller'; + +import { InitializationConfiguration } from '../types'; + +export const subjectMetadataController: InitializationConfiguration< + SubjectMetadataController, + SubjectMetadataControllerMessenger +> = { + name: 'SubjectMetadataController', + init: ({ messenger, state }) => { + const instance = new SubjectMetadataController({ + messenger, + state, + subjectCacheLimit: 100, + }); + + return { + instance, + }; + }, + messenger: (parent) => { + const controllerMessenger: SubjectMetadataControllerMessenger = + new Messenger({ + namespace: 'SubjectMetadataController', + parent, + }); + + parent.delegate({ + messenger: controllerMessenger, + actions: ['PermissionController:hasPermissions'], + }); + + return controllerMessenger; + }, +}; diff --git a/packages/wallet/src/json-rpc/createProviderRpc.ts b/packages/wallet/src/json-rpc/createProviderRpc.ts new file mode 100644 index 00000000000..df9fbdc9cb1 --- /dev/null +++ b/packages/wallet/src/json-rpc/createProviderRpc.ts @@ -0,0 +1,268 @@ +import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { asLegacyMiddleware } from '@metamask/json-rpc-engine/v2'; +import { createEngineStream } from '@metamask/json-rpc-middleware-stream'; +import ObjectMultiplex from '@metamask/object-multiplex'; +import { SubjectType } from '@metamask/permission-controller'; +import { + createWalletSnapPermissionMiddleware, + createSnapsMethodMiddleware, +} from '@metamask/snaps-rpc-methods'; +import { Duplex, pipeline } from 'readable-stream'; + +import { RootMessenger } from '../initialization'; + +const METAMASK_EIP_1193_PROVIDER = 'metamask-provider'; +const METAMASK_CAIP_MULTICHAIN_PROVIDER = 'metamask-multichain-provider'; + +/** + * Sets up stream multiplexing for the given stream + * + * @param connectionStream - the stream to mux + * @returns the multiplexed stream + */ +export function setupMultiplex(connectionStream: Duplex): ObjectMultiplex { + const mux = new ObjectMultiplex(); + pipeline(connectionStream, mux, connectionStream, (err: Error | null) => { + if (err && !err.message?.match('Premature close')) { + console.error(err); + } + }); + return mux; +} + +export function createRpcHooks(origin: string, messenger: RootMessenger) { + return { + clearSnapState: messenger.call.bind( + messenger, + 'SnapController:clearSnapState', + origin, + ), + getUnlockPromise: messenger.call.bind( + messenger, + 'AppStateController:getUnlockPromise', + ), + getSnaps: messenger.call.bind( + messenger, + 'SnapController:getPermittedSnaps', + origin, + ), + requestPermissions: messenger.call.bind( + messenger, + 'PermissionController:requestPermissions', + { origin }, + ), + getPermissions: messenger.call.bind( + messenger, + 'PermissionController:getPermissions', + origin, + ), + getSnapFile: messenger.call.bind( + messenger, + 'SnapController:getSnapFile', + origin, + ), + getSnapState: messenger.call.bind( + messenger, + 'SnapController:getSnapState', + origin, + ), + updateSnapState: messenger.call.bind( + messenger, + 'SnapController:updateSnapState', + origin, + ), + installSnaps: messenger.call.bind( + messenger, + 'SnapController:installSnaps', + origin, + ), + invokeSnap: messenger.call.bind( + messenger, + 'PermissionController:executeRestrictedMethod', + origin, + 'wallet_snap', + ), + getIsLocked: () => { + const { isUnlocked } = messenger.call('KeyringController:getState'); + + return !isUnlocked; + }, + getIsActive: () => { + const { isUnlocked } = messenger.call('KeyringController:getState'); + + return Boolean(this._isClientOpen && isUnlocked); + }, + getVersion: () => { + return process.env.METAMASK_VERSION; + }, + getInterfaceState: (...args) => + messenger.call( + 'SnapInterfaceController:getInterfaceState', + origin, + ...args, + ), + getInterfaceContext: (...args) => + messenger.call('SnapInterfaceController:getInterface', origin, ...args) + .context, + createInterface: messenger.call.bind( + messenger, + 'SnapInterfaceController:createInterface', + origin, + ), + updateInterface: messenger.call.bind( + messenger, + 'SnapInterfaceController:updateInterface', + origin, + ), + resolveInterface: messenger.call.bind( + messenger, + 'SnapInterfaceController:resolveInterface', + origin, + ), + getSnap: messenger.call.bind(messenger, 'SnapController:getSnap'), + trackError: (error) => { + // `captureException` imported from `@sentry/browser` does not seem to + // work in E2E tests. This is a workaround which works in both E2E + // tests and production. + return global.sentry?.captureException?.(error); + }, + /** + trackEvent: this.metaMetricsController.trackEvent.bind( + this.metaMetricsController, + ),* + */ + getAllSnaps: messenger.call.bind(messenger, 'SnapController:getAllSnaps'), + openWebSocket: messenger.call.bind( + messenger, + 'WebSocketService:open', + origin, + ), + closeWebSocket: messenger.call.bind( + messenger, + 'WebSocketService:close', + origin, + ), + getWebSockets: messenger.call.bind( + messenger, + 'WebSocketService:getAll', + origin, + ), + sendWebSocketMessage: messenger.call.bind( + messenger, + 'WebSocketService:sendMessage', + origin, + ), + getCurrencyRate: (currency) => { + const state = this._getMetaMaskState(); + const fiatCurrency = getRatesControllerFiatCurrency(state); + const rate = getRatesControllerRates(state)[currency]; + + if (!rate) { + return undefined; + } + + return { + ...rate, + currency: fiatCurrency, + }; + }, + getEntropySources: () => { + /** + * @type {KeyringController['state']} + */ + const state = messenger.call('KeyringController:getState'); + + return state.keyrings + .map((keyring, index) => { + if (keyring.type === KeyringTypes.hd) { + return { + id: keyring.metadata.id, + name: keyring.metadata.name, + type: 'mnemonic', + primary: index === 0, + }; + } + + return null; + }) + .filter(Boolean); + }, + hasPermission: messenger.call.bind( + messenger, + 'PermissionController:hasPermission', + origin, + ), + scheduleBackgroundEvent: (event) => + messenger.call('CronjobController:schedule', { + ...event, + snapId: origin, + }), + cancelBackgroundEvent: messenger.call.bind( + messenger, + 'CronjobController:cancel', + origin, + ), + getBackgroundEvents: messenger.call.bind( + messenger, + 'CronjobController:get', + origin, + ), + getNetworkConfigurationByChainId: messenger.call.bind( + messenger, + 'NetworkController:getNetworkConfigurationByChainId', + ), + getNetworkClientById: messenger.call.bind( + messenger, + 'NetworkController:getNetworkClientById', + ), + startTrace: () => {}, + endTrace: () => {}, + handleSnapRpcRequest: (args) => + messenger.call('SnapController:handleRequest', { ...args, origin }), + getAllowedKeyringMethods: () => [], + }; +} + +export function createProviderRpc({ + origin, + subjectType, + messenger, + stream, +}: { + origin: string; + subjectType: SubjectType; + messenger: RootMessenger; + stream: Duplex; +}) { + const mux = setupMultiplex(stream); + + // TODO: Use V2, currently not compatible with createEngineStream. + const engine = new JsonRpcEngine(); + + // TODO: A bunch of middlewares are missing. + // TODO: Configure additional client-specific middlewares + + engine.push(asLegacyMiddleware(createWalletSnapPermissionMiddleware())); + + engine.push(messenger.call('PermissionController:createPermissionMiddleware', { origin })); + + const hooks = createRpcHooks(origin, messenger); + + engine.push( + createSnapsMethodMiddleware(subjectType === SubjectType.Snap, hooks), + ); + + // TODO: CAIP provider + const providerStream = mux.createStream(METAMASK_EIP_1193_PROVIDER); + + const engineStream = createEngineStream({ engine }); + + pipeline(providerStream, engineStream, providerStream, (error) => { + engine.destroy(); + if (error && !error.message?.match('Premature close')) { + console.error(error); + } + }); + + return { engine }; +} diff --git a/packages/wallet/src/permissions/specifications.ts b/packages/wallet/src/permissions/specifications.ts new file mode 100644 index 00000000000..ebb3d5fec90 --- /dev/null +++ b/packages/wallet/src/permissions/specifications.ts @@ -0,0 +1,199 @@ +import { + Caip25CaveatType, + caip25CaveatBuilder, + caip25EndowmentBuilder, +} from '@metamask/chain-agnostic-permission'; +import { + buildSnapEndowmentSpecifications, + buildSnapRestrictedMethodSpecifications, + caveatSpecifications as snapsCaveatsSpecifications, + endowmentCaveatSpecifications as snapsEndowmentCaveatSpecifications, +} from '@metamask/snaps-rpc-methods'; + +export const EndowmentPermissions = Object.freeze({ + 'endowment:network-access': 'endowment:network-access', + 'endowment:transaction-insight': 'endowment:transaction-insight', + 'endowment:cronjob': 'endowment:cronjob', + 'endowment:ethereum-provider': 'endowment:ethereum-provider', + 'endowment:rpc': 'endowment:rpc', + 'endowment:webassembly': 'endowment:webassembly', + 'endowment:lifecycle-hooks': 'endowment:lifecycle-hooks', + 'endowment:multichain-provider': 'endowment:multichain-provider', + 'endowment:page-home': 'endowment:page-home', + 'endowment:page-settings': 'endowment:page-settings', + 'endowment:signature-insight': 'endowment:signature-insight', + 'endowment:name-lookup': 'endowment:name-lookup', + 'endowment:assets': 'endowment:assets', + 'endowment:protocol': 'endowment:protocol', + 'endowment:keyring': 'endowment:keyring', +} as const); + +export const ExcludedSnapPermissions = Object.freeze({}); + +export const ExcludedSnapEndowments = Object.freeze({ + 'endowment:caip25': + 'eth_accounts is disabled. For more information please see https://github.com/MetaMask/snaps/issues/990.', +}); + +/** + * Gets the specifications for all permissions that will be recognized by the + * PermissionController. + * + * @returns the permission specifications to construct the PermissionController. + */ +export const getPermissionSpecifications = () => { + return { + [caip25EndowmentBuilder.targetName]: + caip25EndowmentBuilder.specificationBuilder({}), + ...buildSnapEndowmentSpecifications(Object.keys(ExcludedSnapEndowments)), + ...buildSnapRestrictedMethodSpecifications( + Object.keys(ExcludedSnapPermissions), + {}, + ), + }; +}; + +/** + * Gets the specifications for all caveats that will be recognized by the + * PermissionController. + * + * @param options - The options object. + * @param options.listAccounts - A function that returns the + * `AccountsController` internalAccount objects for all evm accounts. + * @param options.findNetworkClientIdByChainId - A function that + * returns the networkClientId given a chainId. + * @param options.isNonEvmScopeSupported - A function that returns true if + * a non-evm scope is supported. + * @param options.getNonEvmAccountAddresses - A function that returns the + * supported CAIP-10 account addresses for a non-evm scope. + * @returns the caveat specifications to construct the PermissionController. + */ +export const getCaveatSpecifications = ({ + listAccounts, + findNetworkClientIdByChainId, + isNonEvmScopeSupported, + getNonEvmAccountAddresses, +}: Parameters[0]) => { + return { + [Caip25CaveatType]: caip25CaveatBuilder({ + listAccounts, + findNetworkClientIdByChainId, + isNonEvmScopeSupported, + getNonEvmAccountAddresses, + }), + ...snapsCaveatsSpecifications, + ...snapsEndowmentCaveatSpecifications, + }; +}; + +/** + * All unrestricted methods recognized by the PermissionController. + * Unrestricted methods are ignored by the permission system, but every + * JSON-RPC request seen by the permission system must correspond to a + * restricted or unrestricted method, or the request will be rejected with a + * "method not found" error. + */ +export const unrestrictedMethods = Object.freeze([ + 'eth_blockNumber', + 'eth_call', + 'eth_chainId', + 'eth_coinbase', + 'eth_decrypt', + 'eth_estimateGas', + 'eth_feeHistory', + 'eth_gasPrice', + 'eth_getBalance', + 'eth_getBlockByHash', + 'eth_getBlockByNumber', + 'eth_getBlockTransactionCountByHash', + 'eth_getBlockTransactionCountByNumber', + 'eth_getCode', + 'eth_getEncryptionPublicKey', + 'eth_getFilterChanges', + 'eth_getFilterLogs', + 'eth_getLogs', + 'eth_getProof', + 'eth_getStorageAt', + 'eth_getTransactionByBlockHashAndIndex', + 'eth_getTransactionByBlockNumberAndIndex', + 'eth_getTransactionByHash', + 'eth_getTransactionCount', + 'eth_getTransactionReceipt', + 'eth_getUncleByBlockHashAndIndex', + 'eth_getUncleByBlockNumberAndIndex', + 'eth_getUncleCountByBlockHash', + 'eth_getUncleCountByBlockNumber', + 'eth_getWork', + 'eth_hashrate', + 'eth_mining', + 'eth_newBlockFilter', + 'eth_newFilter', + 'eth_newPendingTransactionFilter', + 'eth_protocolVersion', + 'eth_requestAccounts', + 'eth_sendRawTransaction', + 'eth_sendTransaction', + 'eth_signTypedData', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + 'eth_submitHashrate', + 'eth_submitWork', + 'eth_subscribe', + 'eth_syncing', + 'eth_uninstallFilter', + 'eth_unsubscribe', + 'metamask_getProviderState', + 'metamask_logWeb3ShimUsage', + 'metamask_sendDomainMetadata', + 'metamask_watchAsset', + 'net_listening', + 'net_peerCount', + 'net_version', + 'personal_ecRecover', + 'personal_sign', + 'wallet_requestExecutionPermissions', + 'wallet_getSupportedExecutionPermissions', + 'wallet_getGrantedExecutionPermissions', + 'wallet_addEthereumChain', + 'wallet_getCallsStatus', + 'wallet_getCapabilities', + 'wallet_getPermissions', + 'wallet_requestPermissions', + 'wallet_revokePermissions', + 'wallet_registerOnboarding', + 'wallet_sendCalls', + 'wallet_switchEthereumChain', + 'wallet_watchAsset', + 'wallet_upgradeAccount', + 'wallet_getAccountUpgradeStatus', + 'web3_clientVersion', + 'web3_sha3', + 'wallet_getAllSnaps', + 'wallet_getSnaps', + 'wallet_requestSnaps', + 'wallet_invokeSnap', + 'wallet_invokeKeyring', + 'snap_getClientStatus', + 'snap_clearState', + 'snap_getFile', + 'snap_getState', + 'snap_listEntropySources', + 'snap_createInterface', + 'snap_updateInterface', + 'snap_getInterfaceState', + 'snap_getInterfaceContext', + 'snap_resolveInterface', + 'snap_setState', + 'snap_scheduleBackgroundEvent', + 'snap_cancelBackgroundEvent', + 'snap_getBackgroundEvents', + 'snap_trackError', + 'snap_trackEvent', + 'snap_openWebSocket', + 'snap_sendWebSocketMessage', + 'snap_closeWebSocket', + 'snap_getWebSockets', + 'snap_startTrace', + 'snap_endTrace', +]); diff --git a/packages/wallet/src/types.ts b/packages/wallet/src/types.ts index e808637e474..3897fcfb353 100644 --- a/packages/wallet/src/types.ts +++ b/packages/wallet/src/types.ts @@ -6,4 +6,5 @@ export type WalletOptions = { showApprovalRequest: () => void; clientConfigApiService: ClientConfigApiService; getMetaMetricsId: () => string; + ensureOnboardingComplete: () => Promise; }; diff --git a/packages/wallet/src/utilities.ts b/packages/wallet/src/utilities.ts index edd281f968b..b27e46ab86a 100644 --- a/packages/wallet/src/utilities.ts +++ b/packages/wallet/src/utilities.ts @@ -23,12 +23,12 @@ export async function importSecretRecoveryPhrase( const indices = phrase.split(' ').map((word) => wordlist.indexOf(word)); const mnemonic = new Uint8Array(new Uint16Array(indices).buffer); - // TODO: This should use the new MultichainAccountService. - await wallet.messenger.call( - 'KeyringController:createNewVaultAndRestore', - password, - mnemonic, + const multichainWallet = await wallet.messenger.call( + 'MultichainAccountService:createMultichainAccountWallet', + { type: 'restore', password, mnemonic }, ); + + await multichainWallet.discoverAccounts(); } /** @@ -41,11 +41,12 @@ export async function createSecretRecoveryPhrase( wallet: Wallet, password: string, ): Promise { - // TODO: This should use the new MultichainAccountService. - await wallet.messenger.call( - 'KeyringController:createNewVaultAndKeychain', - password, + const multichainWallet = await wallet.messenger.call( + 'MultichainAccountService:createMultichainAccountWallet', + { type: 'create', password }, ); + + await multichainWallet.discoverAccounts(); } /** diff --git a/packages/wallet/tsconfig.build.json b/packages/wallet/tsconfig.build.json index a5e012287d5..87e6476f402 100644 --- a/packages/wallet/tsconfig.build.json +++ b/packages/wallet/tsconfig.build.json @@ -24,6 +24,9 @@ { "path": "../messenger/tsconfig.build.json" }, + { + "path": "../multichain-account-service/tsconfig.build.json" + }, { "path": "../network-controller/tsconfig.build.json" }, diff --git a/packages/wallet/tsconfig.json b/packages/wallet/tsconfig.json index 8f0b0c57883..91ec181232f 100644 --- a/packages/wallet/tsconfig.json +++ b/packages/wallet/tsconfig.json @@ -22,6 +22,9 @@ { "path": "../messenger/tsconfig.json" }, + { + "path": "../multichain-account-service/tsconfig.json" + }, { "path": "../network-controller/tsconfig.json" }, diff --git a/yarn.lock b/yarn.lock index b28ccc07915..d077a127ab3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2986,6 +2986,13 @@ __metadata: languageName: unknown linkType: soft +"@metamask/bitcoin-wallet-snap@npm:^1.10.1": + version: 1.10.1 + resolution: "@metamask/bitcoin-wallet-snap@npm:1.10.1" + checksum: 10/647c0c6211011a54f97fe58f97e930693c8dd64a861969d3546486fdadc8f0efb7b39328469a07408c8ab76fc79d63a74d06c12de91e60204ce040f8ee89741d + languageName: node + linkType: hard + "@metamask/bridge-controller@npm:^70.1.1, @metamask/bridge-controller@workspace:packages/bridge-controller": version: 0.0.0-use.local resolution: "@metamask/bridge-controller@workspace:packages/bridge-controller" @@ -4099,7 +4106,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/json-rpc-engine@npm:^10.0.0, @metamask/json-rpc-engine@npm:^10.0.2, @metamask/json-rpc-engine@npm:^10.1.0, @metamask/json-rpc-engine@npm:^10.1.1, @metamask/json-rpc-engine@npm:^10.2.4, @metamask/json-rpc-engine@workspace:packages/json-rpc-engine": +"@metamask/json-rpc-engine@npm:^10.0.0, @metamask/json-rpc-engine@npm:^10.0.2, @metamask/json-rpc-engine@npm:^10.1.0, @metamask/json-rpc-engine@npm:^10.1.1, @metamask/json-rpc-engine@npm:^10.2.3, @metamask/json-rpc-engine@npm:^10.2.4, @metamask/json-rpc-engine@workspace:packages/json-rpc-engine": version: 0.0.0-use.local resolution: "@metamask/json-rpc-engine@workspace:packages/json-rpc-engine" dependencies: @@ -5348,6 +5355,67 @@ __metadata: languageName: node linkType: hard +"@metamask/snaps-controllers@npm:^20.0.1": + version: 20.0.1 + resolution: "@metamask/snaps-controllers@npm:20.0.1" + dependencies: + "@metamask/approval-controller": "npm:^9.0.1" + "@metamask/base-controller": "npm:^9.0.1" + "@metamask/json-rpc-engine": "npm:^10.2.4" + "@metamask/json-rpc-middleware-stream": "npm:^8.0.8" + "@metamask/key-tree": "npm:^10.1.1" + "@metamask/messenger": "npm:^1.1.1" + "@metamask/object-multiplex": "npm:^2.1.0" + "@metamask/permission-controller": "npm:^12.3.0" + "@metamask/post-message-stream": "npm:^10.0.0" + "@metamask/rpc-errors": "npm:^7.0.3" + "@metamask/snaps-registry": "npm:^4.0.0" + "@metamask/snaps-rpc-methods": "npm:^15.1.1" + "@metamask/snaps-sdk": "npm:^11.1.0" + "@metamask/snaps-utils": "npm:^12.2.0" + "@metamask/storage-service": "npm:^1.0.1" + "@metamask/superstruct": "npm:^3.2.1" + "@metamask/utils": "npm:^11.11.0" + "@xstate/fsm": "npm:^2.0.0" + async-mutex: "npm:^0.5.0" + concat-stream: "npm:^2.0.0" + cron-parser: "npm:^4.5.0" + fast-deep-equal: "npm:^3.1.3" + get-npm-tarball-url: "npm:^2.0.3" + immer: "npm:^9.0.21" + luxon: "npm:^3.5.0" + nanoid: "npm:^3.3.10" + readable-stream: "npm:^3.6.2" + readable-web-to-node-stream: "npm:^3.0.2" + semver: "npm:^7.5.4" + tar-stream: "npm:^3.1.7" + peerDependencies: + "@metamask/snaps-execution-environments": ^11.0.2 + peerDependenciesMeta: + "@metamask/snaps-execution-environments": + optional: true + checksum: 10/ccacdffa8b630a777f7e95ad2a795a95663b0ae4da1fe78c35fa4b1e3590318687e5edb3cd769661e74fc03ee2e59bcd62098657e9b00163b216eb8424b8a135 + languageName: node + linkType: hard + +"@metamask/snaps-execution-environments@npm:^11.0.2": + version: 11.0.2 + resolution: "@metamask/snaps-execution-environments@npm:11.0.2" + dependencies: + "@metamask/json-rpc-engine": "npm:^10.2.3" + "@metamask/object-multiplex": "npm:^2.1.0" + "@metamask/post-message-stream": "npm:^10.0.0" + "@metamask/providers": "npm:^22.1.1" + "@metamask/rpc-errors": "npm:^7.0.3" + "@metamask/snaps-sdk": "npm:^11.0.0" + "@metamask/snaps-utils": "npm:^12.1.2" + "@metamask/superstruct": "npm:^3.2.1" + "@metamask/utils": "npm:^11.10.0" + readable-stream: "npm:^3.6.2" + checksum: 10/9faa7bba96688fe1430bbe7797df1693e7f3a67c7d993fdba2731fa6ce062910ab48eecb36188e7448d452d5642bafe42a5bf9ab6abeb3b0e31962fe24495993 + languageName: node + linkType: hard + "@metamask/snaps-registry@npm:^4.0.0": version: 4.0.0 resolution: "@metamask/snaps-registry@npm:4.0.0" @@ -5360,7 +5428,7 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-rpc-methods@npm:^15.0.2": +"@metamask/snaps-rpc-methods@npm:^15.0.2, @metamask/snaps-rpc-methods@npm:^15.1.1": version: 15.1.1 resolution: "@metamask/snaps-rpc-methods@npm:15.1.1" dependencies: @@ -5445,6 +5513,13 @@ __metadata: languageName: unknown linkType: soft +"@metamask/solana-wallet-snap@npm:^2.8.0": + version: 2.8.0 + resolution: "@metamask/solana-wallet-snap@npm:2.8.0" + checksum: 10/5e26f28d585aa00c4da204a7311f15048de30ff336baa90df1ca82f0d151b25dcf7b3fc3daa8b9ab02914aca35f31eb7a2854bf167008f6e96f99e215f02767c + languageName: node + linkType: hard + "@metamask/stake-sdk@npm:^3.2.1": version: 3.2.1 resolution: "@metamask/stake-sdk@npm:3.2.1" @@ -5623,6 +5698,13 @@ __metadata: languageName: unknown linkType: soft +"@metamask/tron-wallet-snap@npm:^1.25.2": + version: 1.25.2 + resolution: "@metamask/tron-wallet-snap@npm:1.25.2" + checksum: 10/1f42a2d14b50895613dca01398b16c94b510ff55c4fa0a4292272d8820ca2664bf2dc8985d833317fa667a124a58565bd89d4be867c74e0f82792c43095b8620 + languageName: node + linkType: hard + "@metamask/user-operation-controller@workspace:packages/user-operation-controller": version: 0.0.0-use.local resolution: "@metamask/user-operation-controller@workspace:packages/user-operation-controller" @@ -5703,22 +5785,34 @@ __metadata: "@metamask/accounts-controller": "npm:^37.2.0" "@metamask/approval-controller": "npm:^9.0.1" "@metamask/auto-changelog": "npm:^6.1.0" + "@metamask/bitcoin-wallet-snap": "npm:^1.10.1" "@metamask/browser-passworder": "npm:^6.0.0" "@metamask/connectivity-controller": "npm:^0.2.0" "@metamask/controller-utils": "npm:^11.20.0" "@metamask/foundryup": "npm:^1.0.1" + "@metamask/json-rpc-engine": "npm:^10.2.4" + "@metamask/json-rpc-middleware-stream": "npm:^8.0.8" "@metamask/keyring-controller": "npm:^25.2.0" "@metamask/messenger": "npm:^1.1.1" + "@metamask/multichain-account-service": "npm:^8.0.1" "@metamask/network-controller": "npm:^30.0.1" + "@metamask/object-multiplex": "npm:^2.1.0" + "@metamask/permission-controller": "npm:^12.3.0" "@metamask/remote-feature-flag-controller": "npm:^4.2.0" "@metamask/scure-bip39": "npm:^2.1.1" + "@metamask/snaps-controllers": "npm:^20.0.1" + "@metamask/snaps-execution-environments": "npm:^11.0.2" + "@metamask/solana-wallet-snap": "npm:^2.8.0" "@metamask/transaction-controller": "npm:^64.3.0" + "@metamask/tron-wallet-snap": "npm:^1.25.2" "@metamask/utils": "npm:^11.9.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" + "@types/readable-stream": "npm:^4" deepmerge: "npm:^4.2.2" jest: "npm:^29.7.0" nock: "npm:^13.3.1" + readable-stream: "npm:^4.7.0" ts-jest: "npm:^29.2.5" tsx: "npm:^4.20.5" typedoc: "npm:^0.25.13" @@ -7007,6 +7101,15 @@ __metadata: languageName: node linkType: hard +"@types/readable-stream@npm:^4": + version: 4.0.23 + resolution: "@types/readable-stream@npm:4.0.23" + dependencies: + "@types/node": "npm:*" + checksum: 10/2798c083eb74c9c5910cdda2e8132e333e2435362cc811eb866871e6a2ea77cd5a665dd3085be0e45ccc90a6d7b533d3d221f3b5ccfcf3080f4133517105b3fd + languageName: node + linkType: hard + "@types/secp256k1@npm:^4.0.1": version: 4.0.6 resolution: "@types/secp256k1@npm:4.0.6" @@ -13440,6 +13543,19 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:^4.7.0": + version: 4.7.0 + resolution: "readable-stream@npm:4.7.0" + dependencies: + abort-controller: "npm:^3.0.0" + buffer: "npm:^6.0.3" + events: "npm:^3.3.0" + process: "npm:^0.11.10" + string_decoder: "npm:^1.3.0" + checksum: 10/bdf096c8ff59452ce5d08f13da9597f9fcfe400b4facfaa88e74ec057e5ad1fdfa140ffe28e5ed806cf4d2055f0b812806e962bca91dce31bc4cef08e53be3a4 + languageName: node + linkType: hard + "readable-web-to-node-stream@npm:^3.0.2": version: 3.0.2 resolution: "readable-web-to-node-stream@npm:3.0.2"