feat(withdraws): improve ux surrounding withdraw balances thresholds and delays (#3402)

Co-authored-by: m.ray <16125548+MadalinaRaicu@users.noreply.github.com>
This commit is contained in:
Bartłomiej Głownia 2023-04-12 11:21:24 +02:00 committed by GitHub
parent 45f2e926c3
commit bbfda65bcc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 148 additions and 30 deletions

View File

@ -11,6 +11,7 @@ import {
} from '@vegaprotocol/ui-toolkit';
import type { ReactNode } from 'react';
import type { Asset } from './asset-data-provider';
import { WITHDRAW_THRESHOLD_TOOLTIP_TEXT } from './constants';
type Rows = {
key: AssetDetail;
@ -121,9 +122,7 @@ export const rows: Rows = [
{
key: AssetDetail.WITHDRAWAL_THRESHOLD,
label: t('Withdrawal threshold'),
tooltip: t(
'The maximum you can withdraw instantly. Theres no limit on the size of a withdrawal, but all withdrawals over the threshold will have a delay time added to them'
),
tooltip: WITHDRAW_THRESHOLD_TOOLTIP_TEXT,
value: (asset) =>
num(asset, (asset.source as Schema.ERC20).withdrawThreshold),
},

View File

@ -0,0 +1,4 @@
import { t } from '@vegaprotocol/i18n';
export const WITHDRAW_THRESHOLD_TOOLTIP_TEXT = t(
"The maximum you can withdraw instantly. There's no limit on the size of a withdrawal, but all withdrawals over the threshold will have a delay time added to them"
);

View File

@ -5,3 +5,4 @@ export * from './assets-data-provider';
export * from './asset-details-dialog';
export * from './asset-details-table';
export * from './asset-option';
export * from './constants';

View File

@ -70,6 +70,8 @@ export const Notification = ({
'text-vega-green dark:text-vega-green': intent === Intent.Success,
'text-yellow-600 dark:text-yellow': intent === Intent.Warning,
'text-vega-pink': intent === Intent.Danger,
'mt-1': !!title,
'mt-[0.125rem]': !title,
},
'flex items-start mt-1'
)}
@ -78,11 +80,16 @@ export const Notification = ({
</div>
<div className="flex flex-col flex-grow items-start gap-1.5">
{title && (
<div className="whitespace-nowrap overflow-hidden text-ellipsis uppercase leading-6">
<div
key="title"
className="whitespace-nowrap overflow-hidden text-ellipsis uppercase leading-6"
>
{title}
</div>
)}
<div className="text-sm [word-break:break-word]">{message}</div>
<div key="message" className="text-sm [word-break:break-word]">
{message}
</div>
{buttonProps && (
<Button
size={buttonProps.size || 'sm'}

View File

@ -6,6 +6,7 @@ import {
removeDecimal,
required,
isAssetTypeERC20,
formatNumber,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
@ -14,12 +15,16 @@ import {
FormGroup,
Input,
InputError,
Notification,
RichSelect,
ExternalLink,
Intent,
} from '@vegaprotocol/ui-toolkit';
import { useWeb3React } from '@web3-react/core';
import BigNumber from 'bignumber.js';
import type { ButtonHTMLAttributes } from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { formatDistanceToNow } from 'date-fns';
import { useForm, Controller, useWatch } from 'react-hook-form';
import type { WithdrawalArgs } from './use-create-withdraw';
import { WithdrawLimits } from './withdraw-limits';
@ -45,6 +50,47 @@ export interface WithdrawFormProps {
submitWithdraw: (withdrawal: WithdrawalArgs) => void;
}
const WithdrawDelayNotification = ({
threshold,
delay,
symbol,
decimals,
}: {
threshold: BigNumber;
delay: number | undefined;
symbol: string;
decimals: number;
}) => {
const replacements = [
symbol,
delay ? formatDistanceToNow(Date.now() + delay * 1000) : ' ',
];
return (
<Notification
intent={Intent.Warning}
testId={
threshold.isFinite()
? 'amount-withdrawal-delay-notification'
: 'withdrawals-delay-notification'
}
message={[
!threshold.isFinite()
? t('All %s withdrawals are subject to a %s delay.', replacements)
: t('Withdrawals of %s %s or more will be delayed for %s.', [
formatNumber(threshold, decimals),
...replacements,
]),
<ExternalLink
className="ml-1"
href="https://docs.vega.xyz/testnet/concepts/deposits-withdrawals#withdrawal-limits"
>
{t('Read more')}
</ExternalLink>,
]}
/>
);
};
export const WithdrawForm = ({
assets,
balance,
@ -113,6 +159,12 @@ export const WithdrawForm = ({
);
};
const showWithdrawDelayNotification =
delay &&
selectedAsset &&
(!threshold.isFinite() ||
new BigNumber(amount).isGreaterThanOrEqualTo(threshold));
return (
<>
<div className="mb-4 text-sm">
@ -206,6 +258,16 @@ export const WithdrawForm = ({
{t('Use maximum')}
</UseButton>
)}
{showWithdrawDelayNotification && (
<div className="mt-2">
<WithdrawDelayNotification
threshold={threshold}
symbol={selectedAsset.symbol}
decimals={selectedAsset.decimals}
delay={delay}
/>
</div>
)}
</FormGroup>
<Button
data-testid="submit-withdrawal"

View File

@ -1,7 +1,12 @@
import type { Asset } from '@vegaprotocol/assets';
import { t } from '@vegaprotocol/i18n';
import { CompactNumber } from '@vegaprotocol/react-helpers';
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
import { WITHDRAW_THRESHOLD_TOOLTIP_TEXT } from '@vegaprotocol/assets';
import {
KeyValueTable,
KeyValueTableRow,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import BigNumber from 'bignumber.js';
import { formatDistanceToNow } from 'date-fns';
@ -25,7 +30,13 @@ export const WithdrawLimits = ({
? formatDistanceToNow(Date.now() + delay * 1000)
: t('None');
const limits = [
const limits: {
key: string;
label: string;
value: string | JSX.Element;
rawValue?: BigNumber;
tooltip?: string;
}[] = [
{
key: 'BALANCE_AVAILABLE',
label: t('Balance available'),
@ -36,24 +47,35 @@ export const WithdrawLimits = ({
'-'
),
},
{
];
if (threshold.isFinite()) {
limits.push({
key: 'WITHDRAWAL_THRESHOLD',
label: t('Delayed withdrawal threshold'),
tooltip: WITHDRAW_THRESHOLD_TOOLTIP_TEXT,
rawValue: threshold,
value: <CompactNumber number={threshold} decimals={asset.decimals} />,
},
{
key: 'DELAY_TIME',
label: t('Delay time'),
value: delayTime,
},
];
});
}
limits.push({
key: 'DELAY_TIME',
label: t('Delay time'),
value: delayTime,
});
return (
<KeyValueTable>
{limits.map(({ key, label, rawValue, value }) => (
{limits.map(({ key, label, rawValue, value, tooltip }) => (
<KeyValueTableRow key={key}>
<div data-testid={`${key}_label`}>{label}</div>
<div data-testid={`${key}_label`}>
{tooltip ? (
<Tooltip description={tooltip}>
<span>{label}</span>
</Tooltip>
) : (
label
)}
</div>
<div
data-testid={`${key}_value`}
className="truncate"

View File

@ -12,15 +12,17 @@ jest.mock('@web3-react/core', () => ({
useWeb3React: () => ({ account: ethereumAddress }),
}));
const withdrawAsset = {
asset,
balance: new BigNumber(1),
min: new BigNumber(0.0000001),
threshold: new BigNumber(1000),
delay: 10,
handleSelectAsset: jest.fn(),
};
jest.mock('./use-withdraw-asset', () => ({
useWithdrawAsset: () => ({
asset,
balance: new BigNumber(1),
min: new BigNumber(0.0000001),
threshold: new BigNumber(1000),
delay: 10,
handleSelectAsset: jest.fn(),
}),
useWithdrawAsset: () => withdrawAsset,
}));
jest.mock('@vegaprotocol/web3', () => ({
@ -109,4 +111,25 @@ describe('WithdrawManager', () => {
});
fireEvent.submit(screen.getByTestId('withdraw-form'));
};
it('shows withdraw delay notification if amount greater than threshold', async () => {
render(generateJsx(props));
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '1000' },
});
expect(
await screen.findByTestId('amount-withdrawal-delay-notification')
).toBeInTheDocument();
});
it('shows withdraw delay notification if threshold is 0', async () => {
withdrawAsset.threshold = new BigNumber(Infinity);
render(generateJsx(props));
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '0.01' },
});
expect(
await screen.findByTestId('withdrawals-delay-notification')
).toBeInTheDocument();
});
});

View File

@ -70,7 +70,7 @@
"react-hook-form": "^7.27.0",
"react-i18next": "^11.11.4",
"react-intersection-observer": "^9.2.2",
"react-markdown": "^8.0.5",
"react-markdown": "^8.0.6",
"react-router-dom": "^6.9.0",
"react-syntax-highlighter": "^15.4.5",
"react-use-websocket": "^3.0.0",

View File

@ -19541,10 +19541,10 @@ react-lifecycles-compat@^3.0.4:
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-markdown@^8.0.5:
version "8.0.5"
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.5.tgz#c9a70a33ca9aeeafb769c6582e7e38843b9d70ad"
integrity sha512-jGJolWWmOWAvzf+xMdB9zwStViODyyFQhNB/bwCerbBKmrTmgmA599CGiOlP58OId1IMoIRsA8UdI1Lod4zb5A==
react-markdown@^8.0.6:
version "8.0.6"
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.6.tgz#3e939018f8bfce800ffdf22cf50aba3cdded7ad1"
integrity sha512-KgPWsYgHuftdx510wwIzpwf+5js/iHqBR+fzxefv8Khk3mFbnioF1bmL2idHN3ler0LMQmICKeDrWnZrX9mtbQ==
dependencies:
"@types/hast" "^2.0.0"
"@types/prop-types" "^15.0.0"