UNPKG

@metamask/snaps-simulation

Version:

A simulation framework for MetaMask Snaps, enabling headless testing of Snaps in a controlled environment

230 lines 11 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.registerActions = exports.getPermittedHooks = exports.getRestrictedHooks = exports.installSnap = void 0; const base_controller_1 = require("@metamask/base-controller"); const json_rpc_middleware_stream_1 = require("@metamask/json-rpc-middleware-stream"); const phishing_controller_1 = require("@metamask/phishing-controller"); const node_1 = require("@metamask/snaps-controllers/node"); const snaps_rpc_methods_1 = require("@metamask/snaps-rpc-methods"); const snaps_utils_1 = require("@metamask/snaps-utils"); const readable_stream_1 = require("readable-stream"); const effects_1 = require("redux-saga/effects"); const controllers_1 = require("./controllers.cjs"); const files_1 = require("./files.cjs"); const helpers_1 = require("./helpers.cjs"); const interface_1 = require("./interface.cjs"); const methods_1 = require("./methods/index.cjs"); const hooks_1 = require("./methods/hooks/index.cjs"); const get_mnemonic_seed_1 = require("./methods/hooks/get-mnemonic-seed.cjs"); const middleware_1 = require("./middleware/index.cjs"); const options_1 = require("./options.cjs"); const store_1 = require("./store/index.cjs"); const account_1 = require("./utils/account.cjs"); /** * Install a Snap in a simulated environment. This will fetch the Snap files, * create a Redux store, set up the controllers and JSON-RPC stack, register the * Snap, and run the Snap code in the execution service. * * @param snapId - The ID of the Snap to install. * @param options - The options to use when installing the Snap. * @param options.executionService - The execution service to use. * @param options.executionServiceOptions - The options to use when creating the * execution service, if any. This should only include options specific to the * provided execution service. * @param options.options - The simulation options. * @returns The installed Snap object. * @template Service - The type of the execution service. */ async function installSnap(snapId, { executionService, executionServiceOptions, options: rawOptions = {}, } = {}) { const options = (0, options_1.getOptions)(rawOptions); // Fetch Snap files. const location = (0, node_1.detectSnapLocation)(snapId, { allowLocal: true, }); const snapFiles = await (0, node_1.fetchSnap)(snapId, location); // Create Redux store. const { store, runSaga } = (0, store_1.createStore)(options); const controllerMessenger = new base_controller_1.Messenger(); registerActions(controllerMessenger, runSaga, options, snapId); // Set up controllers and JSON-RPC stack. const restrictedHooks = getRestrictedHooks(options); const permittedHooks = getPermittedHooks(snapId, snapFiles, controllerMessenger, runSaga); const { subjectMetadataController, permissionController } = (0, controllers_1.getControllers)({ controllerMessenger, hooks: restrictedHooks, runSaga, options, }); const engine = (0, middleware_1.createJsonRpcEngine)({ store, restrictedHooks, permittedHooks, permissionMiddleware: permissionController.createPermissionMiddleware({ origin: snapId, }), }); // Create execution service. const ExecutionService = executionService ?? node_1.NodeThreadExecutionService; const service = new ExecutionService({ ...executionServiceOptions, messenger: controllerMessenger.getRestricted({ name: 'ExecutionService', allowedActions: [], allowedEvents: [], }), setupSnapProvider: (_snapId, rpcStream) => { const mux = (0, node_1.setupMultiplex)(rpcStream, 'snapStream'); const stream = mux.createStream('metamask-provider'); const providerStream = (0, json_rpc_middleware_stream_1.createEngineStream)({ engine }); // Error function is difficult to test, so we ignore it. /* istanbul ignore next 2 */ (0, readable_stream_1.pipeline)(stream, providerStream, stream, (error) => { if (error && !error.message?.match('Premature close')) { (0, snaps_utils_1.logError)(`Provider stream failure.`, error); } }); }, }); // Register the Snap. This sets up the Snap's permissions and subject // metadata. await (0, controllers_1.registerSnap)(snapId, snapFiles.manifest.result, { permissionController, subjectMetadataController, }); // Run the Snap code in the execution service. await service.executeSnap({ snapId, sourceCode: snapFiles.sourceCode.toString('utf8'), endowments: await (0, methods_1.getEndowments)(permissionController, snapId), }); const helpers = (0, helpers_1.getHelpers)({ snapId, store, controllerMessenger, runSaga, executionService: service, options, }); return { snapId, store, executionService: service, controllerMessenger, runSaga, ...helpers, }; } exports.installSnap = installSnap; /** * Get the hooks for the simulation. * * @param options - The simulation options. * @returns The hooks for the simulation. */ function getRestrictedHooks(options) { return { getMnemonic: (0, hooks_1.getGetMnemonicImplementation)(options.secretRecoveryPhrase), getMnemonicSeed: (0, get_mnemonic_seed_1.getGetMnemonicSeedImplementation)(options.secretRecoveryPhrase), getIsLocked: () => false, getClientCryptography: () => ({}), }; } exports.getRestrictedHooks = getRestrictedHooks; /** * Get the permitted hooks for the simulation. * * @param snapId - The ID of the Snap. * @param snapFiles - The fetched Snap files. * @param controllerMessenger - The controller messenger. * @param runSaga - The run saga function. * @returns The permitted hooks for the simulation. */ function getPermittedHooks(snapId, snapFiles, controllerMessenger, runSaga) { return { hasPermission: () => true, getUnlockPromise: (0, methods_1.asyncResolve)(), getIsLocked: () => false, getIsActive: () => true, getSnapFile: async (path, encoding) => await (0, files_1.getSnapFile)(snapFiles.auxiliaryFiles, path, encoding), createInterface: async (...args) => controllerMessenger.call('SnapInterfaceController:createInterface', snapId, ...args), updateInterface: async (...args) => controllerMessenger.call('SnapInterfaceController:updateInterface', snapId, ...args), getInterfaceState: (...args) => controllerMessenger.call('SnapInterfaceController:getInterface', snapId, ...args).state, getInterfaceContext: (...args) => controllerMessenger.call('SnapInterfaceController:getInterface', snapId, ...args).context, resolveInterface: async (...args) => controllerMessenger.call('SnapInterfaceController:resolveInterface', snapId, ...args), getEntropySources: (0, hooks_1.getGetEntropySourcesImplementation)(), getSnapState: (0, hooks_1.getPermittedGetSnapStateMethodImplementation)(runSaga), updateSnapState: (0, hooks_1.getPermittedUpdateSnapStateMethodImplementation)(runSaga), clearSnapState: (0, hooks_1.getPermittedClearSnapStateMethodImplementation)(runSaga), getSnap: (0, hooks_1.getGetSnapImplementation)(true), trackError: (0, hooks_1.getTrackErrorImplementation)(runSaga), trackEvent: (0, hooks_1.getTrackEventImplementation)(runSaga), startTrace: (0, hooks_1.getStartTraceImplementation)(runSaga), endTrace: (0, hooks_1.getEndTraceImplementation)(runSaga), }; } exports.getPermittedHooks = getPermittedHooks; /** * Register mocked action handlers. * * @param controllerMessenger - The controller messenger. * @param runSaga - The run saga function. * @param options - The simulation options. * @param snapId - The ID of the Snap. */ function registerActions(controllerMessenger, runSaga, options, snapId) { controllerMessenger.registerActionHandler('PhishingController:testOrigin', () => ({ result: false, type: phishing_controller_1.PhishingDetectorResultType.All })); controllerMessenger.registerActionHandler('AccountsController:getAccountByAddress', // @ts-expect-error - This is a partial account with only the necessary // data used by the interface controller. (address) => { const matchingAccount = options.accounts.find((account) => address === account.address); if (!matchingAccount) { return undefined; } return (0, account_1.addSnapMetadataToAccount)(matchingAccount, snapId); }); controllerMessenger.registerActionHandler('AccountsController:getSelectedMultichainAccount', // @ts-expect-error - This is a partial account with only the necessary // data used by the interface controller. () => { const selectedAccount = options.accounts.find((account) => account.selected); if (!selectedAccount) { return undefined; } return (0, account_1.addSnapMetadataToAccount)(selectedAccount, snapId); }); controllerMessenger.registerActionHandler('AccountsController:listMultichainAccounts', () => // @ts-expect-error - These are partial accounts with only the necessary // data used by the interface controller. options.accounts.map((account) => (0, account_1.addSnapMetadataToAccount)(account, snapId))); controllerMessenger.registerActionHandler('MultichainAssetsController:getState', () => ({ // @ts-expect-error - These are partial assets with only the // necessary data used by the interface controller. assetsMetadata: options.assets, accountsAssets: options.accounts.reduce((acc, account) => { acc[account.id] = account.assets ?? []; return acc; }, {}), })); controllerMessenger.registerActionHandler('ApprovalController:hasRequest', (opts) => { /** * Get the current interface from the store. * * @yields Selects the current interface from the store. * @returns The current interface. */ function* getCurrentInterfaceSaga() { const currentInterface = yield (0, effects_1.select)(store_1.getCurrentInterface); return currentInterface; } const currentInterface = runSaga(getCurrentInterfaceSaga).result(); return (currentInterface?.type === snaps_rpc_methods_1.DIALOG_APPROVAL_TYPES.default && currentInterface?.id === opts?.id); }); controllerMessenger.registerActionHandler('ApprovalController:acceptRequest', async (_id, value) => { await runSaga(interface_1.resolveWithSaga, value).toPromise(); return { value }; }); } exports.registerActions = registerActions; //# sourceMappingURL=simulation.cjs.map