Skip to content

Commit

Permalink
✨ feat(metamask): Add support for eth_signTypedData_(v3|v4)
Browse files Browse the repository at this point in the history
  • Loading branch information
duckception committed Nov 14, 2023
1 parent 1efba09 commit 70e74a8
Show file tree
Hide file tree
Showing 10 changed files with 204 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { chromium } from 'playwright-core'
import type { WalletSetupFunction } from '../defineWalletSetup'
import { waitForExtensionOnLoadPage } from './waitForExtensionOnLoadPage'

// Inlining the sleep function here cause this is the ONLY place in the entire codebase where sleep should be used!
// Inlining the sleep function here cause this is one of the few places in the entire codebase where sleep should be used!
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

export async function createCacheForWalletSetupFunction(
Expand Down
7 changes: 5 additions & 2 deletions wallets/metamask/src/metamask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export class MetaMask {
if (!this.extensionId) {
throw NO_EXTENSION_ID_ERROR
}

await this.notificationPage.connectToDapp(this.extensionId)
}

Expand All @@ -47,13 +48,15 @@ export class MetaMask {
if (!this.extensionId) {
throw NO_EXTENSION_ID_ERROR
}
await this.notificationPage.signSimpleMessage(this.extensionId)

await this.notificationPage.signMessage(this.extensionId)
}

async rejectSignature() {
if (!this.extensionId) {
throw NO_EXTENSION_ID_ERROR
}
await this.notificationPage.rejectSimpleMessage(this.extensionId)

await this.notificationPage.rejectMessage(this.extensionId)
}
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './connectToDapp'
export * from './signSimpleMessage'
export * from './signStructuredMessage'
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import type { BrowserContext } from '@playwright/test'
import { getNotificationPageAndWaitForLoad } from '../../../utils/getNotificationPageAndWaitForLoad'
import type { Page } from '@playwright/test'
import Selectors from '../selectors'

const signMessage = async (context: BrowserContext, extensionId: string) => {
const notificationPage = await getNotificationPageAndWaitForLoad(context, extensionId)

await notificationPage.locator(Selectors.SignaturePage.signButton).click()
const signMessage = async (notificationPage: Page) => {
await notificationPage.locator(Selectors.SignaturePage.simpleMessage.signButton).click()
}

const rejectMessage = async (context: BrowserContext, extensionId: string) => {
const notificationPage = await getNotificationPageAndWaitForLoad(context, extensionId)

await notificationPage.locator(Selectors.SignaturePage.rejectButton).click()
const rejectMessage = async (notificationPage: Page) => {
await notificationPage.locator(Selectors.SignaturePage.simpleMessage.rejectButton).click()
}

// Used for:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Page } from '@playwright/test'
import Selectors from '../selectors'

const signMessage = async (notificationPage: Page) => {
const scrollDownButton = notificationPage.locator(Selectors.SignaturePage.structuredMessage.scrollDownButton)
const signButton = notificationPage.locator(Selectors.SignaturePage.structuredMessage.signButton)

while (await signButton.isDisabled()) {
await scrollDownButton.click()
}

await notificationPage.locator(Selectors.SignaturePage.structuredMessage.signButton).click()
}

const rejectMessage = async (notificationPage: Page) => {
await notificationPage.locator(Selectors.SignaturePage.structuredMessage.rejectButton).click()
}

// Used for:
// - `eth_signTypedData_v3`
// - `eth_signTypedData_v4`
export const signStructuredMessage = {
sign: signMessage,
reject: rejectMessage
}
46 changes: 41 additions & 5 deletions wallets/metamask/src/pages/NotificationPage/page.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import type { Page } from '@playwright/test'
import { connectToDapp, signSimpleMessage } from './actions'
import { getNotificationPageAndWaitForLoad } from '../../utils/getNotificationPageAndWaitForLoad'
import { waitFor } from '../../utils/waitFor'
import { connectToDapp, signSimpleMessage, signStructuredMessage } from './actions'
import Selectors from './selectors'

export class NotificationPage {
static readonly selectors = Selectors
readonly selectors = Selectors

readonly page: Page

constructor(page: Page) {
Expand All @@ -12,11 +18,41 @@ export class NotificationPage {
await connectToDapp(this.page.context(), extensionId)
}

async signSimpleMessage(extensionId: string) {
await signSimpleMessage.sign(this.page.context(), extensionId)
// TODO: Revisit this logic in the future to see if we can increase the performance by utilizing `Promise.race`.
private async beforeMessageSignature(extensionId: string) {
const notificationPage = await getNotificationPageAndWaitForLoad(this.page.context(), extensionId)

// Most of the time, this function will be used to sign structured messages, so we check for the scroll button first.
const isScrollButtonVisible = await waitFor(
notificationPage.locator(Selectors.SignaturePage.structuredMessage.scrollDownButton),
'visible',
1_500, // TODO: Make this configurable.
false
)

return {
notificationPage,
isScrollButtonVisible
}
}

async rejectSimpleMessage(extensionId: string) {
await signSimpleMessage.reject(this.page.context(), extensionId)
async signMessage(extensionId: string) {
const { notificationPage, isScrollButtonVisible } = await this.beforeMessageSignature(extensionId)

if (isScrollButtonVisible) {
await signStructuredMessage.sign(notificationPage)
} else {
await signSimpleMessage.sign(notificationPage)
}
}

async rejectMessage(extensionId: string) {
const { notificationPage, isScrollButtonVisible } = await this.beforeMessageSignature(extensionId)

if (isScrollButtonVisible) {
await signStructuredMessage.reject(notificationPage)
} else {
await signSimpleMessage.reject(notificationPage)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import { createDataTestSelector } from '../../../utils/selectors/createDataTestSelector'

export default {
const simpleMessage = {
signButton: `.request-signature__footer ${createDataTestSelector('request-signature__sign')}`,
rejectButton: '.request-signature__footer button.btn-secondary'
}

const structuredMessage = {
scrollDownButton: `.signature-request-message ${createDataTestSelector('signature-request-scroll-button')}`,
signButton: `.signature-request-footer ${createDataTestSelector('signature-sign-button')}`,
rejectButton: `.signature-request-footer ${createDataTestSelector('signature-cancel-button')}`
}

export default {
simpleMessage,
structuredMessage
}
50 changes: 50 additions & 0 deletions wallets/metamask/src/utils/waitFor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { Locator } from '@playwright/test'

// Inlining the sleep function here cause this is one of the few places in the entire codebase where sleep should be used!
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

const timeouts = [0, 20, 50, 100, 100, 500] as const

type State = 'visible' | 'hidden'

// TODO: Box this function.
// This functions mimics the one found in Playwright with a few small differences.
// Custom implementation is needed because Playwright lists errors in the report even if they are caught.
export async function waitFor(locator: Locator, state: State, timeout: number, shouldThrow = true) {
let timeoutsSum = 0
let timeoutIndex = 0

let reachedTimeout = false

while (!reachedTimeout) {
let nextTimeout = timeouts.at(Math.min(timeoutIndex++, timeouts.length - 1)) as number

if (timeoutsSum + nextTimeout > timeout) {
nextTimeout = timeout - timeoutsSum
reachedTimeout = true
} else {
timeoutsSum += nextTimeout
}

await sleep(nextTimeout)

const result = await action(locator, state)
if (result) {
return result
}
}

if (shouldThrow) {
throw new Error(`Timeout ${timeout}ms exceeded.`)
}

return false
}

async function action(locator: Locator, state: State) {
if (state === 'hidden') {
return locator.isHidden()
}

return locator.isVisible()
}
36 changes: 36 additions & 0 deletions wallets/metamask/test/e2e/metamask/confirmSignature.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,39 @@ test('should confirm `eth_signTypedData`', async ({ context, metamaskPage, page,

await expect(page.locator('#signTypedDataVerifyResult')).toHaveText('0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266')
})

test('should confirm `eth_signTypedData_v3`', async ({ context, metamaskPage, page, extensionId }) => {
const metamask = new MetaMask(context, metamaskPage, connectedSetup.walletPassword, extensionId)

await page.goto('https://metamask.github.io/test-dapp/')

await page.locator('#signTypedDataV3').click()

await metamask.confirmSignature()

await expect(page.locator('#signTypedDataV3Result')).toHaveText(
'0x6ea8bb309a3401225701f3565e32519f94a0ea91a5910ce9229fe488e773584c0390416a2190d9560219dab757ecca2029e63fa9d1c2aebf676cc25b9f03126a1b'
)

await page.locator('#signTypedDataV3Verify').click()

await expect(page.locator('#signTypedDataV3VerifyResult')).toHaveText('0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266')
})

test('should confirm `eth_signTypedData_v4`', async ({ context, metamaskPage, page, extensionId }) => {
const metamask = new MetaMask(context, metamaskPage, connectedSetup.walletPassword, extensionId)

await page.goto('https://metamask.github.io/test-dapp/')

await page.locator('#signTypedDataV4').click()

await metamask.confirmSignature()

await expect(page.locator('#signTypedDataV4Result')).toHaveText(
'0x789d9365fe0fbf1485b8069cbb000b78abd56b92608f9bc11a0d78e8810cd0434a60e93790c52348e5ac8770a8c5b0bb89411c2fbc61cbb4f56d67d60a3374961c'
)

await page.locator('#signTypedDataV4Verify').click()

await expect(page.locator('#signTypedDataV4VerifyResult')).toHaveText('0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266')
})
28 changes: 28 additions & 0 deletions wallets/metamask/test/e2e/metamask/rejectSignature.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,31 @@ test('should reject `eth_signTypedData`', async ({ context, metamaskPage, page,
'Error: MetaMask Message Signature: User denied message signature.'
)
})

test('should reject `eth_signTypedData_v3`', async ({ context, metamaskPage, page, extensionId }) => {
const metamask = new MetaMask(context, metamaskPage, connectedSetup.walletPassword, extensionId)

await page.goto('https://metamask.github.io/test-dapp/')

await page.locator('#signTypedDataV3').click()

await metamask.rejectSignature()

await expect(page.locator('#signTypedDataV3Result')).toHaveText(
'Error: MetaMask Message Signature: User denied message signature.'
)
})

test('should reject `eth_signTypedData_v4`', async ({ context, metamaskPage, page, extensionId }) => {
const metamask = new MetaMask(context, metamaskPage, connectedSetup.walletPassword, extensionId)

await page.goto('https://metamask.github.io/test-dapp/')

await page.locator('#signTypedDataV4').click()

await metamask.rejectSignature()

await expect(page.locator('#signTypedDataV4Result')).toHaveText(
'Error: MetaMask Message Signature: User denied message signature.'
)
})

0 comments on commit 70e74a8

Please sign in to comment.