Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d35cf0fc0 | ||
|
|
088838d1ca | ||
|
|
cf66ba40ef | ||
|
|
955427c2ed | ||
|
|
73f37c2477 | ||
|
|
607ad06971 | ||
|
|
7aee2a3a7b | ||
|
|
1fdf519780 | ||
|
|
9233778e85 |
@@ -8,6 +8,7 @@ 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';
|
||||
@@ -102,7 +103,10 @@ export const usePollForDelegations = () => {
|
||||
setAccounts(
|
||||
accounts
|
||||
.filter(
|
||||
(a) => a.type === Schema.AccountType.ACCOUNT_TYPE_GENERAL
|
||||
(a) =>
|
||||
a.type === Schema.AccountType.ACCOUNT_TYPE_GENERAL ||
|
||||
a.type === Schema.AccountType.ACCOUNT_TYPE_VESTED_REWARDS ||
|
||||
a.type === Schema.AccountType.ACCOUNT_TYPE_VESTING_REWARDS
|
||||
)
|
||||
.map((a) => {
|
||||
const isVega =
|
||||
@@ -115,14 +119,23 @@ 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 : noIcon,
|
||||
image: isVega
|
||||
? vegaBlack
|
||||
: a.type ===
|
||||
Schema.AccountType.ACCOUNT_TYPE_VESTED_REWARDS ||
|
||||
a.type ===
|
||||
Schema.AccountType.ACCOUNT_TYPE_VESTING_REWARDS
|
||||
? vegaVesting
|
||||
: noIcon,
|
||||
border: isVega,
|
||||
address: isAssetTypeERC20(a.asset)
|
||||
? a.asset.source.contractAddress
|
||||
: undefined,
|
||||
type: a.type,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } 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;
|
||||
@@ -100,8 +109,10 @@ export interface WalletCardAssetProps {
|
||||
symbol: string;
|
||||
balance: BigNumber;
|
||||
decimals: number;
|
||||
assetId?: string;
|
||||
border?: boolean;
|
||||
subheading?: string;
|
||||
type?: Schema.AccountType;
|
||||
}
|
||||
|
||||
export const WalletCardAsset = ({
|
||||
@@ -110,16 +121,37 @@ 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 mt-2 mb-4">
|
||||
<div className="flex flex-nowrap gap-2 mt-2 mb-4">
|
||||
<img
|
||||
alt="Vega"
|
||||
src={image}
|
||||
@@ -129,15 +161,37 @@ export const WalletCardAsset = ({
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
className="flex align-center text-base"
|
||||
className="flex align-center items-baseline text-base gap-2"
|
||||
data-testid="currency-title"
|
||||
>
|
||||
<div className="mb-0 px-2 uppercase">{name}</div>
|
||||
<div className="mb-0 uppercase">{name}</div>
|
||||
<div className="mb-0 uppercase text-neutral-400">
|
||||
{subheading || symbol}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-2 basis-full font-mono" data-testid="currency-value">
|
||||
{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">
|
||||
<span>
|
||||
{integers}
|
||||
{separator}
|
||||
|
||||
@@ -953,5 +953,8 @@
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ jest.mock('@vegaprotocol/accounts', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../components/welcome-dialog/get-started.ts', () => ({
|
||||
jest.mock('../../components/welcome-dialog/get-started', () => ({
|
||||
GetStarted: () => <div>GetStarted</div>,
|
||||
}));
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ jest.mock('../../components/withdraw-container', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../components/welcome-dialog/get-started.ts', () => ({
|
||||
jest.mock('../../components/welcome-dialog/get-started', () => ({
|
||||
GetStarted: () => <div>GetStarted</div>,
|
||||
}));
|
||||
|
||||
|
||||
@@ -36,6 +36,22 @@ query Fees(
|
||||
}
|
||||
}
|
||||
}
|
||||
referrer: referralSets(referrer: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
referrer
|
||||
}
|
||||
}
|
||||
}
|
||||
referee: referralSets(referee: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
referrer
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetReferees(referee: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
|
||||
+17
-1
@@ -15,7 +15,7 @@ export type FeesQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type FeesQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string }, volumeDiscountStats: { __typename?: 'VolumeDiscountStatsConnection', edges: Array<{ __typename?: 'VolumeDiscountStatsEdge', node: { __typename?: 'VolumeDiscountStats', atEpoch: number, discountFactor: string, runningVolume: string } } | null> }, referralSetReferees: { __typename?: 'ReferralSetRefereeConnection', edges: Array<{ __typename?: 'ReferralSetRefereeEdge', node: { __typename?: 'ReferralSetReferee', atEpoch: number } } | null> }, referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, discountFactor: string, referralSetRunningNotionalTakerVolume: string } } | null> } };
|
||||
export type FeesQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string }, volumeDiscountStats: { __typename?: 'VolumeDiscountStatsConnection', edges: Array<{ __typename?: 'VolumeDiscountStatsEdge', node: { __typename?: 'VolumeDiscountStats', atEpoch: number, discountFactor: string, runningVolume: string } } | null> }, referrer: { __typename?: 'ReferralSetConnection', edges: Array<{ __typename?: 'ReferralSetEdge', node: { __typename?: 'ReferralSet', id: string, referrer: string } } | null> }, referee: { __typename?: 'ReferralSetConnection', edges: Array<{ __typename?: 'ReferralSetEdge', node: { __typename?: 'ReferralSet', id: string, referrer: string } } | null> }, referralSetReferees: { __typename?: 'ReferralSetRefereeConnection', edges: Array<{ __typename?: 'ReferralSetRefereeEdge', node: { __typename?: 'ReferralSetReferee', atEpoch: number } } | null> }, referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, discountFactor: string, referralSetRunningNotionalTakerVolume: string } } | null> } };
|
||||
|
||||
|
||||
export const DiscountProgramsDocument = gql`
|
||||
@@ -81,6 +81,22 @@ export const FeesDocument = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
referrer: referralSets(referrer: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
referrer
|
||||
}
|
||||
}
|
||||
}
|
||||
referee: referralSets(referee: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
referrer
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetReferees(referee: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
|
||||
@@ -17,6 +17,14 @@ import { useReferralStats } from './use-referral-stats';
|
||||
import { formatPercentage, getAdjustedFee } from './utils';
|
||||
import { Table, Td, Th, THead, Tr } from './table';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { Links } from '../../lib/links';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Tooltip,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
truncateMiddle,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const FeesContainer = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
@@ -56,16 +64,25 @@ export const FeesContainer = () => {
|
||||
referralTierIndex,
|
||||
referralTiers,
|
||||
epochsInSet,
|
||||
code,
|
||||
isReferrer,
|
||||
} = useReferralStats(
|
||||
feesData?.referralSetStats,
|
||||
feesData?.referralSetReferees,
|
||||
programData?.currentReferralProgram,
|
||||
feesData?.epoch
|
||||
feesData?.epoch,
|
||||
feesData?.referrer,
|
||||
feesData?.referee
|
||||
);
|
||||
|
||||
const loading = paramsLoading || feesLoading || programLoading;
|
||||
const isConnected = Boolean(pubKey);
|
||||
|
||||
const isReferralProgramRunning = Boolean(programData?.currentReferralProgram);
|
||||
const isVolumeDiscountProgramRunning = Boolean(
|
||||
programData?.currentVolumeDiscountProgram
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid auto-rows-min grid-cols-4 gap-3">
|
||||
{isConnected && (
|
||||
@@ -90,6 +107,8 @@ export const FeesContainer = () => {
|
||||
<TotalDiscount
|
||||
referralDiscount={referralDiscount}
|
||||
volumeDiscount={volumeDiscount}
|
||||
isReferralProgramRunning={isReferralProgramRunning}
|
||||
isVolumeDiscountProgramRunning={isVolumeDiscountProgramRunning}
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
@@ -97,23 +116,37 @@ export const FeesContainer = () => {
|
||||
className="sm:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<CurrentVolume
|
||||
tiers={volumeTiers}
|
||||
tierIndex={volumeTierIndex}
|
||||
windowLengthVolume={volumeInWindow}
|
||||
windowLength={volumeDiscountWindowLength}
|
||||
/>
|
||||
{isVolumeDiscountProgramRunning ? (
|
||||
<CurrentVolume
|
||||
tiers={volumeTiers}
|
||||
tierIndex={volumeTierIndex}
|
||||
windowLengthVolume={volumeInWindow}
|
||||
windowLength={volumeDiscountWindowLength}
|
||||
/>
|
||||
) : (
|
||||
<p className="pt-3 text-sm text-muted">
|
||||
{t('No volume discount program active')}
|
||||
</p>
|
||||
)}
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
title={t('Referral benefits')}
|
||||
className="sm:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<ReferralBenefits
|
||||
setRunningNotionalTakerVolume={referralVolumeInWindow}
|
||||
epochsInSet={epochsInSet}
|
||||
epochs={referralDiscountWindowLength}
|
||||
/>
|
||||
{isReferrer ? (
|
||||
<ReferrerInfo code={code} />
|
||||
) : isReferralProgramRunning ? (
|
||||
<ReferralBenefits
|
||||
setRunningNotionalTakerVolume={referralVolumeInWindow}
|
||||
epochsInSet={epochsInSet}
|
||||
epochs={referralDiscountWindowLength}
|
||||
/>
|
||||
) : (
|
||||
<p className="pt-3 text-sm text-muted">
|
||||
{t('No referral program active')}
|
||||
</p>
|
||||
)}
|
||||
</FeeCard>
|
||||
</>
|
||||
)}
|
||||
@@ -142,7 +175,7 @@ export const FeesContainer = () => {
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
title={t('Liquidity fees')}
|
||||
title={t('Fees by market')}
|
||||
className="lg:col-span-full"
|
||||
loading={marketsLoading}
|
||||
>
|
||||
@@ -325,26 +358,64 @@ const ReferralBenefits = ({
|
||||
const TotalDiscount = ({
|
||||
referralDiscount,
|
||||
volumeDiscount,
|
||||
isReferralProgramRunning,
|
||||
isVolumeDiscountProgramRunning,
|
||||
}: {
|
||||
referralDiscount: number;
|
||||
volumeDiscount: number;
|
||||
isReferralProgramRunning: boolean;
|
||||
isVolumeDiscountProgramRunning: boolean;
|
||||
}) => {
|
||||
const totalDiscount = 1 - (1 - volumeDiscount) * (1 - referralDiscount);
|
||||
const totalDiscountDescription = t(
|
||||
'The total discount is calculated according to the following formula: '
|
||||
);
|
||||
const formula = (
|
||||
<span className="italic">
|
||||
1 - (1 - d<sub>volume</sub>) ⋇ (1 - d<sub>referral</sub>)
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Stat
|
||||
value={formatPercentage(referralDiscount + volumeDiscount) + '%'}
|
||||
description={
|
||||
<>
|
||||
{totalDiscountDescription}
|
||||
{formula}
|
||||
</>
|
||||
}
|
||||
value={formatPercentage(totalDiscount) + '%'}
|
||||
highlight={true}
|
||||
/>
|
||||
<table className="w-full mt-0.5 text-xs text-muted">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th className="font-normal text-left">{t('Volume discount')}</th>
|
||||
<td className="text-right">{formatPercentage(volumeDiscount)}%</td>
|
||||
<td className="text-right">
|
||||
{formatPercentage(volumeDiscount)}%
|
||||
{!isVolumeDiscountProgramRunning && (
|
||||
<Tooltip description={t('No active volume discount programme')}>
|
||||
<span className="cursor-help">
|
||||
{' '}
|
||||
<VegaIcon name={VegaIconNames.INFO} size={12} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="font-normal text-left ">{t('Referral discount')}</th>
|
||||
<td className="text-right">
|
||||
{formatPercentage(referralDiscount)}%
|
||||
{!isReferralProgramRunning && (
|
||||
<Tooltip description={t('No active referral programme')}>
|
||||
<span className="cursor-help">
|
||||
{' '}
|
||||
<VegaIcon name={VegaIconNames.INFO} size={12} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -491,3 +562,31 @@ const YourTier = () => {
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const ReferrerInfo = ({ code }: { code?: string }) => (
|
||||
<div className="pt-3 text-sm text-vega-clight-200 dark:vega-cdark-200">
|
||||
<p className="mb-1">
|
||||
{t('Connected key is owner of the referral set')}
|
||||
{code && (
|
||||
<>
|
||||
{' '}
|
||||
<span className="text-transparent bg-rainbow bg-clip-text">
|
||||
{truncateMiddle(code)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{'. '}
|
||||
{t('As owner, it is eligible for commission not fee discounts.')}
|
||||
</p>
|
||||
<p>
|
||||
{t('See')}{' '}
|
||||
<Link
|
||||
className="underline text-black dark:text-white"
|
||||
to={Links.REFERRALS()}
|
||||
>
|
||||
{t('Referrals')}
|
||||
</Link>{' '}
|
||||
{t('for more information.')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export const Stat = ({
|
||||
value,
|
||||
text,
|
||||
highlight,
|
||||
description,
|
||||
}: {
|
||||
value: string | number;
|
||||
text?: string;
|
||||
highlight?: boolean;
|
||||
description?: ReactNode;
|
||||
}) => {
|
||||
const val = (
|
||||
<span
|
||||
className={classNames('inline-block text-3xl leading-none', {
|
||||
'text-transparent bg-rainbow bg-clip-text': highlight,
|
||||
'cursor-help': description,
|
||||
})}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
<p className="pt-3 leading-none first:pt-6">
|
||||
<span
|
||||
className={classNames('inline-block text-3xl leading-none', {
|
||||
'text-transparent bg-rainbow bg-clip-text': highlight,
|
||||
})}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
{description ? <Tooltip description={description}>{val}</Tooltip> : val}
|
||||
{text && (
|
||||
<small className="block mt-0.5 text-xs text-muted">{text}</small>
|
||||
)}
|
||||
|
||||
@@ -73,6 +73,8 @@ describe('useReferralStats', () => {
|
||||
referralTierIndex: -1,
|
||||
referralTiers: [],
|
||||
epochsInSet: 0,
|
||||
code: undefined,
|
||||
isReferrer: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,6 +95,8 @@ describe('useReferralStats', () => {
|
||||
referralTierIndex: 1,
|
||||
referralTiers: program.benefitTiers,
|
||||
epochsInSet: Number(epoch.id) - set.atEpoch,
|
||||
code: undefined,
|
||||
isReferrer: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,12 +2,15 @@ import compact from 'lodash/compact';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
import { getReferralBenefitTier } from './utils';
|
||||
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
|
||||
import { first } from 'lodash';
|
||||
|
||||
export const useReferralStats = (
|
||||
setStats?: FeesQuery['referralSetStats'],
|
||||
setReferees?: FeesQuery['referralSetReferees'],
|
||||
program?: DiscountProgramsQuery['currentReferralProgram'],
|
||||
epoch?: FeesQuery['epoch']
|
||||
epoch?: FeesQuery['epoch'],
|
||||
setIfReferrer?: FeesQuery['referrer'],
|
||||
setIfReferee?: FeesQuery['referee']
|
||||
) => {
|
||||
const referralTiers = program?.benefitTiers || [];
|
||||
|
||||
@@ -18,9 +21,18 @@ export const useReferralStats = (
|
||||
referralTierIndex: -1,
|
||||
referralTiers,
|
||||
epochsInSet: 0,
|
||||
code: undefined,
|
||||
isReferrer: false,
|
||||
};
|
||||
}
|
||||
|
||||
const setIfReferrerData = first(
|
||||
compact(setIfReferrer?.edges).map((e) => e.node)
|
||||
);
|
||||
const setIfRefereeData = first(
|
||||
compact(setIfReferee?.edges).map((e) => e.node)
|
||||
);
|
||||
|
||||
const referralSetsStats = compact(setStats.edges).map((e) => e.node);
|
||||
const referralSets = compact(setReferees.edges).map((e) => e.node);
|
||||
|
||||
@@ -48,5 +60,7 @@ export const useReferralStats = (
|
||||
referralTierIndex,
|
||||
referralTiers,
|
||||
epochsInSet,
|
||||
code: (setIfReferrerData || setIfRefereeData)?.id,
|
||||
isReferrer: Boolean(setIfReferrerData),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('getAdjustedFee', () => {
|
||||
new BigNumber(referralDiscount),
|
||||
];
|
||||
|
||||
// 1 - 0.5 - 0.5
|
||||
// 1 - 0.5 = 0.5
|
||||
const v = new BigNumber(1).minus(new BigNumber(volumeDiscount));
|
||||
|
||||
// 1 - 0.5 = 0.5
|
||||
@@ -34,13 +34,15 @@ describe('getAdjustedFee', () => {
|
||||
// 0.1 + 0.1 + 0.1 = 0.3
|
||||
const totalFees = fees.reduce((sum, x) => sum.plus(x), new BigNumber(0));
|
||||
|
||||
// 0.3 * 0.75 = 0.225
|
||||
const expected = new BigNumber(totalFees).times(factor).toNumber();
|
||||
// (1 - 0.3) * 0.75 = 0.525
|
||||
const expected = new BigNumber(totalFees)
|
||||
.times(new BigNumber(1).minus(factor))
|
||||
.toNumber();
|
||||
|
||||
expect(getAdjustedFee(fees, discounts)).toBe(expected);
|
||||
});
|
||||
|
||||
it('combines discount factors multiplicativly', () => {
|
||||
it('combines discount factors multiplicatively', () => {
|
||||
const volumeDiscount = 0.4;
|
||||
const referralDiscount = 0.1;
|
||||
|
||||
@@ -67,7 +69,9 @@ describe('getAdjustedFee', () => {
|
||||
// summed fees
|
||||
const totalFees = fees.reduce((sum, x) => sum.plus(x), new BigNumber(0));
|
||||
|
||||
const expected = new BigNumber(totalFees).times(factor).toNumber();
|
||||
const expected = new BigNumber(totalFees)
|
||||
.times(new BigNumber(1).minus(factor))
|
||||
.toNumber();
|
||||
|
||||
expect(getAdjustedFee(fees, discounts)).toBe(expected);
|
||||
});
|
||||
|
||||
@@ -12,7 +12,9 @@ export const formatPercentage = (num: number) => {
|
||||
const pct = new BigNumber(num).times(100);
|
||||
const dps = pct.decimalPlaces();
|
||||
const formatter = new Intl.NumberFormat(getUserLocale(), {
|
||||
minimumFractionDigits: dps || 0,
|
||||
// set to 0 in order to remove the "trailing zeroes" for numbers such as:
|
||||
// 0.123456789 -non-zero-min-> 12.3456800% -zero-min-> 12.34568%
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: dps || 0,
|
||||
});
|
||||
return formatter.format(parseFloat(pct.toFixed(5)));
|
||||
@@ -101,5 +103,7 @@ export const getAdjustedFee = (fees: BigNumber[], discounts: BigNumber[]) => {
|
||||
|
||||
const totalFactor = new BigNumber(1).minus(combinedFactors);
|
||||
|
||||
return totalFee.times(BigNumber.max(0, totalFactor)).toNumber();
|
||||
return totalFee
|
||||
.times(new BigNumber(1).minus(BigNumber.max(0, totalFactor)))
|
||||
.toNumber();
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/bin/bash -e
|
||||
yarn --pure-lockfile
|
||||
app=${1:-trading}
|
||||
envCmd="envCmd="yarn env-cmd -f ./apps/${app}/.env.${2:-mainnet}"
|
||||
envCmd="envCmd="yarn -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}
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
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 {
|
||||
AddressField,
|
||||
TransferFee,
|
||||
TransferForm,
|
||||
type TransferFormProps,
|
||||
} from './transfer-form';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import { removeDecimal } from '@vegaprotocol/utils';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
|
||||
describe('TransferForm', () => {
|
||||
const renderComponent = (props: TransferFormProps) => {
|
||||
return render(
|
||||
// Wrap with mock provider as the form will make queries to fetch the selected
|
||||
// toVegaKey accounts. We don't test this for now but we need to wrap so that
|
||||
// the component has access to the client
|
||||
<MockedProvider>
|
||||
<TransferForm {...props} />
|
||||
</MockedProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', { name: 'Confirm transfer' })
|
||||
@@ -66,7 +83,7 @@ describe('TransferForm', () => {
|
||||
// 1003-TRAN-017
|
||||
// 1003-TRAN-018
|
||||
// 1003-TRAN-019
|
||||
render(<TransferForm {...props} />);
|
||||
renderComponent(props);
|
||||
// Select a pubkey
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('To Vega key'),
|
||||
@@ -113,7 +130,7 @@ describe('TransferForm', () => {
|
||||
// 1003-TRAN-012
|
||||
// 1003-TRAN-013
|
||||
// 1003-TRAN-004
|
||||
render(<TransferForm {...props} />);
|
||||
renderComponent(props);
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3); // pubkey is set as default value
|
||||
const toggle = screen.getByText('Enter manually');
|
||||
@@ -139,7 +156,7 @@ describe('TransferForm', () => {
|
||||
// 1002-WITH-010
|
||||
// 1003-TRAN-011
|
||||
// 1003-TRAN-014
|
||||
render(<TransferForm {...props} />);
|
||||
renderComponent(props);
|
||||
|
||||
// check current pubkey not shown
|
||||
const keySelect = screen.getByLabelText<HTMLSelectElement>('To Vega key');
|
||||
@@ -206,7 +223,7 @@ describe('TransferForm', () => {
|
||||
describe('IncludeFeesCheckbox', () => {
|
||||
it('validates fields and submits when checkbox is checked', async () => {
|
||||
const mockSubmit = jest.fn();
|
||||
render(<TransferForm {...props} submitTransfer={mockSubmit} />);
|
||||
renderComponent({ ...props, submitTransfer: mockSubmit });
|
||||
|
||||
// check current pubkey not shown
|
||||
const keySelect = screen.getByLabelText<HTMLSelectElement>('To Vega key');
|
||||
@@ -275,7 +292,7 @@ describe('TransferForm', () => {
|
||||
});
|
||||
|
||||
it('validates fields when checkbox is not checked', async () => {
|
||||
render(<TransferForm {...props} />);
|
||||
renderComponent(props);
|
||||
|
||||
// check current pubkey not shown
|
||||
const keySelect: HTMLSelectElement = screen.getByLabelText('To Vega key');
|
||||
|
||||
@@ -27,6 +27,8 @@ 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';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { accountsDataProvider } from './accounts-data-provider';
|
||||
|
||||
interface FormFields {
|
||||
toVegaKey: string;
|
||||
@@ -35,7 +37,7 @@ interface FormFields {
|
||||
fromAccount: AccountType;
|
||||
}
|
||||
|
||||
interface TransferFormProps {
|
||||
export interface TransferFormProps {
|
||||
pubKey: string | null;
|
||||
pubKeys: string[] | null;
|
||||
accounts: Array<{
|
||||
@@ -72,8 +74,28 @@ export const TransferForm = ({
|
||||
|
||||
const assets = sortBy(
|
||||
accounts
|
||||
.filter((a) => a.type === AccountType.ACCOUNT_TYPE_GENERAL)
|
||||
.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)
|
||||
.map((account) => ({
|
||||
key: account.asset.id,
|
||||
...account.asset,
|
||||
balance: addDecimal(account.balance, account.asset.decimals),
|
||||
})),
|
||||
@@ -87,18 +109,30 @@ export const TransferForm = ({
|
||||
|
||||
const asset = assets.find((a) => a.id === assetId);
|
||||
|
||||
const { data: toAccounts } = useDataProvider({
|
||||
dataProvider: accountsDataProvider,
|
||||
variables: {
|
||||
partyId: selectedPubKey,
|
||||
},
|
||||
skip: !selectedPubKey,
|
||||
});
|
||||
|
||||
const account = accounts.find(
|
||||
(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
|
||||
);
|
||||
});
|
||||
// The general account of the selected pubkey. You can only transfer
|
||||
// to general accounts, either when redeeming vested rewards or just
|
||||
// during normal general -> general transfers
|
||||
const toGeneralAccount =
|
||||
toAccounts &&
|
||||
toAccounts.find((a) => {
|
||||
return (
|
||||
a.asset.id === assetId && a.type === AccountType.ACCOUNT_TYPE_GENERAL
|
||||
);
|
||||
});
|
||||
|
||||
const [includeFee, setIncludeFee] = useState(false);
|
||||
|
||||
@@ -226,7 +260,7 @@ export const TransferForm = ({
|
||||
>
|
||||
{assets.map((a) => (
|
||||
<AssetOption
|
||||
key={a.id}
|
||||
key={a.key}
|
||||
asset={a}
|
||||
balance={
|
||||
<Balance
|
||||
@@ -296,14 +330,16 @@ export const TransferForm = ({
|
||||
defaultValue={AccountType.ACCOUNT_TYPE_GENERAL}
|
||||
>
|
||||
<option value={AccountType.ACCOUNT_TYPE_GENERAL}>
|
||||
{generalAccount
|
||||
{toGeneralAccount
|
||||
? `${
|
||||
AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]
|
||||
} (${addDecimalsFormatNumber(
|
||||
generalAccount.balance,
|
||||
generalAccount.asset.decimals
|
||||
)} ${generalAccount.asset.symbol})`
|
||||
: AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]}
|
||||
toGeneralAccount.balance,
|
||||
toGeneralAccount.asset.decimals
|
||||
)} ${toGeneralAccount.asset.symbol})`
|
||||
: `${AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]} ${
|
||||
asset ? `(0 ${asset.symbol})` : ''
|
||||
}`}
|
||||
</option>
|
||||
</TradingSelect>
|
||||
</TradingFormGroup>
|
||||
|
||||
@@ -72,15 +72,11 @@ export const DealTicketFeeDetails = ({
|
||||
|
||||
return (
|
||||
<KeyValue
|
||||
label={t('Fees')}
|
||||
value={
|
||||
totalDiscountedFeeAmount &&
|
||||
`~${formatValue(totalDiscountedFeeAmount, assetDecimals)}`
|
||||
}
|
||||
formattedValue={
|
||||
label={
|
||||
<>
|
||||
{t('Fees')}
|
||||
{totalDiscountFactor ? (
|
||||
<Pill size="xxs" intent={Intent.Warning} className="mr-1">
|
||||
<Pill size="xxs" intent={Intent.Info} className="ml-1">
|
||||
-
|
||||
{formatNumberPercentage(
|
||||
new BigNumber(totalDiscountFactor).multipliedBy(100),
|
||||
@@ -88,10 +84,16 @@ 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,7 +79,11 @@ export const FeesBreakdown = ({
|
||||
volumeDiscountFactor
|
||||
);
|
||||
|
||||
const { volumeDiscount, referralDiscount } = getDiscountedFee(
|
||||
const {
|
||||
discountedFee: discountedTotalFeeAmount,
|
||||
volumeDiscount,
|
||||
referralDiscount,
|
||||
} = getDiscountedFee(
|
||||
totalFeeAmount,
|
||||
referralDiscountFactor,
|
||||
volumeDiscountFactor
|
||||
@@ -131,7 +135,7 @@ export const FeesBreakdown = ({
|
||||
<FeesBreakdownItem
|
||||
label={t('Total fees')}
|
||||
factor={feeFactors ? sumFeesFactors(feeFactors)?.toString() : undefined}
|
||||
value={totalFeeAmount}
|
||||
value={discountedTotalFeeAmount}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { MockedResponse } from '@apollo/react-testing';
|
||||
import { MockedProvider } from '@apollo/react-testing';
|
||||
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
act,
|
||||
render,
|
||||
renderHook,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { RadioGroup } from '@vegaprotocol/ui-toolkit';
|
||||
import type {
|
||||
NodeCheckTimeUpdateSubscription,
|
||||
@@ -11,30 +17,33 @@ import {
|
||||
NodeCheckTimeUpdateDocument,
|
||||
} from '../../utils/__generated__/NodeCheck';
|
||||
import type { RowDataProps } from './row-data';
|
||||
import { POLL_INTERVAL } from './row-data';
|
||||
import {
|
||||
POLL_INTERVAL,
|
||||
Result,
|
||||
SUBSCRIPTION_TIMEOUT,
|
||||
useNodeBasicStatus,
|
||||
useNodeSubscriptionStatus,
|
||||
useResponseTime,
|
||||
} 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';
|
||||
|
||||
jest.mock('@vegaprotocol/apollo-client', () => ({
|
||||
useHeaderStore: jest.fn().mockReturnValue({}),
|
||||
}));
|
||||
|
||||
const statsQueryMock: MockedResponse<NodeCheckQuery> = {
|
||||
const mockStatsQuery = (
|
||||
blockHeight = '1234'
|
||||
): MockedResponse<NodeCheckQuery> => ({
|
||||
request: {
|
||||
query: NodeCheckDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
statistics: {
|
||||
blockHeight: '1234', // the actual value used in the component is the value from the header store
|
||||
blockHeight,
|
||||
vegaTime: new Date().toISOString(),
|
||||
chainId: 'test-chain-id',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const subMock: MockedResponse<NodeCheckTimeUpdateSubscription> = {
|
||||
request: {
|
||||
@@ -59,18 +68,6 @@ 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>,
|
||||
@@ -86,6 +83,98 @@ 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',
|
||||
@@ -94,9 +183,13 @@ describe('RowData', () => {
|
||||
onBlockHeight: jest.fn(),
|
||||
};
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('radio button enabled after stats query successful', async () => {
|
||||
mockHeaders(props.url);
|
||||
render(renderComponent(props, statsQueryMock, subMock));
|
||||
render(renderComponent(props, mockStatsQuery('100'), subMock));
|
||||
|
||||
// radio should be enabled until query resolves
|
||||
expect(
|
||||
@@ -127,8 +220,6 @@ describe('RowData', () => {
|
||||
});
|
||||
|
||||
it('radio button still enabled if query fails', async () => {
|
||||
mockHeaders(props.url, {});
|
||||
|
||||
const failedQueryMock: MockedResponse<NodeCheckQuery> = {
|
||||
request: {
|
||||
query: NodeCheckDocument,
|
||||
@@ -178,12 +269,11 @@ 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 },
|
||||
statsQueryMock,
|
||||
mockStatsQuery(String(blockHeight)),
|
||||
subMock
|
||||
)
|
||||
);
|
||||
@@ -201,7 +291,7 @@ describe('RowData', () => {
|
||||
rerender(
|
||||
renderComponent(
|
||||
{ ...props, highestBlock: blockHeight + BLOCK_THRESHOLD + 1 },
|
||||
statsQueryMock,
|
||||
mockStatsQuery(String(blockHeight)),
|
||||
subMock
|
||||
)
|
||||
);
|
||||
@@ -216,7 +306,7 @@ describe('RowData', () => {
|
||||
...props,
|
||||
id: CUSTOM_NODE_KEY,
|
||||
},
|
||||
statsQueryMock,
|
||||
mockStatsQuery('1234'),
|
||||
subMock
|
||||
)
|
||||
);
|
||||
@@ -230,16 +320,17 @@ 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 },
|
||||
statsQueryMock,
|
||||
mockStatsQuery(String(blockHeight)),
|
||||
subMock
|
||||
)
|
||||
);
|
||||
|
||||
expect(mockOnBlockHeight).toHaveBeenCalledWith(blockHeight);
|
||||
await waitFor(() => {
|
||||
expect(mockOnBlockHeight).toHaveBeenCalledWith(blockHeight);
|
||||
});
|
||||
});
|
||||
|
||||
it('should poll the query unless an errors is returned', async () => {
|
||||
@@ -275,7 +366,6 @@ describe('RowData', () => {
|
||||
};
|
||||
};
|
||||
|
||||
mockHeaders(props.url);
|
||||
const statsQueryMock1 = createStatsQueryMock('1234');
|
||||
const statsQueryMock2 = createStatsQueryMock('1235');
|
||||
const statsQueryMock3 = createFailedStatsQueryMock();
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
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';
|
||||
@@ -12,6 +10,7 @@ import {
|
||||
import { LayoutCell } from './layout-cell';
|
||||
|
||||
export const POLL_INTERVAL = 1000;
|
||||
export const SUBSCRIPTION_TIMEOUT = 3000;
|
||||
export const BLOCK_THRESHOLD = 3;
|
||||
|
||||
export interface RowDataProps {
|
||||
@@ -21,15 +20,39 @@ export interface RowDataProps {
|
||||
onBlockHeight: (blockHeight: number) => void;
|
||||
}
|
||||
|
||||
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
|
||||
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);
|
||||
|
||||
const { data, error, loading, startPolling, stopPolling } = useNodeCheckQuery(
|
||||
{
|
||||
pollInterval: POLL_INTERVAL,
|
||||
@@ -38,28 +61,7 @@ export const RowData = ({
|
||||
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;
|
||||
@@ -83,56 +85,57 @@ export const RowData = ({
|
||||
};
|
||||
}, [startPolling, stopPolling, error]);
|
||||
|
||||
// measure response time
|
||||
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>();
|
||||
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]) || {};
|
||||
setTime(duration);
|
||||
}, [url, data]);
|
||||
setResponseTime(duration);
|
||||
}, [url, trigger]);
|
||||
return { responseTime };
|
||||
};
|
||||
|
||||
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 (headers?.blockHeight) {
|
||||
onBlockHeight(headers.blockHeight);
|
||||
if (!isNaN(currentBlockHeight)) {
|
||||
onBlockHeight(currentBlockHeight);
|
||||
}
|
||||
}, [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;
|
||||
};
|
||||
}, [currentBlockHeight, onBlockHeight]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -143,72 +146,58 @@ export const RowData = ({
|
||||
)}
|
||||
<LayoutCell
|
||||
label={t('Response time')}
|
||||
isLoading={!error && loading}
|
||||
hasError={Boolean(error)}
|
||||
isLoading={status === Result.Loading}
|
||||
hasError={status === Result.Failed}
|
||||
dataTestId="response-time-cell"
|
||||
>
|
||||
{getResponseTimeDisplayValue(time, error)}
|
||||
{display(status, formatResponseTime(responseTime))}
|
||||
</LayoutCell>
|
||||
<LayoutCell
|
||||
label={t('Block')}
|
||||
isLoading={loading}
|
||||
hasError={getHasError()}
|
||||
isLoading={status === Result.Loading}
|
||||
hasError={
|
||||
status === Result.Failed ||
|
||||
(highestBlock != null &&
|
||||
!isNaN(currentBlockHeight) &&
|
||||
currentBlockHeight < highestBlock - BLOCK_THRESHOLD)
|
||||
}
|
||||
dataTestId="block-height-cell"
|
||||
>
|
||||
<span
|
||||
data-testid="query-block-height"
|
||||
data-query-block-height={
|
||||
error ? 'failed' : data?.statistics.blockHeight
|
||||
status === Result.Failed ? 'failed' : currentBlockHeight
|
||||
}
|
||||
>
|
||||
{getBlockDisplayValue(headers?.blockHeight, error)}
|
||||
{display(status, currentBlockHeight)}
|
||||
</span>
|
||||
</LayoutCell>
|
||||
<LayoutCell
|
||||
label={t('Subscription')}
|
||||
isLoading={subFailed ? false : subLoading}
|
||||
hasError={getSubFailed(subError, subFailed)}
|
||||
isLoading={subStatus === Result.Loading}
|
||||
hasError={subStatus === Result.Failed}
|
||||
dataTestId="subscription-cell"
|
||||
>
|
||||
{getSubscriptionDisplayValue(subFailed, subData?.busEvents, subError)}
|
||||
{display(subStatus, t('Yes'), t('No'))}
|
||||
</LayoutCell>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getResponseTimeDisplayValue = (
|
||||
responseTime?: number,
|
||||
error?: ApolloError
|
||||
) => {
|
||||
if (error) {
|
||||
return t('n/a');
|
||||
}
|
||||
if (typeof responseTime === 'number') {
|
||||
return `${Number(responseTime).toFixed(2)}ms`;
|
||||
}
|
||||
return '-';
|
||||
};
|
||||
const formatResponseTime = (time: number | undefined) =>
|
||||
time != null ? `${Number(time).toFixed(2)}ms` : '-';
|
||||
|
||||
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
|
||||
const display = (
|
||||
status: Result,
|
||||
yes: string | number | undefined,
|
||||
no = t('n/a')
|
||||
) => {
|
||||
if (subFailed || error) {
|
||||
return t('No');
|
||||
switch (status) {
|
||||
case Result.Successful:
|
||||
return yes;
|
||||
case Result.Failed:
|
||||
return no;
|
||||
default:
|
||||
return '-';
|
||||
}
|
||||
if (events?.length) {
|
||||
return t('Yes');
|
||||
}
|
||||
return '-';
|
||||
};
|
||||
|
||||
@@ -126,6 +126,11 @@ 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 =
|
||||
|
||||
@@ -289,17 +289,30 @@ describe('FeesDiscountBreakdownTooltip', () => {
|
||||
const { container } = render(<FeesDiscountBreakdownTooltip {...props} />);
|
||||
const dt = container.querySelectorAll('dt');
|
||||
const dd = container.querySelectorAll('dd');
|
||||
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 expectedDt = [
|
||||
'Infrastructure Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
'Liquidity Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
'Maker Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
];
|
||||
expected.forEach(({ label, value }, i) => {
|
||||
const expectedDD = [
|
||||
'0.05 BTC',
|
||||
'0.06 BTC',
|
||||
'0.01 BTC',
|
||||
'0.02 BTC',
|
||||
'0.03 BTC',
|
||||
'0.04 BTC',
|
||||
];
|
||||
expectedDt.forEach((label, i) => {
|
||||
expect(dt[i]).toHaveTextContent(label);
|
||||
expect(dd[i]).toHaveTextContent(value);
|
||||
});
|
||||
expectedDD.forEach((label, i) => {
|
||||
expect(dd[i]).toHaveTextContent(label);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -391,7 +391,7 @@ const FeesDiscountBreakdownTooltipItem = ({
|
||||
label: string;
|
||||
asset: ReturnType<typeof getAsset>;
|
||||
}) =>
|
||||
value ? (
|
||||
value && value !== '0' ? (
|
||||
<>
|
||||
<dt className="col-span-1">{label}</dt>
|
||||
<dd className="text-right col-span-1">
|
||||
@@ -418,34 +418,47 @@ 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('Infrastructure Fee Referral Discount')}
|
||||
label={t('Referral Discount')}
|
||||
asset={asset}
|
||||
/>
|
||||
<FeesDiscountBreakdownTooltipItem
|
||||
value={fees.infrastructureFeeVolumeDiscount}
|
||||
label={t('Infrastructure Fee Volume Discount')}
|
||||
label={t('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('Liquidity Fee Referral Discount')}
|
||||
label={t('Referral Discount')}
|
||||
asset={asset}
|
||||
/>
|
||||
<FeesDiscountBreakdownTooltipItem
|
||||
value={fees.liquidityFeeVolumeDiscount}
|
||||
label={t('Liquidity Fee Volume Discount')}
|
||||
label={t('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('Maker Fee Referral Discount')}
|
||||
label={t('Referral Discount')}
|
||||
asset={asset}
|
||||
/>
|
||||
<FeesDiscountBreakdownTooltipItem
|
||||
value={fees.makerFeeVolumeDiscount}
|
||||
label={t('Maker Fee Volume Discount')}
|
||||
label={t('Volume Discount')}
|
||||
asset={asset}
|
||||
/>
|
||||
</dl>
|
||||
|
||||
@@ -12,6 +12,7 @@ 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',
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@
|
||||
"jsondiffpatch": "^0.4.1",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "13.3.0",
|
||||
"pennant": "1.14.0",
|
||||
"pennant": "^1.14.1",
|
||||
"react": "18.2.0",
|
||||
"react-copy-to-clipboard": "^5.0.4",
|
||||
"react-dom": "18.2.0",
|
||||
|
||||
@@ -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.0:
|
||||
version "1.14.0"
|
||||
resolved "https://registry.yarnpkg.com/pennant/-/pennant-1.14.0.tgz#4100c25a6d836d6f0ff425181fb6f812f9fe5778"
|
||||
integrity sha512-9H0zWzFUSbD1BlDXnHFmKwkAxXGb1xTxjkUD+RwaMygtSwPXzQEyk2ScVyMqxdcz0RuJmI5HCVmZTOjdr1NwuA==
|
||||
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==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@d3fc/d3fc-technical-indicator" "^8.0.1"
|
||||
|
||||
Reference in New Issue
Block a user