f377e07996
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
457 lines
13 KiB
TypeScript
457 lines
13 KiB
TypeScript
import { CodeTile, StatTile } from './tile';
|
|
import {
|
|
VegaIcon,
|
|
VegaIconNames,
|
|
truncateMiddle,
|
|
ExternalLink,
|
|
Tooltip,
|
|
} from '@vegaprotocol/ui-toolkit';
|
|
|
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
|
import { DEFAULT_AGGREGATION_DAYS, useReferral } from './hooks/use-referral';
|
|
import { CreateCodeContainer } from './create-code-form';
|
|
import classNames from 'classnames';
|
|
import { Table } from './table';
|
|
import {
|
|
addDecimalsFormatNumber,
|
|
getDateFormat,
|
|
getDateTimeFormat,
|
|
getNumberFormat,
|
|
getUserLocale,
|
|
removePaginationWrapper,
|
|
} from '@vegaprotocol/utils';
|
|
import { useReferralSetStatsQuery } from './hooks/__generated__/ReferralSetStats';
|
|
import compact from 'lodash/compact';
|
|
import { useReferralProgram } from './hooks/use-referral-program';
|
|
import { useStakeAvailable } from './hooks/use-stake-available';
|
|
import sortBy from 'lodash/sortBy';
|
|
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
|
|
import BigNumber from 'bignumber.js';
|
|
import maxBy from 'lodash/maxBy';
|
|
import { DocsLinks } from '@vegaprotocol/environment';
|
|
import { useT, ns } from '../../lib/use-t';
|
|
import { Trans } from 'react-i18next';
|
|
|
|
export const ReferralStatistics = () => {
|
|
const { pubKey } = useVegaWallet();
|
|
|
|
const program = useReferralProgram();
|
|
|
|
const { data: referee } = useReferral({
|
|
pubKey,
|
|
role: 'referee',
|
|
aggregationEpochs: program.details?.windowLength,
|
|
});
|
|
const { data: referrer } = useReferral({
|
|
pubKey,
|
|
role: 'referrer',
|
|
aggregationEpochs: program.details?.windowLength,
|
|
});
|
|
|
|
if (referee?.code) {
|
|
return <Statistics data={referee} program={program} as="referee" />;
|
|
}
|
|
|
|
if (referrer?.code) {
|
|
return <Statistics data={referrer} program={program} as="referrer" />;
|
|
}
|
|
|
|
return <CreateCodeContainer />;
|
|
};
|
|
|
|
export const useStats = ({
|
|
data,
|
|
program,
|
|
as,
|
|
}: {
|
|
data?: NonNullable<ReturnType<typeof useReferral>['data']>;
|
|
program: ReturnType<typeof useReferralProgram>;
|
|
as?: 'referrer' | 'referee';
|
|
}) => {
|
|
const { benefitTiers } = program;
|
|
const { data: epochData } = useCurrentEpochInfoQuery();
|
|
const { data: statsData } = useReferralSetStatsQuery({
|
|
variables: {
|
|
code: data?.code || '',
|
|
},
|
|
skip: !data?.code,
|
|
fetchPolicy: 'cache-and-network',
|
|
});
|
|
|
|
const currentEpoch = Number(epochData?.epoch.id);
|
|
|
|
const stats =
|
|
statsData?.referralSetStats.edges &&
|
|
compact(removePaginationWrapper(statsData.referralSetStats.edges));
|
|
const refereeInfo = data?.referee;
|
|
const refereeStats = stats?.find(
|
|
(r) => r.partyId === data?.referee?.refereeId
|
|
);
|
|
|
|
const statsAvailable = stats && stats.length > 0 && stats[0];
|
|
const baseCommissionValue = statsAvailable
|
|
? Number(statsAvailable.rewardFactor)
|
|
: 0;
|
|
const runningVolumeValue = statsAvailable
|
|
? Number(statsAvailable.referralSetRunningNotionalTakerVolume)
|
|
: 0;
|
|
const referrerVolumeValue = statsAvailable
|
|
? Number(statsAvailable.referrerTakerVolume)
|
|
: 0;
|
|
const multiplier = statsAvailable
|
|
? Number(statsAvailable.rewardsMultiplier)
|
|
: 1;
|
|
const finalCommissionValue = isNaN(multiplier)
|
|
? baseCommissionValue
|
|
: multiplier * baseCommissionValue;
|
|
|
|
const discountFactorValue = refereeStats?.discountFactor
|
|
? Number(refereeStats.discountFactor)
|
|
: 0;
|
|
const currentBenefitTierValue = benefitTiers.find(
|
|
(t) =>
|
|
!isNaN(discountFactorValue) &&
|
|
!isNaN(t.discountFactor) &&
|
|
t.discountFactor === discountFactorValue
|
|
);
|
|
const nextBenefitTierValue = currentBenefitTierValue
|
|
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier - 1)
|
|
: maxBy(benefitTiers, (bt) => bt.tier); // max tier number is lowest tier
|
|
const epochsValue =
|
|
!isNaN(currentEpoch) && refereeInfo?.atEpoch
|
|
? currentEpoch - refereeInfo?.atEpoch
|
|
: 0;
|
|
const nextBenefitTierVolumeValue = nextBenefitTierValue
|
|
? nextBenefitTierValue.minimumVolume - runningVolumeValue
|
|
: 0;
|
|
const nextBenefitTierEpochsValue = nextBenefitTierValue
|
|
? nextBenefitTierValue.epochs - epochsValue
|
|
: 0;
|
|
|
|
return {
|
|
baseCommissionValue,
|
|
runningVolumeValue,
|
|
referrerVolumeValue,
|
|
multiplier,
|
|
finalCommissionValue,
|
|
discountFactorValue,
|
|
currentBenefitTierValue,
|
|
nextBenefitTierValue,
|
|
epochsValue,
|
|
nextBenefitTierVolumeValue,
|
|
nextBenefitTierEpochsValue,
|
|
};
|
|
};
|
|
|
|
export const Statistics = ({
|
|
data,
|
|
program,
|
|
as,
|
|
}: {
|
|
data: NonNullable<ReturnType<typeof useReferral>['data']>;
|
|
program: ReturnType<typeof useReferralProgram>;
|
|
as: 'referrer' | 'referee';
|
|
}) => {
|
|
const t = useT();
|
|
const {
|
|
baseCommissionValue,
|
|
runningVolumeValue,
|
|
referrerVolumeValue,
|
|
multiplier,
|
|
finalCommissionValue,
|
|
discountFactorValue,
|
|
currentBenefitTierValue,
|
|
epochsValue,
|
|
nextBenefitTierVolumeValue,
|
|
nextBenefitTierEpochsValue,
|
|
} = useStats({ data, program, as });
|
|
|
|
const isApplyCodePreview = useMemo(
|
|
() => data.referee === null,
|
|
[data.referee]
|
|
);
|
|
|
|
const { benefitTiers } = useReferralProgram();
|
|
|
|
const { stakeAvailable } = useStakeAvailable();
|
|
const { details } = program;
|
|
|
|
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 2,
|
|
notation: 'compact',
|
|
compactDisplay: 'short',
|
|
});
|
|
|
|
const baseCommissionTile = (
|
|
<StatTile
|
|
title={t('Base commission rate')}
|
|
description={t(
|
|
'(Combined set volume {{runningVolume}} over last {{epochs}} epochs)',
|
|
{
|
|
runningVolume: compactNumFormat.format(runningVolumeValue),
|
|
epochs: (
|
|
details?.windowLength || DEFAULT_AGGREGATION_DAYS
|
|
).toString(),
|
|
}
|
|
)}
|
|
>
|
|
{baseCommissionValue * 100}%
|
|
</StatTile>
|
|
);
|
|
const stakingMultiplierTile = (
|
|
<StatTile
|
|
title={t('Staking multiplier')}
|
|
description={t('{{amount}} $VEGA staked', {
|
|
amount: addDecimalsFormatNumber(stakeAvailable?.toString() || 0, 18),
|
|
})}
|
|
>
|
|
{multiplier || t('None')}
|
|
</StatTile>
|
|
);
|
|
const finalCommissionTile = (
|
|
<StatTile
|
|
title={t('Final commission rate')}
|
|
description={
|
|
!isNaN(multiplier)
|
|
? `(${baseCommissionValue * 100}% ⨉ ${multiplier} = ${
|
|
finalCommissionValue * 100
|
|
}%)`
|
|
: undefined
|
|
}
|
|
>
|
|
{finalCommissionValue * 100}%
|
|
</StatTile>
|
|
);
|
|
const numberOfTradersValue = data.referees.length;
|
|
const numberOfTradersTile = (
|
|
<StatTile title={t('Number of traders')}>{numberOfTradersValue}</StatTile>
|
|
);
|
|
|
|
const codeTile = (
|
|
<CodeTile
|
|
code={data?.code}
|
|
createdAt={getDateFormat().format(new Date(data.createdAt))}
|
|
/>
|
|
);
|
|
|
|
const referrerVolumeTile = (
|
|
<StatTile
|
|
title={t('My volume (last {{count}} epochs)', {
|
|
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
|
|
})}
|
|
>
|
|
{compactNumFormat.format(referrerVolumeValue)}
|
|
</StatTile>
|
|
);
|
|
|
|
const totalCommissionValue = data.referees
|
|
.map((r) => new BigNumber(r.totalRefereeGeneratedRewards))
|
|
.reduce((all, r) => all.plus(r), new BigNumber(0));
|
|
const totalCommissionTile = (
|
|
<StatTile
|
|
title={t('Total commission (last {{count}}} epochs)', {
|
|
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
|
|
})}
|
|
description={<QUSDTooltip />}
|
|
>
|
|
{getNumberFormat(0).format(Number(totalCommissionValue))}
|
|
</StatTile>
|
|
);
|
|
|
|
const referrerTiles = (
|
|
<>
|
|
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
|
{baseCommissionTile}
|
|
{stakingMultiplierTile}
|
|
{finalCommissionTile}
|
|
</div>
|
|
|
|
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
|
{codeTile}
|
|
{referrerVolumeTile}
|
|
{numberOfTradersTile}
|
|
{totalCommissionTile}
|
|
</div>
|
|
</>
|
|
);
|
|
|
|
const currentBenefitTierTile = (
|
|
<StatTile title={t('Current tier')}>
|
|
{isApplyCodePreview
|
|
? currentBenefitTierValue?.tier || benefitTiers[0]?.tier || 'None'
|
|
: currentBenefitTierValue?.tier || 'None'}
|
|
</StatTile>
|
|
);
|
|
const discountFactorTile = (
|
|
<StatTile title={t('Discount')}>
|
|
{isApplyCodePreview
|
|
? benefitTiers[0].discountFactor * 100
|
|
: discountFactorValue * 100}
|
|
%
|
|
</StatTile>
|
|
);
|
|
const runningVolumeTile = (
|
|
<StatTile
|
|
title={t('Combined volume (last {{count}} epochs)', {
|
|
count: details?.windowLength,
|
|
})}
|
|
>
|
|
{compactNumFormat.format(runningVolumeValue)}
|
|
</StatTile>
|
|
);
|
|
const epochsTile = (
|
|
<StatTile title={t('Epochs in set')}>{epochsValue}</StatTile>
|
|
);
|
|
const nextTierVolumeTile = (
|
|
<StatTile title={t('Volume to next tier')}>
|
|
{nextBenefitTierVolumeValue <= 0
|
|
? '0'
|
|
: compactNumFormat.format(nextBenefitTierVolumeValue)}
|
|
</StatTile>
|
|
);
|
|
const nextTierEpochsTile = (
|
|
<StatTile title={t('Epochs to next tier')}>
|
|
{nextBenefitTierEpochsValue <= 0 ? '0' : nextBenefitTierEpochsValue}
|
|
</StatTile>
|
|
);
|
|
|
|
const refereeTiles = (
|
|
<>
|
|
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
|
{currentBenefitTierTile}
|
|
{runningVolumeTile}
|
|
{codeTile}
|
|
</div>
|
|
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
|
{discountFactorTile}
|
|
{nextTierVolumeTile}
|
|
{epochsTile}
|
|
{nextTierEpochsTile}
|
|
</div>
|
|
</>
|
|
);
|
|
|
|
const [collapsed, setCollapsed] = useState(false);
|
|
const tableRef = useRef<HTMLTableElement>(null);
|
|
useLayoutEffect(() => {
|
|
if ((tableRef.current?.getBoundingClientRect().height || 0) > 384) {
|
|
setCollapsed(true);
|
|
}
|
|
}, []);
|
|
|
|
return (
|
|
<>
|
|
{/* Stats tiles */}
|
|
<div
|
|
className={classNames(
|
|
'grid grid-cols-1 grid-rows-1 gap-5 mx-auto mb-20'
|
|
)}
|
|
>
|
|
{as === 'referrer' && referrerTiles}
|
|
{as === 'referee' && refereeTiles}
|
|
</div>
|
|
|
|
{/* Referees (only for referrer view) */}
|
|
{as === 'referrer' && data.referees.length > 0 && (
|
|
<div className="mt-20 mb-20">
|
|
<h2 className="mb-5 text-2xl">{t('Referees')}</h2>
|
|
<div
|
|
className={classNames(
|
|
collapsed && [
|
|
'relative max-h-96 overflow-hidden',
|
|
'after:w-full after:h-20 after:absolute after:bottom-0 after:left-0',
|
|
'after:bg-gradient-to-t after:from-white after:dark:from-vega-cdark-900 after:to-transparent',
|
|
]
|
|
)}
|
|
>
|
|
<button
|
|
className={classNames(
|
|
'absolute left-1/2 bottom-0 z-10 p-2 translate-x-[-50%]',
|
|
{
|
|
hidden: !collapsed,
|
|
}
|
|
)}
|
|
onClick={() => setCollapsed(false)}
|
|
>
|
|
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={24} />
|
|
</button>
|
|
<Table
|
|
ref={tableRef}
|
|
columns={[
|
|
{ name: 'party', displayName: t('Trader') },
|
|
{ name: 'joined', displayName: t('Date Joined') },
|
|
{
|
|
name: 'volume',
|
|
displayName: t('Volume (last {{count}} epochs)', {
|
|
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
|
|
}),
|
|
},
|
|
{
|
|
name: 'commission',
|
|
displayName: (
|
|
<Trans
|
|
i18nKey="referral-statistics-commission"
|
|
defaults="Commission earned in <0>qUSD</0> (last {{count}} epochs)"
|
|
values={{
|
|
count:
|
|
details?.windowLength || DEFAULT_AGGREGATION_DAYS,
|
|
}}
|
|
ns={ns}
|
|
/>
|
|
),
|
|
},
|
|
]}
|
|
data={sortBy(
|
|
data.referees.map((r) => ({
|
|
party: (
|
|
<span title={r.refereeId}>
|
|
{truncateMiddle(r.refereeId)}
|
|
</span>
|
|
),
|
|
joined: getDateTimeFormat().format(new Date(r.joinedAt)),
|
|
volume: Number(r.totalRefereeNotionalTakerVolume),
|
|
commission: Number(r.totalRefereeGeneratedRewards),
|
|
})),
|
|
(r) => r.volume
|
|
)
|
|
.map((r) => ({
|
|
...r,
|
|
volume: getNumberFormat(0).format(r.volume),
|
|
commission: getNumberFormat(0).format(r.commission),
|
|
}))
|
|
.reverse()}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const QUSDTooltip = () => {
|
|
const t = useT();
|
|
return (
|
|
<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>
|
|
);
|
|
};
|