Compare commits

..
Author SHA1 Message Date
Edd 8a3612a18c chore: add genesis validation script 2023-11-07 11:25:25 +00:00
23 changed files with 442 additions and 779 deletions
@@ -8,7 +8,6 @@ import { ENV } from '../../config';
import noIcon from '../../images/token-no-icon.png';
import vegaBlack from '../../images/vega_black.png';
import vegaVesting from '../../images/vega_vesting.png';
import { BigNumber } from '../../lib/bignumber';
import type { WalletCardAssetProps } from '../wallet-card';
import { useVegaWallet } from '@vegaprotocol/wallet';
@@ -103,10 +102,7 @@ export const usePollForDelegations = () => {
setAccounts(
accounts
.filter(
(a) =>
a.type === Schema.AccountType.ACCOUNT_TYPE_GENERAL ||
a.type === Schema.AccountType.ACCOUNT_TYPE_VESTED_REWARDS ||
a.type === Schema.AccountType.ACCOUNT_TYPE_VESTING_REWARDS
(a) => a.type === Schema.AccountType.ACCOUNT_TYPE_GENERAL
)
.map((a) => {
const isVega =
@@ -119,23 +115,14 @@ export const usePollForDelegations = () => {
subheading: isVega ? t('collateral') : a.asset.symbol,
symbol: a.asset.symbol,
decimals: a.asset.decimals,
assetId: a.asset.id,
balance: new BigNumber(
addDecimal(a.balance, a.asset.decimals)
),
image: isVega
? vegaBlack
: a.type ===
Schema.AccountType.ACCOUNT_TYPE_VESTED_REWARDS ||
a.type ===
Schema.AccountType.ACCOUNT_TYPE_VESTING_REWARDS
? vegaVesting
: noIcon,
image: isVega ? vegaBlack : noIcon,
border: isVega,
address: isAssetTypeERC20(a.asset)
? a.asset.source.contractAddress
: undefined,
type: a.type,
};
})
.sort((a, b) => {
@@ -1,18 +1,9 @@
import React, { useMemo } from 'react';
import React from 'react';
import { Link } from 'react-router-dom';
import { useAnimateValue } from '../../hooks/use-animate-value';
import type { BigNumber } from '../../lib/bignumber';
import { useNumberParts } from '@vegaprotocol/react-helpers';
import * as Schema from '@vegaprotocol/types';
import { useTranslation } from 'react-i18next';
import { AnchorButton, Tooltip } from '@vegaprotocol/ui-toolkit';
import {
CONSOLE_TRANSFER_ASSET,
DApp,
useLinks,
} from '@vegaprotocol/environment';
import { useNetworkParam } from '@vegaprotocol/network-parameters';
interface WalletCardProps {
children: React.ReactNode;
@@ -109,10 +100,8 @@ export interface WalletCardAssetProps {
symbol: string;
balance: BigNumber;
decimals: number;
assetId?: string;
border?: boolean;
subheading?: string;
type?: Schema.AccountType;
}
export const WalletCardAsset = ({
@@ -121,37 +110,16 @@ export const WalletCardAsset = ({
symbol,
balance,
decimals,
assetId,
border,
subheading,
type,
}: WalletCardAssetProps) => {
const [integers, decimalsPlaces, separator] = useNumberParts(
balance,
decimals
);
const { t } = useTranslation();
const consoleLink = useLinks(DApp.Console);
const transferAssetLink = (assetId: string) =>
consoleLink(CONSOLE_TRANSFER_ASSET.replace(':assetId', assetId));
const { param: baseRate } = useNetworkParam('rewards_vesting_baseRate');
const isRedeemable =
type === Schema.AccountType.ACCOUNT_TYPE_VESTED_REWARDS && assetId;
const accountTypeTooltip = useMemo(() => {
if (type === Schema.AccountType.ACCOUNT_TYPE_VESTED_REWARDS) {
return t('VestedRewardsTooltip');
}
if (type === Schema.AccountType.ACCOUNT_TYPE_VESTING_REWARDS && baseRate) {
return t('VestingRewardsTooltip', { baseRate });
}
return null;
}, [baseRate, t, type]);
return (
<div className="flex flex-nowrap gap-2 mt-2 mb-4">
<div className="flex flex-nowrap mt-2 mb-4">
<img
alt="Vega"
src={image}
@@ -161,37 +129,15 @@ export const WalletCardAsset = ({
/>
<div>
<div
className="flex align-center items-baseline text-base gap-2"
className="flex align-center text-base"
data-testid="currency-title"
>
<div className="mb-0 uppercase">{name}</div>
<div className="mb-0 px-2 uppercase">{name}</div>
<div className="mb-0 uppercase text-neutral-400">
{subheading || symbol}
</div>
</div>
{type ? (
<div className="mb-[2px] flex gap-2 items-baseline">
<Tooltip description={accountTypeTooltip}>
<span className="px-2 py-1 leading-none text-xs bg-vega-cdark-700 rounded">
{Schema.AccountTypeMapping[type]}
</span>
</Tooltip>
{isRedeemable ? (
<Tooltip description={t('RedeemRewardsTooltip')}>
<AnchorButton
variant="primary"
size="xs"
href={transferAssetLink(assetId)}
target="_blank"
className="px-2 py-1 leading-none text-xs bg-vega-yellow text-black rounded"
>
{t('Redeem')}
</AnchorButton>
</Tooltip>
) : null}
</div>
) : null}
<div className="basis-full font-mono" data-testid="currency-value">
<div className="px-2 basis-full font-mono" data-testid="currency-value">
<span>
{integers}
{separator}
@@ -953,8 +953,5 @@
"ACCOUNT_TYPE_REWARD_RELATIVE_RETURN": "Relative return reward account",
"ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY": "Return volatility reward account",
"ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING": "Validator ranking reward account",
"ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD": "Pending fee referral reward account",
"VestingRewardsTooltip": "Vesting rewards will be moved to vested account at a rate of {{baseRate}} per epoch.",
"VestedRewardsTooltip": "Vested rewards can be redeemed using Console",
"RedeemRewardsTooltip": "Click to redeem vested rewards in Console"
"ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD": "Pending fee referral reward account"
}
@@ -8,7 +8,7 @@ jest.mock('@vegaprotocol/accounts', () => ({
),
}));
jest.mock('../../components/welcome-dialog/get-started', () => ({
jest.mock('../../components/welcome-dialog/get-started.ts', () => ({
GetStarted: () => <div>GetStarted</div>,
}));
@@ -8,7 +8,7 @@ jest.mock('../../components/withdraw-container', () => ({
),
}));
jest.mock('../../components/welcome-dialog/get-started', () => ({
jest.mock('../../components/welcome-dialog/get-started.ts', () => ({
GetStarted: () => <div>GetStarted</div>,
}));
+2 -2
View File
@@ -1,11 +1,11 @@
#!/bin/bash -e
yarn --pure-lockfile
app=${1:-trading}
envCmd="envCmd="yarn -f ./apps/${app}/.env.${2:-mainnet}"
envCmd="envCmd="yarn env-cmd -f ./apps/${app}/.env.${2:-mainnet}"
yarn install
if [ "${app}" = "trading" ]; then
$envCmd yarn nx export trading
DIST_LOCATION=dist/apps/trading/exported/
DIST_LOCATION=dist/apps/trading/exported
else
$envCmd yarn nx build ${app}
DIST_LOCATION=dist/apps/${app}
+4 -11
View File
@@ -1,10 +1,9 @@
import sortBy from 'lodash/sortBy';
import * as Schema from '@vegaprotocol/types';
import { truncateByChars } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
NetworkParams,
useNetworkParams,
useNetworkParam,
} from '@vegaprotocol/network-parameters';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type { Transfer } from '@vegaprotocol/wallet';
@@ -22,11 +21,7 @@ export const ALLOWED_ACCOUNTS = [
export const TransferContainer = ({ assetId }: { assetId?: string }) => {
const { pubKey, pubKeys } = useVegaWallet();
const { params } = useNetworkParams([
NetworkParams.transfer_fee_factor,
NetworkParams.transfer_minTransferQuantumMultiple,
]);
const { param } = useNetworkParam(NetworkParams.transfer_fee_factor);
const { data } = useDataProvider({
dataProvider: accountsDataProvider,
variables: { partyId: pubKey || '' },
@@ -45,7 +40,6 @@ 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 (
<>
@@ -65,10 +59,9 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
pubKey={pubKey}
pubKeys={pubKeys ? pubKeys?.map((pk) => pk.publicKey) : null}
assetId={assetId}
feeFactor={params.transfer_fee_factor}
minQuantumMultiple={params.transfer_minTransferQuantumMultiple}
feeFactor={param}
submitTransfer={transfer}
accounts={sortedAccounts}
accounts={accounts}
/>
</>
);
+26 -126
View File
@@ -1,20 +1,11 @@
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,
type TransferFormProps,
} from './transfer-form';
import { AddressField, TransferFee, TransferForm } from './transfer-form';
import { AccountType } from '@vegaprotocol/types';
import { removeDecimal } from '@vegaprotocol/utils';
describe('TransferForm', () => {
const renderComponent = (props: TransferFormProps) => {
return render(<TransferForm {...props} />);
};
const submit = async () => {
await userEvent.click(
screen.getByRole('button', { name: 'Confirm transfer' })
@@ -46,7 +37,6 @@ describe('TransferForm', () => {
symbol: '€',
name: 'EUR',
decimals: 2,
quantum: '1',
};
const props = {
pubKey,
@@ -65,10 +55,9 @@ describe('TransferForm', () => {
{
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
asset,
balance: '10000',
balance: '100000',
},
],
minQuantumMultiple: '1',
};
it('form tooltips correctly displayed', async () => {
@@ -77,7 +66,7 @@ describe('TransferForm', () => {
// 1003-TRAN-017
// 1003-TRAN-018
// 1003-TRAN-019
renderComponent(props);
render(<TransferForm {...props} />);
// Select a pubkey
await userEvent.selectOptions(
screen.getByLabelText('To Vega key'),
@@ -124,9 +113,9 @@ describe('TransferForm', () => {
// 1003-TRAN-012
// 1003-TRAN-013
// 1003-TRAN-004
renderComponent(props);
render(<TransferForm {...props} />);
await submit();
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey is set as default value
expect(await screen.findAllByText('Required')).toHaveLength(3); // pubkey is set as default value
const toggle = screen.getByText('Enter manually');
await userEvent.click(toggle);
// has switched to input
@@ -139,18 +128,18 @@ describe('TransferForm', () => {
screen.getByLabelText('To Vega key'),
'invalid-address'
);
expect(screen.getAllByTestId('input-error-text')[1]).toHaveTextContent(
expect(screen.getAllByTestId('input-error-text')[0]).toHaveTextContent(
'Invalid Vega key'
);
});
it('sends transfer from general accounts', async () => {
it('validates fields and submits', async () => {
// 1003-TRAN-002
// 1003-TRAN-003
// 1002-WITH-010
// 1003-TRAN-011
// 1003-TRAN-014
renderComponent(props);
render(<TransferForm {...props} />);
// check current pubkey not shown
const keySelect = screen.getByLabelText<HTMLSelectElement>('To Vega key');
@@ -162,7 +151,7 @@ describe('TransferForm', () => {
]);
await submit();
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey is set as default value
expect(await screen.findAllByText('Required')).toHaveLength(3); // pubkey is set as default value
// Select a pubkey
await userEvent.selectOptions(
@@ -175,20 +164,15 @@ describe('TransferForm', () => {
await userEvent.selectOptions(
screen.getByLabelText('From account'),
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
AccountType.ACCOUNT_TYPE_VESTED_REWARDS
);
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.clear(amountInput);
await userEvent.type(amountInput, '0.001'); // Below quantum multiple amount
await userEvent.type(amountInput, '0.00000001');
expect(
await screen.findByText(/Amount below minimum requirement/)
await screen.findByText('Value is below minimum')
).toBeInTheDocument();
await userEvent.clear(amountInput);
@@ -209,86 +193,9 @@ describe('TransferForm', () => {
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: {},
});
});
});
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,
to: props.pubKeys[1],
asset: asset.id,
amount: removeDecimal(amount, asset.decimals),
oneOff: {},
@@ -299,7 +206,7 @@ describe('TransferForm', () => {
describe('IncludeFeesCheckbox', () => {
it('validates fields and submits when checkbox is checked', async () => {
const mockSubmit = jest.fn();
renderComponent({ ...props, submitTransfer: mockSubmit });
render(<TransferForm {...props} submitTransfer={mockSubmit} />);
// check current pubkey not shown
const keySelect = screen.getByLabelText<HTMLSelectElement>('To Vega key');
@@ -310,7 +217,7 @@ describe('TransferForm', () => {
);
await submit();
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
expect(await screen.findAllByText('Required')).toHaveLength(3); // pubkey set as default value
// Select a pubkey
await userEvent.selectOptions(
@@ -323,7 +230,7 @@ describe('TransferForm', () => {
await userEvent.selectOptions(
screen.getByLabelText('From account'),
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
AccountType.ACCOUNT_TYPE_VESTED_REWARDS
);
const amountInput = screen.getByLabelText('Amount');
@@ -357,7 +264,7 @@ describe('TransferForm', () => {
// 1003-TRAN-023
expect(mockSubmit).toHaveBeenCalledTimes(1);
expect(mockSubmit).toHaveBeenCalledWith({
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
to: props.pubKeys[1],
asset: asset.id,
@@ -368,7 +275,7 @@ describe('TransferForm', () => {
});
it('validates fields when checkbox is not checked', async () => {
renderComponent(props);
render(<TransferForm {...props} />);
// check current pubkey not shown
const keySelect: HTMLSelectElement = screen.getByLabelText('To Vega key');
@@ -379,7 +286,7 @@ describe('TransferForm', () => {
);
await submit();
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
expect(await screen.findAllByText('Required')).toHaveLength(3); // pubkey set as default value
// Select a pubkey
await userEvent.selectOptions(
@@ -390,11 +297,6 @@ 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();
@@ -414,28 +316,26 @@ describe('TransferForm', () => {
describe('AddressField', () => {
const props = {
mode: 'select' as const,
select: <div>select</div>,
input: <div>input</div>,
onChange: jest.fn(),
};
it('renders correct content by mode prop and calls onChange', async () => {
it('toggles content and calls onChange', async () => {
const mockOnChange = jest.fn();
const { rerender } = render(
<AddressField {...props} onChange={mockOnChange} />
);
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);
});
});
+130 -218
View File
@@ -1,12 +1,12 @@
import sortBy from 'lodash/sortBy';
import {
minSafe,
maxSafe,
required,
vegaPublicKey,
addDecimal,
formatNumber,
addDecimalsFormatNumber,
toBigNum,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
@@ -30,30 +30,21 @@ import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
interface FormFields {
toVegaKey: string;
asset: string; // This is used to simply filter the from account list, the fromAccount type should be used in the tx
asset: string;
amount: string;
fromAccount: string; // AccountType-AssetId
fromAccount: AccountType;
}
interface Asset {
id: string;
symbol: string;
name: string;
decimals: number;
quantum: string;
}
export interface TransferFormProps {
interface TransferFormProps {
pubKey: string | null;
pubKeys: string[] | null;
accounts: Array<{
type: AccountType;
balance: string;
asset: Asset;
asset: { id: string; symbol: string; name: string; decimals: number };
}>;
assetId?: string;
feeFactor: string | null;
minQuantumMultiple: string | null;
submitTransfer: (transfer: Transfer) => void;
}
@@ -64,7 +55,6 @@ export const TransferForm = ({
feeFactor,
submitTransfer,
accounts,
minQuantumMultiple,
}: TransferFormProps) => {
const {
control,
@@ -80,58 +70,43 @@ export const TransferForm = ({
},
});
const [toVegaKeyMode, setToVegaKeyMode] = useState<ToVegaKeyMode>('select');
const assets = sortBy(
accounts
.filter(
(a) =>
a.type === AccountType.ACCOUNT_TYPE_GENERAL ||
a.type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS
)
// Sum the general and vested account balances so the value shown in the asset
// dropdown is correct for all transferable accounts
.reduce((merged, account) => {
const existing = merged.findIndex(
(m) => m.asset.id === account.asset.id
);
if (existing > -1) {
const balance = new BigNumber(merged[existing].balance)
.plus(new BigNumber(account.balance))
.toString();
merged[existing] = { ...merged[existing], balance };
return merged;
}
return [...merged, account];
}, [] as typeof accounts)
.filter((a) => a.type === AccountType.ACCOUNT_TYPE_GENERAL)
.map((account) => ({
key: account.asset.id,
...account.asset,
balance: addDecimal(account.balance, account.asset.decimals),
})),
(a) => a.symbol.toLowerCase()
'name'
);
const selectedPubKey = watch('toVegaKey');
const amount = watch('amount');
const fromAccount = watch('fromAccount');
const selectedAssetId = watch('asset');
const assetId = watch('asset');
// Convert the account type (Type-AssetId) into separate values
const [accountType, accountAssetId] = fromAccount
? parseFromAccount(fromAccount)
: [undefined, undefined];
const fromVested = accountType === AccountType.ACCOUNT_TYPE_VESTED_REWARDS;
const asset = assets.find((a) => a.id === accountAssetId);
const asset = assets.find((a) => a.id === assetId);
const account = accounts.find(
(a) => a.asset.id === accountAssetId && a.type === accountType
(a) => a.asset.id === assetId && a.type === fromAccount
);
const accountBalance =
account && addDecimal(account.balance, account.asset.decimals);
// General account for the selected asset
const generalAccount = accounts.find((a) => {
return (
a.asset.id === assetId && a.type === AccountType.ACCOUNT_TYPE_GENERAL
);
});
const [includeFee, setIncludeFee] = useState(false);
// Min viable amount given asset decimals EG for WEI 0.000000000000000001
const min = asset
? new BigNumber(addDecimal('1', asset.decimals))
: new BigNumber(0);
// Max amount given selected asset and from account
const max = accountBalance ? new BigNumber(accountBalance) : new BigNumber(0);
@@ -155,21 +130,16 @@ export const TransferForm = ({
const onSubmit = useCallback(
(fields: FormFields) => {
if (!transferAmount) {
throw new Error('Submitted transfer with no amount selected');
}
const [type, assetId] = parseFromAccount(fields.fromAccount);
const asset = assets.find((a) => a.id === assetId);
if (!asset) {
throw new Error('Submitted transfer with no asset selected');
}
if (!transferAmount) {
throw new Error('Submitted transfer with no amount selected');
}
const transfer = normalizeTransfer(
fields.toVegaKey,
transferAmount,
type,
fields.fromAccount,
AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form
{
id: asset.id,
@@ -178,7 +148,7 @@ export const TransferForm = ({
);
submitTransfer(transfer);
},
[submitTransfer, transferAmount, assets]
[asset, submitTransfer, transferAmount]
);
// reset for placeholder workaround https://github.com/radix-ui/primitives/issues/1569
@@ -194,10 +164,55 @@ export const TransferForm = ({
className="text-sm"
data-testid="transfer-form"
>
<TradingFormGroup label="To Vega key" labelFor="toVegaKey">
<AddressField
onChange={() => setValue('toVegaKey', '')}
select={
<TradingSelect {...register('toVegaKey')} id="toVegaKey">
<option value="" disabled={true}>
{t('Please select')}
</option>
{pubKeys?.map((pk) => {
const text = pk === pubKey ? t('Current key: ') + pk : pk;
return (
<option key={pk} value={pk}>
{text}
</option>
);
})}
</TradingSelect>
}
input={
<TradingInput
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={true} // focus input immediately after is shown
id="toVegaKey"
type="text"
{...register('toVegaKey', {
validate: {
required,
vegaPublicKey,
},
})}
/>
}
/>
{errors.toVegaKey?.message && (
<TradingInputError forInput="toVegaKey">
{errors.toVegaKey.message}
</TradingInputError>
)}
</TradingFormGroup>
<TradingFormGroup label={t('Asset')} labelFor="asset">
<Controller
control={control}
name="asset"
rules={{
validate: {
required,
},
}}
render={({ field }) => (
<TradingRichSelect
data-testid="select-asset"
@@ -205,14 +220,13 @@ export const TransferForm = ({
name={field.name}
onValueChange={(value) => {
field.onChange(value);
setValue('fromAccount', '');
}}
placeholder={t('Please select an asset')}
value={field.value}
>
{assets.map((a) => (
<AssetOption
key={a.key}
key={a.id}
asset={a}
balance={
<Balance
@@ -232,10 +246,10 @@ export const TransferForm = ({
)}
</TradingFormGroup>
<TradingFormGroup label={t('From account')} labelFor="fromAccount">
<Controller
control={control}
name="fromAccount"
rules={{
<TradingSelect
id="fromAccount"
defaultValue=""
{...register('fromAccount', {
validate: {
required,
sameAccount: (value) => {
@@ -250,106 +264,48 @@ export const TransferForm = ({
return true;
},
},
}}
render={({ field }) => (
<TradingSelect
id="fromAccount"
defaultValue=""
{...field}
onChange={(e) => {
field.onChange(e);
const [type] = parseFromAccount(e.target.value);
// Enforce that if transferring from a vested rewards account it must go to
// the current connected general account
if (
type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS &&
pubKey
) {
setValue('toVegaKey', pubKey);
setToVegaKeyMode('select');
setIncludeFee(false);
}
}}
>
<option value="" disabled={true}>
{t('Please select')}
</option>
{accounts
.filter((a) => {
if (!selectedAssetId) return true;
return selectedAssetId === a.asset.id;
})
.map((a) => {
const id = `${a.type}-${a.asset.id}`;
return (
<option value={id} key={id}>
{AccountTypeMapping[a.type]} (
{addDecimalsFormatNumber(a.balance, a.asset.decimals)}{' '}
{a.asset.symbol})
</option>
);
})}
</TradingSelect>
)}
/>
})}
>
<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="To Vega key" labelFor="toVegaKey">
<AddressField
onChange={() => {
setValue('toVegaKey', '');
setToVegaKeyMode((curr) => (curr === 'input' ? 'select' : 'input'));
}}
mode={toVegaKeyMode}
select={
<TradingSelect
{...register('toVegaKey')}
disabled={fromVested}
id="toVegaKey"
>
<option value="" disabled={true}>
{t('Please select')}
</option>
{pubKeys?.map((pk) => {
const text = pk === pubKey ? t('Current key: ') + pk : pk;
return (
<option key={pk} value={pk}>
{text}
</option>
);
})}
</TradingSelect>
}
input={
fromVested ? null : (
<TradingInput
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={true} // focus input immediately after is shown
id="toVegaKey"
type="text"
disabled={fromVested}
{...register('toVegaKey', {
validate: {
required,
vegaPublicKey,
},
})}
/>
)
}
/>
{errors.toVegaKey?.message && (
<TradingInputError forInput="toVegaKey">
{errors.toVegaKey.message}
</TradingInputError>
)}
<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
@@ -361,43 +317,7 @@ export const TransferForm = ({
{...register('amount', {
validate: {
required,
minSafe: (v) => {
if (!asset || !minQuantumMultiple) return true;
const value = new BigNumber(v);
if (value.isZero()) {
return t('Amount cannot be 0');
}
const minByQuantumMultiple = toBigNum(
minQuantumMultiple,
asset.decimals
);
if (fromVested) {
// special conditions which let you bypass min transfer rules set by quantum multiple
if (value.isGreaterThanOrEqualTo(max)) {
return true;
}
if (value.isLessThan(minByQuantumMultiple)) {
return t(
'Amount below minimum requirements for partial transfer. Use max to bypass'
);
}
return true;
} else {
if (value.isLessThan(minByQuantumMultiple)) {
return t(
'Amount below minimum requirement set by transfer.minTransferQuantumMultiple'
);
}
}
return true;
},
minSafe: (value) => minSafe(new BigNumber(min))(value),
maxSafe: (v) => {
const value = new BigNumber(v);
if (value.isGreaterThan(max)) {
@@ -413,9 +333,7 @@ export const TransferForm = ({
type="button"
className="absolute top-0 right-0 ml-auto text-xs underline"
onClick={() =>
setValue('amount', parseFloat(accountBalance).toString(), {
shouldValidate: true,
})
setValue('amount', parseFloat(accountBalance).toString())
}
>
{t('Use max')}
@@ -436,10 +354,10 @@ export const TransferForm = ({
<div>
<TradingCheckbox
name="include-transfer-fee"
disabled={!transferAmount || fromVested}
disabled={!transferAmount}
label={t('Include transfer fee')}
checked={includeFee}
onCheckedChange={() => setIncludeFee((x) => !x)}
onCheckedChange={() => setIncludeFee(!includeFee)}
/>
</div>
</Tooltip>
@@ -449,7 +367,7 @@ export const TransferForm = ({
amount={transferAmount}
transferAmount={transferAmount}
feeFactor={feeFactor}
fee={fromVested ? '0' : fee}
fee={fee}
decimals={asset?.decimals}
/>
)}
@@ -531,38 +449,32 @@ export const TransferFee = ({
);
};
type ToVegaKeyMode = 'input' | 'select';
interface AddressInputProps {
select: ReactNode;
input: ReactNode;
mode: ToVegaKeyMode;
onChange: () => void;
}
export const AddressField = ({
select,
input,
mode,
onChange,
}: AddressInputProps) => {
const isInput = mode === 'input';
const [isInput, setIsInput] = useState(false);
return (
<>
{isInput ? input : select}
{select && input && (
<button
type="button"
onClick={onChange}
className="absolute top-0 right-0 ml-auto text-xs underline"
>
{isInput ? t('Select from wallet') : t('Enter manually')}
</button>
)}
<button
type="button"
onClick={() => {
setIsInput((curr) => !curr);
onChange();
}}
className="absolute top-0 right-0 ml-auto text-xs underline"
>
{isInput ? t('Select from wallet') : t('Enter manually')}
</button>
</>
);
};
const parseFromAccount = (fromAccountStr: string) => {
return fromAccountStr.split('-') as [AccountType, string];
};
@@ -72,11 +72,15 @@ export const DealTicketFeeDetails = ({
return (
<KeyValue
label={
label={t('Fees')}
value={
totalDiscountedFeeAmount &&
`~${formatValue(totalDiscountedFeeAmount, assetDecimals)}`
}
formattedValue={
<>
{t('Fees')}
{totalDiscountFactor ? (
<Pill size="xxs" intent={Intent.Info} className="ml-1">
<Pill size="xxs" intent={Intent.Warning} className="mr-1">
-
{formatNumberPercentage(
new BigNumber(totalDiscountFactor).multipliedBy(100),
@@ -84,16 +88,10 @@ export const DealTicketFeeDetails = ({
)}
</Pill>
) : null}
{totalDiscountedFeeAmount &&
`~${formatValue(totalDiscountedFeeAmount, assetDecimals, quantum)}`}
</>
}
value={
totalDiscountedFeeAmount &&
`~${formatValue(totalDiscountedFeeAmount, assetDecimals)}`
}
formattedValue={
totalDiscountedFeeAmount &&
`~${formatValue(totalDiscountedFeeAmount, assetDecimals, quantum)}`
}
labelDescription={
<div className="flex flex-col gap-2">
<p>
@@ -79,11 +79,7 @@ export const FeesBreakdown = ({
volumeDiscountFactor
);
const {
discountedFee: discountedTotalFeeAmount,
volumeDiscount,
referralDiscount,
} = getDiscountedFee(
const { volumeDiscount, referralDiscount } = getDiscountedFee(
totalFeeAmount,
referralDiscountFactor,
volumeDiscountFactor
@@ -135,7 +131,7 @@ export const FeesBreakdown = ({
<FeesBreakdownItem
label={t('Total fees')}
factor={feeFactors ? sumFeesFactors(feeFactors)?.toString() : undefined}
value={discountedTotalFeeAmount}
value={totalFeeAmount}
symbol={symbol}
decimals={decimals}
/>
@@ -1,12 +1,6 @@
import type { MockedResponse } from '@apollo/react-testing';
import { MockedProvider } from '@apollo/react-testing';
import {
act,
render,
renderHook,
screen,
waitFor,
} from '@testing-library/react';
import { act, render, screen, waitFor } from '@testing-library/react';
import { RadioGroup } from '@vegaprotocol/ui-toolkit';
import type {
NodeCheckTimeUpdateSubscription,
@@ -17,33 +11,30 @@ import {
NodeCheckTimeUpdateDocument,
} from '../../utils/__generated__/NodeCheck';
import type { RowDataProps } from './row-data';
import {
POLL_INTERVAL,
Result,
SUBSCRIPTION_TIMEOUT,
useNodeBasicStatus,
useNodeSubscriptionStatus,
useResponseTime,
} from './row-data';
import { POLL_INTERVAL } from './row-data';
import { BLOCK_THRESHOLD, RowData } from './row-data';
import type { HeaderEntry } from '@vegaprotocol/apollo-client';
import { useHeaderStore } from '@vegaprotocol/apollo-client';
import { CUSTOM_NODE_KEY } from '../../types';
const mockStatsQuery = (
blockHeight = '1234'
): MockedResponse<NodeCheckQuery> => ({
jest.mock('@vegaprotocol/apollo-client', () => ({
useHeaderStore: jest.fn().mockReturnValue({}),
}));
const statsQueryMock: MockedResponse<NodeCheckQuery> = {
request: {
query: NodeCheckDocument,
},
result: {
data: {
statistics: {
blockHeight,
blockHeight: '1234', // the actual value used in the component is the value from the header store
vegaTime: new Date().toISOString(),
chainId: 'test-chain-id',
},
},
},
});
};
const subMock: MockedResponse<NodeCheckTimeUpdateSubscription> = {
request: {
@@ -68,6 +59,18 @@ global.performance.getEntriesByName = jest.fn().mockReturnValue([
},
]);
const mockHeaders = (
url: string,
headers: Partial<HeaderEntry> = {
blockHeight: 100,
timestamp: new Date(),
}
) => {
(useHeaderStore as unknown as jest.Mock).mockReturnValue({
[url]: headers,
});
};
const renderComponent = (
props: RowDataProps,
queryMock: MockedResponse<NodeCheckQuery>,
@@ -83,98 +86,6 @@ const renderComponent = (
);
};
describe('useNodeSubscriptionStatus', () => {
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
});
const mockWrapper =
(withData = false) =>
({ children }: { children: React.ReactNode }) =>
(
<MockedProvider mocks={withData ? [subMock, subMock, subMock] : []}>
{children}
</MockedProvider>
);
it('results initially as loading', async () => {
const { result } = renderHook(() => useNodeSubscriptionStatus(), {
wrapper: mockWrapper(true),
});
expect(result.current.status).toBe(Result.Loading);
});
it('results as successful when data received', async () => {
const { result } = renderHook(() => useNodeSubscriptionStatus(), {
wrapper: mockWrapper(true),
});
expect(result.current.status).toBe(Result.Loading);
await act(() => {
jest.advanceTimersByTime(SUBSCRIPTION_TIMEOUT);
});
expect(result.current.status).toBe(Result.Successful);
});
it('result as failed when no data received', async () => {
const { result } = renderHook(() => useNodeSubscriptionStatus(), {
wrapper: mockWrapper(false),
});
expect(result.current.status).toBe(Result.Loading);
await act(() => {
jest.advanceTimersByTime(SUBSCRIPTION_TIMEOUT);
});
expect(result.current.status).toBe(Result.Failed);
});
});
describe('useNodeBasicStatus', () => {
const mockWrapper =
(withData = false) =>
({ children }: { children: React.ReactNode }) =>
(
<MockedProvider mocks={withData ? [mockStatsQuery('1234')] : []}>
{children}
</MockedProvider>
);
it('results initially as loading', async () => {
const { result } = renderHook(() => useNodeBasicStatus(), {
wrapper: mockWrapper(true),
});
expect(result.current.status).toBe(Result.Loading);
expect(result.current.currentBlockHeight).toBeNaN();
});
it('results as successful when data received', async () => {
const { result } = renderHook(() => useNodeBasicStatus(), {
wrapper: mockWrapper(true),
});
await waitFor(() => {
expect(result.current.status).toBe(Result.Successful);
expect(result.current.currentBlockHeight).toBe(1234);
});
});
it('result as failed when no data received', async () => {
const { result } = renderHook(() => useNodeBasicStatus(), {
wrapper: mockWrapper(false),
});
await waitFor(() => {
expect(result.current.status).toBe(Result.Failed);
expect(result.current.currentBlockHeight).toBeNaN();
});
});
});
describe('useResponseTime', () => {
it('returns response time when url is valid', () => {
const { result } = renderHook(() =>
useResponseTime('https://localhost:1234')
);
expect(result.current.responseTime).toBe(50);
});
it('does not return response time when url is invalid', () => {
const { result } = renderHook(() => useResponseTime('nope'));
expect(result.current.responseTime).toBeUndefined();
});
});
describe('RowData', () => {
const props = {
id: '0',
@@ -183,13 +94,9 @@ describe('RowData', () => {
onBlockHeight: jest.fn(),
};
afterAll(() => {
jest.useRealTimers();
jest.resetAllMocks();
});
it('radio button enabled after stats query successful', async () => {
render(renderComponent(props, mockStatsQuery('100'), subMock));
mockHeaders(props.url);
render(renderComponent(props, statsQueryMock, subMock));
// radio should be enabled until query resolves
expect(
@@ -220,6 +127,8 @@ describe('RowData', () => {
});
it('radio button still enabled if query fails', async () => {
mockHeaders(props.url, {});
const failedQueryMock: MockedResponse<NodeCheckQuery> = {
request: {
query: NodeCheckDocument,
@@ -269,11 +178,12 @@ describe('RowData', () => {
it('highlights rows with a slow block height', async () => {
const blockHeight = 100;
mockHeaders(props.url, { blockHeight });
const { rerender } = render(
renderComponent(
{ ...props, highestBlock: blockHeight + BLOCK_THRESHOLD },
mockStatsQuery(String(blockHeight)),
statsQueryMock,
subMock
)
);
@@ -291,7 +201,7 @@ describe('RowData', () => {
rerender(
renderComponent(
{ ...props, highestBlock: blockHeight + BLOCK_THRESHOLD + 1 },
mockStatsQuery(String(blockHeight)),
statsQueryMock,
subMock
)
);
@@ -306,7 +216,7 @@ describe('RowData', () => {
...props,
id: CUSTOM_NODE_KEY,
},
mockStatsQuery('1234'),
statsQueryMock,
subMock
)
);
@@ -320,17 +230,16 @@ describe('RowData', () => {
it('updates highest block after new header received', async () => {
const mockOnBlockHeight = jest.fn();
const blockHeight = 200;
mockHeaders(props.url, { blockHeight });
render(
renderComponent(
{ ...props, onBlockHeight: mockOnBlockHeight },
mockStatsQuery(String(blockHeight)),
statsQueryMock,
subMock
)
);
await waitFor(() => {
expect(mockOnBlockHeight).toHaveBeenCalledWith(blockHeight);
});
expect(mockOnBlockHeight).toHaveBeenCalledWith(blockHeight);
});
it('should poll the query unless an errors is returned', async () => {
@@ -366,6 +275,7 @@ describe('RowData', () => {
};
};
mockHeaders(props.url);
const statsQueryMock1 = createStatsQueryMock('1234');
const statsQueryMock2 = createStatsQueryMock('1235');
const statsQueryMock3 = createFailedStatsQueryMock();
@@ -1,3 +1,5 @@
import type { ApolloError } from '@apollo/client';
import { useHeaderStore } from '@vegaprotocol/apollo-client';
import { isValidUrl } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { TradingRadio } from '@vegaprotocol/ui-toolkit';
@@ -10,7 +12,6 @@ import {
import { LayoutCell } from './layout-cell';
export const POLL_INTERVAL = 1000;
export const SUBSCRIPTION_TIMEOUT = 3000;
export const BLOCK_THRESHOLD = 3;
export interface RowDataProps {
@@ -20,39 +21,15 @@ export interface RowDataProps {
onBlockHeight: (blockHeight: number) => void;
}
export enum Result {
Successful,
Failed,
Loading,
}
export const useNodeSubscriptionStatus = () => {
const [status, setStatus] = useState<Result>(Result.Loading);
const { data, error } = useNodeCheckTimeUpdateSubscription();
useEffect(() => {
if (error) {
setStatus(Result.Failed);
}
if (data?.busEvents && data.busEvents.length > 0) {
setStatus(Result.Successful);
}
// set as failed when no data received after SUBSCRIPTION_TIMEOUT ms
const timeout = setTimeout(() => {
if (!data || error) {
setStatus(Result.Failed);
}
}, SUBSCRIPTION_TIMEOUT);
return () => {
clearTimeout(timeout);
};
}, [data, error]);
return { status };
};
export const useNodeBasicStatus = () => {
const [status, setStatus] = useState<Result>(Result.Loading);
export const RowData = ({
id,
url,
highestBlock,
onBlockHeight,
}: RowDataProps) => {
const [subFailed, setSubFailed] = useState(false);
const [time, setTime] = useState<number>();
// no use of data here as we need the data nodes reference to block height
const { data, error, loading, startPolling, stopPolling } = useNodeCheckQuery(
{
pollInterval: POLL_INTERVAL,
@@ -61,7 +38,28 @@ export const useNodeBasicStatus = () => {
ssr: false,
}
);
const headerStore = useHeaderStore();
const headers = headerStore[url];
const {
data: subData,
error: subError,
loading: subLoading,
} = useNodeCheckTimeUpdateSubscription();
useEffect(() => {
const timeout = setTimeout(() => {
if (!subData) {
setSubFailed(true);
}
}, 3000);
return () => {
clearTimeout(timeout);
};
}, [subData]);
// handle polling
useEffect(() => {
const handleStartPoll = () => {
if (error) return;
@@ -85,57 +83,56 @@ export const useNodeBasicStatus = () => {
};
}, [startPolling, stopPolling, error]);
const currentBlockHeight = parseInt(
data?.statistics.blockHeight || 'NONE',
10
);
useEffect(() => {
if (loading) {
setStatus(Result.Loading);
return;
}
if (!error && !isNaN(currentBlockHeight)) {
setStatus(Result.Successful);
return;
}
setStatus(Result.Failed);
}, [currentBlockHeight, error, loading]);
return {
status,
currentBlockHeight,
};
};
export const useResponseTime = (url: string, trigger?: unknown) => {
const [responseTime, setResponseTime] = useState<number>();
// measure response time
useEffect(() => {
if (!isValidUrl(url)) return;
if (typeof window.performance.getEntriesByName !== 'function') return; // protection for test environment
// every time we get data measure response speed
const requestUrl = new URL(url);
const requests = window.performance.getEntriesByName(requestUrl.href);
const { duration } =
(requests.length && requests[requests.length - 1]) || {};
setResponseTime(duration);
}, [url, trigger]);
return { responseTime };
};
setTime(duration);
}, [url, data]);
export const RowData = ({
id,
url,
highestBlock,
onBlockHeight,
}: RowDataProps) => {
const { status: subStatus } = useNodeSubscriptionStatus();
const { status, currentBlockHeight } = useNodeBasicStatus();
const { responseTime } = useResponseTime(url, currentBlockHeight); // measure response time (ms) every time we get data (block height)
useEffect(() => {
if (!isNaN(currentBlockHeight)) {
onBlockHeight(currentBlockHeight);
if (headers?.blockHeight) {
onBlockHeight(headers.blockHeight);
}
}, [currentBlockHeight, onBlockHeight]);
}, [headers?.blockHeight, onBlockHeight]);
const getHasError = () => {
// the stats query errored
if (error) {
return true;
}
// if we are still awaiting a header entry its not an error
// we are still waiting for the query to resolve
if (!headers) {
return false;
}
// highlight this node as 'error' if its more than BLOCK_THRESHOLD blocks behind the most
// advanced node
if (
highestBlock !== null &&
headers.blockHeight < highestBlock - BLOCK_THRESHOLD
) {
return true;
}
return false;
};
const getSubFailed = (
subError: ApolloError | undefined,
subFailed: boolean
) => {
if (subError) return true;
if (subFailed) return true;
return false;
};
return (
<>
@@ -146,58 +143,72 @@ export const RowData = ({
)}
<LayoutCell
label={t('Response time')}
isLoading={status === Result.Loading}
hasError={status === Result.Failed}
isLoading={!error && loading}
hasError={Boolean(error)}
dataTestId="response-time-cell"
>
{display(status, formatResponseTime(responseTime))}
{getResponseTimeDisplayValue(time, error)}
</LayoutCell>
<LayoutCell
label={t('Block')}
isLoading={status === Result.Loading}
hasError={
status === Result.Failed ||
(highestBlock != null &&
!isNaN(currentBlockHeight) &&
currentBlockHeight < highestBlock - BLOCK_THRESHOLD)
}
isLoading={loading}
hasError={getHasError()}
dataTestId="block-height-cell"
>
<span
data-testid="query-block-height"
data-query-block-height={
status === Result.Failed ? 'failed' : currentBlockHeight
error ? 'failed' : data?.statistics.blockHeight
}
>
{display(status, currentBlockHeight)}
{getBlockDisplayValue(headers?.blockHeight, error)}
</span>
</LayoutCell>
<LayoutCell
label={t('Subscription')}
isLoading={subStatus === Result.Loading}
hasError={subStatus === Result.Failed}
isLoading={subFailed ? false : subLoading}
hasError={getSubFailed(subError, subFailed)}
dataTestId="subscription-cell"
>
{display(subStatus, t('Yes'), t('No'))}
{getSubscriptionDisplayValue(subFailed, subData?.busEvents, subError)}
</LayoutCell>
</>
);
};
const formatResponseTime = (time: number | undefined) =>
time != null ? `${Number(time).toFixed(2)}ms` : '-';
const display = (
status: Result,
yes: string | number | undefined,
no = t('n/a')
const getResponseTimeDisplayValue = (
responseTime?: number,
error?: ApolloError
) => {
switch (status) {
case Result.Successful:
return yes;
case Result.Failed:
return no;
default:
return '-';
if (error) {
return t('n/a');
}
if (typeof responseTime === 'number') {
return `${Number(responseTime).toFixed(2)}ms`;
}
return '-';
};
const getBlockDisplayValue = (block?: number, error?: ApolloError) => {
if (error) {
return t('n/a');
}
if (block) {
return block;
}
return '-';
};
const getSubscriptionDisplayValue = (
subFailed: boolean,
events?: { id: string }[] | null,
error?: ApolloError
) => {
if (subFailed || error) {
return t('No');
}
if (events?.length) {
return t('Yes');
}
return '-';
};
-5
View File
@@ -126,11 +126,6 @@ export const useEtherscanLink = () => {
return link;
};
// Console pages
export const CONSOLE_TRANSFER = '#/portfolio/assets/transfer';
export const CONSOLE_TRANSFER_ASSET =
'#/portfolio/assets/transfer?assetId=:assetId';
// Governance pages
export const TOKEN_NEW_MARKET_PROPOSAL = '/proposals/propose/new-market';
export const TOKEN_NEW_NETWORK_PARAM_PROPOSAL =
+9 -22
View File
@@ -289,30 +289,17 @@ describe('FeesDiscountBreakdownTooltip', () => {
const { container } = render(<FeesDiscountBreakdownTooltip {...props} />);
const dt = container.querySelectorAll('dt');
const dd = container.querySelectorAll('dd');
const expectedDt = [
'Infrastructure Fee',
'Referral Discount',
'Volume Discount',
'Liquidity Fee',
'Referral Discount',
'Volume Discount',
'Maker Fee',
'Referral Discount',
'Volume Discount',
const expected = [
{ label: 'Infrastructure Fee Referral Discount', value: '0.05 BTC' },
{ label: 'Infrastructure Fee Volume Discount', value: '0.06 BTC' },
{ label: 'Liquidity Fee Referral Discount', value: '0.01 BTC' },
{ label: 'Liquidity Fee Volume Discount', value: '0.02 BTC' },
{ label: 'Maker Fee Referral Discount', value: '0.03 BTC' },
{ label: 'Maker Fee Volume Discount', value: '0.04 BTC' },
];
const expectedDD = [
'0.05 BTC',
'0.06 BTC',
'0.01 BTC',
'0.02 BTC',
'0.03 BTC',
'0.04 BTC',
];
expectedDt.forEach((label, i) => {
expected.forEach(({ label, value }, i) => {
expect(dt[i]).toHaveTextContent(label);
});
expectedDD.forEach((label, i) => {
expect(dd[i]).toHaveTextContent(label);
expect(dd[i]).toHaveTextContent(value);
});
});
});
+7 -20
View File
@@ -391,7 +391,7 @@ const FeesDiscountBreakdownTooltipItem = ({
label: string;
asset: ReturnType<typeof getAsset>;
}) =>
value && value !== '0' ? (
value ? (
<>
<dt className="col-span-1">{label}</dt>
<dd className="text-right col-span-1">
@@ -418,47 +418,34 @@ export const FeesDiscountBreakdownTooltip = ({
className="max-w-sm bg-vega-light-100 dark:bg-vega-dark-100 border border-vega-light-200 dark:border-vega-dark-200 px-4 py-2 z-20 rounded text-sm break-word text-black dark:text-white"
>
<dl className="grid grid-cols-2 gap-x-1">
{(fees.infrastructureFeeReferralDiscount || '0') !== '0' ||
(fees.infrastructureFeeVolumeDiscount || '0') !== '0' ? (
<dt className="col-span-2">{t('Infrastructure Fee')}</dt>
) : null}
<FeesDiscountBreakdownTooltipItem
value={fees.infrastructureFeeReferralDiscount}
label={t('Referral Discount')}
label={t('Infrastructure Fee Referral Discount')}
asset={asset}
/>
<FeesDiscountBreakdownTooltipItem
value={fees.infrastructureFeeVolumeDiscount}
label={t('Volume Discount')}
label={t('Infrastructure Fee Volume Discount')}
asset={asset}
/>
{(fees.liquidityFeeReferralDiscount || '0') !== '0' ||
(fees.liquidityFeeVolumeDiscount || '0') !== '0' ? (
<dt className="col-span-2">{t('Liquidity Fee')}</dt>
) : null}
<FeesDiscountBreakdownTooltipItem
value={fees.liquidityFeeReferralDiscount}
label={t('Referral Discount')}
label={t('Liquidity Fee Referral Discount')}
asset={asset}
/>
<FeesDiscountBreakdownTooltipItem
value={fees.liquidityFeeVolumeDiscount}
label={t('Volume Discount')}
label={t('Liquidity Fee Volume Discount')}
asset={asset}
/>
{(fees.makerFeeReferralDiscount || '0') !== '0' ||
(fees.makerFeeVolumeDiscount || '0') !== '0' ? (
<dt className="col-span-2">{t('Maker Fee')}</dt>
) : null}
<FeesDiscountBreakdownTooltipItem
value={fees.makerFeeReferralDiscount}
label={t('Referral Discount')}
label={t('Maker Fee Referral Discount')}
asset={asset}
/>
<FeesDiscountBreakdownTooltipItem
value={fees.makerFeeVolumeDiscount}
label={t('Volume Discount')}
label={t('Maker Fee Volume Discount')}
asset={asset}
/>
</dl>
+5 -10
View File
@@ -2,6 +2,7 @@ import { useMemo } from 'react';
import {
addDecimalsFormatNumber,
addDecimalsFormatNumberQuantum,
formatNumberPercentage,
getDateTimeFormat,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
@@ -28,12 +29,6 @@ import { LiquidityProvisionStatus } from '@vegaprotocol/types';
import { LiquidityProvisionStatusMapping } from '@vegaprotocol/types';
import type { LiquidityProvisionData } from './liquidity-data-provider';
const formatNumberPercentage = (value: BigNumber, decimals?: number) => {
const decimalPlaces =
typeof decimals === 'undefined' ? value.dp() || 0 : decimals;
return `${value.toFixed(decimalPlaces, 1)}%`;
};
const percentageFormatter = ({ value }: ValueFormatterParams) => {
if (!value) return '-';
return formatNumberPercentage(new BigNumber(value).times(100), 2) || '-';
@@ -131,11 +126,11 @@ export const LiquidityTable = ({
new BigNumber(data.sla.currentEpochFractionOfTimeOnBook).times(
100
),
4
2
),
formatNumberPercentage(
new BigNumber(data.commitmentMinTimeFraction).times(100),
4
2
),
]
);
@@ -148,7 +143,7 @@ export const LiquidityTable = ({
new BigNumber(data.sla.currentEpochFractionOfTimeOnBook).times(
100
),
4
2
),
]
);
@@ -399,7 +394,7 @@ export const LiquidityTable = ({
headerTooltip: t(
`The liquidity fees accrued by each provider, which will be distributed at the end of the epoch after applying any penalties.`
),
valueFormatter: assetDecimalsQuantumFormatter,
valueFormatter: stakeToCcyVolumeQuantumFormatter,
tooltipValueGetter: feesAccruedTooltip,
cellClassRules: {
'text-warning': ({ data }: { data: LiquidityProvisionData }) => {
@@ -12,7 +12,6 @@ export const NetworkParams = {
'rewards_marketCreationQuantumMultiple',
reward_staking_delegation_payoutDelay:
'reward_staking_delegation_payoutDelay',
rewards_vesting_baseRate: 'rewards_vesting_baseRate',
governance_proposal_market_minVoterBalance:
'governance_proposal_market_minVoterBalance',
governance_proposal_market_minClose: 'governance_proposal_market_minClose',
@@ -176,7 +175,6 @@ export const NetworkParams = {
market_liquidity_feeCalculationTimeStep:
'market_liquidity_feeCalculationTimeStep',
transfer_fee_factor: 'transfer_fee_factor',
transfer_minTransferQuantumMultiple: 'transfer_minTransferQuantumMultiple',
network_validators_incumbentBonus: 'network_validators_incumbentBonus',
} as const;
+1 -1
View File
@@ -71,7 +71,7 @@
"jsondiffpatch": "^0.4.1",
"lodash": "^4.17.21",
"next": "13.3.0",
"pennant": "^1.14.1",
"pennant": "1.14.0",
"react": "18.2.0",
"react-copy-to-clipboard": "^5.0.4",
"react-dom": "18.2.0",
+2
View File
@@ -0,0 +1,2 @@
genesis.json
genesis.tmp
+18 -32
View File
@@ -2,11 +2,10 @@
"app_state": {
"assets": {
"73174a6fb1d5802ba0ac7bd7ab79e0a3a4837b262de0a4e80815a55442692bd0": {
"min_lp_stake": "1",
"decimals": 5,
"name": "BTC (fake)",
"quantum": "1",
"symbol": "fBTC",
"total_supply": "21000000",
"source": {
"builtin_asset": {
"max_faucet_amount_mint": "1000000"
@@ -14,11 +13,10 @@
}
},
"8566db7257222b5b7ef2886394ad28b938b28680a54a169bbc795027b89d6665": {
"min_lp_stake": "1",
"decimals": 5,
"name": "DAI (fake)",
"quantum": "1",
"symbol": "fDAI",
"total_supply": "1000000000",
"source": {
"builtin_asset": {
"max_faucet_amount_mint": "10000000000"
@@ -26,11 +24,10 @@
}
},
"e02d4c15d790d1d2dffaf2dcd1cf06a1fe656656cf4ed18c8ce99f9e83643567": {
"min_lp_stake": "1",
"decimals": 5,
"name": "EURO (fake)",
"symbol": "fEURO",
"total_supply": "1000000000",
"quantum": "1",
"source": {
"builtin_asset": {
"max_faucet_amount_mint": "10000000000"
@@ -38,11 +35,10 @@
}
},
"816af99af60d684502a40824758f6b5377e6af48e50a9ee8ef478ecb879ea8bc": {
"min_lp_stake": "1",
"decimals": 5,
"name": "USDC (fake)",
"symbol": "fUSDC",
"total_supply": "1000000000",
"quantum": "1",
"source": {
"builtin_asset": {
"max_faucet_amount_mint": "1000000000000"
@@ -50,11 +46,10 @@
}
},
"62dfb1ab1cd488862b416cf163c75bc9a279226c65ac287c0dd67650daa444ee": {
"min_lp_stake": "1",
"decimals": 5,
"name": "XYZ (α alpha)",
"quantum": "1",
"symbol": "XYZalpha",
"total_supply": "1000000000",
"source": {
"builtin_asset": {
"max_faucet_amount_mint": "100000000000"
@@ -62,11 +57,10 @@
}
},
"dedb3c42e7bc88d98a7fe0d73c7b9870b34c62c79a713071bd4d645ef7c216ee": {
"min_lp_stake": "1",
"decimals": 5,
"name": "XYZ (β beta)",
"quantum": "1",
"symbol": "XYZbeta",
"total_supply": "1000000000",
"source": {
"builtin_asset": {
"max_faucet_amount_mint": "100000000000"
@@ -74,11 +68,10 @@
}
},
"a0ea8a48eb5cb024e66be5777bea75165ffcaf0be82d9047e376ce352e202f76": {
"min_lp_stake": "1",
"decimals": 5,
"name": "XYZ (γ gamma)",
"quantum": "1",
"symbol": "XYZgamma",
"total_supply": "1000000000",
"source": {
"builtin_asset": {
"max_faucet_amount_mint": "100000000000"
@@ -86,11 +79,10 @@
}
},
"9ed5dd08c88e38f1c92cb8f056bd9d0507512c9ea4e6f308936f73646187b8e9": {
"min_lp_stake": "1",
"decimals": 5,
"name": "XYZ (δ delta)",
"quantum": "1",
"symbol": "XYZdelta",
"total_supply": "1000000000",
"source": {
"builtin_asset": {
"max_faucet_amount_mint": "100000000000"
@@ -98,11 +90,10 @@
}
},
"bdcde894dcc4ffa11d6192065d84f9dcc75814cf24760ff4f3e50d64eaddbc02": {
"min_lp_stake": "1",
"decimals": 5,
"name": "XYZ (ε epsilon)",
"quantum": "1",
"symbol": "XYZepsilon",
"total_supply": "1000000000",
"source": {
"builtin_asset": {
"max_faucet_amount_mint": "100000000000"
@@ -110,11 +101,10 @@
}
},
"{{.GetVegaContractID "tBTC"}}": {
"min_lp_stake": "1",
"decimals": 5,
"name": "BTC (local)",
"quantum": "1",
"symbol": "tBTC",
"total_supply": "0",
"source": {
"erc20": {
"contract_address": "{{.GetEthContractAddr "tBTC"}}"
@@ -122,11 +112,10 @@
}
},
"{{.GetVegaContractID "tDAI"}}": {
"min_lp_stake": "1",
"decimals": 5,
"quantum": "1",
"name": "DAI (local)",
"symbol": "tDAI",
"total_supply": "0",
"source": {
"erc20": {
"contract_address": "{{.GetEthContractAddr "tDAI"}}"
@@ -134,11 +123,10 @@
}
},
"{{.GetVegaContractID "tEURO"}}": {
"min_lp_stake": "1",
"quantum": "1",
"decimals": 5,
"name": "EURO (local)",
"symbol": "tEURO",
"total_supply": "0",
"source": {
"erc20": {
"contract_address": "{{.GetEthContractAddr "tEURO"}}"
@@ -146,11 +134,10 @@
}
},
"{{.GetVegaContractID "tUSDC"}}": {
"min_lp_stake": "1",
"quantum": "1",
"decimals": 5,
"name": "USDC (local)",
"symbol": "tUSDC",
"total_supply": "0",
"source": {
"erc20": {
"contract_address": "{{.GetEthContractAddr "tUSDC"}}"
@@ -158,11 +145,10 @@
}
},
"{{.GetVegaContractID "VEGA"}}": {
"min_lp_stake": "1",
"quantum": "1",
"decimals": 18,
"name": "Vega",
"symbol": "VEGA",
"total_supply": "64999723000000000000000000",
"source": {
"erc20": {
"contract_address": "{{.GetEthContractAddr "VEGA"}}"
@@ -171,7 +157,7 @@
}
},
"network": {
"ReplayAttackThreshold": 30
"replay_attack_threshold": 30
},
"network_parameters": {
"blockchains.ethereumConfig": "{\"network_id\": \"{{ .NetworkID }}\", \"chain_id\": \"{{ .ChainID }}\", \"collateral_bridge_contract\": { \"address\": \"{{.GetEthContractAddr "erc20_bridge_1"}}\" }, \"confirmations\": 3, \"staking_bridge_contract\": { \"address\": \"{{.GetEthContractAddr "staking_bridge"}}\", \"deployment_block_height\": 0}, \"token_vesting_contract\": { \"address\": \"{{.GetEthContractAddr "erc20_vesting"}}\", \"deployment_block_height\": 0 }, \"multisig_control_contract\": { \"address\": \"{{.GetEthContractAddr "MultisigControl"}}\", \"deployment_block_height\": 0 }}",
@@ -194,17 +180,17 @@
"market.auction.minimumDuration": "3s",
"market.fee.factors.infrastructureFee": "0.001",
"market.fee.factors.makerFee": "0.004",
"market.stake.target.timeWindow": "1h0m0s",
"market.stake.target.scalingFactor": "10",
"market.liquidity.stakeToCcyVolume": "0.3",
"market.liquidity.targetstake.triggering.ratio": "0.7",
"market.liquidity.providersFeeCalculationTimeStep": "5s",
"network.checkpoint.timeElapsedBetweenCheckpoints": "10s",
"reward.asset": "{{.GetVegaContractID "VEGA"}}",
"reward.staking.delegation.competitionLevel": "3.1",
"reward.staking.delegation.delegatorShare": "0.883",
"reward.staking.delegation.maxPayoutPerParticipant": "700000000000000000000",
"reward.staking.delegation.maxPayoutPerEpoch": "7000000000000000000000",
"reward.staking.delegation.minimumValidatorStake": "3000000000000000000000",
"reward.staking.delegation.payoutDelay": "10s",
"reward.staking.delegation.payoutFraction": "0.007",
"spam.protection.delegation.min.tokens": "1000000000000000000",
"spam.protection.max.delegations": "390",
"spam.protection.max.proposals": "100",
+63
View File
@@ -0,0 +1,63 @@
#!/bin/bash
run_indented() {
local indent=${INDENT:-" "}
{ "$@" 2> >(sed "s/^/$indent/g" >&2); } | sed "s/^/$indent/g"
}
bold=$(tput bold)
normal=$(tput sgr0)
# The name of the application to check.
APPLICATION="vega"
DEBUG=false
for arg in "$@"; do
if [ "$arg" = "debug" ]; then
# Set the flag to false if 'notidyup' is found
DEBUG=true
break
fi
done
echo "${bold}# Setup${normal}"
# Check if the application exists.
if command -v "$APPLICATION" &> /dev/null; then
# Run the application if it exists.
run_indented "$APPLICATION" version
else
# Print an error message if the application doesn't exist.
echo "Error: $APPLICATION is not installed."
# Exit with a non-zero exit code to indicate failure.
exit 1
fi
echo ""
echo "${bold}# Generating genesis${normal}"
run_indented vegacapsule template genesis --path genesis.tmpl > genesis.tmp
run_indented tail -n +3 genesis.tmp > genesis.json
run_indented rm genesis.tmp
if jq empty "genesis.json" &> /dev/null; then
run_indented echo ""
run_indented echo "${bold}The genesis.json file is valid JSON.${normal}"
else
run_indented echo ""
run_indented echo "${bold}The genesis.json file is invalid JSON.${normal}"
# Exit with a non-zero exit code to indicate failure
exit 1
fi
if $DEBUG; then
run_indented cat genesis.json
fi
echo ""
echo "${bold}# Verifying genesis${normal}"
run_indented vega verify genesis genesis.json
echo ""
# Tidy up
if [[ "$DEBUG" == "false" ]]; then
run_indented rm genesis.json
fi
+4 -4
View File
@@ -20500,10 +20500,10 @@ pend@~1.2.0:
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
pennant@^1.14.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/pennant/-/pennant-1.14.1.tgz#8e7a53256095e398b03397af31c831b02a56032b"
integrity sha512-rjzo/tlFanO96OKhJiyjQtjug7sY6pjVZqVbieD3Tf4zt20bqIb6m4L2JfwOXEe0jqP/yddgCNPY+vlkVuJW1Q==
pennant@1.14.0:
version "1.14.0"
resolved "https://registry.yarnpkg.com/pennant/-/pennant-1.14.0.tgz#4100c25a6d836d6f0ff425181fb6f812f9fe5778"
integrity sha512-9H0zWzFUSbD1BlDXnHFmKwkAxXGb1xTxjkUD+RwaMygtSwPXzQEyk2ScVyMqxdcz0RuJmI5HCVmZTOjdr1NwuA==
dependencies:
"@babel/runtime" "^7.13.10"
"@d3fc/d3fc-technical-indicator" "^8.0.1"