diff --git a/libs/accounts/src/lib/transfer-form.spec.tsx b/libs/accounts/src/lib/transfer-form.spec.tsx
index 601274c5a..131c51fee 100644
--- a/libs/accounts/src/lib/transfer-form.spec.tsx
+++ b/libs/accounts/src/lib/transfer-form.spec.tsx
@@ -1,4 +1,10 @@
-import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import {
+ act,
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from '@testing-library/react';
import BigNumber from 'bignumber.js';
import { AddressField, TransferFee, TransferForm } from './transfer-form';
import { AccountType } from '@vegaprotocol/types';
@@ -27,6 +33,34 @@ describe('TransferForm', () => {
submitTransfer: jest.fn(),
};
+ it('validates a manually entered address', async () => {
+ render();
+ submit();
+ expect(await screen.findAllByText('Required')).toHaveLength(3);
+ const toggle = screen.getByText('Enter manually');
+ fireEvent.click(toggle);
+ // has switched to input
+ expect(toggle).toHaveTextContent('Select from wallet');
+ expect(screen.getByLabelText('Vega key')).toHaveAttribute('type', 'text');
+ fireEvent.change(screen.getByLabelText('Vega key'), {
+ target: { value: 'invalid-address' },
+ });
+ 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 () => {
render();
@@ -62,15 +96,17 @@ describe('TransferForm', () => {
formatNumber(asset.balance, asset.decimals)
);
+ const amountInput = screen.getByLabelText('Amount');
+
// Test amount validation
- fireEvent.change(screen.getByLabelText('Amount'), {
+ fireEvent.change(amountInput, {
target: { value: '0.00000001' },
});
expect(
await screen.findByText('Value is below minimum')
).toBeInTheDocument();
- fireEvent.change(screen.getByLabelText('Amount'), {
+ fireEvent.change(amountInput, {
target: { value: '9999999' },
});
expect(
@@ -78,7 +114,7 @@ describe('TransferForm', () => {
).toBeInTheDocument();
// set valid amount
- fireEvent.change(screen.getByLabelText('Amount'), {
+ fireEvent.change(amountInput, {
target: { value: amount },
});
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(
@@ -100,78 +136,191 @@ describe('TransferForm', () => {
});
});
- it('validates a manually entered address', async () => {
- render();
- submit();
- expect(await screen.findAllByText('Required')).toHaveLength(3);
- const toggle = screen.getByText('Enter manually');
- fireEvent.click(toggle);
- // has switched to input
- expect(toggle).toHaveTextContent('Select from wallet');
- expect(screen.getByLabelText('Vega key')).toHaveAttribute('type', 'text');
- fireEvent.change(screen.getByLabelText('Vega key'), {
- target: { value: 'invalid-address' },
- });
- await waitFor(() => {
- const errors = screen.getAllByTestId('input-error-text');
- expect(errors[0]).toHaveTextContent('Invalid Vega key');
+ describe('IncludeFeesCheckbox', () => {
+ it('validates fields and submits when checkbox is checked', async () => {
+ render();
+
+ // check current pubkey not shown
+ const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
+ expect(keySelect.children).toHaveLength(2);
+ expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
+ '',
+ props.pubKeys[1],
+ ]);
+
+ submit();
+ expect(await screen.findAllByText('Required')).toHaveLength(3);
+
+ // Select a pubkey
+ fireEvent.change(screen.getByLabelText('Vega key'), {
+ target: { value: props.pubKeys[1] },
+ });
+
+ // Select asset
+ fireEvent.change(
+ // 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(screen.getByTestId('asset-balance')).toHaveTextContent(
+ formatNumber(asset.balance, asset.decimals)
+ );
+
+ const amountInput = screen.getByLabelText('Amount');
+ const checkbox = screen.getByTestId('include-transfer-fee');
+ expect(checkbox).not.toBeChecked();
+ act(() => {
+ /* fire events that update state */
+ // set valid amount
+ fireEvent.change(amountInput, {
+ target: { value: amount },
+ });
+ // check include fees checkbox
+ fireEvent.click(checkbox);
+ });
+
+ expect(checkbox).toBeChecked();
+ const expectedFee = new BigNumber(amount)
+ .times(props.feeFactor)
+ .toFixed();
+ const expectedAmount = new BigNumber(amount).minus(expectedFee).toFixed();
+ expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
+ expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
+ expectedAmount
+ );
+ expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(
+ amount
+ );
+
+ submit();
+
+ await waitFor(() => {
+ expect(props.submitTransfer).toHaveBeenCalledTimes(1);
+ expect(props.submitTransfer).toHaveBeenCalledWith({
+ fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
+ toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
+ to: props.pubKeys[1],
+ asset: asset.id,
+ amount: removeDecimal(amount, asset.decimals),
+ oneOff: {},
+ });
+ });
});
- // same pubkey
- fireEvent.change(screen.getByLabelText('Vega key'), {
- target: { value: pubKey },
+ it('validates fields when checkbox is not checked', async () => {
+ render();
+
+ // check current pubkey not shown
+ const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
+ expect(keySelect.children).toHaveLength(2);
+ expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
+ '',
+ props.pubKeys[1],
+ ]);
+
+ submit();
+ expect(await screen.findAllByText('Required')).toHaveLength(3);
+
+ // Select a pubkey
+ fireEvent.change(screen.getByLabelText('Vega key'), {
+ target: { value: props.pubKeys[1] },
+ });
+
+ // Select asset
+ fireEvent.change(
+ // 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(screen.getByTestId('asset-balance')).toHaveTextContent(
+ formatNumber(asset.balance, asset.decimals)
+ );
+
+ const amountInput = screen.getByLabelText('Amount');
+ const checkbox = screen.getByTestId('include-transfer-fee');
+ expect(checkbox).not.toBeChecked();
+ act(() => {
+ /* fire events that update state */
+ // set valid amount
+ fireEvent.change(amountInput, {
+ target: { value: amount },
+ });
+ });
+ expect(checkbox).not.toBeChecked();
+ const expectedFee = new BigNumber(amount)
+ .times(props.feeFactor)
+ .toFixed();
+ 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);
+ });
+ });
+
+ describe('AddressField', () => {
+ const props = {
+ pubKeys: ['pubkey-1', 'pubkey-2'],
+ select:
select
,
+ input: input
,
+ onChange: jest.fn(),
+ };
+
+ it('toggles content and calls onChange', async () => {
+ const mockOnChange = jest.fn();
+ render();
+
+ // select should be shown as multiple pubkeys provided
+ expect(screen.getByText('select')).toBeInTheDocument();
+ expect(screen.queryByText('input')).not.toBeInTheDocument();
+ fireEvent.click(screen.getByText('Enter manually'));
+ expect(screen.queryByText('select')).not.toBeInTheDocument();
+ expect(screen.getByText('input')).toBeInTheDocument();
+ expect(mockOnChange).toHaveBeenCalledTimes(1);
+ fireEvent.click(screen.getByText('Select from wallet'));
+ expect(screen.getByText('select')).toBeInTheDocument();
+ expect(screen.queryByText('input')).not.toBeInTheDocument();
+ expect(mockOnChange).toHaveBeenCalledTimes(2);
});
- await waitFor(() => {
- const errors = screen.getAllByTestId('input-error-text');
- expect(errors[0]).toHaveTextContent('Vega key is the same');
+ it('Does not provide select option if there is only a single key', () => {
+ render();
+ expect(screen.getByText('input')).toBeInTheDocument();
+ expect(screen.queryByText('Select from wallet')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('TransferFee', () => {
+ const props = {
+ amount: '200',
+ feeFactor: '0.001',
+ fee: '0.2',
+ transferAmount: '200',
+ decimals: 8,
+ };
+ it('calculates and renders the transfer fee', () => {
+ render();
+
+ const expected = new BigNumber(props.amount)
+ .times(props.feeFactor)
+ .toFixed();
+ const total = new BigNumber(props.amount).plus(expected).toFixed();
+ expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expected);
+ expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
+ props.amount
+ );
+ expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
});
});
});
-
-describe('AddressField', () => {
- const props = {
- pubKeys: ['pubkey-1', 'pubkey-2'],
- select: select
,
- input: input
,
- onChange: jest.fn(),
- };
-
- it('toggles content and calls onChange', async () => {
- const mockOnChange = jest.fn();
- render();
-
- // select should be shown as multiple pubkeys provided
- expect(screen.getByText('select')).toBeInTheDocument();
- expect(screen.queryByText('input')).not.toBeInTheDocument();
- fireEvent.click(screen.getByText('Enter manually'));
- expect(screen.queryByText('select')).not.toBeInTheDocument();
- expect(screen.getByText('input')).toBeInTheDocument();
- expect(mockOnChange).toHaveBeenCalledTimes(1);
- fireEvent.click(screen.getByText('Select from wallet'));
- expect(screen.getByText('select')).toBeInTheDocument();
- expect(screen.queryByText('input')).not.toBeInTheDocument();
- expect(mockOnChange).toHaveBeenCalledTimes(2);
- });
-
- it('Does not provide select option if there is only a single key', () => {
- render();
- expect(screen.getByText('input')).toBeInTheDocument();
- expect(screen.queryByText('Select from wallet')).not.toBeInTheDocument();
- });
-});
-
-describe('TransferFee', () => {
- const props = {
- amount: '200',
- feeFactor: '0.001',
- };
- it('calculates and renders the transfer fee', () => {
- render();
-
- const expected = new BigNumber(props.amount)
- .times(props.feeFactor)
- .toFixed();
- expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expected);
- });
-});
diff --git a/libs/accounts/src/lib/transfer-form.tsx b/libs/accounts/src/lib/transfer-form.tsx
index 417d17ede..7a56c9342 100644
--- a/libs/accounts/src/lib/transfer-form.tsx
+++ b/libs/accounts/src/lib/transfer-form.tsx
@@ -16,6 +16,7 @@ import {
RichSelect,
Select,
Tooltip,
+ Checkbox,
} from '@vegaprotocol/ui-toolkit';
import type { Transfer } from '@vegaprotocol/wallet';
import { normalizeTransfer } from '@vegaprotocol/wallet';
@@ -63,6 +64,26 @@ export const TransferForm = ({
const amount = watch('amount');
const assetId = watch('asset');
+ const [includeFee, setIncludeFee] = useState(false);
+
+ const transferAmount = useMemo(() => {
+ if (!amount) return undefined;
+ if (includeFee && feeFactor) {
+ return new BigNumber(1).minus(feeFactor).times(amount).toString();
+ }
+ return amount;
+ }, [amount, includeFee, feeFactor]);
+
+ const fee = useMemo(() => {
+ if (!transferAmount) return undefined;
+ if (includeFee) {
+ return new BigNumber(amount).minus(transferAmount).toString();
+ }
+ return (
+ feeFactor && new BigNumber(feeFactor).times(transferAmount).toString()
+ );
+ }, [amount, includeFee, transferAmount, feeFactor]);
+
const asset = useMemo(() => {
return assets.find((a) => a.id === assetId);
}, [assets, assetId]);
@@ -72,13 +93,16 @@ export const TransferForm = ({
if (!asset) {
throw new Error('Submitted transfer with no asset selected');
}
- const transfer = normalizeTransfer(fields.toAddress, fields.amount, {
+ if (!transferAmount) {
+ throw new Error('Submitted transfer with no amount selected');
+ }
+ const transfer = normalizeTransfer(fields.toAddress, transferAmount, {
id: asset.id,
decimals: asset.decimals,
});
submitTransfer(transfer);
},
- [asset, submitTransfer]
+ [asset, submitTransfer, transferAmount]
);
const min = useMemo(() => {
@@ -213,7 +237,32 @@ export const TransferForm = ({
{errors.amount.message}
)}
-
+
+
+ {t('Include transfer fee')}
+
+ }
+ checked={includeFee}
+ onCheckedChange={() => setIncludeFee(!includeFee)}
+ />
+
+ {transferAmount && fee && (
+
+ )}
@@ -223,34 +272,71 @@ export const TransferForm = ({
export const TransferFee = ({
amount,
+ transferAmount,
feeFactor,
+ fee,
+ decimals,
}: {
amount: string;
+ transferAmount: string;
feeFactor: string | null;
+ fee?: string;
+ decimals?: number;
}) => {
- if (!feeFactor || !amount) return null;
+ if (!feeFactor || !amount || !transferAmount || !fee) return null;
- // using toFixed without an argument will always return a
- // number in normal notation without rounding, formatting functions
- // arent working in a way which won't round the decimal places
- const value = new BigNumber(amount).times(feeFactor).toFixed();
+ const totalValue = new BigNumber(transferAmount).plus(fee).toString();
return (
-
-
+
+
{t('Transfer fee')}
+
+
+ {formatNumber(fee, decimals)}
+
-
- {value}
+
+
+ {t('Amount to be transferred')}
+
+
+
+ {formatNumber(amount, decimals)}
+
+
+
+
+ {t('Total amount (with fee)')}
+
+
+
+ {formatNumber(totalValue, decimals)}
+
);
diff --git a/libs/accounts/src/setup-tests.ts b/libs/accounts/src/setup-tests.ts
index 7b0828bfa..880268538 100644
--- a/libs/accounts/src/setup-tests.ts
+++ b/libs/accounts/src/setup-tests.ts
@@ -1 +1,6 @@
import '@testing-library/jest-dom';
+import ResizeObserver from 'resize-observer-polyfill';
+import { defaultFallbackInView } from 'react-intersection-observer';
+
+defaultFallbackInView(true);
+global.ResizeObserver = ResizeObserver;