feat(trading): rewards page (#5222)
This commit is contained in:
parent
6d32fa7362
commit
090d340364
1
apps/trading/client-pages/rewards/index.ts
Normal file
1
apps/trading/client-pages/rewards/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { Rewards } from './rewards';
|
11
apps/trading/client-pages/rewards/rewards.tsx
Normal file
11
apps/trading/client-pages/rewards/rewards.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { RewardsContainer } from '../../components/rewards-container';
|
||||
|
||||
export const Rewards = () => {
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<h1 className="px-4 pb-4 text-2xl">{t('Rewards')}</h1>
|
||||
<RewardsContainer />
|
||||
</div>
|
||||
);
|
||||
};
|
102
apps/trading/components/card/card.tsx
Normal file
102
apps/trading/components/card/card.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLProps, ReactNode } from 'react';
|
||||
|
||||
export const Card = ({
|
||||
children,
|
||||
title,
|
||||
className,
|
||||
loading = false,
|
||||
highlight = false,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
title: string;
|
||||
className?: string;
|
||||
loading?: boolean;
|
||||
highlight?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'bg-vega-clight-800 dark:bg-vega-cdark-800 col-span-full p-0.5 lg:col-auto',
|
||||
'rounded-lg',
|
||||
{
|
||||
'bg-rainbow': highlight,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="bg-vega-clight-800 dark:bg-vega-cdark-800 h-full w-full rounded p-4">
|
||||
<h2 className="mb-3">{title}</h2>
|
||||
{loading ? <CardLoader /> : children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CardLoader = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="bg-vega-clight-600 dark:bg-vega-cdark-600 h-5 w-full" />
|
||||
<div className="bg-vega-clight-600 dark:bg-vega-cdark-600 h-6 w-3/4" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CardStat = ({
|
||||
value,
|
||||
text,
|
||||
highlight,
|
||||
description,
|
||||
testId,
|
||||
}: {
|
||||
value: ReactNode;
|
||||
text?: string;
|
||||
highlight?: boolean;
|
||||
description?: ReactNode;
|
||||
testId?: string;
|
||||
}) => {
|
||||
const val = (
|
||||
<span
|
||||
className={classNames('inline-block text-3xl leading-none', {
|
||||
'bg-rainbow bg-clip-text text-transparent': highlight,
|
||||
'cursor-help': description,
|
||||
})}
|
||||
data-testid={testId}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<p className="leading-none">
|
||||
{description ? <Tooltip description={description}>{val}</Tooltip> : val}
|
||||
{text && (
|
||||
<small className="text-muted mt-0.5 block text-xs">{text}</small>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export const CardTable = (props: HTMLProps<HTMLTableElement>) => {
|
||||
return (
|
||||
<table {...props} className="text-muted mt-0.5 w-full text-xs">
|
||||
<tbody>{props.children}</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export const CardTableTH = (props: HTMLProps<HTMLTableHeaderCellElement>) => {
|
||||
return (
|
||||
<th
|
||||
{...props}
|
||||
className={classNames('text-left font-normal', props.className)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CardTableTD = (props: HTMLProps<HTMLTableCellElement>) => {
|
||||
return (
|
||||
<td {...props} className={classNames('text-right', props.className)} />
|
||||
);
|
||||
};
|
1
apps/trading/components/card/index.ts
Normal file
1
apps/trading/components/card/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { Card, CardStat, CardTable, CardTableTH, CardTableTD } from './card';
|
@ -1,36 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export const FeeCard = ({
|
||||
children,
|
||||
title,
|
||||
className,
|
||||
loading = false,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
title: string;
|
||||
className?: string;
|
||||
loading?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'p-4 bg-vega-clight-800 dark:bg-vega-cdark-800 col-span-full lg:col-auto',
|
||||
'rounded-lg',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<h2 className="mb-3">{title}</h2>
|
||||
{loading ? <FeeCardLoader /> : children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const FeeCardLoader = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="w-full h-5 bg-vega-clight-600 dark:bg-vega-cdark-600" />
|
||||
<div className="w-3/4 h-6 bg-vega-clight-600 dark:bg-vega-cdark-600" />
|
||||
</div>
|
||||
);
|
||||
};
|
@ -9,9 +9,8 @@ import {
|
||||
import { useMarketList } from '@vegaprotocol/markets';
|
||||
import { formatNumber, formatNumberRounded } from '@vegaprotocol/utils';
|
||||
import { useDiscountProgramsQuery, useFeesQuery } from './__generated__/Fees';
|
||||
import { FeeCard } from './fees-card';
|
||||
import { Card, CardStat, CardTable, CardTableTD, CardTableTH } from '../card';
|
||||
import { MarketFees } from './market-fees';
|
||||
import { Stat } from './stat';
|
||||
import { useVolumeStats } from './use-volume-stats';
|
||||
import { useReferralStats } from './use-referral-stats';
|
||||
import { formatPercentage, getAdjustedFee } from './utils';
|
||||
@ -87,7 +86,7 @@ export const FeesContainer = () => {
|
||||
<div className="grid auto-rows-min grid-cols-4 gap-3">
|
||||
{isConnected && (
|
||||
<>
|
||||
<FeeCard
|
||||
<Card
|
||||
title={t('My trading fees')}
|
||||
className="sm:col-span-2"
|
||||
loading={loading}
|
||||
@ -98,8 +97,8 @@ export const FeesContainer = () => {
|
||||
referralDiscount={referralDiscount}
|
||||
volumeDiscount={volumeDiscount}
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
</Card>
|
||||
<Card
|
||||
title={t('Total discount')}
|
||||
className="sm:col-span-2"
|
||||
loading={loading}
|
||||
@ -110,8 +109,8 @@ export const FeesContainer = () => {
|
||||
isReferralProgramRunning={isReferralProgramRunning}
|
||||
isVolumeDiscountProgramRunning={isVolumeDiscountProgramRunning}
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
</Card>
|
||||
<Card
|
||||
title={t('My current volume')}
|
||||
className="sm:col-span-2"
|
||||
loading={loading}
|
||||
@ -124,12 +123,12 @@ export const FeesContainer = () => {
|
||||
windowLength={volumeDiscountWindowLength}
|
||||
/>
|
||||
) : (
|
||||
<p className="pt-3 text-sm text-muted">
|
||||
<p className="text-muted pt-3 text-sm">
|
||||
{t('No volume discount program active')}
|
||||
</p>
|
||||
)}
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
</Card>
|
||||
<Card
|
||||
title={t('Referral benefits')}
|
||||
className="sm:col-span-2"
|
||||
loading={loading}
|
||||
@ -143,14 +142,14 @@ export const FeesContainer = () => {
|
||||
epochs={referralDiscountWindowLength}
|
||||
/>
|
||||
) : (
|
||||
<p className="pt-3 text-sm text-muted">
|
||||
<p className="text-muted pt-3 text-sm">
|
||||
{t('No referral program active')}
|
||||
</p>
|
||||
)}
|
||||
</FeeCard>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
<FeeCard
|
||||
<Card
|
||||
title={t('Volume discount')}
|
||||
className="lg:col-span-full xl:col-span-2"
|
||||
loading={loading}
|
||||
@ -161,8 +160,8 @@ export const FeesContainer = () => {
|
||||
lastEpochVolume={volumeInWindow}
|
||||
windowLength={volumeDiscountWindowLength}
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
</Card>
|
||||
<Card
|
||||
title={t('Referral discount')}
|
||||
className="lg:col-span-full xl:col-span-2"
|
||||
loading={loading}
|
||||
@ -173,8 +172,8 @@ export const FeesContainer = () => {
|
||||
epochsInSet={epochsInSet}
|
||||
referralVolumeInWindow={referralVolumeInWindow}
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
</Card>
|
||||
<Card
|
||||
title={t('Fees by market')}
|
||||
className="lg:col-span-full"
|
||||
loading={marketsLoading}
|
||||
@ -184,7 +183,7 @@ export const FeesContainer = () => {
|
||||
referralDiscount={referralDiscount}
|
||||
volumeDiscount={volumeDiscount}
|
||||
/>
|
||||
</FeeCard>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -244,8 +243,8 @@ export const TradingFees = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="pt-6 leading-none">
|
||||
<div className="pt-4">
|
||||
<div className="leading-none">
|
||||
<p className="block text-3xl leading-none" data-testid="adjusted-fees">
|
||||
{minAdjustedTotal !== undefined && maxAdjustedTotal !== undefined
|
||||
? `${formatPercentage(minAdjustedTotal)}%-${formatPercentage(
|
||||
@ -253,47 +252,43 @@ export const TradingFees = ({
|
||||
)}%`
|
||||
: `${formatPercentage(adjustedTotal)}%`}
|
||||
</p>
|
||||
<table className="w-full mt-0.5 text-xs text-muted">
|
||||
<tbody>
|
||||
<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 && (
|
||||
<tr>
|
||||
<th className="font-normal text-left text-default">
|
||||
{t('Total fee before discount')}
|
||||
</th>
|
||||
<td className="text-right text-default">
|
||||
{minTotal !== undefined && maxTotal !== undefined
|
||||
? `${formatPercentage(
|
||||
minTotal.toNumber()
|
||||
)}%-${formatPercentage(maxTotal.toNumber())}%`
|
||||
: `${formatPercentage(total.toNumber())}%`}
|
||||
</td>
|
||||
<CardTableTH>{t('Liquidity')}</CardTableTH>
|
||||
<CardTableTD>
|
||||
{formatPercentage(Number(minLiq.fees.factors.liquidityFee))}%
|
||||
{'-'}
|
||||
{formatPercentage(Number(maxLiq.fees.factors.liquidityFee))}%
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="font-normal text-left">{t('Infrastructure')}</th>
|
||||
<td className="text-right">
|
||||
{formatPercentage(
|
||||
Number(params.market_fee_factors_infrastructureFee)
|
||||
)}
|
||||
%
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="font-normal text-left ">{t('Maker')}</th>
|
||||
<td className="text-right">
|
||||
{formatPercentage(Number(params.market_fee_factors_makerFee))}%
|
||||
</td>
|
||||
</tr>
|
||||
{minLiq && maxLiq && (
|
||||
<tr>
|
||||
<th className="font-normal text-left ">{t('Liquidity')}</th>
|
||||
<td className="text-right">
|
||||
{formatPercentage(Number(minLiq.fees.factors.liquidityFee))}%
|
||||
{'-'}
|
||||
{formatPercentage(Number(maxLiq.fees.factors.liquidityFee))}%
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</CardTable>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -316,13 +311,13 @@ export const CurrentVolume = ({
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Stat
|
||||
<div className="flex flex-col gap-3 pt-4">
|
||||
<CardStat
|
||||
value={formatNumberRounded(new BigNumber(windowLengthVolume))}
|
||||
text={t('Past %s epochs', windowLength.toString())}
|
||||
/>
|
||||
{requiredForNextTier > 0 && (
|
||||
<Stat
|
||||
<CardStat
|
||||
value={formatNumber(requiredForNextTier)}
|
||||
text={t('Required for next tier')}
|
||||
/>
|
||||
@ -341,8 +336,8 @@ const ReferralBenefits = ({
|
||||
epochs: number;
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<Stat
|
||||
<div className="flex flex-col gap-3 pt-4">
|
||||
<CardStat
|
||||
// all sets volume (not just current party)
|
||||
value={formatNumber(setRunningNotionalTakerVolume)}
|
||||
text={t(
|
||||
@ -350,7 +345,7 @@ const ReferralBenefits = ({
|
||||
epochs.toString()
|
||||
)}
|
||||
/>
|
||||
<Stat value={epochsInSet} text={t('epochs in referral set')} />
|
||||
<CardStat value={epochsInSet} text={t('epochs in referral set')} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -377,8 +372,8 @@ const TotalDiscount = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Stat
|
||||
<div className="pt-4">
|
||||
<CardStat
|
||||
description={
|
||||
<>
|
||||
{totalDiscountDescription}
|
||||
@ -388,38 +383,36 @@ const TotalDiscount = ({
|
||||
value={formatPercentage(totalDiscount) + '%'}
|
||||
highlight={true}
|
||||
/>
|
||||
<table className="w-full mt-0.5 text-xs text-muted">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th className="font-normal text-left">{t('Volume discount')}</th>
|
||||
<td className="text-right">
|
||||
{formatPercentage(volumeDiscount)}%
|
||||
{!isVolumeDiscountProgramRunning && (
|
||||
<Tooltip description={t('No active volume discount programme')}>
|
||||
<span className="cursor-help">
|
||||
{' '}
|
||||
<VegaIcon name={VegaIconNames.INFO} size={12} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="font-normal text-left ">{t('Referral discount')}</th>
|
||||
<td className="text-right">
|
||||
{formatPercentage(referralDiscount)}%
|
||||
{!isReferralProgramRunning && (
|
||||
<Tooltip description={t('No active referral programme')}>
|
||||
<span className="cursor-help">
|
||||
{' '}
|
||||
<VegaIcon name={VegaIconNames.INFO} size={12} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -440,7 +433,7 @@ const VolumeTiers = ({
|
||||
}) => {
|
||||
if (!tiers.length) {
|
||||
return (
|
||||
<p className="text-sm text-muted">
|
||||
<p className="text-muted text-sm">
|
||||
{t('No volume discount program active')}
|
||||
</p>
|
||||
);
|
||||
@ -501,7 +494,7 @@ const ReferralTiers = ({
|
||||
}) => {
|
||||
if (!tiers.length) {
|
||||
return (
|
||||
<p className="text-sm text-muted">{t('No referral program active')}</p>
|
||||
<p className="text-muted text-sm">{t('No referral program active')}</p>
|
||||
);
|
||||
}
|
||||
|
||||
@ -557,20 +550,20 @@ const ReferralTiers = ({
|
||||
|
||||
const YourTier = () => {
|
||||
return (
|
||||
<span className="px-4 py-1.5 rounded-xl bg-rainbow whitespace-nowrap text-white">
|
||||
<span className="bg-rainbow whitespace-nowrap rounded-xl px-4 py-1.5 text-white">
|
||||
{t('Your tier')}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const ReferrerInfo = ({ code }: { code?: string }) => (
|
||||
<div className="pt-3 text-sm text-vega-clight-200 dark:vega-cdark-200">
|
||||
<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="text-transparent bg-rainbow bg-clip-text">
|
||||
<span className="bg-rainbow bg-clip-text text-transparent">
|
||||
{truncateMiddle(code)}
|
||||
</span>
|
||||
</>
|
||||
@ -581,7 +574,7 @@ const ReferrerInfo = ({ code }: { code?: string }) => (
|
||||
<p>
|
||||
{t('See')}{' '}
|
||||
<Link
|
||||
className="underline text-black dark:text-white"
|
||||
className="text-black underline dark:text-white"
|
||||
to={Links.REFERRALS()}
|
||||
>
|
||||
{t('Referrals')}
|
||||
|
@ -1,34 +0,0 @@
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export const Stat = ({
|
||||
value,
|
||||
text,
|
||||
highlight,
|
||||
description,
|
||||
}: {
|
||||
value: string | number;
|
||||
text?: string;
|
||||
highlight?: boolean;
|
||||
description?: ReactNode;
|
||||
}) => {
|
||||
const val = (
|
||||
<span
|
||||
className={classNames('inline-block text-3xl leading-none', {
|
||||
'text-transparent bg-rainbow bg-clip-text': highlight,
|
||||
'cursor-help': description,
|
||||
})}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
<p className="pt-3 leading-none first:pt-6">
|
||||
{description ? <Tooltip description={description}>{val}</Tooltip> : val}
|
||||
{text && (
|
||||
<small className="block mt-0.5 text-xs text-muted">{text}</small>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
};
|
@ -68,6 +68,7 @@ describe('Navbar', () => {
|
||||
['/portfolio', 'Portfolio'],
|
||||
['/referrals', 'Referrals'],
|
||||
['/fees', 'Fees'],
|
||||
['/rewards', 'Rewards'],
|
||||
[expect.stringContaining('governance'), 'Governance'],
|
||||
];
|
||||
|
||||
@ -102,6 +103,7 @@ describe('Navbar', () => {
|
||||
['/portfolio', 'Portfolio'],
|
||||
['/referrals', 'Referrals'],
|
||||
['/fees', 'Fees'],
|
||||
['/rewards', 'Rewards'],
|
||||
[expect.stringContaining('governance'), 'Governance'],
|
||||
];
|
||||
const links = menu.getAllByRole('link');
|
||||
|
@ -73,7 +73,7 @@ export const Navbar = ({
|
||||
</div>
|
||||
|
||||
{/* Right section */}
|
||||
<div className="flex items-center justify-end ml-auto gap-2">
|
||||
<div className="ml-auto flex items-center justify-end gap-2">
|
||||
<ProtocolUpgradeCountdown />
|
||||
<NavbarMobileButton
|
||||
onClick={() => {
|
||||
@ -107,18 +107,18 @@ export const Navbar = ({
|
||||
onOpenChange={(open) => setMenu((x) => (open ? x : null))}
|
||||
>
|
||||
<D.Overlay
|
||||
className="fixed inset-0 z-20 lg:hidden dark:bg-black/80 bg-black/50"
|
||||
className="fixed inset-0 z-20 bg-black/50 dark:bg-black/80 lg:hidden"
|
||||
data-testid="navbar-menu-overlay"
|
||||
/>
|
||||
<D.Content
|
||||
className={classNames(
|
||||
'lg:hidden',
|
||||
'fixed top-0 right-0 z-20 w-3/4 h-screen border-l border-default bg-vega-clight-700 dark:bg-vega-cdark-700',
|
||||
'border-default bg-vega-clight-700 dark:bg-vega-cdark-700 fixed right-0 top-0 z-20 h-screen w-3/4 border-l',
|
||||
navTextClasses
|
||||
)}
|
||||
data-testid="navbar-menu-content"
|
||||
>
|
||||
<div className="flex items-center justify-end h-10 p-1">
|
||||
<div className="flex h-10 items-center justify-end p-1">
|
||||
<NavbarMobileButton onClick={() => setMenu(null)}>
|
||||
<span className="sr-only">{t('Close menu')}</span>
|
||||
<VegaIcon name={VegaIconNames.CROSS} size={24} />
|
||||
@ -142,7 +142,7 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
|
||||
const marketId = useGlobalStore((store) => store.marketId);
|
||||
|
||||
return (
|
||||
<div className="lg:flex lg:h-full gap-3">
|
||||
<div className="gap-3 lg:flex lg:h-full">
|
||||
<NavbarList>
|
||||
<NavbarItem>
|
||||
<NavbarTrigger data-testid="navbar-network-switcher-trigger">
|
||||
@ -192,6 +192,11 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
|
||||
{t('Fees')}
|
||||
</NavbarLink>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<NavbarLink to={Links.REWARDS()} onClick={onClick}>
|
||||
{t('Rewards')}
|
||||
</NavbarLink>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<NavbarLinkExternal to={useLinks(DApp.Governance)()}>
|
||||
{t('Governance')}
|
||||
@ -241,8 +246,8 @@ const NavbarTrigger = ({
|
||||
onPointerMove={preventHover}
|
||||
onPointerLeave={preventHover}
|
||||
className={classNames(
|
||||
'w-full lg:w-auto lg:h-full',
|
||||
'flex items-center justify-between lg:justify-center gap-2 px-6 py-2 lg:p-0',
|
||||
'w-full lg:h-full lg:w-auto',
|
||||
'flex items-center justify-between gap-2 px-6 py-2 lg:justify-center lg:p-0',
|
||||
'text-lg lg:text-sm',
|
||||
'hover:text-vega-clight-100 dark:hover:text-vega-cdark-100'
|
||||
)}
|
||||
@ -273,8 +278,8 @@ const NavbarLink = ({
|
||||
to={to}
|
||||
end={end}
|
||||
className={classNames(
|
||||
'block lg:flex lg:h-full flex-col justify-center',
|
||||
'px-6 py-2 lg:p-0 text-lg lg:text-sm',
|
||||
'block flex-col justify-center lg:flex lg:h-full',
|
||||
'px-6 py-2 text-lg lg:p-0 lg:text-sm',
|
||||
'hover:text-vega-clight-100 dark:hover:text-vega-cdark-100'
|
||||
)}
|
||||
onClick={onClick}
|
||||
@ -297,7 +302,7 @@ const NavbarLink = ({
|
||||
</span>
|
||||
<span
|
||||
className={classNames(
|
||||
'hidden lg:block absolute left-0 bottom-0 w-full h-0',
|
||||
'absolute bottom-0 left-0 hidden h-0 w-full lg:block',
|
||||
borderClasses
|
||||
)}
|
||||
/>
|
||||
@ -318,7 +323,7 @@ const NavbarSubItem = (props: LiHTMLAttributes<HTMLElement>) => {
|
||||
};
|
||||
|
||||
const NavbarList = (props: N.NavigationMenuListProps) => {
|
||||
return <N.List {...props} className="lg:flex lg:h-full gap-6" />;
|
||||
return <N.List {...props} className="gap-6 lg:flex lg:h-full" />;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -329,10 +334,10 @@ const NavbarContent = (props: N.NavigationMenuContentProps) => {
|
||||
<N.Content
|
||||
{...props}
|
||||
className={classNames(
|
||||
'group navbar-content',
|
||||
'lg:absolute lg:mt-2 pl-2 lg:pl-0 z-20 lg:min-w-[290px]',
|
||||
'navbar-content group',
|
||||
'z-20 pl-2 lg:absolute lg:mt-2 lg:min-w-[290px] lg:pl-0',
|
||||
'lg:bg-vega-clight-700 lg:dark:bg-vega-cdark-700',
|
||||
'lg:border border-vega-clight-500 dark:border-vega-cdark-500 lg:rounded'
|
||||
'border-vega-clight-500 dark:border-vega-cdark-500 lg:rounded lg:border'
|
||||
)}
|
||||
onPointerEnter={preventHover}
|
||||
onPointerLeave={preventHover}
|
||||
@ -357,8 +362,8 @@ const NavbarLinkExternal = ({
|
||||
<NavLink
|
||||
to={to}
|
||||
className={classNames(
|
||||
'flex gap-2 lg:h-full items-center',
|
||||
'px-6 py-2 lg:p-0 text-lg lg:text-sm',
|
||||
'flex items-center gap-2 lg:h-full',
|
||||
'px-6 py-2 text-lg lg:p-0 lg:text-sm',
|
||||
'hover:text-vega-clight-100 dark:hover:text-vega-cdark-100'
|
||||
)}
|
||||
onClick={onClick}
|
||||
@ -386,7 +391,7 @@ const BurgerIcon = () => (
|
||||
const NavbarListDivider = () => {
|
||||
return (
|
||||
<div className="px-6 py-2 lg:px-0" role="separator">
|
||||
<div className="w-full h-px lg:h-full lg:w-px bg-vega-clight-500 dark:bg-vega-cdark-500" />
|
||||
<div className="bg-vega-clight-500 dark:bg-vega-cdark-500 h-px w-full lg:h-full lg:w-px" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -399,7 +404,7 @@ const NavbarMobileButton = (props: ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
<button
|
||||
{...props}
|
||||
className={classNames(
|
||||
'w-8 h-8 lg:hidden flex items-center p-1 rounded ',
|
||||
'flex h-8 w-8 items-center rounded p-1 lg:hidden ',
|
||||
'hover:bg-vega-clight-500 dark:hover:bg-vega-cdark-500',
|
||||
'hover:text-vega-clight-50 dark:hover:text-vega-cdark-50'
|
||||
)}
|
||||
|
94
apps/trading/components/rewards-container/Rewards.graphql
Normal file
94
apps/trading/components/rewards-container/Rewards.graphql
Normal file
@ -0,0 +1,94 @@
|
||||
query RewardsPage($partyId: ID!) {
|
||||
party(id: $partyId) {
|
||||
id
|
||||
vestingStats {
|
||||
# AKA hoarder reward multiplier
|
||||
rewardBonusMultiplier
|
||||
}
|
||||
activityStreak {
|
||||
# vesting multiplier
|
||||
rewardVestingMultiplier
|
||||
# AKA streak multiplier
|
||||
rewardDistributionMultiplier
|
||||
}
|
||||
vestingBalancesSummary {
|
||||
epoch
|
||||
vestingBalances {
|
||||
asset {
|
||||
id
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
}
|
||||
balance
|
||||
}
|
||||
lockedBalances {
|
||||
asset {
|
||||
id
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
}
|
||||
balance
|
||||
untilEpoch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query RewardsHistory(
|
||||
$partyId: ID!
|
||||
$epochRewardSummariesPagination: Pagination
|
||||
$partyRewardsPagination: Pagination
|
||||
$fromEpoch: Int
|
||||
$toEpoch: Int
|
||||
) {
|
||||
epochRewardSummaries(
|
||||
filter: { fromEpoch: $fromEpoch, toEpoch: $toEpoch }
|
||||
pagination: $epochRewardSummariesPagination
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
epoch
|
||||
assetId
|
||||
amount
|
||||
rewardType
|
||||
}
|
||||
}
|
||||
}
|
||||
party(id: $partyId) {
|
||||
id
|
||||
rewardsConnection(
|
||||
fromEpoch: $fromEpoch
|
||||
toEpoch: $toEpoch
|
||||
pagination: $partyRewardsPagination
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
amount
|
||||
percentageOfTotal
|
||||
receivedAt
|
||||
rewardType
|
||||
asset {
|
||||
id
|
||||
symbol
|
||||
name
|
||||
decimals
|
||||
}
|
||||
party {
|
||||
id
|
||||
}
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query RewardsEpoch {
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
}
|
205
apps/trading/components/rewards-container/__generated__/Rewards.ts
generated
Normal file
205
apps/trading/components/rewards-container/__generated__/Rewards.ts
generated
Normal file
@ -0,0 +1,205 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type RewardsPageQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type RewardsPageQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, vestingStats?: { __typename?: 'PartyVestingStats', rewardBonusMultiplier: string } | null, activityStreak?: { __typename?: 'PartyActivityStreak', rewardVestingMultiplier: string, rewardDistributionMultiplier: string } | null, vestingBalancesSummary: { __typename?: 'PartyVestingBalancesSummary', epoch?: number | null, vestingBalances?: Array<{ __typename?: 'PartyVestingBalance', balance: string, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, quantum: string } }> | null, lockedBalances?: Array<{ __typename?: 'PartyLockedBalance', balance: string, untilEpoch: number, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, quantum: string } }> | null } } | null };
|
||||
|
||||
export type RewardsHistoryQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
epochRewardSummariesPagination?: Types.InputMaybe<Types.Pagination>;
|
||||
partyRewardsPagination?: Types.InputMaybe<Types.Pagination>;
|
||||
fromEpoch?: Types.InputMaybe<Types.Scalars['Int']>;
|
||||
toEpoch?: Types.InputMaybe<Types.Scalars['Int']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type RewardsHistoryQuery = { __typename?: 'Query', epochRewardSummaries?: { __typename?: 'EpochRewardSummaryConnection', edges?: Array<{ __typename?: 'EpochRewardSummaryEdge', node: { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType } } | null> | null } | null, party?: { __typename?: 'Party', id: string, rewardsConnection?: { __typename?: 'RewardsConnection', edges?: Array<{ __typename?: 'RewardEdge', node: { __typename?: 'Reward', amount: string, percentageOfTotal: string, receivedAt: any, rewardType: Types.AccountType, asset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number }, party: { __typename?: 'Party', id: string }, epoch: { __typename?: 'Epoch', id: string } } } | null> | null } | null } | null };
|
||||
|
||||
export type RewardsEpochQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type RewardsEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string } };
|
||||
|
||||
|
||||
export const RewardsPageDocument = gql`
|
||||
query RewardsPage($partyId: ID!) {
|
||||
party(id: $partyId) {
|
||||
id
|
||||
vestingStats {
|
||||
rewardBonusMultiplier
|
||||
}
|
||||
activityStreak {
|
||||
rewardVestingMultiplier
|
||||
rewardDistributionMultiplier
|
||||
}
|
||||
vestingBalancesSummary {
|
||||
epoch
|
||||
vestingBalances {
|
||||
asset {
|
||||
id
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
}
|
||||
balance
|
||||
}
|
||||
lockedBalances {
|
||||
asset {
|
||||
id
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
}
|
||||
balance
|
||||
untilEpoch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useRewardsPageQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useRewardsPageQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useRewardsPageQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useRewardsPageQuery({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useRewardsPageQuery(baseOptions: Apollo.QueryHookOptions<RewardsPageQuery, RewardsPageQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<RewardsPageQuery, RewardsPageQueryVariables>(RewardsPageDocument, options);
|
||||
}
|
||||
export function useRewardsPageLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<RewardsPageQuery, RewardsPageQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<RewardsPageQuery, RewardsPageQueryVariables>(RewardsPageDocument, options);
|
||||
}
|
||||
export type RewardsPageQueryHookResult = ReturnType<typeof useRewardsPageQuery>;
|
||||
export type RewardsPageLazyQueryHookResult = ReturnType<typeof useRewardsPageLazyQuery>;
|
||||
export type RewardsPageQueryResult = Apollo.QueryResult<RewardsPageQuery, RewardsPageQueryVariables>;
|
||||
export const RewardsHistoryDocument = gql`
|
||||
query RewardsHistory($partyId: ID!, $epochRewardSummariesPagination: Pagination, $partyRewardsPagination: Pagination, $fromEpoch: Int, $toEpoch: Int) {
|
||||
epochRewardSummaries(
|
||||
filter: {fromEpoch: $fromEpoch, toEpoch: $toEpoch}
|
||||
pagination: $epochRewardSummariesPagination
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
epoch
|
||||
assetId
|
||||
amount
|
||||
rewardType
|
||||
}
|
||||
}
|
||||
}
|
||||
party(id: $partyId) {
|
||||
id
|
||||
rewardsConnection(
|
||||
fromEpoch: $fromEpoch
|
||||
toEpoch: $toEpoch
|
||||
pagination: $partyRewardsPagination
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
amount
|
||||
percentageOfTotal
|
||||
receivedAt
|
||||
rewardType
|
||||
asset {
|
||||
id
|
||||
symbol
|
||||
name
|
||||
decimals
|
||||
}
|
||||
party {
|
||||
id
|
||||
}
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useRewardsHistoryQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useRewardsHistoryQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useRewardsHistoryQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useRewardsHistoryQuery({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* epochRewardSummariesPagination: // value for 'epochRewardSummariesPagination'
|
||||
* partyRewardsPagination: // value for 'partyRewardsPagination'
|
||||
* fromEpoch: // value for 'fromEpoch'
|
||||
* toEpoch: // value for 'toEpoch'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useRewardsHistoryQuery(baseOptions: Apollo.QueryHookOptions<RewardsHistoryQuery, RewardsHistoryQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<RewardsHistoryQuery, RewardsHistoryQueryVariables>(RewardsHistoryDocument, options);
|
||||
}
|
||||
export function useRewardsHistoryLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<RewardsHistoryQuery, RewardsHistoryQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<RewardsHistoryQuery, RewardsHistoryQueryVariables>(RewardsHistoryDocument, options);
|
||||
}
|
||||
export type RewardsHistoryQueryHookResult = ReturnType<typeof useRewardsHistoryQuery>;
|
||||
export type RewardsHistoryLazyQueryHookResult = ReturnType<typeof useRewardsHistoryLazyQuery>;
|
||||
export type RewardsHistoryQueryResult = Apollo.QueryResult<RewardsHistoryQuery, RewardsHistoryQueryVariables>;
|
||||
export const RewardsEpochDocument = gql`
|
||||
query RewardsEpoch {
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useRewardsEpochQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useRewardsEpochQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useRewardsEpochQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useRewardsEpochQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useRewardsEpochQuery(baseOptions?: Apollo.QueryHookOptions<RewardsEpochQuery, RewardsEpochQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<RewardsEpochQuery, RewardsEpochQueryVariables>(RewardsEpochDocument, options);
|
||||
}
|
||||
export function useRewardsEpochLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<RewardsEpochQuery, RewardsEpochQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<RewardsEpochQuery, RewardsEpochQueryVariables>(RewardsEpochDocument, options);
|
||||
}
|
||||
export type RewardsEpochQueryHookResult = ReturnType<typeof useRewardsEpochQuery>;
|
||||
export type RewardsEpochLazyQueryHookResult = ReturnType<typeof useRewardsEpochLazyQuery>;
|
||||
export type RewardsEpochQueryResult = Apollo.QueryResult<RewardsEpochQuery, RewardsEpochQueryVariables>;
|
1
apps/trading/components/rewards-container/index.ts
Normal file
1
apps/trading/components/rewards-container/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { RewardsContainer } from './rewards-container';
|
@ -0,0 +1,215 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import type { Account } from '@vegaprotocol/accounts';
|
||||
import { AccountType, AssetStatus } from '@vegaprotocol/types';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import {
|
||||
RewardPot,
|
||||
Vesting,
|
||||
type RewardPotProps,
|
||||
Multipliers,
|
||||
} from './rewards-container';
|
||||
|
||||
const rewardAsset = {
|
||||
id: 'asset-1',
|
||||
symbol: 'ASSET 1',
|
||||
name: 'Asset 1',
|
||||
decimals: 2,
|
||||
quantum: '1',
|
||||
status: AssetStatus.STATUS_ENABLED,
|
||||
source: {
|
||||
__typename: 'ERC20' as const,
|
||||
contractAddress: '0x123',
|
||||
lifetimeLimit: '100',
|
||||
withdrawThreshold: '100',
|
||||
},
|
||||
};
|
||||
|
||||
describe('RewardPot', () => {
|
||||
const renderComponent = (props: RewardPotProps) => {
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<RewardPot {...props} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
it('Shows no rewards message if no accounts or vesting balances provided', () => {
|
||||
renderComponent({
|
||||
pubKey: 'pubkey',
|
||||
assetId: rewardAsset.id,
|
||||
accounts: [],
|
||||
vestingBalancesSummary: {
|
||||
lockedBalances: [],
|
||||
vestingBalances: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByText(/No rewards/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Calculates all the rewards', () => {
|
||||
const asset2 = {
|
||||
id: 'asset-2',
|
||||
symbol: 'ASSET 2',
|
||||
name: 'Asset 2',
|
||||
decimals: 0,
|
||||
quantum: '1000000',
|
||||
status: AssetStatus.STATUS_ENABLED,
|
||||
source: {
|
||||
__typename: 'ERC20' as const,
|
||||
contractAddress: '0x123',
|
||||
lifetimeLimit: '100',
|
||||
withdrawThreshold: '100',
|
||||
},
|
||||
};
|
||||
|
||||
const accounts: Account[] = [
|
||||
{
|
||||
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
balance: '100',
|
||||
asset: rewardAsset,
|
||||
},
|
||||
{
|
||||
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
balance: '100',
|
||||
asset: rewardAsset,
|
||||
},
|
||||
{
|
||||
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
balance: '50',
|
||||
asset: rewardAsset,
|
||||
},
|
||||
{
|
||||
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
balance: '500000',
|
||||
asset: asset2,
|
||||
},
|
||||
{
|
||||
type: AccountType.ACCOUNT_TYPE_VESTING_REWARDS, // should be ignored as its vesting
|
||||
balance: '100',
|
||||
asset: rewardAsset,
|
||||
},
|
||||
{
|
||||
type: AccountType.ACCOUNT_TYPE_VESTING_REWARDS, // should be ignored
|
||||
balance: '2000000',
|
||||
asset: asset2,
|
||||
},
|
||||
];
|
||||
|
||||
const props = {
|
||||
pubKey: 'pubkey',
|
||||
assetId: rewardAsset.id,
|
||||
accounts: accounts,
|
||||
vestingBalancesSummary: {
|
||||
epoch: 1,
|
||||
lockedBalances: [
|
||||
{
|
||||
balance: '150',
|
||||
asset: rewardAsset,
|
||||
untilEpoch: 1,
|
||||
},
|
||||
{
|
||||
balance: '100',
|
||||
asset: rewardAsset,
|
||||
untilEpoch: 1,
|
||||
},
|
||||
{
|
||||
balance: '100',
|
||||
asset: asset2, // should be ignored
|
||||
untilEpoch: 1,
|
||||
},
|
||||
],
|
||||
vestingBalances: [
|
||||
{
|
||||
balance: '250',
|
||||
asset: rewardAsset,
|
||||
},
|
||||
{
|
||||
balance: '200',
|
||||
asset: rewardAsset,
|
||||
},
|
||||
{
|
||||
balance: '100',
|
||||
asset: asset2, // should be ignored
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
renderComponent(props);
|
||||
|
||||
expect(screen.getByTestId('total-rewards')).toHaveTextContent(
|
||||
`7.00 ${rewardAsset.symbol}`
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Locked/).nextElementSibling).toHaveTextContent(
|
||||
'2.50'
|
||||
);
|
||||
expect(screen.getByText(/Vesting/).nextElementSibling).toHaveTextContent(
|
||||
'4.50'
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText(/Available to withdraw/).nextElementSibling
|
||||
).toHaveTextContent('1.50');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Vesting', () => {
|
||||
it('renders vesting rates', () => {
|
||||
render(<Vesting baseRate={'0.25'} pubKey="pubKey" multiplier="2" />);
|
||||
|
||||
expect(screen.getByTestId('vesting-rate')).toHaveTextContent('50%');
|
||||
|
||||
expect(screen.getByText('Base rate').nextElementSibling).toHaveTextContent(
|
||||
'25%'
|
||||
);
|
||||
expect(
|
||||
screen.getByText('Vesting multiplier').nextSibling
|
||||
).toHaveTextContent('2x');
|
||||
});
|
||||
|
||||
it('doesnt use multiplier if not connected', () => {
|
||||
render(<Vesting baseRate={'0.25'} pubKey={null} multiplier={undefined} />);
|
||||
|
||||
expect(screen.getByTestId('vesting-rate')).toHaveTextContent('25%');
|
||||
|
||||
expect(screen.getByText('Base rate').nextElementSibling).toHaveTextContent(
|
||||
'25%'
|
||||
);
|
||||
expect(screen.queryByText('Vesting multiplier')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multipliers', () => {
|
||||
it('shows combined multipliers', () => {
|
||||
render(
|
||||
<Multipliers pubKey="pubkey" streakMultiplier="3" hoarderMultiplier="2" />
|
||||
);
|
||||
expect(screen.getByTestId('combined-multipliers')).toHaveTextContent('6x');
|
||||
expect(
|
||||
screen.getByText('Streak reward multiplier').nextElementSibling
|
||||
).toHaveTextContent('3x');
|
||||
expect(
|
||||
screen.getByText('Hoarder reward multiplier').nextElementSibling
|
||||
).toHaveTextContent('2x');
|
||||
});
|
||||
|
||||
it('shows not connected state', () => {
|
||||
render(
|
||||
<Multipliers pubKey={null} streakMultiplier="3" hoarderMultiplier="2" />
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('combined-multipliers')
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText('Streak reward multiplier')
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText('Hoarder reward multiplier')
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Not connected')).toBeInTheDocument();
|
||||
});
|
||||
});
|
375
apps/trading/components/rewards-container/rewards-container.tsx
Normal file
375
apps/trading/components/rewards-container/rewards-container.tsx
Normal file
@ -0,0 +1,375 @@
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import type { Account } from '@vegaprotocol/accounts';
|
||||
import { useAccounts } from '@vegaprotocol/accounts';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {
|
||||
Card,
|
||||
CardStat,
|
||||
CardTable,
|
||||
CardTableTD,
|
||||
CardTableTH,
|
||||
} from '../card/card';
|
||||
import {
|
||||
type RewardsPageQuery,
|
||||
useRewardsPageQuery,
|
||||
useRewardsEpochQuery,
|
||||
} from './__generated__/Rewards';
|
||||
import {
|
||||
TradingButton,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { formatPercentage } from '../fees-container/utils';
|
||||
import { addDecimalsFormatNumberQuantum } from '@vegaprotocol/utils';
|
||||
import { ViewType, useSidebar } from '../sidebar';
|
||||
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||
import { RewardsHistoryContainer } from './rewards-history';
|
||||
|
||||
export const RewardsContainer = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { params, loading: paramsLoading } = useNetworkParams([
|
||||
NetworkParams.reward_asset,
|
||||
NetworkParams.rewards_activityStreak_benefitTiers,
|
||||
NetworkParams.rewards_vesting_baseRate,
|
||||
]);
|
||||
const { data: accounts, loading: accountsLoading } = useAccounts(pubKey);
|
||||
|
||||
const { data: epochData } = useRewardsEpochQuery();
|
||||
|
||||
// No need to specify the fromEpoch as it will by default give you the last
|
||||
const { data: rewardsData, loading: rewardsLoading } = useRewardsPageQuery({
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!epochData?.epoch) return null;
|
||||
|
||||
const loading = paramsLoading || accountsLoading || rewardsLoading;
|
||||
|
||||
const rewardAccounts = accounts
|
||||
? accounts.filter((a) =>
|
||||
[
|
||||
AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
AccountType.ACCOUNT_TYPE_VESTING_REWARDS,
|
||||
].includes(a.type)
|
||||
)
|
||||
: [];
|
||||
|
||||
const rewardAssetsMap = groupBy(
|
||||
rewardAccounts.filter((a) => a.asset.id !== params.reward_asset),
|
||||
'asset.id'
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid auto-rows-min grid-cols-6 gap-3">
|
||||
{/* Always show reward information for vega */}
|
||||
<Card
|
||||
key={params.reward_asset}
|
||||
title={t('Vega Reward pot')}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
highlight={true}
|
||||
>
|
||||
<RewardPot
|
||||
pubKey={pubKey}
|
||||
accounts={accounts}
|
||||
assetId={params.reward_asset}
|
||||
vestingBalancesSummary={rewardsData?.party?.vestingBalancesSummary}
|
||||
/>
|
||||
</Card>
|
||||
<Card
|
||||
title={t('Vesting')}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<Vesting
|
||||
pubKey={pubKey}
|
||||
baseRate={params.rewards_vesting_baseRate}
|
||||
multiplier={
|
||||
rewardsData?.party?.activityStreak?.rewardVestingMultiplier
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
<Card
|
||||
title={t('Rewards multipliers')}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
highlight={true}
|
||||
>
|
||||
<Multipliers
|
||||
pubKey={pubKey}
|
||||
hoarderMultiplier={
|
||||
rewardsData?.party?.vestingStats?.rewardBonusMultiplier
|
||||
}
|
||||
streakMultiplier={
|
||||
rewardsData?.party?.activityStreak?.rewardDistributionMultiplier
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Show all other reward pots, most of the time users will not have other rewards */}
|
||||
{Object.keys(rewardAssetsMap).map((assetId) => {
|
||||
const asset = rewardAssetsMap[assetId][0].asset;
|
||||
return (
|
||||
<Card
|
||||
key={assetId}
|
||||
title={t('%s Reward pot', asset.symbol)}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<RewardPot
|
||||
pubKey={pubKey}
|
||||
accounts={accounts}
|
||||
assetId={assetId}
|
||||
vestingBalancesSummary={
|
||||
rewardsData?.party?.vestingBalancesSummary
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
<Card
|
||||
title={t('Rewards history')}
|
||||
className="lg:col-span-full"
|
||||
loading={rewardsLoading}
|
||||
>
|
||||
<RewardsHistoryContainer
|
||||
epoch={Number(epochData?.epoch.id)}
|
||||
pubKey={pubKey}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type VestingBalances = NonNullable<
|
||||
RewardsPageQuery['party']
|
||||
>['vestingBalancesSummary'];
|
||||
|
||||
export type RewardPotProps = {
|
||||
pubKey: string | null;
|
||||
accounts: Account[] | null;
|
||||
assetId: string; // VEGA
|
||||
vestingBalancesSummary: VestingBalances | undefined;
|
||||
};
|
||||
|
||||
export const RewardPot = ({
|
||||
pubKey,
|
||||
accounts,
|
||||
assetId,
|
||||
vestingBalancesSummary,
|
||||
}: RewardPotProps) => {
|
||||
// TODO: Opening the sidebar for the first time works, but then clicking on redeem
|
||||
// for a different asset does not update the form
|
||||
const currentRouteId = useGetCurrentRouteId();
|
||||
const setViews = useSidebar((store) => store.setViews);
|
||||
|
||||
// All vested rewards accounts
|
||||
const availableRewardAssetAccounts = accounts
|
||||
? accounts.filter((a) => {
|
||||
return (
|
||||
a.asset.id === assetId &&
|
||||
a.type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS
|
||||
);
|
||||
})
|
||||
: [];
|
||||
|
||||
// Sum of all vested reward account balances
|
||||
const totalVestedRewardsByRewardAsset = BigNumber.sum.apply(
|
||||
null,
|
||||
availableRewardAssetAccounts.length
|
||||
? availableRewardAssetAccounts.map((a) => a.balance)
|
||||
: [0]
|
||||
);
|
||||
|
||||
const lockedEntries = vestingBalancesSummary?.lockedBalances?.filter(
|
||||
(b) => b.asset.id === assetId
|
||||
);
|
||||
const lockedBalances = lockedEntries?.length
|
||||
? lockedEntries.map((e) => e.balance)
|
||||
: [0];
|
||||
const totalLocked = BigNumber.sum.apply(null, lockedBalances);
|
||||
|
||||
const vestingEntries = vestingBalancesSummary?.vestingBalances?.filter(
|
||||
(b) => b.asset.id === assetId
|
||||
);
|
||||
const vestingBalances = vestingEntries?.length
|
||||
? vestingEntries.map((e) => e.balance)
|
||||
: [0];
|
||||
const totalVesting = BigNumber.sum.apply(null, vestingBalances);
|
||||
|
||||
const totalRewards = totalLocked.plus(totalVesting);
|
||||
|
||||
let rewardAsset = undefined;
|
||||
|
||||
if (availableRewardAssetAccounts.length) {
|
||||
rewardAsset = availableRewardAssetAccounts[0].asset;
|
||||
} else if (lockedEntries?.length) {
|
||||
rewardAsset = lockedEntries[0].asset;
|
||||
} else if (vestingEntries?.length) {
|
||||
rewardAsset = vestingEntries[0].asset;
|
||||
}
|
||||
|
||||
if (!pubKey) {
|
||||
return (
|
||||
<div className="pt-4">
|
||||
<p className="text-muted text-sm">{t('Not connected')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pt-4">
|
||||
{rewardAsset ? (
|
||||
<>
|
||||
<CardStat
|
||||
value={`${addDecimalsFormatNumberQuantum(
|
||||
totalRewards.toString(),
|
||||
rewardAsset.decimals,
|
||||
rewardAsset.quantum
|
||||
)} ${rewardAsset.symbol}`}
|
||||
testId="total-rewards"
|
||||
/>
|
||||
<div className="flex flex-col gap-4">
|
||||
<CardTable>
|
||||
<tr>
|
||||
<CardTableTH className="flex items-center gap-1">
|
||||
{t(`Locked ${rewardAsset.symbol}`)}
|
||||
<VegaIcon name={VegaIconNames.LOCK} size={12} />
|
||||
</CardTableTH>
|
||||
<CardTableTD>
|
||||
{addDecimalsFormatNumberQuantum(
|
||||
totalLocked.toString(),
|
||||
rewardAsset.decimals,
|
||||
rewardAsset.quantum
|
||||
)}
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
<tr>
|
||||
<CardTableTH>{t(`Vesting ${rewardAsset.symbol}`)}</CardTableTH>
|
||||
<CardTableTD>
|
||||
{addDecimalsFormatNumberQuantum(
|
||||
totalVesting.toString(),
|
||||
rewardAsset.decimals,
|
||||
rewardAsset.quantum
|
||||
)}
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
<tr>
|
||||
<CardTableTH>
|
||||
{t('Available to withdraw this epoch')}
|
||||
</CardTableTH>
|
||||
<CardTableTD>
|
||||
{addDecimalsFormatNumberQuantum(
|
||||
totalVestedRewardsByRewardAsset.toString(),
|
||||
rewardAsset.decimals,
|
||||
rewardAsset.quantum
|
||||
)}
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
</CardTable>
|
||||
{totalVestedRewardsByRewardAsset.isGreaterThan(0) && (
|
||||
<div>
|
||||
<TradingButton
|
||||
onClick={() =>
|
||||
setViews(
|
||||
{ type: ViewType.Transfer, assetId },
|
||||
currentRouteId
|
||||
)
|
||||
}
|
||||
size="small"
|
||||
>
|
||||
{t('Redeem rewards')}
|
||||
</TradingButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-muted text-sm">{t('No rewards')}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Vesting = ({
|
||||
pubKey,
|
||||
baseRate,
|
||||
multiplier = '1',
|
||||
}: {
|
||||
pubKey: string | null;
|
||||
baseRate: string;
|
||||
multiplier?: string;
|
||||
}) => {
|
||||
const rate = new BigNumber(baseRate).times(multiplier);
|
||||
const rateFormatted = formatPercentage(Number(rate));
|
||||
const baseRateFormatted = formatPercentage(Number(baseRate));
|
||||
|
||||
return (
|
||||
<div className="pt-4">
|
||||
<CardStat value={rateFormatted + '%'} testId="vesting-rate" />
|
||||
<CardTable>
|
||||
<tr>
|
||||
<CardTableTH>{t('Base rate')}</CardTableTH>
|
||||
<CardTableTD>{baseRateFormatted}%</CardTableTD>
|
||||
</tr>
|
||||
{pubKey && (
|
||||
<tr>
|
||||
<CardTableTH>{t('Vesting multiplier')}</CardTableTH>
|
||||
<CardTableTD>{multiplier}x</CardTableTD>
|
||||
</tr>
|
||||
)}
|
||||
</CardTable>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Multipliers = ({
|
||||
pubKey,
|
||||
streakMultiplier = '1',
|
||||
hoarderMultiplier = '1',
|
||||
}: {
|
||||
pubKey: string | null;
|
||||
streakMultiplier?: string;
|
||||
hoarderMultiplier?: string;
|
||||
}) => {
|
||||
const combinedMultiplier = new BigNumber(streakMultiplier).times(
|
||||
hoarderMultiplier
|
||||
);
|
||||
|
||||
if (!pubKey) {
|
||||
return (
|
||||
<div className="pt-4">
|
||||
<p className="text-muted text-sm">{t('Not connected')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pt-4">
|
||||
<CardStat
|
||||
value={combinedMultiplier.toString() + 'x'}
|
||||
testId="combined-multipliers"
|
||||
highlight={true}
|
||||
/>
|
||||
<CardTable>
|
||||
<tr>
|
||||
<CardTableTH>{t('Streak reward multiplier')}</CardTableTH>
|
||||
<CardTableTD>{streakMultiplier}x</CardTableTD>
|
||||
</tr>
|
||||
<tr>
|
||||
<CardTableTH>{t('Hoarder reward multiplier')}</CardTableTH>
|
||||
<CardTableTD>{hoarderMultiplier}x</CardTableTD>
|
||||
</tr>
|
||||
</CardTable>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,194 @@
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { RewardHistoryTable } from './rewards-history';
|
||||
import { AccountType, AssetStatus } from '@vegaprotocol/types';
|
||||
import { type AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
|
||||
const assets: Record<string, AssetFieldsFragment> = {
|
||||
asset1: {
|
||||
id: 'asset1',
|
||||
name: 'Asset 1',
|
||||
status: AssetStatus.STATUS_ENABLED,
|
||||
symbol: 'A ASSET',
|
||||
decimals: 0,
|
||||
quantum: '1',
|
||||
// @ts-ignore not needed
|
||||
source: {},
|
||||
},
|
||||
asset2: {
|
||||
id: 'asset2',
|
||||
name: 'Asset 2',
|
||||
status: AssetStatus.STATUS_ENABLED,
|
||||
symbol: 'B ASSET',
|
||||
decimals: 0,
|
||||
quantum: '1',
|
||||
// @ts-ignore not needed
|
||||
source: {},
|
||||
},
|
||||
};
|
||||
|
||||
const rewardSummaries = [
|
||||
{
|
||||
node: {
|
||||
epoch: 9,
|
||||
assetId: assets.asset1.id,
|
||||
amount: '60',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
epoch: 8,
|
||||
assetId: assets.asset1.id,
|
||||
amount: '20',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
epoch: 8,
|
||||
assetId: assets.asset1.id,
|
||||
amount: '20',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
epoch: 7,
|
||||
assetId: assets.asset2.id,
|
||||
amount: '300',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const getCell = (cells: HTMLElement[], colId: string) => {
|
||||
return within(
|
||||
cells.find((c) => c.getAttribute('col-id') === colId) as HTMLElement
|
||||
);
|
||||
};
|
||||
|
||||
describe('RewarsHistoryTable', () => {
|
||||
const props = {
|
||||
epochRewardSummaries: {
|
||||
edges: rewardSummaries,
|
||||
},
|
||||
partyRewards: {
|
||||
edges: [],
|
||||
},
|
||||
assets,
|
||||
pubKey: 'pubkey',
|
||||
epoch: 10,
|
||||
epochVariables: {
|
||||
from: 1,
|
||||
to: 10,
|
||||
},
|
||||
onEpochChange: jest.fn(),
|
||||
loading: false,
|
||||
};
|
||||
|
||||
it('Renders table with accounts summed up by asset', () => {
|
||||
render(<RewardHistoryTable {...props} />);
|
||||
|
||||
const container = within(
|
||||
document.querySelector('.ag-center-cols-container') as HTMLElement
|
||||
);
|
||||
const rows = container.getAllByRole('row');
|
||||
expect(rows).toHaveLength(
|
||||
Object.keys(groupBy(rewardSummaries, 'node.assetId')).length
|
||||
);
|
||||
|
||||
let row = within(rows[0]);
|
||||
let cells = row.getAllByRole('gridcell');
|
||||
|
||||
let assetCell = getCell(cells, 'asset.symbol');
|
||||
expect(assetCell.getByTestId('stack-cell-primary')).toHaveTextContent(
|
||||
assets.asset2.symbol
|
||||
);
|
||||
expect(assetCell.getByTestId('stack-cell-secondary')).toHaveTextContent(
|
||||
assets.asset2.name
|
||||
);
|
||||
|
||||
const marketCreationCell = getCell(cells, 'marketCreation');
|
||||
expect(
|
||||
marketCreationCell.getByTestId('stack-cell-primary')
|
||||
).toHaveTextContent('300');
|
||||
expect(
|
||||
marketCreationCell.getByTestId('stack-cell-secondary')
|
||||
).toHaveTextContent('100.00%');
|
||||
|
||||
let totalCell = getCell(cells, 'total');
|
||||
expect(totalCell.getByText('300.00')).toBeInTheDocument();
|
||||
|
||||
row = within(rows[1]);
|
||||
cells = row.getAllByRole('gridcell');
|
||||
|
||||
assetCell = getCell(cells, 'asset.symbol');
|
||||
expect(assetCell.getByTestId('stack-cell-primary')).toHaveTextContent(
|
||||
assets.asset1.symbol
|
||||
);
|
||||
expect(assetCell.getByTestId('stack-cell-secondary')).toHaveTextContent(
|
||||
assets.asset1.name
|
||||
);
|
||||
|
||||
// check cells are summed and percentage of totals are shown
|
||||
const priceTakingCell = getCell(cells, 'priceTaking');
|
||||
expect(priceTakingCell.getByTestId('stack-cell-primary')).toHaveTextContent(
|
||||
'80'
|
||||
);
|
||||
expect(
|
||||
priceTakingCell.getByTestId('stack-cell-secondary')
|
||||
).toHaveTextContent('80.00%');
|
||||
|
||||
const avgPositionCell = getCell(cells, 'averagePosition');
|
||||
expect(avgPositionCell.getByTestId('stack-cell-primary')).toHaveTextContent(
|
||||
'20'
|
||||
);
|
||||
expect(
|
||||
avgPositionCell.getByTestId('stack-cell-secondary')
|
||||
).toHaveTextContent('20.00%');
|
||||
|
||||
totalCell = getCell(cells, 'total');
|
||||
expect(totalCell.getByText('100.00')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('changes epochs using pagination', async () => {
|
||||
const epochVariables = {
|
||||
from: 3,
|
||||
to: 4,
|
||||
};
|
||||
const onEpochChange = jest.fn();
|
||||
|
||||
render(
|
||||
<RewardHistoryTable
|
||||
{...props}
|
||||
epoch={5}
|
||||
epochVariables={epochVariables}
|
||||
onEpochChange={onEpochChange}
|
||||
/>
|
||||
);
|
||||
const fromInput = screen.getByLabelText('From epoch');
|
||||
const toInput = screen.getByLabelText('to');
|
||||
expect(fromInput).toHaveValue(epochVariables.from);
|
||||
expect(toInput).toHaveValue(epochVariables.to);
|
||||
|
||||
const buttons = within(screen.getByTestId('fromEpoch')).getAllByRole(
|
||||
'button'
|
||||
);
|
||||
const fromInc = buttons[0];
|
||||
const decInc = buttons[1];
|
||||
|
||||
await userEvent.click(fromInc);
|
||||
expect(onEpochChange).toHaveBeenCalledWith({ from: 4, to: 4 });
|
||||
|
||||
await userEvent.click(decInc);
|
||||
expect(onEpochChange).toHaveBeenCalledWith({ from: 2, to: 4 });
|
||||
|
||||
onEpochChange.mockClear();
|
||||
|
||||
await userEvent.type(fromInput, '1');
|
||||
// no state control so typing will just append to whats there
|
||||
expect(onEpochChange).toHaveBeenCalledWith({ from: 31, to: 4 });
|
||||
});
|
||||
});
|
391
apps/trading/components/rewards-container/rewards-history.tsx
Normal file
391
apps/trading/components/rewards-container/rewards-history.tsx
Normal file
@ -0,0 +1,391 @@
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useMemo, useState } from 'react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { ColDef, ValueFormatterFunc } from 'ag-grid-community';
|
||||
import {
|
||||
useAssetsMapProvider,
|
||||
type AssetFieldsFragment,
|
||||
} from '@vegaprotocol/assets';
|
||||
import {
|
||||
addDecimalsFormatNumberQuantum,
|
||||
formatNumberPercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { AgGrid, StackedCell } from '@vegaprotocol/datagrid';
|
||||
import {
|
||||
TradingButton,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
useRewardsHistoryQuery,
|
||||
type RewardsHistoryQuery,
|
||||
} from './__generated__/Rewards';
|
||||
import { useRewardsRowData } from './use-reward-row-data';
|
||||
|
||||
export const RewardsHistoryContainer = ({
|
||||
epoch,
|
||||
pubKey,
|
||||
}: {
|
||||
pubKey: string | null;
|
||||
epoch: number;
|
||||
}) => {
|
||||
const [epochVariables, setEpochVariables] = useState(() => ({
|
||||
from: epoch - 1,
|
||||
to: epoch,
|
||||
}));
|
||||
|
||||
const { data: assets } = useAssetsMapProvider();
|
||||
|
||||
// No need to specify the fromEpoch as it will by default give you the last
|
||||
const { refetch, data, loading } = useRewardsHistoryQuery({
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
fromEpoch: epochVariables.from,
|
||||
toEpoch: epochVariables.to,
|
||||
},
|
||||
});
|
||||
|
||||
const debouncedRefetch = useMemo(
|
||||
() => debounce((variables) => refetch(variables), 800),
|
||||
[refetch]
|
||||
);
|
||||
|
||||
const handleEpochChange = (incoming: { from: number; to: number }) => {
|
||||
if (!Number.isInteger(incoming.from) || !Number.isInteger(incoming.to)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (incoming.from > incoming.to) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Must be at least the first epoch
|
||||
if (incoming.from < 0 || incoming.to < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (incoming.from > epoch || incoming.to > epoch) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEpochVariables({
|
||||
from: incoming.from,
|
||||
to: Math.min(incoming.to, epoch),
|
||||
});
|
||||
debouncedRefetch({
|
||||
partyId: pubKey || '',
|
||||
fromEpoch: incoming.from,
|
||||
toEpoch: incoming.to,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<RewardHistoryTable
|
||||
pubKey={pubKey}
|
||||
epochRewardSummaries={data?.epochRewardSummaries}
|
||||
partyRewards={data?.party?.rewardsConnection}
|
||||
onEpochChange={handleEpochChange}
|
||||
epoch={epoch}
|
||||
epochVariables={epochVariables}
|
||||
assets={assets}
|
||||
loading={loading}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const defaultColDef = {
|
||||
flex: 1,
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
};
|
||||
|
||||
interface RewardRow {
|
||||
asset: AssetFieldsFragment;
|
||||
staking: number;
|
||||
priceTaking: number;
|
||||
priceMaking: number;
|
||||
liquidityProvision: number;
|
||||
marketCreation: number;
|
||||
averagePosition: number;
|
||||
relativeReturns: number;
|
||||
returnsVolatility: number;
|
||||
validatorRanking: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export type PartyRewardsConnection = NonNullable<
|
||||
RewardsHistoryQuery['party']
|
||||
>['rewardsConnection'];
|
||||
|
||||
export const RewardHistoryTable = ({
|
||||
epochRewardSummaries,
|
||||
partyRewards,
|
||||
assets,
|
||||
pubKey,
|
||||
epochVariables,
|
||||
epoch,
|
||||
onEpochChange,
|
||||
loading,
|
||||
}: {
|
||||
epochRewardSummaries: RewardsHistoryQuery['epochRewardSummaries'];
|
||||
partyRewards: PartyRewardsConnection;
|
||||
assets: Record<string, AssetFieldsFragment> | null;
|
||||
pubKey: string | null;
|
||||
epoch: number;
|
||||
epochVariables: {
|
||||
from: number;
|
||||
to: number;
|
||||
};
|
||||
onEpochChange: (epochVariables: { from: number; to: number }) => void;
|
||||
loading: boolean;
|
||||
}) => {
|
||||
const [isParty, setIsParty] = useState(false);
|
||||
|
||||
const rowData = useRewardsRowData({
|
||||
epochRewardSummaries,
|
||||
partyRewards,
|
||||
assets,
|
||||
partyId: isParty ? pubKey : null,
|
||||
});
|
||||
|
||||
const columnDefs = useMemo<ColDef<RewardRow>[]>(() => {
|
||||
const rewardValueFormatter: ValueFormatterFunc<RewardRow> = ({
|
||||
data,
|
||||
value,
|
||||
}) => {
|
||||
if (!value || !data) {
|
||||
return '-';
|
||||
}
|
||||
return addDecimalsFormatNumberQuantum(
|
||||
value,
|
||||
data.asset.decimals,
|
||||
data.asset.quantum
|
||||
);
|
||||
};
|
||||
|
||||
const rewardCellRenderer = ({
|
||||
data,
|
||||
value,
|
||||
valueFormatted,
|
||||
}: {
|
||||
data: RewardRow;
|
||||
value: number;
|
||||
valueFormatted: string;
|
||||
}) => {
|
||||
if (!value || value <= 0 || !data) {
|
||||
return <span className="text-muted">-</span>;
|
||||
}
|
||||
|
||||
const pctOfTotal = new BigNumber(value).dividedBy(data.total).times(100);
|
||||
|
||||
return (
|
||||
<StackedCell
|
||||
primary={valueFormatted}
|
||||
secondary={formatNumberPercentage(pctOfTotal, 2)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const colDefs: ColDef[] = [
|
||||
{
|
||||
field: 'asset.symbol',
|
||||
cellRenderer: ({ value, data }: { value: string; data: RewardRow }) => {
|
||||
if (!value || !data) return <span>-</span>;
|
||||
return <StackedCell primary={value} secondary={data.asset.name} />;
|
||||
},
|
||||
sort: 'desc',
|
||||
},
|
||||
{
|
||||
field: 'staking',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
cellRenderer: rewardCellRenderer,
|
||||
},
|
||||
{
|
||||
field: 'priceTaking',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
cellRenderer: rewardCellRenderer,
|
||||
},
|
||||
{
|
||||
field: 'priceMaking',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
cellRenderer: rewardCellRenderer,
|
||||
},
|
||||
{
|
||||
field: 'liquidityProvision',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
cellRenderer: rewardCellRenderer,
|
||||
},
|
||||
{
|
||||
field: 'marketCreation',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
cellRenderer: rewardCellRenderer,
|
||||
},
|
||||
{
|
||||
field: 'averagePosition',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
cellRenderer: rewardCellRenderer,
|
||||
},
|
||||
{
|
||||
field: 'relativeReturns',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
cellRenderer: rewardCellRenderer,
|
||||
},
|
||||
{
|
||||
field: 'returnsVolatility',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
cellRenderer: rewardCellRenderer,
|
||||
},
|
||||
{
|
||||
field: 'validatorRanking',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
cellRenderer: rewardCellRenderer,
|
||||
},
|
||||
{
|
||||
field: 'total',
|
||||
type: 'rightAligned',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
},
|
||||
];
|
||||
return colDefs;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<h4 className="text-muted flex items-center gap-2 text-sm">
|
||||
<label htmlFor="fromEpoch">{t('From epoch')}</label>
|
||||
<EpochInput
|
||||
id="fromEpoch"
|
||||
value={epochVariables.from}
|
||||
max={epochVariables.to}
|
||||
onChange={(value) =>
|
||||
onEpochChange({
|
||||
from: value,
|
||||
to: epochVariables.to,
|
||||
})
|
||||
}
|
||||
onIncrement={() =>
|
||||
onEpochChange({
|
||||
from: epochVariables.from + 1,
|
||||
to: epochVariables.to,
|
||||
})
|
||||
}
|
||||
onDecrement={() =>
|
||||
onEpochChange({
|
||||
from: epochVariables.from - 1,
|
||||
to: epochVariables.to,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<label htmlFor="toEpoch">{t('to')}</label>
|
||||
|
||||
<EpochInput
|
||||
id="toEpoch"
|
||||
value={epochVariables.to}
|
||||
max={epoch}
|
||||
onChange={(value) =>
|
||||
onEpochChange({
|
||||
from: epochVariables.from,
|
||||
to: value,
|
||||
})
|
||||
}
|
||||
onIncrement={() =>
|
||||
onEpochChange({
|
||||
from: epochVariables.from,
|
||||
to: epochVariables.to + 1,
|
||||
})
|
||||
}
|
||||
onDecrement={() =>
|
||||
onEpochChange({
|
||||
from: epochVariables.from,
|
||||
to: epochVariables.to - 1,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</h4>
|
||||
|
||||
<div className="flex gap-0.5">
|
||||
<TradingButton
|
||||
onClick={() => setIsParty(false)}
|
||||
size="extra-small"
|
||||
minimal={isParty}
|
||||
>
|
||||
{t('Total distributed')}
|
||||
</TradingButton>
|
||||
<TradingButton
|
||||
onClick={() => setIsParty(true)}
|
||||
size="extra-small"
|
||||
disabled={!pubKey}
|
||||
minimal={!isParty}
|
||||
>
|
||||
{t('Earned by me')}
|
||||
</TradingButton>
|
||||
</div>
|
||||
</div>
|
||||
<AgGrid
|
||||
columnDefs={columnDefs}
|
||||
defaultColDef={defaultColDef}
|
||||
rowData={rowData}
|
||||
rowHeight={45}
|
||||
domLayout="autoHeight"
|
||||
// Show loading message without wiping out the current rows
|
||||
overlayNoRowsTemplate={loading ? t('Loading...') : t('No rows')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EpochInput = ({
|
||||
id,
|
||||
value,
|
||||
max,
|
||||
min = 1,
|
||||
step = 1,
|
||||
onChange,
|
||||
onIncrement,
|
||||
onDecrement,
|
||||
}: {
|
||||
id: string;
|
||||
value: number;
|
||||
max?: number;
|
||||
min?: number;
|
||||
step?: number;
|
||||
onChange: (value: number) => void;
|
||||
onIncrement: () => void;
|
||||
onDecrement: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<span className="flex gap-0.5" data-testid={id}>
|
||||
<span className="bg-vega-clight-600 dark:bg-vega-cdark-600 relative rounded-l-sm">
|
||||
<span className="px-2 opacity-0">{value}</span>
|
||||
<input
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
value={value}
|
||||
className="dark:focus:bg-vega-cdark-700 absolute left-0 top-0 h-full w-full appearance-none bg-transparent px-2 focus:outline-none"
|
||||
type="number"
|
||||
step={step}
|
||||
min={min}
|
||||
max={max}
|
||||
id={id}
|
||||
name={id}
|
||||
/>
|
||||
</span>
|
||||
<span className="flex flex-col gap-0.5 overflow-hidden rounded-r-sm">
|
||||
<button
|
||||
onClick={onIncrement}
|
||||
className="bg-vega-clight-600 dark:bg-vega-cdark-600 flex flex-1 items-center px-1"
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_UP} size={12} />
|
||||
</button>
|
||||
<button
|
||||
onClick={onDecrement}
|
||||
className="bg-vega-clight-600 dark:bg-vega-cdark-600 flex flex-1 items-center px-1"
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={12} />
|
||||
</button>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
};
|
109
apps/trading/components/rewards-container/use-reward-row-data.ts
Normal file
109
apps/trading/components/rewards-container/use-reward-row-data.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { type Asset } from '@vegaprotocol/assets';
|
||||
import { type PartyRewardsConnection } from './rewards-history';
|
||||
import { type RewardsHistoryQuery } from './__generated__/Rewards';
|
||||
|
||||
const REWARD_ACCOUNT_TYPES = [
|
||||
AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_RELATIVE_RETURN,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING,
|
||||
];
|
||||
|
||||
const getRewards = (
|
||||
rewards: Array<{
|
||||
rewardType: AccountType;
|
||||
assetId: string;
|
||||
amount: string;
|
||||
}>,
|
||||
assets: Record<string, Asset> | null
|
||||
) => {
|
||||
const assetMap = groupBy(
|
||||
rewards.filter((r) => REWARD_ACCOUNT_TYPES.includes(r.rewardType)),
|
||||
'assetId'
|
||||
);
|
||||
|
||||
return Object.keys(assetMap).map((assetId) => {
|
||||
const r = assetMap[assetId];
|
||||
const asset = assets ? assets[assetId] : undefined;
|
||||
|
||||
const totals = new Map<AccountType, number>();
|
||||
|
||||
REWARD_ACCOUNT_TYPES.forEach((type) => {
|
||||
const amountsByType = r
|
||||
.filter((a) => a.rewardType === type)
|
||||
.map((a) => a.amount);
|
||||
const typeTotal = BigNumber.sum.apply(
|
||||
null,
|
||||
amountsByType.length ? amountsByType : [0]
|
||||
);
|
||||
|
||||
totals.set(type, typeTotal.toNumber());
|
||||
});
|
||||
|
||||
const total = BigNumber.sum.apply(
|
||||
null,
|
||||
Array.from(totals).map((entry) => entry[1])
|
||||
);
|
||||
|
||||
return {
|
||||
asset,
|
||||
staking: totals.get(AccountType.ACCOUNT_TYPE_GLOBAL_REWARD),
|
||||
priceTaking: totals.get(AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES),
|
||||
priceMaking: totals.get(
|
||||
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES
|
||||
),
|
||||
liquidityProvision: totals.get(
|
||||
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES
|
||||
),
|
||||
marketCreation: totals.get(
|
||||
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS
|
||||
),
|
||||
averagePosition: totals.get(
|
||||
AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION
|
||||
),
|
||||
relativeReturns: totals.get(
|
||||
AccountType.ACCOUNT_TYPE_REWARD_RELATIVE_RETURN
|
||||
),
|
||||
returnsVolatility: totals.get(
|
||||
AccountType.ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY
|
||||
),
|
||||
validatorRanking: totals.get(
|
||||
AccountType.ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING
|
||||
),
|
||||
total: total.toNumber(),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const useRewardsRowData = ({
|
||||
partyRewards,
|
||||
epochRewardSummaries,
|
||||
assets,
|
||||
partyId,
|
||||
}: {
|
||||
partyRewards: PartyRewardsConnection;
|
||||
epochRewardSummaries: RewardsHistoryQuery['epochRewardSummaries'];
|
||||
assets: Record<string, Asset> | null;
|
||||
partyId: string | null;
|
||||
}) => {
|
||||
if (partyId) {
|
||||
const rewards = removePaginationWrapper(partyRewards?.edges).map((r) => ({
|
||||
rewardType: r.rewardType,
|
||||
assetId: r.asset.id,
|
||||
amount: r.amount,
|
||||
}));
|
||||
return getRewards(rewards, assets);
|
||||
}
|
||||
|
||||
const rewards = removePaginationWrapper(epochRewardSummaries?.edges);
|
||||
return getRewards(rewards, assets);
|
||||
};
|
@ -18,6 +18,7 @@ export const Routes = {
|
||||
REFERRALS_CREATE_CODE: '/referrals/create-code',
|
||||
TEAMS: '/teams',
|
||||
FEES: '/fees',
|
||||
REWARDS: '/rewards',
|
||||
} as const;
|
||||
|
||||
type ConsoleLinks = {
|
||||
@ -42,4 +43,5 @@ export const Links: ConsoleLinks = {
|
||||
REFERRALS_CREATE_CODE: () => Routes.REFERRALS_CREATE_CODE,
|
||||
TEAMS: () => Routes.TEAMS,
|
||||
FEES: () => Routes.FEES,
|
||||
REWARDS: () => Routes.REWARDS,
|
||||
};
|
||||
|
@ -14,6 +14,7 @@ import { Deposit } from '../client-pages/deposit';
|
||||
import { Withdraw } from '../client-pages/withdraw';
|
||||
import { Transfer } from '../client-pages/transfer';
|
||||
import { Fees } from '../client-pages/fees';
|
||||
import { Rewards } from '../client-pages/rewards';
|
||||
import { Routes as AppRoutes } from '../lib/links';
|
||||
import { LayoutWithSky } from '../client-pages/referrals/layout';
|
||||
import { Referrals } from '../client-pages/referrals/referrals';
|
||||
@ -96,6 +97,16 @@ export const routerConfig: RouteObject[] = compact([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'rewards/*',
|
||||
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Rewards />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'markets/*',
|
||||
element: (
|
||||
|
@ -4,6 +4,7 @@ import { marketsMapProvider } from '@vegaprotocol/markets';
|
||||
import {
|
||||
makeDataProvider,
|
||||
makeDerivedDataProvider,
|
||||
useDataProvider,
|
||||
} from '@vegaprotocol/data-provider';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { type Market } from '@vegaprotocol/markets';
|
||||
@ -214,3 +215,13 @@ export const aggregatedAccountDataProvider = makeDerivedDataProvider<
|
||||
(account) => account.asset.id === assetId
|
||||
) || null
|
||||
);
|
||||
|
||||
export const useAccounts = (partyId: string | null) => {
|
||||
return useDataProvider({
|
||||
dataProvider: accountsDataProvider,
|
||||
variables: {
|
||||
partyId: partyId || '',
|
||||
},
|
||||
skip: !partyId,
|
||||
});
|
||||
};
|
||||
|
@ -7,3 +7,4 @@ export * from './asset-option';
|
||||
export * from './assets-data-provider';
|
||||
export * from './constants';
|
||||
export * from './use-balances-store';
|
||||
export * from './utils';
|
||||
|
15
libs/assets/src/lib/utils.spec.ts
Normal file
15
libs/assets/src/lib/utils.spec.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { getQuantumValue } from './utils';
|
||||
|
||||
describe('getQuantumValue', () => {
|
||||
it('converts a value into its value in quantum AKA (qUSD)', () => {
|
||||
expect(getQuantumValue('1000000', '1000000').toString()).toEqual('1');
|
||||
expect(getQuantumValue('2000000', '1000000').toString()).toEqual('2');
|
||||
expect(getQuantumValue('2500000', '1000000').toString()).toEqual('2.5');
|
||||
expect(getQuantumValue('10000', '1000000').toString()).toEqual('0.01');
|
||||
expect(
|
||||
getQuantumValue('1000000000000000000', '1000000000000000000').toString()
|
||||
).toEqual('1');
|
||||
expect(getQuantumValue('100000000', '100000000').toString()).toEqual('1');
|
||||
expect(getQuantumValue('150000000', '100000000').toString()).toEqual('1.5');
|
||||
});
|
||||
});
|
5
libs/assets/src/lib/utils.ts
Normal file
5
libs/assets/src/lib/utils.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { toBigNum } from '@vegaprotocol/utils';
|
||||
|
||||
export const getQuantumValue = (value: string, quantum: string) => {
|
||||
return toBigNum(value, 0).dividedBy(toBigNum(quantum, 0));
|
||||
};
|
3
libs/markets/src/lib/__generated__/index.ts
generated
3
libs/markets/src/lib/__generated__/index.ts
generated
@ -2,8 +2,7 @@ export * from './funding-periods';
|
||||
export * from './market-candles';
|
||||
export * from './market-data';
|
||||
export * from './markets';
|
||||
export * from './markets-candles';
|
||||
export * from './markets-data';
|
||||
export * from './OracleMarketsSpec';
|
||||
export * from './OracleSpecDataConnection';
|
||||
export * from './SuccessorMarket'
|
||||
export * from './SuccessorMarket'
|
||||
|
@ -1,4 +1,3 @@
|
||||
export * from './__generated__';
|
||||
export * from './components';
|
||||
export * from './oracle-schema';
|
||||
export * from './oracle-spec-data-connection.mock';
|
||||
@ -13,3 +12,13 @@ export * from './markets-candles-provider';
|
||||
export * from './markets-data-provider';
|
||||
export * from './markets-provider';
|
||||
export * from './product';
|
||||
|
||||
export * from './__generated__/funding-periods';
|
||||
export * from './__generated__/market-candles';
|
||||
export * from './__generated__/markets-candles';
|
||||
export * from './__generated__/market-data';
|
||||
export * from './__generated__/markets';
|
||||
export * from './__generated__/markets-data';
|
||||
export * from './__generated__/OracleMarketsSpec';
|
||||
export * from './__generated__/OracleSpecDataConnection';
|
||||
export * from './__generated__/SuccessorMarket';
|
||||
|
@ -1,12 +1,3 @@
|
||||
fragment MarketCandlesFields on Candle {
|
||||
high
|
||||
low
|
||||
open
|
||||
close
|
||||
volume
|
||||
periodStart
|
||||
}
|
||||
|
||||
query MarketsCandles($interval: Interval!, $since: String!) {
|
||||
marketsConnection {
|
||||
edges {
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
export const NetworkParams = {
|
||||
blockchains_ethereumConfig: 'blockchains_ethereumConfig',
|
||||
reward_asset: 'reward_asset',
|
||||
rewards_activityStreak_benefitTiers: 'rewards_activityStreak_benefitTiers',
|
||||
rewards_marketCreationQuantumMultiple:
|
||||
'rewards_marketCreationQuantumMultiple',
|
||||
reward_staking_delegation_payoutDelay:
|
||||
|
@ -0,0 +1,7 @@
|
||||
export const IconChevronRight = ({ size = 16 }: { size: number }) => {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 16 16">
|
||||
<path d="M4.75 14.38L4 13.62L9.63 8.00001L4 2.38001L4.75 1.62001L11.13 8.00001L4.75 14.38Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
export const IconLock = ({ size = 14 }: { size: number }) => {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 11 15">
|
||||
<path
|
||||
d="M7.5168 6.48099V3.5056C7.5168 2.84108 7.25282 2.20377 6.78293 1.73388C6.31304 1.26399 5.67573 1.00001 5.01121 1.00001C4.34668 1.00001 3.70938 1.26399 3.23949 1.73388C2.7696 2.20377 2.50562 2.84108 2.50562 3.5056V6.48099"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="stroke-current"
|
||||
fill="none"
|
||||
/>
|
||||
<rect
|
||||
x="0.5"
|
||||
y="6.5"
|
||||
width="9"
|
||||
height="7.5"
|
||||
className="stroke-current"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -7,6 +7,7 @@ import { IconBreakdown } from './svg-icons/icon-breakdown';
|
||||
import { IconBullet } from './svg-icons/icon-bullet';
|
||||
import { IconChevronDown } from './svg-icons/icon-chevron-down';
|
||||
import { IconChevronLeft } from './svg-icons/icon-chevron-left';
|
||||
import { IconChevronRight } from './svg-icons/icon-chevron-right';
|
||||
import { IconChevronUp } from './svg-icons/icon-chevron-up';
|
||||
import { IconCog } from './svg-icons/icon-cog';
|
||||
import { IconCopy } from './svg-icons/icon-copy';
|
||||
@ -21,6 +22,7 @@ import { IconGlobe } from './svg-icons/icon-globe';
|
||||
import { IconInfo } from './svg-icons/icon-info';
|
||||
import { IconKebab } from './svg-icons/icon-kebab';
|
||||
import { IconLinkedIn } from './svg-icons/icon-linkedin';
|
||||
import { IconLock } from './svg-icons/icon-lock';
|
||||
import { IconMetaMask } from './svg-icons/icon-metamask';
|
||||
import { IconMinus } from './svg-icons/icon-minus';
|
||||
import { IconMoon } from './svg-icons/icon-moon';
|
||||
@ -49,6 +51,7 @@ export enum VegaIconNames {
|
||||
BULLET = 'bullet',
|
||||
CHEVRON_DOWN = 'chevron-down',
|
||||
CHEVRON_LEFT = 'chevron-left',
|
||||
CHEVRON_RIGHT = 'chevron-right',
|
||||
CHEVRON_UP = 'chevron-up',
|
||||
COG = 'cog',
|
||||
COPY = 'copy',
|
||||
@ -63,6 +66,7 @@ export enum VegaIconNames {
|
||||
INFO = 'info',
|
||||
KEBAB = 'kebab',
|
||||
LINKEDIN = 'linkedin',
|
||||
LOCK = 'lock',
|
||||
METAMASK = 'metamask',
|
||||
MINUS = 'minus',
|
||||
MOON = 'moon',
|
||||
@ -93,6 +97,7 @@ export const VegaIconNameMap: Record<
|
||||
'arrow-up': IconArrowUp,
|
||||
'chevron-down': IconChevronDown,
|
||||
'chevron-left': IconChevronLeft,
|
||||
'chevron-right': IconChevronRight,
|
||||
'chevron-up': IconChevronUp,
|
||||
'eye-off': IconEyeOff,
|
||||
'exclaimation-mark': IconExclaimationMark,
|
||||
@ -113,6 +118,7 @@ export const VegaIconNameMap: Record<
|
||||
info: IconInfo,
|
||||
kebab: IconKebab,
|
||||
linkedin: IconLinkedIn,
|
||||
lock: IconLock,
|
||||
metamask: IconMetaMask,
|
||||
minus: IconMinus,
|
||||
moon: IconMoon,
|
||||
|
@ -15,6 +15,7 @@ type TradingButtonProps = {
|
||||
icon?: ReactNode;
|
||||
subLabel?: ReactNode;
|
||||
fill?: boolean;
|
||||
minimal?: boolean;
|
||||
};
|
||||
|
||||
const getClassName = (
|
||||
@ -23,7 +24,11 @@ const getClassName = (
|
||||
subLabel,
|
||||
intent,
|
||||
fill,
|
||||
}: Pick<TradingButtonProps, 'size' | 'subLabel' | 'intent' | 'fill'>,
|
||||
minimal,
|
||||
}: Pick<
|
||||
TradingButtonProps,
|
||||
'size' | 'subLabel' | 'intent' | 'fill' | 'minimal'
|
||||
>,
|
||||
className?: string
|
||||
) =>
|
||||
classNames(
|
||||
@ -41,27 +46,37 @@ const getClassName = (
|
||||
// colours
|
||||
{
|
||||
'bg-vega-yellow enabled:hover:bg-vega-yellow-550 dark:bg-vega-yellow dark:enabled:hover:bg-vega-yellow-450':
|
||||
intent === Intent.Primary,
|
||||
intent === Intent.Primary && !minimal,
|
||||
'bg-vega-clight-500 enabled:hover:bg-vega-clight-400 dark:bg-vega-cdark-500 dark:enabled:hover:bg-vega-cdark-400':
|
||||
intent === Intent.None,
|
||||
intent === Intent.None && !minimal,
|
||||
'bg-vega-blue-350 enabled:hover:bg-vega-blue-400 dark:bg-vega-blue-650 dark:enabled:hover:bg-vega-blue-600':
|
||||
intent === Intent.Info,
|
||||
intent === Intent.Info && !minimal,
|
||||
'bg-vega-orange-350 enabled:hover:bg-vega-orange-400 dark:bg-vega-orange-650 dark:enabled:hover:bg-vega-orange-600':
|
||||
intent === Intent.Warning,
|
||||
intent === Intent.Warning && !minimal,
|
||||
'bg-vega-red-350 enabled:hover:bg-vega-red-400 dark:bg-vega-red-650 dark:enabled:hover:bg-vega-red-600':
|
||||
intent === Intent.Danger,
|
||||
intent === Intent.Danger && !minimal,
|
||||
'bg-vega-green-350 enabled:hover:bg-vega-green-400 dark:bg-vega-green-650 dark:enabled:hover:bg-vega-green-600':
|
||||
intent === Intent.Success,
|
||||
'text-vega-clight-50 dark:text-vega-cdark-50': intent !== Intent.Primary,
|
||||
'text-vega-clight-900 dark:text-vega-cdark-900':
|
||||
intent === Intent.Primary,
|
||||
intent === Intent.Success && !minimal,
|
||||
// Minimal button
|
||||
'bg-transparent enabled:hover:bg-vega-yellow-550 dark:enabled:hover:bg-vega-yellow-450':
|
||||
intent === Intent.Primary && minimal,
|
||||
'bg-transparent enabled:hover:bg-vega-clight-400 dark:enabled:hover:bg-vega-cdark-400':
|
||||
intent === Intent.None && minimal,
|
||||
'bg-transparent enabled:hover:bg-vega-blue-400 dark:enabled:hover:bg-vega-blue-600':
|
||||
intent === Intent.Info && minimal,
|
||||
'bg-transparent enabled:hover:bg-vega-orange-400 dark:enabled:hover:bg-vega-orange-600':
|
||||
intent === Intent.Warning && minimal,
|
||||
'bg-transparent enabled:hover:bg-vega-red-400 dark:enabled:hover:bg-vega-red-600':
|
||||
intent === Intent.Danger && minimal,
|
||||
'bg-transparent enabled:hover:bg-vega-green-400 dark:enabled:hover:bg-vega-green-600':
|
||||
intent === Intent.Success && minimal,
|
||||
},
|
||||
// text
|
||||
{
|
||||
'text-vega-clight-50 dark:text-vega-cdark-50': intent !== Intent.Primary,
|
||||
'!text-vega-clight-50': intent === Intent.Primary,
|
||||
'text-vega-clight-100 dark:text-vega-cdark-100':
|
||||
intent === Intent.Primary,
|
||||
|
||||
// If its primary the text must always be dark enough for a yellow background
|
||||
'text-vega-clight-50': intent === Intent.Primary,
|
||||
'[&_[data-sub-label]]:text-vega-clight-100': intent === Intent.Primary,
|
||||
},
|
||||
{ 'w-full': fill },
|
||||
@ -99,6 +114,7 @@ export const TradingButton = forwardRef<
|
||||
size = 'medium',
|
||||
intent = Intent.None,
|
||||
type = 'button',
|
||||
minimal = false,
|
||||
icon,
|
||||
children,
|
||||
className,
|
||||
@ -112,7 +128,10 @@ export const TradingButton = forwardRef<
|
||||
ref={ref}
|
||||
type={type}
|
||||
data-trading-button
|
||||
className={getClassName({ size, subLabel, intent, fill }, className)}
|
||||
className={getClassName(
|
||||
{ size, subLabel, intent, fill, minimal },
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Content icon={icon} subLabel={subLabel} children={children} />
|
||||
@ -123,6 +142,7 @@ export const TradingButton = forwardRef<
|
||||
export const TradingAnchorButton = ({
|
||||
size = 'medium',
|
||||
intent = Intent.None,
|
||||
minimal = false,
|
||||
icon,
|
||||
href,
|
||||
children,
|
||||
@ -133,7 +153,7 @@ export const TradingAnchorButton = ({
|
||||
TradingButtonProps & { href: string }) => (
|
||||
<Link
|
||||
to={href}
|
||||
className={getClassName({ size, subLabel, intent }, className)}
|
||||
className={getClassName({ size, subLabel, intent, minimal }, className)}
|
||||
{...props}
|
||||
>
|
||||
<Content icon={icon} subLabel={subLabel} children={children} />
|
||||
|
42
specs/7009-REWA-rewards.md
Normal file
42
specs/7009-REWA-rewards.md
Normal file
@ -0,0 +1,42 @@
|
||||
# Rewards page
|
||||
|
||||
## Reward pots
|
||||
|
||||
- For any asset the connected party has rewards in
|
||||
- **Must** be able to see the connected party's total reward pot for Vega (vestingBalancesSummary.locked + vestingBalancesSummary.vesting) (<a name="7009-REWA-001" href="#7009-REWA-001">7009-REWA-001</a>)
|
||||
- **Must** be able to see how much locked Vega exists for the connected party (<a name="7009-REWA-002" href="#7009-REWA-002">7009-REWA-002</a>)
|
||||
- **Must** be able to see how much vesting Vega exists for the connected party (<a name="7009-REWA-003" href="#7009-REWA-003">7009-REWA-003</a>)
|
||||
- **Must** be able to see how much rewarded Vega is available to withrdraw immediately (<a name="7009-REWA-004" href="#7009-REWA-004">7009-REWA-004</a>)
|
||||
|
||||
### Vega reward pot
|
||||
|
||||
- **Must** always see the Vega reward pot (<a name="7009-REWA-005" href="#7009-REWA-005">7009-REWA-005</a>)
|
||||
- **Must** a disconnected message if not connected (<a name="7009-REWA-006" href="#7009-REWA-006">7009-REWA-006</a>)
|
||||
|
||||
## Vesting
|
||||
|
||||
- **Must** be ablet to see the computed vesting rate (baseRate \* rewardVestingMultiplier) (<a name="7009-REWA-007" href="#7009-REWA-007">7009-REWA-007</a>)
|
||||
- **Must** be the vesting base rate (<a name="7009-REWA-008" href="#7009-REWA-008">7009-REWA-008</a>)
|
||||
- **Must** be able to see the reward vesting multiplier (party.activityStreak.rewardVestingMultiplier) (<a name="7009-REWA-009" href="#7009-REWA-009">7009-REWA-009</a>)
|
||||
|
||||
## Rewards multipliers
|
||||
|
||||
- **Must** be able to view the streak reward multiplier (party.activityStreak.rewardDistributionMultiplier) (<a name="7009-REWA-010" href="#7009-REWA-010">7009-REWA-010</a>):
|
||||
- **Must** be able to view the hoarder reward multiplier (party.vestingStats.rewardsBonusMultiplier) (<a name="7009-REWA-011" href="#7009-REWA-011">7009-REWA-011</a>):
|
||||
|
||||
## Reward history
|
||||
|
||||
- **Must** see a table showing all reward types per asset (<a name="7009-REWA-012" href="#7009-REWA-012">7009-REWA-012</a>)
|
||||
- Asset
|
||||
- Staking
|
||||
- Price taking
|
||||
- Price making
|
||||
- Liquidity provision
|
||||
- Market creation
|
||||
- Average position
|
||||
- Relative returns
|
||||
- Returns volatility
|
||||
- Validator ranking
|
||||
- Total sum of all reward types for asset
|
||||
- **Must** be able to filter rewards by epoch (<a name="7009-REWA-013" href="#7009-REWA-013">7009-REWA-013</a>)
|
||||
- **Must** be able to toggle between seeing all rewards and all rewards earned by the connected party (<a name="7009-REWA-014" href="#7009-REWA-014">7009-REWA-014</a>)
|
Loading…
Reference in New Issue
Block a user