Skip to content

Commit

Permalink
feat: Show error on tx and safe creation if wallet balance is insuffi…
Browse files Browse the repository at this point in the history
…cient (#2986)

* feat: Show error on tx and safe creation if wallet balance is insufficient

* fix: Write tests for useWalletCanPay

* fix: Write another test for ExecuteForm
  • Loading branch information
usame-algan authored Dec 18, 2023
1 parent dad0369 commit fbf7ac9
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 2 deletions.
8 changes: 8 additions & 0 deletions src/components/new-safe/create/steps/ReviewStep/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import ErrorMessage from '@/components/tx/ErrorMessage'
import useWalletCanPay from '@/hooks/useWalletCanPay'
import { useMemo, useState } from 'react'
import { Button, Grid, Typography, Divider, Box, Alert } from '@mui/material'
import { lightPalette } from '@safe-global/safe-react-components'
Expand Down Expand Up @@ -124,6 +126,8 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps<NewSafe
const maxFeePerGas = gasPrice?.maxFeePerGas
const maxPriorityFeePerGas = gasPrice?.maxPriorityFeePerGas

const walletCanPay = useWalletCanPay({ gasLimit, maxFeePerGas, maxPriorityFeePerGas })

const totalFee =
gasLimit && maxFeePerGas
? formatVisualAmount(
Expand Down Expand Up @@ -244,6 +248,10 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps<NewSafe
</Grid>

{isWrongChain && <NetworkWarning />}

{!walletCanPay && !willRelay && (
<ErrorMessage>Your connected wallet doesn&apos;t have enough funds to execute this transaction</ErrorMessage>
)}
</Box>

<Divider />
Expand Down
1 change: 1 addition & 0 deletions src/components/tx/BalanceInfo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import css from './styles.module.css'
import useWalletBalance from '@/hooks/wallets/useWalletBalance'
import WalletBalance from '@/components/common/WalletBalance'

// TODO: Remove this component if not being used
const BalanceInfo = () => {
const [balance] = useWalletBalance()

Expand Down
11 changes: 9 additions & 2 deletions src/components/tx/SignOrExecuteForm/ExecuteForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import BalanceInfo from '@/components/tx/BalanceInfo'
import useWalletCanPay from '@/hooks/useWalletCanPay'
import madProps from '@/utils/mad-props'
import { type ReactElement, type SyntheticEvent, useContext, useState } from 'react'
import { CircularProgress, Box, Button, CardActions, Divider } from '@mui/material'
Expand Down Expand Up @@ -107,6 +107,12 @@ export const ExecuteForm = ({
setTxFlow(<SuccessScreen txId={executedTxId} />, undefined, false)
}

const walletCanPay = useWalletCanPay({
gasLimit,
maxFeePerGas: advancedParams.maxFeePerGas,
maxPriorityFeePerGas: advancedParams.maxPriorityFeePerGas,
})

const cannotPropose = !isOwner && !onlyExecute
const submitDisabled =
!safeTx ||
Expand All @@ -128,7 +134,6 @@ export const ExecuteForm = ({
gasLimitError={gasLimitError}
willRelay={willRelay}
/>
{!canRelay && <BalanceInfo />}

{canRelay && (
<div className={css.noTopBorder}>
Expand All @@ -148,6 +153,8 @@ export const ExecuteForm = ({
<ErrorMessage>
Cannot execute a transaction from the Safe Account itself, please connect a different account.
</ErrorMessage>
) : !walletCanPay && !willRelay ? (
<ErrorMessage>Your connected wallet doesn&apos;t have enough funds to execute this transaction.</ErrorMessage>
) : (
(executionValidationError || gasLimitError) && (
<ErrorMessage error={executionValidationError || gasLimitError}>
Expand Down
20 changes: 20 additions & 0 deletions src/components/tx/SignOrExecuteForm/__tests__/ExecuteForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as useGasLimit from '@/hooks/useGasLimit'
import * as useIsValidExecution from '@/hooks/useIsValidExecution'
import * as useWalletCanRelay from '@/hooks/useWalletCanRelay'
import * as relayUtils from '@/utils/relaying'
import * as walletCanPay from '@/hooks/useWalletCanPay'
import { render } from '@/tests/test-utils'
import { fireEvent, waitFor } from '@testing-library/react'

Expand Down Expand Up @@ -70,6 +71,25 @@ describe.only('ExecuteForm', () => {
).toBeInTheDocument()
})

it('shows an error if the connected wallet has insufficient funds to execute and relaying is not selected', () => {
jest.spyOn(walletCanPay, 'default').mockReturnValue(false)
jest.spyOn(useWalletCanRelay, 'default').mockReturnValue([true, undefined, false])
jest.spyOn(relayUtils, 'hasRemainingRelays').mockReturnValue(true)

const { getByText, queryByText, getByTestId } = render(<ExecuteForm {...defaultProps} />)

expect(
queryByText("Your connected wallet doesn't have enough funds to execute this transaction."),
).not.toBeInTheDocument()

const executeWithWalletOption = getByTestId('connected-wallet-execution-method')
fireEvent.click(executeWithWalletOption)

expect(
getByText("Your connected wallet doesn't have enough funds to execute this transaction."),
).toBeInTheDocument()
})

it('shows a relaying option if relaying is enabled', () => {
jest.spyOn(useWalletCanRelay, 'default').mockReturnValue([true, undefined, false])
jest.spyOn(relayUtils, 'hasRemainingRelays').mockReturnValue(true)
Expand Down
80 changes: 80 additions & 0 deletions src/hooks/__tests__/useWalletCanPay.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import useWalletCanPay from '@/hooks/useWalletCanPay'
import * as walletBalance from '@/hooks/wallets/useWalletBalance'
import { renderHook } from '@/tests/test-utils'
import { BigNumber } from 'ethers'

describe('useWalletCanPay', () => {
beforeEach(() => {
jest.clearAllMocks()
})

it('should return true if gasLimit is missing', () => {
const { result } = renderHook(() =>
useWalletCanPay({ maxFeePerGas: BigNumber.from(1), maxPriorityFeePerGas: BigNumber.from(1) }),
)

expect(result.current).toEqual(true)
})

it('should return true if maxFeePerGas is missing', () => {
const { result } = renderHook(() =>
useWalletCanPay({ gasLimit: BigNumber.from(21000), maxPriorityFeePerGas: BigNumber.from(1) }),
)

expect(result.current).toEqual(true)
})

it('should return true if wallet balance is missing', () => {
jest.spyOn(walletBalance, 'default').mockReturnValue([undefined, undefined, false])

const { result } = renderHook(() =>
useWalletCanPay({ gasLimit: BigNumber.from(21000), maxFeePerGas: BigNumber.from(1) }),
)

expect(result.current).toEqual(true)
})

it('should return false if wallet balance is smaller than gas costs', () => {
jest.spyOn(walletBalance, 'default').mockReturnValue([BigNumber.from(20999), undefined, false])

const { result } = renderHook(() =>
useWalletCanPay({ gasLimit: BigNumber.from(21000), maxFeePerGas: BigNumber.from(1) }),
)

expect(result.current).toEqual(false)
})

it('should return true if wallet balance is larger or equal than gas costs', () => {
jest.spyOn(walletBalance, 'default').mockReturnValue([BigNumber.from(21000), undefined, false])

const { result } = renderHook(() =>
useWalletCanPay({ gasLimit: BigNumber.from(21000), maxFeePerGas: BigNumber.from(1) }),
)

expect(result.current).toEqual(true)
})

it('should return true if wallet balance is larger or equal than gas costs', () => {
jest.spyOn(walletBalance, 'default').mockReturnValue([BigNumber.from(21001), undefined, false])

const { result } = renderHook(() =>
useWalletCanPay({ gasLimit: BigNumber.from(21000), maxFeePerGas: BigNumber.from(1) }),
)

expect(result.current).toEqual(true)
})

it('should take maxPriorityFeePerGas into account', () => {
jest.spyOn(walletBalance, 'default').mockReturnValue([BigNumber.from(42000), undefined, false])

const { result } = renderHook(() =>
useWalletCanPay({
gasLimit: BigNumber.from(21000),
maxFeePerGas: BigNumber.from(1),
maxPriorityFeePerGas: BigNumber.from(1),
}),
)

expect(result.current).toEqual(true)
})
})
24 changes: 24 additions & 0 deletions src/hooks/useWalletCanPay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import useWalletBalance from '@/hooks/wallets/useWalletBalance'
import { BigNumber } from 'ethers'

const useWalletCanPay = ({
gasLimit,
maxFeePerGas,
maxPriorityFeePerGas,
}: {
gasLimit?: BigNumber
maxFeePerGas?: BigNumber | null
maxPriorityFeePerGas?: BigNumber | null
}) => {
const [walletBalance] = useWalletBalance()

// Take an optimistic approach and assume the wallet can pay
// if gasLimit, maxFeePerGas or their walletBalance are missing
if (!gasLimit || !maxFeePerGas || !walletBalance) return true

const totalFee = maxFeePerGas.add(maxPriorityFeePerGas || BigNumber.from(0)).mul(gasLimit)

return walletBalance.gte(totalFee)
}

export default useWalletCanPay

0 comments on commit fbf7ac9

Please sign in to comment.