fix: withdrawal max threshold (#830)

* chore: fix remaning text for large number

* feat: make use max only use account balance, add custom max messages

* fix: withdraw threshold limit display

* fix: assertions in deposit withdrawy e2e tests
This commit is contained in:
Matthew Russell 2022-07-21 09:18:57 +01:00 committed by GitHub
parent 0964d6dee5
commit 065b48535b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 129 additions and 83 deletions

View File

@ -53,6 +53,6 @@ describe('deposit form validation', () => {
.clear() .clear()
.type('100') .type('100')
.next(`[data-testid="${formFieldError}"]`) .next(`[data-testid="${formFieldError}"]`)
.should('have.text', 'Amount is above approved amount'); .should('have.text', 'Insufficient amount in Ethereum wallet');
}); });
}); });

View File

@ -58,7 +58,7 @@ describe('withdraw', () => {
.clear() .clear()
.type('1') // Will be above maximum because the vega wallet doesnt have any collateral .type('1') // Will be above maximum because the vega wallet doesnt have any collateral
.next('[data-testid="input-error-text"]') .next('[data-testid="input-error-text"]')
.should('contain.text', 'Value is above maximum'); .should('contain.text', 'Insufficient amount in account');
}); });
it('can set amount using use maximum button', () => { it('can set amount using use maximum button', () => {

View File

@ -33,7 +33,7 @@ beforeEach(() => {
assets: [asset], assets: [asset],
selectedAsset: undefined, selectedAsset: undefined,
onSelectAsset: jest.fn(), onSelectAsset: jest.fn(),
available: new BigNumber(5), balance: new BigNumber(5),
submitApprove: jest.fn(), submitApprove: jest.fn(),
submitDeposit: jest.fn(), submitDeposit: jest.fn(),
requestFaucet: jest.fn(), requestFaucet: jest.fn(),
@ -125,7 +125,7 @@ describe('Deposit form', () => {
fireEvent.submit(screen.getByTestId('deposit-form')); fireEvent.submit(screen.getByTestId('deposit-form'));
expect( expect(
await screen.findByText('Amount is above permitted maximum') await screen.findByText('Insufficient amount in Ethereum wallet')
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@ -133,6 +133,7 @@ describe('Deposit form', () => {
render( render(
<DepositForm <DepositForm
{...props} {...props}
balance={new BigNumber(100)}
limits={{ max: new BigNumber(100), deposited: new BigNumber(10) }} limits={{ max: new BigNumber(100), deposited: new BigNumber(10) }}
/> />
); );
@ -216,12 +217,13 @@ describe('Deposit form', () => {
max: new BigNumber(20), max: new BigNumber(20),
deposited: new BigNumber(10), deposited: new BigNumber(10),
}; };
const balance = new BigNumber(50);
render( render(
<DepositForm <DepositForm
{...props} {...props}
allowance={new BigNumber(100)} allowance={new BigNumber(100)}
available={new BigNumber(50)} balance={balance}
limits={limits} limits={limits}
selectedAsset={asset} selectedAsset={asset}
/> />
@ -229,12 +231,18 @@ describe('Deposit form', () => {
// Check deposit limit is displayed // Check deposit limit is displayed
expect( expect(
screen.getByText('Max deposit total', { selector: 'th' }) screen.getByText('Balance available', { selector: 'th' })
.nextElementSibling
).toHaveTextContent(balance.toString());
expect(
screen.getByText('Maximum total deposit amount', { selector: 'th' })
.nextElementSibling .nextElementSibling
).toHaveTextContent(limits.max.toString()); ).toHaveTextContent(limits.max.toString());
expect( expect(
screen.getByText('Remaining available', { selector: 'th' }) screen.getByText('Deposited', { selector: 'th' }).nextElementSibling
.nextElementSibling ).toHaveTextContent(limits.deposited.toString());
expect(
screen.getByText('Remaining', { selector: 'th' }).nextElementSibling
).toHaveTextContent(limits.max.minus(limits.deposited).toString()); ).toHaveTextContent(limits.max.minus(limits.deposited).toString());
fireEvent.change(screen.getByLabelText('Amount'), { fireEvent.change(screen.getByLabelText('Amount'), {

View File

@ -37,7 +37,7 @@ export interface DepositFormProps {
assets: Asset[]; assets: Asset[];
selectedAsset?: Asset; selectedAsset?: Asset;
onSelectAsset: (assetId: string) => void; onSelectAsset: (assetId: string) => void;
available: BigNumber | undefined; balance: BigNumber | undefined;
submitApprove: () => Promise<void>; submitApprove: () => Promise<void>;
submitDeposit: (args: { submitDeposit: (args: {
assetSource: string; assetSource: string;
@ -57,7 +57,7 @@ export const DepositForm = ({
assets, assets,
selectedAsset, selectedAsset,
onSelectAsset, onSelectAsset,
available, balance,
submitApprove, submitApprove,
submitDeposit, submitDeposit,
requestFaucet, requestFaucet,
@ -99,7 +99,7 @@ export const DepositForm = ({
const max = useMemo(() => { const max = useMemo(() => {
const maxApproved = allowance ? allowance : new BigNumber(0); const maxApproved = allowance ? allowance : new BigNumber(0);
const maxAvailable = available ? available : new BigNumber(0); const maxAvailable = balance ? balance : new BigNumber(0);
// limits.max is a lifetime deposit limit, so the actual max value for form // limits.max is a lifetime deposit limit, so the actual max value for form
// input is the max minus whats already been deposited // input is the max minus whats already been deposited
@ -116,7 +116,7 @@ export const DepositForm = ({
limit: maxLimit, limit: maxLimit,
amount: BigNumber.minimum(maxLimit, maxApproved, maxAvailable), amount: BigNumber.minimum(maxLimit, maxApproved, maxAvailable),
}; };
}, [limits, allowance, available]); }, [limits, allowance, balance]);
const min = useMemo(() => { const min = useMemo(() => {
// Min viable amount given asset decimals EG for WEI 0.000000000000000001 // Min viable amount given asset decimals EG for WEI 0.000000000000000001
@ -198,7 +198,7 @@ export const DepositForm = ({
</FormGroup> </FormGroup>
{selectedAsset && limits && ( {selectedAsset && limits && (
<div className="mb-20"> <div className="mb-20">
<DepositLimits limits={limits} /> <DepositLimits limits={limits} balance={balance} />
</div> </div>
)} )}
<FormGroup label={t('Amount')} labelFor="amount" className="relative"> <FormGroup label={t('Amount')} labelFor="amount" className="relative">
@ -212,12 +212,12 @@ export const DepositForm = ({
minSafe: (value) => minSafe(new BigNumber(min))(value), minSafe: (value) => minSafe(new BigNumber(min))(value),
maxSafe: (v) => { maxSafe: (v) => {
const value = new BigNumber(v); const value = new BigNumber(v);
if (value.isGreaterThan(max.approved)) { if (value.isGreaterThan(max.available)) {
return t('Amount is above approved amount');
} else if (value.isGreaterThan(max.limit)) {
return t('Amount is above permitted maximum');
} else if (value.isGreaterThan(max.available)) {
return t('Insufficient amount in Ethereum wallet'); return t('Insufficient amount in Ethereum wallet');
} else if (value.isGreaterThan(max.limit)) {
return t('Amount is above temporary deposit limit');
} else if (value.isGreaterThan(max.approved)) {
return t('Amount is above approved amount');
} }
return maxSafe(max.amount)(v); return maxSafe(max.amount)(v);
}, },
@ -229,10 +229,10 @@ export const DepositForm = ({
{errors.amount.message} {errors.amount.message}
</InputError> </InputError>
)} )}
{account && selectedAsset && available && ( {selectedAsset && balance && (
<UseButton <UseButton
onClick={() => { onClick={() => {
setValue('amount', max.amount.toFixed(selectedAsset.decimals)); setValue('amount', balance.toFixed(selectedAsset.decimals));
clearErrors('amount'); clearErrors('amount');
}} }}
> >

View File

@ -6,11 +6,11 @@ interface DepositLimitsProps {
max: BigNumber; max: BigNumber;
deposited: BigNumber; deposited: BigNumber;
}; };
balance?: BigNumber;
} }
export const DepositLimits = ({ limits }: DepositLimitsProps) => { export const DepositLimits = ({ limits, balance }: DepositLimitsProps) => {
let maxLimit = ''; let maxLimit = '';
if (limits.max.isEqualTo(Infinity)) { if (limits.max.isEqualTo(Infinity)) {
maxLimit = t('No limit'); maxLimit = t('No limit');
} else if (limits.max.isGreaterThan(1_000_000)) { } else if (limits.max.isGreaterThan(1_000_000)) {
@ -19,29 +19,35 @@ export const DepositLimits = ({ limits }: DepositLimitsProps) => {
maxLimit = limits.max.toString(); maxLimit = limits.max.toString();
} }
let remaining = '';
if (limits.deposited.isEqualTo(0)) {
remaining = maxLimit;
} else {
remaining = limits.max.minus(limits.deposited).toString();
}
return ( return (
<> <table className="w-full text-ui">
<p className="text-ui font-bold">{t('Deposit limits')}</p> <tbody>
<table className="w-full text-ui"> <tr>
<tbody> <th className="text-left font-normal">{t('Balance available')}</th>
<tr> <td className="text-right">{balance ? balance.toString() : 0}</td>
<th className="text-left font-normal">{t('Max deposit total')}</th> </tr>
<td className="text-right">{maxLimit}</td> <tr>
</tr> <th className="text-left font-normal">
<tr> {t('Maximum total deposit amount')}
<th className="text-left font-normal">{t('Deposited')}</th> </th>
<td className="text-right">{limits.deposited.toString()}</td> <td className="text-right">{maxLimit}</td>
</tr> </tr>
<tr> <tr>
<th className="text-left font-normal"> <th className="text-left font-normal">{t('Deposited')}</th>
{t('Remaining available')} <td className="text-right">{limits.deposited.toString()}</td>
</th> </tr>
<td className="text-right"> <tr>
{limits.max.minus(limits.deposited).toString()} <th className="text-left font-normal">{t('Remaining')}</th>
</td> <td className="text-right">{remaining}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</>
); );
}; };

View File

@ -105,7 +105,7 @@ export const DepositManager = ({
return ( return (
<> <>
<DepositForm <DepositForm
available={balance} balance={balance}
selectedAsset={asset} selectedAsset={asset}
onSelectAsset={(id) => setAssetId(id)} onSelectAsset={(id) => setAssetId(id)}
assets={sortBy(assets, 'name')} assets={sortBy(assets, 'name')}

View File

@ -20,9 +20,11 @@ export const useGetWithdrawLimits = (asset?: Asset) => {
if (!data || !asset) return null; if (!data || !asset) return null;
const max = new BigNumber(addDecimal(data.toString(), asset.decimals)); const value = new BigNumber(addDecimal(data.toString(), asset.decimals));
const max = value.isEqualTo(0)
? new BigNumber(Infinity)
: value.minus(new BigNumber(addDecimal('1', asset.decimals)));
return { return {
max: max.isEqualTo(0) ? new BigNumber(Infinity) : max, max,
}; };
}; };

View File

@ -26,7 +26,10 @@ beforeEach(() => {
props = { props = {
assets, assets,
min: new BigNumber(0.00001), min: new BigNumber(0.00001),
max: new BigNumber(100), max: {
balance: new BigNumber(100),
threshold: new BigNumber(200),
},
limits: { limits: {
max: new BigNumber(200), max: new BigNumber(200),
}, },
@ -75,7 +78,9 @@ describe('Withdrawal form', () => {
expect( expect(
await screen.findByText('Invalid Ethereum address') await screen.findByText('Invalid Ethereum address')
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.getByText('Value is above maximum')).toBeInTheDocument(); expect(
screen.getByText('Insufficient amount in account')
).toBeInTheDocument();
}); });
it('fails when submitted amount is less than the minimum limit', async () => { it('fails when submitted amount is less than the minimum limit', async () => {
@ -111,14 +116,14 @@ describe('Withdrawal form', () => {
}); });
}); });
it('populates amount field with maximum value when clicking the "use maximum" button', () => { it('populates amount field with balance value when clicking the "use maximum" button', () => {
const asset = props.assets[0]; const asset = props.assets[0];
render(<WithdrawForm {...props} selectedAsset={asset} />); render(<WithdrawForm {...props} selectedAsset={asset} />);
fireEvent.click(screen.getByText('Use maximum')); fireEvent.click(screen.getByText('Use maximum'));
expect(screen.getByLabelText('Amount')).toHaveValue( expect(screen.getByLabelText('Amount')).toHaveValue(
Number(props.max.toFixed(asset.decimals)) Number(props.max.balance.toFixed(asset.decimals))
); );
}); });
}); });

View File

@ -1,10 +1,10 @@
import { import {
ethereumAddress, ethereumAddress,
maxSafe,
minSafe, minSafe,
t, t,
removeDecimal, removeDecimal,
required, required,
maxSafe,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import { import {
Button, Button,
@ -15,7 +15,7 @@ import {
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { Web3WalletInput } from '@vegaprotocol/web3'; import { Web3WalletInput } from '@vegaprotocol/web3';
import { useWeb3React } from '@web3-react/core'; import { useWeb3React } from '@web3-react/core';
import type BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import type { ButtonHTMLAttributes, ReactNode } from 'react'; import type { ButtonHTMLAttributes, ReactNode } from 'react';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import type { WithdrawalFields } from './use-withdraw'; import type { WithdrawalFields } from './use-withdraw';
@ -30,7 +30,10 @@ interface FormFields {
export interface WithdrawFormProps { export interface WithdrawFormProps {
assets: Asset[]; assets: Asset[];
max: BigNumber; max: {
balance: BigNumber;
threshold: BigNumber;
};
min: BigNumber; min: BigNumber;
selectedAsset?: Asset; selectedAsset?: Asset;
limits: { limits: {
@ -49,7 +52,7 @@ export const WithdrawForm = ({
onSelectAsset, onSelectAsset,
submitWithdraw, submitWithdraw,
}: WithdrawFormProps) => { }: WithdrawFormProps) => {
const { account } = useWeb3React(); const { account: address } = useWeb3React();
const { const {
register, register,
handleSubmit, handleSubmit,
@ -60,7 +63,7 @@ export const WithdrawForm = ({
} = useForm<FormFields>({ } = useForm<FormFields>({
defaultValues: { defaultValues: {
asset: selectedAsset?.id, asset: selectedAsset?.id,
to: account, to: address,
}, },
}); });
const onSubmit = async (fields: FormFields) => { const onSubmit = async (fields: FormFields) => {
@ -106,7 +109,6 @@ export const WithdrawForm = ({
</Select> </Select>
)} )}
/> />
{errors.asset?.message && ( {errors.asset?.message && (
<InputError intent="danger" className="mt-4"> <InputError intent="danger" className="mt-4">
{errors.asset.message} {errors.asset.message}
@ -132,7 +134,7 @@ export const WithdrawForm = ({
</FormGroup> </FormGroup>
{selectedAsset && limits && ( {selectedAsset && limits && (
<div className="mb-20"> <div className="mb-20">
<WithdrawLimits limits={limits} /> <WithdrawLimits limits={limits} balance={max.balance} />
</div> </div>
)} )}
<FormGroup label={t('Amount')} labelFor="amount" className="relative"> <FormGroup label={t('Amount')} labelFor="amount" className="relative">
@ -143,7 +145,17 @@ export const WithdrawForm = ({
{...register('amount', { {...register('amount', {
validate: { validate: {
required, required,
maxSafe: (value) => maxSafe(max)(value), maxSafe: (v) => {
const value = new BigNumber(v);
if (value.isGreaterThan(max.balance)) {
return t('Insufficient amount in account');
} else if (value.isGreaterThan(max.threshold)) {
return t('Amount is above temporary withdrawal limit');
}
return maxSafe(BigNumber.minimum(max.balance, max.threshold))(
v
);
},
minSafe: (value) => minSafe(min)(value), minSafe: (value) => minSafe(min)(value),
}, },
})} })}
@ -157,7 +169,7 @@ export const WithdrawForm = ({
<UseButton <UseButton
data-testid="use-maximum" data-testid="use-maximum"
onClick={() => { onClick={() => {
setValue('amount', max.toFixed(selectedAsset.decimals)); setValue('amount', max.balance.toFixed(selectedAsset.decimals));
clearErrors('amount'); clearErrors('amount');
}} }}
> >

View File

@ -5,9 +5,10 @@ interface WithdrawLimitsProps {
limits: { limits: {
max: BigNumber; max: BigNumber;
}; };
balance: BigNumber;
} }
export const WithdrawLimits = ({ limits }: WithdrawLimitsProps) => { export const WithdrawLimits = ({ limits, balance }: WithdrawLimitsProps) => {
let maxLimit = ''; let maxLimit = '';
if (limits.max.isEqualTo(Infinity)) { if (limits.max.isEqualTo(Infinity)) {
@ -19,16 +20,17 @@ export const WithdrawLimits = ({ limits }: WithdrawLimitsProps) => {
} }
return ( return (
<> <table className="w-full text-ui">
<p className="text-ui font-bold">{t('Withdraw limits')}</p> <tbody>
<table className="w-full text-ui"> <tr>
<tbody> <th className="text-left font-normal">{t('Balance available')}</th>
<tr> <td className="text-right">{balance.toString()}</td>
<th className="text-left font-normal">{t('Maximum')}</th> </tr>
<td className="text-right">{maxLimit}</td> <tr>
</tr> <th className="text-left font-normal">{t('Maximum withdrawal')}</th>
</tbody> <td className="text-right">{maxLimit}</td>
</table> </tr>
</> </tbody>
</table>
); );
}; };

View File

@ -111,7 +111,9 @@ it('Correct min max values provided to form', async () => {
target: { value: '2' }, target: { value: '2' },
}); });
fireEvent.submit(screen.getByTestId('withdraw-form')); fireEvent.submit(screen.getByTestId('withdraw-form'));
expect(await screen.findByText('Value is above maximum')).toBeInTheDocument(); expect(
await screen.findByText('Insufficient amount in account')
).toBeInTheDocument();
expect(mockSubmit).not.toBeCalled(); expect(mockSubmit).not.toBeCalled();
}); });

View File

@ -38,22 +38,31 @@ export const WithdrawManager = ({
return assets?.find((a) => a.id === assetId); return assets?.find((a) => a.id === assetId);
}, [assets, assetId]); }, [assets, assetId]);
const account = useMemo(() => {
return accounts.find(
(a) => a.type === AccountType.General && a.asset.id === asset?.id
);
}, [asset, accounts]);
const limits = useGetWithdrawLimits(asset); const limits = useGetWithdrawLimits(asset);
const max = useMemo(() => { const max = useMemo(() => {
if (!asset) { if (!asset) {
return new BigNumber(0); return {
balance: new BigNumber(0),
threshold: new BigNumber(0),
};
} }
const account = accounts.find( const balance = account
(a) => a.type === AccountType.General && a.asset.id === asset.id
);
const v = account
? new BigNumber(addDecimal(account.balance, asset.decimals)) ? new BigNumber(addDecimal(account.balance, asset.decimals))
: new BigNumber(0); : new BigNumber(0);
return BigNumber.minimum(v, limits ? limits.max : new BigNumber(Infinity));
}, [asset, accounts, limits]); return {
balance,
threshold: limits ? limits.max : new BigNumber(Infinity),
};
}, [asset, account, limits]);
const min = useMemo(() => { const min = useMemo(() => {
return asset return asset