diff --git a/packages/examples/packages/browserify-plugin/snap.manifest.json b/packages/examples/packages/browserify-plugin/snap.manifest.json index 3ffc4ee3ab..b62beafa40 100644 --- a/packages/examples/packages/browserify-plugin/snap.manifest.json +++ b/packages/examples/packages/browserify-plugin/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "8LxymXn6+X9URWzkmurIZEyCypzF3OUm53FLjlNW0/I=", + "shasum": "IdAFrQlUYgQaMo/lbXgEJOMKTFbB9RYylXwPvUFT6As=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/browserify/snap.manifest.json b/packages/examples/packages/browserify/snap.manifest.json index 3fedc26619..9e281be454 100644 --- a/packages/examples/packages/browserify/snap.manifest.json +++ b/packages/examples/packages/browserify/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "hYGGCiIVhwOlDnwIyfpkscAd5bc2kVAyzXMq3UC6ORQ=", + "shasum": "bzhrHkJoo2dRz2utZ10KRNL2X2mgRxkur3DrGXHbNOc=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/snaps-rpc-methods/jest.config.js b/packages/snaps-rpc-methods/jest.config.js index 266c39bf23..c02c90e6d6 100644 --- a/packages/snaps-rpc-methods/jest.config.js +++ b/packages/snaps-rpc-methods/jest.config.js @@ -10,10 +10,10 @@ module.exports = deepmerge(baseConfig, { ], coverageThreshold: { global: { - branches: 92.85, - functions: 97.23, - lines: 97.8, - statements: 97.31, + branches: 92.88, + functions: 97.26, + lines: 97.84, + statements: 97.36, }, }, }); diff --git a/packages/snaps-rpc-methods/src/permitted/getInterfaceContext.test.ts b/packages/snaps-rpc-methods/src/permitted/getInterfaceContext.test.ts new file mode 100644 index 0000000000..11198c235c --- /dev/null +++ b/packages/snaps-rpc-methods/src/permitted/getInterfaceContext.test.ts @@ -0,0 +1,105 @@ +import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { type GetInterfaceContextResult } from '@metamask/snaps-sdk'; +import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; + +import type { GetInterfaceContextParameters } from './getInterfaceContext'; +import { getInterfaceContextHandler } from './getInterfaceContext'; + +describe('snap_getInterfaceContext', () => { + describe('getInterfaceContextHandler', () => { + it('has the expected shape', () => { + expect(getInterfaceContextHandler).toMatchObject({ + methodNames: ['snap_getInterfaceContext'], + implementation: expect.any(Function), + hookNames: { + getInterfaceContext: true, + }, + }); + }); + }); + + describe('implementation', () => { + it('returns the result from the `getInterfaceContext` hook', async () => { + const { implementation } = getInterfaceContextHandler; + + const getInterfaceContext = jest.fn().mockReturnValue({ foo: 'bar' }); + + const hooks = { + getInterfaceContext, + }; + + const engine = new JsonRpcEngine(); + + engine.push((request, response, next, end) => { + const result = implementation( + request as JsonRpcRequest, + response as PendingJsonRpcResponse, + next, + end, + hooks, + ); + + result?.catch(end); + }); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'snap_getInterfaceContext', + params: { + id: 'foo', + }, + }); + + expect(response).toStrictEqual({ + jsonrpc: '2.0', + id: 1, + result: { foo: 'bar' }, + }); + }); + + it('throws on invalid params', async () => { + const { implementation } = getInterfaceContextHandler; + + const getInterfaceContext = jest.fn().mockReturnValue({ foo: 'bar' }); + + const hooks = { + getInterfaceContext, + }; + + const engine = new JsonRpcEngine(); + + engine.push((request, response, next, end) => { + const result = implementation( + request as JsonRpcRequest, + response as PendingJsonRpcResponse, + next, + end, + hooks, + ); + + result?.catch(end); + }); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'snap_getInterfaceContext', + params: { + id: 42, + }, + }); + + expect(response).toStrictEqual({ + error: { + code: -32602, + message: + 'Invalid params: At path: id -- Expected a string, but received: 42.', + stack: expect.any(String), + }, + id: 1, + jsonrpc: '2.0', + }); + }); + }); +}); diff --git a/packages/snaps-rpc-methods/src/permitted/getInterfaceContext.ts b/packages/snaps-rpc-methods/src/permitted/getInterfaceContext.ts new file mode 100644 index 0000000000..7bec91d8cf --- /dev/null +++ b/packages/snaps-rpc-methods/src/permitted/getInterfaceContext.ts @@ -0,0 +1,100 @@ +import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { PermittedHandlerExport } from '@metamask/permission-controller'; +import { rpcErrors } from '@metamask/rpc-errors'; +import type { + GetInterfaceContextParams, + GetInterfaceContextResult, + InterfaceContext, + JsonRpcRequest, +} from '@metamask/snaps-sdk'; +import { type InferMatching } from '@metamask/snaps-utils'; +import { StructError, create, object, string } from '@metamask/superstruct'; +import type { PendingJsonRpcResponse } from '@metamask/utils'; + +import type { MethodHooksObject } from '../utils'; + +const hookNames: MethodHooksObject = { + getInterfaceContext: true, +}; + +export type GetInterfaceContextMethodHooks = { + /** + * @param id - The interface ID. + * @returns The interface context. + */ + getInterfaceContext: (id: string) => InterfaceContext | null; +}; + +export const getInterfaceContextHandler: PermittedHandlerExport< + GetInterfaceContextMethodHooks, + GetInterfaceContextParameters, + GetInterfaceContextResult +> = { + methodNames: ['snap_getInterfaceContext'], + implementation: getInterfaceContextImplementation, + hookNames, +}; + +const GetInterfaceContextParametersStruct = object({ + id: string(), +}); + +export type GetInterfaceContextParameters = InferMatching< + typeof GetInterfaceContextParametersStruct, + GetInterfaceContextParams +>; + +/** + * The `snap_getInterfaceContext` method implementation. + * + * @param req - The JSON-RPC request object. + * @param res - 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.getInterfaceContext - The function to get the interface context. + * @returns Noting. + */ +function getInterfaceContextImplementation( + req: JsonRpcRequest, + res: PendingJsonRpcResponse, + _next: unknown, + end: JsonRpcEngineEndCallback, + { getInterfaceContext }: GetInterfaceContextMethodHooks, +): void { + const { params } = req; + + try { + const validatedParams = getValidatedParams(params); + + const { id } = validatedParams; + + res.result = getInterfaceContext(id); + } catch (error) { + return end(error); + } + + return end(); +} + +/** + * Validate the getInterfaceContext 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 getInterfaceContext method parameter object. + */ +function getValidatedParams(params: unknown): GetInterfaceContextParameters { + try { + return create(params, GetInterfaceContextParametersStruct); + } catch (error) { + if (error instanceof StructError) { + throw rpcErrors.invalidParams({ + message: `Invalid params: ${error.message}.`, + }); + } + /* istanbul ignore next */ + throw rpcErrors.internal(); + } +} diff --git a/packages/snaps-rpc-methods/src/permitted/handlers.ts b/packages/snaps-rpc-methods/src/permitted/handlers.ts index 5bfacdacd4..83730b1a15 100644 --- a/packages/snaps-rpc-methods/src/permitted/handlers.ts +++ b/packages/snaps-rpc-methods/src/permitted/handlers.ts @@ -4,6 +4,7 @@ import { getAllSnapsHandler } from './getAllSnaps'; import { getClientStatusHandler } from './getClientStatus'; import { getCurrencyRateHandler } from './getCurrencyRate'; import { getFileHandler } from './getFile'; +import { getInterfaceContextHandler } from './getInterfaceContext'; import { getInterfaceStateHandler } from './getInterfaceState'; import { getSnapsHandler } from './getSnaps'; import { invokeKeyringHandler } from './invokeKeyring'; @@ -24,6 +25,7 @@ export const methodHandlers = { snap_createInterface: createInterfaceHandler, snap_updateInterface: updateInterfaceHandler, snap_getInterfaceState: getInterfaceStateHandler, + snap_getInterfaceContext: getInterfaceContextHandler, snap_resolveInterface: resolveInterfaceHandler, snap_getCurrencyRate: getCurrencyRateHandler, snap_experimentalProviderRequest: providerRequestHandler, diff --git a/packages/snaps-sdk/src/types/methods/get-interface-context.ts b/packages/snaps-sdk/src/types/methods/get-interface-context.ts new file mode 100644 index 0000000000..587f738fc8 --- /dev/null +++ b/packages/snaps-sdk/src/types/methods/get-interface-context.ts @@ -0,0 +1,15 @@ +import type { InterfaceContext } from '../interface'; + +/** + * The request parameters for the `snap_getInterfaceContext` method. + * + * @property id - The interface id. + */ +export type GetInterfaceContextParams = { + id: string; +}; + +/** + * The result returned by the `snap_getInterfaceContext` method, which is the context for a given interface. + */ +export type GetInterfaceContextResult = InterfaceContext | null; diff --git a/packages/snaps-sdk/src/types/methods/index.ts b/packages/snaps-sdk/src/types/methods/index.ts index 766910160c..02d604df85 100644 --- a/packages/snaps-sdk/src/types/methods/index.ts +++ b/packages/snaps-sdk/src/types/methods/index.ts @@ -6,6 +6,7 @@ export * from './get-bip44-entropy'; export * from './get-client-status'; export * from './get-entropy'; export * from './get-file'; +export * from './get-interface-context'; export * from './get-interface-state'; export * from './get-locale'; export * from './get-preferences'; diff --git a/packages/snaps-sdk/src/types/methods/methods.ts b/packages/snaps-sdk/src/types/methods/methods.ts index b4547caceb..cf671a6254 100644 --- a/packages/snaps-sdk/src/types/methods/methods.ts +++ b/packages/snaps-sdk/src/types/methods/methods.ts @@ -26,6 +26,10 @@ import type { } from './get-currency-rate'; import type { GetEntropyParams, GetEntropyResult } from './get-entropy'; import type { GetFileParams, GetFileResult } from './get-file'; +import type { + GetInterfaceContextParams, + GetInterfaceContextResult, +} from './get-interface-context'; import type { GetInterfaceStateParams, GetInterfaceStateResult, @@ -79,6 +83,10 @@ export type SnapMethods = { snap_createInterface: [CreateInterfaceParams, CreateInterfaceResult]; snap_updateInterface: [UpdateInterfaceParams, UpdateInterfaceResult]; snap_getInterfaceState: [GetInterfaceStateParams, GetInterfaceStateResult]; + snap_getInterfaceContext: [ + GetInterfaceContextParams, + GetInterfaceContextResult, + ]; snap_resolveInterface: [ResolveInterfaceParams, ResolveInterfaceResult]; wallet_getSnaps: [GetSnapsParams, GetSnapsResult]; wallet_invokeKeyring: [InvokeKeyringParams, InvokeKeyringResult]; diff --git a/packages/snaps-simulation/src/simulation.test.ts b/packages/snaps-simulation/src/simulation.test.ts index 7df4010d5f..479bc8d002 100644 --- a/packages/snaps-simulation/src/simulation.test.ts +++ b/packages/snaps-simulation/src/simulation.test.ts @@ -376,6 +376,46 @@ describe('getHooks', () => { await close(); }); + it('returns the `getInterfaceContext` hook', async () => { + // eslint-disable-next-line no-new + new SnapInterfaceController({ + messenger: + getRestrictedSnapInterfaceControllerMessenger(controllerMessenger), + }); + + jest.spyOn(controllerMessenger, 'call'); + + const { snapId, close } = await getMockServer({ + manifest: getSnapManifest(), + }); + + const location = detectSnapLocation(snapId, { + allowLocal: true, + }); + const snapFiles = await fetchSnap(snapId, location); + + const { createInterface, getInterfaceContext } = getHooks( + getMockOptions(), + snapFiles, + snapId, + controllerMessenger, + ); + + const id = await createInterface(text('foo'), { bar: 'baz' }); + + const result = getInterfaceContext(id); + + expect(controllerMessenger.call).toHaveBeenNthCalledWith( + 3, + 'SnapInterfaceController:getInterface', + snapId, + id, + ); + + expect(result).toStrictEqual({ bar: 'baz' }); + await close(); + }); + it('returns the `resolveInterface` hook', async () => { // eslint-disable-next-line no-new const snapInterfaceController = new SnapInterfaceController({ diff --git a/packages/snaps-simulation/src/simulation.ts b/packages/snaps-simulation/src/simulation.ts index 2559784d9d..29fd2405c0 100644 --- a/packages/snaps-simulation/src/simulation.ts +++ b/packages/snaps-simulation/src/simulation.ts @@ -18,6 +18,7 @@ import type { AuxiliaryFileEncoding, Component, InterfaceState, + InterfaceContext, SnapId, } from '@metamask/snaps-sdk'; import type { FetchedSnapFiles } from '@metamask/snaps-utils'; @@ -115,9 +116,13 @@ export type MiddlewareHooks = { * @returns A boolean flag signaling whether the client is locked. */ getIsLocked: () => boolean; - createInterface: (content: Component) => Promise; + createInterface: ( + content: Component, + context?: InterfaceContext, + ) => Promise; updateInterface: (id: string, content: Component) => Promise; getInterfaceState: (id: string) => InterfaceState; + getInterfaceContext: (id: string) => InterfaceContext | null; resolveInterface: (id: string, value: Json) => Promise; }; @@ -278,6 +283,12 @@ export function getHooks( snapId, ...args, ).state, + getInterfaceContext: (...args) => + controllerMessenger.call( + 'SnapInterfaceController:getInterface', + snapId, + ...args, + ).context, resolveInterface: async (...args) => controllerMessenger.call( 'SnapInterfaceController:resolveInterface', diff --git a/packages/snaps-simulator/jest.config.js b/packages/snaps-simulator/jest.config.js index d1ebf12317..f8048f5e64 100644 --- a/packages/snaps-simulator/jest.config.js +++ b/packages/snaps-simulator/jest.config.js @@ -8,9 +8,9 @@ module.exports = deepmerge(baseConfig, { coverageThreshold: { global: { branches: 54.33, - functions: 60.76, - lines: 80.54, - statements: 80.87, + functions: 60.59, + lines: 80.49, + statements: 80.83, }, }, setupFiles: ['./jest.setup.js'], diff --git a/packages/snaps-simulator/src/features/simulation/sagas.ts b/packages/snaps-simulator/src/features/simulation/sagas.ts index f3184ae0ef..f01c0d68da 100644 --- a/packages/snaps-simulator/src/features/simulation/sagas.ts +++ b/packages/snaps-simulator/src/features/simulation/sagas.ts @@ -213,6 +213,8 @@ export function* initSaga({ payload }: PayloadAction) { await runSaga(createInterface, payload, content).toPromise(), getInterfaceState: (id: string) => runSaga(getInterfaceState, payload, id).result(), + getInterfaceContext: (id: string) => + runSaga(getInterface, payload, id).result().context, updateInterface: async (id: string, content: Component) => await runSaga(updateInterface, payload, id, content).toPromise(), }),