Skip to content

Commit

Permalink
feat: Add snap_getInterfaceContext (#2902)
Browse files Browse the repository at this point in the history
Expose a new RPC method `snap_getInterfaceContext` that makes the
`context` easily accessible outside `onUserInput`. Also adds the
necessary wiring to make this RPC method work in the simulation
environment.

This feature will require the addition of a `getInterfaceContext` method
hook in the clients.

Progresses #2901
  • Loading branch information
FrederikBolding authored Nov 22, 2024
1 parent 126990b commit 45db6e7
Show file tree
Hide file tree
Showing 13 changed files with 294 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/packages/browserify/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/snaps.git"
},
"source": {
"shasum": "hYGGCiIVhwOlDnwIyfpkscAd5bc2kVAyzXMq3UC6ORQ=",
"shasum": "bzhrHkJoo2dRz2utZ10KRNL2X2mgRxkur3DrGXHbNOc=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
8 changes: 4 additions & 4 deletions packages/snaps-rpc-methods/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
});
105 changes: 105 additions & 0 deletions packages/snaps-rpc-methods/src/permitted/getInterfaceContext.test.ts
Original file line number Diff line number Diff line change
@@ -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<GetInterfaceContextParameters>,
response as PendingJsonRpcResponse<GetInterfaceContextResult>,
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<GetInterfaceContextParameters>,
response as PendingJsonRpcResponse<GetInterfaceContextResult>,
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',
});
});
});
});
100 changes: 100 additions & 0 deletions packages/snaps-rpc-methods/src/permitted/getInterfaceContext.ts
Original file line number Diff line number Diff line change
@@ -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<GetInterfaceContextMethodHooks> = {
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<GetInterfaceContextParameters>,
res: PendingJsonRpcResponse<GetInterfaceContextResult>,
_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();
}
}
2 changes: 2 additions & 0 deletions packages/snaps-rpc-methods/src/permitted/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down
15 changes: 15 additions & 0 deletions packages/snaps-sdk/src/types/methods/get-interface-context.ts
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions packages/snaps-sdk/src/types/methods/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
8 changes: 8 additions & 0 deletions packages/snaps-sdk/src/types/methods/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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];
Expand Down
40 changes: 40 additions & 0 deletions packages/snaps-simulation/src/simulation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
13 changes: 12 additions & 1 deletion packages/snaps-simulation/src/simulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
AuxiliaryFileEncoding,
Component,
InterfaceState,
InterfaceContext,
SnapId,
} from '@metamask/snaps-sdk';
import type { FetchedSnapFiles } from '@metamask/snaps-utils';
Expand Down Expand Up @@ -115,9 +116,13 @@ export type MiddlewareHooks = {
* @returns A boolean flag signaling whether the client is locked.
*/
getIsLocked: () => boolean;
createInterface: (content: Component) => Promise<string>;
createInterface: (
content: Component,
context?: InterfaceContext,
) => Promise<string>;
updateInterface: (id: string, content: Component) => Promise<void>;
getInterfaceState: (id: string) => InterfaceState;
getInterfaceContext: (id: string) => InterfaceContext | null;
resolveInterface: (id: string, value: Json) => Promise<void>;
};

Expand Down Expand Up @@ -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',
Expand Down
6 changes: 3 additions & 3 deletions packages/snaps-simulator/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
Loading

0 comments on commit 45db6e7

Please sign in to comment.