feat(accounts): transfer form improvements to handle transfers from vesting accounts (#5212)
Co-authored-by: Madalina Raicu <madalina@raygroup.uk>
This commit is contained in:
parent
73f37c2477
commit
f780013846
@ -1,9 +1,10 @@
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { truncateByChars } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParam,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import type { Transfer } from '@vegaprotocol/wallet';
|
||||
@ -21,7 +22,11 @@ export const ALLOWED_ACCOUNTS = [
|
||||
|
||||
export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
||||
const { pubKey, pubKeys } = useVegaWallet();
|
||||
const { param } = useNetworkParam(NetworkParams.transfer_fee_factor);
|
||||
const { params } = useNetworkParams([
|
||||
NetworkParams.transfer_fee_factor,
|
||||
NetworkParams.transfer_minTransferQuantumMultiple,
|
||||
]);
|
||||
|
||||
const { data } = useDataProvider({
|
||||
dataProvider: accountsDataProvider,
|
||||
variables: { partyId: pubKey || '' },
|
||||
@ -40,6 +45,7 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
||||
const accounts = data
|
||||
? data.filter((account) => ALLOWED_ACCOUNTS.includes(account.type))
|
||||
: [];
|
||||
const sortedAccounts = sortBy(accounts, (a) => a.asset.symbol.toLowerCase());
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -59,9 +65,10 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
||||
pubKey={pubKey}
|
||||
pubKeys={pubKeys ? pubKeys?.map((pk) => pk.publicKey) : null}
|
||||
assetId={assetId}
|
||||
feeFactor={param}
|
||||
feeFactor={params.transfer_fee_factor}
|
||||
minQuantumMultiple={params.transfer_minTransferQuantumMultiple}
|
||||
submitTransfer={transfer}
|
||||
accounts={accounts}
|
||||
accounts={sortedAccounts}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -9,18 +9,10 @@ import {
|
||||
} from './transfer-form';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import { removeDecimal } from '@vegaprotocol/utils';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
|
||||
describe('TransferForm', () => {
|
||||
const renderComponent = (props: TransferFormProps) => {
|
||||
return render(
|
||||
// Wrap with mock provider as the form will make queries to fetch the selected
|
||||
// toVegaKey accounts. We don't test this for now but we need to wrap so that
|
||||
// the component has access to the client
|
||||
<MockedProvider>
|
||||
<TransferForm {...props} />
|
||||
</MockedProvider>
|
||||
);
|
||||
return render(<TransferForm {...props} />);
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
@ -54,6 +46,7 @@ describe('TransferForm', () => {
|
||||
symbol: '€',
|
||||
name: 'EUR',
|
||||
decimals: 2,
|
||||
quantum: '1',
|
||||
};
|
||||
const props = {
|
||||
pubKey,
|
||||
@ -72,9 +65,10 @@ describe('TransferForm', () => {
|
||||
{
|
||||
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
asset,
|
||||
balance: '100000',
|
||||
balance: '10000',
|
||||
},
|
||||
],
|
||||
minQuantumMultiple: '1',
|
||||
};
|
||||
|
||||
it('form tooltips correctly displayed', async () => {
|
||||
@ -132,7 +126,7 @@ describe('TransferForm', () => {
|
||||
// 1003-TRAN-004
|
||||
renderComponent(props);
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3); // pubkey is set as default value
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey is set as default value
|
||||
const toggle = screen.getByText('Enter manually');
|
||||
await userEvent.click(toggle);
|
||||
// has switched to input
|
||||
@ -145,12 +139,12 @@ describe('TransferForm', () => {
|
||||
screen.getByLabelText('To Vega key'),
|
||||
'invalid-address'
|
||||
);
|
||||
expect(screen.getAllByTestId('input-error-text')[0]).toHaveTextContent(
|
||||
expect(screen.getAllByTestId('input-error-text')[1]).toHaveTextContent(
|
||||
'Invalid Vega key'
|
||||
);
|
||||
});
|
||||
|
||||
it('validates fields and submits', async () => {
|
||||
it('sends transfer from general accounts', async () => {
|
||||
// 1003-TRAN-002
|
||||
// 1003-TRAN-003
|
||||
// 1002-WITH-010
|
||||
@ -168,7 +162,7 @@ describe('TransferForm', () => {
|
||||
]);
|
||||
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3); // pubkey is set as default value
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey is set as default value
|
||||
|
||||
// Select a pubkey
|
||||
await userEvent.selectOptions(
|
||||
@ -181,15 +175,20 @@ describe('TransferForm', () => {
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('From account'),
|
||||
AccountType.ACCOUNT_TYPE_VESTED_REWARDS
|
||||
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
|
||||
);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
|
||||
// Test use max button
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
|
||||
expect(amountInput).toHaveValue('1000');
|
||||
|
||||
// Test amount validation
|
||||
await userEvent.type(amountInput, '0.00000001');
|
||||
await userEvent.clear(amountInput);
|
||||
await userEvent.type(amountInput, '0.001'); // Below quantum multiple amount
|
||||
expect(
|
||||
await screen.findByText('Value is below minimum')
|
||||
await screen.findByText(/Amount below minimum requirement/)
|
||||
).toBeInTheDocument();
|
||||
|
||||
await userEvent.clear(amountInput);
|
||||
@ -210,7 +209,7 @@ describe('TransferForm', () => {
|
||||
await waitFor(() => {
|
||||
expect(props.submitTransfer).toHaveBeenCalledTimes(1);
|
||||
expect(props.submitTransfer).toHaveBeenCalledWith({
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
to: props.pubKeys[1],
|
||||
asset: asset.id,
|
||||
@ -220,6 +219,83 @@ describe('TransferForm', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('sends transfer from vested accounts', async () => {
|
||||
const mockSubmit = jest.fn();
|
||||
renderComponent({
|
||||
...props,
|
||||
submitTransfer: mockSubmit,
|
||||
minQuantumMultiple: '100000',
|
||||
});
|
||||
|
||||
// check current pubkey not shown
|
||||
const keySelect: HTMLSelectElement = screen.getByLabelText('To Vega key');
|
||||
const pubKeyOptions = ['', pubKey, props.pubKeys[1]];
|
||||
expect(keySelect.children).toHaveLength(pubKeyOptions.length);
|
||||
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual(
|
||||
pubKeyOptions
|
||||
);
|
||||
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
|
||||
|
||||
// Select a pubkey
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('To Vega key'),
|
||||
props.pubKeys[1] // Use not current pubkey so we can check it switches to current pubkey later
|
||||
);
|
||||
|
||||
// Select asset
|
||||
await selectAsset(asset);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('From account'),
|
||||
`${AccountType.ACCOUNT_TYPE_VESTED_REWARDS}-${asset.id}`
|
||||
);
|
||||
|
||||
// Check switch back to connected key
|
||||
expect(screen.getByLabelText('To Vega key')).toHaveValue(props.pubKey);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
|
||||
const checkbox = screen.getByTestId('include-transfer-fee');
|
||||
expect(checkbox).not.toBeChecked();
|
||||
|
||||
await userEvent.clear(amountInput);
|
||||
await userEvent.type(amountInput, '50');
|
||||
|
||||
expect(await screen.findByText(/Use max to bypass/)).toBeInTheDocument();
|
||||
|
||||
// Test use max button
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
|
||||
expect(amountInput).toHaveValue('100');
|
||||
|
||||
// If transfering from a vested account 'include fees' checkbox should
|
||||
// be disabled and fees should be 0
|
||||
expect(checkbox).not.toBeChecked();
|
||||
expect(checkbox).toBeDisabled();
|
||||
const expectedFee = '0';
|
||||
const total = new BigNumber(amount).plus(expectedFee).toFixed();
|
||||
|
||||
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
|
||||
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(amount);
|
||||
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
|
||||
|
||||
await submit();
|
||||
|
||||
await waitFor(() => {
|
||||
// 1003-TRAN-023
|
||||
expect(mockSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(mockSubmit).toHaveBeenCalledWith({
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
to: props.pubKey,
|
||||
asset: asset.id,
|
||||
amount: removeDecimal(amount, asset.decimals),
|
||||
oneOff: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('IncludeFeesCheckbox', () => {
|
||||
it('validates fields and submits when checkbox is checked', async () => {
|
||||
const mockSubmit = jest.fn();
|
||||
@ -234,7 +310,7 @@ describe('TransferForm', () => {
|
||||
);
|
||||
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3); // pubkey set as default value
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
|
||||
|
||||
// Select a pubkey
|
||||
await userEvent.selectOptions(
|
||||
@ -247,7 +323,7 @@ describe('TransferForm', () => {
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('From account'),
|
||||
AccountType.ACCOUNT_TYPE_VESTED_REWARDS
|
||||
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
|
||||
);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
@ -281,7 +357,7 @@ describe('TransferForm', () => {
|
||||
// 1003-TRAN-023
|
||||
expect(mockSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(mockSubmit).toHaveBeenCalledWith({
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
to: props.pubKeys[1],
|
||||
asset: asset.id,
|
||||
@ -303,7 +379,7 @@ describe('TransferForm', () => {
|
||||
);
|
||||
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3); // pubkey set as default value
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
|
||||
|
||||
// Select a pubkey
|
||||
await userEvent.selectOptions(
|
||||
@ -314,6 +390,11 @@ describe('TransferForm', () => {
|
||||
// Select asset
|
||||
await selectAsset(asset);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('From account'),
|
||||
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
|
||||
);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
const checkbox = screen.getByTestId('include-transfer-fee');
|
||||
expect(checkbox).not.toBeChecked();
|
||||
@ -333,26 +414,28 @@ describe('TransferForm', () => {
|
||||
|
||||
describe('AddressField', () => {
|
||||
const props = {
|
||||
mode: 'select' as const,
|
||||
select: <div>select</div>,
|
||||
input: <div>input</div>,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
|
||||
it('toggles content and calls onChange', async () => {
|
||||
it('renders correct content by mode prop and calls onChange', async () => {
|
||||
const mockOnChange = jest.fn();
|
||||
render(<AddressField {...props} onChange={mockOnChange} />);
|
||||
const { rerender } = render(
|
||||
<AddressField {...props} onChange={mockOnChange} />
|
||||
);
|
||||
|
||||
// select should be shown by default
|
||||
expect(screen.getByText('select')).toBeInTheDocument();
|
||||
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
||||
await userEvent.click(screen.getByText('Enter manually'));
|
||||
expect(mockOnChange).toHaveBeenCalled();
|
||||
|
||||
rerender(<AddressField {...props} mode="input" />);
|
||||
|
||||
expect(screen.queryByText('select')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('input')).toBeInTheDocument();
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(1);
|
||||
await userEvent.click(screen.getByText('Select from wallet'));
|
||||
expect(screen.getByText('select')).toBeInTheDocument();
|
||||
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import {
|
||||
minSafe,
|
||||
maxSafe,
|
||||
required,
|
||||
vegaPublicKey,
|
||||
addDecimal,
|
||||
formatNumber,
|
||||
addDecimalsFormatNumber,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
@ -27,14 +27,20 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { AssetOption, Balance } from '@vegaprotocol/assets';
|
||||
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { accountsDataProvider } from './accounts-data-provider';
|
||||
|
||||
interface FormFields {
|
||||
toVegaKey: string;
|
||||
asset: string;
|
||||
asset: string; // This is used to simply filter the from account list, the fromAccount type should be used in the tx
|
||||
amount: string;
|
||||
fromAccount: AccountType;
|
||||
fromAccount: string; // AccountType-AssetId
|
||||
}
|
||||
|
||||
interface Asset {
|
||||
id: string;
|
||||
symbol: string;
|
||||
name: string;
|
||||
decimals: number;
|
||||
quantum: string;
|
||||
}
|
||||
|
||||
export interface TransferFormProps {
|
||||
@ -43,10 +49,11 @@ export interface TransferFormProps {
|
||||
accounts: Array<{
|
||||
type: AccountType;
|
||||
balance: string;
|
||||
asset: { id: string; symbol: string; name: string; decimals: number };
|
||||
asset: Asset;
|
||||
}>;
|
||||
assetId?: string;
|
||||
feeFactor: string | null;
|
||||
minQuantumMultiple: string | null;
|
||||
submitTransfer: (transfer: Transfer) => void;
|
||||
}
|
||||
|
||||
@ -57,6 +64,7 @@ export const TransferForm = ({
|
||||
feeFactor,
|
||||
submitTransfer,
|
||||
accounts,
|
||||
minQuantumMultiple,
|
||||
}: TransferFormProps) => {
|
||||
const {
|
||||
control,
|
||||
@ -72,6 +80,8 @@ export const TransferForm = ({
|
||||
},
|
||||
});
|
||||
|
||||
const [toVegaKeyMode, setToVegaKeyMode] = useState<ToVegaKeyMode>('select');
|
||||
|
||||
const assets = sortBy(
|
||||
accounts
|
||||
.filter(
|
||||
@ -99,48 +109,29 @@ export const TransferForm = ({
|
||||
...account.asset,
|
||||
balance: addDecimal(account.balance, account.asset.decimals),
|
||||
})),
|
||||
'name'
|
||||
(a) => a.symbol.toLowerCase()
|
||||
);
|
||||
|
||||
const selectedPubKey = watch('toVegaKey');
|
||||
const amount = watch('amount');
|
||||
const fromAccount = watch('fromAccount');
|
||||
const assetId = watch('asset');
|
||||
const selectedAssetId = watch('asset');
|
||||
|
||||
const asset = assets.find((a) => a.id === assetId);
|
||||
|
||||
const { data: toAccounts } = useDataProvider({
|
||||
dataProvider: accountsDataProvider,
|
||||
variables: {
|
||||
partyId: selectedPubKey,
|
||||
},
|
||||
skip: !selectedPubKey,
|
||||
});
|
||||
// Convert the account type (Type-AssetId) into separate values
|
||||
const [accountType, accountAssetId] = fromAccount
|
||||
? parseFromAccount(fromAccount)
|
||||
: [undefined, undefined];
|
||||
const fromVested = accountType === AccountType.ACCOUNT_TYPE_VESTED_REWARDS;
|
||||
const asset = assets.find((a) => a.id === accountAssetId);
|
||||
|
||||
const account = accounts.find(
|
||||
(a) => a.asset.id === assetId && a.type === fromAccount
|
||||
(a) => a.asset.id === accountAssetId && a.type === accountType
|
||||
);
|
||||
const accountBalance =
|
||||
account && addDecimal(account.balance, account.asset.decimals);
|
||||
|
||||
// The general account of the selected pubkey. You can only transfer
|
||||
// to general accounts, either when redeeming vested rewards or just
|
||||
// during normal general -> general transfers
|
||||
const toGeneralAccount =
|
||||
toAccounts &&
|
||||
toAccounts.find((a) => {
|
||||
return (
|
||||
a.asset.id === assetId && a.type === AccountType.ACCOUNT_TYPE_GENERAL
|
||||
);
|
||||
});
|
||||
|
||||
const [includeFee, setIncludeFee] = useState(false);
|
||||
|
||||
// Min viable amount given asset decimals EG for WEI 0.000000000000000001
|
||||
const min = asset
|
||||
? new BigNumber(addDecimal('1', asset.decimals))
|
||||
: new BigNumber(0);
|
||||
|
||||
// Max amount given selected asset and from account
|
||||
const max = accountBalance ? new BigNumber(accountBalance) : new BigNumber(0);
|
||||
|
||||
@ -164,16 +155,21 @@ export const TransferForm = ({
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(fields: FormFields) => {
|
||||
if (!asset) {
|
||||
throw new Error('Submitted transfer with no asset selected');
|
||||
}
|
||||
if (!transferAmount) {
|
||||
throw new Error('Submitted transfer with no amount selected');
|
||||
}
|
||||
|
||||
const [type, assetId] = parseFromAccount(fields.fromAccount);
|
||||
const asset = assets.find((a) => a.id === assetId);
|
||||
|
||||
if (!asset) {
|
||||
throw new Error('Submitted transfer with no asset selected');
|
||||
}
|
||||
|
||||
const transfer = normalizeTransfer(
|
||||
fields.toVegaKey,
|
||||
transferAmount,
|
||||
fields.fromAccount,
|
||||
type,
|
||||
AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form
|
||||
{
|
||||
id: asset.id,
|
||||
@ -182,7 +178,7 @@ export const TransferForm = ({
|
||||
);
|
||||
submitTransfer(transfer);
|
||||
},
|
||||
[asset, submitTransfer, transferAmount]
|
||||
[submitTransfer, transferAmount, assets]
|
||||
);
|
||||
|
||||
// reset for placeholder workaround https://github.com/radix-ui/primitives/issues/1569
|
||||
@ -198,55 +194,10 @@ export const TransferForm = ({
|
||||
className="text-sm"
|
||||
data-testid="transfer-form"
|
||||
>
|
||||
<TradingFormGroup label="To Vega key" labelFor="toVegaKey">
|
||||
<AddressField
|
||||
onChange={() => setValue('toVegaKey', '')}
|
||||
select={
|
||||
<TradingSelect {...register('toVegaKey')} id="toVegaKey">
|
||||
<option value="" disabled={true}>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{pubKeys?.map((pk) => {
|
||||
const text = pk === pubKey ? t('Current key: ') + pk : pk;
|
||||
|
||||
return (
|
||||
<option key={pk} value={pk}>
|
||||
{text}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</TradingSelect>
|
||||
}
|
||||
input={
|
||||
<TradingInput
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={true} // focus input immediately after is shown
|
||||
id="toVegaKey"
|
||||
type="text"
|
||||
{...register('toVegaKey', {
|
||||
validate: {
|
||||
required,
|
||||
vegaPublicKey,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{errors.toVegaKey?.message && (
|
||||
<TradingInputError forInput="toVegaKey">
|
||||
{errors.toVegaKey.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label={t('Asset')} labelFor="asset">
|
||||
<Controller
|
||||
control={control}
|
||||
name="asset"
|
||||
rules={{
|
||||
validate: {
|
||||
required,
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TradingRichSelect
|
||||
data-testid="select-asset"
|
||||
@ -254,6 +205,7 @@ export const TransferForm = ({
|
||||
name={field.name}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
setValue('fromAccount', '');
|
||||
}}
|
||||
placeholder={t('Please select an asset')}
|
||||
value={field.value}
|
||||
@ -280,10 +232,10 @@ export const TransferForm = ({
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label={t('From account')} labelFor="fromAccount">
|
||||
<TradingSelect
|
||||
id="fromAccount"
|
||||
defaultValue=""
|
||||
{...register('fromAccount', {
|
||||
<Controller
|
||||
control={control}
|
||||
name="fromAccount"
|
||||
rules={{
|
||||
validate: {
|
||||
required,
|
||||
sameAccount: (value) => {
|
||||
@ -298,50 +250,106 @@ export const TransferForm = ({
|
||||
return true;
|
||||
},
|
||||
},
|
||||
})}
|
||||
>
|
||||
<option value="" disabled={true}>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{accounts
|
||||
.filter((a) => {
|
||||
if (!assetId) return true;
|
||||
return assetId === a.asset.id;
|
||||
})
|
||||
.map((a) => {
|
||||
return (
|
||||
<option value={a.type} key={`${a.type}-${a.asset.id}`}>
|
||||
{AccountTypeMapping[a.type]} (
|
||||
{addDecimalsFormatNumber(a.balance, a.asset.decimals)}{' '}
|
||||
{a.asset.symbol})
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</TradingSelect>
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TradingSelect
|
||||
id="fromAccount"
|
||||
defaultValue=""
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(e);
|
||||
|
||||
const [type] = parseFromAccount(e.target.value);
|
||||
|
||||
// Enforce that if transferring from a vested rewards account it must go to
|
||||
// the current connected general account
|
||||
if (
|
||||
type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS &&
|
||||
pubKey
|
||||
) {
|
||||
setValue('toVegaKey', pubKey);
|
||||
setToVegaKeyMode('select');
|
||||
setIncludeFee(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="" disabled={true}>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{accounts
|
||||
.filter((a) => {
|
||||
if (!selectedAssetId) return true;
|
||||
return selectedAssetId === a.asset.id;
|
||||
})
|
||||
.map((a) => {
|
||||
const id = `${a.type}-${a.asset.id}`;
|
||||
return (
|
||||
<option value={id} key={id}>
|
||||
{AccountTypeMapping[a.type]} (
|
||||
{addDecimalsFormatNumber(a.balance, a.asset.decimals)}{' '}
|
||||
{a.asset.symbol})
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</TradingSelect>
|
||||
)}
|
||||
/>
|
||||
{errors.fromAccount?.message && (
|
||||
<TradingInputError forInput="fromAccount">
|
||||
{errors.fromAccount.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label={t('To account')} labelFor="toAccount">
|
||||
<TradingSelect
|
||||
id="toAccount"
|
||||
defaultValue={AccountType.ACCOUNT_TYPE_GENERAL}
|
||||
>
|
||||
<option value={AccountType.ACCOUNT_TYPE_GENERAL}>
|
||||
{toGeneralAccount
|
||||
? `${
|
||||
AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]
|
||||
} (${addDecimalsFormatNumber(
|
||||
toGeneralAccount.balance,
|
||||
toGeneralAccount.asset.decimals
|
||||
)} ${toGeneralAccount.asset.symbol})`
|
||||
: `${AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]} ${
|
||||
asset ? `(0 ${asset.symbol})` : ''
|
||||
}`}
|
||||
</option>
|
||||
</TradingSelect>
|
||||
<TradingFormGroup label="To Vega key" labelFor="toVegaKey">
|
||||
<AddressField
|
||||
onChange={() => {
|
||||
setValue('toVegaKey', '');
|
||||
setToVegaKeyMode((curr) => (curr === 'input' ? 'select' : 'input'));
|
||||
}}
|
||||
mode={toVegaKeyMode}
|
||||
select={
|
||||
<TradingSelect
|
||||
{...register('toVegaKey')}
|
||||
disabled={fromVested}
|
||||
id="toVegaKey"
|
||||
>
|
||||
<option value="" disabled={true}>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{pubKeys?.map((pk) => {
|
||||
const text = pk === pubKey ? t('Current key: ') + pk : pk;
|
||||
|
||||
return (
|
||||
<option key={pk} value={pk}>
|
||||
{text}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</TradingSelect>
|
||||
}
|
||||
input={
|
||||
fromVested ? null : (
|
||||
<TradingInput
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={true} // focus input immediately after is shown
|
||||
id="toVegaKey"
|
||||
type="text"
|
||||
disabled={fromVested}
|
||||
{...register('toVegaKey', {
|
||||
validate: {
|
||||
required,
|
||||
vegaPublicKey,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{errors.toVegaKey?.message && (
|
||||
<TradingInputError forInput="toVegaKey">
|
||||
{errors.toVegaKey.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label="Amount" labelFor="amount">
|
||||
<TradingInput
|
||||
@ -353,7 +361,43 @@ export const TransferForm = ({
|
||||
{...register('amount', {
|
||||
validate: {
|
||||
required,
|
||||
minSafe: (value) => minSafe(new BigNumber(min))(value),
|
||||
minSafe: (v) => {
|
||||
if (!asset || !minQuantumMultiple) return true;
|
||||
|
||||
const value = new BigNumber(v);
|
||||
|
||||
if (value.isZero()) {
|
||||
return t('Amount cannot be 0');
|
||||
}
|
||||
|
||||
const minByQuantumMultiple = toBigNum(
|
||||
minQuantumMultiple,
|
||||
asset.decimals
|
||||
);
|
||||
|
||||
if (fromVested) {
|
||||
// special conditions which let you bypass min transfer rules set by quantum multiple
|
||||
if (value.isGreaterThanOrEqualTo(max)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value.isLessThan(minByQuantumMultiple)) {
|
||||
return t(
|
||||
'Amount below minimum requirements for partial transfer. Use max to bypass'
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
if (value.isLessThan(minByQuantumMultiple)) {
|
||||
return t(
|
||||
'Amount below minimum requirement set by transfer.minTransferQuantumMultiple'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
maxSafe: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(max)) {
|
||||
@ -369,7 +413,9 @@ export const TransferForm = ({
|
||||
type="button"
|
||||
className="absolute top-0 right-0 ml-auto text-xs underline"
|
||||
onClick={() =>
|
||||
setValue('amount', parseFloat(accountBalance).toString())
|
||||
setValue('amount', parseFloat(accountBalance).toString(), {
|
||||
shouldValidate: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('Use max')}
|
||||
@ -390,10 +436,10 @@ export const TransferForm = ({
|
||||
<div>
|
||||
<TradingCheckbox
|
||||
name="include-transfer-fee"
|
||||
disabled={!transferAmount}
|
||||
disabled={!transferAmount || fromVested}
|
||||
label={t('Include transfer fee')}
|
||||
checked={includeFee}
|
||||
onCheckedChange={() => setIncludeFee(!includeFee)}
|
||||
onCheckedChange={() => setIncludeFee((x) => !x)}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
@ -403,7 +449,7 @@ export const TransferForm = ({
|
||||
amount={transferAmount}
|
||||
transferAmount={transferAmount}
|
||||
feeFactor={feeFactor}
|
||||
fee={fee}
|
||||
fee={fromVested ? '0' : fee}
|
||||
decimals={asset?.decimals}
|
||||
/>
|
||||
)}
|
||||
@ -485,32 +531,38 @@ export const TransferFee = ({
|
||||
);
|
||||
};
|
||||
|
||||
type ToVegaKeyMode = 'input' | 'select';
|
||||
|
||||
interface AddressInputProps {
|
||||
select: ReactNode;
|
||||
input: ReactNode;
|
||||
mode: ToVegaKeyMode;
|
||||
onChange: () => void;
|
||||
}
|
||||
|
||||
export const AddressField = ({
|
||||
select,
|
||||
input,
|
||||
mode,
|
||||
onChange,
|
||||
}: AddressInputProps) => {
|
||||
const [isInput, setIsInput] = useState(false);
|
||||
|
||||
const isInput = mode === 'input';
|
||||
return (
|
||||
<>
|
||||
{isInput ? input : select}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsInput((curr) => !curr);
|
||||
onChange();
|
||||
}}
|
||||
className="absolute top-0 right-0 ml-auto text-xs underline"
|
||||
>
|
||||
{isInput ? t('Select from wallet') : t('Enter manually')}
|
||||
</button>
|
||||
{select && input && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onChange}
|
||||
className="absolute top-0 right-0 ml-auto text-xs underline"
|
||||
>
|
||||
{isInput ? t('Select from wallet') : t('Enter manually')}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const parseFromAccount = (fromAccountStr: string) => {
|
||||
return fromAccountStr.split('-') as [AccountType, string];
|
||||
};
|
||||
|
@ -176,6 +176,7 @@ export const NetworkParams = {
|
||||
market_liquidity_feeCalculationTimeStep:
|
||||
'market_liquidity_feeCalculationTimeStep',
|
||||
transfer_fee_factor: 'transfer_fee_factor',
|
||||
transfer_minTransferQuantumMultiple: 'transfer_minTransferQuantumMultiple',
|
||||
network_validators_incumbentBonus: 'network_validators_incumbentBonus',
|
||||
} as const;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user