feat(trading): transfer to and from accounts on same key (#5161)
This commit is contained in:
parent
673c896e2f
commit
e5d3f90d45
@ -9,12 +9,17 @@ import { useDataProvider } from '@vegaprotocol/data-provider';
|
|||||||
import type { Transfer } from '@vegaprotocol/wallet';
|
import type { Transfer } from '@vegaprotocol/wallet';
|
||||||
import { useVegaTransactionStore } from '@vegaprotocol/web3';
|
import { useVegaTransactionStore } from '@vegaprotocol/web3';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { accountsDataProvider } from './accounts-data-provider';
|
import { accountsDataProvider } from './accounts-data-provider';
|
||||||
import { TransferForm } from './transfer-form';
|
import { TransferForm } from './transfer-form';
|
||||||
import sortBy from 'lodash/sortBy';
|
import sortBy from 'lodash/sortBy';
|
||||||
import { Lozenge } from '@vegaprotocol/ui-toolkit';
|
import { Lozenge } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
|
export const ALLOWED_ACCOUNTS = [
|
||||||
|
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||||
|
Schema.AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||||
|
];
|
||||||
|
|
||||||
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 { param } = useNetworkParam(NetworkParams.transfer_fee_factor);
|
||||||
@ -33,20 +38,19 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
|||||||
[create]
|
[create]
|
||||||
);
|
);
|
||||||
|
|
||||||
const assets = useMemo(() => {
|
const accounts = data
|
||||||
if (!data) return [];
|
? data.filter((account) => ALLOWED_ACCOUNTS.includes(account.type))
|
||||||
return data
|
: [];
|
||||||
.filter(
|
|
||||||
(account) => account.type === Schema.AccountType.ACCOUNT_TYPE_GENERAL
|
const assets = accounts.map((account) => ({
|
||||||
)
|
id: account.asset.id,
|
||||||
.map((account) => ({
|
symbol: account.asset.symbol,
|
||||||
id: account.asset.id,
|
name: account.asset.name,
|
||||||
symbol: account.asset.symbol,
|
decimals: account.asset.decimals,
|
||||||
name: account.asset.name,
|
balance: addDecimal(account.balance, account.asset.decimals),
|
||||||
decimals: account.asset.decimals,
|
}));
|
||||||
balance: addDecimal(account.balance, account.asset.decimals),
|
|
||||||
}));
|
if (data === null) return null;
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -69,6 +73,7 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
|||||||
assetId={assetId}
|
assetId={assetId}
|
||||||
feeFactor={param}
|
feeFactor={param}
|
||||||
submitTransfer={transfer}
|
submitTransfer={transfer}
|
||||||
|
accounts={accounts}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,18 +1,38 @@
|
|||||||
import {
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||||
act,
|
import userEvent from '@testing-library/user-event';
|
||||||
fireEvent,
|
|
||||||
render,
|
|
||||||
screen,
|
|
||||||
waitFor,
|
|
||||||
} from '@testing-library/react';
|
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
import { AddressField, TransferFee, TransferForm } from './transfer-form';
|
import { AddressField, TransferFee, TransferForm } from './transfer-form';
|
||||||
import { AccountType } from '@vegaprotocol/types';
|
import { AccountType } from '@vegaprotocol/types';
|
||||||
import { addDecimal, formatNumber, removeDecimal } from '@vegaprotocol/utils';
|
import { addDecimal, formatNumber, removeDecimal } from '@vegaprotocol/utils';
|
||||||
import userEvent from '@testing-library/user-event';
|
|
||||||
|
|
||||||
describe('TransferForm', () => {
|
describe('TransferForm', () => {
|
||||||
const submit = () => fireEvent.submit(screen.getByTestId('transfer-form'));
|
const submit = async () => {
|
||||||
|
await userEvent.click(
|
||||||
|
screen.getByRole('button', { name: 'Confirm transfer' })
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectAsset = async (asset: {
|
||||||
|
id: string;
|
||||||
|
balance: string;
|
||||||
|
name: string;
|
||||||
|
decimals: number;
|
||||||
|
}) => {
|
||||||
|
// Bypass RichSelect and target hidden native select
|
||||||
|
// eslint-disable-next-line
|
||||||
|
fireEvent.change(document.querySelector('select[name="asset"]')!, {
|
||||||
|
target: { value: asset.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
// assert rich select as updated
|
||||||
|
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
|
||||||
|
asset.name
|
||||||
|
);
|
||||||
|
expect(await screen.findByTestId('asset-balance')).toHaveTextContent(
|
||||||
|
formatNumber(asset.balance, asset.decimals)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const amount = '100';
|
const amount = '100';
|
||||||
const pubKey =
|
const pubKey =
|
||||||
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
|
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
|
||||||
@ -32,6 +52,18 @@ describe('TransferForm', () => {
|
|||||||
assets: [asset],
|
assets: [asset],
|
||||||
feeFactor: '0.001',
|
feeFactor: '0.001',
|
||||||
submitTransfer: jest.fn(),
|
submitTransfer: jest.fn(),
|
||||||
|
accounts: [
|
||||||
|
{
|
||||||
|
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||||
|
asset,
|
||||||
|
balance: '100',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||||
|
asset,
|
||||||
|
balance: '100',
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
it('form tooltips correctly displayed', async () => {
|
it('form tooltips correctly displayed', async () => {
|
||||||
@ -42,49 +74,45 @@ describe('TransferForm', () => {
|
|||||||
// 1003-TRAN-019
|
// 1003-TRAN-019
|
||||||
render(<TransferForm {...props} />);
|
render(<TransferForm {...props} />);
|
||||||
// Select a pubkey
|
// Select a pubkey
|
||||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
await userEvent.selectOptions(
|
||||||
target: { value: props.pubKeys[1] },
|
screen.getByLabelText('Vega key'),
|
||||||
});
|
props.pubKeys[1]
|
||||||
|
);
|
||||||
|
|
||||||
// Select asset
|
// Select asset
|
||||||
fireEvent.change(
|
await selectAsset(asset);
|
||||||
// Bypass RichSelect and target hidden native select
|
|
||||||
// eslint-disable-next-line
|
|
||||||
document.querySelector('select[name="asset"]')!,
|
|
||||||
{ target: { value: asset.id } }
|
|
||||||
);
|
|
||||||
// set valid amount
|
// set valid amount
|
||||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
const amountInput = screen.getByLabelText('Amount');
|
||||||
target: { value: amount },
|
await userEvent.type(amountInput, amount);
|
||||||
});
|
expect(amountInput).toHaveValue(amount);
|
||||||
|
|
||||||
userEvent.hover(screen.getByText('Include transfer fee'));
|
const includeTransferLabel = screen.getByText('Include transfer fee');
|
||||||
|
await userEvent.hover(includeTransferLabel);
|
||||||
|
expect(await screen.findByRole('tooltip')).toHaveTextContent(
|
||||||
|
'The fee will be taken from the amount you are transferring.'
|
||||||
|
);
|
||||||
|
await userEvent.unhover(screen.getByText('Include transfer fee'));
|
||||||
|
|
||||||
await waitFor(() => {
|
const transferFee = screen.getByText('Transfer fee');
|
||||||
const tooltips = screen.getAllByTestId('tooltip-content');
|
await userEvent.hover(transferFee);
|
||||||
expect(tooltips[0]).toBeVisible();
|
expect(await screen.findByRole('tooltip')).toHaveTextContent(
|
||||||
});
|
/transfer.fee.factor/
|
||||||
|
);
|
||||||
|
await userEvent.unhover(transferFee);
|
||||||
|
|
||||||
userEvent.hover(screen.getByText('Transfer fee'));
|
const amountToBeTransferred = screen.getByText('Amount to be transferred');
|
||||||
|
await userEvent.hover(amountToBeTransferred);
|
||||||
|
expect(await screen.findByRole('tooltip')).toHaveTextContent(
|
||||||
|
/without the fee/
|
||||||
|
);
|
||||||
|
await userEvent.unhover(amountToBeTransferred);
|
||||||
|
|
||||||
await waitFor(() => {
|
const totalAmountWithFee = screen.getByText('Total amount (with fee)');
|
||||||
const tooltips = screen.getAllByTestId('tooltip-content');
|
await userEvent.hover(totalAmountWithFee);
|
||||||
expect(tooltips[0]).toBeVisible();
|
expect(await screen.findByRole('tooltip')).toHaveTextContent(
|
||||||
});
|
/total amount taken from your account/
|
||||||
|
);
|
||||||
userEvent.hover(screen.getByText('Amount to be transferred'));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const tooltips = screen.getAllByTestId('tooltip-content');
|
|
||||||
expect(tooltips[0]).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
userEvent.hover(screen.getByText('Total amount (with fee)'));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const tooltips = screen.getAllByTestId('tooltip-content');
|
|
||||||
expect(tooltips[0]).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('validates a manually entered address', async () => {
|
it('validates a manually entered address', async () => {
|
||||||
@ -92,30 +120,17 @@ describe('TransferForm', () => {
|
|||||||
// 1003-TRAN-013
|
// 1003-TRAN-013
|
||||||
// 1003-TRAN-004
|
// 1003-TRAN-004
|
||||||
render(<TransferForm {...props} />);
|
render(<TransferForm {...props} />);
|
||||||
submit();
|
await submit();
|
||||||
expect(await screen.findAllByText('Required')).toHaveLength(3);
|
expect(await screen.findAllByText('Required')).toHaveLength(4);
|
||||||
const toggle = screen.getByText('Enter manually');
|
const toggle = screen.getByText('Enter manually');
|
||||||
fireEvent.click(toggle);
|
await userEvent.click(toggle);
|
||||||
// has switched to input
|
// has switched to input
|
||||||
expect(toggle).toHaveTextContent('Select from wallet');
|
expect(toggle).toHaveTextContent('Select from wallet');
|
||||||
expect(screen.getByLabelText('Vega key')).toHaveAttribute('type', 'text');
|
expect(screen.getByLabelText('Vega key')).toHaveAttribute('type', 'text');
|
||||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
await userEvent.type(screen.getByLabelText('Vega key'), 'invalid-address');
|
||||||
target: { value: 'invalid-address' },
|
expect(screen.getAllByTestId('input-error-text')[0]).toHaveTextContent(
|
||||||
});
|
'Invalid Vega key'
|
||||||
await waitFor(() => {
|
);
|
||||||
const errors = screen.getAllByTestId('input-error-text');
|
|
||||||
expect(errors[0]).toHaveTextContent('Invalid Vega key');
|
|
||||||
});
|
|
||||||
|
|
||||||
// same pubkey
|
|
||||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
|
||||||
target: { value: pubKey },
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const errors = screen.getAllByTestId('input-error-text');
|
|
||||||
expect(errors[0]).toHaveTextContent('Vega key is the same');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('validates fields and submits', async () => {
|
it('validates fields and submits', async () => {
|
||||||
@ -127,28 +142,25 @@ describe('TransferForm', () => {
|
|||||||
render(<TransferForm {...props} />);
|
render(<TransferForm {...props} />);
|
||||||
|
|
||||||
// check current pubkey not shown
|
// check current pubkey not shown
|
||||||
const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
|
const keySelect = screen.getByLabelText<HTMLSelectElement>('Vega key');
|
||||||
expect(keySelect.children).toHaveLength(2);
|
expect(keySelect.children).toHaveLength(3);
|
||||||
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
|
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
|
||||||
'',
|
'',
|
||||||
|
pubKey,
|
||||||
props.pubKeys[1],
|
props.pubKeys[1],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
submit();
|
await submit();
|
||||||
expect(await screen.findAllByText('Required')).toHaveLength(3);
|
expect(await screen.findAllByText('Required')).toHaveLength(4);
|
||||||
|
|
||||||
// Select a pubkey
|
// Select a pubkey
|
||||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
await userEvent.selectOptions(
|
||||||
target: { value: props.pubKeys[1] },
|
screen.getByLabelText('Vega key'),
|
||||||
});
|
props.pubKeys[1]
|
||||||
|
);
|
||||||
|
|
||||||
// Select asset
|
// Select asset
|
||||||
fireEvent.change(
|
await selectAsset(asset);
|
||||||
// Bypass RichSelect and target hidden native select
|
|
||||||
// eslint-disable-next-line
|
|
||||||
document.querySelector('select[name="asset"]')!,
|
|
||||||
{ target: { value: asset.id } }
|
|
||||||
);
|
|
||||||
|
|
||||||
// assert rich select as updated
|
// assert rich select as updated
|
||||||
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
|
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
|
||||||
@ -158,37 +170,38 @@ describe('TransferForm', () => {
|
|||||||
formatNumber(asset.balance, asset.decimals)
|
formatNumber(asset.balance, asset.decimals)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await userEvent.selectOptions(
|
||||||
|
screen.getByLabelText('From account'),
|
||||||
|
AccountType.ACCOUNT_TYPE_VESTED_REWARDS
|
||||||
|
);
|
||||||
|
|
||||||
const amountInput = screen.getByLabelText('Amount');
|
const amountInput = screen.getByLabelText('Amount');
|
||||||
|
|
||||||
// Test amount validation
|
// Test amount validation
|
||||||
fireEvent.change(amountInput, {
|
await userEvent.type(amountInput, '0.00000001');
|
||||||
target: { value: '0.00000001' },
|
|
||||||
});
|
|
||||||
expect(
|
expect(
|
||||||
await screen.findByText('Value is below minimum')
|
await screen.findByText('Value is below minimum')
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
|
||||||
fireEvent.change(amountInput, {
|
await userEvent.clear(amountInput);
|
||||||
target: { value: '9999999' },
|
await userEvent.type(amountInput, '9999999');
|
||||||
});
|
|
||||||
expect(
|
expect(
|
||||||
await screen.findByText(/cannot transfer more/i)
|
await screen.findByText(/cannot transfer more/i)
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
|
||||||
// set valid amount
|
// set valid amount
|
||||||
fireEvent.change(amountInput, {
|
await userEvent.clear(amountInput);
|
||||||
target: { value: amount },
|
await userEvent.type(amountInput, amount);
|
||||||
});
|
|
||||||
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(
|
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(
|
||||||
new BigNumber(props.feeFactor).times(amount).toFixed()
|
new BigNumber(props.feeFactor).times(amount).toFixed()
|
||||||
);
|
);
|
||||||
|
|
||||||
submit();
|
await submit();
|
||||||
|
|
||||||
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_GENERAL,
|
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||||
to: props.pubKeys[1],
|
to: props.pubKeys[1],
|
||||||
asset: asset.id,
|
asset: asset.id,
|
||||||
@ -200,59 +213,50 @@ describe('TransferForm', () => {
|
|||||||
|
|
||||||
describe('IncludeFeesCheckbox', () => {
|
describe('IncludeFeesCheckbox', () => {
|
||||||
it('validates fields and submits when checkbox is checked', async () => {
|
it('validates fields and submits when checkbox is checked', async () => {
|
||||||
render(<TransferForm {...props} />);
|
const mockSubmit = jest.fn();
|
||||||
|
render(<TransferForm {...props} submitTransfer={mockSubmit} />);
|
||||||
|
|
||||||
// check current pubkey not shown
|
// check current pubkey not shown
|
||||||
const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
|
const keySelect = screen.getByLabelText<HTMLSelectElement>('Vega key');
|
||||||
expect(keySelect.children).toHaveLength(2);
|
const pubKeyOptions = ['', pubKey, props.pubKeys[1]];
|
||||||
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
|
expect(keySelect.children).toHaveLength(pubKeyOptions.length);
|
||||||
'',
|
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual(
|
||||||
props.pubKeys[1],
|
pubKeyOptions
|
||||||
]);
|
);
|
||||||
|
|
||||||
submit();
|
await submit();
|
||||||
expect(await screen.findAllByText('Required')).toHaveLength(3);
|
expect(await screen.findAllByText('Required')).toHaveLength(4);
|
||||||
|
|
||||||
// Select a pubkey
|
// Select a pubkey
|
||||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
await userEvent.selectOptions(
|
||||||
target: { value: props.pubKeys[1] },
|
screen.getByLabelText('Vega key'),
|
||||||
});
|
props.pubKeys[1]
|
||||||
|
);
|
||||||
|
|
||||||
// Select asset
|
// Select asset
|
||||||
fireEvent.change(
|
await selectAsset(asset);
|
||||||
// Bypass RichSelect and target hidden native select
|
|
||||||
// eslint-disable-next-line
|
|
||||||
document.querySelector('select[name="asset"]')!,
|
|
||||||
{ target: { value: asset.id } }
|
|
||||||
);
|
|
||||||
|
|
||||||
// assert rich select as updated
|
await userEvent.selectOptions(
|
||||||
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
|
screen.getByLabelText('From account'),
|
||||||
asset.name
|
AccountType.ACCOUNT_TYPE_VESTED_REWARDS
|
||||||
);
|
|
||||||
expect(await screen.findByTestId('asset-balance')).toHaveTextContent(
|
|
||||||
formatNumber(asset.balance, asset.decimals)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const amountInput = screen.getByLabelText('Amount');
|
const amountInput = screen.getByLabelText('Amount');
|
||||||
const checkbox = screen.getByTestId('include-transfer-fee');
|
const checkbox = screen.getByTestId('include-transfer-fee');
|
||||||
|
|
||||||
// 1003-TRAN-022
|
// 1003-TRAN-022
|
||||||
expect(checkbox).not.toBeChecked();
|
expect(checkbox).not.toBeChecked();
|
||||||
act(() => {
|
|
||||||
/* fire events that update state */
|
await userEvent.clear(amountInput);
|
||||||
// set valid amount
|
await userEvent.type(amountInput, amount);
|
||||||
fireEvent.change(amountInput, {
|
await userEvent.click(checkbox);
|
||||||
target: { value: amount },
|
|
||||||
});
|
|
||||||
// check include fees checkbox
|
|
||||||
fireEvent.click(checkbox);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(checkbox).toBeChecked();
|
expect(checkbox).toBeChecked();
|
||||||
const expectedFee = new BigNumber(amount)
|
const expectedFee = new BigNumber(amount)
|
||||||
.times(props.feeFactor)
|
.times(props.feeFactor)
|
||||||
.toFixed();
|
.toFixed();
|
||||||
const expectedAmount = new BigNumber(amount).minus(expectedFee).toFixed();
|
const expectedAmount = new BigNumber(amount).minus(expectedFee).toFixed();
|
||||||
|
|
||||||
// 1003-TRAN-020
|
// 1003-TRAN-020
|
||||||
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
|
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
|
||||||
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
|
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
|
||||||
@ -262,18 +266,17 @@ describe('TransferForm', () => {
|
|||||||
amount
|
amount
|
||||||
);
|
);
|
||||||
|
|
||||||
submit();
|
await submit();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
// 1003-TRAN-023
|
// 1003-TRAN-023
|
||||||
|
expect(mockSubmit).toHaveBeenCalledTimes(1);
|
||||||
expect(props.submitTransfer).toHaveBeenCalledTimes(1);
|
expect(mockSubmit).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,
|
||||||
amount: removeDecimal(amount, asset.decimals),
|
amount: removeDecimal(expectedAmount, asset.decimals),
|
||||||
oneOff: {},
|
oneOff: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -284,46 +287,29 @@ describe('TransferForm', () => {
|
|||||||
|
|
||||||
// check current pubkey not shown
|
// check current pubkey not shown
|
||||||
const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
|
const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
|
||||||
expect(keySelect.children).toHaveLength(2);
|
const pubKeyOptions = ['', pubKey, props.pubKeys[1]];
|
||||||
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
|
expect(keySelect.children).toHaveLength(pubKeyOptions.length);
|
||||||
'',
|
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual(
|
||||||
props.pubKeys[1],
|
pubKeyOptions
|
||||||
]);
|
);
|
||||||
|
|
||||||
submit();
|
await submit();
|
||||||
expect(await screen.findAllByText('Required')).toHaveLength(3);
|
expect(await screen.findAllByText('Required')).toHaveLength(4);
|
||||||
|
|
||||||
// Select a pubkey
|
// Select a pubkey
|
||||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
await userEvent.selectOptions(
|
||||||
target: { value: props.pubKeys[1] },
|
screen.getByLabelText('Vega key'),
|
||||||
});
|
props.pubKeys[1]
|
||||||
|
);
|
||||||
|
|
||||||
// Select asset
|
// Select asset
|
||||||
fireEvent.change(
|
await selectAsset(asset);
|
||||||
// Bypass RichSelect and target hidden native select
|
|
||||||
// eslint-disable-next-line
|
|
||||||
document.querySelector('select[name="asset"]')!,
|
|
||||||
{ target: { value: asset.id } }
|
|
||||||
);
|
|
||||||
|
|
||||||
// assert rich select as updated
|
|
||||||
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
|
|
||||||
asset.name
|
|
||||||
);
|
|
||||||
expect(await screen.findByTestId('asset-balance')).toHaveTextContent(
|
|
||||||
formatNumber(asset.balance, asset.decimals)
|
|
||||||
);
|
|
||||||
|
|
||||||
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();
|
||||||
act(() => {
|
|
||||||
/* fire events that update state */
|
await userEvent.type(amountInput, amount);
|
||||||
// set valid amount
|
|
||||||
fireEvent.change(amountInput, {
|
|
||||||
target: { value: amount },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
expect(checkbox).not.toBeChecked();
|
expect(checkbox).not.toBeChecked();
|
||||||
const expectedFee = new BigNumber(amount)
|
const expectedFee = new BigNumber(amount)
|
||||||
.times(props.feeFactor)
|
.times(props.feeFactor)
|
||||||
@ -351,11 +337,11 @@ describe('TransferForm', () => {
|
|||||||
// select should be shown as multiple pubkeys provided
|
// select should be shown as multiple pubkeys provided
|
||||||
expect(screen.getByText('select')).toBeInTheDocument();
|
expect(screen.getByText('select')).toBeInTheDocument();
|
||||||
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByText('Enter manually'));
|
await userEvent.click(screen.getByText('Enter manually'));
|
||||||
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);
|
expect(mockOnChange).toHaveBeenCalledTimes(1);
|
||||||
fireEvent.click(screen.getByText('Select from wallet'));
|
await userEvent.click(screen.getByText('Select from wallet'));
|
||||||
expect(screen.getByText('select')).toBeInTheDocument();
|
expect(screen.getByText('select')).toBeInTheDocument();
|
||||||
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
||||||
expect(mockOnChange).toHaveBeenCalledTimes(2);
|
expect(mockOnChange).toHaveBeenCalledTimes(2);
|
||||||
|
@ -24,22 +24,30 @@ import type { ReactNode } from 'react';
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
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';
|
||||||
|
|
||||||
interface FormFields {
|
interface FormFields {
|
||||||
toAddress: string;
|
toAddress: string;
|
||||||
asset: string;
|
asset: string;
|
||||||
amount: string;
|
amount: string;
|
||||||
|
fromAccount: AccountType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Asset {
|
||||||
|
id: string;
|
||||||
|
symbol: string;
|
||||||
|
name: string;
|
||||||
|
decimals: number;
|
||||||
|
balance: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TransferFormProps {
|
interface TransferFormProps {
|
||||||
pubKey: string | null;
|
pubKey: string | null;
|
||||||
pubKeys: string[] | null;
|
pubKeys: string[] | null;
|
||||||
assets: Array<{
|
assets: Array<Asset>;
|
||||||
id: string;
|
accounts: Array<{
|
||||||
symbol: string;
|
type: AccountType;
|
||||||
name: string;
|
asset: { id: string; symbol: string };
|
||||||
decimals: number;
|
|
||||||
balance: string;
|
|
||||||
}>;
|
}>;
|
||||||
assetId?: string;
|
assetId?: string;
|
||||||
feeFactor: string | null;
|
feeFactor: string | null;
|
||||||
@ -53,6 +61,7 @@ export const TransferForm = ({
|
|||||||
assetId: initialAssetId,
|
assetId: initialAssetId,
|
||||||
feeFactor,
|
feeFactor,
|
||||||
submitTransfer,
|
submitTransfer,
|
||||||
|
accounts,
|
||||||
}: TransferFormProps) => {
|
}: TransferFormProps) => {
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@ -67,6 +76,7 @@ export const TransferForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectedPubKey = watch('toAddress');
|
||||||
const amount = watch('amount');
|
const amount = watch('amount');
|
||||||
const assetId = watch('asset');
|
const assetId = watch('asset');
|
||||||
|
|
||||||
@ -102,10 +112,16 @@ export const TransferForm = ({
|
|||||||
if (!transferAmount) {
|
if (!transferAmount) {
|
||||||
throw new Error('Submitted transfer with no amount selected');
|
throw new Error('Submitted transfer with no amount selected');
|
||||||
}
|
}
|
||||||
const transfer = normalizeTransfer(fields.toAddress, transferAmount, {
|
const transfer = normalizeTransfer(
|
||||||
id: asset.id,
|
fields.toAddress,
|
||||||
decimals: asset.decimals,
|
transferAmount,
|
||||||
});
|
fields.fromAccount,
|
||||||
|
AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form
|
||||||
|
{
|
||||||
|
id: asset.id,
|
||||||
|
decimals: asset.decimals,
|
||||||
|
}
|
||||||
|
);
|
||||||
submitTransfer(transfer);
|
submitTransfer(transfer);
|
||||||
},
|
},
|
||||||
[asset, submitTransfer, transferAmount]
|
[asset, submitTransfer, transferAmount]
|
||||||
@ -137,57 +153,53 @@ export const TransferForm = ({
|
|||||||
className="text-sm"
|
className="text-sm"
|
||||||
data-testid="transfer-form"
|
data-testid="transfer-form"
|
||||||
>
|
>
|
||||||
<TradingFormGroup label="Vega key" labelFor="to-address">
|
<TradingFormGroup label="Vega key" labelFor="toAddress">
|
||||||
<AddressField
|
<AddressField
|
||||||
pubKeys={pubKeys}
|
pubKeys={pubKeys}
|
||||||
onChange={() => setValue('toAddress', '')}
|
onChange={() => setValue('toAddress', '')}
|
||||||
select={
|
select={
|
||||||
<TradingSelect
|
<TradingSelect
|
||||||
{...register('toAddress')}
|
{...register('toAddress')}
|
||||||
id="to-address"
|
id="toAddress"
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
>
|
>
|
||||||
<option value="" disabled={true}>
|
<option value="" disabled={true}>
|
||||||
{t('Please select')}
|
{t('Please select')}
|
||||||
</option>
|
</option>
|
||||||
{pubKeys?.length &&
|
{pubKeys?.length &&
|
||||||
pubKeys
|
pubKeys.map((pk) => {
|
||||||
.filter((pk) => pk !== pubKey) // remove currently selected pubkey
|
const text = pk === pubKey ? t('Current key: ') + pk : pk;
|
||||||
.map((pk) => (
|
|
||||||
|
return (
|
||||||
<option key={pk} value={pk}>
|
<option key={pk} value={pk}>
|
||||||
{pk}
|
{text}
|
||||||
</option>
|
</option>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</TradingSelect>
|
</TradingSelect>
|
||||||
}
|
}
|
||||||
input={
|
input={
|
||||||
<TradingInput
|
<TradingInput
|
||||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||||
autoFocus={true} // focus input immediately after is shown
|
autoFocus={true} // focus input immediately after is shown
|
||||||
id="to-address"
|
id="toAddress"
|
||||||
type="text"
|
type="text"
|
||||||
{...register('toAddress', {
|
{...register('toAddress', {
|
||||||
validate: {
|
validate: {
|
||||||
required,
|
required,
|
||||||
vegaPublicKey,
|
vegaPublicKey,
|
||||||
sameKey: (value) => {
|
|
||||||
if (value === pubKey) {
|
|
||||||
return t('Vega key is the same as current key');
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{errors.toAddress?.message && (
|
{errors.toAddress?.message && (
|
||||||
<TradingInputError forInput="to-address">
|
<TradingInputError forInput="toAddress">
|
||||||
{errors.toAddress.message}
|
{errors.toAddress.message}
|
||||||
</TradingInputError>
|
</TradingInputError>
|
||||||
)}
|
)}
|
||||||
</TradingFormGroup>
|
</TradingFormGroup>
|
||||||
<TradingFormGroup label="Asset" labelFor="asset">
|
<TradingFormGroup label={t('Asset')} labelFor="asset">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="asset"
|
name="asset"
|
||||||
@ -228,6 +240,63 @@ export const TransferForm = ({
|
|||||||
</TradingInputError>
|
</TradingInputError>
|
||||||
)}
|
)}
|
||||||
</TradingFormGroup>
|
</TradingFormGroup>
|
||||||
|
<TradingFormGroup label={t('From account')} labelFor="fromAccount">
|
||||||
|
<TradingSelect
|
||||||
|
id="fromAccount"
|
||||||
|
defaultValue=""
|
||||||
|
{...register('fromAccount', {
|
||||||
|
validate: {
|
||||||
|
required,
|
||||||
|
sameAccount: (value) => {
|
||||||
|
if (
|
||||||
|
pubKey === selectedPubKey &&
|
||||||
|
value === AccountType.ACCOUNT_TYPE_GENERAL
|
||||||
|
) {
|
||||||
|
return t(
|
||||||
|
'Cannot transfer to the same account type for the connected key'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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]} ({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}>
|
||||||
|
{asset
|
||||||
|
? `${AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]} (${
|
||||||
|
asset.symbol
|
||||||
|
})`
|
||||||
|
: AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]}
|
||||||
|
</option>
|
||||||
|
</TradingSelect>
|
||||||
|
</TradingFormGroup>
|
||||||
<TradingFormGroup label="Amount" labelFor="amount">
|
<TradingFormGroup label="Amount" labelFor="amount">
|
||||||
<TradingInput
|
<TradingInput
|
||||||
id="amount"
|
id="amount"
|
||||||
@ -391,7 +460,7 @@ export const AddressField = ({
|
|||||||
setIsInput((curr) => !curr);
|
setIsInput((curr) => !curr);
|
||||||
onChange();
|
onChange();
|
||||||
}}
|
}}
|
||||||
className="absolute top-0 right-0 ml-auto text-sm underline"
|
className="absolute top-0 right-0 ml-auto text-xs underline"
|
||||||
>
|
>
|
||||||
{isInput ? t('Select from wallet') : t('Enter manually')}
|
{isInput ? t('Select from wallet') : t('Enter manually')}
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
||||||
import type { Market, Order } from '@vegaprotocol/types';
|
import type { Market, Order } from '@vegaprotocol/types';
|
||||||
import { AccountType } from '@vegaprotocol/types';
|
import type { AccountType } from '@vegaprotocol/types';
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
import { ethers } from 'ethers';
|
import { ethers } from 'ethers';
|
||||||
import { sha3_256 } from 'js-sha3';
|
import { sha3_256 } from 'js-sha3';
|
||||||
@ -47,15 +47,17 @@ export const normalizeOrderAmendment = <T extends Exact<OrderAmendment, T>>(
|
|||||||
export const normalizeTransfer = <T extends Exact<Transfer, T>>(
|
export const normalizeTransfer = <T extends Exact<Transfer, T>>(
|
||||||
address: string,
|
address: string,
|
||||||
amount: string,
|
amount: string,
|
||||||
|
fromAccountType: AccountType,
|
||||||
|
toAccountType: AccountType,
|
||||||
asset: {
|
asset: {
|
||||||
id: string;
|
id: string;
|
||||||
decimals: number;
|
decimals: number;
|
||||||
}
|
}
|
||||||
): Transfer => {
|
): Transfer => {
|
||||||
return {
|
return {
|
||||||
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
|
||||||
to: address,
|
to: address,
|
||||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
fromAccountType,
|
||||||
|
toAccountType,
|
||||||
asset: asset.id,
|
asset: asset.id,
|
||||||
amount: removeDecimal(amount, asset.decimals),
|
amount: removeDecimal(amount, asset.decimals),
|
||||||
// oneOff or recurring required otherwise wallet will error
|
// oneOff or recurring required otherwise wallet will error
|
||||||
|
Loading…
Reference in New Issue
Block a user