2023-10-23 14:57:18 +00:00
|
|
|
import { CodeTile, StatTile } from './tile';
|
2023-09-21 13:25:19 +00:00
|
|
|
import {
|
|
|
|
VegaIcon,
|
|
|
|
VegaIconNames,
|
2023-10-23 14:57:18 +00:00
|
|
|
truncateMiddle,
|
2023-09-21 13:25:19 +00:00
|
|
|
} from '@vegaprotocol/ui-toolkit';
|
|
|
|
|
2023-10-23 14:57:18 +00:00
|
|
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
2023-09-21 13:25:19 +00:00
|
|
|
import { useReferral } from './hooks/use-referral';
|
|
|
|
import { CreateCodeContainer } from './create-code-form';
|
|
|
|
import classNames from 'classnames';
|
2023-10-23 14:57:18 +00:00
|
|
|
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, useRef, useState } from 'react';
|
|
|
|
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
|
|
|
|
import BigNumber from 'bignumber.js';
|
2023-10-24 15:43:33 +00:00
|
|
|
import { t } from '@vegaprotocol/i18n';
|
2023-09-21 13:25:19 +00:00
|
|
|
|
|
|
|
export const ReferralStatistics = () => {
|
|
|
|
const { pubKey } = useVegaWallet();
|
|
|
|
|
2023-10-23 14:57:18 +00:00
|
|
|
const { data: referee } = useReferral({
|
|
|
|
pubKey,
|
|
|
|
role: 'referee',
|
|
|
|
});
|
|
|
|
const { data: referrer } = useReferral({
|
|
|
|
pubKey,
|
|
|
|
role: 'referrer',
|
|
|
|
});
|
2023-09-21 13:25:19 +00:00
|
|
|
|
|
|
|
if (referee?.code) {
|
|
|
|
return <Statistics data={referee} as="referee" />;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (referrer?.code) {
|
|
|
|
return <Statistics data={referrer} as="referrer" />;
|
|
|
|
}
|
|
|
|
|
|
|
|
return <CreateCodeContainer />;
|
|
|
|
};
|
|
|
|
|
2023-10-23 14:57:18 +00:00
|
|
|
export const Statistics = ({
|
2023-09-21 13:25:19 +00:00
|
|
|
data,
|
|
|
|
as,
|
|
|
|
}: {
|
2023-10-23 14:57:18 +00:00
|
|
|
data: NonNullable<ReturnType<typeof useReferral>['data']>;
|
2023-09-21 13:25:19 +00:00
|
|
|
as: 'referrer' | 'referee';
|
|
|
|
}) => {
|
2023-10-23 14:57:18 +00:00
|
|
|
const { data: epochData } = useCurrentEpochInfoQuery();
|
|
|
|
const { stakeAvailable } = useStakeAvailable();
|
|
|
|
const { benefitTiers } = useReferralProgram();
|
|
|
|
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 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);
|
|
|
|
const epochsValue =
|
|
|
|
!isNaN(currentEpoch) && refereeInfo?.atEpoch
|
|
|
|
? currentEpoch - refereeInfo?.atEpoch
|
|
|
|
: 0;
|
|
|
|
const nextBenefitTierVolumeValue = nextBenefitTierValue
|
|
|
|
? nextBenefitTierValue.minimumVolume - runningVolumeValue
|
|
|
|
: 0;
|
|
|
|
const nextBenefitTierEpochsValue = nextBenefitTierValue
|
|
|
|
? nextBenefitTierValue.epochs - epochsValue
|
|
|
|
: 0;
|
|
|
|
|
|
|
|
const baseCommissionTile = (
|
2023-10-24 15:43:33 +00:00
|
|
|
<StatTile title={t('Base commission rate')}>
|
2023-10-23 14:57:18 +00:00
|
|
|
{baseCommissionValue * 100}%
|
|
|
|
</StatTile>
|
|
|
|
);
|
|
|
|
const stakingMultiplierTile = (
|
|
|
|
<StatTile
|
2023-10-24 15:43:33 +00:00
|
|
|
title={t('Staking multiplier')}
|
2023-10-23 14:57:18 +00:00
|
|
|
description={`(${addDecimalsFormatNumber(
|
|
|
|
stakeAvailable?.toString() || 0,
|
|
|
|
18
|
|
|
|
)} $VEGA staked)`}
|
2023-09-21 13:25:19 +00:00
|
|
|
>
|
2023-10-24 15:43:33 +00:00
|
|
|
{multiplier || t('None')}
|
2023-10-23 14:57:18 +00:00
|
|
|
</StatTile>
|
|
|
|
);
|
|
|
|
const finalCommissionTile = (
|
2023-10-24 15:43:33 +00:00
|
|
|
<StatTile title={t('Final commission rate')}>
|
2023-10-23 14:57:18 +00:00
|
|
|
{finalCommissionValue * 100}%
|
|
|
|
</StatTile>
|
|
|
|
);
|
|
|
|
const numberOfTradersValue = data.referees.length;
|
|
|
|
const numberOfTradersTile = (
|
2023-10-24 15:43:33 +00:00
|
|
|
<StatTile title={t('Number of traders')}>{numberOfTradersValue}</StatTile>
|
2023-10-23 14:57:18 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
const codeTile = <CodeTile code={data?.code} />;
|
|
|
|
const createdAtTile = (
|
2023-10-24 15:43:33 +00:00
|
|
|
<StatTile title={t('Created at')}>
|
2023-10-23 14:57:18 +00:00
|
|
|
<span className="text-3xl">
|
|
|
|
{getDateFormat().format(new Date(data.createdAt))}
|
|
|
|
</span>
|
|
|
|
</StatTile>
|
|
|
|
);
|
|
|
|
|
|
|
|
const totalCommissionValue = data.referees
|
|
|
|
.map((r) => new BigNumber(r.totalRefereeGeneratedRewards))
|
|
|
|
.reduce((all, r) => all.plus(r), new BigNumber(0));
|
|
|
|
const totalCommissionTile = (
|
2023-10-24 15:43:33 +00:00
|
|
|
<StatTile
|
|
|
|
title={t('Total commission (last 30 days)')}
|
|
|
|
description={t('(qUSD)')}
|
|
|
|
>
|
2023-10-23 14:57:18 +00:00
|
|
|
{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}
|
|
|
|
{createdAtTile}
|
|
|
|
{numberOfTradersTile}
|
|
|
|
{totalCommissionTile}
|
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
|
|
|
|
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
|
|
|
|
minimumFractionDigits: 0,
|
|
|
|
maximumFractionDigits: 2,
|
|
|
|
notation: 'compact',
|
|
|
|
compactDisplay: 'short',
|
|
|
|
});
|
|
|
|
|
|
|
|
const currentBenefitTierTile = (
|
2023-10-24 15:43:33 +00:00
|
|
|
<StatTile title={t('Current tier')}>
|
2023-10-23 14:57:18 +00:00
|
|
|
{currentBenefitTierValue?.tier || '-'}
|
|
|
|
</StatTile>
|
|
|
|
);
|
|
|
|
const discountFactorTile = (
|
2023-10-24 15:43:33 +00:00
|
|
|
<StatTile title={t('Discount')}>{discountFactorValue * 100}%</StatTile>
|
2023-10-23 14:57:18 +00:00
|
|
|
);
|
|
|
|
const runningVolumeTile = (
|
2023-10-24 15:43:33 +00:00
|
|
|
<StatTile title={t('Combined volume')}>
|
2023-10-23 14:57:18 +00:00
|
|
|
{compactNumFormat.format(runningVolumeValue)}
|
|
|
|
</StatTile>
|
|
|
|
);
|
2023-10-24 15:43:33 +00:00
|
|
|
const epochsTile = (
|
|
|
|
<StatTile title={t('Epochs in set')}>{epochsValue}</StatTile>
|
|
|
|
);
|
2023-10-23 14:57:18 +00:00
|
|
|
const nextTierVolumeTile = (
|
2023-10-24 15:43:33 +00:00
|
|
|
<StatTile title={t('Volume to next tier')}>
|
2023-10-23 14:57:18 +00:00
|
|
|
{nextBenefitTierVolumeValue <= 0
|
2023-10-24 15:43:33 +00:00
|
|
|
? '0'
|
2023-10-23 14:57:18 +00:00
|
|
|
: compactNumFormat.format(nextBenefitTierVolumeValue)}
|
|
|
|
</StatTile>
|
|
|
|
);
|
|
|
|
const nextTierEpochsTile = (
|
2023-10-24 15:43:33 +00:00
|
|
|
<StatTile title={t('Epochs to next tier')}>
|
|
|
|
{nextBenefitTierEpochsValue <= 0 ? '0' : nextBenefitTierEpochsValue}
|
2023-10-23 14:57:18 +00:00
|
|
|
</StatTile>
|
|
|
|
);
|
|
|
|
|
|
|
|
const refereeTiles = (
|
|
|
|
<>
|
|
|
|
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
|
|
|
{currentBenefitTierTile}
|
|
|
|
{discountFactorTile}
|
|
|
|
{codeTile}
|
|
|
|
</div>
|
|
|
|
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
|
|
|
{runningVolumeTile}
|
|
|
|
{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 */}
|
2023-09-21 13:25:19 +00:00
|
|
|
<div
|
2023-10-23 14:57:18 +00:00
|
|
|
className={classNames(
|
|
|
|
'grid grid-cols-1 grid-rows-1 gap-5 mx-auto mb-20'
|
2023-09-21 13:25:19 +00:00
|
|
|
)}
|
2023-10-23 14:57:18 +00:00
|
|
|
>
|
|
|
|
{as === 'referrer' && referrerTiles}
|
|
|
|
{as === 'referee' && refereeTiles}
|
2023-09-21 13:25:19 +00:00
|
|
|
</div>
|
2023-10-23 14:57:18 +00:00
|
|
|
|
|
|
|
{/* Referees (only for referrer view) */}
|
|
|
|
{as === 'referrer' && data.referees.length > 0 && (
|
|
|
|
<div className="mt-20 mb-20">
|
2023-10-24 15:43:33 +00:00
|
|
|
<h2 className="text-2xl mb-5">{t('Referees')}</h2>
|
2023-10-23 14:57:18 +00:00
|
|
|
<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={[
|
2023-10-24 15:43:33 +00:00
|
|
|
{ name: 'party', displayName: t('Trader') },
|
|
|
|
{ name: 'joined', displayName: t('Date Joined') },
|
|
|
|
{ name: 'volume', displayName: t('Volume (last 30 days)') },
|
2023-10-23 14:57:18 +00:00
|
|
|
{
|
|
|
|
name: 'commission',
|
2023-10-24 15:43:33 +00:00
|
|
|
displayName: t('Commission earned (last 30 days)'),
|
2023-10-23 14:57:18 +00:00
|
|
|
},
|
|
|
|
]}
|
|
|
|
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>
|
|
|
|
)}
|
|
|
|
</>
|
2023-09-21 13:25:19 +00:00
|
|
|
);
|
|
|
|
};
|