feat(trading): rewards page (#5222)

This commit is contained in:
Matthew Russell 2023-11-15 13:46:19 -08:00 committed by GitHub
parent 6d32fa7362
commit 090d340364
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1990 additions and 220 deletions

View File

@ -0,0 +1 @@
export { Rewards } from './rewards';

View 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>
);
};

View 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)} />
);
};

View File

@ -0,0 +1 @@
export { Card, CardStat, CardTable, CardTableTH, CardTableTD } from './card';

View File

@ -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>
);
};

View File

@ -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')}

View File

@ -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>
);
};

View File

@ -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');

View File

@ -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'
)}

View 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
}
}

View 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>;

View File

@ -0,0 +1 @@
export { RewardsContainer } from './rewards-container';

View File

@ -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();
});
});

View 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>
);
};

View File

@ -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 });
});
});

View 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>
);
};

View 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);
};

View File

@ -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,
};

View File

@ -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: (

View File

@ -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,
});
};

View File

@ -7,3 +7,4 @@ export * from './asset-option';
export * from './assets-data-provider';
export * from './constants';
export * from './use-balances-store';
export * from './utils';

View 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');
});
});

View 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));
};

View File

@ -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'

View File

@ -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';

View File

@ -1,12 +1,3 @@
fragment MarketCandlesFields on Candle {
high
low
open
close
volume
periodStart
}
query MarketsCandles($interval: Interval!, $since: String!) {
marketsConnection {
edges {

View File

@ -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:

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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,

View File

@ -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} />

View 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>)