From 3b060ece17a2a6320957db2c95afee769f5cd092 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 1 Oct 2025 15:25:01 +0200 Subject: [PATCH 1/5] perf: Reduce JSON validation during state updates --- .../src/permitted/setState.test.ts | 18 +++++++ .../src/permitted/setState.ts | 45 ++++++++++------ .../src/restricted/manageState.test.ts | 45 ++++++++-------- .../src/restricted/manageState.ts | 51 ++++++++++++------- packages/snaps-utils/src/json.ts | 7 +-- 5 files changed, 107 insertions(+), 59 deletions(-) diff --git a/packages/snaps-rpc-methods/src/permitted/setState.test.ts b/packages/snaps-rpc-methods/src/permitted/setState.test.ts index 5861bcb471..c993f54be1 100644 --- a/packages/snaps-rpc-methods/src/permitted/setState.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/setState.test.ts @@ -34,12 +34,14 @@ describe('snap_setState', () => { const updateSnapState = jest.fn().mockReturnValue(null); const getUnlockPromise = jest.fn().mockResolvedValue(undefined); const hasPermission = jest.fn().mockReturnValue(true); + const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); const hooks = { getSnapState, updateSnapState, getUnlockPromise, hasPermission, + getSnap, }; const engine = new JsonRpcEngine(); @@ -87,12 +89,14 @@ describe('snap_setState', () => { const updateSnapState = jest.fn().mockReturnValue(null); const getUnlockPromise = jest.fn().mockResolvedValue(undefined); const hasPermission = jest.fn().mockReturnValue(true); + const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); const hooks = { getSnapState, updateSnapState, getUnlockPromise, hasPermission, + getSnap, }; const engine = new JsonRpcEngine(); @@ -141,12 +145,14 @@ describe('snap_setState', () => { const updateSnapState = jest.fn().mockReturnValue(null); const getUnlockPromise = jest.fn().mockResolvedValue(undefined); const hasPermission = jest.fn().mockReturnValue(true); + const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); const hooks = { getSnapState, updateSnapState, getUnlockPromise, hasPermission, + getSnap, }; const engine = new JsonRpcEngine(); @@ -200,12 +206,14 @@ describe('snap_setState', () => { const updateSnapState = jest.fn().mockReturnValue(null); const getUnlockPromise = jest.fn().mockResolvedValue(undefined); const hasPermission = jest.fn().mockReturnValue(false); + const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); const hooks = { getSnapState, updateSnapState, getUnlockPromise, hasPermission, + getSnap, }; const engine = new JsonRpcEngine(); @@ -252,12 +260,14 @@ describe('snap_setState', () => { const updateSnapState = jest.fn().mockReturnValue(null); const getUnlockPromise = jest.fn().mockResolvedValue(undefined); const hasPermission = jest.fn().mockReturnValue(true); + const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); const hooks = { getSnapState, updateSnapState, getUnlockPromise, hasPermission, + getSnap, }; const engine = new JsonRpcEngine(); @@ -303,12 +313,14 @@ describe('snap_setState', () => { const updateSnapState = jest.fn().mockReturnValue(null); const getUnlockPromise = jest.fn().mockResolvedValue(undefined); const hasPermission = jest.fn().mockReturnValue(true); + const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); const hooks = { getSnapState, updateSnapState, getUnlockPromise, hasPermission, + getSnap, }; const engine = new JsonRpcEngine(); @@ -358,12 +370,14 @@ describe('snap_setState', () => { const updateSnapState = jest.fn().mockReturnValue(null); const getUnlockPromise = jest.fn().mockResolvedValue(undefined); const hasPermission = jest.fn().mockReturnValue(true); + const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); const hooks = { getSnapState, updateSnapState, getUnlockPromise, hasPermission, + getSnap, }; const engine = new JsonRpcEngine(); @@ -408,12 +422,14 @@ describe('snap_setState', () => { const updateSnapState = jest.fn().mockReturnValue(null); const getUnlockPromise = jest.fn().mockResolvedValue(undefined); const hasPermission = jest.fn().mockReturnValue(true); + const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); const hooks = { getSnapState, updateSnapState, getUnlockPromise, hasPermission, + getSnap, }; const engine = new JsonRpcEngine(); @@ -461,12 +477,14 @@ describe('snap_setState', () => { const updateSnapState = jest.fn().mockReturnValue(null); const getUnlockPromise = jest.fn().mockResolvedValue(undefined); const hasPermission = jest.fn().mockReturnValue(true); + const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); const hooks = { getSnapState, updateSnapState, getUnlockPromise, hasPermission, + getSnap, }; const engine = new JsonRpcEngine(); diff --git a/packages/snaps-rpc-methods/src/permitted/setState.ts b/packages/snaps-rpc-methods/src/permitted/setState.ts index dca7addc09..b03e2c11d8 100644 --- a/packages/snaps-rpc-methods/src/permitted/setState.ts +++ b/packages/snaps-rpc-methods/src/permitted/setState.ts @@ -3,7 +3,11 @@ import type { PermittedHandlerExport } from '@metamask/permission-controller'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import type { SetStateParams, SetStateResult } from '@metamask/snaps-sdk'; import type { JsonObject } from '@metamask/snaps-sdk/jsx'; -import { type InferMatching } from '@metamask/snaps-utils'; +import { + getJsonSizeUnsafe, + type InferMatching, + type Snap, +} from '@metamask/snaps-utils'; import { boolean, create, @@ -16,13 +20,7 @@ import type { Json, JsonRpcRequest, } from '@metamask/utils'; -import { - getJsonSize, - hasProperty, - isObject, - assert, - JsonStruct, -} from '@metamask/utils'; +import { hasProperty, isObject, assert, JsonStruct } from '@metamask/utils'; import { manageStateBuilder, @@ -36,6 +34,7 @@ const hookNames: MethodHooksObject = { getSnapState: true, getUnlockPromise: true, updateSnapState: true, + getSnap: true, }; /** @@ -85,6 +84,13 @@ export type SetStateHooks = { newState: Record, encrypted: boolean, ) => Promise; + + /** + * Get Snap metadata. + * + * @param snapId - The ID of a Snap. + */ + getSnap: (snapId: string) => Snap | undefined; }; const SetStateParametersStruct = objectStruct({ @@ -112,6 +118,7 @@ export type SetStateParameters = InferMatching< * @param hooks.getSnapState - Get the state of the requesting Snap. * @param hooks.getUnlockPromise - Wait for the extension to be unlocked. * @param hooks.updateSnapState - Update the state of the requesting Snap. + * @param hooks.getSnap - The hook function to get Snap metadata. * @returns Nothing. */ async function setStateImplementation( @@ -124,6 +131,7 @@ async function setStateImplementation( getSnapState, getUnlockPromise, updateSnapState, + getSnap, }: SetStateHooks, ): Promise { const { params } = request; @@ -150,13 +158,20 @@ async function setStateImplementation( const newState = await getNewState(key, value, encrypted, getSnapState); - const size = getJsonSize(newState); - if (size > STORAGE_SIZE_LIMIT) { - throw rpcErrors.invalidParams({ - message: `Invalid params: The new state must not exceed ${ - STORAGE_SIZE_LIMIT / 1_000_000 - } MB in size.`, - }); + const snap = getSnap( + (request as JsonRpcRequest & { origin: string }).origin, + ); + + if (!snap?.preinstalled) { + // We know that the state is valid JSON as per previous validation. + const size = getJsonSizeUnsafe(newState, true); + if (size > STORAGE_SIZE_LIMIT) { + throw rpcErrors.invalidParams({ + message: `Invalid params: The new state must not exceed ${ + STORAGE_SIZE_LIMIT / 1_000_000 + } MB in size.`, + }); + } } await updateSnapState(newState, encrypted); diff --git a/packages/snaps-rpc-methods/src/restricted/manageState.test.ts b/packages/snaps-rpc-methods/src/restricted/manageState.test.ts index c1a04b8cff..97452b90e2 100644 --- a/packages/snaps-rpc-methods/src/restricted/manageState.test.ts +++ b/packages/snaps-rpc-methods/src/restricted/manageState.test.ts @@ -38,8 +38,6 @@ describe('getEncryptionEntropy', () => { }); describe('snap_manageState', () => { - const MOCK_SMALLER_STORAGE_SIZE_LIMIT = 10; // In bytes - describe('specification', () => { it('builds specification', () => { const methodHooks = { @@ -47,6 +45,7 @@ describe('snap_manageState', () => { getSnapState: jest.fn(), updateSnapState: jest.fn(), getUnlockPromise: jest.fn(), + getSnap: jest.fn(), }; expect( @@ -75,12 +74,14 @@ describe('snap_manageState', () => { const clearSnapState = jest.fn().mockReturnValueOnce(true); const getSnapState = jest.fn().mockReturnValueOnce(mockSnapState); const updateSnapState = jest.fn().mockReturnValueOnce(true); + const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); const manageStateImplementation = getManageStateImplementation({ clearSnapState, getSnapState, updateSnapState, getUnlockPromise: jest.fn(), + getSnap, }); const result = await manageStateImplementation({ @@ -104,12 +105,14 @@ describe('snap_manageState', () => { const getSnapState = jest.fn().mockReturnValueOnce(mockSnapState); const updateSnapState = jest.fn().mockReturnValueOnce(true); const getUnlockPromise = jest.fn(); + const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); const manageStateImplementation = getManageStateImplementation({ clearSnapState, getSnapState, updateSnapState, getUnlockPromise, + getSnap, }); const result = await manageStateImplementation({ @@ -127,12 +130,14 @@ describe('snap_manageState', () => { const clearSnapState = jest.fn().mockReturnValueOnce(true); const getSnapState = jest.fn().mockReturnValueOnce(null); const updateSnapState = jest.fn().mockReturnValueOnce(true); + const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); const manageStateImplementation = getManageStateImplementation({ clearSnapState, getSnapState, updateSnapState, getUnlockPromise: jest.fn(), + getSnap, }); const result = await manageStateImplementation({ @@ -150,12 +155,14 @@ describe('snap_manageState', () => { const getSnapState = jest.fn().mockReturnValueOnce(true); const updateSnapState = jest.fn().mockReturnValueOnce(true); const getUnlockPromise = jest.fn(); + const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); const manageStateImplementation = getManageStateImplementation({ clearSnapState, getSnapState, updateSnapState, getUnlockPromise, + getSnap, }); await manageStateImplementation({ @@ -173,12 +180,14 @@ describe('snap_manageState', () => { const getSnapState = jest.fn().mockReturnValueOnce(true); const updateSnapState = jest.fn().mockReturnValueOnce(true); const getUnlockPromise = jest.fn(); + const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); const manageStateImplementation = getManageStateImplementation({ clearSnapState, getSnapState, updateSnapState, getUnlockPromise, + getSnap, }); await manageStateImplementation({ @@ -204,12 +213,14 @@ describe('snap_manageState', () => { const clearSnapState = jest.fn().mockReturnValueOnce(true); const getSnapState = jest.fn().mockReturnValueOnce(true); const updateSnapState = jest.fn().mockReturnValueOnce(true); + const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); const manageStateImplementation = getManageStateImplementation({ clearSnapState, getSnapState, updateSnapState, getUnlockPromise: jest.fn(), + getSnap, }); await manageStateImplementation({ @@ -241,12 +252,14 @@ describe('snap_manageState', () => { .mockReturnValueOnce(JSON.stringify(mockSnapState)); const updateSnapState = jest.fn().mockReturnValueOnce(true); const getUnlockPromise = jest.fn(); + const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); const manageStateImplementation = getManageStateImplementation({ clearSnapState, getSnapState, updateSnapState, getUnlockPromise, + getSnap, }); await manageStateImplementation({ @@ -277,12 +290,14 @@ describe('snap_manageState', () => { const clearSnapState = jest.fn().mockReturnValueOnce(true); const getSnapState = jest.fn().mockReturnValueOnce(true); const updateSnapState = jest.fn().mockReturnValueOnce(true); + const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); const manageStateImplementation = getManageStateImplementation({ clearSnapState, getSnapState, updateSnapState, getUnlockPromise: jest.fn(), + getSnap, }); expect(async () => @@ -301,12 +316,14 @@ describe('snap_manageState', () => { const clearSnapState = jest.fn().mockReturnValueOnce(true); const getSnapState = jest.fn().mockReturnValueOnce(true); const updateSnapState = jest.fn().mockReturnValueOnce(true); + const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); const manageStateImplementation = getManageStateImplementation({ clearSnapState, getSnapState, updateSnapState, getUnlockPromise: jest.fn(), + getSnap, }); const newState = (a: unknown) => { @@ -338,12 +355,14 @@ describe('snap_manageState', () => { const clearSnapState = jest.fn().mockReturnValueOnce(true); const getSnapState = jest.fn().mockReturnValueOnce(true); const updateSnapState = jest.fn().mockReturnValueOnce(true); + const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); const manageStateImplementation = getManageStateImplementation({ clearSnapState, getSnapState, updateSnapState, getUnlockPromise: jest.fn(), + getSnap, }); const newState = { @@ -471,27 +490,5 @@ describe('snap_manageState', () => { 'Invalid snap_manageState "newState" parameter: The new state must be JSON serializable.', ); }); - - it('throws an error if the new state object is exceeding the JSON size limit', () => { - const mockInvalidNewStateObject = { - something: { - something: { - whatever: 'whatever', - }, - }, - }; - - expect(() => - getValidatedParams( - { operation: 'update', newState: mockInvalidNewStateObject }, - 'snap_manageState', - MOCK_SMALLER_STORAGE_SIZE_LIMIT, - ), - ).toThrow( - `Invalid snap_manageState "newState" parameter: The new state must not exceed ${ - MOCK_SMALLER_STORAGE_SIZE_LIMIT / 1_000_000 - } MB in size.`, - ); - }); }); }); diff --git a/packages/snaps-rpc-methods/src/restricted/manageState.ts b/packages/snaps-rpc-methods/src/restricted/manageState.ts index 74e3f88cdc..24b64834b1 100644 --- a/packages/snaps-rpc-methods/src/restricted/manageState.ts +++ b/packages/snaps-rpc-methods/src/restricted/manageState.ts @@ -8,9 +8,13 @@ import { PermissionType, SubjectType } from '@metamask/permission-controller'; import { rpcErrors } from '@metamask/rpc-errors'; import type { ManageStateParams, ManageStateResult } from '@metamask/snaps-sdk'; import { ManageStateOperation } from '@metamask/snaps-sdk'; -import { STATE_ENCRYPTION_MAGIC_VALUE } from '@metamask/snaps-utils'; +import type { Snap } from '@metamask/snaps-utils'; +import { + getJsonSizeUnsafe, + STATE_ENCRYPTION_MAGIC_VALUE, +} from '@metamask/snaps-utils'; import type { Json, NonEmptyArray } from '@metamask/utils'; -import { isObject, getJsonSize } from '@metamask/utils'; +import { isObject, isValidJson } from '@metamask/utils'; import type { MethodHooksObject } from '../utils'; import { deriveEntropyFromSeed } from '../utils'; @@ -53,6 +57,13 @@ export type ManageStateMethodHooks = { newState: Record, encrypted: boolean, ) => Promise; + + /** + * Get Snap metadata. + * + * @param snapId - The ID of a Snap. + */ + getSnap: (snapId: string) => Snap | undefined; }; type ManageStateSpecificationBuilderOptions = { @@ -99,6 +110,7 @@ const methodHooks: MethodHooksObject = { clearSnapState: true, getSnapState: true, updateSnapState: true, + getSnap: true, }; export const manageStateBuilder = Object.freeze({ @@ -157,6 +169,7 @@ export async function getEncryptionEntropy({ * @param hooks.getUnlockPromise - A function that resolves once the MetaMask * extension is unlocked and prompts the user to unlock their MetaMask if it is * locked. + * @param hooks.getSnap - The hook function to get Snap metadata. * @returns The method implementation which either returns `null` for a * successful state update/deletion or returns the decrypted state. * @throws If the params are invalid. @@ -166,6 +179,7 @@ export function getManageStateImplementation({ clearSnapState, getSnapState, updateSnapState, + getSnap, }: ManageStateMethodHooks) { return async function manageState( options: RestrictedMethodOptions, @@ -177,6 +191,23 @@ export function getManageStateImplementation({ } = options; const validatedParams = getValidatedParams(params, method); + const snap = getSnap(origin); + + if ( + !snap?.preinstalled && + validatedParams.operation === ManageStateOperation.UpdateState + ) { + const size = getJsonSizeUnsafe(validatedParams.newState, true); + + if (size > STORAGE_SIZE_LIMIT) { + throw rpcErrors.invalidParams({ + message: `Invalid ${method} "newState" parameter: The new state must not exceed ${ + STORAGE_SIZE_LIMIT / 1_000_000 + } MB in size.`, + }); + } + } + // If the encrypted param is undefined or null we default to true. const shouldEncrypt = validatedParams.encrypted ?? true; @@ -219,13 +250,11 @@ export function getManageStateImplementation({ * * @param params - The unvalidated params object from the method request. * @param method - RPC method name used for debugging errors. - * @param storageSizeLimit - Maximum allowed size (in bytes) of a new state object. * @returns The validated method parameter object. */ export function getValidatedParams( params: unknown, method: string, - storageSizeLimit = STORAGE_SIZE_LIMIT, ): ManageStateParams { if (!isObject(params)) { throw rpcErrors.invalidParams({ @@ -260,23 +289,11 @@ export function getValidatedParams( }); } - let size; - try { - // `getJsonSize` will throw if the state is not JSON serializable. - size = getJsonSize(newState); - } catch { + if (!isValidJson(newState)) { throw rpcErrors.invalidParams({ message: `Invalid ${method} "newState" parameter: The new state must be JSON serializable.`, }); } - - if (size > storageSizeLimit) { - throw rpcErrors.invalidParams({ - message: `Invalid ${method} "newState" parameter: The new state must not exceed ${ - storageSizeLimit / 1_000_000 - } MB in size.`, - }); - } } return params as ManageStateParams; diff --git a/packages/snaps-utils/src/json.ts b/packages/snaps-utils/src/json.ts index 83df1819af..53c2dcbaad 100644 --- a/packages/snaps-utils/src/json.ts +++ b/packages/snaps-utils/src/json.ts @@ -23,14 +23,15 @@ export function parseJson(json: string) { * * This may sometimes be preferred over `getJsonSize` for performance reasons. * - * Note: This function does not encode the string to bytes, thus the input may + * Note: By default this function does not encode the string to bytes, thus the input may * be about 4x larger in practice. Use this function with caution. * * @param value - The JSON value to get the size of. + * @param encode - Whether the value should be encoded before measuring. * @returns The size of the JSON value in bytes. */ -export function getJsonSizeUnsafe(value: Json): number { +export function getJsonSizeUnsafe(value: Json, encode = false): number { const json = JSON.stringify(value); // We intentionally don't use `TextEncoder` because of bad performance on React Native. - return json.length; + return encode ? new TextEncoder().encode(json).byteLength : json.length; } From bb64a7a1894945dc389bae3d7f7f51e8b0837b74 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 1 Oct 2025 15:32:50 +0200 Subject: [PATCH 2/5] Add tests --- packages/snaps-rpc-methods/jest.config.js | 6 +-- .../src/restricted/manageState.test.ts | 38 +++++++++++++++++++ .../src/restricted/manageState.ts | 1 + packages/snaps-utils/src/json.test.ts | 5 +++ 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/packages/snaps-rpc-methods/jest.config.js b/packages/snaps-rpc-methods/jest.config.js index 729024cb74..38828b642d 100644 --- a/packages/snaps-rpc-methods/jest.config.js +++ b/packages/snaps-rpc-methods/jest.config.js @@ -10,10 +10,10 @@ module.exports = deepmerge(baseConfig, { ], coverageThreshold: { global: { - branches: 95.37, + branches: 95.7, functions: 98.75, - lines: 98.92, - statements: 98.62, + lines: 98.99, + statements: 98.69, }, }, }); diff --git a/packages/snaps-rpc-methods/src/restricted/manageState.test.ts b/packages/snaps-rpc-methods/src/restricted/manageState.test.ts index 97452b90e2..6bf6de772e 100644 --- a/packages/snaps-rpc-methods/src/restricted/manageState.test.ts +++ b/packages/snaps-rpc-methods/src/restricted/manageState.test.ts @@ -393,6 +393,44 @@ describe('snap_manageState', () => { true, ); }); + + it('throws an error on update if the new state is too large', async () => { + const clearSnapState = jest.fn().mockReturnValueOnce(true); + const getSnapState = jest.fn().mockReturnValueOnce(true); + const updateSnapState = jest.fn().mockReturnValueOnce(true); + const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); + + const manageStateImplementation = getManageStateImplementation({ + clearSnapState, + getSnapState, + updateSnapState, + getUnlockPromise: jest.fn(), + getSnap, + }); + + const newState = { + foo: 'foo'.repeat(21_500_000), + }; + + await expect( + manageStateImplementation({ + context: { origin: 'snap-origin' }, + method: 'snap_manageState', + params: { + operation: ManageStateOperation.UpdateState, + newState, + }, + }), + ).rejects.toThrow( + 'Invalid snap_manageState "newState" parameter: The new state must not exceed 64 MB in size.', + ); + + expect(updateSnapState).not.toHaveBeenCalledWith( + 'snap-origin', + newState, + true, + ); + }); }); describe('getValidatedParams', () => { diff --git a/packages/snaps-rpc-methods/src/restricted/manageState.ts b/packages/snaps-rpc-methods/src/restricted/manageState.ts index 24b64834b1..4f85a6433a 100644 --- a/packages/snaps-rpc-methods/src/restricted/manageState.ts +++ b/packages/snaps-rpc-methods/src/restricted/manageState.ts @@ -234,6 +234,7 @@ export function getManageStateImplementation({ return null; } + /* istanbul ignore next */ default: throw rpcErrors.invalidParams( `Invalid ${method} operation: "${ diff --git a/packages/snaps-utils/src/json.test.ts b/packages/snaps-utils/src/json.test.ts index 7d8b54d101..96b714be12 100644 --- a/packages/snaps-utils/src/json.test.ts +++ b/packages/snaps-utils/src/json.test.ts @@ -13,4 +13,9 @@ describe('getJsonSizeUnsafe', () => { const input = { foo: 'bar' }; expect(getJsonSizeUnsafe(input)).toBe(13); }); + + it('calculates the size of the JSON input in bytes', () => { + const input = { foo: 'bar€' }; + expect(getJsonSizeUnsafe(input, true)).toBe(16); + }); }); From 65f55386d8ae469f0d9ff033562e2f8b7f5afea6 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 1 Oct 2025 15:34:23 +0200 Subject: [PATCH 3/5] Tweak comment --- packages/snaps-utils/src/json.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snaps-utils/src/json.ts b/packages/snaps-utils/src/json.ts index 53c2dcbaad..b69376e5f4 100644 --- a/packages/snaps-utils/src/json.ts +++ b/packages/snaps-utils/src/json.ts @@ -32,6 +32,6 @@ export function parseJson(json: string) { */ export function getJsonSizeUnsafe(value: Json, encode = false): number { const json = JSON.stringify(value); - // We intentionally don't use `TextEncoder` because of bad performance on React Native. + // We intentionally don't use `TextEncoder` by default because of bad performance on React Native. return encode ? new TextEncoder().encode(json).byteLength : json.length; } From f7f6ae4a0a2bca806d13911f3faf4335266b8b21 Mon Sep 17 00:00:00 2001 From: MetaMask Bot Date: Wed, 1 Oct 2025 13:47:21 +0000 Subject: [PATCH 4/5] Update LavaMoat policies --- .../lavamoat/webpack/iframe/policy.json | 1 + .../lavamoat/webpack/node-process/policy.json | 1 + .../lavamoat/webpack/node-thread/policy.json | 1 + .../lavamoat/webpack/webview/policy.json | 1 + 4 files changed, 4 insertions(+) diff --git a/packages/snaps-execution-environments/lavamoat/webpack/iframe/policy.json b/packages/snaps-execution-environments/lavamoat/webpack/iframe/policy.json index 013c733b84..e4e44220a9 100644 --- a/packages/snaps-execution-environments/lavamoat/webpack/iframe/policy.json +++ b/packages/snaps-execution-environments/lavamoat/webpack/iframe/policy.json @@ -79,6 +79,7 @@ }, "@metamask/snaps-utils": { "globals": { + "TextEncoder": true, "URL": true, "console.error": true, "console.log": true, diff --git a/packages/snaps-execution-environments/lavamoat/webpack/node-process/policy.json b/packages/snaps-execution-environments/lavamoat/webpack/node-process/policy.json index 0e608d3ee7..7684fadfc1 100644 --- a/packages/snaps-execution-environments/lavamoat/webpack/node-process/policy.json +++ b/packages/snaps-execution-environments/lavamoat/webpack/node-process/policy.json @@ -85,6 +85,7 @@ }, "@metamask/snaps-utils": { "globals": { + "TextEncoder": true, "URL": true, "console.error": true, "console.log": true, diff --git a/packages/snaps-execution-environments/lavamoat/webpack/node-thread/policy.json b/packages/snaps-execution-environments/lavamoat/webpack/node-thread/policy.json index 0e608d3ee7..7684fadfc1 100644 --- a/packages/snaps-execution-environments/lavamoat/webpack/node-thread/policy.json +++ b/packages/snaps-execution-environments/lavamoat/webpack/node-thread/policy.json @@ -85,6 +85,7 @@ }, "@metamask/snaps-utils": { "globals": { + "TextEncoder": true, "URL": true, "console.error": true, "console.log": true, diff --git a/packages/snaps-execution-environments/lavamoat/webpack/webview/policy.json b/packages/snaps-execution-environments/lavamoat/webpack/webview/policy.json index 013c733b84..e4e44220a9 100644 --- a/packages/snaps-execution-environments/lavamoat/webpack/webview/policy.json +++ b/packages/snaps-execution-environments/lavamoat/webpack/webview/policy.json @@ -79,6 +79,7 @@ }, "@metamask/snaps-utils": { "globals": { + "TextEncoder": true, "URL": true, "console.error": true, "console.log": true, From 168610a7c182db08368ac59045652b7666007e2d Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 1 Oct 2025 16:04:22 +0200 Subject: [PATCH 5/5] Fix simulation --- packages/snaps-simulation/src/simulation.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/snaps-simulation/src/simulation.ts b/packages/snaps-simulation/src/simulation.ts index 33d774f09d..5518eb0fca 100644 --- a/packages/snaps-simulation/src/simulation.ts +++ b/packages/snaps-simulation/src/simulation.ts @@ -141,6 +141,14 @@ export type RestrictedMiddlewareHooks = { * @returns The cryptographic functions to use for the client. */ getClientCryptography: () => CryptographicFunctions; + + /** + * A hook that returns metadata about a given Snap. + * + * @param snapId - The ID of a Snap. + * @returns The metadata for the given Snap. + */ + getSnap: (snapId: string) => Snap; }; export type PermittedMiddlewareHooks = { @@ -450,6 +458,7 @@ export function getRestrictedHooks( ), getIsLocked: () => false, getClientCryptography: () => ({}), + getSnap: getGetSnapImplementation(true), }; }