Skip to content

Commit

Permalink
feat(accounts): transfer form improvements to handle transfers from v…
Browse files Browse the repository at this point in the history
…esting accounts (#5212)

Co-authored-by: Madalina Raicu <[email protected]>
  • Loading branch information
mattrussell36 and MadalinaRaicu authored Nov 9, 2023
1 parent 73f37c2 commit f780013
Show file tree
Hide file tree
Showing 4 changed files with 319 additions and 176 deletions.
15 changes: 11 additions & 4 deletions libs/accounts/src/lib/transfer-container.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 || '' },
Expand All @@ -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 (
<>
Expand All @@ -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}
/>
</>
);
Expand Down
141 changes: 112 additions & 29 deletions libs/accounts/src/lib/transfer-form.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -54,6 +46,7 @@ describe('TransferForm', () => {
symbol: '€',
name: 'EUR',
decimals: 2,
quantum: '1',
};
const props = {
pubKey,
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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(
Expand All @@ -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);
Expand All @@ -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,
Expand All @@ -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();
Expand All @@ -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(
Expand All @@ -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');
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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();
Expand All @@ -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);
});
});

Expand Down
Loading

0 comments on commit f780013

Please sign in to comment.