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:
parent
45f2e926c3
commit
bbfda65bcc
@ -11,6 +11,7 @@ import {
|
|||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import type { Asset } from './asset-data-provider';
|
import type { Asset } from './asset-data-provider';
|
||||||
|
import { WITHDRAW_THRESHOLD_TOOLTIP_TEXT } from './constants';
|
||||||
|
|
||||||
type Rows = {
|
type Rows = {
|
||||||
key: AssetDetail;
|
key: AssetDetail;
|
||||||
@ -121,9 +122,7 @@ export const rows: Rows = [
|
|||||||
{
|
{
|
||||||
key: AssetDetail.WITHDRAWAL_THRESHOLD,
|
key: AssetDetail.WITHDRAWAL_THRESHOLD,
|
||||||
label: t('Withdrawal threshold'),
|
label: t('Withdrawal threshold'),
|
||||||
tooltip: t(
|
tooltip: WITHDRAW_THRESHOLD_TOOLTIP_TEXT,
|
||||||
'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'
|
|
||||||
),
|
|
||||||
value: (asset) =>
|
value: (asset) =>
|
||||||
num(asset, (asset.source as Schema.ERC20).withdrawThreshold),
|
num(asset, (asset.source as Schema.ERC20).withdrawThreshold),
|
||||||
},
|
},
|
||||||
|
4
libs/assets/src/lib/constants.ts
Normal file
4
libs/assets/src/lib/constants.ts
Normal 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"
|
||||||
|
);
|
@ -5,3 +5,4 @@ export * from './assets-data-provider';
|
|||||||
export * from './asset-details-dialog';
|
export * from './asset-details-dialog';
|
||||||
export * from './asset-details-table';
|
export * from './asset-details-table';
|
||||||
export * from './asset-option';
|
export * from './asset-option';
|
||||||
|
export * from './constants';
|
||||||
|
@ -70,6 +70,8 @@ export const Notification = ({
|
|||||||
'text-vega-green dark:text-vega-green': intent === Intent.Success,
|
'text-vega-green dark:text-vega-green': intent === Intent.Success,
|
||||||
'text-yellow-600 dark:text-yellow': intent === Intent.Warning,
|
'text-yellow-600 dark:text-yellow': intent === Intent.Warning,
|
||||||
'text-vega-pink': intent === Intent.Danger,
|
'text-vega-pink': intent === Intent.Danger,
|
||||||
|
'mt-1': !!title,
|
||||||
|
'mt-[0.125rem]': !title,
|
||||||
},
|
},
|
||||||
'flex items-start mt-1'
|
'flex items-start mt-1'
|
||||||
)}
|
)}
|
||||||
@ -78,11 +80,16 @@ export const Notification = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col flex-grow items-start gap-1.5">
|
<div className="flex flex-col flex-grow items-start gap-1.5">
|
||||||
{title && (
|
{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}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="text-sm [word-break:break-word]">{message}</div>
|
<div key="message" className="text-sm [word-break:break-word]">
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
{buttonProps && (
|
{buttonProps && (
|
||||||
<Button
|
<Button
|
||||||
size={buttonProps.size || 'sm'}
|
size={buttonProps.size || 'sm'}
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
removeDecimal,
|
removeDecimal,
|
||||||
required,
|
required,
|
||||||
isAssetTypeERC20,
|
isAssetTypeERC20,
|
||||||
|
formatNumber,
|
||||||
} from '@vegaprotocol/utils';
|
} from '@vegaprotocol/utils';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { useLocalStorage } from '@vegaprotocol/react-helpers';
|
import { useLocalStorage } from '@vegaprotocol/react-helpers';
|
||||||
@ -14,12 +15,16 @@ import {
|
|||||||
FormGroup,
|
FormGroup,
|
||||||
Input,
|
Input,
|
||||||
InputError,
|
InputError,
|
||||||
|
Notification,
|
||||||
RichSelect,
|
RichSelect,
|
||||||
|
ExternalLink,
|
||||||
|
Intent,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { useWeb3React } from '@web3-react/core';
|
import { useWeb3React } from '@web3-react/core';
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
import type { ButtonHTMLAttributes } from 'react';
|
import type { ButtonHTMLAttributes } from 'react';
|
||||||
import type { ControllerRenderProps } from 'react-hook-form';
|
import type { ControllerRenderProps } from 'react-hook-form';
|
||||||
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import { useForm, Controller, useWatch } from 'react-hook-form';
|
import { useForm, Controller, useWatch } from 'react-hook-form';
|
||||||
import type { WithdrawalArgs } from './use-create-withdraw';
|
import type { WithdrawalArgs } from './use-create-withdraw';
|
||||||
import { WithdrawLimits } from './withdraw-limits';
|
import { WithdrawLimits } from './withdraw-limits';
|
||||||
@ -45,6 +50,47 @@ export interface WithdrawFormProps {
|
|||||||
submitWithdraw: (withdrawal: WithdrawalArgs) => void;
|
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 = ({
|
export const WithdrawForm = ({
|
||||||
assets,
|
assets,
|
||||||
balance,
|
balance,
|
||||||
@ -113,6 +159,12 @@ export const WithdrawForm = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showWithdrawDelayNotification =
|
||||||
|
delay &&
|
||||||
|
selectedAsset &&
|
||||||
|
(!threshold.isFinite() ||
|
||||||
|
new BigNumber(amount).isGreaterThanOrEqualTo(threshold));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-4 text-sm">
|
<div className="mb-4 text-sm">
|
||||||
@ -206,6 +258,16 @@ export const WithdrawForm = ({
|
|||||||
{t('Use maximum')}
|
{t('Use maximum')}
|
||||||
</UseButton>
|
</UseButton>
|
||||||
)}
|
)}
|
||||||
|
{showWithdrawDelayNotification && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<WithdrawDelayNotification
|
||||||
|
threshold={threshold}
|
||||||
|
symbol={selectedAsset.symbol}
|
||||||
|
decimals={selectedAsset.decimals}
|
||||||
|
delay={delay}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<Button
|
<Button
|
||||||
data-testid="submit-withdrawal"
|
data-testid="submit-withdrawal"
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
import type { Asset } from '@vegaprotocol/assets';
|
import type { Asset } from '@vegaprotocol/assets';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { CompactNumber } from '@vegaprotocol/react-helpers';
|
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 BigNumber from 'bignumber.js';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
|
|
||||||
@ -25,7 +30,13 @@ export const WithdrawLimits = ({
|
|||||||
? formatDistanceToNow(Date.now() + delay * 1000)
|
? formatDistanceToNow(Date.now() + delay * 1000)
|
||||||
: t('None');
|
: t('None');
|
||||||
|
|
||||||
const limits = [
|
const limits: {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
value: string | JSX.Element;
|
||||||
|
rawValue?: BigNumber;
|
||||||
|
tooltip?: string;
|
||||||
|
}[] = [
|
||||||
{
|
{
|
||||||
key: 'BALANCE_AVAILABLE',
|
key: 'BALANCE_AVAILABLE',
|
||||||
label: t('Balance available'),
|
label: t('Balance available'),
|
||||||
@ -36,24 +47,35 @@ export const WithdrawLimits = ({
|
|||||||
'-'
|
'-'
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
if (threshold.isFinite()) {
|
||||||
|
limits.push({
|
||||||
key: 'WITHDRAWAL_THRESHOLD',
|
key: 'WITHDRAWAL_THRESHOLD',
|
||||||
label: t('Delayed withdrawal threshold'),
|
label: t('Delayed withdrawal threshold'),
|
||||||
|
tooltip: WITHDRAW_THRESHOLD_TOOLTIP_TEXT,
|
||||||
rawValue: threshold,
|
rawValue: threshold,
|
||||||
value: <CompactNumber number={threshold} decimals={asset.decimals} />,
|
value: <CompactNumber number={threshold} decimals={asset.decimals} />,
|
||||||
},
|
});
|
||||||
{
|
}
|
||||||
|
limits.push({
|
||||||
key: 'DELAY_TIME',
|
key: 'DELAY_TIME',
|
||||||
label: t('Delay time'),
|
label: t('Delay time'),
|
||||||
value: delayTime,
|
value: delayTime,
|
||||||
},
|
});
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyValueTable>
|
<KeyValueTable>
|
||||||
{limits.map(({ key, label, rawValue, value }) => (
|
{limits.map(({ key, label, rawValue, value, tooltip }) => (
|
||||||
<KeyValueTableRow key={key}>
|
<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
|
<div
|
||||||
data-testid={`${key}_value`}
|
data-testid={`${key}_value`}
|
||||||
className="truncate"
|
className="truncate"
|
||||||
|
@ -12,15 +12,17 @@ jest.mock('@web3-react/core', () => ({
|
|||||||
useWeb3React: () => ({ account: ethereumAddress }),
|
useWeb3React: () => ({ account: ethereumAddress }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('./use-withdraw-asset', () => ({
|
const withdrawAsset = {
|
||||||
useWithdrawAsset: () => ({
|
|
||||||
asset,
|
asset,
|
||||||
balance: new BigNumber(1),
|
balance: new BigNumber(1),
|
||||||
min: new BigNumber(0.0000001),
|
min: new BigNumber(0.0000001),
|
||||||
threshold: new BigNumber(1000),
|
threshold: new BigNumber(1000),
|
||||||
delay: 10,
|
delay: 10,
|
||||||
handleSelectAsset: jest.fn(),
|
handleSelectAsset: jest.fn(),
|
||||||
}),
|
};
|
||||||
|
|
||||||
|
jest.mock('./use-withdraw-asset', () => ({
|
||||||
|
useWithdrawAsset: () => withdrawAsset,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('@vegaprotocol/web3', () => ({
|
jest.mock('@vegaprotocol/web3', () => ({
|
||||||
@ -109,4 +111,25 @@ describe('WithdrawManager', () => {
|
|||||||
});
|
});
|
||||||
fireEvent.submit(screen.getByTestId('withdraw-form'));
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -70,7 +70,7 @@
|
|||||||
"react-hook-form": "^7.27.0",
|
"react-hook-form": "^7.27.0",
|
||||||
"react-i18next": "^11.11.4",
|
"react-i18next": "^11.11.4",
|
||||||
"react-intersection-observer": "^9.2.2",
|
"react-intersection-observer": "^9.2.2",
|
||||||
"react-markdown": "^8.0.5",
|
"react-markdown": "^8.0.6",
|
||||||
"react-router-dom": "^6.9.0",
|
"react-router-dom": "^6.9.0",
|
||||||
"react-syntax-highlighter": "^15.4.5",
|
"react-syntax-highlighter": "^15.4.5",
|
||||||
"react-use-websocket": "^3.0.0",
|
"react-use-websocket": "^3.0.0",
|
||||||
|
@ -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"
|
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||||
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
||||||
|
|
||||||
react-markdown@^8.0.5:
|
react-markdown@^8.0.6:
|
||||||
version "8.0.5"
|
version "8.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.5.tgz#c9a70a33ca9aeeafb769c6582e7e38843b9d70ad"
|
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.6.tgz#3e939018f8bfce800ffdf22cf50aba3cdded7ad1"
|
||||||
integrity sha512-jGJolWWmOWAvzf+xMdB9zwStViODyyFQhNB/bwCerbBKmrTmgmA599CGiOlP58OId1IMoIRsA8UdI1Lod4zb5A==
|
integrity sha512-KgPWsYgHuftdx510wwIzpwf+5js/iHqBR+fzxefv8Khk3mFbnioF1bmL2idHN3ler0LMQmICKeDrWnZrX9mtbQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/hast" "^2.0.0"
|
"@types/hast" "^2.0.0"
|
||||||
"@types/prop-types" "^15.0.0"
|
"@types/prop-types" "^15.0.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user