Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

feat: allow signing typed data #191

Merged
merged 11 commits into from
Nov 24, 2022
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
47 changes: 43 additions & 4 deletions src/helpers/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,23 @@ export const openNetworkDropdown = async (
await networkSwitcher.click();
await page.waitForSelector(".network-dropdown-list", {
visible: true,
timeout: 3000,
timeout: 4000,
});
}
};

export const openProfileDropdown = async (
page: DappeteerPage
export const profileDropdownClick = async (
page: DappeteerPage,
expectToClose = false
): Promise<void> => {
const accountSwitcher = await page.waitForSelector(".identicon", {
const accountSwitcher = await page.waitForSelector(".account-menu__icon", {
visible: true,
});
await page.waitForTimeout(500);
await accountSwitcher.click();
await page.waitForSelector(".account-menu__accounts", {
hidden: expectToClose,
});
};

export const openAccountDropdown = async (
Expand Down Expand Up @@ -112,3 +117,37 @@ export const typeOnInputField = async (
await input.type(text);
return true;
};

/**
*
* @param page
*/
export const clickOnLittleDownArrowIfNeeded = async (
page: DappeteerPage
): Promise<void> => {
// wait for the signature page and content to be loaded
await page.waitForSelector('[data-testid="signature-cancel-button"]', {
visible: true,
});

// Metamask requires users to read all the data
// and scroll until the bottom of the message
// before enabling the "Sign" button
const isSignButtonDisabled = await page.$eval(
'[data-testid="signature-sign-button"]',
(button: HTMLButtonElement) => {
return button.disabled;
}
);

if (isSignButtonDisabled) {
const littleArrowDown = await page.waitForSelector(
".signature-request-message__scroll-button",
{
visible: true,
}
);

await littleArrowDown.click();
}
};
7 changes: 7 additions & 0 deletions src/metamask/addNetwork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@ export const acceptAddNetwork =
async (shouldSwitch = false): Promise<void> => {
await page.bringToFront();
await page.reload();
await page.waitForSelector(".confirmation-footer", {
visible: true,
});
await clickOnButton(page, "Approve");
if (shouldSwitch) {
await clickOnButton(page, "Switch network");
await page.waitForSelector(".new-network-info__wrapper", {
visible: true,
});
await clickOnButton(page, "Got it");
} else {
await clickOnButton(page, "Cancel");
}
Expand Down
4 changes: 2 additions & 2 deletions src/metamask/importPk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
clickOnButton,
clickOnElement,
getErrorMessage,
openProfileDropdown,
profileDropdownClick,
typeOnInputField,
} from "../helpers";
import { DappeteerPage } from "../page";
Expand All @@ -11,7 +11,7 @@ export const importPk =
(page: DappeteerPage) =>
async (privateKey: string): Promise<void> => {
await page.bringToFront();
await openProfileDropdown(page);
await profileDropdownClick(page);

await clickOnElement(page, "Import account");
await typeOnInputField(page, "your private key", privateKey);
Expand Down
2 changes: 2 additions & 0 deletions src/metamask/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { deleteAccount, deleteNetwork, getTokenBalance } from "./helpers";
import { importPk } from "./importPk";
import { lock } from "./lock";
import { sign } from "./sign";
import { signTypedData } from "./signTypedData";
import { switchAccount } from "./switchAccount";
import { switchNetwork } from "./switchNetwork";
import { unlock } from "./unlock";
Expand Down Expand Up @@ -46,6 +47,7 @@ export const getMetaMask = (page: DappeteerPage): Promise<Dappeteer> => {
importPK: importPk(page),
lock: lock(page, setSignedIn, getSingedIn),
sign: sign(page, getSingedIn),
signTypedData: signTypedData(page, getSingedIn),
switchAccount: switchAccount(page),
switchNetwork: switchNetwork(page),
unlock: unlock(page, setSignedIn, getSingedIn),
Expand Down
4 changes: 2 additions & 2 deletions src/metamask/lock.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { clickOnButton, openProfileDropdown } from "../helpers";
import { clickOnButton, profileDropdownClick } from "../helpers";
import { DappeteerPage } from "../page";

import { GetSingedIn, SetSignedIn } from "./index";
Expand All @@ -11,7 +11,7 @@ export const lock =
}
await page.bringToFront();

await openProfileDropdown(page);
await profileDropdownClick(page);
await clickOnButton(page, "Lock");

await setSignedIn(false);
Expand Down
5 changes: 5 additions & 0 deletions src/metamask/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,9 @@ export const sign =
await page.reload();

await clickOnButton(page, "Sign");

// wait for MM to be back in a stable state
await page.waitForSelector(".app-header", {
visible: true,
});
};
20 changes: 20 additions & 0 deletions src/metamask/signTypedData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { clickOnButton, clickOnLittleDownArrowIfNeeded } from "../helpers";

import { DappeteerPage } from "../page";
import { GetSingedIn } from ".";

export const signTypedData =
(page: DappeteerPage, getSingedIn: GetSingedIn) =>
async (): Promise<void> => {
await page.bringToFront();
if (!(await getSingedIn())) {
throw new Error("You haven't signed in yet");
}
await page.reload();
await clickOnLittleDownArrowIfNeeded(page);
await clickOnButton(page, "Sign");
// wait for MM to be back in a stable state
await page.waitForSelector(".app-header", {
visible: true,
});
};
4 changes: 2 additions & 2 deletions src/metamask/switchAccount.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { clickOnElement, openProfileDropdown } from "../helpers";
import { clickOnElement, profileDropdownClick } from "../helpers";
import { DappeteerPage } from "../page";

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const switchAccount =
(page: DappeteerPage) =>
async (accountNumber: number): Promise<void> => {
await page.bringToFront();
await openProfileDropdown(page);
await profileDropdownClick(page);

// TODO: use different approach? maybe change param to account name
await clickOnElement(page, `Account ${accountNumber}`);
Expand Down
2 changes: 1 addition & 1 deletion src/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export interface DappeteerPage<P = unknown> {
): Promise<Response>;
waitForSelector(
selector: string,
opts?: Partial<{ visible: boolean; timeout: number }>
opts?: Partial<{ visible: boolean; timeout: number; hidden: boolean }>
Tbaut marked this conversation as resolved.
Show resolved Hide resolved
): Promise<DappeteerElementHandle>;
waitForXPath(
xpath: string,
Expand Down
4 changes: 2 additions & 2 deletions src/playwright/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,12 @@ export class DPlaywrightPage implements DappeteerPage<Page> {

async waitForSelector(
selector: string,
opts?: Partial<{ visible: boolean; timeout: number }>
opts?: Partial<{ visible: boolean; timeout: number; hidden: boolean }>
): Promise<DappeteerElementHandle<ElementHandle<HTMLElement>>> {
return new DPlaywrightElementHandle(
(await this.page.waitForSelector(selector, {
timeout: opts?.timeout,
state: opts?.visible ? "visible" : "attached",
state: opts?.visible ? "visible" : opts?.hidden ? "hidden" : "attached",
})) as ElementHandle<HTMLElement>
);
}
Expand Down
3 changes: 3 additions & 0 deletions src/puppeteer/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ export class DPuppeteerBrowser
this.emit("targetcreated", page)
);
}

wsEndpoint(): string {
return this.browser.wsEndpoint();
}

async close(): Promise<void> {
await this.browser.close();
}
Expand All @@ -26,6 +28,7 @@ export class DPuppeteerBrowser
return new DPupeteerPage(p, this) as DappeteerPage<Page>;
});
}

async newPage(): Promise<DappeteerPage<Page>> {
return new DPupeteerPage(
await this.browser.newPage(),
Expand Down
2 changes: 1 addition & 1 deletion src/puppeteer/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export class DPupeteerPage implements DappeteerPage<Page> {

async waitForSelector(
selector: string,
opts?: Partial<{ visible: boolean; timeout: number }>
opts?: Partial<{ visible: boolean; timeout: number; hidden: boolean }>
): Promise<DappeteerElementHandle<ElementHandle<HTMLElement>>> {
return new DPuppeteerElementHandle(
(await this.page.waitForSelector(
Expand Down
8 changes: 5 additions & 3 deletions src/setup/setupMetaMask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const flaskMetaMaskSteps: Step<MetaMaskOptions>[] = [
closeWhatsNewModal,
];

const MM_HOME_REGEX = "chrome-extension://[a-z]+/home.html";

export async function setupMetaMask<Options = MetaMaskOptions>(
browser: DappeteerBrowser,
options?: Options,
Expand All @@ -62,19 +64,19 @@ async function getMetamaskPage(
): Promise<DappeteerPage> {
const pages = await browser.pages();
for (const page of pages) {
if (page.url().match("chrome-extension://[a-z]+/home.html")) {
if (page.url().match(MM_HOME_REGEX)) {
return page;
}
}
return new Promise((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
browser.on("targetcreated", async (target: any) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
if (target.url().match("chrome-extension://[a-z]+/home.html")) {
if (target.url().match(MM_HOME_REGEX)) {
try {
const pages = await browser.pages();
for (const page of pages) {
if (page.url().match("chrome-extension://[a-z]+/home.html")) {
if (page.url().match(MM_HOME_REGEX)) {
resolve(page);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/snap/getAllNotifications.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { clickOnElement, openProfileDropdown } from "../helpers";
import { clickOnElement, profileDropdownClick } from "../helpers";
import { DappeteerPage } from "../page";
import { NotificationList } from "./types";

export const getAllNotifications =
(page: DappeteerPage) => async (): Promise<NotificationList> => {
await page.bringToFront();
await openProfileDropdown(page);
await profileDropdownClick(page);
await clickOnElement(page, "Notifications");
await page.waitForSelector(".notifications__item__details__message");
const notificationList: NotificationList = await page.$$eval(
Expand Down
4 changes: 2 additions & 2 deletions src/snap/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
clickOnButton,
clickOnElement,
clickOnLogo,
openProfileDropdown,
profileDropdownClick,
} from "../helpers";
import { DappeteerPage } from "../page";
import { startSnapServer, toUrl } from "./install-utils";
Expand Down Expand Up @@ -89,7 +89,7 @@ export async function isSnapInstalled(
snapId: string
): Promise<boolean> {
await page.bringToFront();
await openProfileDropdown(page);
await profileDropdownClick(page);

await clickOnElement(page, "Settings");
await clickOnElement(page, "Snaps");
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type Dappeteer = {
switchNetwork: (network: string) => Promise<void>;
confirmTransaction: (options?: TransactionOptions) => Promise<void>;
sign: () => Promise<void>;
signTypedData: () => Promise<void>;
Tbaut marked this conversation as resolved.
Show resolved Hide resolved
approve: () => Promise<void>;
helpers: {
getTokenBalance: (tokenSymbol: string) => Promise<number>;
Expand Down
31 changes: 22 additions & 9 deletions test/basic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect, use } from "chai";
import chaiAsPromised from "chai-as-promised";

import * as dappeteer from "../src";
import { openProfileDropdown } from "../src/helpers";
import { profileDropdownClick } from "../src/helpers";
import { DappeteerPage } from "../src/page";

import { PASSWORD, TestContext } from "./constant";
Expand All @@ -28,10 +28,6 @@ describe("basic interactions", function () {
}
});

afterEach(async function () {
await metamask.page.reload();
});

after(async function () {
await testPage.close();
});
Expand All @@ -44,6 +40,24 @@ describe("basic interactions", function () {
await testPage.waitForSelector("#signed", { visible: false });
});

it("should be able to sign typed data", async () => {
await clickElement(testPage, ".sign-typedData-button");

await metamask.signTypedData();

await testPage.waitForSelector("#signed-typedData", { visible: false });
});

it("should be able to sign short typed data", async () => {
await clickElement(testPage, ".sign-short-typedData-button");

await metamask.signTypedData();

await testPage.waitForSelector("#signed-short-typedData", {
visible: false,
});
});

it("should switch network", async () => {
await metamask.switchNetwork("localhost");

Expand Down Expand Up @@ -89,18 +103,17 @@ describe("basic interactions", function () {

it("should add network and switch", async () => {
await clickElement(testPage, ".add-network-button");
await metamask.page.waitForTimeout(500);
await metamask.page.waitForTimeout(1000);
await metamask.acceptAddNetwork(true);
await testPage.waitForSelector("#addNetworkResultSuccess");
await metamask.switchNetwork("mainnet");
});

it("should import private key", async () => {
const countAccounts = async (): Promise<number> => {
await openProfileDropdown(metamask.page);
await profileDropdownClick(metamask.page, false);
const container = await metamask.page.$(".account-menu__accounts");
const count = (await container.$$(".account-menu__account")).length;
await openProfileDropdown(metamask.page);
await profileDropdownClick(metamask.page, true);
return count;
};

Expand Down
5 changes: 5 additions & 0 deletions test/dapp/index.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Local MetaMask test</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="web3.min.js"></script>
</head>

<body>
<button class="connect-button">Connect</button>
<button class="sign-button">Sign</button>
<button class="sign-typedData-button">Sign Typed Data</button>
<button class="sign-short-typedData-button">Sign Short Typed Data</button>
<button class="increase-button">Increase</button>
<button class="increase-fees-button">Increase Fees</button>
<button class="transfer-button">Transfer</button>
Expand All @@ -19,4 +23,5 @@
<script src="data.js"></script>
<script src="main.js"></script>
</body>

</html>
Loading