From fc9f4c4af228e636a25929880841d69b55106237 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 4 Dec 2025 13:58:15 +0100 Subject: [PATCH 01/20] feat: Allow external images --- packages/snaps-sdk/src/index.ts | 2 + packages/snaps-sdk/src/internals/index.ts | 1 + packages/snaps-sdk/src/internals/uri.ts | 35 ++++++++++++++++ .../snaps-sdk/src/jsx/validation.test.tsx | 2 + packages/snaps-sdk/src/jsx/validation.ts | 3 +- packages/snaps-utils/src/types.ts | 41 +++---------------- 6 files changed, 47 insertions(+), 37 deletions(-) create mode 100644 packages/snaps-sdk/src/internals/uri.ts diff --git a/packages/snaps-sdk/src/index.ts b/packages/snaps-sdk/src/index.ts index 720afd5560..b42dc6b833 100644 --- a/packages/snaps-sdk/src/index.ts +++ b/packages/snaps-sdk/src/index.ts @@ -1,5 +1,6 @@ // Only internals that are used by other Snaps packages should be exported here. export type { EnumToUnion } from './internals'; +export type { UriOptions } from './internals'; export { getErrorData, getErrorMessage, @@ -13,6 +14,7 @@ export { selectiveUnion, nonEmptyRecord, ISO8601DateStruct, + uri, } from './internals'; // Re-exported from `@metamask/utils` for convenience. diff --git a/packages/snaps-sdk/src/internals/index.ts b/packages/snaps-sdk/src/internals/index.ts index d11fba8537..203a5dc2c2 100644 --- a/packages/snaps-sdk/src/internals/index.ts +++ b/packages/snaps-sdk/src/internals/index.ts @@ -5,3 +5,4 @@ export * from './structs'; export * from './jsx'; export * from './svg'; export * from './time'; +export * from './uri'; diff --git a/packages/snaps-sdk/src/internals/uri.ts b/packages/snaps-sdk/src/internals/uri.ts new file mode 100644 index 0000000000..49d594443d --- /dev/null +++ b/packages/snaps-sdk/src/internals/uri.ts @@ -0,0 +1,35 @@ +import { + refine, + string, + type, + assert as assertSuperstruct, + StructError, +} from '@metamask/superstruct'; +import type { Struct } from '@metamask/superstruct'; + +import { getErrorMessage } from '@metamask/snaps-sdk'; + +export type UriOptions = { + protocol?: Struct; + hash?: Struct; + port?: Struct; + hostname?: Struct; + pathname?: Struct; + search?: Struct; +}; + +export const uri = (opts: UriOptions = {}) => + refine(string(), 'uri', (value) => { + try { + const url = new URL(value); + + const UrlStruct = type(opts); + assertSuperstruct(url, UrlStruct); + return true; + } catch (error) { + if (error instanceof StructError) { + return getErrorMessage(error); + } + return `Expected URL, got "${value.toString()}"`; + } + }); diff --git a/packages/snaps-sdk/src/jsx/validation.test.tsx b/packages/snaps-sdk/src/jsx/validation.test.tsx index e92863d325..0e1e056317 100644 --- a/packages/snaps-sdk/src/jsx/validation.test.tsx +++ b/packages/snaps-sdk/src/jsx/validation.test.tsx @@ -1305,6 +1305,7 @@ describe('ImageStruct', () => { alt, , alt, + , ])('validates an image element', (value) => { expect(is(value, ImageStruct)).toBe(true); }); @@ -1322,6 +1323,7 @@ describe('ImageStruct', () => { {42}, // @ts-expect-error - Invalid props. , + , foo, foo diff --git a/packages/snaps-sdk/src/jsx/validation.ts b/packages/snaps-sdk/src/jsx/validation.ts index f1a6a7c988..f1336afd30 100644 --- a/packages/snaps-sdk/src/jsx/validation.ts +++ b/packages/snaps-sdk/src/jsx/validation.ts @@ -91,6 +91,7 @@ import { svg, typedUnion, ISO8601DateStruct, + uri, } from '../internals'; import { NonEip155AssetTypeStruct, @@ -238,7 +239,7 @@ export const BorderRadiusStruct = nullUnion([ * A struct for the {@link ImageElement} type. */ export const ImageStruct: Describe = element('Image', { - src: svg(), + src: nullUnion([svg(), uri({ protocol: literal('https:') })]), alt: optional(string()), borderRadius: optional(BorderRadiusStruct), }); diff --git a/packages/snaps-utils/src/types.ts b/packages/snaps-utils/src/types.ts index f5337ab018..73949697a0 100644 --- a/packages/snaps-utils/src/types.ts +++ b/packages/snaps-utils/src/types.ts @@ -1,15 +1,7 @@ -import { getErrorMessage } from '@metamask/snaps-sdk'; -import { - is, - optional, - refine, - size, - string, - type, - assert as assertSuperstruct, - StructError, -} from '@metamask/superstruct'; -import type { Infer, Struct } from '@metamask/superstruct'; +import type { UriOptions } from '@metamask/snaps-sdk'; +import { uri } from '@metamask/snaps-sdk'; +import { is, optional, size, string, type } from '@metamask/superstruct'; +import type { Infer } from '@metamask/superstruct'; import type { Json } from '@metamask/utils'; import { definePattern, VersionStruct } from '@metamask/utils'; @@ -109,30 +101,7 @@ type ObjectParameters< export type SnapExportsParameters = ObjectParameters; -type UriOptions = { - protocol?: Struct; - hash?: Struct; - port?: Struct; - hostname?: Struct; - pathname?: Struct; - search?: Struct; -}; - -export const uri = (opts: UriOptions = {}) => - refine(string(), 'uri', (value) => { - try { - const url = new URL(value); - - const UrlStruct = type(opts); - assertSuperstruct(url, UrlStruct); - return true; - } catch (error) { - if (error instanceof StructError) { - return getErrorMessage(error); - } - return `Expected URL, got "${value.toString()}"`; - } - }); +export { uri } from '@metamask/snaps-sdk'; /** * Returns whether a given value is a valid URL. From 4f6314816d2a83e4c33c2cac3876a66451ead3ad Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 4 Dec 2025 15:14:21 +0100 Subject: [PATCH 02/20] Add permission check --- .../src/interface/SnapInterfaceController.ts | 20 +++++++++++++++---- packages/snaps-utils/src/ui.tsx | 14 +++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/packages/snaps-controllers/src/interface/SnapInterfaceController.ts b/packages/snaps-controllers/src/interface/SnapInterfaceController.ts index af81e680f1..7b0a7f4f44 100644 --- a/packages/snaps-controllers/src/interface/SnapInterfaceController.ts +++ b/packages/snaps-controllers/src/interface/SnapInterfaceController.ts @@ -41,6 +41,7 @@ import { validateInterfaceContext, } from './utils'; import type { GetSnap } from '../snaps'; +import { HasPermission } from '@metamask/permission-controller'; const MAX_UI_CONTENT_SIZE = 10_000_000; // 10 mb @@ -114,7 +115,8 @@ export type SnapInterfaceControllerAllowedActions = | MultichainAssetsControllerGetStateAction | AccountsControllerGetSelectedMultichainAccountAction | AccountsControllerGetAccountByAddressAction - | AccountsControllerListMultichainAccountsAction; + | AccountsControllerListMultichainAccountsAction + | HasPermission; export type SnapInterfaceControllerActions = | CreateInterface @@ -282,7 +284,7 @@ export class SnapInterfaceController extends BaseController< contentType?: ContentType, ) { const element = getJsxInterface(content); - this.#validateContent(element); + this.#validateContent(snapId, element); validateInterfaceContext(context); const id = nanoid(); @@ -339,7 +341,7 @@ export class SnapInterfaceController extends BaseController< ) { this.#validateArgs(snapId, id); const element = getJsxInterface(content); - this.#validateContent(element); + this.#validateContent(snapId, element); validateInterfaceContext(context); const oldState = this.state.interfaces[id].state; @@ -530,13 +532,22 @@ export class SnapInterfaceController extends BaseController< return this.messenger.call('SnapController:get', id); } + #hasPermission(snapId: SnapId, permission: string) { + return this.messenger.call( + 'PermissionController:hasPermission', + snapId, + permission, + ); + } + /** * Utility function to validate the components of an interface. * Throws if something is invalid. * + * @param snapId - The Snap ID. * @param element - The JSX element to verify. */ - #validateContent(element: JSXElement) { + #validateContent(snapId: SnapId, element: JSXElement) { // We assume the validity of this JSON to be validated by the caller. // E.g., in the RPC method implementation. const size = getJsonSizeUnsafe(element); @@ -549,6 +560,7 @@ export class SnapInterfaceController extends BaseController< isOnPhishingList: this.#checkPhishingList.bind(this), getSnap: this.#getSnap.bind(this), getAccountByAddress: this.#getAccountByAddress.bind(this), + hasPermission: this.#hasPermission.bind(this, snapId), }); } diff --git a/packages/snaps-utils/src/ui.tsx b/packages/snaps-utils/src/ui.tsx index 845b06df43..2c6fa1f894 100644 --- a/packages/snaps-utils/src/ui.tsx +++ b/packages/snaps-utils/src/ui.tsx @@ -3,6 +3,7 @@ import { NodeType } from '@metamask/snaps-sdk'; import type { BoldChildren, GenericSnapElement, + ImageElement, ItalicChildren, JSXElement, LinkElement, @@ -42,6 +43,7 @@ import type { Token, Tokens } from 'marked'; import type { InternalAccount } from './account'; import type { Snap } from './snaps'; import { parseMetaMaskUrl } from './url'; +import { isValidUrl } from './types'; const MAX_TEXT_LENGTH = 50_000; // 50 kb const ALLOWED_PROTOCOLS = ['https:', 'mailto:', 'metamask:']; @@ -433,6 +435,7 @@ export function validateAssetSelector( * phishing list. * @param hooks.getSnap - The function that returns a snap if installed, undefined otherwise. * @param hooks.getAccountByAddress - The function that returns an account by address. + * @param hooks.hasPermission - A function that checks whether the Snap has a given permission. */ export function validateJsxElements( node: JSXElement, @@ -440,12 +443,14 @@ export function validateJsxElements( isOnPhishingList, getSnap, getAccountByAddress, + hasPermission, }: { isOnPhishingList: (url: string) => boolean; getSnap: (id: string) => Snap | undefined; getAccountByAddress: ( address: CaipAccountId, ) => InternalAccount | undefined; + hasPermission: (permission: string) => boolean; }, ) { walkJsx(node, (childNode) => { @@ -461,6 +466,15 @@ export function validateJsxElements( getAccountByAddress, ); break; + case 'Image': { + const { src } = (childNode as ImageElement).props; + const isUrl = isValidUrl(src); + assert( + !isUrl || (isUrl && hasPermission('endowment:network-access')), + 'Using external images is only permitted with the network access endowment.', + ); + break; + } default: break; } From ca2471ef509477cc49e7e5d31eab4bb07f597ef2 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Fri, 5 Dec 2025 10:37:22 +0100 Subject: [PATCH 03/20] Add test and fix some linting issues --- .../SnapInterfaceController.test.tsx | 33 +++++++++++++++++++ .../src/interface/SnapInterfaceController.ts | 2 +- .../src/test-utils/controller.tsx | 1 + packages/snaps-utils/src/ui.tsx | 5 ++- 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/snaps-controllers/src/interface/SnapInterfaceController.test.tsx b/packages/snaps-controllers/src/interface/SnapInterfaceController.test.tsx index 1473239dfc..0636e42475 100644 --- a/packages/snaps-controllers/src/interface/SnapInterfaceController.test.tsx +++ b/packages/snaps-controllers/src/interface/SnapInterfaceController.test.tsx @@ -524,6 +524,39 @@ describe('SnapInterfaceController', () => { ).toThrow('A Snap interface context may not be larger than 5 MB'); }); + it('throws if the Snap attempts to use external images without permission', async () => { + const rootMessenger = getRootSnapInterfaceControllerMessenger(); + const controllerMessenger = + getRestrictedSnapInterfaceControllerMessenger(rootMessenger); + + rootMessenger.registerActionHandler( + 'PermissionController:hasPermission', + () => false, + ); + + // eslint-disable-next-line no-new + new SnapInterfaceController({ + messenger: controllerMessenger, + }); + + const element = ( + + + + ); + + expect(() => + rootMessenger.call( + 'SnapInterfaceController:createInterface', + MOCK_SNAP_ID, + element, + {}, + ), + ).toThrow( + 'Using external images is only permitted with the network access endowment', + ); + }); + it('throws if a link is on the phishing list', async () => { const rootMessenger = getRootSnapInterfaceControllerMessenger(); const controllerMessenger = getRestrictedSnapInterfaceControllerMessenger( diff --git a/packages/snaps-controllers/src/interface/SnapInterfaceController.ts b/packages/snaps-controllers/src/interface/SnapInterfaceController.ts index 7b0a7f4f44..77a83e556f 100644 --- a/packages/snaps-controllers/src/interface/SnapInterfaceController.ts +++ b/packages/snaps-controllers/src/interface/SnapInterfaceController.ts @@ -8,6 +8,7 @@ import type { } from '@metamask/base-controller'; import { BaseController } from '@metamask/base-controller'; import type { Messenger } from '@metamask/messenger'; +import type { HasPermission } from '@metamask/permission-controller'; import type { TestOrigin } from '@metamask/phishing-controller'; import type { InterfaceState, @@ -41,7 +42,6 @@ import { validateInterfaceContext, } from './utils'; import type { GetSnap } from '../snaps'; -import { HasPermission } from '@metamask/permission-controller'; const MAX_UI_CONTENT_SIZE = 10_000_000; // 10 mb diff --git a/packages/snaps-controllers/src/test-utils/controller.tsx b/packages/snaps-controllers/src/test-utils/controller.tsx index 508fcabe48..14bb8638a3 100644 --- a/packages/snaps-controllers/src/test-utils/controller.tsx +++ b/packages/snaps-controllers/src/test-utils/controller.tsx @@ -768,6 +768,7 @@ export const getRestrictedSnapInterfaceControllerMessenger = ( 'SnapController:get', 'AccountsController:getSelectedMultichainAccount', 'AccountsController:listMultichainAccounts', + 'PermissionController:hasPermission', ], events: ['NotificationServicesController:notificationsListUpdated'], messenger: snapInterfaceControllerMessenger, diff --git a/packages/snaps-utils/src/ui.tsx b/packages/snaps-utils/src/ui.tsx index 2c6fa1f894..e13dfb18f6 100644 --- a/packages/snaps-utils/src/ui.tsx +++ b/packages/snaps-utils/src/ui.tsx @@ -3,7 +3,6 @@ import { NodeType } from '@metamask/snaps-sdk'; import type { BoldChildren, GenericSnapElement, - ImageElement, ItalicChildren, JSXElement, LinkElement, @@ -42,8 +41,8 @@ import type { Token, Tokens } from 'marked'; import type { InternalAccount } from './account'; import type { Snap } from './snaps'; -import { parseMetaMaskUrl } from './url'; import { isValidUrl } from './types'; +import { parseMetaMaskUrl } from './url'; const MAX_TEXT_LENGTH = 50_000; // 50 kb const ALLOWED_PROTOCOLS = ['https:', 'mailto:', 'metamask:']; @@ -467,7 +466,7 @@ export function validateJsxElements( ); break; case 'Image': { - const { src } = (childNode as ImageElement).props; + const { src } = childNode.props; const isUrl = isValidUrl(src); assert( !isUrl || (isUrl && hasPermission('endowment:network-access')), From 0969c0cbaeff47c7f13757f8178aa2db5de1ed07 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Fri, 5 Dec 2025 10:51:15 +0100 Subject: [PATCH 04/20] Move tests to SDK package --- packages/snaps-sdk/src/internals/uri.test.ts | 36 ++++++++++++++++++++ packages/snaps-utils/coverage.json | 8 ++--- 2 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 packages/snaps-sdk/src/internals/uri.test.ts diff --git a/packages/snaps-sdk/src/internals/uri.test.ts b/packages/snaps-sdk/src/internals/uri.test.ts new file mode 100644 index 0000000000..c6fd6babbb --- /dev/null +++ b/packages/snaps-sdk/src/internals/uri.test.ts @@ -0,0 +1,36 @@ +import { enums, is, literal } from '@metamask/superstruct'; + +import { uri } from './uri'; + +describe('uri', () => { + it.each([ + 'npm:foo-bar', + 'http://asd.com', + 'https://dsa.com/foo', + 'http://dsa.com/foo?asd=5&dsa=6#bar', + 'npm:foo/bar?asd', + 'local:asd.com', + 'http://asd@asd.com', + 'http://asd:foo@asd.com', + ])('validates correct uri', (value) => { + expect(is(value, uri())).toBe(true); + }); + + it.each([5, 'asd', undefined, null, {}, uri, URL])( + 'invalidates invalid uri', + (value) => { + expect(is(value, uri())).toBe(false); + }, + ); + + it('takes additional constraints', () => { + const constraints = { + protocol: enums(['foo:', 'bar:']), + hash: literal('#hello'), + }; + const struct = uri(constraints); + expect(is('foo://asd.com/#hello', struct)).toBe(true); + expect(is('foo://asd.com/', struct)).toBe(false); + expect(is('http://asd.com/#hello', struct)).toBe(false); + }); +}); diff --git a/packages/snaps-utils/coverage.json b/packages/snaps-utils/coverage.json index 5eee8de081..0b54be4e04 100644 --- a/packages/snaps-utils/coverage.json +++ b/packages/snaps-utils/coverage.json @@ -1,6 +1,6 @@ { - "branches": 99.76, - "functions": 99.01, - "lines": 98.69, - "statements": 97.29 + "branches": 98.87, + "functions": 99.03, + "lines": 98.43, + "statements": 96.94 } From b5855b3882da8eb9f36970afc07ce001c07fcb85 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Fri, 5 Dec 2025 10:56:19 +0100 Subject: [PATCH 05/20] Update LavaMoat policies --- .../lavamoat/webpack/iframe/policy.json | 4 +++- .../lavamoat/webpack/node-process/policy.json | 4 +++- .../lavamoat/webpack/node-thread/policy.json | 4 +++- .../lavamoat/webpack/webview/policy.json | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/snaps-execution-environments/lavamoat/webpack/iframe/policy.json b/packages/snaps-execution-environments/lavamoat/webpack/iframe/policy.json index 03edb96300..e534639ec3 100644 --- a/packages/snaps-execution-environments/lavamoat/webpack/iframe/policy.json +++ b/packages/snaps-execution-environments/lavamoat/webpack/iframe/policy.json @@ -72,6 +72,9 @@ } }, "@metamask/snaps-sdk": { + "globals": { + "URL": true + }, "packages": { "@metamask/superstruct": true, "@metamask/utils": true, @@ -81,7 +84,6 @@ "@metamask/snaps-utils": { "globals": { "TextEncoder": true, - "URL": true, "console.error": true, "console.log": true, "console.warn": 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 984466b8b9..852d7308d8 100644 --- a/packages/snaps-execution-environments/lavamoat/webpack/node-process/policy.json +++ b/packages/snaps-execution-environments/lavamoat/webpack/node-process/policy.json @@ -79,6 +79,9 @@ } }, "@metamask/snaps-sdk": { + "globals": { + "URL": true + }, "packages": { "@metamask/superstruct": true, "@metamask/utils": true, @@ -88,7 +91,6 @@ "@metamask/snaps-utils": { "globals": { "TextEncoder": true, - "URL": true, "console.error": true, "console.log": true, "console.warn": 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 984466b8b9..852d7308d8 100644 --- a/packages/snaps-execution-environments/lavamoat/webpack/node-thread/policy.json +++ b/packages/snaps-execution-environments/lavamoat/webpack/node-thread/policy.json @@ -79,6 +79,9 @@ } }, "@metamask/snaps-sdk": { + "globals": { + "URL": true + }, "packages": { "@metamask/superstruct": true, "@metamask/utils": true, @@ -88,7 +91,6 @@ "@metamask/snaps-utils": { "globals": { "TextEncoder": true, - "URL": true, "console.error": true, "console.log": true, "console.warn": true diff --git a/packages/snaps-execution-environments/lavamoat/webpack/webview/policy.json b/packages/snaps-execution-environments/lavamoat/webpack/webview/policy.json index 03edb96300..e534639ec3 100644 --- a/packages/snaps-execution-environments/lavamoat/webpack/webview/policy.json +++ b/packages/snaps-execution-environments/lavamoat/webpack/webview/policy.json @@ -72,6 +72,9 @@ } }, "@metamask/snaps-sdk": { + "globals": { + "URL": true + }, "packages": { "@metamask/superstruct": true, "@metamask/utils": true, @@ -81,7 +84,6 @@ "@metamask/snaps-utils": { "globals": { "TextEncoder": true, - "URL": true, "console.error": true, "console.log": true, "console.warn": true From b091f06c51d4a79640a2c9f53260653f838f303f Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Fri, 5 Dec 2025 11:52:00 +0100 Subject: [PATCH 06/20] Fix lint --- packages/snaps-sdk/src/internals/uri.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/snaps-sdk/src/internals/uri.ts b/packages/snaps-sdk/src/internals/uri.ts index 49d594443d..895214354f 100644 --- a/packages/snaps-sdk/src/internals/uri.ts +++ b/packages/snaps-sdk/src/internals/uri.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from '@metamask/snaps-sdk'; import { refine, string, @@ -7,8 +8,6 @@ import { } from '@metamask/superstruct'; import type { Struct } from '@metamask/superstruct'; -import { getErrorMessage } from '@metamask/snaps-sdk'; - export type UriOptions = { protocol?: Struct; hash?: Struct; From e635711daa07cc356007078e48435c10de28d504 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Fri, 5 Dec 2025 11:56:17 +0100 Subject: [PATCH 07/20] Add one more test --- packages/snaps-utils/coverage.json | 2 +- packages/snaps-utils/src/ui.test.tsx | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/snaps-utils/coverage.json b/packages/snaps-utils/coverage.json index 0b54be4e04..b2fa064f4b 100644 --- a/packages/snaps-utils/coverage.json +++ b/packages/snaps-utils/coverage.json @@ -1,5 +1,5 @@ { - "branches": 98.87, + "branches": 99.77, "functions": 99.03, "lines": 98.43, "statements": 96.94 diff --git a/packages/snaps-utils/src/ui.test.tsx b/packages/snaps-utils/src/ui.test.tsx index b13149e557..48b848dfab 100644 --- a/packages/snaps-utils/src/ui.test.tsx +++ b/packages/snaps-utils/src/ui.test.tsx @@ -851,6 +851,7 @@ describe('validateJsxElements', () => { isOnPhishingList, getSnap: jest.fn(), getAccountByAddress: jest.fn(), + hasPermission: jest.fn(), }), ).not.toThrow(); }); @@ -868,6 +869,7 @@ describe('validateJsxElements', () => { isOnPhishingList, getSnap: jest.fn(), getAccountByAddress: jest.fn(), + hasPermission: jest.fn(), }, ), ).not.toThrow(); @@ -892,6 +894,7 @@ describe('validateJsxElements', () => { isOnPhishingList, getSnap: jest.fn(), getAccountByAddress: jest.fn(), + hasPermission: jest.fn(), }), ).toThrow('Invalid URL: The specified URL is not allowed.'); }); @@ -904,6 +907,7 @@ describe('validateJsxElements', () => { isOnPhishingList, getSnap: jest.fn(), getAccountByAddress: jest.fn(), + hasPermission: jest.fn(), }), ).toThrow( 'Invalid URL: Protocol must be one of: https:, mailto:, metamask:.', @@ -918,6 +922,7 @@ describe('validateJsxElements', () => { isOnPhishingList, getSnap: jest.fn(), getAccountByAddress: jest.fn(), + hasPermission: jest.fn(), }), ).toThrow('Invalid URL: Unable to parse URL.'); }); @@ -939,6 +944,7 @@ describe('validateJsxElements', () => { getAccountByAddress, isOnPhishingList: jest.fn(), getSnap: jest.fn(), + hasPermission: jest.fn(), }, ), ).not.toThrow(); @@ -959,12 +965,26 @@ describe('validateJsxElements', () => { getAccountByAddress, isOnPhishingList: jest.fn(), getSnap: jest.fn(), + hasPermission: jest.fn(), }, ), ).toThrow( 'Could not find account for address: solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:7S3P4HxJpyyigGzodYwHtCxZyUQe9JiBMHyRWXArAaKv', ); }); + + it('throws if the Snap tries to use external images without permission', () => { + expect(() => + validateJsxElements(, { + getAccountByAddress: jest.fn(), + isOnPhishingList: jest.fn(), + getSnap: jest.fn(), + hasPermission: jest.fn().mockReturnValue(false), + }), + ).toThrow( + 'Using external images is only permitted with the network access endowment.', + ); + }); }); describe('getTotalTextLength', () => { From 9278178d7f3f243b9b56f2345698b0324db88d29 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Fri, 5 Dec 2025 12:52:23 +0100 Subject: [PATCH 08/20] Update example --- .../examples/packages/images/snap.config.ts | 2 +- .../packages/images/snap.manifest.json | 2 +- .../src/{index.test.ts => index.test.tsx} | 61 +++++++++++----- .../images/src/{index.ts => index.tsx} | 71 ++++++++++++++----- packages/snaps-simulation/src/controllers.ts | 1 + 5 files changed, 99 insertions(+), 38 deletions(-) rename packages/examples/packages/images/src/{index.test.ts => index.test.tsx} (75%) rename packages/examples/packages/images/src/{index.ts => index.tsx} (63%) diff --git a/packages/examples/packages/images/snap.config.ts b/packages/examples/packages/images/snap.config.ts index ce31ca55a0..2c0327f5be 100644 --- a/packages/examples/packages/images/snap.config.ts +++ b/packages/examples/packages/images/snap.config.ts @@ -1,7 +1,7 @@ import type { SnapConfig } from '@metamask/snaps-cli'; const config: SnapConfig = { - input: './src/index.ts', + input: './src/index.tsx', server: { port: 8026, }, diff --git a/packages/examples/packages/images/snap.manifest.json b/packages/examples/packages/images/snap.manifest.json index 659f7e00a4..7a57b7106f 100644 --- a/packages/examples/packages/images/snap.manifest.json +++ b/packages/examples/packages/images/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "1naltBL/H7TBXHn+Zxgrq7wz6HUKiUUxFQTYTbp5KkQ=", + "shasum": "+3/A5op8WHKPwQMpVcNL6FA8J6NYG3z6LcxMt+IAkjs=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/images/src/index.test.ts b/packages/examples/packages/images/src/index.test.tsx similarity index 75% rename from packages/examples/packages/images/src/index.test.ts rename to packages/examples/packages/images/src/index.test.tsx index bc50ae4bf8..a49cb17565 100644 --- a/packages/examples/packages/images/src/index.test.ts +++ b/packages/examples/packages/images/src/index.test.tsx @@ -1,11 +1,12 @@ import { expect } from '@jest/globals'; -import { installSnap } from '@metamask/snaps-jest'; -import { DialogType, image, panel, text } from '@metamask/snaps-sdk'; +import { installSnap, assertIsAlertDialog } from '@metamask/snaps-jest'; +import { DialogType } from '@metamask/snaps-sdk'; +import { Box, Text, Image } from '@metamask/snaps-sdk/jsx'; import { renderSVG } from 'uqr'; describe('onRpcRequest', () => { it('throws an error if the requested method does not exist', async () => { - const { request, close } = await installSnap(); + const { request } = await installSnap(); const response = await request({ method: 'foo', @@ -20,8 +21,6 @@ describe('onRpcRequest', () => { cause: null, }, }); - - await close(); }); describe('getQrCode', () => { @@ -36,15 +35,16 @@ describe('onRpcRequest', () => { }); const ui = await response.getInterface(); + assertIsAlertDialog(ui); + expect(ui).toRender( - panel([ - text(`The following is a QR code for the data "Hello, world!":`), - image(renderSVG('Hello, world!')), - ]), + + The following is a QR code for the data "Hello, world!": + + , ); - // TODO(ritave): Fix types in SnapInterface - await (ui as any).ok(); + await ui.ok(); expect(await response).toRespondWith(null); }); @@ -61,6 +61,8 @@ describe('onRpcRequest', () => { }); const ui = await response.getInterface(); + assertIsAlertDialog(ui); + expect(ui).toStrictEqual( expect.objectContaining({ type: DialogType.Alert, @@ -80,8 +82,31 @@ describe('onRpcRequest', () => { }), ); - // TODO(ritave): Fix types in SnapInterface - await (ui as any).ok(); + await ui.ok(); + + expect(await response).toRespondWith(null); + }); + }); + + describe('getCatExternal', () => { + it('shows a cat using an external URL', async () => { + const { request } = await installSnap(); + + const response = request({ + method: 'getCatExternal', + }); + + const ui = await response.getInterface(); + assertIsAlertDialog(ui); + + expect(ui).toRender( + + Enjoy your cat! + + , + ); + + await ui.ok(); expect(await response).toRespondWith(null); }); @@ -96,6 +121,8 @@ describe('onRpcRequest', () => { }); const ui = await response.getInterface(); + assertIsAlertDialog(ui); + // eslint-disable-next-line jest/prefer-strict-equal expect(ui.content).toEqual({ type: 'Box', @@ -120,8 +147,7 @@ describe('onRpcRequest', () => { key: null, }); - // TODO(ritave): Fix types in SnapInterface - await (ui as any).ok(); + await ui.ok(); expect(await response).toRespondWith(null); }); @@ -136,6 +162,8 @@ describe('onRpcRequest', () => { }); const ui = await response.getInterface(); + assertIsAlertDialog(ui); + // eslint-disable-next-line jest/prefer-strict-equal expect(ui.content).toEqual({ type: 'Box', @@ -160,8 +188,7 @@ describe('onRpcRequest', () => { key: null, }); - // TODO(ritave): Fix types in SnapInterface - await (ui as any).ok(); + await ui.ok(); expect(await response).toRespondWith(null); }); diff --git a/packages/examples/packages/images/src/index.ts b/packages/examples/packages/images/src/index.tsx similarity index 63% rename from packages/examples/packages/images/src/index.ts rename to packages/examples/packages/images/src/index.tsx index 42e71a7f17..2125ea9d12 100644 --- a/packages/examples/packages/images/src/index.ts +++ b/packages/examples/packages/images/src/index.tsx @@ -2,11 +2,9 @@ import type { OnRpcRequestHandler } from '@metamask/snaps-sdk'; import { DialogType, getImageComponent, - image, - panel, - text, MethodNotFoundError, } from '@metamask/snaps-sdk'; +import { Box, Text, Image } from '@metamask/snaps-sdk/jsx'; import { renderSVG } from 'uqr'; import pngIcon from './images/icon.png'; @@ -51,10 +49,12 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { method: 'snap_dialog', params: { type: DialogType.Alert, - content: panel([ - text(`The following is a QR code for the data "${data}":`), - image(qr), - ]), + content: ( + + The following is a QR code for the data "{data}": + + + ), }, }); } @@ -64,15 +64,38 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { method: 'snap_dialog', params: { type: DialogType.Alert, - content: panel([ - text('Enjoy your cat!'), - + content: ( // The `getImageComponent` helper can also be used to fetch an image - // from a URL and render it using the `image` component. - await getImageComponent('https://cataas.com/cat', { - width: 400, - }), - ]), + // from a URL and render it using the `Image` component. + + Enjoy your cat! + + + ), + }, + }); + } + + case 'getCatExternal': { + return await snap.request({ + method: 'snap_dialog', + params: { + type: DialogType.Alert, + // TODO: Figure out how to support width + content: ( + + Enjoy your cat! + + + ), }, }); } @@ -84,8 +107,13 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { type: DialogType.Alert, // `.svg` files are imported as strings, so they can be used directly - // with the `image` component. - content: panel([text('Here is an SVG icon:'), image(svgIcon)]), + // with the `Image` component. + content: ( + + Here is an SVG icon: + + + ), }, }); } @@ -97,8 +125,13 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { type: DialogType.Alert, // `.png` files are imported as SVGs containing an `` tag, - // so they can be used directly with the `image` component. - content: panel([text('Here is a PNG icon:'), image(pngIcon)]), + // so they can be used directly with the `Image` component. + content: ( + + Here is a PNG icon: + + + ), }, }); } diff --git a/packages/snaps-simulation/src/controllers.ts b/packages/snaps-simulation/src/controllers.ts index 9d841beae0..020416bf85 100644 --- a/packages/snaps-simulation/src/controllers.ts +++ b/packages/snaps-simulation/src/controllers.ts @@ -95,6 +95,7 @@ export function getControllers(options: GetControllersOptions): Controllers { 'AccountsController:getSelectedMultichainAccount', 'AccountsController:listMultichainAccounts', 'MultichainAssetsController:getState', + 'PermissionController:hasPermission', ], events: ['NotificationServicesController:notificationsListUpdated'], }); From 1964b1153b250dfda60eae1fc753c18fe7cf2e47 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Fri, 5 Dec 2025 13:35:09 +0100 Subject: [PATCH 09/20] Use relative import --- packages/snaps-sdk/src/internals/uri.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/snaps-sdk/src/internals/uri.ts b/packages/snaps-sdk/src/internals/uri.ts index 895214354f..396804b3c5 100644 --- a/packages/snaps-sdk/src/internals/uri.ts +++ b/packages/snaps-sdk/src/internals/uri.ts @@ -1,4 +1,3 @@ -import { getErrorMessage } from '@metamask/snaps-sdk'; import { refine, string, @@ -8,6 +7,8 @@ import { } from '@metamask/superstruct'; import type { Struct } from '@metamask/superstruct'; +import { getErrorMessage } from './errors'; + export type UriOptions = { protocol?: Struct; hash?: Struct; From 9e087ee9ee847e5f9f7bf5c814bdc6ab15a8063c Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Mon, 8 Dec 2025 10:00:45 +0100 Subject: [PATCH 10/20] Fix test --- packages/examples/packages/images/src/index.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/examples/packages/images/src/index.test.tsx b/packages/examples/packages/images/src/index.test.tsx index a49cb17565..387c1acde8 100644 --- a/packages/examples/packages/images/src/index.test.tsx +++ b/packages/examples/packages/images/src/index.test.tsx @@ -39,7 +39,9 @@ describe('onRpcRequest', () => { expect(ui).toRender( - The following is a QR code for the data "Hello, world!": + + The following is a QR code for the data "{'Hello, world!'}": + , ); From a8219c98062168e94cf007f1795595cb5c1cdb60 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Mon, 8 Dec 2025 10:05:22 +0100 Subject: [PATCH 11/20] Update manifest --- packages/examples/packages/images/snap.manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/examples/packages/images/snap.manifest.json b/packages/examples/packages/images/snap.manifest.json index 7a57b7106f..6910fbdf7f 100644 --- a/packages/examples/packages/images/snap.manifest.json +++ b/packages/examples/packages/images/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "+3/A5op8WHKPwQMpVcNL6FA8J6NYG3z6LcxMt+IAkjs=", + "shasum": "Tq95iZPYgpTGqhLCUmwWTVPcVXdV4D6ZbXeNMVCEfbo=", "location": { "npm": { "filePath": "dist/bundle.js", From 65e9e96b47d2e3625a72da8dd425467481796091 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Mon, 8 Dec 2025 10:14:47 +0100 Subject: [PATCH 12/20] Support width/height --- packages/examples/packages/images/snap.manifest.json | 2 +- packages/examples/packages/images/src/index.tsx | 3 +-- packages/snaps-sdk/src/jsx/components/Image.ts | 4 ++++ packages/snaps-sdk/src/jsx/validation.test.tsx | 1 + packages/snaps-sdk/src/jsx/validation.ts | 2 ++ 5 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/examples/packages/images/snap.manifest.json b/packages/examples/packages/images/snap.manifest.json index 6910fbdf7f..9598fab4c7 100644 --- a/packages/examples/packages/images/snap.manifest.json +++ b/packages/examples/packages/images/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "Tq95iZPYgpTGqhLCUmwWTVPcVXdV4D6ZbXeNMVCEfbo=", + "shasum": "JD232NPirmuJ0UTjZGtQx0so1T7yGKgxss+XdbqUDnA=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/images/src/index.tsx b/packages/examples/packages/images/src/index.tsx index 2125ea9d12..08fcc862e0 100644 --- a/packages/examples/packages/images/src/index.tsx +++ b/packages/examples/packages/images/src/index.tsx @@ -89,11 +89,10 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { method: 'snap_dialog', params: { type: DialogType.Alert, - // TODO: Figure out how to support width content: ( Enjoy your cat! - + ), }, diff --git a/packages/snaps-sdk/src/jsx/components/Image.ts b/packages/snaps-sdk/src/jsx/components/Image.ts index a097f6d6f2..3fa023d3bb 100644 --- a/packages/snaps-sdk/src/jsx/components/Image.ts +++ b/packages/snaps-sdk/src/jsx/components/Image.ts @@ -14,6 +14,8 @@ type ImageProps = { src: string; alt?: string | undefined; borderRadius?: BorderRadius | undefined; + height?: number | undefined; + width?: number | undefined; }; const TYPE = 'Image'; @@ -30,6 +32,8 @@ const TYPE = 'Image'; * @param props.alt - The alternative text of the image, which describes the * image for users who cannot see it. * @param props.borderRadius - The border radius applied to the image. + * @param props.width - The width of the image. + * @param props.height - The height of the image. * @returns An image element. * @example * An example image diff --git a/packages/snaps-sdk/src/jsx/validation.test.tsx b/packages/snaps-sdk/src/jsx/validation.test.tsx index 0e1e056317..d1f1a171ff 100644 --- a/packages/snaps-sdk/src/jsx/validation.test.tsx +++ b/packages/snaps-sdk/src/jsx/validation.test.tsx @@ -1306,6 +1306,7 @@ describe('ImageStruct', () => { , alt, , + , ])('validates an image element', (value) => { expect(is(value, ImageStruct)).toBe(true); }); diff --git a/packages/snaps-sdk/src/jsx/validation.ts b/packages/snaps-sdk/src/jsx/validation.ts index f1336afd30..2dab4873c1 100644 --- a/packages/snaps-sdk/src/jsx/validation.ts +++ b/packages/snaps-sdk/src/jsx/validation.ts @@ -242,6 +242,8 @@ export const ImageStruct: Describe = element('Image', { src: nullUnion([svg(), uri({ protocol: literal('https:') })]), alt: optional(string()), borderRadius: optional(BorderRadiusStruct), + width: optional(number()), + height: optional(number()), }); const IconNameStruct: Struct<`${IconName}`, null> = nullUnion( From 5e9ecf37bacbef34a822468f079b2c9971c00af8 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Mon, 8 Dec 2025 10:16:07 +0100 Subject: [PATCH 13/20] Add test-snaps button --- packages/test-snaps/src/features/snaps/images/Images.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/test-snaps/src/features/snaps/images/Images.tsx b/packages/test-snaps/src/features/snaps/images/Images.tsx index 69840bd7b3..0b1aa97e10 100644 --- a/packages/test-snaps/src/features/snaps/images/Images.tsx +++ b/packages/test-snaps/src/features/snaps/images/Images.tsx @@ -14,6 +14,7 @@ export const Images: FunctionComponent = () => { testId="images" > + From 350116d08cd07e0995083222f9c7a84a4d349ac0 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Mon, 8 Dec 2025 10:23:59 +0100 Subject: [PATCH 14/20] Fix test --- packages/examples/packages/images/src/index.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/examples/packages/images/src/index.test.tsx b/packages/examples/packages/images/src/index.test.tsx index 387c1acde8..cd1e30d48a 100644 --- a/packages/examples/packages/images/src/index.test.tsx +++ b/packages/examples/packages/images/src/index.test.tsx @@ -104,7 +104,7 @@ describe('onRpcRequest', () => { expect(ui).toRender( Enjoy your cat! - + , ); From 208facddc7228773e812852f93f0d0acc209936a Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Mon, 8 Dec 2025 10:44:05 +0100 Subject: [PATCH 15/20] Add missing test --- packages/snaps-utils/src/ui.test.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/snaps-utils/src/ui.test.tsx b/packages/snaps-utils/src/ui.test.tsx index 48b848dfab..a5ead5c379 100644 --- a/packages/snaps-utils/src/ui.test.tsx +++ b/packages/snaps-utils/src/ui.test.tsx @@ -843,7 +843,8 @@ describe('validateJsxElements', () => { Bar , - ])('does not throw for a safe JSX text component', async (element) => { + , + ])('does not throw for a safe JSX component', async (element) => { const isOnPhishingList = () => false; expect(() => @@ -851,7 +852,7 @@ describe('validateJsxElements', () => { isOnPhishingList, getSnap: jest.fn(), getAccountByAddress: jest.fn(), - hasPermission: jest.fn(), + hasPermission: jest.fn().mockReturnValue(true), }), ).not.toThrow(); }); From 9e72ce7efd823dad16821877961011c7a9b63766 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Mon, 8 Dec 2025 11:26:29 +0100 Subject: [PATCH 16/20] Remove usage of getImageComponent --- .../packages/images/snap.manifest.json | 2 +- .../packages/images/src/index.test.tsx | 41 +------------------ .../examples/packages/images/src/index.tsx | 39 ++---------------- packages/snaps-sdk/src/images.ts | 1 + .../src/features/snaps/images/Images.tsx | 1 - 5 files changed, 7 insertions(+), 77 deletions(-) diff --git a/packages/examples/packages/images/snap.manifest.json b/packages/examples/packages/images/snap.manifest.json index 9598fab4c7..ec73b9e78f 100644 --- a/packages/examples/packages/images/snap.manifest.json +++ b/packages/examples/packages/images/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "JD232NPirmuJ0UTjZGtQx0so1T7yGKgxss+XdbqUDnA=", + "shasum": "A2pzU53b9kEBkckTMFDxcPr9/lbaEsOvNUJK+rNx3/8=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/images/src/index.test.tsx b/packages/examples/packages/images/src/index.test.tsx index cd1e30d48a..9c36ee857b 100644 --- a/packages/examples/packages/images/src/index.test.tsx +++ b/packages/examples/packages/images/src/index.test.tsx @@ -1,6 +1,5 @@ import { expect } from '@jest/globals'; import { installSnap, assertIsAlertDialog } from '@metamask/snaps-jest'; -import { DialogType } from '@metamask/snaps-sdk'; import { Box, Text, Image } from '@metamask/snaps-sdk/jsx'; import { renderSVG } from 'uqr'; @@ -53,49 +52,11 @@ describe('onRpcRequest', () => { }); describe('getCat', () => { - // This test is flaky, so we disable it for now. - // eslint-disable-next-line jest/no-disabled-tests - it.skip('shows a cat', async () => { - const { request } = await installSnap(); - - const response = request({ - method: 'getCat', - }); - - const ui = await response.getInterface(); - assertIsAlertDialog(ui); - - expect(ui).toStrictEqual( - expect.objectContaining({ - type: DialogType.Alert, - content: { - type: 'panel', - children: [ - { - type: 'text', - value: 'Enjoy your cat!', - }, - { - type: 'image', - value: expect.any(String), - }, - ], - }, - }), - ); - - await ui.ok(); - - expect(await response).toRespondWith(null); - }); - }); - - describe('getCatExternal', () => { it('shows a cat using an external URL', async () => { const { request } = await installSnap(); const response = request({ - method: 'getCatExternal', + method: 'getCat', }); const ui = await response.getInterface(); diff --git a/packages/examples/packages/images/src/index.tsx b/packages/examples/packages/images/src/index.tsx index 08fcc862e0..10388acb2d 100644 --- a/packages/examples/packages/images/src/index.tsx +++ b/packages/examples/packages/images/src/index.tsx @@ -1,9 +1,5 @@ import type { OnRpcRequestHandler } from '@metamask/snaps-sdk'; -import { - DialogType, - getImageComponent, - MethodNotFoundError, -} from '@metamask/snaps-sdk'; +import { DialogType, MethodNotFoundError } from '@metamask/snaps-sdk'; import { Box, Text, Image } from '@metamask/snaps-sdk/jsx'; import { renderSVG } from 'uqr'; @@ -24,10 +20,8 @@ type GetQrCodeParams = { * `wallet_invokeSnap` method. This handler handles two methods: * * - `getQrCode`: Show a QR code to the user. The QR code is generated using - * the `uqr` library, and rendered using the `image` component. - * - `getCat`: Show a cat to the user. The cat image is fetched using the - * `getImageComponent` helper. The helper returns an `image` component, which - * can be rendered in a Snap dialog, for example. + * the `uqr` library, and rendered using the `Image` component. + * - `getCat`: Show a cat to the user using an external image. * * @param params - The request parameters. * @param params.request - The JSON-RPC request object. @@ -42,7 +36,7 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { const { data } = request.params as GetQrCodeParams; // `renderSVG` returns a `` element as a string, which can be - // rendered using the `image` component. + // rendered using the `Image` component. const qr = renderSVG(data); return await snap.request({ @@ -60,31 +54,6 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { } case 'getCat': { - return await snap.request({ - method: 'snap_dialog', - params: { - type: DialogType.Alert, - content: ( - // The `getImageComponent` helper can also be used to fetch an image - // from a URL and render it using the `Image` component. - - Enjoy your cat! - - - ), - }, - }); - } - - case 'getCatExternal': { return await snap.request({ method: 'snap_dialog', params: { diff --git a/packages/snaps-sdk/src/images.ts b/packages/snaps-sdk/src/images.ts index 686deb58d7..1aa2e9a6e2 100644 --- a/packages/snaps-sdk/src/images.ts +++ b/packages/snaps-sdk/src/images.ts @@ -105,6 +105,7 @@ export type ImageOptions = { * @param options.request - The options to use when fetching the image data. * This is passed directly to `fetch`. * @returns A promise that resolves to the image data as an image component. + * @deprecated Use `` instead. This function will be removed in a future release. */ export async function getImageComponent( url: string, diff --git a/packages/test-snaps/src/features/snaps/images/Images.tsx b/packages/test-snaps/src/features/snaps/images/Images.tsx index 0b1aa97e10..69840bd7b3 100644 --- a/packages/test-snaps/src/features/snaps/images/Images.tsx +++ b/packages/test-snaps/src/features/snaps/images/Images.tsx @@ -14,7 +14,6 @@ export const Images: FunctionComponent = () => { testId="images" > - From 956fcf5ce6ce31d6b04d591b834189420aa3ffea Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Mon, 8 Dec 2025 15:13:41 +0100 Subject: [PATCH 17/20] Update packages/snaps-utils/src/ui.tsx Co-authored-by: Maarten Zuidhoorn --- packages/snaps-utils/src/ui.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snaps-utils/src/ui.tsx b/packages/snaps-utils/src/ui.tsx index e13dfb18f6..7e8982e953 100644 --- a/packages/snaps-utils/src/ui.tsx +++ b/packages/snaps-utils/src/ui.tsx @@ -470,7 +470,7 @@ export function validateJsxElements( const isUrl = isValidUrl(src); assert( !isUrl || (isUrl && hasPermission('endowment:network-access')), - 'Using external images is only permitted with the network access endowment.', + 'Using external images is only permitted with the `endowment:network-access` permission.', ); break; } From 429aec23e2cdc98433d22600d5ca11c0dc55ca1e Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Mon, 8 Dec 2025 15:14:54 +0100 Subject: [PATCH 18/20] Fix tests --- .../src/interface/SnapInterfaceController.test.tsx | 2 +- packages/snaps-utils/src/ui.test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/snaps-controllers/src/interface/SnapInterfaceController.test.tsx b/packages/snaps-controllers/src/interface/SnapInterfaceController.test.tsx index 0636e42475..31bc780a69 100644 --- a/packages/snaps-controllers/src/interface/SnapInterfaceController.test.tsx +++ b/packages/snaps-controllers/src/interface/SnapInterfaceController.test.tsx @@ -553,7 +553,7 @@ describe('SnapInterfaceController', () => { {}, ), ).toThrow( - 'Using external images is only permitted with the network access endowment', + 'Using external images is only permitted with the `endowment:network-access` permission.', ); }); diff --git a/packages/snaps-utils/src/ui.test.tsx b/packages/snaps-utils/src/ui.test.tsx index a5ead5c379..f8ae588b72 100644 --- a/packages/snaps-utils/src/ui.test.tsx +++ b/packages/snaps-utils/src/ui.test.tsx @@ -983,7 +983,7 @@ describe('validateJsxElements', () => { hasPermission: jest.fn().mockReturnValue(false), }), ).toThrow( - 'Using external images is only permitted with the network access endowment.', + 'Using external images is only permitted with the `endowment:network-access` permission.', ); }); }); From 41c756297a8e022f2f665f06417993031329b79e Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Tue, 9 Dec 2025 14:58:29 +0100 Subject: [PATCH 19/20] Add missing mock --- packages/snaps-controllers/src/test-utils/controller.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/snaps-controllers/src/test-utils/controller.tsx b/packages/snaps-controllers/src/test-utils/controller.tsx index 14bb8638a3..51eda543aa 100644 --- a/packages/snaps-controllers/src/test-utils/controller.tsx +++ b/packages/snaps-controllers/src/test-utils/controller.tsx @@ -831,6 +831,13 @@ export const getRestrictedSnapInterfaceControllerMessenger = ( messenger.registerActionHandler('SnapController:get', (snapId: string) => { return getSnapObject({ id: snapId as SnapId }); }); + + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => { + return true; + }, + ); } jest.spyOn(snapInterfaceControllerMessenger, 'call'); From 394d7ed31a9bb042a968353cad4cc1bec0f07ef4 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Tue, 9 Dec 2025 15:12:59 +0100 Subject: [PATCH 20/20] Stop collecting coverage from test-utils --- jest.config.base.js | 1 + packages/snaps-controllers/coverage.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/jest.config.base.js b/jest.config.base.js index a5f902bad2..3e1d49b841 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -29,6 +29,7 @@ module.exports = { '!./src/**/*.test.tsx', '!./src/**/*.test.browser.ts', '!./src/test-utils/**/*.ts', + '!./src/test-utils/**/*.tsx', '!./src/**/*.d.ts', '!./src/**/__test__/**', '!./src/**/__mocks__/**', diff --git a/packages/snaps-controllers/coverage.json b/packages/snaps-controllers/coverage.json index 89feea7f15..ba99bfafeb 100644 --- a/packages/snaps-controllers/coverage.json +++ b/packages/snaps-controllers/coverage.json @@ -1,6 +1,6 @@ { - "branches": 94.77, - "functions": 98.03, + "branches": 95.16, + "functions": 99, "lines": 98.63, "statements": 98.43 }