vega-frontend-monorepo/apps/trading/components/fees-container/fees-container.tsx

606 lines
17 KiB
TypeScript
Raw Normal View History

2023-10-25 21:59:30 +00:00
import maxBy from 'lodash/maxBy';
import minBy from 'lodash/minBy';
import { useVegaWallet } from '@vegaprotocol/wallet';
import {
useNetworkParams,
NetworkParams,
} from '@vegaprotocol/network-parameters';
import { useMarketList } from '@vegaprotocol/markets';
import { formatNumber, formatNumberRounded } from '@vegaprotocol/utils';
2023-10-25 21:59:30 +00:00
import { useDiscountProgramsQuery, useFeesQuery } from './__generated__/Fees';
2023-11-15 21:46:19 +00:00
import { Card, CardStat, CardTable, CardTableTD, CardTableTH } from '../card';
2023-10-25 21:59:30 +00:00
import { MarketFees } from './market-fees';
import { useVolumeStats } from './use-volume-stats';
import { useReferralStats } from './use-referral-stats';
import { formatPercentage, getAdjustedFee } from './utils';
import { Table, Td, Th, THead, Tr } from './table';
import BigNumber from 'bignumber.js';
2023-11-09 15:50:50 +00:00
import { Links } from '../../lib/links';
import { Link } from 'react-router-dom';
import {
Tooltip,
VegaIcon,
VegaIconNames,
truncateMiddle,
} from '@vegaprotocol/ui-toolkit';
import { useT } from '../../lib/use-t';
2023-10-25 21:59:30 +00:00
export const FeesContainer = () => {
const t = useT();
2023-10-25 21:59:30 +00:00
const { pubKey } = useVegaWallet();
const { params, loading: paramsLoading } = useNetworkParams([
NetworkParams.market_fee_factors_makerFee,
NetworkParams.market_fee_factors_infrastructureFee,
]);
const { data: markets, loading: marketsLoading } = useMarketList();
const { data: programData, loading: programLoading } =
useDiscountProgramsQuery({ errorPolicy: 'ignore' });
2023-10-25 21:59:30 +00:00
const volumeDiscountWindowLength =
2023-10-25 21:59:30 +00:00
programData?.currentVolumeDiscountProgram?.windowLength || 1;
const referralDiscountWindowLength =
2023-10-25 21:59:30 +00:00
programData?.currentReferralProgram?.windowLength || 1;
const { data: feesData, loading: feesLoading } = useFeesQuery({
variables: {
partyId: pubKey || '',
},
skip: !pubKey,
2023-10-25 21:59:30 +00:00
});
const previousEpoch = (Number(feesData?.epoch.id) || 0) - 1;
2023-10-25 21:59:30 +00:00
const { volumeDiscount, volumeTierIndex, volumeInWindow, volumeTiers } =
useVolumeStats(
previousEpoch,
feesData?.volumeDiscountStats.edges?.[0]?.node,
2023-10-25 21:59:30 +00:00
programData?.currentVolumeDiscountProgram
);
const {
referralDiscount,
referralVolumeInWindow,
referralTierIndex,
referralTiers,
epochsInSet,
2023-11-09 15:50:50 +00:00
code,
isReferrer,
2023-10-25 21:59:30 +00:00
} = useReferralStats(
previousEpoch,
feesData?.referralSetStats.edges?.[0]?.node,
feesData?.referralSetReferees.edges?.[0]?.node,
2023-10-25 21:59:30 +00:00
programData?.currentReferralProgram,
feesData?.referrer.edges?.[0]?.node,
feesData?.referee.edges?.[0]?.node
2023-10-25 21:59:30 +00:00
);
const loading = paramsLoading || feesLoading || programLoading;
const isConnected = Boolean(pubKey);
2023-11-09 15:50:50 +00:00
const isReferralProgramRunning = Boolean(programData?.currentReferralProgram);
const isVolumeDiscountProgramRunning = Boolean(
programData?.currentVolumeDiscountProgram
);
2023-10-25 21:59:30 +00:00
return (
<div className="grid auto-rows-min grid-cols-4 gap-3">
{isConnected && (
<>
2023-11-15 21:46:19 +00:00
<Card
2023-10-25 21:59:30 +00:00
title={t('My trading fees')}
className="sm:col-span-2"
loading={loading}
>
<TradingFees
params={params}
markets={markets}
referralDiscount={referralDiscount}
volumeDiscount={volumeDiscount}
/>
2023-11-15 21:46:19 +00:00
</Card>
<Card
2023-10-25 21:59:30 +00:00
title={t('Total discount')}
className="sm:col-span-2"
loading={loading}
>
<TotalDiscount
referralDiscount={referralDiscount}
volumeDiscount={volumeDiscount}
2023-11-09 15:50:50 +00:00
isReferralProgramRunning={isReferralProgramRunning}
isVolumeDiscountProgramRunning={isVolumeDiscountProgramRunning}
2023-10-25 21:59:30 +00:00
/>
2023-11-15 21:46:19 +00:00
</Card>
<Card
2023-10-25 21:59:30 +00:00
title={t('My current volume')}
className="sm:col-span-2"
loading={loading}
>
2023-11-09 15:50:50 +00:00
{isVolumeDiscountProgramRunning ? (
<CurrentVolume
tiers={volumeTiers}
tierIndex={volumeTierIndex}
windowLengthVolume={volumeInWindow}
windowLength={volumeDiscountWindowLength}
/>
) : (
2023-11-15 21:46:19 +00:00
<p className="text-muted pt-3 text-sm">
2023-11-09 15:50:50 +00:00
{t('No volume discount program active')}
</p>
)}
2023-11-15 21:46:19 +00:00
</Card>
<Card
2023-10-25 21:59:30 +00:00
title={t('Referral benefits')}
className="sm:col-span-2"
loading={loading}
>
2023-11-09 15:50:50 +00:00
{isReferrer ? (
<ReferrerInfo code={code} />
) : isReferralProgramRunning ? (
<ReferralBenefits
setRunningNotionalTakerVolume={referralVolumeInWindow}
epochsInSet={epochsInSet}
epochs={referralDiscountWindowLength}
/>
) : (
2023-11-15 21:46:19 +00:00
<p className="text-muted pt-3 text-sm">
2023-11-09 15:50:50 +00:00
{t('No referral program active')}
</p>
)}
2023-11-15 21:46:19 +00:00
</Card>
2023-10-25 21:59:30 +00:00
</>
)}
2023-11-15 21:46:19 +00:00
<Card
2023-10-25 21:59:30 +00:00
title={t('Volume discount')}
className="lg:col-span-full xl:col-span-2"
loading={loading}
>
<VolumeTiers
tiers={volumeTiers}
tierIndex={volumeTierIndex}
lastEpochVolume={volumeInWindow}
windowLength={volumeDiscountWindowLength}
2023-10-25 21:59:30 +00:00
/>
2023-11-15 21:46:19 +00:00
</Card>
<Card
2023-10-25 21:59:30 +00:00
title={t('Referral discount')}
className="lg:col-span-full xl:col-span-2"
loading={loading}
>
<ReferralTiers
tiers={referralTiers}
tierIndex={referralTierIndex}
epochsInSet={epochsInSet}
referralVolumeInWindow={referralVolumeInWindow}
/>
2023-11-15 21:46:19 +00:00
</Card>
<Card
2023-11-09 15:50:50 +00:00
title={t('Fees by market')}
2023-10-25 21:59:30 +00:00
className="lg:col-span-full"
loading={marketsLoading}
>
<MarketFees
markets={markets}
referralDiscount={referralDiscount}
volumeDiscount={volumeDiscount}
/>
2023-11-15 21:46:19 +00:00
</Card>
2023-10-25 21:59:30 +00:00
</div>
);
};
export const TradingFees = ({
params,
markets,
referralDiscount,
volumeDiscount,
}: {
params: {
market_fee_factors_infrastructureFee: string;
market_fee_factors_makerFee: string;
};
markets: Array<{ fees: { factors: { liquidityFee: string } } }> | null;
referralDiscount: number;
volumeDiscount: number;
}) => {
const t = useT();
2023-10-25 21:59:30 +00:00
const referralDiscountBigNum = new BigNumber(referralDiscount);
const volumeDiscountBigNum = new BigNumber(volumeDiscount);
// Show min and max liquidity fees from all markets
const minLiq = minBy(markets, (m) => Number(m.fees.factors.liquidityFee));
const maxLiq = maxBy(markets, (m) => Number(m.fees.factors.liquidityFee));
const total = new BigNumber(params.market_fee_factors_makerFee).plus(
new BigNumber(params.market_fee_factors_infrastructureFee)
);
const adjustedTotal = getAdjustedFee(
[total],
[referralDiscountBigNum, volumeDiscountBigNum]
);
let minTotal;
let maxTotal;
let minAdjustedTotal;
let maxAdjustedTotal;
if (minLiq && maxLiq) {
const minLiqFee = new BigNumber(minLiq.fees.factors.liquidityFee);
const maxLiqFee = new BigNumber(maxLiq.fees.factors.liquidityFee);
minTotal = total.plus(minLiqFee);
maxTotal = total.plus(maxLiqFee);
minAdjustedTotal = getAdjustedFee(
[total, minLiqFee],
[referralDiscountBigNum, volumeDiscountBigNum]
);
maxAdjustedTotal = getAdjustedFee(
[total, maxLiqFee],
[referralDiscountBigNum, volumeDiscountBigNum]
);
}
return (
2023-11-15 21:46:19 +00:00
<div className="pt-4">
<div className="leading-none">
2023-10-25 21:59:30 +00:00
<p className="block text-3xl leading-none" data-testid="adjusted-fees">
{minAdjustedTotal !== undefined && maxAdjustedTotal !== undefined
? `${formatPercentage(minAdjustedTotal)}%-${formatPercentage(
maxAdjustedTotal
)}%`
: `${formatPercentage(adjustedTotal)}%`}
</p>
2023-11-15 21:46:19 +00:00
<CardTable>
<tr className="text-default">
<CardTableTH>{t('Total fee before discount')}</CardTableTH>
<CardTableTD>
{minTotal !== undefined && maxTotal !== undefined
? `${formatPercentage(minTotal.toNumber())}%-${formatPercentage(
maxTotal.toNumber()
)}%`
: `${formatPercentage(total.toNumber())}%`}
</CardTableTD>
</tr>
<tr>
<CardTableTH>{t('Infrastructure')}</CardTableTH>
<CardTableTD>
{formatPercentage(
Number(params.market_fee_factors_infrastructureFee)
)}
%
</CardTableTD>
</tr>
<tr>
<CardTableTH>{t('Maker')}</CardTableTH>
<CardTableTD>
{formatPercentage(Number(params.market_fee_factors_makerFee))}%
</CardTableTD>
</tr>
{minLiq && maxLiq && (
2023-10-25 21:59:30 +00:00
<tr>
2023-11-15 21:46:19 +00:00
<CardTableTH>{t('Liquidity')}</CardTableTH>
<CardTableTD>
{formatPercentage(Number(minLiq.fees.factors.liquidityFee))}%
{'-'}
{formatPercentage(Number(maxLiq.fees.factors.liquidityFee))}%
</CardTableTD>
2023-10-25 21:59:30 +00:00
</tr>
2023-11-15 21:46:19 +00:00
)}
</CardTable>
2023-10-25 21:59:30 +00:00
</div>
</div>
);
};
export const CurrentVolume = ({
tiers,
tierIndex,
windowLengthVolume,
windowLength,
2023-10-25 21:59:30 +00:00
}: {
tiers: Array<{ minimumRunningNotionalTakerVolume: string }>;
tierIndex: number;
windowLengthVolume: number;
windowLength: number;
2023-10-25 21:59:30 +00:00
}) => {
const t = useT();
2023-10-25 21:59:30 +00:00
const nextTier = tiers[tierIndex + 1];
const requiredForNextTier = nextTier
? new BigNumber(nextTier.minimumRunningNotionalTakerVolume).minus(
windowLengthVolume
)
: new BigNumber(0);
const currentVolume = new BigNumber(windowLengthVolume);
2023-10-25 21:59:30 +00:00
return (
2023-11-15 21:46:19 +00:00
<div className="flex flex-col gap-3 pt-4">
<CardStat
value={
currentVolume.isZero()
? `<${formatNumberRounded(requiredForNextTier)}`
: formatNumberRounded(currentVolume)
}
text={t('pastEpochs', 'Past {{count}} epochs', {
count: windowLength,
})}
2023-10-25 21:59:30 +00:00
/>
{requiredForNextTier.isGreaterThan(0) && (
2023-11-15 21:46:19 +00:00
<CardStat
2023-10-25 21:59:30 +00:00
value={formatNumber(requiredForNextTier)}
text={t('Required for next tier')}
/>
)}
</div>
);
};
const ReferralBenefits = ({
epochsInSet,
setRunningNotionalTakerVolume,
epochs,
}: {
epochsInSet: number;
setRunningNotionalTakerVolume: number;
epochs: number;
}) => {
const t = useT();
2023-10-25 21:59:30 +00:00
return (
2023-11-15 21:46:19 +00:00
<div className="flex flex-col gap-3 pt-4">
<CardStat
2023-10-25 21:59:30 +00:00
// all sets volume (not just current party)
value={formatNumber(setRunningNotionalTakerVolume)}
text={t(
'runningNotionalOverEpochs',
'Combined running notional over the {{count}} epochs',
{
count: epochs,
}
)}
2023-10-25 21:59:30 +00:00
/>
2023-11-15 21:46:19 +00:00
<CardStat value={epochsInSet} text={t('epochs in referral set')} />
2023-10-25 21:59:30 +00:00
</div>
);
};
const TotalDiscount = ({
referralDiscount,
volumeDiscount,
2023-11-09 15:50:50 +00:00
isReferralProgramRunning,
isVolumeDiscountProgramRunning,
2023-10-25 21:59:30 +00:00
}: {
referralDiscount: number;
volumeDiscount: number;
2023-11-09 15:50:50 +00:00
isReferralProgramRunning: boolean;
isVolumeDiscountProgramRunning: boolean;
2023-10-25 21:59:30 +00:00
}) => {
const t = useT();
2023-11-09 15:50:50 +00:00
const totalDiscount = 1 - (1 - volumeDiscount) * (1 - referralDiscount);
const totalDiscountDescription = t(
'The total discount is calculated according to the following formula: '
);
const formula = (
<span className="italic">
1 - (1 - d<sub>volume</sub>) (1 - d<sub>referral</sub>)
</span>
);
2023-10-25 21:59:30 +00:00
return (
2023-11-15 21:46:19 +00:00
<div className="pt-4">
<CardStat
2023-11-09 15:50:50 +00:00
description={
<>
{totalDiscountDescription}
{formula}
</>
}
value={formatPercentage(totalDiscount) + '%'}
2023-10-25 21:59:30 +00:00
highlight={true}
/>
2023-11-15 21:46:19 +00:00
<CardTable>
<tr>
<CardTableTH>{t('Volume discount')}</CardTableTH>
<CardTableTD>
{formatPercentage(volumeDiscount)}%
{!isVolumeDiscountProgramRunning && (
<Tooltip description={t('No active volume discount programme')}>
<span className="cursor-help">
{' '}
<VegaIcon name={VegaIconNames.INFO} size={12} />
</span>
</Tooltip>
)}
</CardTableTD>
</tr>
<tr>
<CardTableTH>{t('Referral discount')}</CardTableTH>
<CardTableTD>
{formatPercentage(referralDiscount)}%
{!isReferralProgramRunning && (
<Tooltip description={t('No active referral programme')}>
<span className="cursor-help">
{' '}
<VegaIcon name={VegaIconNames.INFO} size={12} />
</span>
</Tooltip>
)}
</CardTableTD>
</tr>
</CardTable>
2023-10-25 21:59:30 +00:00
</div>
);
};
const VolumeTiers = ({
tiers,
tierIndex,
lastEpochVolume,
windowLength,
2023-10-25 21:59:30 +00:00
}: {
tiers: Array<{
volumeDiscountFactor: string;
minimumRunningNotionalTakerVolume: string;
}>;
tierIndex: number;
lastEpochVolume: number;
windowLength: number;
2023-10-25 21:59:30 +00:00
}) => {
const t = useT();
2023-10-25 21:59:30 +00:00
if (!tiers.length) {
return (
2023-11-15 21:46:19 +00:00
<p className="text-muted text-sm">
2023-10-25 21:59:30 +00:00
{t('No volume discount program active')}
</p>
);
}
return (
<div>
<Table>
<THead>
<tr>
<Th>{t('Tier')}</Th>
<Th>{t('Discount')}</Th>
<Th>{t('Min. trading volume')}</Th>
<Th>
{t('myVolume', 'My volume (last {{count}} epochs)', {
count: windowLength,
})}
</Th>
2023-10-25 21:59:30 +00:00
<Th />
</tr>
</THead>
<tbody>
{Array.from(tiers).map((tier, i) => {
const isUserTier = tierIndex === i;
return (
<Tr key={i}>
<Td>{i + 1}</Td>
<Td>{formatPercentage(Number(tier.volumeDiscountFactor))}%</Td>
<Td>{formatNumber(tier.minimumRunningNotionalTakerVolume)}</Td>
<Td>{isUserTier ? formatNumber(lastEpochVolume) : ''}</Td>
<Td>{isUserTier ? <YourTier /> : null}</Td>
</Tr>
);
})}
2023-10-25 21:59:30 +00:00
</tbody>
</Table>
</div>
);
};
const ReferralTiers = ({
tiers,
tierIndex,
epochsInSet,
referralVolumeInWindow,
}: {
tiers: Array<{
referralDiscountFactor: string;
minimumRunningNotionalTakerVolume: string;
minimumEpochs: number;
}>;
tierIndex: number;
epochsInSet: number;
referralVolumeInWindow: number;
}) => {
const t = useT();
2023-10-25 21:59:30 +00:00
if (!tiers.length) {
return (
2023-11-15 21:46:19 +00:00
<p className="text-muted text-sm">{t('No referral program active')}</p>
2023-10-25 21:59:30 +00:00
);
}
return (
<div>
<Table>
<THead>
<tr>
<Th>{t('Tier')}</Th>
<Th>{t('Discount')}</Th>
<Th>{t('Min. trading volume')}</Th>
<Th>{t('Required epochs')}</Th>
<Th />
</tr>
</THead>
<tbody>
{Array.from(tiers).map((t, i) => {
const isUserTier = tierIndex === i;
const requiredVolume = Number(t.minimumRunningNotionalTakerVolume);
let unlocksIn = null;
if (
referralVolumeInWindow >= requiredVolume &&
epochsInSet < t.minimumEpochs
) {
unlocksIn = (
<span className="text-muted">
Unlocks in {t.minimumEpochs - epochsInSet} epochs
</span>
2023-10-25 21:59:30 +00:00
);
}
return (
<Tr key={i}>
<Td>{i + 1}</Td>
<Td>{formatPercentage(Number(t.referralDiscountFactor))}%</Td>
<Td>{formatNumber(t.minimumRunningNotionalTakerVolume)}</Td>
<Td>{t.minimumEpochs}</Td>
<Td>{isUserTier ? <YourTier /> : unlocksIn}</Td>
</Tr>
);
})}
2023-10-25 21:59:30 +00:00
</tbody>
</Table>
</div>
);
};
const YourTier = () => {
const t = useT();
2023-10-25 21:59:30 +00:00
return (
2023-11-15 21:46:19 +00:00
<span className="bg-rainbow whitespace-nowrap rounded-xl px-4 py-1.5 text-white">
2023-10-25 21:59:30 +00:00
{t('Your tier')}
</span>
);
};
2023-11-09 15:50:50 +00:00
const ReferrerInfo = ({ code }: { code?: string }) => {
const t = useT();
return (
<div className="text-vega-clight-200 dark:vega-cdark-200 pt-3 text-sm">
<p className="mb-1">
{t('Connected key is owner of the referral set')}
{code && (
<>
{' '}
<span className="bg-rainbow bg-clip-text text-transparent">
{truncateMiddle(code)}
</span>
</>
)}
{'. '}
{t('As owner, it is eligible for commission not fee discounts.')}
</p>
<p>
{t('See')}{' '}
<Link
className="text-black underline dark:text-white"
to={Links.REFERRALS()}
>
{t('Referrals')}
</Link>{' '}
{t('for more information.')}
</p>
</div>
);
};