From 3dcc10bd4c26060eb0926bec07faac17d3fa4679 Mon Sep 17 00:00:00 2001 From: Kate Johnson Date: Fri, 19 Apr 2024 13:03:45 -0400 Subject: [PATCH 1/5] feat: added testing site to gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 53646a6..51620fc 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,7 @@ node_modules/ # IntelliJ .idea/ /.idea/ + +# Testing site +site/ +/site/ From fd5237bade3c81559ee86860a070a8e96e176451 Mon Sep 17 00:00:00 2001 From: Kate Johnson Date: Fri, 19 Apr 2024 13:47:36 -0400 Subject: [PATCH 2/5] feat: added isMainnet util and check in validation for ENS --- snap.manifest.json | 2 +- src/index.ts | 8 ++++++-- src/test/setup.ts | 3 +++ src/ui/ui-utils.test.ts | 13 +++++++++++++ src/ui/ui-utils.ts | 32 +++++++++++++++++++++++--------- 5 files changed, 46 insertions(+), 12 deletions(-) diff --git a/snap.manifest.json b/snap.manifest.json index 0f787d3..728cde3 100644 --- a/snap.manifest.json +++ b/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/metamask/snap-watch-only.git" }, "source": { - "shasum": "axNCjj2hVcJFJ9K7BM65w8Av6KqR9F+bVXv9i+mizqg=", + "shasum": "k3uPqA1yqVfGuO9FVLMrJ300SyIkRJzDfrahKC8TgDA=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/src/index.ts b/src/index.ts index c6ddb73..7147d54 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,7 @@ import { originPermissions } from './permissions'; import { getState } from './stateManagement'; import { WatchFormNames } from './ui/components'; import { createInterface, showErrorMessage, showSuccess } from './ui/ui'; -import { validateUserInput } from './ui/ui-utils'; +import { isMainnet, validateUserInput } from './ui/ui-utils'; let keyring: WatchOnlyKeyring; @@ -89,9 +89,13 @@ export const onUserInput: OnUserInputHandler = async ({ id, event }) => { event.name === WatchFormNames.AddressForm ) { const inputValue = event.value[WatchFormNames.AddressInput]; + const onMainnet = await isMainnet(); if (!inputValue) { - await showErrorMessage(id, 'Address or ENS is required'); + const emptyInputMessage = onMainnet + ? 'Address or ENS is required' + : 'Address is required'; + await showErrorMessage(id, emptyInputMessage); return; } diff --git a/src/test/setup.ts b/src/test/setup.ts index 9660aef..118e56b 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -8,6 +8,9 @@ export const TEST_VALUES = { // eslint-disable-next-line import/unambiguous jest.mock('ethers', () => { const BrowserProvider = jest.fn().mockImplementation((_ethereum) => ({ + getNetwork: jest.fn().mockImplementation(async () => { + return Promise.resolve({ chainId: 1 }); + }), getCode: jest.fn().mockImplementation(async (address) => { return Promise.resolve( address === TEST_VALUES.smartContractAddress ? '0x123' : '0x', diff --git a/src/ui/ui-utils.test.ts b/src/ui/ui-utils.test.ts index 6afd1fd..bfa35ec 100644 --- a/src/ui/ui-utils.test.ts +++ b/src/ui/ui-utils.test.ts @@ -93,6 +93,19 @@ describe('UI Utils', () => { message: 'Invalid ENS name', }); }); + + // TODO: Fix this test + it('should return ENS is only supported on Ethereum mainnet message', async () => { + jest.mock('./ui-utils', () => { + return { + isMainnet: jest.fn().mockResolvedValueOnce(false), + }; + }); + const result = await validateUserInput(TEST_VALUES.validEns); + expect(result).toStrictEqual({ + message: 'ENS is only supported on Ethereum mainnet', + }); + }); }); describe('when input is invalid', () => { diff --git a/src/ui/ui-utils.ts b/src/ui/ui-utils.ts index d8ada3c..349772a 100644 --- a/src/ui/ui-utils.ts +++ b/src/ui/ui-utils.ts @@ -10,6 +10,16 @@ export type ValidationResult = { address?: string; }; +/** + * Checks if the network is Ethereum mainnet. + * + * @returns True if the network is Ethereum mainnet, false otherwise. + */ +export async function isMainnet(): Promise { + const provider = new ethers.BrowserProvider(ethereum); + return Number((await provider.getNetwork()).chainId) === 1; +} + /** * Get Ethereum address from ENS name. * @@ -19,8 +29,8 @@ export type ValidationResult = { export const getAddressFromEns = async ( name: string, ): Promise => { - const provider = new ethers.BrowserProvider(ethereum); try { + const provider = new ethers.BrowserProvider(ethereum); return await provider.resolveName(name); } catch (error) { logger.error(`Failed to resolve ENS name '${name}': `, error); @@ -37,8 +47,8 @@ export const getAddressFromEns = async ( export const getEnsFromAddress = async ( address: string, ): Promise => { - const provider = new ethers.BrowserProvider(ethereum); try { + const provider = new ethers.BrowserProvider(ethereum); return await provider.lookupAddress(address); } catch (error) { logger.error(`Failed to lookup ENS name for '${address}': `, error); @@ -75,13 +85,17 @@ export async function validateUserInput( } // ENS Name Resolution else if (input.endsWith('.eth')) { - const address = await getAddressFromEns(input); - // Valid ENS Name - if (address) { - return { message: formatAddress(address), address }; + if (await isMainnet()) { + const address = await getAddressFromEns(input); + // Valid ENS Name + if (address) { + return { message: formatAddress(address), address }; + } + // Invalid ENS Name + return { message: 'Invalid ENS name' }; } - // Invalid ENS Name - return { message: 'Invalid ENS name' }; + // ENS only supported on Ethereum mainnet + return { message: 'ENS is only supported on Ethereum mainnet' }; } // Default case for invalid input return { message: 'Invalid input' }; @@ -122,8 +136,8 @@ export function formatAddress(address: string): string { export async function isSmartContractAddress( address: string, ): Promise { - const provider = new ethers.BrowserProvider(ethereum); try { + const provider = new ethers.BrowserProvider(ethereum); const code = await provider.getCode(address); return code !== '0x' && code !== '0x0'; } catch (error) { From e4716aef3d7a11c30040310a88bc36e00fa3eb83 Mon Sep 17 00:00:00 2001 From: Kate Johnson Date: Fri, 19 Apr 2024 15:46:39 -0400 Subject: [PATCH 3/5] feat: added mainnet specification to form content to conditionally show ENS messages --- snap.manifest.json | 2 +- src/ui/components.ts | 15 +++++++-------- src/ui/content.ts | 7 ++++++- src/ui/ui-utils.test.ts | 15 +++++++++------ src/ui/ui.ts | 10 ++++++---- 5 files changed, 29 insertions(+), 20 deletions(-) diff --git a/snap.manifest.json b/snap.manifest.json index 728cde3..a2ffd49 100644 --- a/snap.manifest.json +++ b/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/metamask/snap-watch-only.git" }, "source": { - "shasum": "k3uPqA1yqVfGuO9FVLMrJ300SyIkRJzDfrahKC8TgDA=", + "shasum": "jDKW45/aCM/oZkMRngeI0G60Dxk96Zsxke3GqioMxgk=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/src/ui/components.ts b/src/ui/components.ts index 2ec3652..deca8ab 100644 --- a/src/ui/components.ts +++ b/src/ui/components.ts @@ -24,9 +24,11 @@ import { import { WATCH_FORM_DESCRIPTION, + WATCH_FORM_DESCRIPTION_MAINNET, WATCH_FORM_HEADER, WATCH_FORM_INPUT_LABEL, WATCH_FORM_INPUT_PLACEHOLDER, + WATCH_FORM_INPUT_PLACEHOLDER_MAINNET, WATCH_FORM_INSTRUCTIONS, } from './content'; @@ -39,16 +41,14 @@ export enum WatchFormNames { /** * Generate the watch form component. * - * @param validationMessage - The validation message to display (if any). + * @param onMainnet - Whether the user is on the mainnet (default: false). * @returns The watch form component to display. */ -export function generateWatchFormComponent( - validationMessage?: string, -): Component { - if (validationMessage) { +export function generateWatchFormComponent(onMainnet?: boolean): Component { + if (onMainnet) { return panel([ heading(WATCH_FORM_HEADER), - text(WATCH_FORM_DESCRIPTION), + text(WATCH_FORM_DESCRIPTION_MAINNET), text(WATCH_FORM_INSTRUCTIONS), form({ name: WatchFormNames.AddressForm, @@ -56,7 +56,7 @@ export function generateWatchFormComponent( input({ name: WatchFormNames.AddressInput, label: WATCH_FORM_INPUT_LABEL, - placeholder: WATCH_FORM_INPUT_PLACEHOLDER, + placeholder: WATCH_FORM_INPUT_PLACEHOLDER_MAINNET, }), button({ variant: ButtonVariant.Primary, @@ -66,7 +66,6 @@ export function generateWatchFormComponent( }), ], }), - text(validationMessage), ]); } return panel([ diff --git a/src/ui/content.ts b/src/ui/content.ts index 48fcb9c..d14a077 100644 --- a/src/ui/content.ts +++ b/src/ui/content.ts @@ -1,6 +1,9 @@ export const WATCH_FORM_HEADER = 'Watch any Ethereum account πŸ‘€'; export const WATCH_FORM_DESCRIPTION = + 'Enter any public address to add an account to watch within MetaMask.'; + +export const WATCH_FORM_DESCRIPTION_MAINNET = 'Enter any public address or ENS name to add an account to watch within MetaMask.'; export const WATCH_FORM_INSTRUCTIONS = @@ -8,5 +11,7 @@ export const WATCH_FORM_INSTRUCTIONS = export const WATCH_FORM_INPUT_LABEL = 'Ethereum address'; -export const WATCH_FORM_INPUT_PLACEHOLDER = +export const WATCH_FORM_INPUT_PLACEHOLDER = 'Enter a public address'; + +export const WATCH_FORM_INPUT_PLACEHOLDER_MAINNET = 'Enter a public address or ENS name'; diff --git a/src/ui/ui-utils.test.ts b/src/ui/ui-utils.test.ts index bfa35ec..202d0fc 100644 --- a/src/ui/ui-utils.test.ts +++ b/src/ui/ui-utils.test.ts @@ -96,12 +96,15 @@ describe('UI Utils', () => { // TODO: Fix this test it('should return ENS is only supported on Ethereum mainnet message', async () => { - jest.mock('./ui-utils', () => { - return { - isMainnet: jest.fn().mockResolvedValueOnce(false), - }; - }); - const result = await validateUserInput(TEST_VALUES.validEns); + jest.mock('./ui-utils', () => ({ + ...jest.requireActual('./ui-utils'), // this line ensures other functions are still accessible as they originally were + isMainnet: jest.fn().mockImplementation(async () => { + console.log('inside isMainnet mock'); + return Promise.resolve(false); + }), + })); + + const result = await validateUserInput('something.eth'); expect(result).toStrictEqual({ message: 'ENS is only supported on Ethereum mainnet', }); diff --git a/src/ui/ui.ts b/src/ui/ui.ts index c80245a..f3ac07d 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -4,6 +4,7 @@ import { generateSuccessMessageComponent, generateWatchFormComponent, } from './components'; +import { isMainnet } from './ui-utils'; /** * Initiate a new interface with the starting screen. @@ -11,10 +12,11 @@ import { * @returns The Snap interface ID. */ export async function createInterface(): Promise { + const onMainnet = await isMainnet(); return await snap.request({ method: 'snap_createInterface', params: { - ui: generateWatchFormComponent(), + ui: generateWatchFormComponent(onMainnet), }, }); } @@ -23,14 +25,14 @@ export async function createInterface(): Promise { * Update the interface with the watch-only form containing an input and a submit button. * * @param id - The Snap interface ID to update. - * @param validationMessage - The validation message to display. */ -export async function showForm(id: string, validationMessage?: string) { +export async function showForm(id: string) { + const onMainnet = await isMainnet(); await snap.request({ method: 'snap_updateInterface', params: { id, - ui: generateWatchFormComponent(validationMessage), + ui: generateWatchFormComponent(onMainnet), }, }); } From 68cd706d7132c061203388748be79a948ee53ab8 Mon Sep 17 00:00:00 2001 From: Kate Johnson Date: Tue, 23 Apr 2024 15:59:48 -0400 Subject: [PATCH 4/5] test: working on debugging 'isMainnet' mock --- src/ui/ui-utils.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/ui-utils.test.ts b/src/ui/ui-utils.test.ts index 202d0fc..0c2ef60 100644 --- a/src/ui/ui-utils.test.ts +++ b/src/ui/ui-utils.test.ts @@ -97,7 +97,7 @@ describe('UI Utils', () => { // TODO: Fix this test it('should return ENS is only supported on Ethereum mainnet message', async () => { jest.mock('./ui-utils', () => ({ - ...jest.requireActual('./ui-utils'), // this line ensures other functions are still accessible as they originally were + ...jest.requireActual('./ui-utils'), isMainnet: jest.fn().mockImplementation(async () => { console.log('inside isMainnet mock'); return Promise.resolve(false); From a5ce047432e01128cc1a57da285d595ab67b196e Mon Sep 17 00:00:00 2001 From: Kate Johnson Date: Wed, 1 May 2024 11:59:47 -0400 Subject: [PATCH 5/5] feat: changed form text to use '*' and same message for all networks and fixed mainnet ENS util test --- snap.manifest.json | 2 +- src/index.ts | 2 +- src/test/setup.ts | 8 +++++--- src/ui/components.ts | 30 +++--------------------------- src/ui/content.ts | 12 +++++------- src/ui/ui-utils.test.ts | 14 +++++--------- src/ui/ui.ts | 7 ++----- 7 files changed, 22 insertions(+), 53 deletions(-) diff --git a/snap.manifest.json b/snap.manifest.json index a2ffd49..08caa22 100644 --- a/snap.manifest.json +++ b/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/metamask/snap-watch-only.git" }, "source": { - "shasum": "jDKW45/aCM/oZkMRngeI0G60Dxk96Zsxke3GqioMxgk=", + "shasum": "9GSsDt2F+C27DmhZdjVz92aTRp6b9NzkWautlxpxLa0=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/src/index.ts b/src/index.ts index 7147d54..e42b827 100644 --- a/src/index.ts +++ b/src/index.ts @@ -89,9 +89,9 @@ export const onUserInput: OnUserInputHandler = async ({ id, event }) => { event.name === WatchFormNames.AddressForm ) { const inputValue = event.value[WatchFormNames.AddressInput]; - const onMainnet = await isMainnet(); if (!inputValue) { + const onMainnet = await isMainnet(); const emptyInputMessage = onMainnet ? 'Address or ENS is required' : 'Address is required'; diff --git a/src/test/setup.ts b/src/test/setup.ts index 118e56b..24c9df3 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -5,12 +5,14 @@ export const TEST_VALUES = { smartContractAddress: '0x0227628f3F023bb0B980b67D528571c95c6DaC1c', }; +export const mockGetNetwork = jest.fn(async () => { + return { chainId: 1 }; +}); + // eslint-disable-next-line import/unambiguous jest.mock('ethers', () => { const BrowserProvider = jest.fn().mockImplementation((_ethereum) => ({ - getNetwork: jest.fn().mockImplementation(async () => { - return Promise.resolve({ chainId: 1 }); - }), + getNetwork: mockGetNetwork, getCode: jest.fn().mockImplementation(async (address) => { return Promise.resolve( address === TEST_VALUES.smartContractAddress ? '0x123' : '0x', diff --git a/src/ui/components.ts b/src/ui/components.ts index deca8ab..1172825 100644 --- a/src/ui/components.ts +++ b/src/ui/components.ts @@ -24,11 +24,10 @@ import { import { WATCH_FORM_DESCRIPTION, - WATCH_FORM_DESCRIPTION_MAINNET, + WATCH_FORM_ENS_DISCLAIMER, WATCH_FORM_HEADER, WATCH_FORM_INPUT_LABEL, WATCH_FORM_INPUT_PLACEHOLDER, - WATCH_FORM_INPUT_PLACEHOLDER_MAINNET, WATCH_FORM_INSTRUCTIONS, } from './content'; @@ -41,33 +40,9 @@ export enum WatchFormNames { /** * Generate the watch form component. * - * @param onMainnet - Whether the user is on the mainnet (default: false). * @returns The watch form component to display. */ -export function generateWatchFormComponent(onMainnet?: boolean): Component { - if (onMainnet) { - return panel([ - heading(WATCH_FORM_HEADER), - text(WATCH_FORM_DESCRIPTION_MAINNET), - text(WATCH_FORM_INSTRUCTIONS), - form({ - name: WatchFormNames.AddressForm, - children: [ - input({ - name: WatchFormNames.AddressInput, - label: WATCH_FORM_INPUT_LABEL, - placeholder: WATCH_FORM_INPUT_PLACEHOLDER_MAINNET, - }), - button({ - variant: ButtonVariant.Primary, - value: 'Watch account', - name: WatchFormNames.SubmitButton, - buttonType: ButtonType.Submit, - }), - ], - }), - ]); - } +export function generateWatchFormComponent(): Component { return panel([ heading(WATCH_FORM_HEADER), text(WATCH_FORM_DESCRIPTION), @@ -88,6 +63,7 @@ export function generateWatchFormComponent(onMainnet?: boolean): Component { }), ], }), + text(WATCH_FORM_ENS_DISCLAIMER), ]); } diff --git a/src/ui/content.ts b/src/ui/content.ts index d14a077..c2fe963 100644 --- a/src/ui/content.ts +++ b/src/ui/content.ts @@ -1,17 +1,15 @@ export const WATCH_FORM_HEADER = 'Watch any Ethereum account πŸ‘€'; export const WATCH_FORM_DESCRIPTION = - 'Enter any public address to add an account to watch within MetaMask.'; - -export const WATCH_FORM_DESCRIPTION_MAINNET = - 'Enter any public address or ENS name to add an account to watch within MetaMask.'; + 'Enter any public address or *ENS name to add an account to watch within MetaMask.'; export const WATCH_FORM_INSTRUCTIONS = 'The watched accounts will be listed alongside the rest of your accounts in a safe, watch-only mode. Remember, you can look but you can’t sign or transact.'; export const WATCH_FORM_INPUT_LABEL = 'Ethereum address'; -export const WATCH_FORM_INPUT_PLACEHOLDER = 'Enter a public address'; - -export const WATCH_FORM_INPUT_PLACEHOLDER_MAINNET = +export const WATCH_FORM_INPUT_PLACEHOLDER = 'Enter a public address or ENS name'; + +export const WATCH_FORM_ENS_DISCLAIMER = + '*ENS names are only supported on Ethereum mainnet'; diff --git a/src/ui/ui-utils.test.ts b/src/ui/ui-utils.test.ts index 0c2ef60..a127788 100644 --- a/src/ui/ui-utils.test.ts +++ b/src/ui/ui-utils.test.ts @@ -3,7 +3,7 @@ import { isSmartContractAddress, validateUserInput, } from './ui-utils'; -import { TEST_VALUES } from '../test/setup'; +import { mockGetNetwork, TEST_VALUES } from '../test/setup'; // @ts-expect-error Mocking ethereum global object global.ethereum = { @@ -94,15 +94,11 @@ describe('UI Utils', () => { }); }); - // TODO: Fix this test it('should return ENS is only supported on Ethereum mainnet message', async () => { - jest.mock('./ui-utils', () => ({ - ...jest.requireActual('./ui-utils'), - isMainnet: jest.fn().mockImplementation(async () => { - console.log('inside isMainnet mock'); - return Promise.resolve(false); - }), - })); + // Override getNetwork to return a non-mainnet chainId here + mockGetNetwork.mockImplementationOnce(async () => { + return { chainId: 59144 }; // Custom or test network chain ID + }); const result = await validateUserInput('something.eth'); expect(result).toStrictEqual({ diff --git a/src/ui/ui.ts b/src/ui/ui.ts index f3ac07d..74c2e6c 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -4,7 +4,6 @@ import { generateSuccessMessageComponent, generateWatchFormComponent, } from './components'; -import { isMainnet } from './ui-utils'; /** * Initiate a new interface with the starting screen. @@ -12,11 +11,10 @@ import { isMainnet } from './ui-utils'; * @returns The Snap interface ID. */ export async function createInterface(): Promise { - const onMainnet = await isMainnet(); return await snap.request({ method: 'snap_createInterface', params: { - ui: generateWatchFormComponent(onMainnet), + ui: generateWatchFormComponent(), }, }); } @@ -27,12 +25,11 @@ export async function createInterface(): Promise { * @param id - The Snap interface ID to update. */ export async function showForm(id: string) { - const onMainnet = await isMainnet(); await snap.request({ method: 'snap_updateInterface', params: { id, - ui: generateWatchFormComponent(onMainnet), + ui: generateWatchFormComponent(), }, }); }