Skip to content

Commit

Permalink
✨ feat(metamask): Add gas customization to confirmTransaction (#1048)
Browse files Browse the repository at this point in the history
  • Loading branch information
duckception authored Dec 31, 2023
1 parent 7b09f24 commit 08e1a36
Show file tree
Hide file tree
Showing 6 changed files with 314 additions and 30 deletions.
12 changes: 7 additions & 5 deletions wallets/metamask/src/metamask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { BrowserContext, Page } from '@playwright/test'
import { CrashPage, HomePage, LockPage, NotificationPage, OnboardingPage } from './pages'
import type { Network } from './pages/HomePage/actions'
import { SettingsSidebarMenus } from './pages/HomePage/selectors/settings'
import type { GasSetting } from './pages/NotificationPage/actions'

const NO_EXTENSION_ID_ERROR = new Error('MetaMask extensionId is not set')

Expand Down Expand Up @@ -114,12 +115,12 @@ export class MetaMask {
await this.notificationPage.rejectSwitchNetwork(this.extensionId)
}

async confirmTransaction() {
async confirmTransaction(gasSetting: GasSetting = 'site') {
if (!this.extensionId) {
throw NO_EXTENSION_ID_ERROR
}

await this.notificationPage.confirmTransaction(this.extensionId)
await this.notificationPage.confirmTransaction(this.extensionId, gasSetting)
}

async rejectTransaction() {
Expand Down Expand Up @@ -169,18 +170,19 @@ export class MetaMask {
// ---- EXPERIMENTAL FEATURES ----

public readonly experimental = {
confirmTransactionAndWaitForMining: async () => await this.confirmTransactionAndWaitForMining(),
confirmTransactionAndWaitForMining: async (gasSetting: GasSetting = 'site') =>
await this.confirmTransactionAndWaitForMining(gasSetting),
// Note: `txIndex` starts from 0.
openTransactionDetails: async (txIndex: number) => await this.openTransactionDetails(txIndex),
closeTransactionDetails: async () => await this.closeTransactionDetails()
}

private async confirmTransactionAndWaitForMining() {
private async confirmTransactionAndWaitForMining(gasSetting: GasSetting) {
if (!this.extensionId) {
throw NO_EXTENSION_ID_ERROR
}

await this.notificationPage.confirmTransactionAndWaitForMining(this.extensionId)
await this.notificationPage.confirmTransactionAndWaitForMining(this.extensionId, gasSetting)
}

private async openTransactionDetails(txIndex: number) {
Expand Down
120 changes: 117 additions & 3 deletions wallets/metamask/src/pages/NotificationPage/actions/transaction.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,127 @@
import type { Page } from '@playwright/test'
import { z } from 'zod'
import { waitFor } from '../../../utils/waitFor'
import HomePageSelectors from '../../HomePage/selectors'
import Selectors from '../selectors'

const confirmTransaction = async (notificationPage: Page) => {
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<typeof GasSetting>

const confirmTransaction = async (notificationPage: Page, options: GasSetting) => {
const gasSetting = GasSetting.parse(options)

// By default, the `site` gas setting is used.
if (gasSetting === 'site') {
await notificationPage.locator(Selectors.ActionFooter.confirmActionButton).click()

return
}

// TODO: This button can be invisible in case of a network issue. Verify this, and handle in the future.
await notificationPage.locator(Selectors.TransactionPage.editGasFeeMenu.editGasFeeButton).click()

const estimationNotAvailableErrorMessage = (gasSetting: string) =>
`[ConfirmTransaction] Estimated fee is not available for the "${gasSetting}" gas setting. By default, MetaMask would use the "site" gas setting in this case, however, this is not YOUR intention.`

const handleLowMediumOrAggressiveGasSetting = async (
gasSetting: string,
selectors: { button: string; maxFee: string }
) => {
if ((await notificationPage.locator(selectors.maxFee).textContent()) === '--') {
throw new Error(estimationNotAvailableErrorMessage(gasSetting))
}

await notificationPage.locator(selectors.button).click()
}

if (gasSetting === 'low') {
await handleLowMediumOrAggressiveGasSetting(gasSetting, Selectors.TransactionPage.editGasFeeMenu.lowGasFee)
} else if (gasSetting === 'market') {
await handleLowMediumOrAggressiveGasSetting(gasSetting, Selectors.TransactionPage.editGasFeeMenu.marketGasFee)
} else if (gasSetting === 'aggressive') {
await handleLowMediumOrAggressiveGasSetting(gasSetting, Selectors.TransactionPage.editGasFeeMenu.aggressiveGasFee)
} else {
await notificationPage.locator(Selectors.TransactionPage.editGasFeeMenu.advancedGasFeeButton).click()

await notificationPage.locator(Selectors.TransactionPage.editGasFeeMenu.advancedGasFeeMenu.maxBaseFeeInput).fill('')
await notificationPage
.locator(Selectors.TransactionPage.editGasFeeMenu.advancedGasFeeMenu.maxBaseFeeInput)
.fill(gasSetting.maxBaseFee.toString())

await notificationPage
.locator(Selectors.TransactionPage.editGasFeeMenu.advancedGasFeeMenu.priorityFeeInput)
.fill('')
await notificationPage
.locator(Selectors.TransactionPage.editGasFeeMenu.advancedGasFeeMenu.priorityFeeInput)
.fill(gasSetting.priorityFee.toString())

if (gasSetting.gasLimit) {
await notificationPage
.locator(Selectors.TransactionPage.editGasFeeMenu.advancedGasFeeMenu.gasLimitEditButton)
.click()

await notificationPage.locator(Selectors.TransactionPage.editGasFeeMenu.advancedGasFeeMenu.gasLimitInput).fill('')
await notificationPage
.locator(Selectors.TransactionPage.editGasFeeMenu.advancedGasFeeMenu.gasLimitInput)
.fill(gasSetting.gasLimit.toString())

const gasLimitErrorLocator = notificationPage.locator(
Selectors.TransactionPage.editGasFeeMenu.advancedGasFeeMenu.gasLimitError
)
const isGasLimitErrorHidden = await waitFor(() => gasLimitErrorLocator.isHidden(), 1_000, false) // TODO: Extract & make configurable

if (!isGasLimitErrorHidden) {
const errorText = await gasLimitErrorLocator.textContent({
timeout: 1_000 // TODO: Extract & make configurable
})

throw new Error(`[ConfirmTransaction] Invalid gas limit: ${errorText}`)
}
}

await notificationPage.locator(Selectors.TransactionPage.editGasFeeMenu.advancedGasFeeMenu.saveButton).click()
}

// We wait until the tooltip is not visible anymore. This indicates a gas setting was changed.
// Ideally, we would wait until the edit button changes its text, i.e., "Site" -> "Aggressive", however, this is not possible right now.
// For some unknown reason, if the manual gas setting is too high (>1 ETH), the edit button displays "Site" instead of "Advanced" ¯\_(ツ)_/¯
const waitForAction = async () => {
const isTooltipVisible = await notificationPage
.locator(Selectors.TransactionPage.editGasFeeMenu.editGasFeeButtonToolTip)
.isVisible()

return !isTooltipVisible
}

// TODO: Extract & make configurable
await waitFor(waitForAction, 3_000, true)

await notificationPage.locator(Selectors.ActionFooter.confirmActionButton).click()
}

const confirmTransactionAndWaitForMining = async (walletPage: Page, notificationPage: Page) => {
const confirmTransactionAndWaitForMining = async (walletPage: Page, notificationPage: Page, options: GasSetting) => {
await walletPage.locator(HomePageSelectors.activityTab.activityTabButton).click()

const waitForUnapprovedTxs = async () => {
Expand All @@ -23,7 +137,7 @@ const confirmTransactionAndWaitForMining = async (walletPage: Page, notification
throw new Error('No new pending transactions found in 30s')
}

await confirmTransaction(notificationPage)
await confirmTransaction(notificationPage, options)

const waitForMining = async () => {
const unapprovedTxs = await walletPage.locator(HomePageSelectors.activityTab.pendingUnapprovedTransactions).count()
Expand Down
9 changes: 5 additions & 4 deletions wallets/metamask/src/pages/NotificationPage/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Page } from '@playwright/test'
import { getNotificationPageAndWaitForLoad } from '../../utils/getNotificationPageAndWaitForLoad'
import { waitFor } from '../../utils/waitFor'
import {
type GasSetting,
approvePermission,
connectToDapp,
network,
Expand Down Expand Up @@ -89,10 +90,10 @@ export class NotificationPage {
await network.rejectSwitchNetwork(notificationPage)
}

async confirmTransaction(extensionId: string) {
async confirmTransaction(extensionId: string, gasSetting: GasSetting) {
const notificationPage = await getNotificationPageAndWaitForLoad(this.page.context(), extensionId)

await transaction.confirm(notificationPage)
await transaction.confirm(notificationPage, gasSetting)
}

async rejectTransaction(extensionId: string) {
Expand All @@ -101,10 +102,10 @@ export class NotificationPage {
await transaction.reject(notificationPage)
}

async confirmTransactionAndWaitForMining(extensionId: string) {
async confirmTransactionAndWaitForMining(extensionId: string, gasSetting: GasSetting) {
const notificationPage = await getNotificationPageAndWaitForLoad(this.page.context(), extensionId)

await transaction.confirmAndWaitForMining(this.page, notificationPage)
await transaction.confirmAndWaitForMining(this.page, notificationPage, gasSetting)
}

async approvePermission(extensionId: string, spendLimit?: 'max' | number) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import ActionFooter from './actionFooter'
import NetworkPage from './networkPage'
import PermissionPage from './permissionPage'
import SignaturePage from './signaturePage'
import TransactionPage from './transactionPage'

export default {
ActionFooter,
SignaturePage,
NetworkPage,
PermissionPage
PermissionPage,
TransactionPage
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { createDataTestSelector } from '../../../utils/selectors/createDataTestSelector'

const advancedGasFeeMenu = {
maxBaseFeeInput: createDataTestSelector('base-fee-input'),
priorityFeeInput: createDataTestSelector('priority-fee-input'),
gasLimitEditButton: createDataTestSelector('advanced-gas-fee-edit'),
gasLimitInput: createDataTestSelector('gas-limit-input'),
gasLimitError: `div:has(> ${createDataTestSelector('gas-limit-input')}) + .form-field__error`,
saveButton: '.popover-footer > button.btn-primary'
}

const lowGasFee = {
button: createDataTestSelector('edit-gas-fee-item-low'),
maxFee: `${createDataTestSelector('edit-gas-fee-item-low')} .edit-gas-item__fee-estimate`
}

const marketGasFee = {
button: createDataTestSelector('edit-gas-fee-item-medium'),
maxFee: `${createDataTestSelector('edit-gas-fee-item-medium')} .edit-gas-item__fee-estimate`
}

const aggressiveGasFee = {
button: createDataTestSelector('edit-gas-fee-item-high'),
maxFee: `${createDataTestSelector('edit-gas-fee-item-high')} .edit-gas-item__fee-estimate`
}

const editGasFeeMenu = {
editGasFeeButton: createDataTestSelector('edit-gas-fee-button'),
editGasFeeButtonToolTip: '.edit-gas-fee-button .info-tooltip',
lowGasFee,
marketGasFee,
aggressiveGasFee,
siteSuggestedGasFeeButton: createDataTestSelector('edit-gas-fee-item-dappSuggested'),
advancedGasFeeButton: createDataTestSelector('edit-gas-fee-item-custom'),
advancedGasFeeMenu
}

export default {
editGasFeeMenu
}
Loading

0 comments on commit 08e1a36

Please sign in to comment.