Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ens resolution only supported on mainnet #29

Merged
merged 5 commits into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,7 @@ node_modules/
# IntelliJ
.idea/
/.idea/

# Testing site
site/
/site/
2 changes: 1 addition & 1 deletion snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/metamask/snap-watch-only.git"
},
"source": {
"shasum": "axNCjj2hVcJFJ9K7BM65w8Av6KqR9F+bVXv9i+mizqg=",
"shasum": "9GSsDt2F+C27DmhZdjVz92aTRp6b9NzkWautlxpxLa0=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
8 changes: 6 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -91,7 +91,11 @@ export const onUserInput: OnUserInputHandler = async ({ id, event }) => {
const inputValue = event.value[WatchFormNames.AddressInput];

if (!inputValue) {
await showErrorMessage(id, 'Address or ENS is required');
const onMainnet = await isMainnet();
const emptyInputMessage = onMainnet
? 'Address or ENS is required'
: 'Address is required';
await showErrorMessage(id, emptyInputMessage);
return;
}

Expand Down
5 changes: 5 additions & 0 deletions src/test/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +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: mockGetNetwork,
getCode: jest.fn().mockImplementation(async (address) => {
return Promise.resolve(
address === TEST_VALUES.smartContractAddress ? '0x123' : '0x',
Expand Down
31 changes: 3 additions & 28 deletions src/ui/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {

import {
WATCH_FORM_DESCRIPTION,
WATCH_FORM_ENS_DISCLAIMER,
WATCH_FORM_HEADER,
WATCH_FORM_INPUT_LABEL,
WATCH_FORM_INPUT_PLACEHOLDER,
Expand All @@ -39,36 +40,9 @@ export enum WatchFormNames {
/**
* Generate the watch form component.
*
* @param validationMessage - The validation message to display (if any).
* @returns The watch form component to display.
*/
export function generateWatchFormComponent(
validationMessage?: string,
): Component {
if (validationMessage) {
return panel([
heading(WATCH_FORM_HEADER),
text(WATCH_FORM_DESCRIPTION),
text(WATCH_FORM_INSTRUCTIONS),
form({
name: WatchFormNames.AddressForm,
children: [
input({
name: WatchFormNames.AddressInput,
label: WATCH_FORM_INPUT_LABEL,
placeholder: WATCH_FORM_INPUT_PLACEHOLDER,
}),
button({
variant: ButtonVariant.Primary,
value: 'Watch account',
name: WatchFormNames.SubmitButton,
buttonType: ButtonType.Submit,
}),
],
}),
text(validationMessage),
]);
}
export function generateWatchFormComponent(): Component {
return panel([
heading(WATCH_FORM_HEADER),
text(WATCH_FORM_DESCRIPTION),
Expand All @@ -89,6 +63,7 @@ export function generateWatchFormComponent(
}),
],
}),
text(WATCH_FORM_ENS_DISCLAIMER),
]);
}

Expand Down
5 changes: 4 additions & 1 deletion src/ui/content.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const WATCH_FORM_HEADER = 'Watch any Ethereum account 👀';

export const WATCH_FORM_DESCRIPTION =
'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.';
Expand All @@ -10,3 +10,6 @@ export const WATCH_FORM_INPUT_LABEL = 'Ethereum address';

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';
14 changes: 13 additions & 1 deletion src/ui/ui-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -93,6 +93,18 @@ describe('UI Utils', () => {
message: 'Invalid ENS name',
});
});

it('should return ENS is only supported on Ethereum mainnet message', async () => {
// 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({
message: 'ENS is only supported on Ethereum mainnet',
});
});
});

describe('when input is invalid', () => {
Expand Down
32 changes: 23 additions & 9 deletions src/ui/ui-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> {
const provider = new ethers.BrowserProvider(ethereum);
return Number((await provider.getNetwork()).chainId) === 1;
}

/**
* Get Ethereum address from ENS name.
*
Expand All @@ -19,8 +29,8 @@ export type ValidationResult = {
export const getAddressFromEns = async (
name: string,
): Promise<string | null> => {
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);
Expand All @@ -37,8 +47,8 @@ export const getAddressFromEns = async (
export const getEnsFromAddress = async (
address: string,
): Promise<string | null> => {
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);
Expand Down Expand Up @@ -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' };
Expand Down Expand Up @@ -122,8 +136,8 @@ export function formatAddress(address: string): string {
export async function isSmartContractAddress(
address: string,
): Promise<boolean> {
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) {
Expand Down
5 changes: 2 additions & 3 deletions src/ui/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,13 @@ export async function createInterface(): Promise<string> {
* 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) {
await snap.request({
method: 'snap_updateInterface',
params: {
id,
ui: generateWatchFormComponent(validationMessage),
ui: generateWatchFormComponent(),
},
});
}
Expand Down
Loading