Skip to content

Commit 283252f

Browse files
authored
fix: TokenIcon runtime type enforcement (#155)
* fix: TokenIcon runtime type enforcement
1 parent e9029aa commit 283252f

File tree

4 files changed

+152
-10
lines changed

4 files changed

+152
-10
lines changed

packages/gator-permissions-snap/snap.manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"url": "https://github.com/MetaMask/snap-7715-permissions.git"
88
},
99
"source": {
10-
"shasum": "DK0xWacmpY2VdHt48Tn5iw8Rpivla7dufa88PvDvPCw=",
10+
"shasum": "VduGx+y+jERmJB9UXFPLYo4xfSv4HDOCX9091+ueuqU=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",

packages/gator-permissions-snap/src/ui/components/TokenIcon.tsx

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import {
2+
extractZodError,
3+
logger,
4+
} from '@metamask/7715-permissions-shared/utils';
15
import type { SnapComponent } from '@metamask/snaps-sdk/jsx';
26
import { Image, Text } from '@metamask/snaps-sdk/jsx';
7+
import { z } from 'zod';
38

49
export type TokenIconParams = {
510
imageDataBase64: string | null;
@@ -8,17 +13,39 @@ export type TokenIconParams = {
813
height?: number;
914
};
1015

11-
export const TokenIcon: SnapComponent<TokenIconParams> = ({
12-
imageDataBase64,
13-
altText,
14-
width = 24,
15-
height = 24,
16-
}) => {
17-
if (!imageDataBase64) {
16+
const MAX_ICON_SIZE = 512;
17+
18+
// zod schema for runtime validation
19+
const TokenIconParamsSchema = z.object({
20+
imageDataBase64: z
21+
.string()
22+
.regex(
23+
/^data:image\/png;base64,[A-Za-z0-9+/=]+$/u,
24+
'Must be a valid PNG base64 data URI',
25+
),
26+
altText: z.string().default(''),
27+
width: z.number().int().positive().max(MAX_ICON_SIZE).default(24),
28+
height: z.number().int().positive().max(MAX_ICON_SIZE).default(24),
29+
});
30+
31+
export const TokenIcon: SnapComponent<TokenIconParams> = (props) => {
32+
if (!props.imageDataBase64) {
1833
return <Text> </Text>;
1934
}
2035

21-
const imageSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
36+
const parseResult = TokenIconParamsSchema.safeParse(props);
37+
38+
if (!parseResult.success) {
39+
logger.warn(
40+
'TokenIcon: Invalid parameters',
41+
extractZodError(parseResult.error.errors),
42+
);
43+
return <Text> </Text>;
44+
}
45+
46+
const { imageDataBase64, altText, width, height } = parseResult.data;
47+
48+
const imageSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
2249
<image href="${imageDataBase64}" width="${width}" height="${height}" />
2350
</svg>`;
2451

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { describe, expect, it, jest } from '@jest/globals';
2+
3+
import { TokenIcon } from '../../../src/ui/components/TokenIcon';
4+
5+
// Mock the logger and extractZodError utilities
6+
jest.mock('@metamask/7715-permissions-shared/utils', () => ({
7+
logger: {
8+
warn: jest.fn(),
9+
},
10+
extractZodError: jest.fn((errors) => errors),
11+
}));
12+
13+
describe('TokenIcon', () => {
14+
const validBase64Image =
15+
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==';
16+
17+
it('should return empty Text when imageDataBase64 is null', () => {
18+
const result = TokenIcon({
19+
imageDataBase64: null,
20+
altText: 'Test Alt Text',
21+
});
22+
23+
expect(result).toStrictEqual(
24+
expect.objectContaining({
25+
type: 'Text',
26+
props: { children: ' ' },
27+
}),
28+
);
29+
});
30+
31+
it('should render Image with valid base64 PNG data URI', () => {
32+
const result = TokenIcon({
33+
imageDataBase64: validBase64Image,
34+
altText: 'Test PNG',
35+
});
36+
37+
const expectedSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
38+
<image href="${validBase64Image}" width="24" height="24" />
39+
</svg>`;
40+
41+
expect(result).toStrictEqual(
42+
expect.objectContaining({
43+
type: 'Image',
44+
props: { src: expectedSvg, alt: 'Test PNG' },
45+
}),
46+
);
47+
});
48+
49+
it('should use custom width and height when provided', () => {
50+
const result = TokenIcon({
51+
imageDataBase64: validBase64Image,
52+
altText: 'Custom Size',
53+
width: 48,
54+
height: 48,
55+
});
56+
57+
const expectedSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
58+
<image href="${validBase64Image}" width="48" height="48" />
59+
</svg>`;
60+
61+
expect(result).toStrictEqual(
62+
expect.objectContaining({
63+
type: 'Image',
64+
props: { src: expectedSvg, alt: 'Custom Size' },
65+
}),
66+
);
67+
});
68+
69+
it('should return empty Text for invalid data URI format', () => {
70+
const result = TokenIcon({
71+
imageDataBase64: 'invalid-data-uri',
72+
altText: 'Invalid',
73+
});
74+
75+
expect(result).toStrictEqual(
76+
expect.objectContaining({
77+
type: 'Text',
78+
props: { children: ' ' },
79+
}),
80+
);
81+
});
82+
83+
it('should return empty Text for non-PNG image types', () => {
84+
const jpegImage =
85+
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8A8A';
86+
87+
const result = TokenIcon({
88+
imageDataBase64: jpegImage,
89+
altText: 'JPEG',
90+
});
91+
92+
expect(result).toStrictEqual(
93+
expect.objectContaining({
94+
type: 'Text',
95+
props: { children: ' ' },
96+
}),
97+
);
98+
});
99+
100+
it('should return empty Text when dimensions exceed maximum', () => {
101+
const result = TokenIcon({
102+
imageDataBase64: validBase64Image,
103+
altText: 'Too Large',
104+
width: 1000,
105+
height: 1000,
106+
});
107+
108+
expect(result).toStrictEqual(
109+
expect.objectContaining({
110+
type: 'Text',
111+
props: { children: ' ' },
112+
}),
113+
);
114+
});
115+
});

packages/permissions-kernel-snap/snap.manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"url": "https://github.com/MetaMask/snap-7715-permissions.git"
88
},
99
"source": {
10-
"shasum": "BbJKtMNO27QngXRYsexD21RKiiRxDrtS+/XoFXjBEjA=",
10+
"shasum": "NScg2w2IdLkNq/HcCtnPtp/EFpn/QCTCfwgurVyI/kg=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",

0 commit comments

Comments
 (0)