Skip to content

Commit

Permalink
fix: Validation for spending limits (#2277)
Browse files Browse the repository at this point in the history
* fix: Validation for spending limits

* fix: Reset amount field to empty string instead of 0 when switching tokens

* fix: Add readonly addressbook input to spending limits
  • Loading branch information
usame-algan authored Jul 12, 2023
1 parent f487649 commit cbb24e3
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 35 deletions.
24 changes: 16 additions & 8 deletions src/components/common/TokenAmountInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AutocompleteItem } from '@/components/tx-flow/flows/TokenTransfer/Creat
import { useFormContext } from 'react-hook-form'
import { type BigNumber } from '@ethersproject/bignumber'
import classNames from 'classnames'
import { useCallback } from 'react'

export enum TokenAmountFields {
tokenAddress = 'tokenAddress',
Expand All @@ -18,11 +19,13 @@ const TokenAmountInput = ({
selectedToken,
onMaxAmountClick,
maxAmount,
validate,
}: {
balances: SafeBalanceResponse['items']
selectedToken: SafeBalanceResponse['items'][number] | undefined
onMaxAmountClick: () => void
maxAmount: BigNumber
onMaxAmountClick?: () => void
maxAmount?: BigNumber
validate?: (value: string) => string | undefined
}) => {
const {
formState: { errors },
Expand All @@ -34,6 +37,14 @@ const TokenAmountInput = ({
const tokenAddress = watch(TokenAmountFields.tokenAddress)
const isAmountError = !!errors[TokenAmountFields.tokenAddress] || !!errors[TokenAmountFields.amount]

const validateAmount = useCallback(
(value: string) => {
const decimals = selectedToken?.tokenInfo.decimals
return validateLimitedAmount(value, decimals, maxAmount?.toString()) || validateDecimalLength(value, decimals)
},
[maxAmount, selectedToken?.tokenInfo.decimals],
)

return (
<FormControl className={classNames(css.outline, { [css.error]: isAmountError })} fullWidth>
<InputLabel shrink required className={css.label}>
Expand All @@ -44,7 +55,7 @@ const TokenAmountInput = ({
variant="standard"
InputProps={{
disableUnderline: true,
endAdornment: (
endAdornment: onMaxAmountClick && (
<Button className={css.max} onClick={onMaxAmountClick}>
Max
</Button>
Expand All @@ -55,10 +66,7 @@ const TokenAmountInput = ({
placeholder="0"
{...register(TokenAmountFields.amount, {
required: true,
validate: (val) => {
const decimals = selectedToken?.tokenInfo.decimals
return validateLimitedAmount(val, decimals, maxAmount.toString()) || validateDecimalLength(val, decimals)
},
validate: validate ?? validateAmount,
})}
/>
<Divider orientation="vertical" flexItem />
Expand All @@ -72,7 +80,7 @@ const TokenAmountInput = ({
{...register(TokenAmountFields.tokenAddress, {
required: true,
onChange: () => {
resetField(TokenAmountFields.amount, { defaultValue: '0' })
resetField(TokenAmountFields.amount, { defaultValue: '' })
},
})}
value={tokenAddress}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useMemo } from 'react'
import { useCallback, useMemo, useState } from 'react'
import { Controller, FormProvider, useForm } from 'react-hook-form'
import { Button, CardActions, FormControl, InputLabel, MenuItem, Select, Typography } from '@mui/material'
import { Box, Button, CardActions, FormControl, InputLabel, MenuItem, Select, Typography } from '@mui/material'
import ExpandMoreRoundedIcon from '@mui/icons-material/ExpandMoreRounded'
import { defaultAbiCoder, parseUnits } from 'ethers/lib/utils'

Expand All @@ -12,9 +12,10 @@ import type { NewSpendingLimitFlowProps } from '.'
import TxCard from '../../common/TxCard'
import css from '@/components/tx/ExecuteCheckbox/styles.module.css'
import TokenAmountInput from '@/components/common/TokenAmountInput'
import { BigNumber } from '@ethersproject/bignumber'
import { safeFormatUnits } from '@/utils/formatters'
import { SpendingLimitFields } from '.'
import { validateAmount, validateDecimalLength } from '@/utils/validation'
import AddressInputReadOnly from '@/components/common/AddressInputReadOnly'
import useAddressBook from '@/hooks/useAddressBook'

export const _validateSpendingLimit = (val: string, decimals?: number) => {
// Allowance amount is uint96 https://github.com/safe-global/safe-modules/blob/master/allowances/contracts/AlowanceModule.sol#L52
Expand All @@ -33,8 +34,10 @@ export const CreateSpendingLimit = ({
params: NewSpendingLimitFlowProps
onSubmit: (data: NewSpendingLimitFlowProps) => void
}) => {
const [recipientFocus, setRecipientFocus] = useState(!params.beneficiary)
const chainId = useChainId()
const { balances } = useVisibleBalances()
const addressBook = useAddressBook()

const resetTimeOptions = useMemo(() => getResetTimeOptions(chainId), [chainId])

Expand All @@ -45,37 +48,43 @@ export const CreateSpendingLimit = ({

const { handleSubmit, setValue, watch, control } = formMethods

const beneficiary = watch(SpendingLimitFields.beneficiary)
const tokenAddress = watch(SpendingLimitFields.tokenAddress)
const selectedToken = tokenAddress
? balances.items.find((item) => item.tokenInfo.address === tokenAddress)
: undefined

const totalAmount = BigNumber.from(selectedToken?.balance || 0)

const onMaxAmountClick = () => {
if (!selectedToken) return

const amount = selectedToken.balance

setValue(SpendingLimitFields.amount, safeFormatUnits(amount, selectedToken.tokenInfo.decimals), {
shouldValidate: true,
})
}
const validateSpendingLimit = useCallback(
(value: string) => {
return (
validateAmount(value) ||
validateDecimalLength(value, selectedToken?.tokenInfo.decimals) ||
_validateSpendingLimit(value, selectedToken?.tokenInfo.decimals)
)
},
[selectedToken?.tokenInfo.decimals],
)

return (
<TxCard>
<FormProvider {...formMethods}>
<form onSubmit={handleSubmit(onSubmit)}>
<FormControl fullWidth sx={{ mb: 3 }}>
<AddressBookInput name={SpendingLimitFields.beneficiary} label="Beneficiary" />
{addressBook[beneficiary] ? (
<Box
onClick={() => {
setValue(SpendingLimitFields.beneficiary, '')
setRecipientFocus(true)
}}
>
<AddressInputReadOnly label="Sending to" address={beneficiary} />
</Box>
) : (
<AddressBookInput name={SpendingLimitFields.beneficiary} label="Beneficiary" focused={recipientFocus} />
)}
</FormControl>

<TokenAmountInput
balances={balances.items}
selectedToken={selectedToken}
maxAmount={totalAmount}
onMaxAmountClick={onMaxAmountClick}
/>
<TokenAmountInput balances={balances.items} selectedToken={selectedToken} validate={validateSpendingLimit} />

<Typography variant="h4" fontWeight={700} mt={3}>
Reset Timer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import useChainId from '@/hooks/useChainId'
import { trackEvent, SETTINGS_EVENTS } from '@/services/analytics'
import { createNewSpendingLimitTx } from '@/services/tx/tx-sender'
import { selectSpendingLimits } from '@/store/spendingLimitsSlice'
import { relativeTime } from '@/utils/date'
import { formatVisualAmount } from '@/utils/formatters'
import type { SpendingLimitState } from '@/store/spendingLimitsSlice'
import type { NewSpendingLimitFlowProps } from '.'
Expand Down Expand Up @@ -56,14 +55,22 @@ export const ReviewSpendingLimit = ({ params }: { params: NewSpendingLimitFlowPr
})
}

const existingAmount = existingSpendingLimit
? formatVisualAmount(BigNumber.from(existingSpendingLimit?.amount), decimals)
: undefined

const oldResetTime = existingSpendingLimit
? getResetTimeOptions(chainId).find((time) => time.value === existingSpendingLimit?.resetTimeMin)?.label
: undefined

return (
<SignOrExecuteForm onSubmit={onFormSubmit}>
{token && (
<SendAmountBlock amount={params.amount} tokenInfo={token.tokenInfo} title="Amount">
{!!existingSpendingLimit && (
{existingAmount && existingAmount !== params.amount && (
<>
<Typography color="error" sx={{ textDecoration: 'line-through' }} component="span">
{formatVisualAmount(BigNumber.from(existingSpendingLimit.amount), decimals)}
{existingAmount}
</Typography>
{'→'}
</>
Expand Down Expand Up @@ -109,7 +116,7 @@ export const ReviewSpendingLimit = ({ params }: { params: NewSpendingLimitFlowPr
display="inline"
component="span"
>
{relativeTime(existingSpendingLimit.lastResetMin, existingSpendingLimit.resetTimeMin)}
{oldResetTime}
</Typography>
{' → '}
</>
Expand Down
2 changes: 1 addition & 1 deletion src/components/tx-flow/flows/RemoveGuard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type RemoveGuardFlowProps = {

const RemoveGuardFlow = ({ address }: RemoveGuardFlowProps) => {
return (
<TxLayout title="Remove guard">
<TxLayout title="Confirm transaction" subtitle="Remove guard">
<ReviewRemoveGuard params={{ address }} />
</TxLayout>
)
Expand Down

0 comments on commit cbb24e3

Please sign in to comment.