Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add snap_getInterfaceContext #2902

Merged
merged 6 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 `getInterfaceState` hook', async () => {
FrederikBolding marked this conversation as resolved.
Show resolved Hide resolved
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
2 changes: 2 additions & 0 deletions packages/snaps-simulator/src/features/simulation/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ export function* initSaga({ payload }: PayloadAction<string>) {
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(),
}),
Expand Down
Loading