@metamask/snaps-rpc-methods
Version:
MetaMask Snaps JSON-RPC method implementations
140 lines • 4.84 kB
JavaScript
import { providerErrors, rpcErrors } from "@metamask/rpc-errors";
import { boolean, create, object, optional, StructError } from "@metamask/superstruct";
import { hasProperty, isObject } from "@metamask/utils";
import { manageStateBuilder } from "../restricted/manageState.mjs";
import { FORBIDDEN_KEYS, StateKeyStruct } from "../utils.mjs";
const hookNames = {
getUnlockPromise: true,
};
/**
* Get the state of the Snap, or a specific value within the state. By default,
* the data is automatically encrypted using a Snap-specific key and
* automatically decrypted when retrieved. You can set `encrypted` to `false` to
* use unencrypted storage (available when the client is locked).
*
* @example
* ```json name="Manifest"
* {
* "initialPermissions": {
* "snap_manageState": {}
* }
* }
* ```
* ```ts name="Usage"
* const state = await snap.request({
* method: 'snap_getState',
* params: {
* key: 'some.nested.value', // Optional, defaults to entire state
* encrypted: true, // Optional, defaults to `true`
* },
* });
* ```
*/
export const getStateHandler = {
implementation: getStateImplementation,
hookNames,
actionNames: [
'PermissionController:hasPermission',
'SnapController:getSnapState',
],
};
const GetStateParametersStruct = object({
key: optional(StateKeyStruct),
encrypted: optional(boolean()),
});
/**
* The `snap_getState` method implementation.
*
* @param request - The JSON-RPC request object.
* @param response - The JSON-RPC response object.
* @param _next - The `json-rpc-engine` "next" callback. Not used by this
* function.
* @param end - The `json-rpc-engine` "end" callback.
* @param hooks - The RPC method hooks.
* @param hooks.getUnlockPromise - Wait for the extension to be unlocked.
* @param messenger - The messenger used to call controller actions.
* @returns Nothing.
*/
async function getStateImplementation(request,
// `GetStateResult` is an alias for `Json` (which is the default type argument
// for `PendingJsonRpcResponse`), but that may not be the case in the future.
// We use `GetStateResult` here to make it clear that this is the expected
// type of the result.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-arguments
response, _next, end, { getUnlockPromise }, messenger) {
const { params, origin } = request;
if (!messenger.call('PermissionController:hasPermission', origin, manageStateBuilder.targetName)) {
return end(providerErrors.unauthorized());
}
try {
const validatedParams = getValidatedParams(params);
const { key, encrypted = true } = validatedParams;
if (encrypted) {
await getUnlockPromise(true);
}
const state = await messenger.call('SnapController:getSnapState', origin, encrypted);
response.result = get(state, key);
}
catch (error) {
return end(error);
}
return end();
}
/**
* Validate the parameters of the `snap_getState` method.
*
* @param params - The parameters to validate.
* @returns The validated parameters.
*/
function getValidatedParams(params) {
try {
return create(params, GetStateParametersStruct);
}
catch (error) {
if (error instanceof StructError) {
throw rpcErrors.invalidParams({
message: `Invalid params: ${error.message}.`,
});
}
/* istanbul ignore next */
throw rpcErrors.internal();
}
}
/**
* Get the value of a key in an object. The key may contain Lodash-style path
* syntax, e.g., `a.b.c` (with the exception of array syntax). If the key does
* not exist, `null` is returned.
*
* This is a simplified version of Lodash's `get` function, but Lodash doesn't
* seem to be maintained anymore, so we're using our own implementation.
*
* @param value - The object to get the key from.
* @param key - The key to get.
* @returns The value of the key in the object, or `null` if the key does not
* exist.
*/
export function get(value, key) {
if (key === undefined) {
return value;
}
const keys = key.split('.');
let result = value;
// Intentionally using a classic for loop here for performance reasons.
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < keys.length; i++) {
const currentKey = keys[i];
if (FORBIDDEN_KEYS.includes(currentKey)) {
throw rpcErrors.invalidParams('Invalid params: Key contains forbidden characters.');
}
if (isObject(result)) {
if (!hasProperty(result, currentKey)) {
return null;
}
result = result[currentKey];
continue;
}
return null;
}
return result;
}
//# sourceMappingURL=getState.mjs.map