UNPKG

@metamask/snaps-simulation

Version:

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

169 lines 7.13 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getInterfaceApi = exports.getInterfaceFromResult = exports.handleRequest = void 0; const snaps_sdk_1 = require("@metamask/snaps-sdk"); const snaps_utils_1 = require("@metamask/snaps-utils"); const superstruct_1 = require("@metamask/superstruct"); const utils_1 = require("@metamask/utils"); const toolkit_1 = require("@reduxjs/toolkit"); const interface_1 = require("./interface.cjs"); const store_1 = require("./store/index.cjs"); const structs_1 = require("./structs.cjs"); /** * Send a JSON-RPC request to the Snap, and wrap the response in a * {@link SnapResponse} object. * * @param options - The request options. * @param options.snapId - The ID of the Snap to send the request to. * @param options.store - The Redux store. * @param options.executionService - The execution service to use to send the * request. * @param options.handler - The handler to use to send the request. * @param options.controllerMessenger - The controller messenger used to call actions. * @param options.simulationOptions - The simulation options. * @param options.runSaga - A function to run a saga outside the usual Redux * flow. * @param options.request - The request to send. * @param options.request.id - The ID of the request. If not provided, a random * ID will be generated. * @param options.request.origin - The origin of the request. Defaults to * `https://metamask.io`. * @returns The response, wrapped in a {@link SnapResponse} object. */ function handleRequest({ snapId, store, executionService, handler, controllerMessenger, simulationOptions, runSaga, request: { id = (0, toolkit_1.nanoid)(), origin = 'https://metamask.io', ...options }, }) { const getInterfaceError = () => { throw new Error('Unable to get the interface from the Snap: The request to the Snap failed.'); }; const promise = executionService .handleRpcRequest(snapId, { origin, handler, request: { jsonrpc: '2.0', id: 1, ...options, }, }) .then(async (result) => { const state = store.getState(); const notifications = (0, store_1.getNotifications)(state); const errors = (0, store_1.getErrors)(state); const events = (0, store_1.getEvents)(state); const traces = (0, store_1.getTraces)(state); const interfaceId = notifications[0]?.content; store.dispatch((0, store_1.clearNotifications)()); store.dispatch((0, store_1.clearTrackables)()); try { const getInterfaceFn = await getInterfaceApi(result, snapId, controllerMessenger, simulationOptions, interfaceId); return { id: String(id), response: { result: (0, utils_1.getSafeJson)(result), }, notifications, tracked: { errors, events, traces, }, ...(getInterfaceFn ? { getInterface: getInterfaceFn } : {}), }; } catch (error) { const [unwrappedError] = (0, snaps_utils_1.unwrapError)(error); return { id: String(id), response: { error: unwrappedError.serialize(), }, notifications: [], tracked: { errors: [], events: [], traces: [], }, getInterface: getInterfaceError, }; } }) .catch((error) => { const [unwrappedError] = (0, snaps_utils_1.unwrapError)(error); return { id: String(id), response: { error: unwrappedError.serialize(), }, notifications: [], tracked: { errors: [], events: [], traces: [], }, getInterface: getInterfaceError, }; }); promise.getInterface = async () => { const sagaPromise = runSaga(interface_1.getInterface, runSaga, snapId, controllerMessenger, simulationOptions).toPromise(); const result = await Promise.race([promise, sagaPromise]); // If the request promise has resolved to an error, we should throw // instead of waiting for an interface that likely will never be displayed if ((0, superstruct_1.is)(result, structs_1.SnapResponseStruct) && (0, utils_1.hasProperty)(result.response, 'error')) { throw new Error(`Unable to get the interface from the Snap: The returned interface may be invalid. The error message received was: ${result.response.error.message}`); } return await sagaPromise; }; return promise; } exports.handleRequest = handleRequest; /** * Get the interface ID from the result if it's available or create a new interface if the result contains static components. * * @param result - The handler result object. * @param snapId - The Snap ID. * @param controllerMessenger - The controller messenger. * @returns The interface ID or undefined if the result doesn't include content. */ async function getInterfaceFromResult(result, snapId, controllerMessenger) { if ((0, utils_1.isPlainObject)(result) && (0, utils_1.hasProperty)(result, 'id')) { return result.id; } if ((0, utils_1.isPlainObject)(result) && (0, utils_1.hasProperty)(result, 'content')) { (0, utils_1.assert)((0, superstruct_1.is)(result.content, snaps_sdk_1.ComponentOrElementStruct), 'The Snap returned an invalid interface.'); const id = await controllerMessenger.call('SnapInterfaceController:createInterface', snapId, result.content); return id; } return undefined; } exports.getInterfaceFromResult = getInterfaceFromResult; /** * Get the response content from the `SnapInterfaceController` and include the * interaction methods. * * @param result - The handler result object. * @param snapId - The Snap ID. * @param controllerMessenger - The controller messenger. * @param options - The simulation options. * @param contentId - The id of the interface if it exists outside of the result. * @returns The content components if any. */ async function getInterfaceApi(result, snapId, controllerMessenger, options, contentId) { const interfaceId = await getInterfaceFromResult(result, snapId, controllerMessenger); const id = interfaceId ?? contentId; if (id) { return () => { const { content } = controllerMessenger.call('SnapInterfaceController:getInterface', snapId, id); const actions = (0, interface_1.getInterfaceActions)(snapId, controllerMessenger, options, { id, content, }); return { content, ...actions, }; }; } return undefined; } exports.getInterfaceApi = getInterfaceApi; //# sourceMappingURL=request.cjs.map