import minBy from 'lodash/minBy'; import { CodeTile, StatTile } from './tile'; import { VegaIcon, VegaIconNames, truncateMiddle, TextChildrenTooltip as Tooltip, } from '@vegaprotocol/ui-toolkit'; import { useVegaWallet } from '@vegaprotocol/wallet'; import { DEFAULT_AGGREGATION_DAYS, useReferral, useUpdateReferees, } from './hooks/use-referral'; 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 { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch'; import BigNumber from 'bignumber.js'; import { useT, ns } from '../../lib/use-t'; import { Trans } from 'react-i18next'; import { ApplyCodeForm, ApplyCodeFormContainer } from './apply-code-form'; import { QUSDTooltip } from './qusd-tooltip'; export const ReferralStatistics = () => { const { pubKey } = useVegaWallet(); const program = useReferralProgram(); const { data: referee, refetch: refereeRefetch } = useReferral({ pubKey, role: 'referee', aggregationEpochs: program.details?.windowLength, }); const { data: referrer, refetch: referrerRefetch } = useUpdateReferees( useReferral({ pubKey, role: 'referrer', aggregationEpochs: program.details?.windowLength, }), DEFAULT_AGGREGATION_DAYS, ['totalRefereeGeneratedRewards'], DEFAULT_AGGREGATION_DAYS === program.details?.windowLength ); const refetch = useCallback(() => { refereeRefetch(); referrerRefetch(); }, [refereeRefetch, referrerRefetch]); if (referee?.code) { return ( <> {!referee.isEligible && } ); } if (referrer?.code) { return ( <> ); } return ; }; export const useStats = ({ data, program, }: { data?: NonNullable['data']>; program: ReturnType; }) => { const { benefitTiers } = program; const { data: epochData } = useCurrentEpochInfoQuery({ fetchPolicy: 'network-only', }); 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 : new BigNumber(multiplier).times(baseCommissionValue).toNumber(); 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) : minBy(benefitTiers, (bt) => bt.tier); // min 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['data']>; program: ReturnType; as: 'referrer' | 'referee'; }) => { const t = useT(); const { baseCommissionValue, runningVolumeValue, referrerVolumeValue, multiplier, finalCommissionValue, discountFactorValue, currentBenefitTierValue, epochsValue, nextBenefitTierValue, nextBenefitTierVolumeValue, nextBenefitTierEpochsValue, } = useStats({ data, program }); const isApplyCodePreview = useMemo( () => data.referee === null, [data.referee] ); const { benefitTiers } = useReferralProgram(); const { stakeAvailable, isEligible } = useStakeAvailable(); const { details } = program; const compactNumFormat = new Intl.NumberFormat(getUserLocale(), { minimumFractionDigits: 0, maximumFractionDigits: 2, notation: 'compact', compactDisplay: 'short', }); const baseCommissionTile = ( {baseCommissionValue * 100}% ); const stakingMultiplierTile = ( {t('{{amount}} $VEGA staked', { amount: addDecimalsFormatNumber( stakeAvailable?.toString() || 0, 18 ), })} } overrideWithNoProgram={!details} > {multiplier || t('None')} ); const baseCommissionFormatted = BigNumber(baseCommissionValue) .times(100) .toString(); const finalCommissionFormatted = new BigNumber(finalCommissionValue) .times(100) .toString(); const finalCommissionTile = ( {finalCommissionFormatted}% ); const numberOfTradersValue = data.referees.length; const numberOfTradersTile = ( {numberOfTradersValue} ); const codeTile = ( ); const referrerVolumeTile = ( {compactNumFormat.format(referrerVolumeValue)} ); const totalCommissionValue = data.referees .map((r) => new BigNumber(r.totalRefereeGeneratedRewards)) .reduce((all, r) => all.plus(r), new BigNumber(0)); const totalCommissionTile = ( last 30 epochs , ]} /> } description={} > {getNumberFormat(0).format(Number(totalCommissionValue))} ); const referrerTiles = ( <>
{baseCommissionTile} {stakingMultiplierTile} {finalCommissionTile}
{codeTile} {referrerVolumeTile} {numberOfTradersTile} {totalCommissionTile}
); const currentBenefitTierTile = ( {isApplyCodePreview ? currentBenefitTierValue?.tier || benefitTiers[0]?.tier || 'None' : currentBenefitTierValue?.tier || 'None'} ); const discountFactorTile = ( {isApplyCodePreview && benefitTiers.length >= 1 ? benefitTiers[0].discountFactor * 100 : discountFactorValue * 100} % ); const runningVolumeTile = ( {compactNumFormat.format(runningVolumeValue)} ); const epochsTile = ( {epochsValue} ); const nextTierVolumeTile = ( {nextBenefitTierVolumeValue <= 0 ? '0' : compactNumFormat.format(nextBenefitTierVolumeValue)} ); const nextTierEpochsTile = ( {nextBenefitTierEpochsValue <= 0 ? '0' : nextBenefitTierEpochsValue} ); const refereeTiles = ( <>
{currentBenefitTierTile} {runningVolumeTile} {codeTile}
{discountFactorTile} {nextTierVolumeTile} {epochsTile} {nextTierEpochsTile}
); const eligibilityWarning = as === 'referee' && !isEligible && (

{t('Referral code no longer valid')}

{t( 'Your referral code is no longer valid as the referrer no longer meets the minimum requirements. Apply a new code to continue receiving discounts.' )}

); return (
{as === 'referrer' && referrerTiles} {as === 'referee' && refereeTiles}
{eligibilityWarning}
); }; export const RefereesTable = ({ data, program, }: { data: NonNullable['data']>; program: ReturnType; }) => { const t = useT(); const [collapsed, setCollapsed] = useState(false); const tableRef = useRef(null); const { details } = program; useLayoutEffect(() => { if ((tableRef.current?.getBoundingClientRect().height || 0) > 384) { setCollapsed(true); } }, []); return ( <> {/* Referees (only for referrer view) */} {data.referees.length > 0 && (

{t('Referees')}

, last 30 epochs , ]} values={{ count: DEFAULT_AGGREGATION_DAYS, }} ns={ns} /> ), }, ]} data={sortBy( data.referees.map((r) => ({ party: ( {truncateMiddle(r.refereeId)} ), 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()} /> )} ); };