diff --git a/libs/accounts/src/lib/transfer-container.tsx b/libs/accounts/src/lib/transfer-container.tsx
index c831fe624..fc9af6bec 100644
--- a/libs/accounts/src/lib/transfer-container.tsx
+++ b/libs/accounts/src/lib/transfer-container.tsx
@@ -9,12 +9,17 @@ import { useDataProvider } from '@vegaprotocol/data-provider';
import type { Transfer } from '@vegaprotocol/wallet';
import { useVegaTransactionStore } from '@vegaprotocol/web3';
import { useVegaWallet } from '@vegaprotocol/wallet';
-import { useCallback, useMemo } from 'react';
+import { useCallback } from 'react';
import { accountsDataProvider } from './accounts-data-provider';
import { TransferForm } from './transfer-form';
import sortBy from 'lodash/sortBy';
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 }) => {
const { pubKey, pubKeys } = useVegaWallet();
const { param } = useNetworkParam(NetworkParams.transfer_fee_factor);
@@ -33,20 +38,19 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
[create]
);
- const assets = useMemo(() => {
- if (!data) return [];
- return data
- .filter(
- (account) => account.type === Schema.AccountType.ACCOUNT_TYPE_GENERAL
- )
- .map((account) => ({
- id: account.asset.id,
- symbol: account.asset.symbol,
- name: account.asset.name,
- decimals: account.asset.decimals,
- balance: addDecimal(account.balance, account.asset.decimals),
- }));
- }, [data]);
+ const accounts = data
+ ? data.filter((account) => ALLOWED_ACCOUNTS.includes(account.type))
+ : [];
+
+ const assets = accounts.map((account) => ({
+ id: account.asset.id,
+ symbol: account.asset.symbol,
+ name: account.asset.name,
+ decimals: account.asset.decimals,
+ balance: addDecimal(account.balance, account.asset.decimals),
+ }));
+
+ if (data === null) return null;
return (
<>
@@ -69,6 +73,7 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
assetId={assetId}
feeFactor={param}
submitTransfer={transfer}
+ accounts={accounts}
/>
>
);
diff --git a/libs/accounts/src/lib/transfer-form.spec.tsx b/libs/accounts/src/lib/transfer-form.spec.tsx
index 2e9bfafcf..886bfe556 100644
--- a/libs/accounts/src/lib/transfer-form.spec.tsx
+++ b/libs/accounts/src/lib/transfer-form.spec.tsx
@@ -1,18 +1,38 @@
-import {
- act,
- fireEvent,
- render,
- screen,
- waitFor,
-} from '@testing-library/react';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import BigNumber from 'bignumber.js';
import { AddressField, TransferFee, TransferForm } from './transfer-form';
import { AccountType } from '@vegaprotocol/types';
import { addDecimal, formatNumber, removeDecimal } from '@vegaprotocol/utils';
-import userEvent from '@testing-library/user-event';
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 pubKey =
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
@@ -32,6 +52,18 @@ describe('TransferForm', () => {
assets: [asset],
feeFactor: '0.001',
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 () => {
@@ -42,49 +74,45 @@ describe('TransferForm', () => {
// 1003-TRAN-019
render();
// Select a pubkey
- fireEvent.change(screen.getByLabelText('Vega key'), {
- target: { value: props.pubKeys[1] },
- });
+ await userEvent.selectOptions(
+ screen.getByLabelText('Vega key'),
+ 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 } }
- );
+ await selectAsset(asset);
+
// set valid amount
- fireEvent.change(screen.getByLabelText('Amount'), {
- target: { value: amount },
- });
+ const amountInput = screen.getByLabelText('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 tooltips = screen.getAllByTestId('tooltip-content');
- expect(tooltips[0]).toBeVisible();
- });
+ const transferFee = screen.getByText('Transfer fee');
+ await userEvent.hover(transferFee);
+ 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 tooltips = screen.getAllByTestId('tooltip-content');
- expect(tooltips[0]).toBeVisible();
- });
-
- 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();
- });
+ const totalAmountWithFee = screen.getByText('Total amount (with fee)');
+ await userEvent.hover(totalAmountWithFee);
+ expect(await screen.findByRole('tooltip')).toHaveTextContent(
+ /total amount taken from your account/
+ );
});
it('validates a manually entered address', async () => {
@@ -92,30 +120,17 @@ describe('TransferForm', () => {
// 1003-TRAN-013
// 1003-TRAN-004
render();
- submit();
- expect(await screen.findAllByText('Required')).toHaveLength(3);
+ await submit();
+ expect(await screen.findAllByText('Required')).toHaveLength(4);
const toggle = screen.getByText('Enter manually');
- fireEvent.click(toggle);
+ await userEvent.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');
- });
+ await userEvent.type(screen.getByLabelText('Vega key'), 'invalid-address');
+ expect(screen.getAllByTestId('input-error-text')[0]).toHaveTextContent(
+ 'Invalid Vega key'
+ );
});
it('validates fields and submits', async () => {
@@ -127,28 +142,25 @@ describe('TransferForm', () => {
render();
// check current pubkey not shown
- const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
- expect(keySelect.children).toHaveLength(2);
+ const keySelect = screen.getByLabelText('Vega key');
+ expect(keySelect.children).toHaveLength(3);
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
'',
+ pubKey,
props.pubKeys[1],
]);
- submit();
- expect(await screen.findAllByText('Required')).toHaveLength(3);
+ await submit();
+ expect(await screen.findAllByText('Required')).toHaveLength(4);
// Select a pubkey
- fireEvent.change(screen.getByLabelText('Vega key'), {
- target: { value: props.pubKeys[1] },
- });
+ await userEvent.selectOptions(
+ screen.getByLabelText('Vega key'),
+ 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 } }
- );
+ await selectAsset(asset);
// assert rich select as updated
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
@@ -158,37 +170,38 @@ describe('TransferForm', () => {
formatNumber(asset.balance, asset.decimals)
);
+ await userEvent.selectOptions(
+ screen.getByLabelText('From account'),
+ AccountType.ACCOUNT_TYPE_VESTED_REWARDS
+ );
+
const amountInput = screen.getByLabelText('Amount');
// Test amount validation
- fireEvent.change(amountInput, {
- target: { value: '0.00000001' },
- });
+ await userEvent.type(amountInput, '0.00000001');
expect(
await screen.findByText('Value is below minimum')
).toBeInTheDocument();
- fireEvent.change(amountInput, {
- target: { value: '9999999' },
- });
+ await userEvent.clear(amountInput);
+ await userEvent.type(amountInput, '9999999');
expect(
await screen.findByText(/cannot transfer more/i)
).toBeInTheDocument();
// set valid amount
- fireEvent.change(amountInput, {
- target: { value: amount },
- });
+ await userEvent.clear(amountInput);
+ await userEvent.type(amountInput, amount);
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(
new BigNumber(props.feeFactor).times(amount).toFixed()
);
- submit();
+ await submit();
await waitFor(() => {
expect(props.submitTransfer).toHaveBeenCalledTimes(1);
expect(props.submitTransfer).toHaveBeenCalledWith({
- fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
+ fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
to: props.pubKeys[1],
asset: asset.id,
@@ -200,59 +213,50 @@ describe('TransferForm', () => {
describe('IncludeFeesCheckbox', () => {
it('validates fields and submits when checkbox is checked', async () => {
- render();
+ const mockSubmit = jest.fn();
+ 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],
- ]);
+ const keySelect = screen.getByLabelText('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
+ );
- submit();
- expect(await screen.findAllByText('Required')).toHaveLength(3);
+ await submit();
+ expect(await screen.findAllByText('Required')).toHaveLength(4);
// Select a pubkey
- fireEvent.change(screen.getByLabelText('Vega key'), {
- target: { value: props.pubKeys[1] },
- });
+ await userEvent.selectOptions(
+ screen.getByLabelText('Vega key'),
+ 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 } }
- );
+ await selectAsset(asset);
- // 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)
+ await userEvent.selectOptions(
+ screen.getByLabelText('From account'),
+ AccountType.ACCOUNT_TYPE_VESTED_REWARDS
);
const amountInput = screen.getByLabelText('Amount');
const checkbox = screen.getByTestId('include-transfer-fee');
+
// 1003-TRAN-022
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);
- });
+
+ await userEvent.clear(amountInput);
+ await userEvent.type(amountInput, amount);
+ await userEvent.click(checkbox);
expect(checkbox).toBeChecked();
const expectedFee = new BigNumber(amount)
.times(props.feeFactor)
.toFixed();
const expectedAmount = new BigNumber(amount).minus(expectedFee).toFixed();
+
// 1003-TRAN-020
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
@@ -262,18 +266,17 @@ describe('TransferForm', () => {
amount
);
- submit();
+ await submit();
await waitFor(() => {
// 1003-TRAN-023
-
- expect(props.submitTransfer).toHaveBeenCalledTimes(1);
- expect(props.submitTransfer).toHaveBeenCalledWith({
- fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
+ expect(mockSubmit).toHaveBeenCalledTimes(1);
+ expect(mockSubmit).toHaveBeenCalledWith({
+ fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
to: props.pubKeys[1],
asset: asset.id,
- amount: removeDecimal(amount, asset.decimals),
+ amount: removeDecimal(expectedAmount, asset.decimals),
oneOff: {},
});
});
@@ -284,46 +287,29 @@ describe('TransferForm', () => {
// 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],
- ]);
+ const pubKeyOptions = ['', pubKey, props.pubKeys[1]];
+ expect(keySelect.children).toHaveLength(pubKeyOptions.length);
+ expect(Array.from(keySelect.options).map((o) => o.value)).toEqual(
+ pubKeyOptions
+ );
- submit();
- expect(await screen.findAllByText('Required')).toHaveLength(3);
+ await submit();
+ expect(await screen.findAllByText('Required')).toHaveLength(4);
// Select a pubkey
- fireEvent.change(screen.getByLabelText('Vega key'), {
- target: { value: props.pubKeys[1] },
- });
+ await userEvent.selectOptions(
+ screen.getByLabelText('Vega key'),
+ 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(await screen.findByTestId('asset-balance')).toHaveTextContent(
- formatNumber(asset.balance, asset.decimals)
- );
+ await selectAsset(asset);
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 },
- });
- });
+
+ await userEvent.type(amountInput, amount);
expect(checkbox).not.toBeChecked();
const expectedFee = new BigNumber(amount)
.times(props.feeFactor)
@@ -351,11 +337,11 @@ describe('TransferForm', () => {
// 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'));
+ await userEvent.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'));
+ await userEvent.click(screen.getByText('Select from wallet'));
expect(screen.getByText('select')).toBeInTheDocument();
expect(screen.queryByText('input')).not.toBeInTheDocument();
expect(mockOnChange).toHaveBeenCalledTimes(2);
diff --git a/libs/accounts/src/lib/transfer-form.tsx b/libs/accounts/src/lib/transfer-form.tsx
index d74208ac2..8a6bd542d 100644
--- a/libs/accounts/src/lib/transfer-form.tsx
+++ b/libs/accounts/src/lib/transfer-form.tsx
@@ -24,22 +24,30 @@ import type { ReactNode } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { AssetOption, Balance } from '@vegaprotocol/assets';
+import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
interface FormFields {
toAddress: string;
asset: string;
amount: string;
+ fromAccount: AccountType;
+}
+
+interface Asset {
+ id: string;
+ symbol: string;
+ name: string;
+ decimals: number;
+ balance: string;
}
interface TransferFormProps {
pubKey: string | null;
pubKeys: string[] | null;
- assets: Array<{
- id: string;
- symbol: string;
- name: string;
- decimals: number;
- balance: string;
+ assets: Array;
+ accounts: Array<{
+ type: AccountType;
+ asset: { id: string; symbol: string };
}>;
assetId?: string;
feeFactor: string | null;
@@ -53,6 +61,7 @@ export const TransferForm = ({
assetId: initialAssetId,
feeFactor,
submitTransfer,
+ accounts,
}: TransferFormProps) => {
const {
control,
@@ -67,6 +76,7 @@ export const TransferForm = ({
},
});
+ const selectedPubKey = watch('toAddress');
const amount = watch('amount');
const assetId = watch('asset');
@@ -102,10 +112,16 @@ export const TransferForm = ({
if (!transferAmount) {
throw new Error('Submitted transfer with no amount selected');
}
- const transfer = normalizeTransfer(fields.toAddress, transferAmount, {
- id: asset.id,
- decimals: asset.decimals,
- });
+ const transfer = normalizeTransfer(
+ fields.toAddress,
+ transferAmount,
+ fields.fromAccount,
+ AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form
+ {
+ id: asset.id,
+ decimals: asset.decimals,
+ }
+ );
submitTransfer(transfer);
},
[asset, submitTransfer, transferAmount]
@@ -137,57 +153,53 @@ export const TransferForm = ({
className="text-sm"
data-testid="transfer-form"
>
-
+
setValue('toAddress', '')}
select={
{pubKeys?.length &&
- pubKeys
- .filter((pk) => pk !== pubKey) // remove currently selected pubkey
- .map((pk) => (
+ pubKeys.map((pk) => {
+ const text = pk === pubKey ? t('Current key: ') + pk : pk;
+
+ return (
- ))}
+ );
+ })}
}
input={
{
- if (value === pubKey) {
- return t('Vega key is the same as current key');
- }
- return true;
- },
},
})}
/>
}
/>
{errors.toAddress?.message && (
-
+
{errors.toAddress.message}
)}
-
+
)}
+
+ {
+ if (
+ pubKey === selectedPubKey &&
+ value === AccountType.ACCOUNT_TYPE_GENERAL
+ ) {
+ return t(
+ 'Cannot transfer to the same account type for the connected key'
+ );
+ }
+ return true;
+ },
+ },
+ })}
+ >
+
+ {accounts
+ .filter((a) => {
+ if (!assetId) return true;
+ return assetId === a.asset.id;
+ })
+ .map((a) => {
+ return (
+
+ );
+ })}
+
+ {errors.fromAccount?.message && (
+
+ {errors.fromAccount.message}
+
+ )}
+
+
+
+
+
+
!curr);
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')}
diff --git a/libs/wallet/src/utils.ts b/libs/wallet/src/utils.ts
index 1b0ce0a19..8c0c20c64 100644
--- a/libs/wallet/src/utils.ts
+++ b/libs/wallet/src/utils.ts
@@ -1,6 +1,6 @@
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
import type { Market, Order } from '@vegaprotocol/types';
-import { AccountType } from '@vegaprotocol/types';
+import type { AccountType } from '@vegaprotocol/types';
import BigNumber from 'bignumber.js';
import { ethers } from 'ethers';
import { sha3_256 } from 'js-sha3';
@@ -47,15 +47,17 @@ export const normalizeOrderAmendment = >(
export const normalizeTransfer = >(
address: string,
amount: string,
+ fromAccountType: AccountType,
+ toAccountType: AccountType,
asset: {
id: string;
decimals: number;
}
): Transfer => {
return {
- fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
to: address,
- toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
+ fromAccountType,
+ toAccountType,
asset: asset.id,
amount: removeDecimal(amount, asset.decimals),
// oneOff or recurring required otherwise wallet will error