From 8e43222221efb55f9b632dd03f38a3565e7fef3f Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 30 Jul 2025 12:55:45 +0200 Subject: [PATCH 1/6] Add `snap_trackError` and `snap_trackEvent` support to `snaps-jest` --- .../packages/preinstalled/snap.manifest.json | 2 +- .../packages/preinstalled/src/index.test.tsx | 37 ++ .../packages/preinstalled/src/index.tsx | 16 + packages/snaps-jest/src/global.ts | 33 ++ packages/snaps-jest/src/matchers.test.tsx | 316 ++++++++++++++++++ packages/snaps-jest/src/matchers.ts | 70 ++++ .../snaps-jest/src/test-utils/response.ts | 6 + .../src/permitted/trackError.ts | 18 +- packages/snaps-simulation/package.json | 1 + .../src/methods/hooks/get-snap.test.ts | 58 ++++ .../src/methods/hooks/get-snap.ts | 50 +++ .../src/methods/hooks/index.ts | 3 + .../src/methods/hooks/track-error.test.ts | 20 ++ .../src/methods/hooks/track-error.ts | 31 ++ .../src/methods/hooks/track-event.test.ts | 30 ++ .../src/methods/hooks/track-event.ts | 32 ++ .../snaps-simulation/src/request.test.tsx | 8 + packages/snaps-simulation/src/request.ts | 21 +- packages/snaps-simulation/src/simulation.ts | 46 ++- packages/snaps-simulation/src/store/index.ts | 1 + .../snaps-simulation/src/store/store.test.ts | 12 + packages/snaps-simulation/src/store/store.ts | 2 + .../src/store/trackables.test.ts | 71 ++++ .../snaps-simulation/src/store/trackables.ts | 75 +++++ .../snaps-simulation/src/structs.test.tsx | 144 ++++++++ packages/snaps-simulation/src/structs.ts | 11 + packages/snaps-simulation/src/types.ts | 16 +- packages/snaps-utils/src/errors.test.ts | 56 ++++ packages/snaps-utils/src/errors.ts | 15 +- yarn.lock | 1 + 30 files changed, 1173 insertions(+), 29 deletions(-) create mode 100644 packages/snaps-simulation/src/methods/hooks/get-snap.test.ts create mode 100644 packages/snaps-simulation/src/methods/hooks/get-snap.ts create mode 100644 packages/snaps-simulation/src/methods/hooks/track-error.test.ts create mode 100644 packages/snaps-simulation/src/methods/hooks/track-error.ts create mode 100644 packages/snaps-simulation/src/methods/hooks/track-event.test.ts create mode 100644 packages/snaps-simulation/src/methods/hooks/track-event.ts create mode 100644 packages/snaps-simulation/src/store/trackables.test.ts create mode 100644 packages/snaps-simulation/src/store/trackables.ts diff --git a/packages/examples/packages/preinstalled/snap.manifest.json b/packages/examples/packages/preinstalled/snap.manifest.json index ea4f745a6d..e68222d6f4 100644 --- a/packages/examples/packages/preinstalled/snap.manifest.json +++ b/packages/examples/packages/preinstalled/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "mTAP3og3khORsMc/TjpVQNieT8ORJC3h01SxkDOFqLE=", + "shasum": "1YmQwedWBW0Se/Kf6zB9BtIhuslYewgyGF4qomtLbPU=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/preinstalled/src/index.test.tsx b/packages/examples/packages/preinstalled/src/index.test.tsx index 12ddeb69fb..f6553c2565 100644 --- a/packages/examples/packages/preinstalled/src/index.test.tsx +++ b/packages/examples/packages/preinstalled/src/index.test.tsx @@ -87,6 +87,43 @@ describe('onRpcRequest', () => { }); }); }); + + describe('trackError', () => { + it('tracks an error', async () => { + const { request } = await installSnap(); + + const response = await request({ + method: 'trackError', + }); + + expect(response).toRespondWith(null); + expect(response).toTrackError( + expect.objectContaining({ + name: 'TestError', + message: 'This is a test error.', + }), + ); + }); + }); + + describe('trackEvent', () => { + it('tracks an event', async () => { + const { request } = await installSnap(); + + const response = await request({ + method: 'trackEvent', + }); + + expect(response).toRespondWith(null); + expect(response).toTrackEvent({ + event: 'Test Event', + properties: { + // eslint-disable-next-line @typescript-eslint/naming-convention + test_property: 'test value', + }, + }); + }); + }); }); describe('onSettingsPage', () => { diff --git a/packages/examples/packages/preinstalled/src/index.tsx b/packages/examples/packages/preinstalled/src/index.tsx index 09ae77058b..13cd69a2be 100644 --- a/packages/examples/packages/preinstalled/src/index.tsx +++ b/packages/examples/packages/preinstalled/src/index.tsx @@ -64,6 +64,22 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { }); } + case 'trackEvent': { + return await snap.request({ + method: 'snap_trackEvent', + params: { + event: { + event: 'Test Event', + properties: { + // Event properties must be snake_case. + // eslint-disable-next-line @typescript-eslint/naming-convention + test_property: 'test value', + }, + }, + }, + }); + } + case 'startTrace': return await snap.request({ method: 'snap_startTrace', diff --git a/packages/snaps-jest/src/global.ts b/packages/snaps-jest/src/global.ts index d3075187d7..cc67b49739 100644 --- a/packages/snaps-jest/src/global.ts +++ b/packages/snaps-jest/src/global.ts @@ -1,9 +1,11 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions, @typescript-eslint/naming-convention, @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars, @typescript-eslint/no-namespace */ import type { + TrackEventParams, EnumToUnion, NotificationType, ComponentOrElement, + TrackableError, } from '@metamask/snaps-sdk'; import type { JSXElement } from '@metamask/snaps-sdk/jsx'; @@ -74,6 +76,37 @@ interface SnapsMatchers { * expect(ui).toRender(panel([heading('Hello, world!')])); */ toRender(component: ComponentOrElement): void; + + /** + * Assert that the Snap tracked an error with the expected parameters. This + * is equivalent to calling `expect(response.errors).toContainEqual(error)`. + * + * @param error - The expected error parameters. + * @throws If the snap did not track an error with the expected parameters. + * @example + * const response = await request({ method: 'foo' }); + * expect(response).toTrackError({ + * name: 'Error', + * message: 'This is an error.', + * }); + */ + toTrackError(error?: unknown): void; + + /** + * Assert that the Snap tracked an event with the expected parameters. This + * is equivalent to calling `expect(response.events).toContainEqual(event)`. + * + * @param event - The expected event parameters. + * @throws If the snap did not track an event with the expected parameters. + * @example + * const response = await request({ method: 'foo' }); + * expect(response).toTrackEvent({ + * event: 'bar', + * properties: { baz: 'qux' }, + * sensitiveProperties: { quux: 'corge' }, + * }); + */ + toTrackEvent(event?: unknown): void; } // Extend the `expect` interface with the new matchers. This is used when diff --git a/packages/snaps-jest/src/matchers.test.tsx b/packages/snaps-jest/src/matchers.test.tsx index a1810b3350..36f877c215 100644 --- a/packages/snaps-jest/src/matchers.test.tsx +++ b/packages/snaps-jest/src/matchers.test.tsx @@ -13,6 +13,8 @@ import { toRespondWith, toRespondWithError, toSendNotification, + toTrackError, + toTrackEvent, } from './matchers'; import { getMockInterfaceResponse, @@ -25,6 +27,8 @@ expect.extend({ toRespondWithError, toSendNotification, toRender, + toTrackError, + toTrackEvent, }); describe('toRespondWith', () => { @@ -597,3 +601,315 @@ describe('toRender', () => { }); }); }); + +describe('toTrackError', () => { + it('passes when the error is correct', () => { + expect( + getMockResponse({ + errors: [ + { + name: 'foo', + message: 'bar', + stack: 'baz', + cause: null, + }, + ], + }), + ).toTrackError({ + name: 'foo', + message: 'bar', + stack: 'baz', + cause: null, + }); + }); + + it('passes when the partial error is correct', () => { + expect( + getMockResponse({ + errors: [ + { + name: 'foo', + message: 'bar', + stack: 'baz', + cause: null, + }, + ], + }), + ).toTrackError( + expect.objectContaining({ + name: 'foo', + message: 'bar', + }), + ); + }); + + it('passes when any error is tracked', () => { + expect( + getMockResponse({ + errors: [ + { + name: 'foo', + message: 'bar', + stack: 'baz', + cause: null, + }, + ], + }), + ).toTrackError(); + }); + + it('fails when the error is incorrect', () => { + expect(() => + expect( + getMockResponse({ + errors: [ + { + name: 'foo', + message: 'bar', + stack: 'baz', + cause: null, + }, + ], + }), + ).toTrackError({ + name: 'baz', + message: 'qux', + }), + ).toThrow('Received'); + }); + + it('fails when the error is missing', () => { + expect(() => + expect( + getMockResponse({ + errors: [], + }), + ).toTrackError({ + name: 'foo', + message: 'bar', + }), + ).toThrow('Expected to track error with data'); + }); + + describe('not', () => { + it('passes when the error is correct', () => { + expect( + getMockResponse({ + errors: [ + { + name: 'foo', + message: 'bar', + stack: 'baz', + cause: null, + }, + ], + }), + ).not.toTrackError({ + name: 'baz', + message: 'qux', + }); + }); + + it('passes when there are no errors', () => { + expect( + getMockResponse({ + errors: [], + }), + ).not.toTrackError(); + }); + + it('fails when the error is incorrect', () => { + expect(() => + expect( + getMockResponse({ + errors: [ + { + name: 'foo', + message: 'bar', + stack: 'baz', + cause: null, + }, + ], + }), + ).not.toTrackError({ + name: 'foo', + message: 'bar', + stack: 'baz', + cause: null, + }), + ).toThrow('Expected not to track error with data'); + }); + }); +}); + +describe('toTrackEvent', () => { + it('passes when the event is correct', () => { + expect( + getMockResponse({ + events: [ + { + event: 'foo', + properties: { bar: 'baz' }, + sensitiveProperties: { qux: 'quux' }, + }, + ], + }), + ).toTrackEvent({ + event: 'foo', + properties: { bar: 'baz' }, + sensitiveProperties: { qux: 'quux' }, + }); + }); + + it('passes when the partial event is correct', () => { + expect( + getMockResponse({ + events: [ + { + event: 'foo', + properties: { bar: 'baz' }, + }, + ], + }), + ).toTrackEvent({ + event: 'foo', + properties: { bar: 'baz' }, + }); + }); + + it('passes when any event is tracked', () => { + expect( + getMockResponse({ + events: [ + { + event: 'foo', + properties: { bar: 'baz' }, + sensitiveProperties: { qux: 'quux' }, + }, + ], + }), + ).toTrackEvent(); + }); + + it('fails when the event is incorrect', () => { + expect(() => + expect( + getMockResponse({ + events: [ + { + event: 'foo', + properties: { bar: 'baz' }, + sensitiveProperties: { qux: 'quux' }, + }, + ], + }), + ).toTrackEvent({ + event: 'bar', + properties: { bar: 'baz' }, + sensitiveProperties: { qux: 'quux' }, + }), + ).toThrow('Received'); + }); + + it('fails when the properties are incorrect', () => { + expect(() => + expect( + getMockResponse({ + events: [ + { + event: 'foo', + properties: { bar: 'baz' }, + sensitiveProperties: { qux: 'quux' }, + }, + ], + }), + ).toTrackEvent({ + event: 'foo', + properties: { bar: 'qux' }, + sensitiveProperties: { qux: 'quux' }, + }), + ).toThrow('Received'); + }); + + it('fails when the sensitive properties are incorrect', () => { + expect(() => + expect( + getMockResponse({ + events: [ + { + event: 'foo', + properties: { bar: 'baz' }, + sensitiveProperties: { qux: 'quux' }, + }, + ], + }), + ).toTrackEvent({ + event: 'foo', + properties: { bar: 'baz' }, + sensitiveProperties: { qux: 'corge' }, + }), + ).toThrow('Received'); + }); + + it('fails when the event is missing', () => { + expect(() => + expect( + getMockResponse({ + events: [], + }), + ).toTrackEvent({ + event: 'foo', + properties: { bar: 'baz' }, + sensitiveProperties: { qux: 'quux' }, + }), + ).toThrow('Expected to track event with data:'); + }); + + describe('not', () => { + it('passes when the event is correct', () => { + expect( + getMockResponse({ + events: [ + { + event: 'foo', + properties: { bar: 'baz' }, + sensitiveProperties: { qux: 'quux' }, + }, + ], + }), + ).not.toTrackEvent({ + event: 'bar', + properties: { bar: 'baz' }, + sensitiveProperties: { qux: 'quux' }, + }); + }); + + it('passes when there are no events', () => { + expect( + getMockResponse({ + events: [], + }), + ).not.toTrackEvent(); + }); + + it('fails when the event is incorrect', () => { + expect(() => + expect( + getMockResponse({ + events: [ + { + event: 'foo', + properties: { bar: 'baz' }, + sensitiveProperties: { qux: 'quux' }, + }, + ], + }), + ).not.toTrackEvent({ + event: 'foo', + properties: { bar: 'baz' }, + sensitiveProperties: { qux: 'quux' }, + }), + ).toThrow('Expected not to track event with data'); + }); + }); +}); diff --git a/packages/snaps-jest/src/matchers.ts b/packages/snaps-jest/src/matchers.ts index 4507d76cb1..6f96d5c936 100644 --- a/packages/snaps-jest/src/matchers.ts +++ b/packages/snaps-jest/src/matchers.ts @@ -8,6 +8,7 @@ import type { ComponentOrElement, Component, NotificationType, + TrackableError, } from '@metamask/snaps-sdk'; import type { JSXElement, SnapNode } from '@metamask/snaps-sdk/jsx'; import { isJSXElementUnsafe, JSXElementStruct } from '@metamask/snaps-sdk/jsx'; @@ -368,9 +369,78 @@ export const toRender: MatcherFunction<[expected: ComponentOrElement]> = return { message, pass }; }; +export const toTrackError: MatcherFunction< + [errorData?: Partial] +> = function (actual, errorData) { + assertActualIsSnapResponse(actual, 'toTrackError'); + + const errorValidator = (error: SnapResponse['errors'][number]) => { + if (!errorData) { + // If no error data is provided, we just check for the existence of an error. + return true; + } + + return this.equals(error, errorData); + }; + + const { errors } = actual; + const pass = errors.some(errorValidator); + + const message = pass + ? () => + `${this.utils.matcherHint('.not.toTrackError')}\n\n` + + `Expected not to track error with data: ${this.utils.printExpected( + errorData, + )}\n` + + `Received errors: ${this.utils.printReceived(errors)}` + : () => + `${this.utils.matcherHint('.toTrackError')}\n\n` + + `Expected to track error with data: ${this.utils.printExpected( + errorData, + )}\n` + + `Received errors: ${this.utils.printReceived(errors)}`; + + return { message, pass }; +}; + +export const toTrackEvent: MatcherFunction<[eventData?: Json | undefined]> = + function (actual, eventData) { + assertActualIsSnapResponse(actual, 'toTrackEvent'); + + const eventValidator = (event: SnapResponse['events'][number]) => { + if (!eventData) { + // If no event data is provided, we just check for the existence of an event. + return true; + } + + return this.equals(event, eventData); + }; + + const { events } = actual; + const pass = events.some(eventValidator); + + const message = pass + ? () => + `${this.utils.matcherHint('.not.toTrackEvent')}\n\n` + + `Expected not to track event with data: ${this.utils.printExpected( + eventData, + )}\n` + + `Received events: ${this.utils.printReceived(events)}` + : () => + `${this.utils.matcherHint('.toTrackEvent')}\n\n` + + `Expected to track event with data: ${this.utils.printExpected( + eventData, + )}\n` + + `Received events: ${this.utils.printReceived(events)}`; + + return { message, pass }; + }; + expect.extend({ toRespondWith, toRespondWithError, toSendNotification, toRender, + toTrackError, + toTrackEvent, }); diff --git a/packages/snaps-jest/src/test-utils/response.ts b/packages/snaps-jest/src/test-utils/response.ts index b316de1f40..0d393abaab 100644 --- a/packages/snaps-jest/src/test-utils/response.ts +++ b/packages/snaps-jest/src/test-utils/response.ts @@ -12,6 +12,8 @@ import type { * @param options.id - The ID to use. * @param options.response - The response to use. * @param options.notifications - The notifications to use. + * @param options.errors - The errors to use. + * @param options.events - The events to use. * @param options.getInterface - The `getInterface` function to use. * @returns The mock response. */ @@ -21,12 +23,16 @@ export function getMockResponse({ result: 'foo', }, notifications = [], + errors = [], + events = [], getInterface, }: Partial): SnapResponse { return { id, response, notifications, + errors, + events, ...(getInterface ? { getInterface } : {}), }; } diff --git a/packages/snaps-rpc-methods/src/permitted/trackError.ts b/packages/snaps-rpc-methods/src/permitted/trackError.ts index 7d599de512..ef894db91c 100644 --- a/packages/snaps-rpc-methods/src/permitted/trackError.ts +++ b/packages/snaps-rpc-methods/src/permitted/trackError.ts @@ -8,15 +8,8 @@ import type { TrackErrorResult, } from '@metamask/snaps-sdk'; import type { InferMatching, Snap } from '@metamask/snaps-utils'; -import type { Struct } from '@metamask/superstruct'; -import { - create, - lazy, - nullable, - object, - string, - StructError, -} from '@metamask/superstruct'; +import { TrackableErrorStruct } from '@metamask/snaps-utils'; +import { create, object, StructError } from '@metamask/superstruct'; import type { PendingJsonRpcResponse } from '@metamask/utils'; import type { MethodHooksObject } from '../utils'; @@ -44,13 +37,6 @@ export type TrackErrorMethodHooks = { getSnap: (snapId: string) => Snap | undefined; }; -const TrackableErrorStruct: Struct = object({ - name: string(), - message: string(), - stack: nullable(string()), - cause: nullable(lazy(() => TrackableErrorStruct)), -}); - const TrackErrorParametersStruct = object({ error: TrackableErrorStruct, }); diff --git a/packages/snaps-simulation/package.json b/packages/snaps-simulation/package.json index 03da417c74..cf77f374e2 100644 --- a/packages/snaps-simulation/package.json +++ b/packages/snaps-simulation/package.json @@ -71,6 +71,7 @@ "@metamask/utils": "^11.4.2", "@reduxjs/toolkit": "^1.9.5", "fast-deep-equal": "^3.1.3", + "immer": "^9.0.21", "mime": "^3.0.0", "readable-stream": "^3.6.2", "redux-saga": "^1.2.3" diff --git a/packages/snaps-simulation/src/methods/hooks/get-snap.test.ts b/packages/snaps-simulation/src/methods/hooks/get-snap.test.ts new file mode 100644 index 0000000000..14f097ba42 --- /dev/null +++ b/packages/snaps-simulation/src/methods/hooks/get-snap.test.ts @@ -0,0 +1,58 @@ +import { getGetSnapImplementation } from './get-snap'; + +describe('getGetSnapImplementation', () => { + it('returns a method that returns a Snap', () => { + const getSnap = getGetSnapImplementation(false); + const snap = getSnap(); + + expect(snap.preinstalled).toBe(false); + expect(snap).toMatchInlineSnapshot(` + { + "blocked": false, + "enabled": true, + "id": "npm:@metamask/snaps-simulation", + "initialPermissions": {}, + "manifest": { + "description": "A test Snap for simulation purposes.", + "initialPermissions": {}, + "manifestVersion": "0.1", + "proposedName": "Test Snap", + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/snaps.git", + }, + "source": { + "location": { + "npm": { + "filePath": "dist/index.js", + "packageName": "@metamask/snaps-simulation", + "registry": "https://registry.npmjs.org", + }, + }, + "shasum": "unused", + }, + "version": "0.1.0", + }, + "preinstalled": false, + "sourceCode": "", + "status": "running", + "version": "0.1.0", + "versionHistory": [], + } + `); + }); + + it('returns a method that returns a preinstalled Snap', () => { + const getSnap = getGetSnapImplementation(true); + const snap = getSnap(); + + expect(snap.preinstalled).toBe(true); + }); + + it('returns a method that returns a preinstalled Snap by default', () => { + const getSnap = getGetSnapImplementation(); + const snap = getSnap(); + + expect(snap.preinstalled).toBe(true); + }); +}); diff --git a/packages/snaps-simulation/src/methods/hooks/get-snap.ts b/packages/snaps-simulation/src/methods/hooks/get-snap.ts new file mode 100644 index 0000000000..2fd04d2f7c --- /dev/null +++ b/packages/snaps-simulation/src/methods/hooks/get-snap.ts @@ -0,0 +1,50 @@ +import type { SnapId } from '@metamask/snaps-sdk'; +import type { Snap } from '@metamask/snaps-utils'; +import { SnapStatus } from '@metamask/snaps-utils'; +import type { SemVerVersion } from '@metamask/utils'; + +/** + * Get a method that can be used to track an event. + * + * @param preinstalled - Whether the Snap is preinstalled. If `true`, the Snap + * will be returned as a preinstalled Snap. + * @returns A method that can be used to track an event. + */ +export function getGetSnapImplementation(preinstalled: boolean = true) { + return (): Snap => { + // This is a mock Snap for simulation purposes. Most of the fields are not + // actually used, but returned for type-safety sake. + return { + id: 'npm:@metamask/snaps-simulation' as SnapId, + version: '0.1.0' as SemVerVersion, + enabled: true, + blocked: false, + status: SnapStatus.Running, + versionHistory: [], + initialPermissions: {}, + sourceCode: '', + manifest: { + version: '0.1.0' as SemVerVersion, + proposedName: 'Test Snap', + description: 'A test Snap for simulation purposes.', + repository: { + type: 'git', + url: 'https://github.com/MetaMask/snaps.git', + }, + source: { + shasum: 'unused', + location: { + npm: { + filePath: 'dist/index.js', + packageName: '@metamask/snaps-simulation', + registry: 'https://registry.npmjs.org', + }, + }, + }, + initialPermissions: {}, + manifestVersion: '0.1', + }, + preinstalled, + }; + }; +} diff --git a/packages/snaps-simulation/src/methods/hooks/index.ts b/packages/snaps-simulation/src/methods/hooks/index.ts index 940e90e24e..ba2272e8f0 100644 --- a/packages/snaps-simulation/src/methods/hooks/index.ts +++ b/packages/snaps-simulation/src/methods/hooks/index.ts @@ -1,8 +1,11 @@ export * from './get-entropy-sources'; export * from './get-mnemonic'; export * from './get-preferences'; +export * from './get-snap'; export * from './interface'; export * from './notifications'; export * from './permitted'; export * from './request-user-approval'; export * from './state'; +export * from './track-error'; +export * from './track-event'; diff --git a/packages/snaps-simulation/src/methods/hooks/track-error.test.ts b/packages/snaps-simulation/src/methods/hooks/track-error.test.ts new file mode 100644 index 0000000000..5b5a81eb13 --- /dev/null +++ b/packages/snaps-simulation/src/methods/hooks/track-error.test.ts @@ -0,0 +1,20 @@ +import { getTrackErrorImplementation } from './track-error'; +import { getMockOptions } from '../../test-utils'; +import { createStore } from '@metamask/snaps-simulation'; + +describe('getTrackErrorImplementation', () => { + it('returns the implementation of the `trackError` hook', async () => { + const { store, runSaga } = createStore(getMockOptions()); + const fn = getTrackErrorImplementation(runSaga); + + expect(fn(new Error('foo'))).toBeNull(); + expect(store.getState().trackables.errors).toStrictEqual([ + { + name: 'Error', + message: 'foo', + stack: expect.any(String), + cause: null, + }, + ]); + }); +}); diff --git a/packages/snaps-simulation/src/methods/hooks/track-error.ts b/packages/snaps-simulation/src/methods/hooks/track-error.ts new file mode 100644 index 0000000000..460e7765a6 --- /dev/null +++ b/packages/snaps-simulation/src/methods/hooks/track-error.ts @@ -0,0 +1,31 @@ +import { getJsonError } from '@metamask/snaps-sdk'; +import type { SagaIterator } from 'redux-saga'; +import { put } from 'redux-saga/effects'; + +import { trackError } from '../../store/trackables'; +import type { RunSagaFunction } from '@metamask/snaps-simulation'; + +/** + * Track an error. + * + * @param error - The error to track. + * @returns `null`. + * @yields Adds the error to the store. + */ +function* trackErrorImplementation(error: Error): SagaIterator { + const serialisedError = getJsonError(error); + yield put(trackError(serialisedError)); + return null; +} + +/** + * Get a method that can be used to track an error. + * + * @param runSaga - A function to run a saga outside the usual Redux flow. + * @returns A method that can be used to track an error. + */ +export function getTrackErrorImplementation(runSaga: RunSagaFunction) { + return (...args: Parameters) => { + return runSaga(trackErrorImplementation, ...args).result(); + }; +} diff --git a/packages/snaps-simulation/src/methods/hooks/track-event.test.ts b/packages/snaps-simulation/src/methods/hooks/track-event.test.ts new file mode 100644 index 0000000000..7f32d6ccf6 --- /dev/null +++ b/packages/snaps-simulation/src/methods/hooks/track-event.test.ts @@ -0,0 +1,30 @@ +import { getTrackEventImplementation } from './track-event'; +import { getMockOptions } from '../../test-utils'; +import { createStore } from '@metamask/snaps-simulation'; + +describe('getTrackEventImplementation', () => { + it('returns the implementation of the `trackEvent` hook', async () => { + const { store, runSaga } = createStore(getMockOptions()); + const fn = getTrackEventImplementation(runSaga); + + expect( + fn({ + event: 'Test Event', + properties: { + // eslint-disable-next-line @typescript-eslint/naming-convention + test_property: 'test value', + }, + }), + ).toBeNull(); + + expect(store.getState().trackables.events).toStrictEqual([ + { + event: 'Test Event', + properties: { + // eslint-disable-next-line @typescript-eslint/naming-convention + test_property: 'test value', + }, + }, + ]); + }); +}); diff --git a/packages/snaps-simulation/src/methods/hooks/track-event.ts b/packages/snaps-simulation/src/methods/hooks/track-event.ts new file mode 100644 index 0000000000..42a7336242 --- /dev/null +++ b/packages/snaps-simulation/src/methods/hooks/track-event.ts @@ -0,0 +1,32 @@ +import type { TrackEventParams } from '@metamask/snaps-sdk'; +import type { SagaIterator } from 'redux-saga'; +import { put } from 'redux-saga/effects'; + +import { trackEvent } from '../../store/trackables'; +import type { RunSagaFunction } from '@metamask/snaps-simulation'; + +/** + * Track an event. + * + * @param event - The event to track. + * @returns `null`. + * @yields Adds the event to the store. + */ +function* trackEventImplementation( + event: TrackEventParams['event'], +): SagaIterator { + yield put(trackEvent(event)); + return null; +} + +/** + * Get a method that can be used to track an event. + * + * @param runSaga - A function to run a saga outside the usual Redux flow. + * @returns A method that can be used to track an event. + */ +export function getTrackEventImplementation(runSaga: RunSagaFunction) { + return (...args: Parameters) => { + return runSaga(trackEventImplementation, ...args).result(); + }; +} diff --git a/packages/snaps-simulation/src/request.test.tsx b/packages/snaps-simulation/src/request.test.tsx index 04c145f7c1..e7c0908801 100644 --- a/packages/snaps-simulation/src/request.test.tsx +++ b/packages/snaps-simulation/src/request.test.tsx @@ -59,6 +59,8 @@ describe('handleRequest', () => { result: 'Hello, world!', }, notifications: [], + errors: [], + events: [], }); await closeServer(); @@ -172,6 +174,8 @@ describe('handleRequest', () => { }, }, notifications: [], + errors: [], + events: [], getInterface: expect.any(Function), }); @@ -212,6 +216,8 @@ describe('handleRequest', () => { }), }, notifications: [], + errors: [], + events: [], getInterface: expect.any(Function), }); @@ -287,6 +293,8 @@ describe('handleRequest', () => { }), }, notifications: [], + errors: [], + events: [], getInterface: expect.any(Function), }); diff --git a/packages/snaps-simulation/src/request.ts b/packages/snaps-simulation/src/request.ts index 7a08e7fa76..3a13a4cfc2 100644 --- a/packages/snaps-simulation/src/request.ts +++ b/packages/snaps-simulation/src/request.ts @@ -19,8 +19,14 @@ import { nanoid } from '@reduxjs/toolkit'; import type { RootControllerMessenger } from './controllers'; import { getInterface, getInterfaceActions } from './interface'; import type { SimulationOptions } from './options'; -import { clearNotifications, getNotifications } from './store'; import type { RunSagaFunction, Store } from './store'; +import { + getErrors, + clearNotifications, + getNotifications, + getEvents, + clearTrackables, +} from './store'; import { SnapResponseStruct } from './structs'; import type { RequestOptions, @@ -87,9 +93,14 @@ export function handleRequest({ }, }) .then(async (result) => { - const notifications = getNotifications(store.getState()); + const state = store.getState(); + const notifications = getNotifications(state); + const errors = getErrors(state); + const events = getEvents(state); const interfaceId = notifications[0]?.content; + store.dispatch(clearNotifications()); + store.dispatch(clearTrackables()); try { const getInterfaceFn = await getInterfaceApi( @@ -106,6 +117,8 @@ export function handleRequest({ result: getSafeJson(result), }, notifications, + errors, + events, ...(getInterfaceFn ? { getInterface: getInterfaceFn } : {}), }; } catch (error) { @@ -115,7 +128,9 @@ export function handleRequest({ response: { error: unwrappedError.serialize(), }, + errors: [], notifications: [], + events: [], getInterface: getInterfaceError, }; } @@ -128,7 +143,9 @@ export function handleRequest({ response: { error: unwrappedError.serialize(), }, + errors: [], notifications: [], + events: [], getInterface: getInterfaceError, }; }) as unknown as SnapRequest; diff --git a/packages/snaps-simulation/src/simulation.ts b/packages/snaps-simulation/src/simulation.ts index fc57562181..d8e551c058 100644 --- a/packages/snaps-simulation/src/simulation.ts +++ b/packages/snaps-simulation/src/simulation.ts @@ -14,15 +14,16 @@ import { setupMultiplex, } from '@metamask/snaps-controllers/node'; import { DIALOG_APPROVAL_TYPES } from '@metamask/snaps-rpc-methods'; -import { - type AuxiliaryFileEncoding, - type Component, - type InterfaceState, - type InterfaceContext, - type SnapId, - type EntropySource, +import type { + TrackEventParams, + AuxiliaryFileEncoding, + Component, + InterfaceState, + InterfaceContext, + SnapId, + EntropySource, } from '@metamask/snaps-sdk'; -import type { FetchedSnapFiles } from '@metamask/snaps-utils'; +import type { FetchedSnapFiles, Snap } from '@metamask/snaps-utils'; import { logError } from '@metamask/snaps-utils'; import type { CaipAssetType, Json } from '@metamask/utils'; import type { Duplex } from 'readable-stream'; @@ -43,6 +44,9 @@ import { getPermittedUpdateSnapStateMethodImplementation, getGetEntropySourcesImplementation, getGetMnemonicImplementation, + getGetSnapImplementation, + getTrackEventImplementation, + getTrackErrorImplementation, } from './methods/hooks'; import { getGetMnemonicSeedImplementation } from './methods/hooks/get-mnemonic-seed'; import { createJsonRpcEngine } from './middleware'; @@ -261,6 +265,28 @@ export type PermittedMiddlewareHooks = { * @param value - The value to resolve the interface with. */ resolveInterface: (id: string, value: Json) => Promise; + + /** + * A hook that gets the Snap's metadata. + * + * @param snapId - The ID of the Snap to get. + * @returns The Snap's metadata. + */ + getSnap(snapId: string): Snap; + + /** + * A hook that tracks an error. + * + * @param error - The error object containing error details and properties. + */ + trackError(error: Error): void; + + /** + * A hook that tracks an event. + * + * @param event - The event object containing event details and properties. + */ + trackEvent(event: TrackEventParams['event']): void; }; /** @@ -466,6 +492,10 @@ export function getPermittedHooks( getSnapState: getPermittedGetSnapStateMethodImplementation(runSaga), updateSnapState: getPermittedUpdateSnapStateMethodImplementation(runSaga), clearSnapState: getPermittedClearSnapStateMethodImplementation(runSaga), + + getSnap: getGetSnapImplementation(true), + trackError: getTrackErrorImplementation(runSaga), + trackEvent: getTrackEventImplementation(runSaga), }; } diff --git a/packages/snaps-simulation/src/store/index.ts b/packages/snaps-simulation/src/store/index.ts index 4ec0adc667..88dff327d4 100644 --- a/packages/snaps-simulation/src/store/index.ts +++ b/packages/snaps-simulation/src/store/index.ts @@ -2,4 +2,5 @@ export * from './mocks'; export * from './notifications'; export * from './state'; export * from './store'; +export * from './trackables'; export * from './ui'; diff --git a/packages/snaps-simulation/src/store/store.test.ts b/packages/snaps-simulation/src/store/store.test.ts index 70e13ba0a2..37d6cafd77 100644 --- a/packages/snaps-simulation/src/store/store.test.ts +++ b/packages/snaps-simulation/src/store/store.test.ts @@ -18,6 +18,10 @@ describe('createStore', () => { "encrypted": null, "unencrypted": null, }, + "trackables": { + "errors": [], + "events": [], + }, "ui": { "current": null, }, @@ -47,6 +51,10 @@ describe('createStore', () => { "encrypted": "{"foo":"bar"}", "unencrypted": null, }, + "trackables": { + "errors": [], + "events": [], + }, "ui": { "current": null, }, @@ -76,6 +84,10 @@ describe('createStore', () => { "encrypted": null, "unencrypted": "{"foo":"bar"}", }, + "trackables": { + "errors": [], + "events": [], + }, "ui": { "current": null, }, diff --git a/packages/snaps-simulation/src/store/store.ts b/packages/snaps-simulation/src/store/store.ts index b2f6f49af4..9553c78074 100644 --- a/packages/snaps-simulation/src/store/store.ts +++ b/packages/snaps-simulation/src/store/store.ts @@ -4,6 +4,7 @@ import createSagaMiddleware from 'redux-saga'; import { mocksSlice } from './mocks'; import { notificationsSlice } from './notifications'; import { setState, stateSlice } from './state'; +import { trackablesSlice } from './trackables'; import { uiSlice } from './ui'; import type { SimulationOptions } from '../options'; @@ -22,6 +23,7 @@ export function createStore({ state, unencryptedState }: SimulationOptions) { mocks: mocksSlice.reducer, notifications: notificationsSlice.reducer, state: stateSlice.reducer, + trackables: trackablesSlice.reducer, ui: uiSlice.reducer, }, middleware: (getDefaultMiddleware) => diff --git a/packages/snaps-simulation/src/store/trackables.test.ts b/packages/snaps-simulation/src/store/trackables.test.ts new file mode 100644 index 0000000000..389aa95c24 --- /dev/null +++ b/packages/snaps-simulation/src/store/trackables.test.ts @@ -0,0 +1,71 @@ +import type { TrackedError, TrackedEvent } from './trackables'; +import { trackablesSlice, trackError } from './trackables'; + +describe('trackablesSlice', () => { + describe('trackError', () => { + it('adds an error to the state', () => { + const initialState = { + events: [], + errors: [], + }; + + const error: TrackedError = { + name: 'TestError', + message: 'Test error', + stack: 'Error stack trace', + cause: null, + }; + + const state = trackablesSlice.reducer(initialState, trackError(error)); + + expect(state.errors).toHaveLength(1); + expect(state.errors[0]).toStrictEqual(error); + }); + }); + + describe('trackEvent', () => { + it('adds an event to the state', () => { + const initialState = { + events: [], + errors: [], + }; + + const event: TrackedEvent = { + event: 'TestEvent', + properties: { key: 'value' }, + }; + + const state = trackablesSlice.reducer( + initialState, + trackablesSlice.actions.trackEvent(event), + ); + + expect(state.events).toHaveLength(1); + expect(state.events[0]).toStrictEqual(event); + }); + }); + + describe('clearTrackables', () => { + it('clears all events and errors from the state', () => { + const initialState = { + events: [{ event: 'TestEvent', properties: {} }], + errors: [ + { + name: 'TestError', + message: 'Test error', + stack: 'Error stack trace', + cause: null, + }, + ], + }; + + const state = trackablesSlice.reducer( + initialState, + trackablesSlice.actions.clearTrackables(), + ); + + expect(state.events).toHaveLength(0); + expect(state.errors).toHaveLength(0); + }); + }); +}); diff --git a/packages/snaps-simulation/src/store/trackables.ts b/packages/snaps-simulation/src/store/trackables.ts new file mode 100644 index 0000000000..a152f87084 --- /dev/null +++ b/packages/snaps-simulation/src/store/trackables.ts @@ -0,0 +1,75 @@ +import type { TrackErrorParams, TrackEventParams } from '@metamask/snaps-sdk'; +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSelector, createSlice } from '@reduxjs/toolkit'; +import { castDraft } from 'immer'; + +import type { ApplicationState } from './store'; + +export type TrackedEvent = TrackEventParams['event']; + +export type TrackedError = TrackErrorParams['error']; + +/** + * The trackables state. + */ +export type TrackablesState = { + /** + * An array of tracked events. + */ + events: TrackedEvent[]; + + /** + * An array of tracked errors. + */ + errors: TrackedError[]; +}; + +/** + * The initial events state. + */ +const INITIAL_STATE: TrackablesState = { + events: [], + errors: [], +}; + +export const trackablesSlice = createSlice({ + name: 'trackables', + initialState: INITIAL_STATE, + reducers: { + trackError: (state, action: PayloadAction) => { + state.errors.push(castDraft(action.payload)); + }, + trackEvent: (state, action: PayloadAction) => { + state.events.push(castDraft(action.payload)); + }, + clearTrackables: (state) => { + state.events = []; + state.errors = []; + }, + }, +}); + +export const { trackError, trackEvent, clearTrackables } = + trackablesSlice.actions; + +/** + * Get the errors from the state. + * + * @param state - The application state. + * @returns An array of errors. + */ +export const getErrors = createSelector( + (state: ApplicationState) => state.trackables, + ({ errors }) => errors, +); + +/** + * Get the events from the state. + * + * @param state - The application state. + * @returns An array of events. + */ +export const getEvents = createSelector( + (state: ApplicationState) => state.trackables, + ({ events }) => events, +); diff --git a/packages/snaps-simulation/src/structs.test.tsx b/packages/snaps-simulation/src/structs.test.tsx index d19c8ba61a..bceb3d5bd6 100644 --- a/packages/snaps-simulation/src/structs.test.tsx +++ b/packages/snaps-simulation/src/structs.test.tsx @@ -221,6 +221,24 @@ describe('SnapResponseWithInterfaceStruct', () => { message: 'Hello, world!', }, ], + errors: [ + { + name: 'Error', + message: 'An error occurred', + stack: + 'Error: An error occurred\n at Object. (test.js:1:1)', + cause: null, + }, + ], + events: [ + { + event: 'Test Event', + properties: { + // eslint-disable-next-line @typescript-eslint/naming-convention + test_property: 'test value', + }, + }, + ], getInterface: () => undefined, }, SnapResponseWithInterfaceStruct, @@ -238,6 +256,24 @@ describe('SnapResponseWithInterfaceStruct', () => { message: 'Hello, world!', }, ], + errors: [ + { + name: 'Error', + message: 'An error occurred', + stack: + 'Error: An error occurred\n at Object. (test.js:1:1)', + cause: null, + }, + ], + events: [ + { + event: 'Test Event', + properties: { + // eslint-disable-next-line @typescript-eslint/naming-convention + test_property: 'test value', + }, + }, + ], getInterface: expect.any(Function), }); }); @@ -263,6 +299,24 @@ describe('SnapResponseWithoutInterfaceStruct', () => { message: 'Hello, world!', }, ], + errors: [ + { + name: 'Error', + message: 'An error occurred', + stack: + 'Error: An error occurred\n at Object. (test.js:1:1)', + cause: null, + }, + ], + events: [ + { + event: 'Test Event', + properties: { + // eslint-disable-next-line @typescript-eslint/naming-convention + test_property: 'test value', + }, + }, + ], }, SnapResponseWithoutInterfaceStruct, ); @@ -279,6 +333,24 @@ describe('SnapResponseWithoutInterfaceStruct', () => { message: 'Hello, world!', }, ], + errors: [ + { + name: 'Error', + message: 'An error occurred', + stack: + 'Error: An error occurred\n at Object. (test.js:1:1)', + cause: null, + }, + ], + events: [ + { + event: 'Test Event', + properties: { + // eslint-disable-next-line @typescript-eslint/naming-convention + test_property: 'test value', + }, + }, + ], }); }); @@ -303,6 +375,24 @@ describe('SnapResponseStruct', () => { message: 'Hello, world!', }, ], + errors: [ + { + name: 'Error', + message: 'An error occurred', + stack: + 'Error: An error occurred\n at Object. (test.js:1:1)', + cause: null, + }, + ], + events: [ + { + event: 'Test Event', + properties: { + // eslint-disable-next-line @typescript-eslint/naming-convention + test_property: 'test value', + }, + }, + ], getInterface: () => undefined, }, SnapResponseStruct, @@ -320,6 +410,24 @@ describe('SnapResponseStruct', () => { message: 'Hello, world!', }, ], + errors: [ + { + name: 'Error', + message: 'An error occurred', + stack: + 'Error: An error occurred\n at Object. (test.js:1:1)', + cause: null, + }, + ], + events: [ + { + event: 'Test Event', + properties: { + // eslint-disable-next-line @typescript-eslint/naming-convention + test_property: 'test value', + }, + }, + ], getInterface: expect.any(Function), }); @@ -336,6 +444,24 @@ describe('SnapResponseStruct', () => { message: 'Hello, world!', }, ], + errors: [ + { + name: 'Error', + message: 'An error occurred', + stack: + 'Error: An error occurred\n at Object. (test.js:1:1)', + cause: null, + }, + ], + events: [ + { + event: 'Test Event', + properties: { + // eslint-disable-next-line @typescript-eslint/naming-convention + test_property: 'test value', + }, + }, + ], }, SnapResponseStruct, ); @@ -352,6 +478,24 @@ describe('SnapResponseStruct', () => { message: 'Hello, world!', }, ], + errors: [ + { + name: 'Error', + message: 'An error occurred', + stack: + 'Error: An error occurred\n at Object. (test.js:1:1)', + cause: null, + }, + ], + events: [ + { + event: 'Test Event', + properties: { + // eslint-disable-next-line @typescript-eslint/naming-convention + test_property: 'test value', + }, + }, + ], }); }); diff --git a/packages/snaps-simulation/src/structs.ts b/packages/snaps-simulation/src/structs.ts index 6e90495006..ae1ee78377 100644 --- a/packages/snaps-simulation/src/structs.ts +++ b/packages/snaps-simulation/src/structs.ts @@ -1,5 +1,6 @@ import { NotificationType, enumValue } from '@metamask/snaps-sdk'; import { JSXElementStruct } from '@metamask/snaps-sdk/jsx'; +import { TrackableErrorStruct } from '@metamask/snaps-utils'; import { array, assign, @@ -265,6 +266,16 @@ export const SnapResponseWithoutInterfaceStruct = object({ ), }), ), + + errors: array(TrackableErrorStruct), + + events: array( + object({ + event: string(), + properties: optional(record(string(), JsonStruct)), + sensitiveProperties: optional(record(string(), JsonStruct)), + }), + ), }); export const SnapResponseWithInterfaceStruct = assign( diff --git a/packages/snaps-simulation/src/types.ts b/packages/snaps-simulation/src/types.ts index 1ba9f4159c..f9f9cac1a8 100644 --- a/packages/snaps-simulation/src/types.ts +++ b/packages/snaps-simulation/src/types.ts @@ -1,4 +1,8 @@ -import type { NotificationType, EnumToUnion } from '@metamask/snaps-sdk'; +import type { + NotificationType, + EnumToUnion, + TrackableError, +} from '@metamask/snaps-sdk'; import type { JSXElement } from '@metamask/snaps-sdk/jsx'; import type { InferMatching } from '@metamask/snaps-utils'; import type { Infer } from '@metamask/superstruct'; @@ -573,6 +577,7 @@ export type SnapHandlerInterface = { export type SnapResponseWithInterface = { id: string; response: { result: Json } | { error: Json }; + notifications: { id: string; message: string; @@ -581,6 +586,15 @@ export type SnapResponseWithInterface = { content?: string | undefined; footerLink?: { text: string; href: string } | undefined; }[]; + + errors: TrackableError[]; + + events: { + event: string; + properties?: Record; + sensitiveProperties?: Record; + }[]; + getInterface(): SnapHandlerInterface; }; diff --git a/packages/snaps-utils/src/errors.test.ts b/packages/snaps-utils/src/errors.test.ts index aa062682c7..caac1f6723 100644 --- a/packages/snaps-utils/src/errors.test.ts +++ b/packages/snaps-utils/src/errors.test.ts @@ -4,6 +4,7 @@ import { SNAP_ERROR_CODE, SNAP_ERROR_MESSAGE, } from '@metamask/snaps-sdk'; +import { is } from '@metamask/superstruct'; import { isSerializedSnapError, @@ -11,6 +12,7 @@ import { isWrappedSnapError, SNAP_ERROR_WRAPPER_CODE, SNAP_ERROR_WRAPPER_MESSAGE, + TrackableErrorStruct, unwrapError, WrappedSnapError, } from './errors'; @@ -345,3 +347,57 @@ describe('unwrapError', () => { ); }); }); + +describe('TrackableError', () => { + it.each([ + { + name: 'TestError', + message: 'Test error', + stack: 'Error stack trace', + cause: null, + }, + { + name: 'TestError', + message: 'Test error', + stack: null, + cause: { + name: 'CauseError', + message: 'Cause error', + stack: 'Cause error stack trace', + cause: { + name: 'NestedCauseError', + message: 'Nested cause error', + stack: 'Nested cause error stack trace', + cause: null, + }, + }, + }, + { + name: 'TestError', + message: 'Test error', + stack: null, + cause: null, + }, + ])('validates a trackable error', (value) => { + expect(is(value, TrackableErrorStruct)).toBe(true); + }); + + it.each([ + true, + false, + 0, + 'TestError', + { name: 'TestError' }, + { message: 'Test error' }, + { stack: 'Error stack trace' }, + { cause: null }, + { + name: 'TestError', + message: 'Test error', + stack: 'Error stack trace', + cause: {}, + }, + ])('validates an invalid trackable error', (value) => { + expect(is(value, TrackableErrorStruct)).toBe(false); + }); +}); diff --git a/packages/snaps-utils/src/errors.ts b/packages/snaps-utils/src/errors.ts index 911dbb2551..0aa649772b 100644 --- a/packages/snaps-utils/src/errors.ts +++ b/packages/snaps-utils/src/errors.ts @@ -4,13 +4,19 @@ import { serializeCause, } from '@metamask/rpc-errors'; import type { DataWithOptionalCause } from '@metamask/rpc-errors'; -import type { SerializedSnapError, SnapError } from '@metamask/snaps-sdk'; +import type { + SerializedSnapError, + SnapError, + TrackableError, +} from '@metamask/snaps-sdk'; import { getErrorMessage, getErrorStack, SNAP_ERROR_CODE, SNAP_ERROR_MESSAGE, } from '@metamask/snaps-sdk'; +import type { Struct } from '@metamask/superstruct'; +import { lazy, nullable, object, string } from '@metamask/superstruct'; import type { Json, JsonRpcError } from '@metamask/utils'; import { isObject, isJsonRpcError } from '@metamask/utils'; @@ -246,3 +252,10 @@ export function unwrapError( false, ]; } + +export const TrackableErrorStruct: Struct = object({ + name: string(), + message: string(), + stack: nullable(string()), + cause: nullable(lazy(() => TrackableErrorStruct)), +}); diff --git a/yarn.lock b/yarn.lock index 01e3932b25..217b559ea8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4523,6 +4523,7 @@ __metadata: eslint: "npm:^9.11.0" express: "npm:^5.1.0" fast-deep-equal: "npm:^3.1.3" + immer: "npm:^9.0.21" jest: "npm:^29.0.2" jest-it-up: "npm:^2.0.0" jest-silent-reporter: "npm:^0.6.0" From ed9720d3cd5149a0b9facacad81969992883e663 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 30 Jul 2025 13:07:46 +0200 Subject: [PATCH 2/6] Fix lint and coverage --- packages/snaps-rpc-methods/jest.config.js | 2 +- .../snaps-simulation/src/methods/hooks/track-error.test.ts | 2 +- packages/snaps-simulation/src/methods/hooks/track-error.ts | 4 ++-- .../snaps-simulation/src/methods/hooks/track-event.test.ts | 2 +- packages/snaps-simulation/src/methods/hooks/track-event.ts | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/snaps-rpc-methods/jest.config.js b/packages/snaps-rpc-methods/jest.config.js index c589611a1b..729024cb74 100644 --- a/packages/snaps-rpc-methods/jest.config.js +++ b/packages/snaps-rpc-methods/jest.config.js @@ -11,7 +11,7 @@ module.exports = deepmerge(baseConfig, { coverageThreshold: { global: { branches: 95.37, - functions: 98.76, + functions: 98.75, lines: 98.92, statements: 98.62, }, diff --git a/packages/snaps-simulation/src/methods/hooks/track-error.test.ts b/packages/snaps-simulation/src/methods/hooks/track-error.test.ts index 5b5a81eb13..16e9e7c56e 100644 --- a/packages/snaps-simulation/src/methods/hooks/track-error.test.ts +++ b/packages/snaps-simulation/src/methods/hooks/track-error.test.ts @@ -1,6 +1,6 @@ import { getTrackErrorImplementation } from './track-error'; +import { createStore } from '../../store'; import { getMockOptions } from '../../test-utils'; -import { createStore } from '@metamask/snaps-simulation'; describe('getTrackErrorImplementation', () => { it('returns the implementation of the `trackError` hook', async () => { diff --git a/packages/snaps-simulation/src/methods/hooks/track-error.ts b/packages/snaps-simulation/src/methods/hooks/track-error.ts index 460e7765a6..afb6a11ca2 100644 --- a/packages/snaps-simulation/src/methods/hooks/track-error.ts +++ b/packages/snaps-simulation/src/methods/hooks/track-error.ts @@ -2,8 +2,8 @@ import { getJsonError } from '@metamask/snaps-sdk'; import type { SagaIterator } from 'redux-saga'; import { put } from 'redux-saga/effects'; -import { trackError } from '../../store/trackables'; -import type { RunSagaFunction } from '@metamask/snaps-simulation'; +import type { RunSagaFunction } from '../../store'; +import { trackError } from '../../store'; /** * Track an error. diff --git a/packages/snaps-simulation/src/methods/hooks/track-event.test.ts b/packages/snaps-simulation/src/methods/hooks/track-event.test.ts index 7f32d6ccf6..3bbb5ceee2 100644 --- a/packages/snaps-simulation/src/methods/hooks/track-event.test.ts +++ b/packages/snaps-simulation/src/methods/hooks/track-event.test.ts @@ -1,6 +1,6 @@ import { getTrackEventImplementation } from './track-event'; +import { createStore } from '../../store'; import { getMockOptions } from '../../test-utils'; -import { createStore } from '@metamask/snaps-simulation'; describe('getTrackEventImplementation', () => { it('returns the implementation of the `trackEvent` hook', async () => { diff --git a/packages/snaps-simulation/src/methods/hooks/track-event.ts b/packages/snaps-simulation/src/methods/hooks/track-event.ts index 42a7336242..27a8a9247a 100644 --- a/packages/snaps-simulation/src/methods/hooks/track-event.ts +++ b/packages/snaps-simulation/src/methods/hooks/track-event.ts @@ -2,8 +2,8 @@ import type { TrackEventParams } from '@metamask/snaps-sdk'; import type { SagaIterator } from 'redux-saga'; import { put } from 'redux-saga/effects'; -import { trackEvent } from '../../store/trackables'; -import type { RunSagaFunction } from '@metamask/snaps-simulation'; +import type { RunSagaFunction } from '../../store'; +import { trackEvent } from '../../store'; /** * Track an event. From b1faf2a85a3fe73d97351b46b67c211380bae66c Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 30 Jul 2025 13:20:29 +0200 Subject: [PATCH 3/6] Fix Immer version --- packages/snaps-controllers/package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/snaps-controllers/package.json b/packages/snaps-controllers/package.json index ff29443dd7..e1a72e0eeb 100644 --- a/packages/snaps-controllers/package.json +++ b/packages/snaps-controllers/package.json @@ -100,7 +100,7 @@ "cron-parser": "^4.5.0", "fast-deep-equal": "^3.1.3", "get-npm-tarball-url": "^2.0.3", - "immer": "^9.0.6", + "immer": "^9.0.21", "luxon": "^3.5.0", "nanoid": "^3.3.10", "readable-stream": "^3.6.2", diff --git a/yarn.lock b/yarn.lock index 217b559ea8..930332dafd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4246,7 +4246,7 @@ __metadata: eslint: "npm:^9.11.0" fast-deep-equal: "npm:^3.1.3" get-npm-tarball-url: "npm:^2.0.3" - immer: "npm:^9.0.6" + immer: "npm:^9.0.21" jest: "npm:^29.0.2" jest-fetch-mock: "npm:^3.0.3" jest-silent-reporter: "npm:^0.6.0" From e9d5faa277f946f2764d6730e0ed84721553b59d Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 30 Jul 2025 13:22:42 +0200 Subject: [PATCH 4/6] Update manifest --- packages/examples/packages/preinstalled/snap.manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/examples/packages/preinstalled/snap.manifest.json b/packages/examples/packages/preinstalled/snap.manifest.json index e68222d6f4..574602b886 100644 --- a/packages/examples/packages/preinstalled/snap.manifest.json +++ b/packages/examples/packages/preinstalled/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "1YmQwedWBW0Se/Kf6zB9BtIhuslYewgyGF4qomtLbPU=", + "shasum": "3ngRXZHVyqZB3sk6u9Hpj2V4SUjrhxb6D/aqesFSmeY=", "location": { "npm": { "filePath": "dist/bundle.js", From f871a57ccf94ad67673047ebec69ceb6ad2e85db Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 30 Jul 2025 15:10:58 +0200 Subject: [PATCH 5/6] Update `getSnap` hook --- packages/snaps-simulation/src/methods/hooks/get-snap.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/snaps-simulation/src/methods/hooks/get-snap.ts b/packages/snaps-simulation/src/methods/hooks/get-snap.ts index 2fd04d2f7c..af622295ca 100644 --- a/packages/snaps-simulation/src/methods/hooks/get-snap.ts +++ b/packages/snaps-simulation/src/methods/hooks/get-snap.ts @@ -4,14 +4,15 @@ import { SnapStatus } from '@metamask/snaps-utils'; import type { SemVerVersion } from '@metamask/utils'; /** - * Get a method that can be used to track an event. + * Get a method that gets a Snap by its ID. * * @param preinstalled - Whether the Snap is preinstalled. If `true`, the Snap * will be returned as a preinstalled Snap. - * @returns A method that can be used to track an event. + * @returns A method that gets a Snap by its ID. It will always return a mock + * Snap for simulation purposes. */ export function getGetSnapImplementation(preinstalled: boolean = true) { - return (): Snap => { + return (_snapId: string): Snap => { // This is a mock Snap for simulation purposes. Most of the fields are not // actually used, but returned for type-safety sake. return { From dba66a36606feaf6161499553b719053e104b9ac Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 30 Jul 2025 15:33:11 +0200 Subject: [PATCH 6/6] Move `errors` and `events` to `tracked` property to avoid confusion --- packages/snaps-jest/src/global.ts | 8 +- packages/snaps-jest/src/matchers.test.tsx | 250 ++++++++------- packages/snaps-jest/src/matchers.ts | 10 +- .../snaps-jest/src/test-utils/response.ts | 23 +- .../snaps-simulation/src/request.test.tsx | 24 +- packages/snaps-simulation/src/request.ts | 18 +- .../snaps-simulation/src/structs.test.tsx | 284 +++++++++--------- packages/snaps-simulation/src/structs.ts | 20 +- packages/snaps-simulation/src/types.ts | 17 +- 9 files changed, 368 insertions(+), 286 deletions(-) diff --git a/packages/snaps-jest/src/global.ts b/packages/snaps-jest/src/global.ts index cc67b49739..7e2756e230 100644 --- a/packages/snaps-jest/src/global.ts +++ b/packages/snaps-jest/src/global.ts @@ -1,11 +1,9 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions, @typescript-eslint/naming-convention, @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars, @typescript-eslint/no-namespace */ import type { - TrackEventParams, EnumToUnion, NotificationType, ComponentOrElement, - TrackableError, } from '@metamask/snaps-sdk'; import type { JSXElement } from '@metamask/snaps-sdk/jsx'; @@ -79,7 +77,8 @@ interface SnapsMatchers { /** * Assert that the Snap tracked an error with the expected parameters. This - * is equivalent to calling `expect(response.errors).toContainEqual(error)`. + * is equivalent to calling + * `expect(response.tracked.errors).toContainEqual(error)`. * * @param error - The expected error parameters. * @throws If the snap did not track an error with the expected parameters. @@ -94,7 +93,8 @@ interface SnapsMatchers { /** * Assert that the Snap tracked an event with the expected parameters. This - * is equivalent to calling `expect(response.events).toContainEqual(event)`. + * is equivalent to calling + * `expect(response.tracked.events).toContainEqual(event)`. * * @param event - The expected event parameters. * @throws If the snap did not track an event with the expected parameters. diff --git a/packages/snaps-jest/src/matchers.test.tsx b/packages/snaps-jest/src/matchers.test.tsx index 36f877c215..37228442e0 100644 --- a/packages/snaps-jest/src/matchers.test.tsx +++ b/packages/snaps-jest/src/matchers.test.tsx @@ -606,14 +606,16 @@ describe('toTrackError', () => { it('passes when the error is correct', () => { expect( getMockResponse({ - errors: [ - { - name: 'foo', - message: 'bar', - stack: 'baz', - cause: null, - }, - ], + tracked: { + errors: [ + { + name: 'foo', + message: 'bar', + stack: 'baz', + cause: null, + }, + ], + }, }), ).toTrackError({ name: 'foo', @@ -626,14 +628,16 @@ describe('toTrackError', () => { it('passes when the partial error is correct', () => { expect( getMockResponse({ - errors: [ - { - name: 'foo', - message: 'bar', - stack: 'baz', - cause: null, - }, - ], + tracked: { + errors: [ + { + name: 'foo', + message: 'bar', + stack: 'baz', + cause: null, + }, + ], + }, }), ).toTrackError( expect.objectContaining({ @@ -646,22 +650,7 @@ describe('toTrackError', () => { it('passes when any error is tracked', () => { expect( getMockResponse({ - errors: [ - { - name: 'foo', - message: 'bar', - stack: 'baz', - cause: null, - }, - ], - }), - ).toTrackError(); - }); - - it('fails when the error is incorrect', () => { - expect(() => - expect( - getMockResponse({ + tracked: { errors: [ { name: 'foo', @@ -670,6 +659,25 @@ describe('toTrackError', () => { cause: null, }, ], + }, + }), + ).toTrackError(); + }); + + it('fails when the error is incorrect', () => { + expect(() => + expect( + getMockResponse({ + tracked: { + errors: [ + { + name: 'foo', + message: 'bar', + stack: 'baz', + cause: null, + }, + ], + }, }), ).toTrackError({ name: 'baz', @@ -682,7 +690,9 @@ describe('toTrackError', () => { expect(() => expect( getMockResponse({ - errors: [], + tracked: { + errors: [], + }, }), ).toTrackError({ name: 'foo', @@ -695,14 +705,16 @@ describe('toTrackError', () => { it('passes when the error is correct', () => { expect( getMockResponse({ - errors: [ - { - name: 'foo', - message: 'bar', - stack: 'baz', - cause: null, - }, - ], + tracked: { + errors: [ + { + name: 'foo', + message: 'bar', + stack: 'baz', + cause: null, + }, + ], + }, }), ).not.toTrackError({ name: 'baz', @@ -713,7 +725,9 @@ describe('toTrackError', () => { it('passes when there are no errors', () => { expect( getMockResponse({ - errors: [], + tracked: { + errors: [], + }, }), ).not.toTrackError(); }); @@ -722,14 +736,16 @@ describe('toTrackError', () => { expect(() => expect( getMockResponse({ - errors: [ - { - name: 'foo', - message: 'bar', - stack: 'baz', - cause: null, - }, - ], + tracked: { + errors: [ + { + name: 'foo', + message: 'bar', + stack: 'baz', + cause: null, + }, + ], + }, }), ).not.toTrackError({ name: 'foo', @@ -746,13 +762,15 @@ describe('toTrackEvent', () => { it('passes when the event is correct', () => { expect( getMockResponse({ - events: [ - { - event: 'foo', - properties: { bar: 'baz' }, - sensitiveProperties: { qux: 'quux' }, - }, - ], + tracked: { + events: [ + { + event: 'foo', + properties: { bar: 'baz' }, + sensitiveProperties: { qux: 'quux' }, + }, + ], + }, }), ).toTrackEvent({ event: 'foo', @@ -764,12 +782,14 @@ describe('toTrackEvent', () => { it('passes when the partial event is correct', () => { expect( getMockResponse({ - events: [ - { - event: 'foo', - properties: { bar: 'baz' }, - }, - ], + tracked: { + events: [ + { + event: 'foo', + properties: { bar: 'baz' }, + }, + ], + }, }), ).toTrackEvent({ event: 'foo', @@ -780,13 +800,15 @@ describe('toTrackEvent', () => { it('passes when any event is tracked', () => { expect( getMockResponse({ - events: [ - { - event: 'foo', - properties: { bar: 'baz' }, - sensitiveProperties: { qux: 'quux' }, - }, - ], + tracked: { + events: [ + { + event: 'foo', + properties: { bar: 'baz' }, + sensitiveProperties: { qux: 'quux' }, + }, + ], + }, }), ).toTrackEvent(); }); @@ -795,13 +817,15 @@ describe('toTrackEvent', () => { expect(() => expect( getMockResponse({ - events: [ - { - event: 'foo', - properties: { bar: 'baz' }, - sensitiveProperties: { qux: 'quux' }, - }, - ], + tracked: { + events: [ + { + event: 'foo', + properties: { bar: 'baz' }, + sensitiveProperties: { qux: 'quux' }, + }, + ], + }, }), ).toTrackEvent({ event: 'bar', @@ -815,13 +839,15 @@ describe('toTrackEvent', () => { expect(() => expect( getMockResponse({ - events: [ - { - event: 'foo', - properties: { bar: 'baz' }, - sensitiveProperties: { qux: 'quux' }, - }, - ], + tracked: { + events: [ + { + event: 'foo', + properties: { bar: 'baz' }, + sensitiveProperties: { qux: 'quux' }, + }, + ], + }, }), ).toTrackEvent({ event: 'foo', @@ -835,13 +861,15 @@ describe('toTrackEvent', () => { expect(() => expect( getMockResponse({ - events: [ - { - event: 'foo', - properties: { bar: 'baz' }, - sensitiveProperties: { qux: 'quux' }, - }, - ], + tracked: { + events: [ + { + event: 'foo', + properties: { bar: 'baz' }, + sensitiveProperties: { qux: 'quux' }, + }, + ], + }, }), ).toTrackEvent({ event: 'foo', @@ -855,7 +883,9 @@ describe('toTrackEvent', () => { expect(() => expect( getMockResponse({ - events: [], + tracked: { + events: [], + }, }), ).toTrackEvent({ event: 'foo', @@ -869,13 +899,15 @@ describe('toTrackEvent', () => { it('passes when the event is correct', () => { expect( getMockResponse({ - events: [ - { - event: 'foo', - properties: { bar: 'baz' }, - sensitiveProperties: { qux: 'quux' }, - }, - ], + tracked: { + events: [ + { + event: 'foo', + properties: { bar: 'baz' }, + sensitiveProperties: { qux: 'quux' }, + }, + ], + }, }), ).not.toTrackEvent({ event: 'bar', @@ -887,7 +919,9 @@ describe('toTrackEvent', () => { it('passes when there are no events', () => { expect( getMockResponse({ - events: [], + tracked: { + events: [], + }, }), ).not.toTrackEvent(); }); @@ -896,13 +930,15 @@ describe('toTrackEvent', () => { expect(() => expect( getMockResponse({ - events: [ - { - event: 'foo', - properties: { bar: 'baz' }, - sensitiveProperties: { qux: 'quux' }, - }, - ], + tracked: { + events: [ + { + event: 'foo', + properties: { bar: 'baz' }, + sensitiveProperties: { qux: 'quux' }, + }, + ], + }, }), ).not.toTrackEvent({ event: 'foo', diff --git a/packages/snaps-jest/src/matchers.ts b/packages/snaps-jest/src/matchers.ts index 6f96d5c936..5f39f6986a 100644 --- a/packages/snaps-jest/src/matchers.ts +++ b/packages/snaps-jest/src/matchers.ts @@ -374,7 +374,7 @@ export const toTrackError: MatcherFunction< > = function (actual, errorData) { assertActualIsSnapResponse(actual, 'toTrackError'); - const errorValidator = (error: SnapResponse['errors'][number]) => { + const errorValidator = (error: SnapResponse['tracked']['errors'][number]) => { if (!errorData) { // If no error data is provided, we just check for the existence of an error. return true; @@ -383,7 +383,7 @@ export const toTrackError: MatcherFunction< return this.equals(error, errorData); }; - const { errors } = actual; + const { errors } = actual.tracked; const pass = errors.some(errorValidator); const message = pass @@ -407,7 +407,9 @@ export const toTrackEvent: MatcherFunction<[eventData?: Json | undefined]> = function (actual, eventData) { assertActualIsSnapResponse(actual, 'toTrackEvent'); - const eventValidator = (event: SnapResponse['events'][number]) => { + const eventValidator = ( + event: SnapResponse['tracked']['events'][number], + ) => { if (!eventData) { // If no event data is provided, we just check for the existence of an event. return true; @@ -416,7 +418,7 @@ export const toTrackEvent: MatcherFunction<[eventData?: Json | undefined]> = return this.equals(event, eventData); }; - const { events } = actual; + const { events } = actual.tracked; const pass = events.some(eventValidator); const message = pass diff --git a/packages/snaps-jest/src/test-utils/response.ts b/packages/snaps-jest/src/test-utils/response.ts index 0d393abaab..77dcb15db1 100644 --- a/packages/snaps-jest/src/test-utils/response.ts +++ b/packages/snaps-jest/src/test-utils/response.ts @@ -12,8 +12,7 @@ import type { * @param options.id - The ID to use. * @param options.response - The response to use. * @param options.notifications - The notifications to use. - * @param options.errors - The errors to use. - * @param options.events - The events to use. + * @param options.tracked - The tracked errors and events to use. * @param options.getInterface - The `getInterface` function to use. * @returns The mock response. */ @@ -23,16 +22,26 @@ export function getMockResponse({ result: 'foo', }, notifications = [], - errors = [], - events = [], + tracked = { + errors: [], + events: [], + }, getInterface, -}: Partial): SnapResponse { +}: Omit, 'tracked'> & { + tracked?: { + errors?: SnapResponse['tracked']['errors']; + events?: SnapResponse['tracked']['events']; + }; +}): SnapResponse { return { id, response, notifications, - errors, - events, + tracked: { + errors: [], + events: [], + ...tracked, + }, ...(getInterface ? { getInterface } : {}), }; } diff --git a/packages/snaps-simulation/src/request.test.tsx b/packages/snaps-simulation/src/request.test.tsx index e7c0908801..966accb628 100644 --- a/packages/snaps-simulation/src/request.test.tsx +++ b/packages/snaps-simulation/src/request.test.tsx @@ -59,8 +59,10 @@ describe('handleRequest', () => { result: 'Hello, world!', }, notifications: [], - errors: [], - events: [], + tracked: { + errors: [], + events: [], + }, }); await closeServer(); @@ -174,8 +176,10 @@ describe('handleRequest', () => { }, }, notifications: [], - errors: [], - events: [], + tracked: { + errors: [], + events: [], + }, getInterface: expect.any(Function), }); @@ -216,8 +220,10 @@ describe('handleRequest', () => { }), }, notifications: [], - errors: [], - events: [], + tracked: { + errors: [], + events: [], + }, getInterface: expect.any(Function), }); @@ -293,8 +299,10 @@ describe('handleRequest', () => { }), }, notifications: [], - errors: [], - events: [], + tracked: { + errors: [], + events: [], + }, getInterface: expect.any(Function), }); diff --git a/packages/snaps-simulation/src/request.ts b/packages/snaps-simulation/src/request.ts index 3a13a4cfc2..3f3afb5c9b 100644 --- a/packages/snaps-simulation/src/request.ts +++ b/packages/snaps-simulation/src/request.ts @@ -117,8 +117,10 @@ export function handleRequest({ result: getSafeJson(result), }, notifications, - errors, - events, + tracked: { + errors, + events, + }, ...(getInterfaceFn ? { getInterface: getInterfaceFn } : {}), }; } catch (error) { @@ -128,9 +130,11 @@ export function handleRequest({ response: { error: unwrappedError.serialize(), }, - errors: [], notifications: [], - events: [], + tracked: { + errors: [], + events: [], + }, getInterface: getInterfaceError, }; } @@ -143,9 +147,11 @@ export function handleRequest({ response: { error: unwrappedError.serialize(), }, - errors: [], notifications: [], - events: [], + tracked: { + errors: [], + events: [], + }, getInterface: getInterfaceError, }; }) as unknown as SnapRequest; diff --git a/packages/snaps-simulation/src/structs.test.tsx b/packages/snaps-simulation/src/structs.test.tsx index bceb3d5bd6..13f2093de2 100644 --- a/packages/snaps-simulation/src/structs.test.tsx +++ b/packages/snaps-simulation/src/structs.test.tsx @@ -221,24 +221,26 @@ describe('SnapResponseWithInterfaceStruct', () => { message: 'Hello, world!', }, ], - errors: [ - { - name: 'Error', - message: 'An error occurred', - stack: - 'Error: An error occurred\n at Object. (test.js:1:1)', - cause: null, - }, - ], - events: [ - { - event: 'Test Event', - properties: { - // eslint-disable-next-line @typescript-eslint/naming-convention - test_property: 'test value', + tracked: { + errors: [ + { + name: 'Error', + message: 'An error occurred', + stack: + 'Error: An error occurred\n at Object. (test.js:1:1)', + cause: null, }, - }, - ], + ], + events: [ + { + event: 'Test Event', + properties: { + // eslint-disable-next-line @typescript-eslint/naming-convention + test_property: 'test value', + }, + }, + ], + }, getInterface: () => undefined, }, SnapResponseWithInterfaceStruct, @@ -256,24 +258,26 @@ describe('SnapResponseWithInterfaceStruct', () => { message: 'Hello, world!', }, ], - errors: [ - { - name: 'Error', - message: 'An error occurred', - stack: - 'Error: An error occurred\n at Object. (test.js:1:1)', - cause: null, - }, - ], - events: [ - { - event: 'Test Event', - properties: { - // eslint-disable-next-line @typescript-eslint/naming-convention - test_property: 'test value', + tracked: { + errors: [ + { + name: 'Error', + message: 'An error occurred', + stack: + 'Error: An error occurred\n at Object. (test.js:1:1)', + cause: null, }, - }, - ], + ], + events: [ + { + event: 'Test Event', + properties: { + // eslint-disable-next-line @typescript-eslint/naming-convention + test_property: 'test value', + }, + }, + ], + }, getInterface: expect.any(Function), }); }); @@ -299,6 +303,43 @@ describe('SnapResponseWithoutInterfaceStruct', () => { message: 'Hello, world!', }, ], + tracked: { + errors: [ + { + name: 'Error', + message: 'An error occurred', + stack: + 'Error: An error occurred\n at Object. (test.js:1:1)', + cause: null, + }, + ], + events: [ + { + event: 'Test Event', + properties: { + // eslint-disable-next-line @typescript-eslint/naming-convention + test_property: 'test value', + }, + }, + ], + }, + }, + SnapResponseWithoutInterfaceStruct, + ); + + expect(options).toStrictEqual({ + id: '1', + response: { + result: '0x1', + }, + notifications: [ + { + id: '1', + type: 'native', + message: 'Hello, world!', + }, + ], + tracked: { errors: [ { name: 'Error', @@ -318,39 +359,6 @@ describe('SnapResponseWithoutInterfaceStruct', () => { }, ], }, - SnapResponseWithoutInterfaceStruct, - ); - - expect(options).toStrictEqual({ - id: '1', - response: { - result: '0x1', - }, - notifications: [ - { - id: '1', - type: 'native', - message: 'Hello, world!', - }, - ], - errors: [ - { - name: 'Error', - message: 'An error occurred', - stack: - 'Error: An error occurred\n at Object. (test.js:1:1)', - cause: null, - }, - ], - events: [ - { - event: 'Test Event', - properties: { - // eslint-disable-next-line @typescript-eslint/naming-convention - test_property: 'test value', - }, - }, - ], }); }); @@ -375,24 +383,26 @@ describe('SnapResponseStruct', () => { message: 'Hello, world!', }, ], - errors: [ - { - name: 'Error', - message: 'An error occurred', - stack: - 'Error: An error occurred\n at Object. (test.js:1:1)', - cause: null, - }, - ], - events: [ - { - event: 'Test Event', - properties: { - // eslint-disable-next-line @typescript-eslint/naming-convention - test_property: 'test value', + tracked: { + errors: [ + { + name: 'Error', + message: 'An error occurred', + stack: + 'Error: An error occurred\n at Object. (test.js:1:1)', + cause: null, }, - }, - ], + ], + events: [ + { + event: 'Test Event', + properties: { + // eslint-disable-next-line @typescript-eslint/naming-convention + test_property: 'test value', + }, + }, + ], + }, getInterface: () => undefined, }, SnapResponseStruct, @@ -410,24 +420,26 @@ describe('SnapResponseStruct', () => { message: 'Hello, world!', }, ], - errors: [ - { - name: 'Error', - message: 'An error occurred', - stack: - 'Error: An error occurred\n at Object. (test.js:1:1)', - cause: null, - }, - ], - events: [ - { - event: 'Test Event', - properties: { - // eslint-disable-next-line @typescript-eslint/naming-convention - test_property: 'test value', + tracked: { + errors: [ + { + name: 'Error', + message: 'An error occurred', + stack: + 'Error: An error occurred\n at Object. (test.js:1:1)', + cause: null, }, - }, - ], + ], + events: [ + { + event: 'Test Event', + properties: { + // eslint-disable-next-line @typescript-eslint/naming-convention + test_property: 'test value', + }, + }, + ], + }, getInterface: expect.any(Function), }); @@ -444,6 +456,43 @@ describe('SnapResponseStruct', () => { message: 'Hello, world!', }, ], + tracked: { + errors: [ + { + name: 'Error', + message: 'An error occurred', + stack: + 'Error: An error occurred\n at Object. (test.js:1:1)', + cause: null, + }, + ], + events: [ + { + event: 'Test Event', + properties: { + // eslint-disable-next-line @typescript-eslint/naming-convention + test_property: 'test value', + }, + }, + ], + }, + }, + SnapResponseStruct, + ); + + expect(optionsWithoutInterface).toStrictEqual({ + id: '1', + response: { + result: '0x1', + }, + notifications: [ + { + id: '1', + type: 'native', + message: 'Hello, world!', + }, + ], + tracked: { errors: [ { name: 'Error', @@ -463,39 +512,6 @@ describe('SnapResponseStruct', () => { }, ], }, - SnapResponseStruct, - ); - - expect(optionsWithoutInterface).toStrictEqual({ - id: '1', - response: { - result: '0x1', - }, - notifications: [ - { - id: '1', - type: 'native', - message: 'Hello, world!', - }, - ], - errors: [ - { - name: 'Error', - message: 'An error occurred', - stack: - 'Error: An error occurred\n at Object. (test.js:1:1)', - cause: null, - }, - ], - events: [ - { - event: 'Test Event', - properties: { - // eslint-disable-next-line @typescript-eslint/naming-convention - test_property: 'test value', - }, - }, - ], }); }); diff --git a/packages/snaps-simulation/src/structs.ts b/packages/snaps-simulation/src/structs.ts index ae1ee78377..f9c4bc501f 100644 --- a/packages/snaps-simulation/src/structs.ts +++ b/packages/snaps-simulation/src/structs.ts @@ -267,15 +267,17 @@ export const SnapResponseWithoutInterfaceStruct = object({ }), ), - errors: array(TrackableErrorStruct), - - events: array( - object({ - event: string(), - properties: optional(record(string(), JsonStruct)), - sensitiveProperties: optional(record(string(), JsonStruct)), - }), - ), + tracked: object({ + errors: array(TrackableErrorStruct), + + events: array( + object({ + event: string(), + properties: optional(record(string(), JsonStruct)), + sensitiveProperties: optional(record(string(), JsonStruct)), + }), + ), + }), }); export const SnapResponseWithInterfaceStruct = assign( diff --git a/packages/snaps-simulation/src/types.ts b/packages/snaps-simulation/src/types.ts index f9f9cac1a8..25bf1bda9c 100644 --- a/packages/snaps-simulation/src/types.ts +++ b/packages/snaps-simulation/src/types.ts @@ -574,6 +574,15 @@ export type SnapHandlerInterface = { content: JSXElement; } & SnapInterfaceActions; +export type TrackedSnapResponseData = { + errors: TrackableError[]; + events: { + event: string; + properties?: Record; + sensitiveProperties?: Record; + }[]; +}; + export type SnapResponseWithInterface = { id: string; response: { result: Json } | { error: Json }; @@ -587,13 +596,7 @@ export type SnapResponseWithInterface = { footerLink?: { text: string; href: string } | undefined; }[]; - errors: TrackableError[]; - - events: { - event: string; - properties?: Record; - sensitiveProperties?: Record; - }[]; + tracked: TrackedSnapResponseData; getInterface(): SnapHandlerInterface; };