UNPKG

@metamask/snaps-rpc-methods

Version:
218 lines 8.12 kB
import { PermissionType, SubjectType } from "@metamask/permission-controller"; import { rpcErrors } from "@metamask/rpc-errors"; import { DialogType, enumValue, ComponentOrElementStruct, selectiveUnion } from "@metamask/snaps-sdk"; import { create, object, optional, size, string } from "@metamask/superstruct"; import { hasProperty, isObject, isPlainObject } from "@metamask/utils"; const methodName = 'snap_dialog'; export const DIALOG_APPROVAL_TYPES = { [DialogType.Alert]: `${methodName}:alert`, [DialogType.Confirmation]: `${methodName}:confirmation`, [DialogType.Prompt]: `${methodName}:prompt`, default: methodName, }; const PlaceholderStruct = optional(size(string(), 1, 40)); /** * The specification builder for the `snap_dialog` permission. `snap_dialog` * lets the Snap display one of the following dialogs to the user: * - An alert, for displaying information. * - A confirmation, for accepting or rejecting some action. * - A prompt, for inputting some information. * * @param options - The specification builder options. * @param options.allowedCaveats - The optional allowed caveats for the * permission. * @param options.methodHooks - The RPC method hooks needed by the method * implementation. * @returns The specification for the `snap_dialog` permission. */ const specificationBuilder = ({ allowedCaveats = null, methodHooks, }) => { return { permissionType: PermissionType.RestrictedMethod, targetName: methodName, allowedCaveats, methodImplementation: getDialogImplementation(methodHooks), subjectTypes: [SubjectType.Snap], }; }; const methodHooks = { requestUserApproval: true, createInterface: true, getInterface: true, }; export const dialogBuilder = Object.freeze({ targetName: methodName, specificationBuilder, methodHooks, }); const AlertParametersWithContentStruct = object({ type: enumValue(DialogType.Alert), content: ComponentOrElementStruct, }); const AlertParametersWithIdStruct = object({ type: enumValue(DialogType.Alert), id: string(), }); const AlertParametersStruct = selectiveUnion((value) => { if (isPlainObject(value) && hasProperty(value, 'id')) { return AlertParametersWithIdStruct; } return AlertParametersWithContentStruct; }); const ConfirmationParametersWithContentStruct = object({ type: enumValue(DialogType.Confirmation), content: ComponentOrElementStruct, }); const ConfirmationParametersWithIdStruct = object({ type: enumValue(DialogType.Confirmation), id: string(), }); const ConfirmationParametersStruct = selectiveUnion((value) => { if (isPlainObject(value) && hasProperty(value, 'id')) { return ConfirmationParametersWithIdStruct; } return ConfirmationParametersWithContentStruct; }); const PromptParametersWithContentStruct = object({ type: enumValue(DialogType.Prompt), content: ComponentOrElementStruct, placeholder: PlaceholderStruct, }); const PromptParametersWithIdStruct = object({ type: enumValue(DialogType.Prompt), id: string(), placeholder: PlaceholderStruct, }); const PromptParametersStruct = selectiveUnion((value) => { if (isPlainObject(value) && hasProperty(value, 'id')) { return PromptParametersWithIdStruct; } return PromptParametersWithContentStruct; }); const DefaultParametersWithContentStruct = object({ content: ComponentOrElementStruct, }); const DefaultParametersWithIdStruct = object({ id: string(), }); const DefaultParametersStruct = selectiveUnion((value) => { if (isPlainObject(value) && hasProperty(value, 'id')) { return DefaultParametersWithIdStruct; } return DefaultParametersWithContentStruct; }); const DialogParametersStruct = selectiveUnion((value) => { if (isPlainObject(value) && hasProperty(value, 'type')) { switch (value.type) { // We cannot use typedUnion here unfortunately. case DialogType.Alert: return AlertParametersStruct; case DialogType.Confirmation: return ConfirmationParametersStruct; case DialogType.Prompt: return PromptParametersStruct; default: throw new Error(`The "type" property must be one of: ${Object.values(DialogType).join(', ')}.`); } } return DefaultParametersStruct; }); /** * Builds the method implementation for `snap_dialog`. * * @param hooks - The RPC method hooks. * @param hooks.requestUserApproval - A function that creates a new Approval in the ApprovalController. * This function should return a Promise that resolves with the appropriate value when the user has approved or rejected the request. * @param hooks.createInterface - A function that creates the interface in SnapInterfaceController. * @param hooks.getInterface - A function that gets an interface from SnapInterfaceController. * @returns The method implementation which return value depends on the dialog * type, valid return types are: string, boolean, null. */ export function getDialogImplementation({ requestUserApproval, createInterface, getInterface, }) { return async function dialogImplementation(args) { const { params, context: { origin }, } = args; if (!isObject(params)) { throw rpcErrors.invalidParams({ message: 'Invalid params: Expected params to be a single object.', }); } const validatedParams = getValidatedParams(params); const placeholder = isPromptDialog(validatedParams) ? validatedParams.placeholder : undefined; const validatedType = hasProperty(validatedParams, 'type') ? validatedParams.type : 'default'; const approvalType = DIALOG_APPROVAL_TYPES[validatedType]; if (hasProperty(validatedParams, 'content')) { const id = await createInterface(origin, validatedParams.content); return requestUserApproval({ id: approvalType === DIALOG_APPROVAL_TYPES.default ? id : undefined, origin, type: approvalType, requestData: { id, placeholder }, }); } validateInterface(origin, validatedParams.id, getInterface); return requestUserApproval({ id: approvalType === DIALOG_APPROVAL_TYPES.default ? validatedParams.id : undefined, origin, type: approvalType, requestData: { id: validatedParams.id, placeholder }, }); }; } /** * Validate that the interface ID is valid. * * @param origin - The origin of the request. * @param id - The interface ID. * @param getInterface - The function to get the interface. */ function validateInterface(origin, id, getInterface) { try { getInterface(origin, id); } catch (error) { throw rpcErrors.invalidParams({ message: `Invalid params: ${error.message}`, }); } } /** * Gets the dialog type from the dialog parameters. * * @param params - The dialog parameters. * @returns The dialog type. */ function getDialogType(params) { return hasProperty(params, 'type') ? params.type : undefined; } /** * Checks if the dialog parameters are for a prompt dialog. * * @param params - The dialog parameters. * @returns `true` if the dialog parameters are for a prompt dialog, `false` otherwise. */ function isPromptDialog(params) { return getDialogType(params) === DialogType.Prompt; } /** * Validates the confirm method `params` and returns them cast to the correct * type. Throws if validation fails. * * @param params - The unvalidated params object from the method request. * @returns The validated confirm method parameter object. */ function getValidatedParams(params) { try { return create(params, DialogParametersStruct); } catch (error) { throw rpcErrors.invalidParams({ message: `Invalid params: ${error.message}`, }); } } //# sourceMappingURL=dialog.mjs.map