Compare commits

...
35 changed files with 956 additions and 416 deletions
@@ -909,6 +909,14 @@
"BenefitTierReferralDiscountFactorDescription": "The proportion of the referee's taker fees to be discounted",
"BenefitTierReferralRewardFactor": "Referral reward factor",
"BenefitTierReferralRewardFactorDescription": "The proportion of the referee's taker fees to be rewarded to the referrer",
"BenefitTierMinimumActivityStreak": "Minimum activity streak",
"BenefitTierMinimumActivityStreakDescription": "The minimum number of times the party needs to have completed the activity",
"BenefitTierMinimumQuantumBalance": "Minimum quantum balance",
"BenefitTierMinimumQuantumBalanceDescription": "The minimum amount of the vesting token to qualify",
"BenefitTierVestingMultiplier": "Vesting multiplier",
"BenefitTierVestingMultiplierDescription": "Vesting multiplier for the tier",
"BenefitTierRewardMultiplier": "Reward multiplier",
"BenefitTierRewardMultiplierDescription": "The multiplier",
"StakingTiers": "Staking tiers",
"StakingTierMinimumStakedTokens": "Minimum staked tokens",
"StakingTierMinimumStakedTokensDescription": "Required number of governance tokens ($VEGA) a referrer must have staked to receive the multiplier",
@@ -0,0 +1 @@
export * from './proposal-update-benefit-tiers-details';
@@ -0,0 +1,158 @@
import { render, screen } from '@testing-library/react';
import { ProposalUpdateBenefitTiers } from './proposal-update-benefit-tiers-details';
import { generateProposal } from '../../test-helpers/generate-proposals';
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
useAppState: () => ({
appState: {
decimals: 2,
},
}),
}));
const mockVestingBenefitTierProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateNetworkParameter',
networkParameter: {
key: 'blah.blah.benefitTiers',
value: JSON.stringify({
tiers: [
{
minimum_quantum_balance: '10000',
reward_multiplier: '0.05',
},
{
minimum_quantum_balance: '500000000000',
reward_multiplier: '0.1',
},
{
minimum_quantum_balance: '10000000000000',
reward_multiplier: '10',
},
],
}),
},
},
},
});
const mockActivityStreakBenefitTierProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateNetworkParameter',
networkParameter: {
key: 'blah.blah.benefitTiers',
value: JSON.stringify({
tiers: [
{
minimum_activity_streak: '10000',
vesting_multiplier: '5',
reward_multiplier: '0.1',
},
{
minimum_activity_streak: '10000000000000',
vesting_multiplier: '100',
reward_multiplier: '10',
},
],
}),
},
},
},
});
describe('ProposalUpdateBenefitTiers', () => {
it('should not render if proposal is null', () => {
render(<ProposalUpdateBenefitTiers proposal={null} />);
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
});
it('should not render if __typename is not UpdateNetworkParameter', () => {
const updateMarketProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateMarket',
},
},
});
render(<ProposalUpdateBenefitTiers proposal={updateMarketProposal} />);
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
});
it('should not render if there are no relevant fields', () => {
const incompleteProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateNetworkParameter',
},
},
});
render(<ProposalUpdateBenefitTiers proposal={incompleteProposal} />);
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
});
it('should not render if there are relevant fields that are empty', () => {
const incompleteProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateNetworkParameter',
networkParameter: {
key: 'blah.blah.benefitTiers',
value: JSON.stringify({}),
},
},
},
});
render(<ProposalUpdateBenefitTiers proposal={incompleteProposal} />);
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
});
it('should render a valid vesting benefit tier proposal', () => {
render(
<ProposalUpdateBenefitTiers proposal={mockVestingBenefitTierProposal} />
);
// 3 tiers in the sample data
expect(screen.getByText('Tier 1')).toBeInTheDocument();
expect(screen.getByText('Tier 2')).toBeInTheDocument();
expect(screen.getByText('Tier 3')).toBeInTheDocument();
expect(screen.getAllByText('Minimum quantum balance').length).toBe(3);
expect(screen.getAllByText('Reward multiplier').length).toBe(3);
expect(screen.getByText('0.00000000000001')).toBeInTheDocument();
expect(screen.getByText('0.05x')).toBeInTheDocument();
expect(screen.getByText('0.0000005')).toBeInTheDocument();
expect(screen.getByText('0.1x')).toBeInTheDocument();
expect(screen.getByText('0.00001')).toBeInTheDocument();
expect(screen.getByText('10x')).toBeInTheDocument();
});
it('should render a valid activity streak benefit tier proposal', () => {
render(
<ProposalUpdateBenefitTiers
proposal={mockActivityStreakBenefitTierProposal}
/>
);
// 3 tiers in the sample data
expect(screen.getByText('Tier 1')).toBeInTheDocument();
expect(screen.getByText('Tier 2')).toBeInTheDocument();
expect(screen.getAllByText('Minimum activity streak').length).toBe(2);
expect(screen.getAllByText('Vesting multiplier').length).toBe(2);
expect(screen.getAllByText('Reward multiplier').length).toBe(2);
expect(screen.getByText('10000')).toBeInTheDocument();
expect(screen.getByText('5x')).toBeInTheDocument();
expect(screen.getByText('0.1x')).toBeInTheDocument();
expect(screen.getByText('10000000000000')).toBeInTheDocument();
expect(screen.getByText('100x')).toBeInTheDocument();
expect(screen.getByText('10x')).toBeInTheDocument();
});
});
@@ -0,0 +1,164 @@
import { useTranslation } from 'react-i18next';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import {
KeyValueTable,
KeyValueTableRow,
RoundedWrapper,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import {
formatMinimumStakedTokens,
formatReferralRewardMultiplier,
} from '../proposal-referral-program-details';
import { formatNumberPercentage } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
// These types are not generated as it's not known how dynamic these are
type VestingBenefitTier = {
minimum_quantum_balance: string;
reward_multiplier: string;
};
type ActivityStreakBenefitTier = {
minimum_activity_streak: number;
reward_multiplier: string;
vesting_multiplier: string;
};
export type BenefitTiers =
| Array<ActivityStreakBenefitTier>
| Array<VestingBenefitTier>;
export function getBenefitTiers(json: string): BenefitTiers {
try {
const parsed = JSON.parse(json);
return parsed.tiers;
} catch (e) {
return [];
}
}
export const formatVolumeDiscountFactor = (value: string) => {
return formatNumberPercentage(new BigNumber(value).times(100));
};
interface ProposalReferralProgramDetailsProps {
proposal: ProposalQuery['proposal'];
}
/**
* Special rendered for network proposals that change any benefit tiers,
* which is detected by:
* 1) it being a network parameter change
* 2) the name of the field ending in `.benefitTiers`
*
* It only renders known fields so that they can be formatted correctly.
*/
export const ProposalUpdateBenefitTiers = ({
proposal,
}: ProposalReferralProgramDetailsProps) => {
const { t } = useTranslation();
if (
proposal?.terms?.change?.__typename !== 'UpdateNetworkParameter' ||
proposal?.terms?.change?.networkParameter.key.slice(-13) !== '.benefitTiers'
) {
return null;
}
const benefitTiersString = proposal?.terms?.change?.networkParameter.value;
const benefitTiers = getBenefitTiers(benefitTiersString);
if (!benefitTiers) {
return null;
}
return (
<div data-testid="proposal-update-benefit-tiers">
<RoundedWrapper paddingBottom={true}>
{benefitTiers && (
<div
className="mb-6"
data-testid="proposal-volume-discount-program-benefit-tiers"
>
<h3 className="mb-3 uppercase font-semibold text-lg">
{t('BenefitTiers')}
</h3>
<KeyValueTable>
{benefitTiers
.sort(
(a, b) =>
Number(a.reward_multiplier) - Number(b.reward_multiplier)
)
.map((benefitTier, index) => (
<div className="mb-4" key={index}>
<h4 className="font-semibold uppercase">
Tier {index + 1}
</h4>
{'minimum_activity_streak' in benefitTier && (
<KeyValueTableRow
data-testid={`mas-${benefitTier.reward_multiplier}`}
>
<Tooltip
description={t(
'BenefitTierMinimumActivityStreakDescription'
)}
>
<span>{t('BenefitTierMinimumActivityStreak')}</span>
</Tooltip>
{benefitTier.minimum_activity_streak}
</KeyValueTableRow>
)}
{'minimum_quantum_balance' in benefitTier && (
<KeyValueTableRow
data-testid={`mqb-${benefitTier.reward_multiplier}`}
>
<Tooltip
description={t(
'BenefitTierMinimumQuantumBalanceDescription'
)}
>
<span>{t('BenefitTierMinimumQuantumBalance')}</span>
</Tooltip>
{formatMinimumStakedTokens(
benefitTier.minimum_quantum_balance,
18
)}
</KeyValueTableRow>
)}
{'vesting_multiplier' in benefitTier && (
<KeyValueTableRow
data-testid={`vm-${benefitTier.reward_multiplier}`}
>
<Tooltip
description={t('BenefitTierVestingMultiplier')}
>
<span>{t('BenefitTierVestingMultiplier')}</span>
</Tooltip>
{formatReferralRewardMultiplier(
benefitTier.vesting_multiplier
)}
</KeyValueTableRow>
)}
{'reward_multiplier' in benefitTier && (
<KeyValueTableRow
data-testid={`rm-${benefitTier.reward_multiplier}`}
>
<Tooltip description={t('BenefitTierRewardMultiplier')}>
<span>{t('BenefitTierRewardMultiplier')}</span>
</Tooltip>
{formatReferralRewardMultiplier(
benefitTier.reward_multiplier
)}
</KeyValueTableRow>
)}
</div>
))}
</KeyValueTable>
</div>
)}
</RoundedWrapper>
</div>
);
};
@@ -27,6 +27,7 @@ import {
ProposalTransferDetails,
} from '../proposal-transfer';
import { FLAGS } from '@vegaprotocol/environment';
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
export interface ProposalProps {
proposal: ProposalQuery['proposal'];
@@ -243,6 +244,14 @@ export const Proposal = ({
</div>
)}
{proposal.terms.change.__typename === 'UpdateNetworkParameter' &&
proposal.terms.change.networkParameter.key.slice(-13) ===
'.benefitTiers' && (
<div className="mb-4">
<ProposalUpdateBenefitTiers proposal={proposal} />
</div>
)}
{governanceTransferDetails}
<div className="mb-10">
+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>
+20 -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,19 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
[create]
);
const assets = useMemo(() => {
if (!data) return [];
return data
.filter(
(account) => account.type === Schema.AccountType.ACCOUNT_TYPE_GENERAL
)
.map((account) => ({
id: account.asset.id,
symbol: account.asset.symbol,
name: account.asset.name,
decimals: account.asset.decimals,
balance: addDecimal(account.balance, account.asset.decimals),
}));
}, [data]);
const accounts = data
? data.filter((account) => ALLOWED_ACCOUNTS.includes(account.type))
: [];
const assets = accounts.map((account) => ({
id: account.asset.id,
symbol: account.asset.symbol,
name: account.asset.name,
decimals: account.asset.decimals,
balance: addDecimal(account.balance, account.asset.decimals),
}));
if (data === null) return null;
return (
<>
@@ -69,6 +73,7 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
assetId={assetId}
feeFactor={param}
submitTransfer={transfer}
accounts={accounts}
/>
</>
);
+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);
+96 -27
View File
@@ -24,22 +24,30 @@ import type { ReactNode } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { AssetOption, Balance } from '@vegaprotocol/assets';
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
interface FormFields {
toAddress: string;
asset: string;
amount: string;
fromAccount: AccountType;
}
interface Asset {
id: string;
symbol: string;
name: string;
decimals: number;
balance: string;
}
interface TransferFormProps {
pubKey: string | null;
pubKeys: string[] | null;
assets: Array<{
id: string;
symbol: string;
name: string;
decimals: number;
balance: string;
assets: Array<Asset>;
accounts: Array<{
type: AccountType;
asset: { id: string; symbol: string };
}>;
assetId?: string;
feeFactor: string | null;
@@ -53,6 +61,7 @@ export const TransferForm = ({
assetId: initialAssetId,
feeFactor,
submitTransfer,
accounts,
}: TransferFormProps) => {
const {
control,
@@ -67,6 +76,7 @@ export const TransferForm = ({
},
});
const selectedPubKey = watch('toAddress');
const amount = watch('amount');
const assetId = watch('asset');
@@ -102,10 +112,16 @@ export const TransferForm = ({
if (!transferAmount) {
throw new Error('Submitted transfer with no amount selected');
}
const transfer = normalizeTransfer(fields.toAddress, transferAmount, {
id: asset.id,
decimals: asset.decimals,
});
const transfer = normalizeTransfer(
fields.toAddress,
transferAmount,
fields.fromAccount,
AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form
{
id: asset.id,
decimals: asset.decimals,
}
);
submitTransfer(transfer);
},
[asset, submitTransfer, transferAmount]
@@ -137,57 +153,53 @@ export const TransferForm = ({
className="text-sm"
data-testid="transfer-form"
>
<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 +240,63 @@ 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]} ({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}>
{asset
? `${AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]} (${
asset.symbol
})`
: AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]}
</option>
</TradingSelect>
</TradingFormGroup>
<TradingFormGroup label="Amount" labelFor="amount">
<TradingInput
id="amount"
@@ -391,7 +460,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
View File
@@ -23,3 +23,4 @@ export * from './lib/type-helpers';
export * from './lib/cells/grid-progress-bar';
export * from './lib/use-datagrid-events';
export * from './lib/pagination';
+51
View File
@@ -0,0 +1,51 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Pagination } from './pagination';
describe('Pagination', () => {
const props = {
pageInfo: {
hasNextPage: true,
},
count: 0,
onLoad: () => undefined,
showRetentionMessage: false,
hasDisplayedRows: false,
};
it('renders message for 0 rows', async () => {
const mockOnLoad = jest.fn();
render(<Pagination {...props} onLoad={mockOnLoad} />);
expect(screen.getByText('0 rows loaded')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: 'Load more' }));
expect(mockOnLoad).toHaveBeenCalled();
});
it('renders mesasge for multiple rows', async () => {
const mockOnLoad = jest.fn();
const count = 10;
render(<Pagination {...props} count={count} onLoad={mockOnLoad} />);
expect(screen.getByText(`${count} rows loaded`)).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: 'Load more' }));
expect(mockOnLoad).toHaveBeenCalled();
});
it('renders message for a single row', async () => {
const mockOnLoad = jest.fn();
const count = 1;
render(<Pagination {...props} count={count} onLoad={mockOnLoad} />);
expect(screen.getByText(`${count} row loaded`)).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: 'Load more' }));
expect(mockOnLoad).toHaveBeenCalled();
});
it('renders the data rentention message', () => {
render(<Pagination {...props} showRetentionMessage={true} />);
expect(screen.getByText(/data node retention/)).toBeInTheDocument();
});
it('renders the row filter message', () => {
render(<Pagination {...props} count={1} hasDisplayedRows={false} />);
expect(screen.getByText(/No rows matching/)).toBeInTheDocument();
});
});
+53
View File
@@ -0,0 +1,53 @@
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
export const Pagination = ({
count,
pageInfo,
onLoad,
hasDisplayedRows,
showRetentionMessage,
}: {
count: number;
pageInfo: { hasNextPage?: boolean } | null;
onLoad: () => void;
hasDisplayedRows: boolean;
showRetentionMessage: boolean;
}) => {
let rowMessage = '';
if (count && !pageInfo?.hasNextPage) {
rowMessage = t('all %s rows loaded', count.toString());
} else {
if (count === 1) {
rowMessage = t('%s row loaded', count.toString());
} else {
rowMessage = t('%s rows loaded', count.toString());
}
}
return (
<div className="flex items-center justify-between p-1 border-t border-default">
<div className="text-xs">
{false}
{showRetentionMessage &&
t(
'Depending on data node retention you may not be able see the "full" history'
)}
</div>
<div className="flex items-center text-xs">
<span>{rowMessage}</span>
{pageInfo?.hasNextPage ? (
<Button size="extra-small" className="ml-1" onClick={onLoad}>
{t('Load more')}
</Button>
) : null}
</div>
{count && hasDisplayedRows === false ? (
<div className="absolute text-xs top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
{t('No rows matching selected filters')}
</div>
) : null}
</div>
);
};
@@ -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;
+8 -26
View File
@@ -3,10 +3,10 @@ import { useCallback, useRef, useState } from 'react';
import { t } from '@vegaprotocol/i18n';
import { FillsTable } from './fills-table';
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
import { Pagination } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type * as Schema from '@vegaprotocol/types';
import { fillsWithMarketProvider } from './fills-data-provider';
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
interface FillsManagerProps {
partyId: string;
@@ -52,31 +52,13 @@ export const FillsManager = ({
overlayNoRowsTemplate={error ? error.message : t('No fills')}
{...props}
/>
<div className="flex justify-between border-t border-default p-1 items-center">
<div className="text-xs">
{t(
'Depending on data node retention you may not be able see the "full" history'
)}
</div>
<div className="flex text-xs items-center">
{data?.length && !pageInfo?.hasNextPage
? t('all %s items loaded', [data.length.toString()])
: t('%s items loaded', [
data?.length ? data.length.toString() : ' ',
])}
{pageInfo?.hasNextPage ? (
<Button size="extra-small" className="ml-1" onClick={() => load()}>
{t('Load more')}
</Button>
) : null}
</div>
{data?.length && hasDisplayedRow === false ? (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
{t('No fills matching selected filters')}
</div>
) : null}
</div>
<Pagination
count={data?.length || 0}
pageInfo={pageInfo}
showRetentionMessage={true}
onLoad={load}
hasDisplayedRows={hasDisplayedRow || false}
/>
</div>
);
};
@@ -2,10 +2,10 @@ import type { AgGridReact } from 'ag-grid-react';
import { useCallback, useRef, useState } from 'react';
import { t } from '@vegaprotocol/i18n';
import { FundingPaymentsTable } from './funding-payments-table';
import { Pagination } from '@vegaprotocol/datagrid';
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { fundingPaymentsWithMarketProvider } from './funding-payments-data-provider';
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
interface FundingPaymentsManagerProps {
partyId: string;
@@ -57,30 +57,13 @@ export const FundingPaymentsManager = ({
overlayNoRowsTemplate={error ? error.message : t('No funding payments')}
{...props}
/>
<div className="flex justify-between border-t border-default p-1 items-center">
<div className="text-xs">
{t(
'Depending on data node retention you may not be able see the "full" history'
)}
</div>
<div className="flex text-xs items-center">
{data?.length && !pageInfo?.hasNextPage
? t('all %s items loaded', [data.length.toString()])
: t('%s items loaded', [
data?.length ? data.length.toString() : ' ',
])}
{pageInfo?.hasNextPage ? (
<Button size="extra-small" className="ml-1" onClick={() => load()}>
{t('Load more')}
</Button>
) : null}
</div>
{data?.length && hasDisplayedRow === false ? (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
{t('No funding payments matching selected filters')}
</div>
) : null}
</div>
<Pagination
count={data?.length || 0}
pageInfo={pageInfo}
onLoad={load}
hasDisplayedRows={hasDisplayedRow || false}
showRetentionMessage={true}
/>
</div>
);
};
@@ -21,7 +21,10 @@ import type {
} from './__generated__/MarketLiquidity';
export type LiquidityProvisionFields = LiquidityProvisionFieldsFragment &
Schema.LiquiditySLAParameters;
Schema.LiquiditySLAParameters & {
currentCommitmentAmount?: string;
currentFee?: string;
};
export const liquidityProvisionsDataProvider = makeDataProvider<
LiquidityProvisionsQuery,
@@ -34,10 +37,28 @@ export const liquidityProvisionsDataProvider = makeDataProvider<
getData: (responseData: LiquidityProvisionsQuery | null) => {
return (responseData?.market?.liquidityProvisions?.edges
?.filter((n) => !!n)
.map((e) => ({
...e?.node.current,
...responseData.market?.liquiditySLAParameters,
})) ?? []) as LiquidityProvisionFields[];
.map((e) => {
let node;
if (!e?.node.pending && e?.node.current) {
node = {
...e?.node.current,
...responseData.market?.liquiditySLAParameters,
};
} else if (!e?.node.current && e?.node.pending) {
node = {
...e?.node.pending,
...responseData.market?.liquiditySLAParameters,
};
} else {
node = {
...e?.node.pending,
currentCommitmentAmount: e?.node.current.commitmentAmount,
currentFee: e?.node.current.fee,
...responseData.market?.liquiditySLAParameters,
};
}
return node;
}) ?? []) as LiquidityProvisionFields[];
},
});
@@ -91,7 +112,8 @@ export const matchFilter = (filter: Filter, lp: LiquidityProvisionData) => {
}
if (
filter.active === true &&
lp.status !== Schema.LiquidityProvisionStatus.STATUS_ACTIVE
lp.status !== Schema.LiquidityProvisionStatus.STATUS_ACTIVE &&
lp.status !== Schema.LiquidityProvisionStatus.STATUS_PENDING
) {
return false;
}
@@ -105,7 +127,7 @@ export const matchFilter = (filter: Filter, lp: LiquidityProvisionData) => {
};
export interface LiquidityProvisionData
extends Omit<LiquidityProvisionFieldsFragment, '__typename'>,
extends Omit<LiquidityProvisionFields, '__typename'>,
Partial<LiquidityProviderFieldsFragment>,
Omit<Schema.LiquiditySLAParameters, '__typename'> {
assetDecimalPlaces?: number;
@@ -113,11 +135,12 @@ export interface LiquidityProvisionData
averageEntryValuation?: string;
equityLikeShare?: string;
earmarkedFees?: number;
status: Schema.LiquidityProvisionStatus;
}
export const getLiquidityProvision = (
liquidityProvisions: LiquidityProvisionFields[],
liquidityProvider: LiquidityProviderFieldsFragment[],
liquidityProviders: LiquidityProviderFieldsFragment[],
filter?: Filter
): LiquidityProvisionData[] => {
return liquidityProvisions
@@ -136,12 +159,14 @@ export const getLiquidityProvision = (
}
return true;
})
.map((lp) => {
const lpObj = liquidityProvider.find((f) => lp.party.id === f.partyId);
if (!lpObj) return lp;
const accounts = compact(lp.party.accountsConnection?.edges).map(
(e) => e.node
.map((liquidityProvision) => {
const liquidityProvider = liquidityProviders.find(
(f) => liquidityProvision.party.id === f.partyId
);
if (!liquidityProvider) return liquidityProvision;
const accounts = compact(
liquidityProvision.party.accountsConnection?.edges
).map((e) => e.node);
const bondAccounts = accounts?.filter(
(a) => a?.type === Schema.AccountType.ACCOUNT_TYPE_BOND
);
@@ -164,8 +189,8 @@ export const getLiquidityProvision = (
)
.toNumber() ?? 0;
return {
...lp,
...lpObj,
...liquidityProvision,
...liquidityProvider,
balance,
earmarkedFees,
__typename: undefined,
+57 -5
View File
@@ -6,7 +6,10 @@ import {
getDateTimeFormat,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
import type {
TypedDataAgGrid,
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { AgGrid } from '@vegaprotocol/datagrid';
import {
CopyWithTooltip,
@@ -22,7 +25,7 @@ import type {
ValueFormatterParams,
} from 'ag-grid-community';
import BigNumber from 'bignumber.js';
import type { LiquidityProvisionStatus } from '@vegaprotocol/types';
import { LiquidityProvisionStatus } from '@vegaprotocol/types';
import { LiquidityProvisionStatusMapping } from '@vegaprotocol/types';
import type { LiquidityProvisionData } from './liquidity-data-provider';
@@ -187,7 +190,32 @@ export const LiquidityTable = ({
headerTooltip: t(
'The amount committed to the market by this liquidity provider.'
),
valueFormatter: assetDecimalsQuantumFormatter,
valueFormatter: ({
data,
value,
}: VegaValueFormatterParams<
LiquidityProvisionData,
'commitmentAmount'
>) => {
if (!value) return '-';
const formattedCommitmentAmount = addDecimalsFormatNumberQuantum(
value,
assetDecimalPlaces ?? 0,
quantum ?? 0
);
if (
data?.currentCommitmentAmount &&
data?.currentCommitmentAmount !== value
) {
return `${addDecimalsFormatNumberQuantum(
data.currentCommitmentAmount,
assetDecimalPlaces ?? 0,
quantum ?? 0
)}/${formattedCommitmentAmount}`;
} else {
return formattedCommitmentAmount;
}
},
tooltipValueGetter: assetDecimalsFormatter,
},
{
@@ -207,7 +235,22 @@ export const LiquidityTable = ({
),
field: 'fee',
type: 'rightAligned',
valueFormatter: percentageFormatter,
valueFormatter: ({
data,
value,
}: ValueFormatterParams<LiquidityProvisionData, 'fee'>) => {
if (!value) return '-';
const formattedValue =
formatNumberPercentage(new BigNumber(value).times(100), 2) ||
'-';
if (data?.currentFee && data?.currentFee !== value) {
return `${formatNumberPercentage(
new BigNumber(data.currentFee).times(100),
2
)}/${formattedValue}`;
}
return formattedValue;
},
},
{
headerName: t('Adjusted stake share'),
@@ -328,8 +371,17 @@ export const LiquidityTable = ({
headerName: t('Status'),
headerTooltip: t('The current status of this liquidity provision.'),
field: 'status',
valueFormatter: ({ value }) => {
valueFormatter: ({
data,
value,
}: ValueFormatterParams<LiquidityProvisionData, 'status'>) => {
if (!value) return value;
if (
data?.status === LiquidityProvisionStatus.STATUS_PENDING &&
(data?.currentCommitmentAmount || data?.currentFee)
) {
return t('Updating next epoch');
}
return LiquidityProvisionStatusMapping[
value as LiquidityProvisionStatus
];
+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
+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
@@ -3,6 +3,7 @@ import { useCallback, useRef, useState, useEffect } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import { OrderListTable } from '../order-list';
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
import { Pagination } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { ordersWithMarketProvider } from '../order-data-provider/order-data-provider';
import { normalizeOrderAmendment } from '@vegaprotocol/wallet';
@@ -11,7 +12,7 @@ import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/web3';
import { OrderEditDialog } from '../order-list/order-edit-dialog';
import type { Order } from '../order-data-provider';
import { OrderViewDialog } from '../order-list/order-view-dialog';
import { Splash, TradingButton as Button } from '@vegaprotocol/ui-toolkit';
import { Splash } from '@vegaprotocol/ui-toolkit';
export enum Filter {
'Open' = 'Open',
@@ -90,61 +91,34 @@ export const OrderListManager = ({
return (
<>
<div className="h-full relative">
<div className="flex flex-col h-full">
<OrderListTable
rowData={data}
ref={gridRef}
filter={filter}
onCancel={cancel}
onEdit={setEditOrder}
onView={setViewOrder}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
onFilterChanged={(event) => {
onRowDataUpdated(event);
if (onFilterChanged) {
onFilterChanged(event);
}
}}
onRowDataUpdated={onRowDataUpdated}
isReadOnly={isReadOnly}
overlayNoRowsTemplate={noRowsMessage || t('No orders')}
{...props}
/>
<div className="flex justify-between border-t border-default p-1 items-center">
<div className="text-xs">
{variables.filter?.liveOnly
? null
: t(
'Depending on data node retention you may not be able see the "full" history'
)}
</div>
{data ? (
<div className="flex text-xs items-center">
{data?.length && !pageInfo?.hasNextPage
? t('all %s items loaded', [data.length.toString()])
: t('%s items loaded', [
data?.length ? data.length.toString() : ' ',
])}
{pageInfo?.hasNextPage ? (
<Button
size="extra-small"
className="ml-1"
onClick={() => load()}
>
{t('Load more')}
</Button>
) : null}
</div>
) : null}
{data?.length && hasDisplayedRow === false ? (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
{t('No orders matching selected filters')}
</div>
) : null}
</div>
</div>
<div className="relative flex flex-col h-full">
<OrderListTable
rowData={data}
ref={gridRef}
filter={filter}
onCancel={cancel}
onEdit={setEditOrder}
onView={setViewOrder}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
onFilterChanged={(event) => {
onRowDataUpdated(event);
if (onFilterChanged) {
onFilterChanged(event);
}
}}
onRowDataUpdated={onRowDataUpdated}
isReadOnly={isReadOnly}
overlayNoRowsTemplate={noRowsMessage || t('No orders')}
{...props}
/>
<Pagination
count={data?.length || 0}
pageInfo={pageInfo}
onLoad={load}
hasDisplayedRows={hasDisplayedRow || false}
showRetentionMessage={variables.filter?.liveOnly || true}
/>
</div>
{editOrder && (
<OrderEditDialog
+8 -25
View File
@@ -3,10 +3,10 @@ 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';
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
interface TradesContainerProps {
marketId: string;
@@ -49,30 +49,13 @@ export const TradesManager = ({
overlayNoRowsTemplate={error ? error.message : t('No trades')}
{...props}
/>
<div className="flex justify-between border-t border-default p-1 items-center">
<div className="text-xs">
{t(
'Depending on data node retention you may not be able see the "full" history'
)}
</div>
<div className="flex text-xs items-center">
{data?.length && !pageInfo?.hasNextPage
? t('all %s items loaded', [data.length.toString()])
: t('%s items loaded', [
data?.length ? data.length.toString() : ' ',
])}
{pageInfo?.hasNextPage ? (
<Button size="extra-small" className="ml-1" onClick={() => load()}>
{t('Load more')}
</Button>
) : null}
</div>
{data?.length && hasDisplayedRow === false ? (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
{t('No trades matching selected filters')}
</div>
) : null}
</div>
<Pagination
count={data?.length || 0}
pageInfo={pageInfo}
onLoad={load}
hasDisplayedRows={hasDisplayedRow || false}
showRetentionMessage={true}
/>
</div>
);
};
@@ -72,7 +72,7 @@ LessThan24HoursIncrease.args = {
export const LessThan24HoursDecrease = Template.bind({});
LessThan24HoursDecrease.args = {
data: [20, 21, 22, 23, 24, 6, 7, 9, 11, 13, 11, 9],
data: [20990000, 20939973, 20980130],
width: 110,
height: 30,
};
@@ -44,14 +44,12 @@ export const SparklineView = ({
return null;
}
const midValue = (min + max) / 2;
// Market may be less than 24hr old so padd the data array
// with values that is the mid value (avg of min and max).
// This will rendera horizontal line until the real data shifts the line
const padCount = data.length < points ? points - data.length : 0;
const padArr = new Array(padCount).fill(midValue);
const trimmedData = data.slice(-points);
const padCount = data.length < points ? points - data.length : 0;
const padArr = new Array(padCount).fill(trimmedData[0]);
// Get the last 24 values if data has more than needed
const lineData: [number, number][] = [...padArr, ...trimmedData].map(
+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
+4 -2
View File
@@ -1,2 +1,4 @@
[functions]
included_files = ["!node_modules/@sentry/cli/sentry-cli"]
[[redirects]]
from = "/*"
to = "/index.html"
status = 200