Compare commits

..
28 changed files with 432 additions and 312 deletions
@@ -36,13 +36,13 @@ describe('ProposalReferralProgramDetails helper functions', () => {
it('should format referral discount factor correctly', () => {
const input = '0.05';
const formatted = formatReferralDiscountFactor(input);
expect(formatted).toBe('5.00%');
expect(formatted).toBe('5%');
});
it('should format referral reward factor correctly', () => {
const input = '0.1';
const formatted = formatReferralRewardFactor(input);
expect(formatted).toBe('10.00%');
expect(formatted).toBe('10%');
});
it('should format minimum staked tokens correctly', () => {
+1 -1
View File
@@ -5,7 +5,7 @@
"next",
"next/core-web-vitals"
],
"ignorePatterns": ["!**/*", "__generated__"],
"ignorePatterns": ["!**/*", "__generated__", ".next"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
@@ -71,7 +71,7 @@ export const useReferral = (args: UseReferralArgs) => {
variables: {
code: referralSet?.id as string,
aggregationEpochs:
args.aggregationEpochs != null
args.aggregationEpochs !== null
? args.aggregationEpochs
: DEFAULT_AGGREGATION_DAYS,
},
@@ -3,6 +3,8 @@ import {
VegaIcon,
VegaIconNames,
truncateMiddle,
ExternalLink,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
@@ -28,6 +30,7 @@ import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
import BigNumber from 'bignumber.js';
import { t } from '@vegaprotocol/i18n';
import maxBy from 'lodash/maxBy';
import { DocsLinks } from '@vegaprotocol/environment';
export const ReferralStatistics = () => {
const { pubKey } = useVegaWallet();
@@ -201,7 +204,7 @@ export const Statistics = ({
'Total commission (last %s epochs)',
(details?.windowLength || DEFAULT_AGGREGATION_DAYS).toString()
)}
description={t('(qUSD)')}
description={<QUSDTooltip />}
>
{getNumberFormat(0).format(Number(totalCommissionValue))}
</StatTile>
@@ -233,7 +236,12 @@ export const Statistics = ({
<StatTile title={t('Discount')}>{discountFactorValue * 100}%</StatTile>
);
const runningVolumeTile = (
<StatTile title={t('Combined volume')}>
<StatTile
title={t(
'Combined volume (last %s epochs)',
details?.windowLength.toString()
)}
>
{compactNumFormat.format(runningVolumeValue)}
</StatTile>
);
@@ -241,24 +249,14 @@ export const Statistics = ({
<StatTile title={t('Epochs in set')}>{epochsValue}</StatTile>
);
const nextTierVolumeTile = (
<StatTile
title={t(
'Volume to next tier %s',
nextBenefitTierValue?.tier ? `(${nextBenefitTierValue.tier})` : ''
)}
>
<StatTile title={t('Volume to next tier')}>
{nextBenefitTierVolumeValue <= 0
? '0'
: compactNumFormat.format(nextBenefitTierVolumeValue)}
</StatTile>
);
const nextTierEpochsTile = (
<StatTile
title={t(
'Epochs to next tier %s',
nextBenefitTierValue?.tier ? `(${nextBenefitTierValue.tier})` : ''
)}
>
<StatTile title={t('Epochs to next tier')}>
{nextBenefitTierEpochsValue <= 0 ? '0' : nextBenefitTierEpochsValue}
</StatTile>
);
@@ -339,11 +337,16 @@ export const Statistics = ({
},
{
name: 'commission',
displayName: t(
'Commission earned (last %s epochs)',
(
details?.windowLength || DEFAULT_AGGREGATION_DAYS
).toString()
displayName: (
<>
{t('Commission earned in')} <QUSDTooltip />{' '}
{t(
'(last %s epochs)',
(
details?.windowLength || DEFAULT_AGGREGATION_DAYS
).toString()
)}
</>
),
},
]}
@@ -373,3 +376,25 @@ export const Statistics = ({
</>
);
};
export const QUSDTooltip = () => (
<Tooltip
description={
<>
<p className="mb-1">
{t(
'qUSD provides a rough USD equivalent of balances across all assets using the value of "Quantum" for that asset'
)}
</p>
{DocsLinks && (
<ExternalLink href={DocsLinks.QUANTUM}>
{t('Find out more')}
</ExternalLink>
)}
</>
}
underline={true}
>
<span>{t('qUSD')}</span>
</Tooltip>
);
+11 -14
View File
@@ -1,10 +1,10 @@
import { Tooltip, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import { forwardRef, type HTMLAttributes } from 'react';
import { forwardRef, type ReactNode, type HTMLAttributes } from 'react';
import { BORDER_COLOR, GRADIENT } from './constants';
type TableColumnDefinition = {
displayName?: string;
displayName?: ReactNode;
name: string;
tooltip?: string;
className?: string;
@@ -46,7 +46,7 @@ export const Table = forwardRef<
INNER_BORDER_STYLE
)}
>
<span className="flex flex-row gap-2 items-center">
<span className="flex flex-row items-center gap-2">
<span>{displayName}</span>
{tooltip ? (
<Tooltip description={tooltip}>
@@ -102,17 +102,14 @@ export const Table = forwardRef<
key={`${i}-${name}`}
>
{/** display column name in mobile view */}
{!noCollapse &&
!noHeader &&
displayName &&
displayName.length > 0 && (
<span
aria-hidden
className="md:hidden font-mono text-xs px-0 text-vega-clight-100 dark:text-vega-cdark-100"
>
{displayName}
</span>
)}
{!noCollapse && !noHeader && displayName && (
<span
aria-hidden
className="px-0 font-mono text-xs md:hidden text-vega-clight-100 dark:text-vega-cdark-100"
>
{displayName}
</span>
)}
<span>{d[name]}</span>
</td>
))}
+3 -3
View File
@@ -29,7 +29,7 @@ export const Tile = ({
type StatTileProps = {
title: string;
description?: string;
description?: ReactNode;
children?: ReactNode;
};
export const StatTile = ({ title, description, children }: StatTileProps) => {
@@ -67,11 +67,11 @@ export const CodeTile = ({
title={t('Your referral code')}
description={createdAt ? t('(Created at: %s)', createdAt) : undefined}
>
<div className="flex gap-2 items-center justify-between">
<div className="flex items-center justify-between gap-2">
<Tooltip
description={
<div className="break-all">
<span className="text-xl bg-rainbow bg-clip-text text-transparent">
<span className="text-xl text-transparent bg-rainbow bg-clip-text">
{code}
</span>
</div>
@@ -112,6 +112,8 @@ const USE_ACCOUNT_TYPES = [
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
AccountType.ACCOUNT_TYPE_FEES_MAKER,
AccountType.ACCOUNT_TYPE_PENDING_TRANSFERS,
AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
AccountType.ACCOUNT_TYPE_VESTING_REWARDS,
];
const getAssetIds = (data: Account[]) =>
+22 -15
View File
@@ -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,21 @@ 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
// Theres only one general account for each asset, this will give us a list
// of assets the user has accounts for
.filter((a) => a.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),
}));
return (
<>
@@ -69,6 +75,7 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
assetId={assetId}
feeFactor={param}
submitTransfer={transfer}
accounts={accounts}
/>
</>
);
+147 -161
View File
@@ -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(<TransferForm {...props} />);
// 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(<TransferForm {...props} />);
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(<TransferForm {...props} />);
// check current pubkey not shown
const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
expect(keySelect.children).toHaveLength(2);
const keySelect = screen.getByLabelText<HTMLSelectElement>('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(<TransferForm {...props} />);
const mockSubmit = jest.fn();
render(<TransferForm {...props} submitTransfer={mockSubmit} />);
// 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<HTMLSelectElement>('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);
+110 -30
View File
@@ -5,6 +5,7 @@ import {
vegaPublicKey,
addDecimal,
formatNumber,
addDecimalsFormatNumber,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
@@ -24,22 +25,31 @@ 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;
assets: Array<Asset>;
accounts: Array<{
type: AccountType;
balance: string;
asset: { id: string; symbol: string; decimals: number };
}>;
assetId?: string;
feeFactor: string | null;
@@ -53,6 +63,7 @@ export const TransferForm = ({
assetId: initialAssetId,
feeFactor,
submitTransfer,
accounts,
}: TransferFormProps) => {
const {
control,
@@ -67,8 +78,10 @@ export const TransferForm = ({
},
});
const selectedPubKey = watch('toAddress');
const amount = watch('amount');
const assetId = watch('asset');
const asset = assets.find((a) => a.id === assetId);
const [includeFee, setIncludeFee] = useState(false);
@@ -90,10 +103,6 @@ export const TransferForm = ({
);
}, [amount, includeFee, transferAmount, feeFactor]);
const asset = useMemo(() => {
return assets.find((a) => a.id === assetId);
}, [assets, assetId]);
const onSubmit = useCallback(
(fields: FormFields) => {
if (!asset) {
@@ -102,10 +111,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]
@@ -131,63 +146,66 @@ export const TransferForm = ({
}
}, [setValue, pubKey]);
// General account for the selected asset
const generalAccount = accounts.find((a) => {
return (
a.asset.id === assetId && a.type === AccountType.ACCOUNT_TYPE_GENERAL
);
});
return (
<form
onSubmit={handleSubmit(onSubmit)}
className="text-sm"
data-testid="transfer-form"
>
<TradingFormGroup label="Vega key" labelFor="to-address">
<TradingFormGroup label="Vega key" labelFor="toAddress">
<AddressField
pubKeys={pubKeys}
onChange={() => setValue('toAddress', '')}
select={
<TradingSelect
{...register('toAddress')}
id="to-address"
id="toAddress"
defaultValue=""
>
<option value="" disabled={true}>
{t('Please select')}
</option>
{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 (
<option key={pk} value={pk}>
{pk}
{text}
</option>
))}
);
})}
</TradingSelect>
}
input={
<TradingInput
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={true} // focus input immediately after is shown
id="to-address"
id="toAddress"
type="text"
{...register('toAddress', {
validate: {
required,
vegaPublicKey,
sameKey: (value) => {
if (value === pubKey) {
return t('Vega key is the same as current key');
}
return true;
},
},
})}
/>
}
/>
{errors.toAddress?.message && (
<TradingInputError forInput="to-address">
<TradingInputError forInput="toAddress">
{errors.toAddress.message}
</TradingInputError>
)}
</TradingFormGroup>
<TradingFormGroup label="Asset" labelFor="asset">
<TradingFormGroup label={t('Asset')} labelFor="asset">
<Controller
control={control}
name="asset"
@@ -228,6 +246,68 @@ export const TransferForm = ({
</TradingInputError>
)}
</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]} (
{addDecimalsFormatNumber(a.balance, a.asset.decimals)}{' '}
{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}>
{generalAccount
? `${
AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]
} (${addDecimalsFormatNumber(
generalAccount.balance,
generalAccount.asset.decimals
)} ${generalAccount.asset.symbol})`
: AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]}
</option>
</TradingSelect>
</TradingFormGroup>
<TradingFormGroup label="Amount" labelFor="amount">
<TradingInput
id="amount"
@@ -391,7 +471,7 @@ export const AddressField = ({
setIsInput((curr) => !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')}
</button>
+5 -1
View File
@@ -20,7 +20,11 @@ query Candles($marketId: ID!, $interval: Interval!, $since: String!) {
code
}
}
candlesConnection(interval: $interval, since: $since) {
candlesConnection(
interval: $interval
since: $since
pagination: { last: 5000 }
) {
edges {
node {
...CandleFields
+1 -1
View File
@@ -46,7 +46,7 @@ export const CandlesDocument = gql`
code
}
}
candlesConnection(interval: $interval, since: $since) {
candlesConnection(interval: $interval, since: $since, pagination: {last: 5000}) {
edges {
node {
...CandleFields
+1 -1
View File
@@ -32,7 +32,7 @@ export const Pagination = ({
{false}
{showRetentionMessage &&
t(
'Depending on data node retention you may not be able see the "full" history'
'Depending on data node retention you may not be able see the full history'
)}
</div>
<div className="flex items-center text-xs">
@@ -79,7 +79,7 @@ export const DealTicketFeeDetails = ({
}
formattedValue={
<>
{totalDiscountFactor && (
{totalDiscountFactor ? (
<Pill size="xxs" intent={Intent.Warning} className="mr-1">
-
{formatNumberPercentage(
@@ -87,14 +87,14 @@ export const DealTicketFeeDetails = ({
2
)}
</Pill>
)}
) : null}
{totalDiscountedFeeAmount &&
`~${formatValue(totalDiscountedFeeAmount, assetDecimals, quantum)}`}
</>
}
labelDescription={
<>
<p className="mb-2">
<div className="flex flex-col gap-2">
<p>
{t(
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}. Fees estimated are "taker" fees and will only be payable if the order trades aggressively. Rebate equal to the maker portion will be paid to the trader if the order trades passively.`
)}
@@ -108,7 +108,7 @@ export const DealTicketFeeDetails = ({
symbol={assetSymbol}
decimals={assetDecimals}
/>
</>
</div>
}
symbol={assetSymbol}
/>
@@ -0,0 +1,36 @@
import { render, screen } from '@testing-library/react';
import { FeesBreakdown } from './fees-breakdown';
describe('FeesBreakdown', () => {
it('formats fee factors correctly', () => {
const feeFactors = {
makerFee: '0.00005',
infrastructureFee: '0.001',
liquidityFee: '0.5',
};
const fees = {
makerFee: '100',
infrastructureFee: '100',
liquidityFee: '100',
};
const props = {
totalFeeAmount: '100',
fees,
feeFactors,
symbol: 'USD',
decimals: 2,
referralDiscountFactor: '0.01',
volumeDiscountFactor: '0.01',
};
render(<FeesBreakdown {...props} />);
expect(screen.getByText('Maker fee').nextElementSibling).toHaveTextContent(
'0.005%'
);
expect(
screen.getByText('Infrastructure fee').nextElementSibling
).toHaveTextContent('0.1%');
expect(
screen.getByText('Liquidity fee').nextElementSibling
).toHaveTextContent('50%');
});
});
@@ -33,7 +33,7 @@ const FeesBreakdownItem = ({
<dt className="col-span-2">{label}</dt>
{factor && (
<dd className="text-right col-span-1">
{formatNumberPercentage(new BigNumber(factor).times(100), 2)}
{formatNumberPercentage(new BigNumber(factor).times(100))}
</dd>
)}
<dd className="text-right col-span-3">
@@ -6,7 +6,7 @@ import type { EstimateFeesQuery } from './__generated__/EstimateOrder';
const data: EstimateFeesQuery = {
epoch: {
id: '2',
id: '3',
},
volumeDiscountStats: {
edges: [
@@ -117,7 +117,7 @@ describe('useEstimateFees', () => {
});
});
it('returns 0 discounts if discount stats are not at the current epoch', () => {
it('returns 0 discounts if discount stats are not at the current epoch -1', () => {
const { result } = renderHook(() =>
useEstimateFees(
{
@@ -136,7 +136,7 @@ describe('useEstimateFees', () => {
});
it('returns discounts', () => {
data.epoch.id = '1';
data.epoch.id = '2';
const { result } = renderHook(() =>
useEstimateFees(
{
@@ -34,14 +34,13 @@ export const useEstimateFees = (
skip: !pubKey || !order?.size || !order?.price || order.postOnly,
});
const data = loading ? currentData || previousData : currentData;
const atEpoch = (Number(data?.epoch.id) || 0) - 1;
const volumeDiscountFactor =
(data?.volumeDiscountStats.edges[0]?.node.atEpoch.toString() ===
data?.epoch.id &&
(data?.volumeDiscountStats.edges[0]?.node.atEpoch === atEpoch &&
data?.volumeDiscountStats.edges[0]?.node.discountFactor) ||
'0';
const referralDiscountFactor =
(data?.referralSetStats.edges[0]?.node.atEpoch.toString() ===
data?.epoch.id &&
(data?.referralSetStats.edges[0]?.node.atEpoch === atEpoch &&
data?.referralSetStats.edges[0]?.node.discountFactor) ||
'0';
if (order?.postOnly) {
+1
View File
@@ -84,6 +84,7 @@ export const DocsLinks = VEGA_DOCS_URL
ETH_DATA_SOURCES: `${VEGA_DOCS_URL}/concepts/trading-on-vega/data-sources#ethereum-data-sources`,
ICEBERG_ORDERS: `${VEGA_DOCS_URL}/concepts/trading-on-vega/orders#iceberg-order`,
POST_REDUCE_ONLY: `${VEGA_DOCS_URL}/concepts/trading-on-vega/orders#conditional-order-parameters`,
QUANTUM: `${VEGA_DOCS_URL}/concepts/assets/asset-framework#quantum`,
}
: undefined;
+1 -1
View File
@@ -37,7 +37,7 @@ export const MarketCandlesDocument = gql`
marketsConnection(id: $marketId) {
edges {
node {
candlesConnection(interval: $interval, since: $since) {
candlesConnection(interval: $interval, since: $since, pagination: {last: 1000}) {
edges {
node {
...MarketCandlesFields
+1 -1
View File
@@ -19,7 +19,7 @@ export const MarketsCandlesDocument = gql`
edges {
node {
id
candlesConnection(interval: $interval, since: $since) {
candlesConnection(interval: $interval, since: $since, pagination: {last: 1000}) {
edges {
node {
...MarketCandlesFields
+5 -1
View File
@@ -11,7 +11,11 @@ query MarketCandles($interval: Interval!, $since: String!, $marketId: ID!) {
marketsConnection(id: $marketId) {
edges {
node {
candlesConnection(interval: $interval, since: $since) {
candlesConnection(
interval: $interval
since: $since
pagination: { last: 1000 }
) {
edges {
node {
...MarketCandlesFields
+4 -4
View File
@@ -72,10 +72,10 @@ describe('totalFeesFactorsPercentage', () => {
makerFee: f[2].toString(),
});
it.each([
{ i: createFee(0, 0, 1), o: '100.00%' },
{ i: createFee(0, 1, 0), o: '100.00%' },
{ i: createFee(1, 0, 0), o: '100.00%' },
{ i: createFee(0.01, 0.02, 0.003), o: '3.30%' },
{ i: createFee(0, 0, 1), o: '100%' },
{ i: createFee(0, 1, 0), o: '100%' },
{ i: createFee(1, 0, 0), o: '100%' },
{ i: createFee(0.01, 0.02, 0.003), o: '3.3%' },
{ i: createFee(0.01, 0.056782, 0.003), o: '6.9782%' },
{ i: createFee(0.01, 0.056782, 0), o: '6.6782%' },
])('adds fees correctly', ({ i, o }) => {
+5 -1
View File
@@ -12,7 +12,11 @@ query MarketsCandles($interval: Interval!, $since: String!) {
edges {
node {
id
candlesConnection(interval: $interval, since: $since) {
candlesConnection(
interval: $interval
since: $since
pagination: { last: 1000 }
) {
edges {
node {
...MarketCandlesFields
+9 -36
View File
@@ -3,10 +3,7 @@ import { tradesWithMarketProvider } from './trades-data-provider';
import { TradesTable } from './trades-table';
import { useDealTicketFormValues } from '@vegaprotocol/deal-ticket';
import { t } from '@vegaprotocol/i18n';
import { Pagination } from '@vegaprotocol/datagrid';
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
import { useCallback, useState } from 'react';
import type { AgGridReact } from 'ag-grid-react';
interface TradesContainerProps {
marketId: string;
@@ -19,43 +16,19 @@ export const TradesManager = ({
}: TradesContainerProps) => {
const update = useDealTicketFormValues((state) => state.updateAll);
const { data, error, load, pageInfo } = useDataProvider({
const { data, error } = useDataProvider({
dataProvider: tradesWithMarketProvider,
variables: { marketId },
});
const [hasDisplayedRow, setHasDisplayedRow] = useState<boolean | undefined>(
undefined
);
const { onFilterChanged, ...props } = gridProps || {};
const onRowDataUpdated = useCallback(
({ api }: { api: AgGridReact['api'] }) => {
setHasDisplayedRow(!!api.getDisplayedRowCount());
},
[]
);
return (
<div className="flex flex-col h-full">
<TradesTable
rowData={data}
onClick={(price?: string) => {
update(marketId, { price });
}}
onFilterChanged={(event) => {
onRowDataUpdated(event);
onFilterChanged(event);
}}
onRowDataUpdated={onRowDataUpdated}
overlayNoRowsTemplate={error ? error.message : t('No trades')}
{...props}
/>
<Pagination
count={data?.length || 0}
pageInfo={pageInfo}
onLoad={load}
hasDisplayedRows={hasDisplayedRow || false}
showRetentionMessage={true}
/>
</div>
<TradesTable
rowData={data}
onClick={(price?: string) => {
update(marketId, { price });
}}
overlayNoRowsTemplate={error ? error.message : t('No trades')}
{...gridProps}
/>
);
};
+2 -2
View File
@@ -77,8 +77,8 @@ describe('number utils', () => {
{ v: new BigNumber(123.123), d: 3, o: '123.123%' },
{ v: new BigNumber(123.123), d: 6, o: '123.123%' },
{ v: new BigNumber(123.123), d: 0, o: '123%' },
{ v: new BigNumber(123), d: undefined, o: '123.00%' }, // it default to 2 decimal places
{ v: new BigNumber(30000), d: undefined, o: '30,000.00%' },
{ v: new BigNumber(123), d: undefined, o: '123%' }, // it default to 2 decimal places
{ v: new BigNumber(30000), d: undefined, o: '30,000%' },
{ v: new BigNumber(3.000001), d: undefined, o: '3.000001%' },
])('formats given number correctly', ({ v, d, o }) => {
expect(formatNumberPercentage(v, d)).toStrictEqual(o);
+1 -1
View File
@@ -158,7 +158,7 @@ export const addDecimalsFixedFormatNumber = (
export const formatNumberPercentage = (value: BigNumber, decimals?: number) => {
const decimalPlaces =
typeof decimals === 'undefined' ? Math.max(value.dp() || 0, 2) : decimals;
typeof decimals === 'undefined' ? value.dp() || 0 : decimals;
return `${formatNumber(value, decimalPlaces)}%`;
};
+5 -3
View File
@@ -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 = <T extends Exact<OrderAmendment, T>>(
export const normalizeTransfer = <T extends Exact<Transfer, T>>(
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