diff --git a/wallets/metamask/src/cypress/MetaMask.ts b/wallets/metamask/src/cypress/MetaMask.ts index 6bc39eaad..45c678888 100644 --- a/wallets/metamask/src/cypress/MetaMask.ts +++ b/wallets/metamask/src/cypress/MetaMask.ts @@ -4,6 +4,8 @@ import { MetaMask as MetaMaskPlaywright } from '../playwright/MetaMask' import { waitFor } from '../playwright/utils/waitFor' import HomePageSelectors from '../selectors/pages/HomePage' import Selectors from '../selectors/pages/HomePage' +import TransactionPage from '../selectors/pages/NotificationPage/transactionPage' +import type { GasSettings } from '../type/GasSettings' import type { Network } from '../type/Network' import getPlaywrightMetamask from './getPlaywrightMetamask' @@ -152,6 +154,13 @@ export default class MetaMask { // Token async deployToken() { + await waitFor( + () => + this.metamaskExtensionPage.locator(TransactionPage.nftApproveAllConfirmationPopup.approveButton).isVisible(), + 3_000, + false + ) + await this.metamaskPlaywright.confirmTransaction() return true @@ -165,6 +174,22 @@ export default class MetaMask { return true } + async approveTokenPermission(options?: { + spendLimit?: number | 'max' + gasSetting?: GasSettings + }) { + return await this.metamaskPlaywright + .approveTokenPermission(options) + .then(() => { + return true + }) + .catch(() => { + return false + }) + } + + // Network + async approveNewNetwork() { await this.metamaskPlaywright.approveNewNetwork() diff --git a/wallets/metamask/src/cypress/configureSynpress.ts b/wallets/metamask/src/cypress/configureSynpress.ts index 8549ce609..88e9931f9 100644 --- a/wallets/metamask/src/cypress/configureSynpress.ts +++ b/wallets/metamask/src/cypress/configureSynpress.ts @@ -1,6 +1,7 @@ import type { BrowserContext, Page } from '@playwright/test' import { ensureRdpPort } from '@synthetixio/synpress-core' import type { CreateAnvilOptions } from '@viem/anvil' +import type { GasSettings } from '../type/GasSettings' import type { Network } from '../type/Network' import MetaMask from './MetaMask' import importMetaMaskWallet from './support/importMetaMaskWallet' @@ -100,6 +101,10 @@ export default function configureSynpress(on: Cypress.PluginEvents, config: Cypr // Token deployToken: () => metamask?.deployToken(), addNewToken: () => metamask?.addNewToken(), + approveTokenPermission: (options?: { + spendLimit?: number | 'max' + gasSetting?: GasSettings + }) => metamask?.approveTokenPermission(options), // Encryption providePublicEncryptionKey: () => metamask?.providePublicEncryptionKey(), diff --git a/wallets/metamask/src/cypress/support/synpressCommands.ts b/wallets/metamask/src/cypress/support/synpressCommands.ts index 39f874360..167e53634 100644 --- a/wallets/metamask/src/cypress/support/synpressCommands.ts +++ b/wallets/metamask/src/cypress/support/synpressCommands.ts @@ -10,6 +10,7 @@ // *********************************************** import type { Anvil, CreateAnvilOptions } from '@viem/anvil' +import type { GasSettings } from '../../type/GasSettings' import type { Network } from '../../type/Network' declare global { @@ -38,6 +39,10 @@ declare global { deployToken(): Chainable addNewToken(): Chainable + approveTokenPermission(options?: { + spendLimit?: number | 'max' + gasSetting?: GasSettings + }): Chainable providePublicEncryptionKey(): Chainable decrypt(): Chainable @@ -122,6 +127,15 @@ export default function synpressCommands() { Cypress.Commands.add('addNewToken', () => { return cy.task('addNewToken') }) + Cypress.Commands.add( + 'approveTokenPermission', + (options?: { + spendLimit?: number | 'max' + gasSetting?: GasSettings + }) => { + return cy.task('approveTokenPermission', options) + } + ) // Others diff --git a/wallets/metamask/src/playwright/MetaMask.ts b/wallets/metamask/src/playwright/MetaMask.ts index 3afea4d8a..19f56f57e 100644 --- a/wallets/metamask/src/playwright/MetaMask.ts +++ b/wallets/metamask/src/playwright/MetaMask.ts @@ -1,9 +1,9 @@ import type { BrowserContext, Page } from '@playwright/test' import { SettingsSidebarMenus } from '../selectors/pages/HomePage/settings' +import type { GasSettings } from '../type/GasSettings' import { MetaMaskAbstract } from '../type/MetaMaskAbstract' import type { Network } from '../type/Network' import { CrashPage, HomePage, LockPage, NotificationPage, OnboardingPage } from './pages' -import type { GasSetting } from './pages/NotificationPage/actions' import { SettingsPage } from './pages/SettingsPage/page' const NO_EXTENSION_ID_ERROR = new Error('MetaMask extensionId is not set') @@ -186,7 +186,7 @@ export class MetaMask extends MetaMaskAbstract { await this.notificationPage.rejectSwitchNetwork(this.extensionId) } - async confirmTransaction(options?: { gasSetting?: GasSetting }) { + async confirmTransaction(options?: { gasSetting?: GasSettings }) { if (!this.extensionId) { throw NO_EXTENSION_ID_ERROR } @@ -204,7 +204,7 @@ export class MetaMask extends MetaMaskAbstract { async approveTokenPermission(options?: { spendLimit?: 'max' | number - gasSetting?: GasSetting + gasSetting?: GasSettings }) { if (!this.extensionId) { throw NO_EXTENSION_ID_ERROR @@ -280,7 +280,7 @@ export class MetaMask extends MetaMaskAbstract { } async confirmTransactionAndWaitForMining(options?: { - gasSetting?: GasSetting + gasSetting?: GasSettings }) { if (!this.extensionId) { throw NO_EXTENSION_ID_ERROR diff --git a/wallets/metamask/src/playwright/pages/NotificationPage/actions/approvePermission.ts b/wallets/metamask/src/playwright/pages/NotificationPage/actions/approvePermission.ts index 0c8b0b03b..64a38145a 100644 --- a/wallets/metamask/src/playwright/pages/NotificationPage/actions/approvePermission.ts +++ b/wallets/metamask/src/playwright/pages/NotificationPage/actions/approvePermission.ts @@ -1,6 +1,7 @@ import type { Page } from '@playwright/test' import Selectors from '../../../../selectors/pages/NotificationPage' -import { type GasSetting, transaction } from './transaction' +import { transaction } from './transaction' +import type { GasSettings } from '../../../../type/GasSettings' const editTokenPermission = async (notificationPage: Page, customSpendLimit: 'max' | number) => { if (customSpendLimit === 'max') { @@ -13,7 +14,7 @@ const editTokenPermission = async (notificationPage: Page, customSpendLimit: 'ma .fill(customSpendLimit.toString()) } -const approveTokenPermission = async (notificationPage: Page, gasSetting: GasSetting) => { +const approveTokenPermission = async (notificationPage: Page, gasSetting: GasSettings) => { // Click the "Next" button. await notificationPage.locator(Selectors.ActionFooter.confirmActionButton).click() diff --git a/wallets/metamask/src/playwright/pages/NotificationPage/actions/transaction.ts b/wallets/metamask/src/playwright/pages/NotificationPage/actions/transaction.ts index 5558ccddb..217b3bc41 100644 --- a/wallets/metamask/src/playwright/pages/NotificationPage/actions/transaction.ts +++ b/wallets/metamask/src/playwright/pages/NotificationPage/actions/transaction.ts @@ -1,36 +1,11 @@ import type { Page } from '@playwright/test' -import { z } from 'zod' import HomePageSelectors from '../../../../selectors/pages/HomePage' import Selectors from '../../../../selectors/pages/NotificationPage' +import { GasSettingValidation, type GasSettings } from '../../../../type/GasSettings' import { waitFor } from '../../../utils/waitFor' -const GasSetting = z.union([ - z.literal('low'), - z.literal('market'), - z.literal('aggressive'), - z.literal('site'), - z - .object({ - maxBaseFee: z.number(), - priorityFee: z.number(), - // TODO: Add gasLimit range validation. - gasLimit: z.number().optional() - }) - .superRefine(({ maxBaseFee, priorityFee }, ctx) => { - if (priorityFee > maxBaseFee) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'Max base fee cannot be lower than priority fee', - path: ['MetaMask', 'confirmTransaction', 'gasSetting', 'maxBaseFee'] - }) - } - }) -]) - -export type GasSetting = z.input - -const confirmTransaction = async (notificationPage: Page, options: GasSetting) => { - const gasSetting = GasSetting.parse(options) +const confirmTransaction = async (notificationPage: Page, options: GasSettings) => { + const gasSetting = GasSettingValidation.parse(options) const handleNftSetApprovalForAll = async (page: Page) => { try { @@ -144,7 +119,7 @@ const confirmTransaction = async (notificationPage: Page, options: GasSetting) = await handleNftSetApprovalForAll(notificationPage) } -const confirmTransactionAndWaitForMining = async (walletPage: Page, notificationPage: Page, options: GasSetting) => { +const confirmTransactionAndWaitForMining = async (walletPage: Page, notificationPage: Page, options: GasSettings) => { await walletPage.locator(HomePageSelectors.activityTab.activityTabButton).click() const waitForUnapprovedTxs = async () => { diff --git a/wallets/metamask/src/playwright/pages/NotificationPage/page.ts b/wallets/metamask/src/playwright/pages/NotificationPage/page.ts index 8756d526d..1052ccd41 100644 --- a/wallets/metamask/src/playwright/pages/NotificationPage/page.ts +++ b/wallets/metamask/src/playwright/pages/NotificationPage/page.ts @@ -1,8 +1,8 @@ import type { Page } from '@playwright/test' import Selectors from '../../../selectors/pages/NotificationPage' +import type { GasSettings } from '../../../type/GasSettings' import { getNotificationPageAndWaitForLoad } from '../../utils/getNotificationPageAndWaitForLoad' import { - type GasSetting, approvePermission, connectToDapp, decryptMessage, @@ -99,7 +99,7 @@ export class NotificationPage { await network.rejectSwitchNetwork(notificationPage) } - async confirmTransaction(extensionId: string, options?: { gasSetting?: GasSetting }) { + async confirmTransaction(extensionId: string, options?: { gasSetting?: GasSettings }) { const notificationPage = await getNotificationPageAndWaitForLoad(this.page.context(), extensionId) await transaction.confirm(notificationPage, options?.gasSetting ?? 'site') @@ -111,7 +111,7 @@ export class NotificationPage { await transaction.reject(notificationPage) } - async confirmTransactionAndWaitForMining(extensionId: string, options?: { gasSetting?: GasSetting }) { + async confirmTransactionAndWaitForMining(extensionId: string, options?: { gasSetting?: GasSettings }) { const notificationPage = await getNotificationPageAndWaitForLoad(this.page.context(), extensionId) await transaction.confirmAndWaitForMining(this.page, notificationPage, options?.gasSetting ?? 'site') @@ -119,7 +119,7 @@ export class NotificationPage { async approveTokenPermission( extensionId: string, - options?: { spendLimit?: 'max' | number; gasSetting?: GasSetting } + options?: { spendLimit?: 'max' | number; gasSetting?: GasSettings } ) { const notificationPage = await getNotificationPageAndWaitForLoad(this.page.context(), extensionId) diff --git a/wallets/metamask/src/type/GasSettings.ts b/wallets/metamask/src/type/GasSettings.ts new file mode 100644 index 000000000..342cab308 --- /dev/null +++ b/wallets/metamask/src/type/GasSettings.ts @@ -0,0 +1,26 @@ +import { z } from 'zod' + +export const GasSettingValidation = z.union([ + z.literal('low'), + z.literal('market'), + z.literal('aggressive'), + z.literal('site'), + z + .object({ + maxBaseFee: z.number(), + priorityFee: z.number(), + // TODO: Add gasLimit range validation. + gasLimit: z.number().optional() + }) + .superRefine(({ maxBaseFee, priorityFee }, ctx) => { + if (priorityFee > maxBaseFee) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Max base fee cannot be lower than priority fee', + path: ['MetaMask', 'confirmTransaction', 'gasSetting', 'maxBaseFee'] + }) + } + }) +]) + +export type GasSettings = z.input diff --git a/wallets/metamask/src/type/MetaMaskAbstract.ts b/wallets/metamask/src/type/MetaMaskAbstract.ts index 52321c9b6..6ae0cdf81 100644 --- a/wallets/metamask/src/type/MetaMaskAbstract.ts +++ b/wallets/metamask/src/type/MetaMaskAbstract.ts @@ -1,6 +1,6 @@ -import type { GasSetting } from '../playwright/pages/NotificationPage/actions' import { SettingsSidebarMenus } from '../selectors/pages/HomePage/settings' import type { Network } from './Network' +import type { GasSettings } from './GasSettings'; export abstract class MetaMaskAbstract { /** @@ -132,7 +132,7 @@ export abstract class MetaMaskAbstract { * @param options - The transaction options. * @param options.gasSetting - The gas setting to use for the transaction. */ - abstract confirmTransaction(options?: { gasSetting?: GasSetting }): void + abstract confirmTransaction(options?: { gasSetting?: GasSettings }): void /** * Rejects a transaction request. @@ -152,7 +152,7 @@ export abstract class MetaMaskAbstract { */ abstract approveTokenPermission(options?: { spendLimit?: 'max' | number - gasSetting?: GasSetting + gasSetting?: GasSettings }): void /** @@ -239,7 +239,7 @@ export abstract class MetaMaskAbstract { * @group Experimental Methods */ abstract confirmTransactionAndWaitForMining(options?: { - gasSetting?: GasSetting + gasSetting?: GasSettings }): void /** diff --git a/wallets/metamask/test/cypress/addNewToken.cy.ts b/wallets/metamask/test/cypress/addNewToken.cy.ts index 535c4710c..4a583d1e9 100644 --- a/wallets/metamask/test/cypress/addNewToken.cy.ts +++ b/wallets/metamask/test/cypress/addNewToken.cy.ts @@ -13,9 +13,6 @@ before(() => { it('should add new token to MetaMask', () => { cy.get('#createToken').click() - // wait for the blockchain - todo: replace with an event handler - cy.wait(5000) - cy.deployToken().then(() => { // wait for the blockchain - todo: replace with an event handler cy.wait(5000) diff --git a/wallets/metamask/test/cypress/approvePermission.cy.ts b/wallets/metamask/test/cypress/approvePermission.cy.ts new file mode 100644 index 000000000..e29dad339 --- /dev/null +++ b/wallets/metamask/test/cypress/approvePermission.cy.ts @@ -0,0 +1,109 @@ +before(() => { + cy.getNetwork().then((network) => { + if (network !== 'Anvil') { + cy.switchNetwork('Anvil') + } + }) +}) + +it('should approve tokens with the default limit by default', () => { + cy.get('#createToken').click() + + cy.deployToken().then(() => { + cy.wait(5000) // wait for the blockchain - todo: replace with an event handler + + cy.get('#approveTokens').click() + + cy.approveTokenPermission() + }) +}) + +it('should approve tokens with the `max` limit', () => { + cy.get('#createToken').click() + + cy.deployToken().then(() => { + cy.wait(5000) // wait for the blockchain - todo: replace with an event handler + + cy.get('#approveTokens').click() + + cy.approveTokenPermission({ spendLimit: 'max' }) + }) +}) + +it('should approve tokens with the custom limit', () => { + cy.get('#createToken').click() + + cy.deployToken().then(() => { + cy.wait(5000) // wait for the blockchain - todo: replace with an event handler + + cy.get('#approveTokens').click() + + cy.approveTokenPermission({ spendLimit: 420 }) + }) +}) + +it('should approve tokens with the default spend limit', () => { + cy.get('#createToken').click() + + cy.deployToken().then(() => { + cy.wait(5000) // wait for the blockchain - todo: replace with an event handler + + cy.get('#approveTokens').click() + + cy.approveTokenPermission({ + gasSetting: 'site' + }) + }) +}) + +it('should approve tokens with the `max` spend limit and custom gas setting', () => { + cy.get('#createToken').click() + + cy.deployToken().then(() => { + cy.wait(5000) // wait for the blockchain - todo: replace with an event handler + + cy.get('#approveTokens').click() + + cy.approveTokenPermission({ + spendLimit: 'max', + gasSetting: { + maxBaseFee: 250, + priorityFee: 150 + } + }) + }) +}) + +it('should approve tokens with the custom spend limit and custom gas limit', () => { + cy.get('#createToken').click() + + cy.deployToken().then(() => { + cy.wait(5000) // wait for the blockchain - todo: replace with an event handler + + cy.get('#approveTokens').click() + + cy.approveTokenPermission({ + spendLimit: 420, + gasSetting: { + maxBaseFee: 250, + priorityFee: 150, + gasLimit: 200_000 + } + }) + }) +}) + +it('should request permissions', () => { + cy.get('#revokeAccountsPermission').click() + cy.get('#getPermissions').click() + + cy.get('#permissionsResult').should('have.text', 'No permissions found.') + + cy.wait(1000) + + cy.get('#requestPermissions').click() + + cy.connectToDapp().then(() => { + cy.get('#permissionsResult').should('have.text', 'eth_accounts') + }) +})