@metamask/snaps-rpc-methods
Version:
MetaMask Snaps JSON-RPC method implementations
218 lines • 8.12 kB
JavaScript
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