@metamask/snaps-rpc-methods
Version:
MetaMask Snaps JSON-RPC method implementations
235 lines • 9.05 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getValidatedParams = exports.getImplementation = exports.notifyBuilder = exports.specificationBuilder = void 0;
const permission_controller_1 = require("@metamask/permission-controller");
const rpc_errors_1 = require("@metamask/rpc-errors");
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 methodName = 'snap_notify';
const NativeNotificationStruct = (0, superstruct_1.object)({
type: (0, snaps_sdk_1.enumValue)(snaps_sdk_1.NotificationType.Native),
message: (0, superstruct_1.string)(),
});
const InAppNotificationStruct = (0, superstruct_1.object)({
type: (0, snaps_sdk_1.enumValue)(snaps_sdk_1.NotificationType.InApp),
message: (0, superstruct_1.string)(),
});
const InAppNotificationWithDetailsStruct = (0, superstruct_1.object)({
type: (0, snaps_sdk_1.enumValue)(snaps_sdk_1.NotificationType.InApp),
message: (0, superstruct_1.string)(),
content: snaps_sdk_1.ComponentOrElementStruct,
title: (0, superstruct_1.string)(),
footerLink: (0, superstruct_1.optional)((0, superstruct_1.object)({
href: (0, superstruct_1.string)(),
text: (0, superstruct_1.string)(),
})),
});
const NotificationParametersStruct = (0, snaps_sdk_1.union)([
InAppNotificationStruct,
InAppNotificationWithDetailsStruct,
NativeNotificationStruct,
]);
/**
* The specification builder for the `snap_notify` permission.
* `snap_notify` allows snaps to send multiple types of notifications to its users.
*
* @param options - The specification builder options.
* @param options.allowedCaveats - The optional allowed caveats for the permission.
* @param options.messenger - The messenger.
* @param options.methodHooks - The RPC method hooks needed by the method implementation.
* @returns The specification for the `snap_notify` permission.
*/
const specificationBuilder = ({ allowedCaveats = null, methodHooks, messenger, }) => {
return {
permissionType: permission_controller_1.PermissionType.RestrictedMethod,
targetName: methodName,
allowedCaveats,
methodImplementation: getImplementation({ methodHooks, messenger }),
subjectTypes: [permission_controller_1.SubjectType.Snap],
};
};
exports.specificationBuilder = specificationBuilder;
const methodHooks = {
isOnPhishingList: true,
maybeUpdatePhishingList: true,
};
/**
* Display a
* [notification](https://docs.metamask.io/snaps/features/notifications/) in
* MetaMask or natively in the OS. Snaps can trigger a short (up to 80
* characters) notification message for actionable or time sensitive
* information. `inApp` notifications can also include an optional
* [expanded view](https://docs.metamask.io/snaps/features/notifications/#expanded-view).
* The expanded view has a title, content, and optional footer link shown when
* a user clicks on the notification.
*
* @example Basic in app notification
* ```json name="Manifest"
* {
* "initialPermissions": {
* "snap_notify": {}
* }
* }
* ```
* ```ts name="Usage"
* snap.request({
* method: 'snap_notify',
* params: {
* type: 'inApp',
* message: 'This is an in-app notification',
* },
* });
* ```
*
* @example Expandable in app notification
* ```json name="Manifest"
* {
* "initialPermissions": {
* "snap_notify": {}
* }
* }
* ```
* ```ts name="Usage"
* snap.request({
* method: 'snap_notify',
* params: {
* type: 'inApp',
* message: 'This is an in-app notification',
* title: 'Notification Title',
* content: (
* <Box>
* <Text>This is the expanded content of the notification.</Text>
* </Box>
* ),
* footerLink: {
* href: 'https://example.com',
* text: 'Click here for more info',
* },
* },
* });
* ```
*
* @example Native notification
* ```json name="Manifest"
* {
* "initialPermissions": {
* "snap_notify": {}
* }
* }
* ```
* ```ts name="Usage"
* snap.request({
* method: 'snap_notify',
* params: {
* type: 'native',
* message: 'This is a native notification',
* },
* });
* ```
*/
exports.notifyBuilder = Object.freeze({
targetName: methodName,
specificationBuilder: exports.specificationBuilder,
methodHooks,
actionNames: [
'RateLimitController:call',
'SnapController:getSnap',
'SnapInterfaceController:createInterface',
],
});
/**
* Builds the method implementation for `snap_notify`.
*
* @param options - The options.
* @param options.messenger - The messenger.
* @param options.methodHooks - The RPC method hooks.
* @param options.methodHooks.isOnPhishingList - A function that checks for links against the phishing list.
* @param options.methodHooks.maybeUpdatePhishingList - A function that updates the phishing list if needed.
* @returns The method implementation which returns `null` on success.
* @throws If the params are invalid.
*/
function getImplementation({ methodHooks: { isOnPhishingList, maybeUpdatePhishingList }, messenger, }) {
return async function implementation(args) {
const { params, context: { origin }, } = args;
await maybeUpdatePhishingList();
const validatedParams = getValidatedParams(params, isOnPhishingList, messenger);
if ((0, utils_1.hasProperty)(validatedParams, 'content')) {
const id = messenger.call('SnapInterfaceController:createInterface', origin, validatedParams.content, undefined, snaps_sdk_1.ContentType.Notification);
validatedParams.content = id;
}
switch (validatedParams.type) {
case snaps_sdk_1.NotificationType.Native:
return (await messenger.call('RateLimitController:call', origin, 'showNativeNotification', origin, validatedParams.message));
case snaps_sdk_1.NotificationType.InApp: {
const { content, message, title, footerLink } = validatedParams;
return (await messenger.call('RateLimitController:call', origin, 'showInAppNotification', origin, {
interfaceId: content,
message,
title,
footerLink,
}));
}
/* istanbul ignore next */
default:
return (0, utils_1.assertExhaustive)(validatedParams.type);
}
};
}
exports.getImplementation = getImplementation;
/**
* Validates the notify method `params` and returns them cast to the correct
* type. Throws if validation fails.
*
* @param params - The unvalidated params object from the method request.
* @param isOnPhishingList - The function that checks for links against the phishing list.
* @param messenger - The messenger.
* @returns The validated method parameter object.
* @throws If the params are invalid.
*/
function getValidatedParams(params, isOnPhishingList, messenger) {
if (!(0, utils_1.isObject)(params)) {
throw rpc_errors_1.rpcErrors.invalidParams({
message: 'Expected params to be a single object.',
});
}
const { type, message } = params;
if (!type ||
typeof type !== 'string' ||
!Object.values(snaps_sdk_1.NotificationType).includes(type)) {
throw rpc_errors_1.rpcErrors.invalidParams({
message: 'Must specify a valid notification "type".',
});
}
const isNotString = !message || typeof message !== 'string';
// Set to the max message length on a Mac notification for now.
if (type === snaps_sdk_1.NotificationType.Native &&
(isNotString || message.length >= 50)) {
throw rpc_errors_1.rpcErrors.invalidParams({
message: 'Must specify a non-empty string "message" less than 50 characters long.',
});
}
if (type === snaps_sdk_1.NotificationType.InApp &&
(isNotString || message.length >= 500)) {
throw rpc_errors_1.rpcErrors.invalidParams({
message: 'Must specify a non-empty string "message" less than 500 characters long.',
});
}
try {
const validatedParams = (0, snaps_utils_1.createUnion)(params, NotificationParametersStruct, 'type');
const getSnap = (snapId) => messenger.call('SnapController:getSnap', snapId);
(0, snaps_utils_1.validateTextLinks)(validatedParams.message, isOnPhishingList, getSnap);
if ((0, utils_1.hasProperty)(validatedParams, 'footerLink')) {
(0, snaps_utils_1.validateLink)(validatedParams.footerLink.href, isOnPhishingList, getSnap);
}
return validatedParams;
}
catch (error) {
throw rpc_errors_1.rpcErrors.invalidParams({
message: `Invalid params: ${(0, snaps_sdk_1.getErrorMessage)(error)}`,
});
}
}
exports.getValidatedParams = getValidatedParams;
//# sourceMappingURL=notify.cjs.map