Compare commits
1 Commits
develop
...
feat/5702-
Author | SHA1 | Date | |
---|---|---|---|
|
f93ac8d0c6 |
@ -16,9 +16,7 @@ import {
|
|||||||
useVegaWallet,
|
useVegaWallet,
|
||||||
useDialogStore,
|
useDialogStore,
|
||||||
} from '@vegaprotocol/wallet-react';
|
} from '@vegaprotocol/wallet-react';
|
||||||
import { useIsInReferralSet, useReferral } from './hooks/use-referral';
|
|
||||||
import { Routes } from '../../lib/links';
|
import { Routes } from '../../lib/links';
|
||||||
import { Statistics, useStats } from './referral-statistics';
|
|
||||||
import { useReferralProgram } from './hooks/use-referral-program';
|
import { useReferralProgram } from './hooks/use-referral-program';
|
||||||
import { ns, useT } from '../../lib/use-t';
|
import { ns, useT } from '../../lib/use-t';
|
||||||
import { useFundsAvailable } from './hooks/use-funds-available';
|
import { useFundsAvailable } from './hooks/use-funds-available';
|
||||||
@ -26,6 +24,12 @@ import { ViewType, useSidebar } from '../../components/sidebar';
|
|||||||
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||||
import { QUSDTooltip } from './qusd-tooltip';
|
import { QUSDTooltip } from './qusd-tooltip';
|
||||||
import { Trans } from 'react-i18next';
|
import { Trans } from 'react-i18next';
|
||||||
|
import { PreviewRefereeStatistics } from './referee-statistics';
|
||||||
|
import {
|
||||||
|
useReferralSet,
|
||||||
|
useIsInReferralSet,
|
||||||
|
} from './hooks/use-find-referral-set';
|
||||||
|
import minBy from 'lodash/minBy';
|
||||||
|
|
||||||
const RELOAD_DELAY = 3000;
|
const RELOAD_DELAY = 3000;
|
||||||
|
|
||||||
@ -106,9 +110,11 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
|||||||
|
|
||||||
const codeField = watch('code');
|
const codeField = watch('code');
|
||||||
|
|
||||||
const { data: previewData, loading: previewLoading } = useReferral({
|
const {
|
||||||
code: validateCode(codeField, t) ? codeField : undefined,
|
data: previewData,
|
||||||
});
|
loading: previewLoading,
|
||||||
|
isEligible: isPreviewEligible,
|
||||||
|
} = useReferralSet(validateCode(codeField, t) ? codeField : undefined);
|
||||||
|
|
||||||
const { send, status } = useSimpleTransaction({
|
const { send, status } = useSimpleTransaction({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@ -141,19 +147,14 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
|||||||
* Validates the set a user tries to apply to.
|
* Validates the set a user tries to apply to.
|
||||||
*/
|
*/
|
||||||
const validateSet = useCallback(() => {
|
const validateSet = useCallback(() => {
|
||||||
if (
|
if (codeField && !previewLoading && previewData && !isPreviewEligible) {
|
||||||
codeField &&
|
|
||||||
!previewLoading &&
|
|
||||||
previewData &&
|
|
||||||
!previewData.isEligible
|
|
||||||
) {
|
|
||||||
return t('The code is no longer valid.');
|
return t('The code is no longer valid.');
|
||||||
}
|
}
|
||||||
if (codeField && !previewLoading && !previewData) {
|
if (codeField && !previewLoading && !previewData) {
|
||||||
return t('The code is invalid');
|
return t('The code is invalid');
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}, [codeField, previewData, previewLoading, t]);
|
}, [codeField, isPreviewEligible, previewData, previewLoading, t]);
|
||||||
|
|
||||||
const noFunds = validateFundsAvailable() !== true ? true : false;
|
const noFunds = validateFundsAvailable() !== true ? true : false;
|
||||||
|
|
||||||
@ -200,8 +201,6 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
|||||||
// });
|
// });
|
||||||
};
|
};
|
||||||
|
|
||||||
const { epochsValue, nextBenefitTierValue } = useStats({ program });
|
|
||||||
|
|
||||||
// show "code applied" message when successfully applied
|
// show "code applied" message when successfully applied
|
||||||
if (status === 'confirmed') {
|
if (status === 'confirmed') {
|
||||||
return (
|
return (
|
||||||
@ -264,9 +263,10 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextBenefitTierEpochsValue = nextBenefitTierValue
|
// calculate minimum amount of epochs a referee has to be in a set in order
|
||||||
? nextBenefitTierValue.epochs - epochsValue
|
// to benefit from it
|
||||||
: 0;
|
const firstBenefitTier = minBy(program.benefitTiers, (bt) => bt.epochs);
|
||||||
|
const minEpochs = firstBenefitTier ? firstBenefitTier.epochs : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -335,17 +335,17 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
|||||||
<Loader />
|
<Loader />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{/* TODO: Re-check plural forms once i18n is updated */}
|
|
||||||
{previewData && previewData.isEligible ? (
|
{previewData && isPreviewEligible ? (
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
<h2 className="mb-5 text-2xl">
|
<h2 className="mb-5 text-2xl">
|
||||||
{t(
|
{t(
|
||||||
'youAreJoiningTheGroup',
|
'youAreJoiningTheGroup',
|
||||||
'You are joining the group shown, but will not have access to benefits until you have completed at least {{count}} epochs.',
|
'You are joining the group shown, but will not have access to benefits until you have completed at least {{count}} epochs.',
|
||||||
{ count: nextBenefitTierEpochsValue }
|
{ count: minEpochs }
|
||||||
)}
|
)}
|
||||||
</h2>
|
</h2>
|
||||||
<Statistics data={previewData} program={program} as="referee" />
|
<PreviewRefereeStatistics setId={codeField} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import { type ApolloError } from '@apollo/client';
|
||||||
|
import { getUserLocale } from '@vegaprotocol/utils';
|
||||||
|
|
||||||
export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
|
export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
|
||||||
export const GRADIENT =
|
export const GRADIENT =
|
||||||
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
|
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
|
||||||
@ -8,3 +11,19 @@ export const REFERRAL_DOCS_LINK =
|
|||||||
export const ABOUT_REFERRAL_DOCS_LINK =
|
export const ABOUT_REFERRAL_DOCS_LINK =
|
||||||
'https://docs.vega.xyz/mainnet/concepts/trading-on-vega/discounts-rewards#referral-program';
|
'https://docs.vega.xyz/mainnet/concepts/trading-on-vega/discounts-rewards#referral-program';
|
||||||
export const DISCLAIMER_REFERRAL_DOCS_LINK = 'https://docs.vega.xyz/';
|
export const DISCLAIMER_REFERRAL_DOCS_LINK = 'https://docs.vega.xyz/';
|
||||||
|
|
||||||
|
export const DEFAULT_AGGREGATION_DAYS = 30;
|
||||||
|
|
||||||
|
export type StatValue<T> = {
|
||||||
|
value: T;
|
||||||
|
loading: boolean;
|
||||||
|
error?: ApolloError | Error;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const COMPACT_NUMBER_FORMAT = (maximumFractionDigits = 2) =>
|
||||||
|
new Intl.NumberFormat(getUserLocale(), {
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits,
|
||||||
|
notation: 'compact',
|
||||||
|
compactDisplay: 'short',
|
||||||
|
});
|
||||||
|
@ -16,13 +16,16 @@ import {
|
|||||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||||
import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment';
|
import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment';
|
||||||
import { ABOUT_REFERRAL_DOCS_LINK } from './constants';
|
import { ABOUT_REFERRAL_DOCS_LINK } from './constants';
|
||||||
import { useIsInReferralSet, useReferral } from './hooks/use-referral';
|
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { Link, Navigate, useNavigate } from 'react-router-dom';
|
import { Link, Navigate, useNavigate } from 'react-router-dom';
|
||||||
import { Links, Routes } from '../../lib/links';
|
import { Links, Routes } from '../../lib/links';
|
||||||
import { useReferralProgram } from './hooks/use-referral-program';
|
import { useReferralProgram } from './hooks/use-referral-program';
|
||||||
import { useReferralSetTransaction } from '../../lib/hooks/use-referral-set-transaction';
|
import { useReferralSetTransaction } from '../../lib/hooks/use-referral-set-transaction';
|
||||||
import { Trans } from 'react-i18next';
|
import { Trans } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
useFindReferralSet,
|
||||||
|
useIsInReferralSet,
|
||||||
|
} from './hooks/use-find-referral-set';
|
||||||
|
|
||||||
export const CreateCodeContainer = () => {
|
export const CreateCodeContainer = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
@ -145,7 +148,7 @@ const CreateCodeDialog = ({
|
|||||||
const t = useT();
|
const t = useT();
|
||||||
const createLink = useLinks(DApp.Governance);
|
const createLink = useLinks(DApp.Governance);
|
||||||
const { pubKey } = useVegaWallet();
|
const { pubKey } = useVegaWallet();
|
||||||
const { refetch } = useReferral({ pubKey, role: 'referrer' });
|
const { refetch } = useFindReferralSet(pubKey);
|
||||||
const {
|
const {
|
||||||
err,
|
err,
|
||||||
code,
|
code,
|
||||||
|
@ -0,0 +1,122 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
type ReferralSetsQueryVariables,
|
||||||
|
useReferralSetsQuery,
|
||||||
|
} from './__generated__/ReferralSets';
|
||||||
|
import { useStakeAvailable } from '../../../lib/hooks/use-stake-available';
|
||||||
|
|
||||||
|
export type Role = 'referrer' | 'referee';
|
||||||
|
type Args = (
|
||||||
|
| { setId: string | undefined }
|
||||||
|
| { pubKey: string | undefined; role: Role }
|
||||||
|
) & {
|
||||||
|
aggregationEpochs?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const prepareVariables = (
|
||||||
|
args: Args
|
||||||
|
): [ReferralSetsQueryVariables, boolean] => {
|
||||||
|
const byId = 'setId' in args;
|
||||||
|
const byRole = 'pubKey' in args && 'role' in args;
|
||||||
|
let variables = {};
|
||||||
|
let skip = true;
|
||||||
|
if (byId) {
|
||||||
|
variables = {
|
||||||
|
id: args.setId,
|
||||||
|
};
|
||||||
|
skip = !args.setId;
|
||||||
|
}
|
||||||
|
if (byRole) {
|
||||||
|
if (args.role === 'referee') {
|
||||||
|
variables = { referee: args.pubKey };
|
||||||
|
}
|
||||||
|
if (args.role === 'referrer') {
|
||||||
|
variables = { referrer: args.pubKey };
|
||||||
|
}
|
||||||
|
skip = !args.pubKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [variables, skip];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFindReferralSet = (pubKey?: string) => {
|
||||||
|
const [referrerVariables, referrerSkip] = prepareVariables({
|
||||||
|
pubKey,
|
||||||
|
role: 'referrer',
|
||||||
|
});
|
||||||
|
const [refereeVariables, refereeSkip] = prepareVariables({
|
||||||
|
pubKey,
|
||||||
|
role: 'referee',
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: referrerData,
|
||||||
|
loading: referrerLoading,
|
||||||
|
error: referrerError,
|
||||||
|
refetch: referrerRefetch,
|
||||||
|
} = useReferralSetsQuery({
|
||||||
|
variables: referrerVariables,
|
||||||
|
skip: referrerSkip,
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
data: refereeData,
|
||||||
|
loading: refereeLoading,
|
||||||
|
error: refereeError,
|
||||||
|
refetch: refereeRefetch,
|
||||||
|
} = useReferralSetsQuery({
|
||||||
|
variables: refereeVariables,
|
||||||
|
skip: refereeSkip,
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
});
|
||||||
|
|
||||||
|
const set =
|
||||||
|
referrerData?.referralSets.edges[0]?.node ||
|
||||||
|
refereeData?.referralSets.edges[0]?.node;
|
||||||
|
const role: Role | undefined = set
|
||||||
|
? set?.referrer === pubKey
|
||||||
|
? 'referrer'
|
||||||
|
: 'referee'
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const { isEligible } = useStakeAvailable(set?.referrer);
|
||||||
|
|
||||||
|
const refetch = useCallback(() => {
|
||||||
|
referrerRefetch();
|
||||||
|
refereeRefetch();
|
||||||
|
}, [refereeRefetch, referrerRefetch]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: set,
|
||||||
|
role,
|
||||||
|
loading: referrerLoading || refereeLoading,
|
||||||
|
error: referrerError || refereeError,
|
||||||
|
refetch,
|
||||||
|
isEligible: set ? isEligible : undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useReferralSet = (setId?: string) => {
|
||||||
|
const [variables, skip] = prepareVariables({ setId });
|
||||||
|
const { data, loading, error, refetch } = useReferralSetsQuery({
|
||||||
|
variables,
|
||||||
|
skip,
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
});
|
||||||
|
|
||||||
|
const set = data?.referralSets.edges[0]?.node;
|
||||||
|
const { isEligible } = useStakeAvailable(set?.referrer);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: set,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
isEligible: set ? isEligible : undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useIsInReferralSet = (pubKey: string | undefined) => {
|
||||||
|
const { data } = useFindReferralSet(pubKey);
|
||||||
|
return Boolean(data);
|
||||||
|
};
|
117
apps/trading/client-pages/referrals/hooks/use-referee-stats.ts
Normal file
117
apps/trading/client-pages/referrals/hooks/use-referee-stats.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||||
|
import { useReferralSetStatsQuery } from './__generated__/ReferralSetStats';
|
||||||
|
import { findReferee, useReferees } from './use-referees';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import { type BenefitTier, useReferralProgram } from './use-referral-program';
|
||||||
|
import { type StatValue } from '../constants';
|
||||||
|
import minBy from 'lodash/minBy';
|
||||||
|
import { useEpochInfoQuery } from '../../../lib/hooks/__generated__/Epoch';
|
||||||
|
|
||||||
|
export type RefereeStats = {
|
||||||
|
/** the discount factor -> `discountFactor` ~ `referralDiscountFactor` */
|
||||||
|
discountFactor: StatValue<BigNumber>;
|
||||||
|
/** the benefit tier matching the referee's discount factor */
|
||||||
|
benefitTier: StatValue<BenefitTier | undefined>;
|
||||||
|
/** the next benefit tier after the current referee's tier */
|
||||||
|
nextBenefitTier: StatValue<BenefitTier | undefined>;
|
||||||
|
/** the running volume */
|
||||||
|
runningVolume: StatValue<BigNumber>;
|
||||||
|
/** the number of epochs in set */
|
||||||
|
epochs: StatValue<BigNumber>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ZERO = BigNumber(0);
|
||||||
|
|
||||||
|
export const useRefereeStats = (
|
||||||
|
pubKey: string,
|
||||||
|
setId: string,
|
||||||
|
aggregationEpochs: number
|
||||||
|
): RefereeStats => {
|
||||||
|
const { data, loading, error } = useReferralSetStatsQuery({
|
||||||
|
variables: {
|
||||||
|
code: setId,
|
||||||
|
},
|
||||||
|
skip: !setId || setId.length === 0 || !pubKey || pubKey.length === 0,
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
benefitTiers,
|
||||||
|
loading: programLoading,
|
||||||
|
error: programError,
|
||||||
|
} = useReferralProgram();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: epochData,
|
||||||
|
loading: epochsLoading,
|
||||||
|
error: epochsError,
|
||||||
|
} = useEpochInfoQuery({
|
||||||
|
fetchPolicy: 'network-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: refereesData,
|
||||||
|
loading: refereesLoading,
|
||||||
|
error: refereesError,
|
||||||
|
} = useReferees(setId, aggregationEpochs);
|
||||||
|
|
||||||
|
const referee = findReferee(pubKey, refereesData);
|
||||||
|
const stats = removePaginationWrapper(data?.referralSetStats.edges).find(
|
||||||
|
(s) => s.partyId === pubKey
|
||||||
|
);
|
||||||
|
|
||||||
|
const discountFactor = {
|
||||||
|
value: stats?.discountFactor ? BigNumber(stats.discountFactor) : ZERO,
|
||||||
|
loading: loading || refereesLoading,
|
||||||
|
error: error || refereesError,
|
||||||
|
};
|
||||||
|
|
||||||
|
const benefitTier = {
|
||||||
|
value: benefitTiers.find(
|
||||||
|
(t) =>
|
||||||
|
!discountFactor.value.isNaN() &&
|
||||||
|
!isNaN(t.discountFactor) &&
|
||||||
|
t.discountFactor === discountFactor.value.toNumber()
|
||||||
|
),
|
||||||
|
loading: programLoading || discountFactor.loading,
|
||||||
|
error: programError || discountFactor.error,
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextTier = benefitTier.value?.tier
|
||||||
|
? benefitTier.value.tier + 1
|
||||||
|
: undefined;
|
||||||
|
const nextBenefitTier = {
|
||||||
|
value: nextTier
|
||||||
|
? benefitTiers.find((t) => t.tier === nextTier)
|
||||||
|
: minBy(benefitTiers, (t) => t.tier), // min tier number is lowest tier
|
||||||
|
loading: benefitTier.loading,
|
||||||
|
error: benefitTier.error,
|
||||||
|
};
|
||||||
|
|
||||||
|
const runningVolume = {
|
||||||
|
value: stats?.referralSetRunningNotionalTakerVolume
|
||||||
|
? BigNumber(stats.referralSetRunningNotionalTakerVolume)
|
||||||
|
: ZERO,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
|
||||||
|
const joinedAtEpoch = BigNumber(referee?.atEpoch || '');
|
||||||
|
const currentEpoch = BigNumber(epochData?.epoch.id || '');
|
||||||
|
const epochs = {
|
||||||
|
value:
|
||||||
|
!currentEpoch.isNaN() && !joinedAtEpoch.isNaN()
|
||||||
|
? currentEpoch.minus(joinedAtEpoch)
|
||||||
|
: ZERO,
|
||||||
|
loading: refereesLoading || epochsLoading,
|
||||||
|
error: refereesError || epochsError,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
discountFactor,
|
||||||
|
benefitTier,
|
||||||
|
nextBenefitTier,
|
||||||
|
runningVolume,
|
||||||
|
epochs,
|
||||||
|
};
|
||||||
|
};
|
107
apps/trading/client-pages/referrals/hooks/use-referees.ts
Normal file
107
apps/trading/client-pages/referrals/hooks/use-referees.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { type RefereesQuery } from './__generated__/Referees';
|
||||||
|
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||||
|
import { useRefereesQuery } from './__generated__/Referees';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import pick from 'lodash/pick';
|
||||||
|
|
||||||
|
export type Referee = Omit<
|
||||||
|
NonNullable<RefereesQuery['referralSetReferees']['edges'][0]>['node'],
|
||||||
|
'__typename'
|
||||||
|
>;
|
||||||
|
|
||||||
|
/** The properties that can be overwritten by `propertiesOptions`. */
|
||||||
|
type RefereeProperty = keyof Pick<
|
||||||
|
Referee,
|
||||||
|
'totalRefereeGeneratedRewards' | 'totalRefereeNotionalTakerVolume'
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options determining which properties should be overwritten based
|
||||||
|
* on the different `aggregationEpochs`.
|
||||||
|
*/
|
||||||
|
export type PropertiesWithDifferentAggregationEpochs = {
|
||||||
|
properties: RefereeProperty[];
|
||||||
|
aggregationEpochs: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Find referee by its public key (id) */
|
||||||
|
export const findReferee = (pubKey: string, referees: Referee[]) =>
|
||||||
|
referees.find((r) => r.refereeId === pubKey);
|
||||||
|
|
||||||
|
export const useReferees = (
|
||||||
|
id: string | undefined | null,
|
||||||
|
aggregationEpochs: number,
|
||||||
|
propertiesOptions?: PropertiesWithDifferentAggregationEpochs
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
data: refereesData,
|
||||||
|
loading: refereesLoading,
|
||||||
|
error: refereesError,
|
||||||
|
refetch: refereesRefetch,
|
||||||
|
} = useRefereesQuery({
|
||||||
|
variables: {
|
||||||
|
code: id as string,
|
||||||
|
aggregationEpochs,
|
||||||
|
},
|
||||||
|
skip: !id,
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
context: { isEnlargedTimeout: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: extraData,
|
||||||
|
loading: extraLoading,
|
||||||
|
error: extraError,
|
||||||
|
refetch: extraRefetch,
|
||||||
|
} = useRefereesQuery({
|
||||||
|
variables: {
|
||||||
|
code: id as string,
|
||||||
|
aggregationEpochs: propertiesOptions?.aggregationEpochs,
|
||||||
|
},
|
||||||
|
skip:
|
||||||
|
// skip if the aggregation epochs are the same
|
||||||
|
!id ||
|
||||||
|
!propertiesOptions?.aggregationEpochs ||
|
||||||
|
propertiesOptions.aggregationEpochs === aggregationEpochs,
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
context: { isEnlargedTimeout: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
let referees = [];
|
||||||
|
|
||||||
|
const refereesList = removePaginationWrapper(
|
||||||
|
refereesData?.referralSetReferees.edges
|
||||||
|
);
|
||||||
|
const extraRefereesList = removePaginationWrapper(
|
||||||
|
extraData?.referralSetReferees.edges
|
||||||
|
);
|
||||||
|
|
||||||
|
referees = refereesList.map((r) =>
|
||||||
|
overwriteProperties(r, extraRefereesList, propertiesOptions?.properties)
|
||||||
|
);
|
||||||
|
|
||||||
|
const loading = refereesLoading || extraLoading;
|
||||||
|
const error = refereesError || extraError;
|
||||||
|
const refetch = useCallback(() => {
|
||||||
|
refereesRefetch();
|
||||||
|
extraRefetch();
|
||||||
|
}, [refereesRefetch, extraRefetch]);
|
||||||
|
|
||||||
|
return { data: referees, loading, error, refetch };
|
||||||
|
};
|
||||||
|
|
||||||
|
const overwriteProperties = (
|
||||||
|
referee: Referee,
|
||||||
|
referees: Referee[],
|
||||||
|
properties?: PropertiesWithDifferentAggregationEpochs['properties']
|
||||||
|
) => {
|
||||||
|
let updatedProperties = {};
|
||||||
|
const extraRefereeData = findReferee(referee.refereeId, referees);
|
||||||
|
if (properties && extraRefereeData) {
|
||||||
|
updatedProperties = pick(extraRefereeData, properties);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...referee,
|
||||||
|
...updatedProperties,
|
||||||
|
};
|
||||||
|
};
|
@ -1,8 +1,12 @@
|
|||||||
import { formatNumber } from '@vegaprotocol/utils';
|
import { formatNumber } from '@vegaprotocol/utils';
|
||||||
import sortBy from 'lodash/sortBy';
|
import sortBy from 'lodash/sortBy';
|
||||||
import omit from 'lodash/omit';
|
import omit from 'lodash/omit';
|
||||||
import { useReferralProgramQuery } from './__generated__/CurrentReferralProgram';
|
import {
|
||||||
|
type ReferralProgramQuery,
|
||||||
|
useReferralProgramQuery,
|
||||||
|
} from './__generated__/CurrentReferralProgram';
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
|
import { type ApolloError } from '@apollo/client';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const MOCK = {
|
const MOCK = {
|
||||||
@ -82,7 +86,37 @@ const MOCK = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useReferralProgram = () => {
|
type ProgramDetail = Omit<
|
||||||
|
NonNullable<ReferralProgramQuery['currentReferralProgram']>,
|
||||||
|
'benefitTiers' | 'stakingTiers'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type BenefitTier = {
|
||||||
|
tier: number;
|
||||||
|
rewardFactor: number;
|
||||||
|
commission: string;
|
||||||
|
discountFactor: number;
|
||||||
|
discount: string;
|
||||||
|
minimumVolume: number;
|
||||||
|
volume: string;
|
||||||
|
epochs: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StakingTier = {
|
||||||
|
tier: number;
|
||||||
|
minimumStakedTokens: string;
|
||||||
|
referralRewardMultiplier: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReferralProgramData = {
|
||||||
|
benefitTiers: BenefitTier[];
|
||||||
|
stakingTiers: StakingTier[];
|
||||||
|
details: ProgramDetail | undefined;
|
||||||
|
loading: boolean;
|
||||||
|
error?: ApolloError;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useReferralProgram = (): ReferralProgramData => {
|
||||||
const { data, loading, error } = useReferralProgramQuery({
|
const { data, loading, error } = useReferralProgramQuery({
|
||||||
fetchPolicy: 'cache-and-network',
|
fetchPolicy: 'cache-and-network',
|
||||||
});
|
});
|
||||||
|
@ -5,20 +5,22 @@ import {
|
|||||||
ToastHeading,
|
ToastHeading,
|
||||||
Button,
|
Button,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { useReferral } from './use-referral';
|
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet-react';
|
import { useVegaWallet } from '@vegaprotocol/wallet-react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useT } from '../../../lib/use-t';
|
import { useT } from '../../../lib/use-t';
|
||||||
import { matchPath, useLocation, useNavigate } from 'react-router-dom';
|
import { matchPath, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { Routes } from '../../../lib/links';
|
import { Routes } from '../../../lib/links';
|
||||||
import { useEpochInfoQuery } from '../../../lib/hooks/__generated__/Epoch';
|
import { useEpochInfoQuery } from '../../../lib/hooks/__generated__/Epoch';
|
||||||
|
import { useFindReferralSet } from './use-find-referral-set';
|
||||||
|
|
||||||
const REFETCH_INTERVAL = 60 * 60 * 1000; // 1h
|
const REFETCH_INTERVAL = 60 * 60 * 1000; // 1h
|
||||||
const NON_ELIGIBLE_REFERRAL_SET_TOAST_ID = 'non-eligible-referral-set';
|
const NON_ELIGIBLE_REFERRAL_SET_TOAST_ID = 'non-eligible-referral-set';
|
||||||
|
|
||||||
const useNonEligibleReferralSet = () => {
|
const useNonEligibleReferralSet = () => {
|
||||||
const { pubKey } = useVegaWallet();
|
const { pubKey } = useVegaWallet();
|
||||||
const { data, loading, refetch } = useReferral({ pubKey, role: 'referee' });
|
const { data, loading, role, isEligible, refetch } =
|
||||||
|
useFindReferralSet(pubKey);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: epochData,
|
data: epochData,
|
||||||
loading: epochLoading,
|
loading: epochLoading,
|
||||||
@ -36,7 +38,13 @@ const useNonEligibleReferralSet = () => {
|
|||||||
};
|
};
|
||||||
}, [epochRefetch, refetch]);
|
}, [epochRefetch, refetch]);
|
||||||
|
|
||||||
return { data, epoch: epochData?.epoch.id, loading: loading || epochLoading };
|
return {
|
||||||
|
data,
|
||||||
|
isEligible,
|
||||||
|
role,
|
||||||
|
epoch: epochData?.epoch.id,
|
||||||
|
loading: loading || epochLoading,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useReferralToasts = () => {
|
export const useReferralToasts = () => {
|
||||||
@ -49,14 +57,16 @@ export const useReferralToasts = () => {
|
|||||||
store.update,
|
store.update,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { data, epoch, loading } = useNonEligibleReferralSet();
|
const { data, role, isEligible, epoch, loading } =
|
||||||
|
useNonEligibleReferralSet();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
data &&
|
data &&
|
||||||
|
role === 'referee' &&
|
||||||
epoch &&
|
epoch &&
|
||||||
!loading &&
|
!loading &&
|
||||||
!data.isEligible &&
|
!isEligible &&
|
||||||
!hasToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch)
|
!hasToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch)
|
||||||
) {
|
) {
|
||||||
const nonEligibleReferralToast: Toast = {
|
const nonEligibleReferralToast: Toast = {
|
||||||
@ -98,9 +108,11 @@ export const useReferralToasts = () => {
|
|||||||
data,
|
data,
|
||||||
epoch,
|
epoch,
|
||||||
hasToast,
|
hasToast,
|
||||||
|
isEligible,
|
||||||
loading,
|
loading,
|
||||||
navigate,
|
navigate,
|
||||||
pathname,
|
pathname,
|
||||||
|
role,
|
||||||
setToast,
|
setToast,
|
||||||
t,
|
t,
|
||||||
updateToast,
|
updateToast,
|
||||||
|
@ -1,217 +0,0 @@
|
|||||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { useRefereesQuery } from './__generated__/Referees';
|
|
||||||
import compact from 'lodash/compact';
|
|
||||||
import pick from 'lodash/pick';
|
|
||||||
import type {
|
|
||||||
ReferralSetsQuery,
|
|
||||||
ReferralSetsQueryVariables,
|
|
||||||
} from './__generated__/ReferralSets';
|
|
||||||
import { useReferralSetsQuery } from './__generated__/ReferralSets';
|
|
||||||
import { useStakeAvailable } from '../../../lib/hooks/use-stake-available';
|
|
||||||
|
|
||||||
export const DEFAULT_AGGREGATION_DAYS = 30;
|
|
||||||
|
|
||||||
export type Role = 'referrer' | 'referee';
|
|
||||||
type UseReferralArgs = (
|
|
||||||
| { code: string | undefined }
|
|
||||||
| { pubKey: string | undefined; role: Role }
|
|
||||||
) & {
|
|
||||||
aggregationEpochs?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const prepareVariables = (
|
|
||||||
args: UseReferralArgs
|
|
||||||
): [ReferralSetsQueryVariables, boolean] => {
|
|
||||||
const byCode = 'code' in args;
|
|
||||||
const byRole = 'pubKey' in args && 'role' in args;
|
|
||||||
let variables = {};
|
|
||||||
let skip = true;
|
|
||||||
if (byCode) {
|
|
||||||
variables = {
|
|
||||||
id: args.code,
|
|
||||||
};
|
|
||||||
skip = !args.code;
|
|
||||||
}
|
|
||||||
if (byRole) {
|
|
||||||
if (args.role === 'referee') {
|
|
||||||
variables = { referee: args.pubKey };
|
|
||||||
}
|
|
||||||
if (args.role === 'referrer') {
|
|
||||||
variables = { referrer: args.pubKey };
|
|
||||||
}
|
|
||||||
skip = !args.pubKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [variables, skip];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useReferral = (args: UseReferralArgs) => {
|
|
||||||
const [variables, skip] = prepareVariables(args);
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: referralData,
|
|
||||||
loading: referralLoading,
|
|
||||||
error: referralError,
|
|
||||||
refetch: referralRefetch,
|
|
||||||
} = useReferralSetsQuery({
|
|
||||||
variables,
|
|
||||||
skip,
|
|
||||||
fetchPolicy: 'cache-and-network',
|
|
||||||
});
|
|
||||||
|
|
||||||
// A user can only have 1 active referral program at a time
|
|
||||||
const referralSet =
|
|
||||||
referralData?.referralSets.edges &&
|
|
||||||
referralData.referralSets.edges.length > 0
|
|
||||||
? referralData.referralSets.edges[0]?.node
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const { isEligible } = useStakeAvailable(referralSet?.referrer);
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: refereesData,
|
|
||||||
loading: refereesLoading,
|
|
||||||
error: refereesError,
|
|
||||||
refetch: refereesRefetch,
|
|
||||||
} = useRefereesQuery({
|
|
||||||
variables: {
|
|
||||||
code: referralSet?.id as string,
|
|
||||||
aggregationEpochs:
|
|
||||||
args.aggregationEpochs !== null
|
|
||||||
? args.aggregationEpochs
|
|
||||||
: DEFAULT_AGGREGATION_DAYS,
|
|
||||||
},
|
|
||||||
skip: !referralSet?.id,
|
|
||||||
fetchPolicy: 'cache-and-network',
|
|
||||||
context: { isEnlargedTimeout: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
const referees = compact(
|
|
||||||
removePaginationWrapper(refereesData?.referralSetReferees.edges)
|
|
||||||
);
|
|
||||||
|
|
||||||
const refetch = useCallback(() => {
|
|
||||||
referralRefetch();
|
|
||||||
refereesRefetch();
|
|
||||||
}, [refereesRefetch, referralRefetch]);
|
|
||||||
|
|
||||||
const byReferee =
|
|
||||||
'role' in args && 'pubKey' in args && args.role === 'referee';
|
|
||||||
const referee = byReferee
|
|
||||||
? referees.find((r) => r.refereeId === args.pubKey) || null
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const data =
|
|
||||||
referralSet && refereesData
|
|
||||||
? {
|
|
||||||
code: referralSet.id,
|
|
||||||
role: 'role' in args ? args.role : null,
|
|
||||||
referee: referee,
|
|
||||||
referrerId: referralSet.referrer,
|
|
||||||
createdAt: referralSet.createdAt,
|
|
||||||
isEligible,
|
|
||||||
referees,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
loading: referralLoading || refereesLoading,
|
|
||||||
error: referralError || refereesError,
|
|
||||||
refetch,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type Referee = NonNullable<
|
|
||||||
NonNullable<ReturnType<typeof useReferral>['data']>['referee']
|
|
||||||
>;
|
|
||||||
|
|
||||||
type RefereeProperties = (keyof Referee)[];
|
|
||||||
|
|
||||||
const findReferee = (referee: Referee, referees: Referee[]) =>
|
|
||||||
referees.find((r) => r.refereeId === referee?.refereeId) || referee;
|
|
||||||
|
|
||||||
const updateReferee = (
|
|
||||||
referee: Referee,
|
|
||||||
referees: Referee[],
|
|
||||||
properties: RefereeProperties
|
|
||||||
) => ({
|
|
||||||
...referee,
|
|
||||||
...pick(findReferee(referee, referees), properties),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const useUpdateReferees = (
|
|
||||||
referral: ReturnType<typeof useReferral>,
|
|
||||||
aggregationEpochs: number,
|
|
||||||
properties: RefereeProperties,
|
|
||||||
skip?: boolean
|
|
||||||
): ReturnType<typeof useReferral> => {
|
|
||||||
const { data, loading, error, refetch } = useRefereesQuery({
|
|
||||||
variables: {
|
|
||||||
code: referral?.data?.code as string,
|
|
||||||
aggregationEpochs,
|
|
||||||
},
|
|
||||||
skip: skip || !referral?.data?.code,
|
|
||||||
fetchPolicy: 'cache-and-network',
|
|
||||||
context: { isEnlargedTimeout: true },
|
|
||||||
});
|
|
||||||
const refetchAll = useCallback(() => {
|
|
||||||
refetch();
|
|
||||||
referral.refetch();
|
|
||||||
}, [refetch, referral]);
|
|
||||||
if (!referral.data || skip) {
|
|
||||||
return referral;
|
|
||||||
}
|
|
||||||
const referees = compact(
|
|
||||||
removePaginationWrapper(data?.referralSetReferees.edges)
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: data && {
|
|
||||||
...referral.data,
|
|
||||||
referees: referral.data.referees.map((referee) =>
|
|
||||||
updateReferee(referee, referees, properties)
|
|
||||||
),
|
|
||||||
referee:
|
|
||||||
referral.data.referee &&
|
|
||||||
updateReferee(referral.data.referee, referees, properties),
|
|
||||||
},
|
|
||||||
loading: loading || referral.loading,
|
|
||||||
error: error || referral.error,
|
|
||||||
refetch: refetchAll,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const retrieveReferralSetData = (data: ReferralSetsQuery | undefined) =>
|
|
||||||
data?.referralSets.edges && data.referralSets.edges.length > 0
|
|
||||||
? data.referralSets.edges[0]?.node
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
export const useIsInReferralSet = (pubKey: string | undefined) => {
|
|
||||||
const [asRefereeVariables, asRefereeSkip] = prepareVariables({
|
|
||||||
pubKey,
|
|
||||||
role: 'referee',
|
|
||||||
});
|
|
||||||
const [asReferrerVariables, asReferrerSkip] = prepareVariables({
|
|
||||||
pubKey,
|
|
||||||
role: 'referrer',
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: asRefereeData } = useReferralSetsQuery({
|
|
||||||
variables: asRefereeVariables,
|
|
||||||
skip: asRefereeSkip,
|
|
||||||
fetchPolicy: 'cache-and-network',
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: asReferrerData } = useReferralSetsQuery({
|
|
||||||
variables: asReferrerVariables,
|
|
||||||
skip: asReferrerSkip,
|
|
||||||
fetchPolicy: 'cache-and-network',
|
|
||||||
});
|
|
||||||
|
|
||||||
return Boolean(
|
|
||||||
retrieveReferralSetData(asRefereeData) ||
|
|
||||||
retrieveReferralSetData(asReferrerData)
|
|
||||||
);
|
|
||||||
};
|
|
104
apps/trading/client-pages/referrals/hooks/use-referrer-stats.ts
Normal file
104
apps/trading/client-pages/referrals/hooks/use-referrer-stats.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { useReferralSetStatsQuery } from './__generated__/ReferralSetStats';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import { useReferees } from './use-referees';
|
||||||
|
import { type StatValue } from '../constants';
|
||||||
|
|
||||||
|
export type ReferrerStats = {
|
||||||
|
/** the base commission -> `rewardFactor` ~ `referralRewardFactor` */
|
||||||
|
baseCommission: StatValue<BigNumber>;
|
||||||
|
/** the staking multiplier -> `rewardsMultiplier` ~ `referralRewardMultiplier` */
|
||||||
|
multiplier: StatValue<BigNumber>;
|
||||||
|
/** the final commission -> base * multiplier */
|
||||||
|
finalCommission: StatValue<BigNumber>;
|
||||||
|
/** the referrer taker volume -> `referrerTakerVolume` */
|
||||||
|
volume: StatValue<BigNumber>;
|
||||||
|
/** the number of referees -> referees query required */
|
||||||
|
referees: StatValue<BigNumber>;
|
||||||
|
/** the total commission -> sum of `totalRefereeGeneratedRewards` */
|
||||||
|
totalCommission: StatValue<BigNumber>;
|
||||||
|
runningVolume: StatValue<BigNumber>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ZERO = BigNumber(0);
|
||||||
|
const ONE = BigNumber(1);
|
||||||
|
|
||||||
|
export const useReferrerStats = (
|
||||||
|
setId: string,
|
||||||
|
aggregationEpochs: number
|
||||||
|
): ReferrerStats => {
|
||||||
|
const { data, loading, error } = useReferralSetStatsQuery({
|
||||||
|
variables: {
|
||||||
|
code: setId,
|
||||||
|
},
|
||||||
|
skip: !setId || setId.length === 0,
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: refereesData,
|
||||||
|
loading: refereesLoading,
|
||||||
|
error: refereesError,
|
||||||
|
} = useReferees(setId, aggregationEpochs);
|
||||||
|
|
||||||
|
const statsAvailable = data?.referralSetStats.edges[0]?.node;
|
||||||
|
|
||||||
|
const baseCommission = {
|
||||||
|
value: statsAvailable ? BigNumber(statsAvailable.rewardFactor) : ZERO,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
|
||||||
|
const multiplier = {
|
||||||
|
value: statsAvailable ? BigNumber(statsAvailable.rewardsMultiplier) : ONE,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalCommission = {
|
||||||
|
value: !multiplier.value.isNaN()
|
||||||
|
? baseCommission.value
|
||||||
|
: new BigNumber(multiplier.value).times(baseCommission.value),
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
|
||||||
|
const volume = {
|
||||||
|
value: statsAvailable
|
||||||
|
? BigNumber(statsAvailable.referrerTakerVolume)
|
||||||
|
: ZERO,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
|
||||||
|
const referees = {
|
||||||
|
value: BigNumber(refereesData.length),
|
||||||
|
loading: refereesLoading,
|
||||||
|
error: refereesError,
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalCommission = {
|
||||||
|
value: refereesData
|
||||||
|
.map((r) => new BigNumber(r.totalRefereeGeneratedRewards))
|
||||||
|
.reduce((all, r) => all.plus(r), ZERO),
|
||||||
|
loading: refereesLoading,
|
||||||
|
error: refereesError,
|
||||||
|
};
|
||||||
|
|
||||||
|
const runningVolume = {
|
||||||
|
value: statsAvailable?.referralSetRunningNotionalTakerVolume
|
||||||
|
? BigNumber(statsAvailable.referralSetRunningNotionalTakerVolume)
|
||||||
|
: ZERO,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseCommission,
|
||||||
|
multiplier,
|
||||||
|
finalCommission,
|
||||||
|
volume,
|
||||||
|
referees,
|
||||||
|
totalCommission,
|
||||||
|
runningVolume,
|
||||||
|
};
|
||||||
|
};
|
202
apps/trading/client-pages/referrals/referee-statistics.tsx
Normal file
202
apps/trading/client-pages/referrals/referee-statistics.tsx
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import { useRefereeStats } from './hooks/use-referee-stats';
|
||||||
|
import {
|
||||||
|
BenefitTierTile,
|
||||||
|
DiscountTile,
|
||||||
|
EpochsTile,
|
||||||
|
NextTierEpochsTile,
|
||||||
|
NextTierVolumeTile,
|
||||||
|
RunningVolumeTile,
|
||||||
|
TeamTile,
|
||||||
|
} from './tiles';
|
||||||
|
import { useStakeAvailable } from '../../lib/hooks/use-stake-available';
|
||||||
|
import { CodeTile } from './tile';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
import { ApplyCodeForm } from './apply-code-form';
|
||||||
|
import { useVegaWallet } from '@vegaprotocol/wallet-react';
|
||||||
|
import { useReferralProgram } from './hooks/use-referral-program';
|
||||||
|
import { DEFAULT_AGGREGATION_DAYS } from './constants';
|
||||||
|
import { useReferralSet } from './hooks/use-find-referral-set';
|
||||||
|
import { Loader } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import minBy from 'lodash/minBy';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
|
||||||
|
export const RefereeStatistics = ({
|
||||||
|
aggregationEpochs,
|
||||||
|
setId,
|
||||||
|
pubKey,
|
||||||
|
referrerPubKey,
|
||||||
|
}: {
|
||||||
|
/** The aggregation epochs used to calculate statistics. */
|
||||||
|
aggregationEpochs: number;
|
||||||
|
/** The set id (code). */
|
||||||
|
setId: string;
|
||||||
|
/** The referee public key. */
|
||||||
|
pubKey: string;
|
||||||
|
/** The referrer's public key. */
|
||||||
|
referrerPubKey: string;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
const {
|
||||||
|
benefitTier,
|
||||||
|
discountFactor,
|
||||||
|
epochs,
|
||||||
|
nextBenefitTier,
|
||||||
|
runningVolume,
|
||||||
|
} = useRefereeStats(pubKey, setId, aggregationEpochs);
|
||||||
|
|
||||||
|
const { isEligible } = useStakeAvailable(referrerPubKey);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
data-testid="referral-statistics"
|
||||||
|
data-as="referee"
|
||||||
|
className="relative mx-auto mb-20"
|
||||||
|
>
|
||||||
|
<div className={classNames('grid grid-cols-1 grid-rows-1 gap-5')}>
|
||||||
|
{/** TEAM TILE - referral set id is the same as team id */}
|
||||||
|
<TeamTile teamId={setId} />
|
||||||
|
{/** TILES ROW 1 */}
|
||||||
|
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
||||||
|
<BenefitTierTile
|
||||||
|
benefitTier={benefitTier}
|
||||||
|
nextBenefitTier={nextBenefitTier}
|
||||||
|
/>
|
||||||
|
<RunningVolumeTile
|
||||||
|
aggregationEpochs={aggregationEpochs}
|
||||||
|
runningVolume={runningVolume}
|
||||||
|
/>
|
||||||
|
<CodeTile code={setId} />
|
||||||
|
</div>
|
||||||
|
{/** TILES ROW 2 */}
|
||||||
|
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||||
|
<DiscountTile discountFactor={discountFactor} />
|
||||||
|
<NextTierVolumeTile
|
||||||
|
nextBenefitTier={nextBenefitTier}
|
||||||
|
runningVolume={runningVolume}
|
||||||
|
/>
|
||||||
|
<EpochsTile epochs={epochs} />
|
||||||
|
<NextTierEpochsTile
|
||||||
|
epochs={epochs}
|
||||||
|
nextBenefitTier={nextBenefitTier}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/** ELIGIBILITY WARNING */}
|
||||||
|
{!isEligible ? (
|
||||||
|
<div
|
||||||
|
data-testid="referral-eligibility-warning"
|
||||||
|
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center w-1/2 lg:w-1/3"
|
||||||
|
>
|
||||||
|
<h2 className="text-2xl mb-2">
|
||||||
|
{t('Referral code no longer valid')}
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
{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.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{!isEligible && <ApplyCodeForm />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PreviewRefereeStatistics = ({ setId }: { setId: string }) => {
|
||||||
|
const program = useReferralProgram();
|
||||||
|
const aggregationEpochs =
|
||||||
|
program.details?.windowLength || DEFAULT_AGGREGATION_DAYS;
|
||||||
|
|
||||||
|
const { pubKey } = useVegaWallet();
|
||||||
|
|
||||||
|
const { data: referralSet, loading } = useReferralSet(setId);
|
||||||
|
|
||||||
|
const { epochs, runningVolume } = useRefereeStats(
|
||||||
|
pubKey || '',
|
||||||
|
referralSet?.id || '',
|
||||||
|
aggregationEpochs
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-testid="referral-statistics"
|
||||||
|
data-as="referee"
|
||||||
|
data-preview
|
||||||
|
className="relative mx-auto mb-20"
|
||||||
|
>
|
||||||
|
<Loader size="small" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!referralSet) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stat = <T,>(value: T) => ({
|
||||||
|
value,
|
||||||
|
loading: false,
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstBenefitTier = stat(minBy(program.benefitTiers, (bt) => bt.epochs));
|
||||||
|
|
||||||
|
const nextBenefitTier = stat(
|
||||||
|
program.benefitTiers.find(
|
||||||
|
(bt) =>
|
||||||
|
bt.tier ===
|
||||||
|
(firstBenefitTier.value?.tier
|
||||||
|
? firstBenefitTier.value.tier + 1
|
||||||
|
: undefined)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const discountFactor = stat(
|
||||||
|
firstBenefitTier.value?.discountFactor
|
||||||
|
? BigNumber(firstBenefitTier.value?.discountFactor)
|
||||||
|
: BigNumber(0)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-testid="referral-statistics"
|
||||||
|
data-as="referee"
|
||||||
|
data-preview
|
||||||
|
className="relative mx-auto mb-20"
|
||||||
|
>
|
||||||
|
<div className={classNames('grid grid-cols-1 grid-rows-1 gap-5')}>
|
||||||
|
{/** TEAM TILE - referral set id is the same as team id */}
|
||||||
|
<TeamTile teamId={setId} />
|
||||||
|
{/** TILES ROW 1 */}
|
||||||
|
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
||||||
|
<BenefitTierTile
|
||||||
|
benefitTier={firstBenefitTier}
|
||||||
|
nextBenefitTier={nextBenefitTier}
|
||||||
|
/>
|
||||||
|
<RunningVolumeTile
|
||||||
|
aggregationEpochs={aggregationEpochs}
|
||||||
|
runningVolume={runningVolume}
|
||||||
|
/>
|
||||||
|
<CodeTile code={setId} />
|
||||||
|
</div>
|
||||||
|
{/** TILES ROW 2 */}
|
||||||
|
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||||
|
<DiscountTile discountFactor={discountFactor} />
|
||||||
|
<NextTierVolumeTile
|
||||||
|
nextBenefitTier={nextBenefitTier}
|
||||||
|
runningVolume={runningVolume}
|
||||||
|
/>
|
||||||
|
<EpochsTile epochs={epochs} />
|
||||||
|
<NextTierEpochsTile
|
||||||
|
epochs={epochs}
|
||||||
|
nextBenefitTier={nextBenefitTier}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
146
apps/trading/client-pages/referrals/referees.tsx
Normal file
146
apps/trading/client-pages/referrals/referees.tsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { useLayoutEffect, useRef, useState } from 'react';
|
||||||
|
import { ns, useT } from '../../lib/use-t';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import {
|
||||||
|
Loader,
|
||||||
|
Tooltip,
|
||||||
|
VegaIcon,
|
||||||
|
VegaIconNames,
|
||||||
|
truncateMiddle,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { Table } from '../../components/table';
|
||||||
|
import { formatNumber, getDateTimeFormat } from '@vegaprotocol/utils';
|
||||||
|
import sortBy from 'lodash/sortBy';
|
||||||
|
import { Trans } from 'react-i18next';
|
||||||
|
import { QUSDTooltip } from './qusd-tooltip';
|
||||||
|
import { type Referee, useReferees } from './hooks/use-referees';
|
||||||
|
import { DEFAULT_AGGREGATION_DAYS } from './constants';
|
||||||
|
|
||||||
|
export const Referees = ({
|
||||||
|
setId,
|
||||||
|
aggregationEpochs,
|
||||||
|
}: {
|
||||||
|
setId: string;
|
||||||
|
aggregationEpochs: number;
|
||||||
|
}) => {
|
||||||
|
const { data, loading } = useReferees(setId, aggregationEpochs, {
|
||||||
|
// get total referree generated rewards for the last 30 days
|
||||||
|
aggregationEpochs: DEFAULT_AGGREGATION_DAYS,
|
||||||
|
properties: ['totalRefereeGeneratedRewards'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Loader size="small" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <RefereesTable data={data} aggregationEpochs={aggregationEpochs} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RefereesTable = ({
|
||||||
|
data: referees,
|
||||||
|
aggregationEpochs,
|
||||||
|
}: {
|
||||||
|
data: Referee[];
|
||||||
|
aggregationEpochs: number;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
const tableRef = useRef<HTMLTableElement>(null);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if ((tableRef.current?.getBoundingClientRect().height || 0) > 384) {
|
||||||
|
setCollapsed(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Referees (only for referrer view) */}
|
||||||
|
{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(
|
||||||
|
'volumeLastEpochs',
|
||||||
|
'Volume (last {{count}} epochs)',
|
||||||
|
{
|
||||||
|
count: aggregationEpochs,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// NOTE: This should be gotten for the last 30 days regardless of the program's window length
|
||||||
|
name: 'commission',
|
||||||
|
displayName: (
|
||||||
|
<Trans
|
||||||
|
i18nKey="referralStatisticsCommission"
|
||||||
|
defaults="Commission earned in <0>qUSD</0> (<1>last {{count}} epochs</1>)"
|
||||||
|
components={[
|
||||||
|
<QUSDTooltip key="0" />,
|
||||||
|
<Tooltip
|
||||||
|
key="1"
|
||||||
|
description={t(
|
||||||
|
'Depending on data node retention you may not be able see the full 30 days'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>last 30 epochs</span>
|
||||||
|
</Tooltip>,
|
||||||
|
]}
|
||||||
|
values={{
|
||||||
|
count: DEFAULT_AGGREGATION_DAYS,
|
||||||
|
}}
|
||||||
|
ns={ns}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
data={sortBy(
|
||||||
|
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: formatNumber(r.volume, 0),
|
||||||
|
commission: formatNumber(r.commission, 0),
|
||||||
|
}))
|
||||||
|
.reverse()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,597 +1,58 @@
|
|||||||
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
import { Loader } from '@vegaprotocol/ui-toolkit';
|
||||||
import BigNumber from 'bignumber.js';
|
|
||||||
import minBy from 'lodash/minBy';
|
|
||||||
import sortBy from 'lodash/sortBy';
|
|
||||||
import compact from 'lodash/compact';
|
|
||||||
import { Trans } from 'react-i18next';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import {
|
|
||||||
VegaIcon,
|
|
||||||
VegaIconNames,
|
|
||||||
truncateMiddle,
|
|
||||||
TextChildrenTooltip as Tooltip,
|
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet-react';
|
import { useVegaWallet } from '@vegaprotocol/wallet-react';
|
||||||
import {
|
import { ApplyCodeFormContainer } from './apply-code-form';
|
||||||
addDecimalsFormatNumber,
|
|
||||||
formatNumber,
|
|
||||||
getDateFormat,
|
|
||||||
getDateTimeFormat,
|
|
||||||
getUserLocale,
|
|
||||||
removePaginationWrapper,
|
|
||||||
} from '@vegaprotocol/utils';
|
|
||||||
import { useReferralSetStatsQuery } from './hooks/__generated__/ReferralSetStats';
|
|
||||||
import { useStakeAvailable } from '../../lib/hooks/use-stake-available';
|
|
||||||
import { useT, ns } from '../../lib/use-t';
|
|
||||||
import { useTeam } from '../../lib/hooks/use-team';
|
|
||||||
import { TeamAvatar } from '../../components/competitions/team-avatar';
|
|
||||||
import { TeamStats } from '../../components/competitions/team-stats';
|
|
||||||
import { Table } from '../../components/table';
|
|
||||||
import {
|
|
||||||
DEFAULT_AGGREGATION_DAYS,
|
|
||||||
useReferral,
|
|
||||||
useUpdateReferees,
|
|
||||||
} from './hooks/use-referral';
|
|
||||||
import { ApplyCodeForm, ApplyCodeFormContainer } from './apply-code-form';
|
|
||||||
import { useReferralProgram } from './hooks/use-referral-program';
|
import { useReferralProgram } from './hooks/use-referral-program';
|
||||||
import { useEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
|
import { useFindReferralSet } from './hooks/use-find-referral-set';
|
||||||
import { QUSDTooltip } from './qusd-tooltip';
|
import { Referees } from './referees';
|
||||||
import { CodeTile, StatTile, Tile } from './tile';
|
import { ReferrerStatistics } from './referrer-statistics';
|
||||||
import { areTeamGames, useGames } from '../../lib/hooks/use-games';
|
import { RefereeStatistics } from './referee-statistics';
|
||||||
|
import { DEFAULT_AGGREGATION_DAYS } from './constants';
|
||||||
|
|
||||||
export const ReferralStatistics = () => {
|
export const ReferralStatistics = () => {
|
||||||
const { pubKey } = useVegaWallet();
|
const { pubKey } = useVegaWallet();
|
||||||
|
|
||||||
const program = useReferralProgram();
|
const program = useReferralProgram();
|
||||||
|
|
||||||
const { data: referee, refetch: refereeRefetch } = useReferral({
|
const {
|
||||||
pubKey,
|
data: referralSet,
|
||||||
role: 'referee',
|
loading: referralSetLoading,
|
||||||
aggregationEpochs: program.details?.windowLength,
|
role,
|
||||||
});
|
refetch,
|
||||||
|
} = useFindReferralSet(pubKey);
|
||||||
|
|
||||||
const { data: referrer, refetch: referrerRefetch } = useUpdateReferees(
|
if (referralSetLoading) {
|
||||||
useReferral({
|
return <Loader size="small" />;
|
||||||
pubKey,
|
}
|
||||||
role: 'referrer',
|
|
||||||
aggregationEpochs: program.details?.windowLength,
|
|
||||||
}),
|
|
||||||
DEFAULT_AGGREGATION_DAYS,
|
|
||||||
['totalRefereeGeneratedRewards'],
|
|
||||||
DEFAULT_AGGREGATION_DAYS === program.details?.windowLength
|
|
||||||
);
|
|
||||||
|
|
||||||
const refetch = useCallback(() => {
|
const aggregationEpochs =
|
||||||
refereeRefetch();
|
program.details?.windowLength || DEFAULT_AGGREGATION_DAYS;
|
||||||
referrerRefetch();
|
|
||||||
}, [refereeRefetch, referrerRefetch]);
|
|
||||||
|
|
||||||
if (referee?.code) {
|
if (referralSet?.id && role === 'referrer') {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Statistics data={referee} program={program} as="referee" />
|
<ReferrerStatistics
|
||||||
{!referee.isEligible && <ApplyCodeForm />}
|
aggregationEpochs={aggregationEpochs}
|
||||||
|
createdAt={referralSet.createdAt}
|
||||||
|
setId={referralSet.id}
|
||||||
|
/>
|
||||||
|
<Referees
|
||||||
|
setId={referralSet.id}
|
||||||
|
aggregationEpochs={aggregationEpochs}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (referrer?.code) {
|
if (pubKey && referralSet?.id && role === 'referee') {
|
||||||
return (
|
return (
|
||||||
<>
|
<RefereeStatistics
|
||||||
<Statistics data={referrer} program={program} as="referrer" />
|
aggregationEpochs={aggregationEpochs}
|
||||||
<RefereesTable data={referrer} program={program} />
|
pubKey={pubKey}
|
||||||
</>
|
referrerPubKey={referralSet.referrer}
|
||||||
|
setId={referralSet.id}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ApplyCodeFormContainer onSuccess={refetch} />;
|
return <ApplyCodeFormContainer onSuccess={refetch} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useStats = ({
|
|
||||||
data,
|
|
||||||
program,
|
|
||||||
}: {
|
|
||||||
data?: NonNullable<ReturnType<typeof useReferral>['data']>;
|
|
||||||
program: ReturnType<typeof useReferralProgram>;
|
|
||||||
}) => {
|
|
||||||
const { benefitTiers } = program;
|
|
||||||
const { data: epochData } = useEpochInfoQuery({
|
|
||||||
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<ReturnType<typeof useReferral>['data']>;
|
|
||||||
program: ReturnType<typeof useReferralProgram>;
|
|
||||||
as: 'referrer' | 'referee';
|
|
||||||
}) => {
|
|
||||||
const t = useT();
|
|
||||||
const {
|
|
||||||
baseCommissionValue,
|
|
||||||
runningVolumeValue,
|
|
||||||
referrerVolumeValue,
|
|
||||||
multiplier,
|
|
||||||
finalCommissionValue,
|
|
||||||
discountFactorValue,
|
|
||||||
currentBenefitTierValue,
|
|
||||||
epochsValue,
|
|
||||||
nextBenefitTierValue,
|
|
||||||
nextBenefitTierVolumeValue,
|
|
||||||
nextBenefitTierEpochsValue,
|
|
||||||
} = useStats({ data, program });
|
|
||||||
|
|
||||||
const isApplyCodePreview = data.referee === null;
|
|
||||||
|
|
||||||
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 = (
|
|
||||||
<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(),
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
testId="base-commission-rate"
|
|
||||||
overrideWithNoProgram={!details}
|
|
||||||
>
|
|
||||||
{baseCommissionValue * 100}%
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
|
|
||||||
const stakingMultiplierTile = (
|
|
||||||
<StatTile
|
|
||||||
title={t('Staking multiplier')}
|
|
||||||
testId="staking-multiplier"
|
|
||||||
description={
|
|
||||||
<span
|
|
||||||
className={classNames({
|
|
||||||
'text-vega-red': !isEligible,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{t('{{amount}} $VEGA staked', {
|
|
||||||
amount: addDecimalsFormatNumber(
|
|
||||||
stakeAvailable?.toString() || 0,
|
|
||||||
18
|
|
||||||
),
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
overrideWithNoProgram={!details}
|
|
||||||
>
|
|
||||||
{multiplier || t('None')}
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
const baseCommissionFormatted = BigNumber(baseCommissionValue)
|
|
||||||
.times(100)
|
|
||||||
.toString();
|
|
||||||
const finalCommissionFormatted = new BigNumber(finalCommissionValue)
|
|
||||||
.times(100)
|
|
||||||
.toString();
|
|
||||||
const finalCommissionTile = (
|
|
||||||
<StatTile
|
|
||||||
title={t('Final commission rate')}
|
|
||||||
description={
|
|
||||||
!isNaN(multiplier)
|
|
||||||
? `(${baseCommissionFormatted}% ⨉ ${multiplier} = ${finalCommissionFormatted}%)`
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
testId="final-commission-rate"
|
|
||||||
overrideWithNoProgram={!details}
|
|
||||||
>
|
|
||||||
{finalCommissionFormatted}%
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
const numberOfTradersValue = data.referees.length;
|
|
||||||
const numberOfTradersTile = (
|
|
||||||
<StatTile title={t('Number of traders')} testId="number-of-traders">
|
|
||||||
{numberOfTradersValue}
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
|
|
||||||
const codeTile = (
|
|
||||||
<CodeTile
|
|
||||||
code={data?.code}
|
|
||||||
createdAt={getDateFormat().format(new Date(data.createdAt))}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const referrerVolumeTile = (
|
|
||||||
<StatTile
|
|
||||||
title={t('myVolume', 'My volume (last {{count}} epochs)', {
|
|
||||||
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
|
|
||||||
})}
|
|
||||||
testId="my-volume"
|
|
||||||
overrideWithNoProgram={!details}
|
|
||||||
>
|
|
||||||
{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
|
|
||||||
testId="total-commission"
|
|
||||||
title={
|
|
||||||
<Trans
|
|
||||||
i18nKey="totalCommission"
|
|
||||||
defaults="Total commission (<0>last {{count}} epochs</0>)"
|
|
||||||
values={{
|
|
||||||
count: DEFAULT_AGGREGATION_DAYS,
|
|
||||||
}}
|
|
||||||
components={[
|
|
||||||
<Tooltip
|
|
||||||
key="1"
|
|
||||||
description={t(
|
|
||||||
'Depending on data node retention you may not be able see the full 30 days'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
last 30 epochs
|
|
||||||
</Tooltip>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
description={<QUSDTooltip />}
|
|
||||||
>
|
|
||||||
{formatNumber(totalCommissionValue, 0)}
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentBenefitTierTile = (
|
|
||||||
<StatTile
|
|
||||||
title={t('Current tier')}
|
|
||||||
testId="current-tier"
|
|
||||||
description={
|
|
||||||
nextBenefitTierValue?.tier
|
|
||||||
? t('(Next tier: {{nextTier}})', {
|
|
||||||
nextTier: nextBenefitTierValue?.tier,
|
|
||||||
})
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
overrideWithNoProgram={!details}
|
|
||||||
>
|
|
||||||
{isApplyCodePreview
|
|
||||||
? currentBenefitTierValue?.tier || benefitTiers[0]?.tier || 'None'
|
|
||||||
: currentBenefitTierValue?.tier || 'None'}
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
const discountFactorTile = (
|
|
||||||
<StatTile
|
|
||||||
title={t('Discount')}
|
|
||||||
testId="discount"
|
|
||||||
overrideWithNoProgram={!details}
|
|
||||||
>
|
|
||||||
{isApplyCodePreview && benefitTiers.length >= 1
|
|
||||||
? benefitTiers[0].discountFactor * 100
|
|
||||||
: discountFactorValue * 100}
|
|
||||||
%
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
const runningVolumeTile = (
|
|
||||||
<StatTile
|
|
||||||
title={t(
|
|
||||||
'runningNotionalOverEpochs',
|
|
||||||
'Combined volume (last {{count}} epochs)',
|
|
||||||
{
|
|
||||||
count: details?.windowLength,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
testId="combined-volume"
|
|
||||||
overrideWithNoProgram={!details}
|
|
||||||
>
|
|
||||||
{compactNumFormat.format(runningVolumeValue)}
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
const epochsTile = (
|
|
||||||
<StatTile title={t('Epochs in set')} testId="epochs-in-set">
|
|
||||||
{epochsValue}
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
const nextTierVolumeTile = (
|
|
||||||
<StatTile
|
|
||||||
title={t('Volume to next tier')}
|
|
||||||
testId="vol-to-next-tier"
|
|
||||||
overrideWithNoProgram={!details}
|
|
||||||
>
|
|
||||||
{nextBenefitTierVolumeValue <= 0
|
|
||||||
? '0'
|
|
||||||
: compactNumFormat.format(nextBenefitTierVolumeValue)}
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
const nextTierEpochsTile = (
|
|
||||||
<StatTile
|
|
||||||
title={t('Epochs to next tier')}
|
|
||||||
testId="epochs-to-next-tier"
|
|
||||||
overrideWithNoProgram={!details}
|
|
||||||
>
|
|
||||||
{nextBenefitTierEpochsValue <= 0 ? '0' : nextBenefitTierEpochsValue}
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
|
|
||||||
const eligibilityWarningOverlay = as === 'referee' && !isEligible && (
|
|
||||||
<div
|
|
||||||
data-testid="referral-eligibility-warning"
|
|
||||||
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center w-1/2 lg:w-1/3"
|
|
||||||
>
|
|
||||||
<h2 className="text-2xl mb-2">{t('Referral code no longer valid')}</h2>
|
|
||||||
<p>
|
|
||||||
{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.'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const referrerTiles = (
|
|
||||||
<>
|
|
||||||
<Team teamId={data.code} />
|
|
||||||
<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 refereeTiles = (
|
|
||||||
<>
|
|
||||||
<Team teamId={data.code} />
|
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-testid="referral-statistics"
|
|
||||||
data-as={as}
|
|
||||||
className="relative mx-auto mb-20"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={classNames('grid grid-cols-1 grid-rows-1 gap-5', {
|
|
||||||
'opacity-20 pointer-events-none': as === 'referee' && !isEligible,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{as === 'referrer' && referrerTiles}
|
|
||||||
{as === 'referee' && refereeTiles}
|
|
||||||
</div>
|
|
||||||
{eligibilityWarningOverlay}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RefereesTable = ({
|
|
||||||
data,
|
|
||||||
program,
|
|
||||||
}: {
|
|
||||||
data: NonNullable<ReturnType<typeof useReferral>['data']>;
|
|
||||||
program: ReturnType<typeof useReferralProgram>;
|
|
||||||
}) => {
|
|
||||||
const t = useT();
|
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
|
||||||
const tableRef = useRef<HTMLTableElement>(null);
|
|
||||||
const { details } = program;
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if ((tableRef.current?.getBoundingClientRect().height || 0) > 384) {
|
|
||||||
setCollapsed(true);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* Referees (only for referrer view) */}
|
|
||||||
{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(
|
|
||||||
'volumeLastEpochs',
|
|
||||||
'Volume (last {{count}} epochs)',
|
|
||||||
{
|
|
||||||
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'commission',
|
|
||||||
displayName: (
|
|
||||||
<Trans
|
|
||||||
i18nKey="referralStatisticsCommission"
|
|
||||||
defaults="Commission earned in <0>qUSD</0> (<1>last {{count}} epochs</1>)"
|
|
||||||
components={[
|
|
||||||
<QUSDTooltip key="0" />,
|
|
||||||
<Tooltip
|
|
||||||
key="1"
|
|
||||||
description={t(
|
|
||||||
'Depending on data node retention you may not be able see the full 30 days'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
last 30 epochs
|
|
||||||
</Tooltip>,
|
|
||||||
]}
|
|
||||||
values={{
|
|
||||||
count: 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: formatNumber(r.volume, 0),
|
|
||||||
commission: formatNumber(r.commission, 0),
|
|
||||||
}))
|
|
||||||
.reverse()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Team = ({ teamId }: { teamId?: string }) => {
|
|
||||||
const { team, members } = useTeam(teamId);
|
|
||||||
const { data: games } = useGames(teamId);
|
|
||||||
|
|
||||||
if (!team) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tile className="flex gap-3 lg:gap-4">
|
|
||||||
<TeamAvatar teamId={team.teamId} imgUrl={team.avatarUrl} />
|
|
||||||
<div className="flex flex-col items-start gap-1 lg:gap-3">
|
|
||||||
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl">{team.name}</h1>
|
|
||||||
<TeamStats
|
|
||||||
members={members}
|
|
||||||
games={areTeamGames(games) ? games : undefined}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Tile>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
@ -10,12 +10,12 @@ import { TabLink } from './buttons';
|
|||||||
import { Outlet, useMatch } from 'react-router-dom';
|
import { Outlet, useMatch } from 'react-router-dom';
|
||||||
import { Routes } from '../../lib/links';
|
import { Routes } from '../../lib/links';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet-react';
|
import { useVegaWallet } from '@vegaprotocol/wallet-react';
|
||||||
import { useReferral } from './hooks/use-referral';
|
|
||||||
import { REFERRAL_DOCS_LINK } from './constants';
|
import { REFERRAL_DOCS_LINK } from './constants';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { ErrorBoundary } from '../../components/error-boundary';
|
import { ErrorBoundary } from '../../components/error-boundary';
|
||||||
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||||
|
import { useFindReferralSet } from './hooks/use-find-referral-set';
|
||||||
|
|
||||||
const Nav = () => {
|
const Nav = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
@ -34,26 +34,9 @@ export const Referrals = () => {
|
|||||||
const t = useT();
|
const t = useT();
|
||||||
const { pubKey } = useVegaWallet();
|
const { pubKey } = useVegaWallet();
|
||||||
|
|
||||||
const {
|
const { data, loading, error } = useFindReferralSet(pubKey);
|
||||||
data: referee,
|
|
||||||
loading: refereeLoading,
|
|
||||||
error: refereeError,
|
|
||||||
} = useReferral({
|
|
||||||
pubKey,
|
|
||||||
role: 'referee',
|
|
||||||
});
|
|
||||||
const {
|
|
||||||
data: referrer,
|
|
||||||
loading: referrerLoading,
|
|
||||||
error: referrerError,
|
|
||||||
} = useReferral({
|
|
||||||
pubKey,
|
|
||||||
role: 'referrer',
|
|
||||||
});
|
|
||||||
|
|
||||||
const error = refereeError || referrerError;
|
const showNav = !loading && !error && !data;
|
||||||
const loading = refereeLoading || referrerLoading;
|
|
||||||
const showNav = !loading && !error && !referrer && !referee;
|
|
||||||
|
|
||||||
usePageTitle(t('Referrals'));
|
usePageTitle(t('Referrals'));
|
||||||
|
|
||||||
|
73
apps/trading/client-pages/referrals/referrer-statistics.tsx
Normal file
73
apps/trading/client-pages/referrals/referrer-statistics.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import { useReferrerStats } from './hooks/use-referrer-stats';
|
||||||
|
import {
|
||||||
|
BaseCommissionTile,
|
||||||
|
FinalCommissionTile,
|
||||||
|
RefereesTile,
|
||||||
|
StakingMultiplierTile,
|
||||||
|
TeamTile,
|
||||||
|
TotalCommissionTile,
|
||||||
|
VolumeTile,
|
||||||
|
dateFormatter,
|
||||||
|
} from './tiles';
|
||||||
|
import { CodeTile } from './tile';
|
||||||
|
|
||||||
|
export const ReferrerStatistics = ({
|
||||||
|
aggregationEpochs,
|
||||||
|
setId,
|
||||||
|
createdAt,
|
||||||
|
}: {
|
||||||
|
/** The aggregation epochs used to calculate statistics. */
|
||||||
|
aggregationEpochs: number;
|
||||||
|
/** The set id (code). */
|
||||||
|
setId: string;
|
||||||
|
/** The referral set date of creation. */
|
||||||
|
createdAt: string;
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
baseCommission,
|
||||||
|
finalCommission,
|
||||||
|
multiplier,
|
||||||
|
referees,
|
||||||
|
runningVolume,
|
||||||
|
totalCommission,
|
||||||
|
volume,
|
||||||
|
} = useReferrerStats(setId, aggregationEpochs);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-testid="referral-statistics"
|
||||||
|
data-as="referrer"
|
||||||
|
className="relative mx-auto mb-20"
|
||||||
|
>
|
||||||
|
<div className={classNames('grid grid-cols-1 grid-rows-1 gap-5')}>
|
||||||
|
{/** TEAM TILE - referral set id is the same as team id */}
|
||||||
|
<TeamTile teamId={setId} />
|
||||||
|
{/** TILES ROW 1 */}
|
||||||
|
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
||||||
|
<BaseCommissionTile
|
||||||
|
aggregationEpochs={aggregationEpochs}
|
||||||
|
baseCommission={baseCommission}
|
||||||
|
runningVolume={runningVolume}
|
||||||
|
/>
|
||||||
|
<StakingMultiplierTile multiplier={multiplier} />
|
||||||
|
<FinalCommissionTile
|
||||||
|
baseCommission={baseCommission}
|
||||||
|
multiplier={multiplier}
|
||||||
|
finalCommission={finalCommission}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/** TILES ROW 2 */}
|
||||||
|
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||||
|
<CodeTile code={setId} createdAt={dateFormatter(createdAt)} />
|
||||||
|
<VolumeTile aggregationEpochs={aggregationEpochs} volume={volume} />
|
||||||
|
<RefereesTile referees={referees} />
|
||||||
|
<TotalCommissionTile
|
||||||
|
aggregationEpochs={aggregationEpochs}
|
||||||
|
totalCommission={totalCommission}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
440
apps/trading/client-pages/referrals/tiles.tsx
Normal file
440
apps/trading/client-pages/referrals/tiles.tsx
Normal file
@ -0,0 +1,440 @@
|
|||||||
|
import { addDecimalsFormatNumber, getDateFormat } from '@vegaprotocol/utils';
|
||||||
|
import { useStakeAvailable } from '../../lib/hooks/use-stake-available';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { type ReactNode } from 'react';
|
||||||
|
import { Trans } from 'react-i18next';
|
||||||
|
import { type StatValue, COMPACT_NUMBER_FORMAT } from './constants';
|
||||||
|
import { type ReferrerStats } from './hooks/use-referrer-stats';
|
||||||
|
import { type RefereeStats } from './hooks/use-referee-stats';
|
||||||
|
import { QUSDTooltip } from './qusd-tooltip';
|
||||||
|
import { NoProgramTile, StatTile, Tile } from './tile';
|
||||||
|
import { Loader, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { type BenefitTier } from './hooks/use-referral-program';
|
||||||
|
import { useTeam } from '../../lib/hooks/use-team';
|
||||||
|
import { areTeamGames, useGames } from '../../lib/hooks/use-games';
|
||||||
|
import { TeamAvatar } from '../../components/competitions/team-avatar';
|
||||||
|
import { TeamStats } from '../../components/competitions/team-stats';
|
||||||
|
|
||||||
|
/* Formatters */
|
||||||
|
|
||||||
|
const percentageFormatter = (value: BigNumber) =>
|
||||||
|
value.times(100).toFixed(2) + '%';
|
||||||
|
|
||||||
|
const compactFormatter =
|
||||||
|
(maximumFractionDigits = 2) =>
|
||||||
|
(value: BigNumber) =>
|
||||||
|
COMPACT_NUMBER_FORMAT(maximumFractionDigits).format(value.toNumber());
|
||||||
|
|
||||||
|
const valueFormatter = (noValueLabel: string) => (value: BigNumber) => {
|
||||||
|
if (value.isNaN() || value.isZero()) {
|
||||||
|
return noValueLabel;
|
||||||
|
}
|
||||||
|
return value.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dateFormatter = (value: string) => {
|
||||||
|
try {
|
||||||
|
return getDateFormat().format(new Date(value));
|
||||||
|
} catch {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Helpers */
|
||||||
|
|
||||||
|
const Value = <T,>({
|
||||||
|
data: { value, loading, error },
|
||||||
|
formatter,
|
||||||
|
}: {
|
||||||
|
data: StatValue<T>;
|
||||||
|
formatter: (value: T) => ReactNode;
|
||||||
|
}) => {
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<span className="p-[33px]">
|
||||||
|
<Loader size="small" />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
return <span data-error={error.message}>-</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatter(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Referrer tiles */
|
||||||
|
|
||||||
|
export const BaseCommissionTile = ({
|
||||||
|
baseCommission,
|
||||||
|
runningVolume,
|
||||||
|
aggregationEpochs,
|
||||||
|
}: {
|
||||||
|
baseCommission: ReferrerStats['baseCommission'];
|
||||||
|
runningVolume: ReferrerStats['runningVolume'];
|
||||||
|
aggregationEpochs: number;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
const runningVolumeDescription = compactFormatter(2)(runningVolume.value);
|
||||||
|
const description = t(
|
||||||
|
'(Combined set volume {{runningVolume}} over last {{epochs}} epochs)',
|
||||||
|
{
|
||||||
|
runningVolume: runningVolumeDescription,
|
||||||
|
epochs: aggregationEpochs.toString(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatTile
|
||||||
|
title={t('Base commission rate')}
|
||||||
|
description={description}
|
||||||
|
testId="base-commission-rate"
|
||||||
|
>
|
||||||
|
<Value data={baseCommission} formatter={percentageFormatter} />
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StakingMultiplierTile = ({
|
||||||
|
multiplier,
|
||||||
|
}: {
|
||||||
|
multiplier: ReferrerStats['multiplier'];
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
const { stakeAvailable, isEligible } = useStakeAvailable();
|
||||||
|
|
||||||
|
const description = (
|
||||||
|
<span
|
||||||
|
className={classNames({
|
||||||
|
'text-vega-red': !isEligible,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{t('{{amount}} $VEGA staked', {
|
||||||
|
amount: addDecimalsFormatNumber(stakeAvailable?.toString() || 0, 18),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatTile
|
||||||
|
title={t('Staking multiplier')}
|
||||||
|
description={description}
|
||||||
|
testId="staking-multiplier"
|
||||||
|
>
|
||||||
|
<Value data={multiplier} formatter={valueFormatter(t('None'))} />
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FinalCommissionTile = ({
|
||||||
|
baseCommission,
|
||||||
|
multiplier,
|
||||||
|
finalCommission,
|
||||||
|
}: {
|
||||||
|
baseCommission: ReferrerStats['baseCommission'];
|
||||||
|
multiplier: ReferrerStats['multiplier'];
|
||||||
|
finalCommission: ReferrerStats['finalCommission'];
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
const description =
|
||||||
|
!baseCommission.loading && !finalCommission.loading && !multiplier.loading
|
||||||
|
? `(${percentageFormatter(
|
||||||
|
baseCommission.value
|
||||||
|
)} × ${multiplier.value.toString()} = ${percentageFormatter(
|
||||||
|
finalCommission.value
|
||||||
|
)})`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatTile
|
||||||
|
title={t('Final commission rate')}
|
||||||
|
description={description}
|
||||||
|
testId="final-commission-rate"
|
||||||
|
>
|
||||||
|
<Value data={finalCommission} formatter={percentageFormatter} />
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VolumeTile = ({
|
||||||
|
volume,
|
||||||
|
aggregationEpochs,
|
||||||
|
}: {
|
||||||
|
volume: ReferrerStats['volume'];
|
||||||
|
aggregationEpochs: number;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatTile
|
||||||
|
title={t('myVolume', 'My volume (last {{count}} epochs)', {
|
||||||
|
count: aggregationEpochs,
|
||||||
|
})}
|
||||||
|
testId="my-volume"
|
||||||
|
>
|
||||||
|
<Value data={volume} formatter={compactFormatter(2)} />
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TotalCommissionTile = ({
|
||||||
|
totalCommission,
|
||||||
|
aggregationEpochs,
|
||||||
|
}: {
|
||||||
|
totalCommission: ReferrerStats['totalCommission'];
|
||||||
|
aggregationEpochs: number;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatTile
|
||||||
|
testId="total-commission"
|
||||||
|
title={
|
||||||
|
<Trans
|
||||||
|
i18nKey="totalCommission"
|
||||||
|
defaults="Total commission (<0>last {{count}} epochs</0>)"
|
||||||
|
values={{
|
||||||
|
count: aggregationEpochs,
|
||||||
|
}}
|
||||||
|
components={[
|
||||||
|
<Tooltip
|
||||||
|
key="0"
|
||||||
|
description={t(
|
||||||
|
'Depending on data node retention you may not be able see the full 30 days'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>last 30 epochs</span>
|
||||||
|
</Tooltip>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
description={<QUSDTooltip />}
|
||||||
|
>
|
||||||
|
<Value data={totalCommission} formatter={compactFormatter(0)} />
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RefereesTile = ({
|
||||||
|
referees,
|
||||||
|
}: {
|
||||||
|
referees: ReferrerStats['referees'];
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
return (
|
||||||
|
<StatTile title={t('Number of traders')} testId="number-of-traders">
|
||||||
|
<Value data={referees} formatter={valueFormatter(t('None'))} />
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Referee tiles */
|
||||||
|
|
||||||
|
export const BenefitTierTile = ({
|
||||||
|
benefitTier,
|
||||||
|
nextBenefitTier,
|
||||||
|
}: {
|
||||||
|
benefitTier: RefereeStats['benefitTier'];
|
||||||
|
nextBenefitTier: RefereeStats['nextBenefitTier'];
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
const formatter = (value: BenefitTier | undefined) =>
|
||||||
|
value?.tier || t('None');
|
||||||
|
|
||||||
|
const next = nextBenefitTier.value?.tier;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatTile
|
||||||
|
title={t('Current tier')}
|
||||||
|
testId="current-tier"
|
||||||
|
description={
|
||||||
|
next
|
||||||
|
? t('(Next tier: {{nextTier}})', {
|
||||||
|
nextTier: next,
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Value<BenefitTier | undefined>
|
||||||
|
data={benefitTier}
|
||||||
|
formatter={formatter}
|
||||||
|
/>
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RunningVolumeTile = ({
|
||||||
|
runningVolume,
|
||||||
|
aggregationEpochs,
|
||||||
|
}: {
|
||||||
|
runningVolume: RefereeStats['runningVolume'];
|
||||||
|
aggregationEpochs: number;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
return (
|
||||||
|
<StatTile
|
||||||
|
title={t(
|
||||||
|
'runningNotionalOverEpochs',
|
||||||
|
'Combined volume (last {{count}} epochs)',
|
||||||
|
{
|
||||||
|
count: aggregationEpochs,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
testId="combined-volume"
|
||||||
|
>
|
||||||
|
<Value data={runningVolume} formatter={compactFormatter(2)} />
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DiscountTile = ({
|
||||||
|
discountFactor,
|
||||||
|
}: {
|
||||||
|
discountFactor: RefereeStats['discountFactor'];
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
return (
|
||||||
|
<StatTile title={t('Discount')} testId="discount">
|
||||||
|
<Value data={discountFactor} formatter={percentageFormatter} />
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NextTierVolumeTile = ({
|
||||||
|
runningVolume,
|
||||||
|
nextBenefitTier,
|
||||||
|
}: {
|
||||||
|
runningVolume: RefereeStats['runningVolume'];
|
||||||
|
nextBenefitTier: RefereeStats['nextBenefitTier'];
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
loading: runningVolume.loading || nextBenefitTier.loading,
|
||||||
|
error: runningVolume.error || nextBenefitTier.error,
|
||||||
|
value: [runningVolume.value, nextBenefitTier.value] as [
|
||||||
|
BigNumber,
|
||||||
|
BenefitTier | undefined
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatter = ([runningVolume, nextBenefitTier]: [
|
||||||
|
BigNumber,
|
||||||
|
BenefitTier | undefined
|
||||||
|
]) => {
|
||||||
|
if (!nextBenefitTier) return '0';
|
||||||
|
const volume = BigNumber(nextBenefitTier.minimumVolume).minus(
|
||||||
|
runningVolume
|
||||||
|
);
|
||||||
|
if (volume.isNaN() || volume.isLessThan(0)) return '0';
|
||||||
|
return compactFormatter(0)(volume);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatTile title={t('Volume to next tier')} testId="vol-to-next-tier">
|
||||||
|
<Value<[BigNumber, BenefitTier | undefined]>
|
||||||
|
data={data}
|
||||||
|
formatter={formatter}
|
||||||
|
/>
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EpochsTile = ({ epochs }: { epochs: RefereeStats['epochs'] }) => {
|
||||||
|
const t = useT();
|
||||||
|
return (
|
||||||
|
<StatTile title={t('Epochs in set')} testId="epochs-in-set">
|
||||||
|
<Value data={epochs} formatter={valueFormatter(t('None'))} />
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NextTierEpochsTile = ({
|
||||||
|
epochs,
|
||||||
|
nextBenefitTier,
|
||||||
|
}: {
|
||||||
|
epochs: RefereeStats['epochs'];
|
||||||
|
nextBenefitTier: RefereeStats['nextBenefitTier'];
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
value: [epochs.value, nextBenefitTier.value] as [
|
||||||
|
BigNumber,
|
||||||
|
BenefitTier | undefined
|
||||||
|
],
|
||||||
|
loading: epochs.loading || nextBenefitTier.loading,
|
||||||
|
error: epochs.error || nextBenefitTier.error,
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatter = ([epochs, nextBenefitTier]: [
|
||||||
|
BigNumber,
|
||||||
|
BenefitTier | undefined
|
||||||
|
]) => {
|
||||||
|
if (!nextBenefitTier) return '-';
|
||||||
|
const value = BigNumber(nextBenefitTier.epochs).minus(epochs);
|
||||||
|
if (value.isLessThan(0)) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
return value.toString(10);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatTile title={t('Epochs to next tier')} testId="epochs-to-next-tier">
|
||||||
|
<Value data={data} formatter={formatter} />
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Additional settings */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list for tiles that should be replaced with `NoProgramTile`
|
||||||
|
* when the referral program is not set.
|
||||||
|
*/
|
||||||
|
const NO_PROGRAM_TILES = {
|
||||||
|
[BaseCommissionTile.name]: 'Base commission rate',
|
||||||
|
[StakingMultiplierTile.name]: 'Staking multiplier',
|
||||||
|
[FinalCommissionTile.name]: 'Final commission rate',
|
||||||
|
[VolumeTile.name]: 'My volume',
|
||||||
|
[BenefitTierTile.name]: 'Current tier',
|
||||||
|
[DiscountTile.name]: 'Discount',
|
||||||
|
[RunningVolumeTile.name]: 'Combined volume',
|
||||||
|
[NextTierEpochsTile.name]: 'Epochs to next tier',
|
||||||
|
[NextTierVolumeTile.name]: 'Volume to next tier',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NoProgramTileFor = ({ tile }: { tile: string }) => {
|
||||||
|
const t = useT();
|
||||||
|
if (Object.keys(NO_PROGRAM_TILES).includes(tile)) {
|
||||||
|
return <NoProgramTile title={t(NO_PROGRAM_TILES[tile])} />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Teams */
|
||||||
|
|
||||||
|
export const TeamTile = ({ teamId }: { teamId?: string }) => {
|
||||||
|
const { team, members } = useTeam(teamId);
|
||||||
|
const { data: games } = useGames(teamId);
|
||||||
|
|
||||||
|
if (!team) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tile className="flex gap-3 lg:gap-4">
|
||||||
|
<TeamAvatar teamId={team.teamId} imgUrl={team.avatarUrl} />
|
||||||
|
<div className="flex flex-col items-start gap-1 lg:gap-3">
|
||||||
|
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl">{team.name}</h1>
|
||||||
|
<TeamStats
|
||||||
|
members={members}
|
||||||
|
games={areTeamGames(games) ? games : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tile>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user