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 * as Schema from '@vegaprotocol/types';
|
||||||
import { truncateByChars } from '@vegaprotocol/utils';
|
import { truncateByChars } from '@vegaprotocol/utils';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import {
|
import {
|
||||||
NetworkParams,
|
NetworkParams,
|
||||||
useNetworkParam,
|
useNetworkParams,
|
||||||
} from '@vegaprotocol/network-parameters';
|
} from '@vegaprotocol/network-parameters';
|
||||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||||
import type { Transfer } from '@vegaprotocol/wallet';
|
import type { Transfer } from '@vegaprotocol/wallet';
|
||||||
@ -21,7 +22,11 @@ export const ALLOWED_ACCOUNTS = [
|
|||||||
|
|
||||||
export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
||||||
const { pubKey, pubKeys } = useVegaWallet();
|
const { pubKey, pubKeys } = useVegaWallet();
|
||||||
const { param } = useNetworkParam(NetworkParams.transfer_fee_factor);
|
const { params } = useNetworkParams([
|
||||||
|
NetworkParams.transfer_fee_factor,
|
||||||
|
NetworkParams.transfer_minTransferQuantumMultiple,
|
||||||
|
]);
|
||||||
|
|
||||||
const { data } = useDataProvider({
|
const { data } = useDataProvider({
|
||||||
dataProvider: accountsDataProvider,
|
dataProvider: accountsDataProvider,
|
||||||
variables: { partyId: pubKey || '' },
|
variables: { partyId: pubKey || '' },
|
||||||
@ -40,6 +45,7 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
|||||||
const accounts = data
|
const accounts = data
|
||||||
? data.filter((account) => ALLOWED_ACCOUNTS.includes(account.type))
|
? data.filter((account) => ALLOWED_ACCOUNTS.includes(account.type))
|
||||||
: [];
|
: [];
|
||||||
|
const sortedAccounts = sortBy(accounts, (a) => a.asset.symbol.toLowerCase());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -59,9 +65,10 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
|||||||
pubKey={pubKey}
|
pubKey={pubKey}
|
||||||
pubKeys={pubKeys ? pubKeys?.map((pk) => pk.publicKey) : null}
|
pubKeys={pubKeys ? pubKeys?.map((pk) => pk.publicKey) : null}
|
||||||
assetId={assetId}
|
assetId={assetId}
|
||||||
feeFactor={param}
|
feeFactor={params.transfer_fee_factor}
|
||||||
|
minQuantumMultiple={params.transfer_minTransferQuantumMultiple}
|
||||||
submitTransfer={transfer}
|
submitTransfer={transfer}
|
||||||
accounts={accounts}
|
accounts={sortedAccounts}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -9,18 +9,10 @@ import {
|
|||||||
} from './transfer-form';
|
} from './transfer-form';
|
||||||
import { AccountType } from '@vegaprotocol/types';
|
import { AccountType } from '@vegaprotocol/types';
|
||||||
import { removeDecimal } from '@vegaprotocol/utils';
|
import { removeDecimal } from '@vegaprotocol/utils';
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
|
||||||
|
|
||||||
describe('TransferForm', () => {
|
describe('TransferForm', () => {
|
||||||
const renderComponent = (props: TransferFormProps) => {
|
const renderComponent = (props: TransferFormProps) => {
|
||||||
return render(
|
return render(<TransferForm {...props} />);
|
||||||
// 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>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
@ -54,6 +46,7 @@ describe('TransferForm', () => {
|
|||||||
symbol: '€',
|
symbol: '€',
|
||||||
name: 'EUR',
|
name: 'EUR',
|
||||||
decimals: 2,
|
decimals: 2,
|
||||||
|
quantum: '1',
|
||||||
};
|
};
|
||||||
const props = {
|
const props = {
|
||||||
pubKey,
|
pubKey,
|
||||||
@ -72,9 +65,10 @@ describe('TransferForm', () => {
|
|||||||
{
|
{
|
||||||
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||||
asset,
|
asset,
|
||||||
balance: '100000',
|
balance: '10000',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
minQuantumMultiple: '1',
|
||||||
};
|
};
|
||||||
|
|
||||||
it('form tooltips correctly displayed', async () => {
|
it('form tooltips correctly displayed', async () => {
|
||||||
@ -132,7 +126,7 @@ describe('TransferForm', () => {
|
|||||||
// 1003-TRAN-004
|
// 1003-TRAN-004
|
||||||
renderComponent(props);
|
renderComponent(props);
|
||||||
await submit();
|
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');
|
const toggle = screen.getByText('Enter manually');
|
||||||
await userEvent.click(toggle);
|
await userEvent.click(toggle);
|
||||||
// has switched to input
|
// has switched to input
|
||||||
@ -145,12 +139,12 @@ describe('TransferForm', () => {
|
|||||||
screen.getByLabelText('To Vega key'),
|
screen.getByLabelText('To Vega key'),
|
||||||
'invalid-address'
|
'invalid-address'
|
||||||
);
|
);
|
||||||
expect(screen.getAllByTestId('input-error-text')[0]).toHaveTextContent(
|
expect(screen.getAllByTestId('input-error-text')[1]).toHaveTextContent(
|
||||||
'Invalid Vega key'
|
'Invalid Vega key'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('validates fields and submits', async () => {
|
it('sends transfer from general accounts', async () => {
|
||||||
// 1003-TRAN-002
|
// 1003-TRAN-002
|
||||||
// 1003-TRAN-003
|
// 1003-TRAN-003
|
||||||
// 1002-WITH-010
|
// 1002-WITH-010
|
||||||
@ -168,7 +162,7 @@ describe('TransferForm', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
await submit();
|
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
|
// Select a pubkey
|
||||||
await userEvent.selectOptions(
|
await userEvent.selectOptions(
|
||||||
@ -181,15 +175,20 @@ describe('TransferForm', () => {
|
|||||||
|
|
||||||
await userEvent.selectOptions(
|
await userEvent.selectOptions(
|
||||||
screen.getByLabelText('From account'),
|
screen.getByLabelText('From account'),
|
||||||
AccountType.ACCOUNT_TYPE_VESTED_REWARDS
|
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const amountInput = screen.getByLabelText('Amount');
|
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
|
// Test amount validation
|
||||||
await userEvent.type(amountInput, '0.00000001');
|
await userEvent.clear(amountInput);
|
||||||
|
await userEvent.type(amountInput, '0.001'); // Below quantum multiple amount
|
||||||
expect(
|
expect(
|
||||||
await screen.findByText('Value is below minimum')
|
await screen.findByText(/Amount below minimum requirement/)
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
|
||||||
await userEvent.clear(amountInput);
|
await userEvent.clear(amountInput);
|
||||||
@ -210,7 +209,7 @@ describe('TransferForm', () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(props.submitTransfer).toHaveBeenCalledTimes(1);
|
expect(props.submitTransfer).toHaveBeenCalledTimes(1);
|
||||||
expect(props.submitTransfer).toHaveBeenCalledWith({
|
expect(props.submitTransfer).toHaveBeenCalledWith({
|
||||||
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||||
to: props.pubKeys[1],
|
to: props.pubKeys[1],
|
||||||
asset: asset.id,
|
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', () => {
|
describe('IncludeFeesCheckbox', () => {
|
||||||
it('validates fields and submits when checkbox is checked', async () => {
|
it('validates fields and submits when checkbox is checked', async () => {
|
||||||
const mockSubmit = jest.fn();
|
const mockSubmit = jest.fn();
|
||||||
@ -234,7 +310,7 @@ describe('TransferForm', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await submit();
|
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
|
// Select a pubkey
|
||||||
await userEvent.selectOptions(
|
await userEvent.selectOptions(
|
||||||
@ -247,7 +323,7 @@ describe('TransferForm', () => {
|
|||||||
|
|
||||||
await userEvent.selectOptions(
|
await userEvent.selectOptions(
|
||||||
screen.getByLabelText('From account'),
|
screen.getByLabelText('From account'),
|
||||||
AccountType.ACCOUNT_TYPE_VESTED_REWARDS
|
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const amountInput = screen.getByLabelText('Amount');
|
const amountInput = screen.getByLabelText('Amount');
|
||||||
@ -281,7 +357,7 @@ describe('TransferForm', () => {
|
|||||||
// 1003-TRAN-023
|
// 1003-TRAN-023
|
||||||
expect(mockSubmit).toHaveBeenCalledTimes(1);
|
expect(mockSubmit).toHaveBeenCalledTimes(1);
|
||||||
expect(mockSubmit).toHaveBeenCalledWith({
|
expect(mockSubmit).toHaveBeenCalledWith({
|
||||||
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||||
to: props.pubKeys[1],
|
to: props.pubKeys[1],
|
||||||
asset: asset.id,
|
asset: asset.id,
|
||||||
@ -303,7 +379,7 @@ describe('TransferForm', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await submit();
|
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
|
// Select a pubkey
|
||||||
await userEvent.selectOptions(
|
await userEvent.selectOptions(
|
||||||
@ -314,6 +390,11 @@ describe('TransferForm', () => {
|
|||||||
// Select asset
|
// Select asset
|
||||||
await selectAsset(asset);
|
await selectAsset(asset);
|
||||||
|
|
||||||
|
await userEvent.selectOptions(
|
||||||
|
screen.getByLabelText('From account'),
|
||||||
|
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
|
||||||
|
);
|
||||||
|
|
||||||
const amountInput = screen.getByLabelText('Amount');
|
const amountInput = screen.getByLabelText('Amount');
|
||||||
const checkbox = screen.getByTestId('include-transfer-fee');
|
const checkbox = screen.getByTestId('include-transfer-fee');
|
||||||
expect(checkbox).not.toBeChecked();
|
expect(checkbox).not.toBeChecked();
|
||||||
@ -333,26 +414,28 @@ describe('TransferForm', () => {
|
|||||||
|
|
||||||
describe('AddressField', () => {
|
describe('AddressField', () => {
|
||||||
const props = {
|
const props = {
|
||||||
|
mode: 'select' as const,
|
||||||
select: <div>select</div>,
|
select: <div>select</div>,
|
||||||
input: <div>input</div>,
|
input: <div>input</div>,
|
||||||
onChange: jest.fn(),
|
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();
|
const mockOnChange = jest.fn();
|
||||||
render(<AddressField {...props} onChange={mockOnChange} />);
|
const { rerender } = render(
|
||||||
|
<AddressField {...props} onChange={mockOnChange} />
|
||||||
|
);
|
||||||
|
|
||||||
// select should be shown by default
|
// select should be shown by default
|
||||||
expect(screen.getByText('select')).toBeInTheDocument();
|
expect(screen.getByText('select')).toBeInTheDocument();
|
||||||
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
||||||
await userEvent.click(screen.getByText('Enter manually'));
|
await userEvent.click(screen.getByText('Enter manually'));
|
||||||
|
expect(mockOnChange).toHaveBeenCalled();
|
||||||
|
|
||||||
|
rerender(<AddressField {...props} mode="input" />);
|
||||||
|
|
||||||
expect(screen.queryByText('select')).not.toBeInTheDocument();
|
expect(screen.queryByText('select')).not.toBeInTheDocument();
|
||||||
expect(screen.getByText('input')).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 sortBy from 'lodash/sortBy';
|
||||||
import {
|
import {
|
||||||
minSafe,
|
|
||||||
maxSafe,
|
maxSafe,
|
||||||
required,
|
required,
|
||||||
vegaPublicKey,
|
vegaPublicKey,
|
||||||
addDecimal,
|
addDecimal,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
addDecimalsFormatNumber,
|
addDecimalsFormatNumber,
|
||||||
|
toBigNum,
|
||||||
} from '@vegaprotocol/utils';
|
} from '@vegaprotocol/utils';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import {
|
import {
|
||||||
@ -27,14 +27,20 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { AssetOption, Balance } from '@vegaprotocol/assets';
|
import { AssetOption, Balance } from '@vegaprotocol/assets';
|
||||||
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
|
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
|
||||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
|
||||||
import { accountsDataProvider } from './accounts-data-provider';
|
|
||||||
|
|
||||||
interface FormFields {
|
interface FormFields {
|
||||||
toVegaKey: string;
|
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;
|
amount: string;
|
||||||
fromAccount: AccountType;
|
fromAccount: string; // AccountType-AssetId
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Asset {
|
||||||
|
id: string;
|
||||||
|
symbol: string;
|
||||||
|
name: string;
|
||||||
|
decimals: number;
|
||||||
|
quantum: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TransferFormProps {
|
export interface TransferFormProps {
|
||||||
@ -43,10 +49,11 @@ export interface TransferFormProps {
|
|||||||
accounts: Array<{
|
accounts: Array<{
|
||||||
type: AccountType;
|
type: AccountType;
|
||||||
balance: string;
|
balance: string;
|
||||||
asset: { id: string; symbol: string; name: string; decimals: number };
|
asset: Asset;
|
||||||
}>;
|
}>;
|
||||||
assetId?: string;
|
assetId?: string;
|
||||||
feeFactor: string | null;
|
feeFactor: string | null;
|
||||||
|
minQuantumMultiple: string | null;
|
||||||
submitTransfer: (transfer: Transfer) => void;
|
submitTransfer: (transfer: Transfer) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,6 +64,7 @@ export const TransferForm = ({
|
|||||||
feeFactor,
|
feeFactor,
|
||||||
submitTransfer,
|
submitTransfer,
|
||||||
accounts,
|
accounts,
|
||||||
|
minQuantumMultiple,
|
||||||
}: TransferFormProps) => {
|
}: TransferFormProps) => {
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@ -72,6 +80,8 @@ export const TransferForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [toVegaKeyMode, setToVegaKeyMode] = useState<ToVegaKeyMode>('select');
|
||||||
|
|
||||||
const assets = sortBy(
|
const assets = sortBy(
|
||||||
accounts
|
accounts
|
||||||
.filter(
|
.filter(
|
||||||
@ -99,48 +109,29 @@ export const TransferForm = ({
|
|||||||
...account.asset,
|
...account.asset,
|
||||||
balance: addDecimal(account.balance, account.asset.decimals),
|
balance: addDecimal(account.balance, account.asset.decimals),
|
||||||
})),
|
})),
|
||||||
'name'
|
(a) => a.symbol.toLowerCase()
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedPubKey = watch('toVegaKey');
|
const selectedPubKey = watch('toVegaKey');
|
||||||
const amount = watch('amount');
|
const amount = watch('amount');
|
||||||
const fromAccount = watch('fromAccount');
|
const fromAccount = watch('fromAccount');
|
||||||
const assetId = watch('asset');
|
const selectedAssetId = watch('asset');
|
||||||
|
|
||||||
const asset = assets.find((a) => a.id === assetId);
|
// Convert the account type (Type-AssetId) into separate values
|
||||||
|
const [accountType, accountAssetId] = fromAccount
|
||||||
const { data: toAccounts } = useDataProvider({
|
? parseFromAccount(fromAccount)
|
||||||
dataProvider: accountsDataProvider,
|
: [undefined, undefined];
|
||||||
variables: {
|
const fromVested = accountType === AccountType.ACCOUNT_TYPE_VESTED_REWARDS;
|
||||||
partyId: selectedPubKey,
|
const asset = assets.find((a) => a.id === accountAssetId);
|
||||||
},
|
|
||||||
skip: !selectedPubKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
const account = accounts.find(
|
const account = accounts.find(
|
||||||
(a) => a.asset.id === assetId && a.type === fromAccount
|
(a) => a.asset.id === accountAssetId && a.type === accountType
|
||||||
);
|
);
|
||||||
const accountBalance =
|
const accountBalance =
|
||||||
account && addDecimal(account.balance, account.asset.decimals);
|
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);
|
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
|
// Max amount given selected asset and from account
|
||||||
const max = accountBalance ? new BigNumber(accountBalance) : new BigNumber(0);
|
const max = accountBalance ? new BigNumber(accountBalance) : new BigNumber(0);
|
||||||
|
|
||||||
@ -164,16 +155,21 @@ export const TransferForm = ({
|
|||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
(fields: FormFields) => {
|
(fields: FormFields) => {
|
||||||
if (!asset) {
|
|
||||||
throw new Error('Submitted transfer with no asset selected');
|
|
||||||
}
|
|
||||||
if (!transferAmount) {
|
if (!transferAmount) {
|
||||||
throw new Error('Submitted transfer with no amount selected');
|
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(
|
const transfer = normalizeTransfer(
|
||||||
fields.toVegaKey,
|
fields.toVegaKey,
|
||||||
transferAmount,
|
transferAmount,
|
||||||
fields.fromAccount,
|
type,
|
||||||
AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form
|
AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form
|
||||||
{
|
{
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
@ -182,7 +178,7 @@ export const TransferForm = ({
|
|||||||
);
|
);
|
||||||
submitTransfer(transfer);
|
submitTransfer(transfer);
|
||||||
},
|
},
|
||||||
[asset, submitTransfer, transferAmount]
|
[submitTransfer, transferAmount, assets]
|
||||||
);
|
);
|
||||||
|
|
||||||
// reset for placeholder workaround https://github.com/radix-ui/primitives/issues/1569
|
// reset for placeholder workaround https://github.com/radix-ui/primitives/issues/1569
|
||||||
@ -198,55 +194,10 @@ export const TransferForm = ({
|
|||||||
className="text-sm"
|
className="text-sm"
|
||||||
data-testid="transfer-form"
|
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">
|
<TradingFormGroup label={t('Asset')} labelFor="asset">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="asset"
|
name="asset"
|
||||||
rules={{
|
|
||||||
validate: {
|
|
||||||
required,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<TradingRichSelect
|
<TradingRichSelect
|
||||||
data-testid="select-asset"
|
data-testid="select-asset"
|
||||||
@ -254,6 +205,7 @@ export const TransferForm = ({
|
|||||||
name={field.name}
|
name={field.name}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
field.onChange(value);
|
field.onChange(value);
|
||||||
|
setValue('fromAccount', '');
|
||||||
}}
|
}}
|
||||||
placeholder={t('Please select an asset')}
|
placeholder={t('Please select an asset')}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
@ -280,10 +232,10 @@ export const TransferForm = ({
|
|||||||
)}
|
)}
|
||||||
</TradingFormGroup>
|
</TradingFormGroup>
|
||||||
<TradingFormGroup label={t('From account')} labelFor="fromAccount">
|
<TradingFormGroup label={t('From account')} labelFor="fromAccount">
|
||||||
<TradingSelect
|
<Controller
|
||||||
id="fromAccount"
|
control={control}
|
||||||
defaultValue=""
|
name="fromAccount"
|
||||||
{...register('fromAccount', {
|
rules={{
|
||||||
validate: {
|
validate: {
|
||||||
required,
|
required,
|
||||||
sameAccount: (value) => {
|
sameAccount: (value) => {
|
||||||
@ -298,50 +250,106 @@ export const TransferForm = ({
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})}
|
}}
|
||||||
>
|
render={({ field }) => (
|
||||||
<option value="" disabled={true}>
|
<TradingSelect
|
||||||
{t('Please select')}
|
id="fromAccount"
|
||||||
</option>
|
defaultValue=""
|
||||||
{accounts
|
{...field}
|
||||||
.filter((a) => {
|
onChange={(e) => {
|
||||||
if (!assetId) return true;
|
field.onChange(e);
|
||||||
return assetId === a.asset.id;
|
|
||||||
})
|
const [type] = parseFromAccount(e.target.value);
|
||||||
.map((a) => {
|
|
||||||
return (
|
// Enforce that if transferring from a vested rewards account it must go to
|
||||||
<option value={a.type} key={`${a.type}-${a.asset.id}`}>
|
// the current connected general account
|
||||||
{AccountTypeMapping[a.type]} (
|
if (
|
||||||
{addDecimalsFormatNumber(a.balance, a.asset.decimals)}{' '}
|
type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS &&
|
||||||
{a.asset.symbol})
|
pubKey
|
||||||
</option>
|
) {
|
||||||
);
|
setValue('toVegaKey', pubKey);
|
||||||
})}
|
setToVegaKeyMode('select');
|
||||||
</TradingSelect>
|
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 && (
|
{errors.fromAccount?.message && (
|
||||||
<TradingInputError forInput="fromAccount">
|
<TradingInputError forInput="fromAccount">
|
||||||
{errors.fromAccount.message}
|
{errors.fromAccount.message}
|
||||||
</TradingInputError>
|
</TradingInputError>
|
||||||
)}
|
)}
|
||||||
</TradingFormGroup>
|
</TradingFormGroup>
|
||||||
<TradingFormGroup label={t('To account')} labelFor="toAccount">
|
<TradingFormGroup label="To Vega key" labelFor="toVegaKey">
|
||||||
<TradingSelect
|
<AddressField
|
||||||
id="toAccount"
|
onChange={() => {
|
||||||
defaultValue={AccountType.ACCOUNT_TYPE_GENERAL}
|
setValue('toVegaKey', '');
|
||||||
>
|
setToVegaKeyMode((curr) => (curr === 'input' ? 'select' : 'input'));
|
||||||
<option value={AccountType.ACCOUNT_TYPE_GENERAL}>
|
}}
|
||||||
{toGeneralAccount
|
mode={toVegaKeyMode}
|
||||||
? `${
|
select={
|
||||||
AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]
|
<TradingSelect
|
||||||
} (${addDecimalsFormatNumber(
|
{...register('toVegaKey')}
|
||||||
toGeneralAccount.balance,
|
disabled={fromVested}
|
||||||
toGeneralAccount.asset.decimals
|
id="toVegaKey"
|
||||||
)} ${toGeneralAccount.asset.symbol})`
|
>
|
||||||
: `${AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]} ${
|
<option value="" disabled={true}>
|
||||||
asset ? `(0 ${asset.symbol})` : ''
|
{t('Please select')}
|
||||||
}`}
|
</option>
|
||||||
</option>
|
{pubKeys?.map((pk) => {
|
||||||
</TradingSelect>
|
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>
|
||||||
<TradingFormGroup label="Amount" labelFor="amount">
|
<TradingFormGroup label="Amount" labelFor="amount">
|
||||||
<TradingInput
|
<TradingInput
|
||||||
@ -353,7 +361,43 @@ export const TransferForm = ({
|
|||||||
{...register('amount', {
|
{...register('amount', {
|
||||||
validate: {
|
validate: {
|
||||||
required,
|
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) => {
|
maxSafe: (v) => {
|
||||||
const value = new BigNumber(v);
|
const value = new BigNumber(v);
|
||||||
if (value.isGreaterThan(max)) {
|
if (value.isGreaterThan(max)) {
|
||||||
@ -369,7 +413,9 @@ export const TransferForm = ({
|
|||||||
type="button"
|
type="button"
|
||||||
className="absolute top-0 right-0 ml-auto text-xs underline"
|
className="absolute top-0 right-0 ml-auto text-xs underline"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setValue('amount', parseFloat(accountBalance).toString())
|
setValue('amount', parseFloat(accountBalance).toString(), {
|
||||||
|
shouldValidate: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('Use max')}
|
{t('Use max')}
|
||||||
@ -390,10 +436,10 @@ export const TransferForm = ({
|
|||||||
<div>
|
<div>
|
||||||
<TradingCheckbox
|
<TradingCheckbox
|
||||||
name="include-transfer-fee"
|
name="include-transfer-fee"
|
||||||
disabled={!transferAmount}
|
disabled={!transferAmount || fromVested}
|
||||||
label={t('Include transfer fee')}
|
label={t('Include transfer fee')}
|
||||||
checked={includeFee}
|
checked={includeFee}
|
||||||
onCheckedChange={() => setIncludeFee(!includeFee)}
|
onCheckedChange={() => setIncludeFee((x) => !x)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -403,7 +449,7 @@ export const TransferForm = ({
|
|||||||
amount={transferAmount}
|
amount={transferAmount}
|
||||||
transferAmount={transferAmount}
|
transferAmount={transferAmount}
|
||||||
feeFactor={feeFactor}
|
feeFactor={feeFactor}
|
||||||
fee={fee}
|
fee={fromVested ? '0' : fee}
|
||||||
decimals={asset?.decimals}
|
decimals={asset?.decimals}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -485,32 +531,38 @@ export const TransferFee = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ToVegaKeyMode = 'input' | 'select';
|
||||||
|
|
||||||
interface AddressInputProps {
|
interface AddressInputProps {
|
||||||
select: ReactNode;
|
select: ReactNode;
|
||||||
input: ReactNode;
|
input: ReactNode;
|
||||||
|
mode: ToVegaKeyMode;
|
||||||
onChange: () => void;
|
onChange: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddressField = ({
|
export const AddressField = ({
|
||||||
select,
|
select,
|
||||||
input,
|
input,
|
||||||
|
mode,
|
||||||
onChange,
|
onChange,
|
||||||
}: AddressInputProps) => {
|
}: AddressInputProps) => {
|
||||||
const [isInput, setIsInput] = useState(false);
|
const isInput = mode === 'input';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isInput ? input : select}
|
{isInput ? input : select}
|
||||||
<button
|
{select && input && (
|
||||||
type="button"
|
<button
|
||||||
onClick={() => {
|
type="button"
|
||||||
setIsInput((curr) => !curr);
|
onClick={onChange}
|
||||||
onChange();
|
className="absolute top-0 right-0 ml-auto text-xs underline"
|
||||||
}}
|
>
|
||||||
className="absolute top-0 right-0 ml-auto text-xs underline"
|
{isInput ? t('Select from wallet') : t('Enter manually')}
|
||||||
>
|
</button>
|
||||||
{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:
|
||||||
'market_liquidity_feeCalculationTimeStep',
|
'market_liquidity_feeCalculationTimeStep',
|
||||||
transfer_fee_factor: 'transfer_fee_factor',
|
transfer_fee_factor: 'transfer_fee_factor',
|
||||||
|
transfer_minTransferQuantumMultiple: 'transfer_minTransferQuantumMultiple',
|
||||||
network_validators_incumbentBonus: 'network_validators_incumbentBonus',
|
network_validators_incumbentBonus: 'network_validators_incumbentBonus',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user