@metamask/snaps-simulation
Version:
A simulation framework for MetaMask Snaps, enabling headless testing of Snaps in a controlled environment
169 lines • 7.13 kB
JavaScript
;
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