Skip to content
This repository was 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
30 changes: 30 additions & 0 deletions src/helpers/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,33 @@ export const typeOnInputField = async (
await input.type(text);
return true;
};

/**
*
* @param page
*/
export const clickOnLittleDownArrowIfNeeded = async (
page: DappeteerPage
): Promise<void> => {
// 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();
}
};
3 changes: 2 additions & 1 deletion src/metamask/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { confirmTransaction } from "./confirmTransaction";
import { deleteAccount, deleteNetwork, getTokenBalance } from "./helpers";
import { importPk } from "./importPk";
import { lock } from "./lock";
import { sign } from "./sign";
import { sign, signTypedData } from "./sign";
import { switchAccount } from "./switchAccount";
import { switchNetwork } from "./switchNetwork";
import { unlock } from "./unlock";
Expand Down Expand Up @@ -45,6 +45,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
16 changes: 15 additions & 1 deletion src/metamask/sign.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { clickOnButton } from "../helpers";
import { clickOnButton, clickOnLittleDownArrowIfNeeded } from "../helpers";

import { DappeteerPage } from "../page";
import { GetSingedIn } from ".";
Expand All @@ -16,3 +16,17 @@ export const sign =

await clickOnButton(page, "Sign");
};

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 page.waitForTimeout(300);
await clickOnLittleDownArrowIfNeeded(page);
await page.waitForTimeout(300);
await clickOnButton(page, "Sign");
};
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export type Dappeteer = {
switchNetwork: (network: string) => Promise<void>;
confirmTransaction: (options?: TransactionOptions) => Promise<void>;
sign: () => Promise<void>;
signTypedData: () => Promise<void>;
approve: () => Promise<void>;
helpers: {
getTokenBalance: (tokenSymbol: string) => Promise<number>;
Expand Down
18 changes: 18 additions & 0 deletions test/basic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,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
5 changes: 5 additions & 0 deletions test/dapp/index.html
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
<!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>

<script src="data.js"></script>
<script src="main.js"></script>
</body>

</html>
194 changes: 171 additions & 23 deletions test/dapp/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,194 @@ async function start() {

const web3 = new Web3(window.ethereum);
console.log(web3);
const counterContract = new web3.eth.Contract(ContractInfo.abi, ContractInfo.address);
const counterContract = new web3.eth.Contract(
ContractInfo.abi,
ContractInfo.address
);

const increaseButton = document.querySelector('.increase-button');
increaseButton.addEventListener('click', async function () {
const increaseButton = document.querySelector(".increase-button");
increaseButton.addEventListener("click", async function () {
await counterContract.methods.increase().send({ from: accounts[0] });
const txSent = document.createElement('div');
txSent.id = 'txSent';
const txSent = document.createElement("div");
txSent.id = "txSent";
document.body.appendChild(txSent);
});

const increaseFeesButton = document.querySelector('.increase-fees-button');
increaseFeesButton.addEventListener('click', async function () {
const increaseFeesButton = document.querySelector(".increase-fees-button");
increaseFeesButton.addEventListener("click", async function () {
await counterContract.methods.increase().send({ from: accounts[0] });
const txSent = document.createElement('div');
txSent.id = 'feesTxSent';
const txSent = document.createElement("div");
txSent.id = "feesTxSent";
document.body.appendChild(txSent);
});

const connectButton = document.querySelector('.connect-button');
connectButton.addEventListener('click', async function () {
const connectButton = document.querySelector(".connect-button");
connectButton.addEventListener("click", async function () {
accounts = await web3.eth.requestAccounts();
const connected = document.createElement('div');
connected.id = 'connected';
const connected = document.createElement("div");
connected.id = "connected";
document.body.appendChild(connected);
});

const signButton = document.querySelector('.sign-button');
signButton.addEventListener('click', async function () {
const message = web3.utils.sha3('TEST');
const signButton = document.querySelector(".sign-button");
signButton.addEventListener("click", async function () {
const message = web3.utils.sha3("TEST");
await web3.eth.sign(message, accounts[0]);
const signed = document.createElement('div');
signed.id = 'signed';
const signed = document.createElement("div");
signed.id = "signed";
document.body.appendChild(signed);
});

const transferButton = document.querySelector('.transfer-button');
transferButton.addEventListener('click', async function () {
await web3.eth.sendTransaction({ to: accounts[0], from: accounts[0], value: web3.utils.toWei('0.01') });
const transfer = document.createElement('div');
transfer.id = 'transferred';
const signTypedDataButton = document.querySelector(".sign-typedData-button");
signTypedDataButton.addEventListener("click", async function () {
const msgParams = JSON.stringify({
domain: {
// Defining the chain aka Rinkeby testnet or Ethereum Main Net
chainId: 1,
// Give a user friendly name to the specific contract you are signing for.
name: "Ether Mail",
// If name isn't enough add verifying contract to make sure you are establishing contracts with the proper entity
verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
// Just let's you know the latest version. Definitely make sure the field name is correct.
version: "1",
},

// Defining the message signing data content.
message: {
/*
- Anything you want. Just a JSON Blob that encodes the data you want to send
- No required fields
- This is DApp Specific
- Be as explicit as possible when building out the message schema.
*/
contents: "Hello, Bob!",
attachedMoneyInEth: 4.2,
from: {
name: "Cow",
wallets: [
"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826",
"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF",
],
},
to: [
{
name: "Bob",
wallets: [
"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB",
"0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57",
"0xB0B0b0b0b0b0B000000000000000000000000000",
],
},
],
},
// Refers to the keys of the *types* object below.
primaryType: "Mail",
types: {
// TODO: Clarify if EIP712Domain refers to the domain the contract is hosted on
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
],
// Not an EIP712Domain definition
Group: [
{ name: "name", type: "string" },
{ name: "members", type: "Person[]" },
],
// Refer to PrimaryType
Mail: [
{ name: "from", type: "Person" },
{ name: "to", type: "Person[]" },
{ name: "contents", type: "string" },
],
// Not an EIP712Domain definition
Person: [
{ name: "name", type: "string" },
{ name: "wallets", type: "address[]" },
],
},
});

var params = [accounts[0], msgParams];
var method = "eth_signTypedData_v4";

web3.currentProvider.sendAsync(
{
method,
params,
from: accounts[0],
},
function (err, result) {
if (err) return;
if (result.error) {
console.error(result.error.message);
}
const signed = document.createElement("div");
signed.id = "signed-typedData";
document.body.appendChild(signed);
}
);
});

const signShortTypedDataButton = document.querySelector(
".sign-short-typedData-button"
);
signShortTypedDataButton.addEventListener("click", async function () {
const msgParams = JSON.stringify({
domain: {
chainId: 1,
name: "Ether Mail",
verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
version: "1",
},

// Defining the message signing data content.
message: {
contents: "Hello, Bob!",
},
primaryType: "Mail",
types: {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
],
Mail: [{ name: "contents", type: "string" }],
},
});

var params = [accounts[0], msgParams];
var method = "eth_signTypedData_v4";

web3.currentProvider.sendAsync(
{
method,
params,
from: accounts[0],
},
function (err, result) {
if (err) return;
if (result.error) {
console.error(result.error.message);
}
const signed = document.createElement("div");
signed.id = "signed-short-typedData";
document.body.appendChild(signed);
}
);
});

const transferButton = document.querySelector(".transfer-button");
transferButton.addEventListener("click", async function () {
await web3.eth.sendTransaction({
to: accounts[0],
from: accounts[0],
value: web3.utils.toWei("0.01"),
});
const transfer = document.createElement("div");
transfer.id = "transferred";
document.body.appendChild(transfer);
});
}
Expand Down