Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement rewards calculator and merge payout history #2482

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f4e0537
Save changes on feature/rewardscalc
joeljkrb Jan 22, 2025
6723e20
feat: Migrate PayoutHistory to new Rewards Calculator page
joeljkrb Feb 3, 2025
5841df6
chore: rewards & payouts page merge
joeljkrb Feb 3, 2025
319cab2
chore: minor language updates
joeljkrb Feb 3, 2025
a934b20
chore: fixes
joeljkrb Feb 3, 2025
7f765b7
Merge branch 'main' into tabbed-rewards-calc
joeljkrb Feb 3, 2025
6b1627d
chore: fixes
joeljkrb Feb 4, 2025
e0f25b0
Merge branch 'tabbed-rewards-calc' of https://github.com/polkadot-clo…
joeljkrb Feb 4, 2025
be105ff
chore: ES update fix
joeljkrb Feb 4, 2025
403c0b9
chore: language updates
joeljkrb Feb 4, 2025
8ef27c1
Merge branch 'tabbed-rewards-calc' of https://github.com/polkadot-clo…
joeljkrb Feb 4, 2025
494e538
order locales
rossbulat Feb 4, 2025
e5bf291
fixing ross's breaking changes
rossbulat Feb 4, 2025
a766263
Merge branch 'main' into tabbed-rewards-calc
rossbulat Feb 6, 2025
3b4f8f3
remove `useTokenPrice` hook, use `useTokenPrices`
rossbulat Feb 6, 2025
fc61035
update copyright
rossbulat Feb 6, 2025
85fb460
Merge branch 'main' into tabbed-rewards-calc
rossbulat Feb 9, 2025
ecabfc6
fix breaking changes
rossbulat Feb 9, 2025
3fbc801
use pagination from `ListProvider`
rossbulat Feb 9, 2025
f952669
add `getStakedBalance` to `TransferOptionsProvider`
rossbulat Feb 9, 2025
d4a6e9e
use `getStakedBalance`, formalise Stat components
rossbulat Feb 9, 2025
5ec3b72
+ `Button` stat card, Reward Calculator stat, `StatButton core ui
rossbulat Feb 9, 2025
03e85a2
don't scale `Button` stat on hover
rossbulat Feb 9, 2025
a2afc12
amend styling
rossbulat Feb 9, 2025
4817363
fix: use child combinator
rossbulat Feb 9, 2025
0bc147b
style amendments
rossbulat Feb 9, 2025
6c121e4
fix: use locale
rossbulat Feb 9, 2025
e4c8e0e
Update packages/app/src/config/pages.ts
joeljkrb Feb 9, 2025
cf7c3c4
Update packages/app/src/pages/Rewards/Active/index.tsx
joeljkrb Feb 9, 2025
695778e
Update packages/app/src/pages/Rewards/Active/index.tsx
joeljkrb Feb 9, 2025
3649eb1
Merge branch 'main' into tabbed-rewards-calc
rossbulat Feb 12, 2025
41218d0
Merge branch 'main' into tabbed-rewards-calc
rossbulat Feb 13, 2025
eea134b
fix staking api
rossbulat Feb 13, 2025
5306ed2
resolve some feedack
rossbulat Feb 13, 2025
ae37f13
restructuring
rossbulat Feb 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions packages/app/src/config/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import type { PageCategoryItems, PagesConfigItems } from 'common-types'
import { Community } from 'pages/Community'
import { Nominate } from 'pages/Nominate'
import { Overview } from 'pages/Overview'
import { Payouts } from 'pages/Payouts'
import { Pools } from 'pages/Pools'
import { Rewards } from 'pages/Rewards'
import { Validators } from 'pages/Validators'

const BASE_URL = import.meta.env.BASE_URL
Expand Down Expand Up @@ -52,10 +52,10 @@ export const PagesConfig: PagesConfigItems = [
},
{
category: 2,
key: 'payouts',
uri: `${BASE_URL}payouts`,
hash: '/payouts',
Entry: Payouts,
key: 'rewards',
uri: `${BASE_URL}rewards`,
hash: '/rewards',
Entry: Rewards,
lottie: 'analytics',
},
{
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/contexts/TransferOptions/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { TransferOptions, TransferOptionsContextInterface } from './types'

export const defaultTransferOptionsContext: TransferOptionsContextInterface = {
getTransferOptions: (a) => defaultTransferOptions,
getStakedBalance: (a) => new BigNumber(0),
setFeeReserveBalance: (r) => {},
feeReserve: new BigNumber(0),
getFeeReserve: (address) => new BigNumber(0),
Expand Down
30 changes: 30 additions & 0 deletions packages/app/src/contexts/TransferOptions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useNetwork } from 'contexts/Network'
import type { ReactNode } from 'react'
import { createContext, useContext, useState } from 'react'
import type { MaybeAddress } from 'types'
import { planckToUnitBn } from 'utils'
import { defaultTransferOptionsContext } from './defaults'
import type { TransferOptions, TransferOptionsContextInterface } from './types'
import { getLocalFeeReserve, setLocalFeeReserve } from './Utils'
Expand Down Expand Up @@ -122,6 +123,34 @@ export const TransferOptionsProvider = ({
setFeeReserve(amount)
}

// Gets staked balance, whether nominating or in pool, for an account
const getStakedBalance = (address: MaybeAddress): BigNumber => {
const allTransferOptions = getTransferOptions(address)

// Total funds nominating
const nominating = planckToUnitBn(
allTransferOptions.nominate.active
.plus(allTransferOptions.nominate.totalUnlocking)
.plus(allTransferOptions.nominate.totalUnlocked),
units
)

// Total funds in pool
const inPool = planckToUnitBn(
allTransferOptions.pool.active
.plus(allTransferOptions.pool.totalUnlocking)
.plus(allTransferOptions.pool.totalUnlocked),
units
)

// Determine the actual staked balance
return !nominating.isZero()
? nominating
: !inPool.isZero()
? inPool
: new BigNumber(0)
}

// Gets a feeReserve from local storage for an account, or the default value otherwise
const getFeeReserve = (address: MaybeAddress): BigNumber =>
getLocalFeeReserve(address, defaultFeeReserve, { network, units })
Expand All @@ -137,6 +166,7 @@ export const TransferOptionsProvider = ({
<TransferOptionsContext.Provider
value={{
getTransferOptions,
getStakedBalance,
setFeeReserveBalance,
feeReserve,
getFeeReserve,
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/contexts/TransferOptions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { MaybeAddress } from 'types'

export interface TransferOptionsContextInterface {
getTransferOptions: (a: MaybeAddress) => TransferOptions
getStakedBalance: (a: MaybeAddress) => BigNumber
setFeeReserveBalance: (r: BigNumber) => void
feeReserve: BigNumber
getFeeReserve: (address: MaybeAddress) => BigNumber
Expand Down
57 changes: 57 additions & 0 deletions packages/app/src/library/StatCards/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2025 @polkadot-cloud/polkadot-staking-dashboard authors & contributors
// SPDX-License-Identifier: GPL-3.0-only

import { faTimes, faUpRightFromSquare } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useHelp } from 'contexts/Help'
import { ButtonHelp } from 'ui-buttons'
import {
StatButton,
StatContent,
StatGraphic,
StatSubtitle,
StatTitle,
} from 'ui-core/base'
import type { ButtonProps } from './types'

export const Button = ({
Icon,
label,
title,
helpKey,
onClick,
active,
}: ButtonProps) => {
const { openHelp } = useHelp()

return (
<StatButton>
<button type="button" onClick={() => onClick()}>
<div style={{ position: 'absolute', right: '2rem', top: '0.5rem' }}>
{active ? (
<FontAwesomeIcon
icon={faTimes}
color="var(--text-color-secondary)"
/>
) : (
<FontAwesomeIcon
icon={faUpRightFromSquare}
transform="shrink-3"
color="var(--text-color-secondary)"
/>
)}
</div>
<StatGraphic>{Icon}</StatGraphic>
<StatContent>
<StatTitle>{title}</StatTitle>
<StatSubtitle primary>
{label}{' '}
{helpKey !== undefined ? (
<ButtonHelp marginLeft onClick={() => openHelp(helpKey)} />
) : null}
</StatSubtitle>
</StatContent>
</button>
</StatButton>
)
}
10 changes: 10 additions & 0 deletions packages/app/src/library/StatCards/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only

import type { TimeLeftFormatted } from '@w3ux/types'
import type { ReactNode } from 'react'

export interface NumberProps {
label: string
Expand Down Expand Up @@ -41,3 +42,12 @@ export interface TimeleftProps {
tooltip?: string
helpKey?: string
}

export interface ButtonProps {
Icon: ReactNode
label: string
title: string
helpKey?: string
active: boolean
onClick: () => void
}
175 changes: 175 additions & 0 deletions packages/app/src/pages/Rewards/Overview/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// Copyright 2025 @polkadot-cloud/polkadot-staking-dashboard authors & contributors
// SPDX-License-Identifier: GPL-3.0-only

import { useActiveAccounts } from 'contexts/ActiveAccounts'
import { useNetwork } from 'contexts/Network'
import { useTokenPrices } from 'contexts/TokenPrice'
import { useTransferOptions } from 'contexts/TransferOptions'
import { AnimatePresence, motion } from 'framer-motion'
import { useAverageRewardRate } from 'hooks/useAverageRewardRate'
import { CardWrapper } from 'library/Card/Wrappers'
import { AverageRewardRate } from 'pages/Overview/Stats/AveragelRewardRate'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { CardHeader, PageRow, StatRow } from 'ui-core/base'
import { RewardCalculator } from '../Stats/RewardCalculator'
import { StakedBalance } from '../Stats/StakedBalance'
import { RewardsGrid } from '../Wrappers'
import type { PageProps } from '../types'

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const Overview = (_: PageProps) => {
const { t } = useTranslation('pages')
const { networkData } = useNetwork()
const { price: dotPrice } = useTokenPrices()
const { activeAccount } = useActiveAccounts()
const { getStakedBalance } = useTransferOptions()
const { getAverageRewardRate } = useAverageRewardRate()
const [manualStake, setManualStake] = useState<number | null>(null)
const [isCustomStake, setIsCustomStake] = useState(false)

const { avgRateBeforeCommission } = getAverageRewardRate(false)
const rewardRate = avgRateBeforeCommission.toNumber()

const currentStake =
isCustomStake && manualStake !== null
? manualStake
: getStakedBalance(activeAccount).toNumber()

const annualReward = currentStake * (rewardRate / 100) || 0
const monthlyReward = annualReward / 12 || 0
const dailyReward = annualReward / 365 || 0

const handleToggleStake = () => {
if (isCustomStake) {
setManualStake(null)
}
setIsCustomStake(!isCustomStake)
}

return (
<>
<StatRow>
<AverageRewardRate />
<StakedBalance isCustomStake={isCustomStake} />
<RewardCalculator
isCustomStake={isCustomStake}
onClick={handleToggleStake}
/>
</StatRow>

<AnimatePresence>
{isCustomStake && (
<PageRow>
<motion.div
initial={{ opacity: 0, height: 0, marginTop: 0 }}
animate={{ opacity: 1, height: 'auto', marginTop: '1rem' }}
exit={{ opacity: 0, height: 0, marginTop: 0 }}
transition={{
duration: 0.3,
ease: 'easeInOut',
}}
style={{ overflow: 'hidden' }}
>
<CardWrapper>
<CardHeader>
<h4>{t('rewards.adjustStake')}</h4>
</CardHeader>

<div style={{ padding: '1rem' }}>
<label htmlFor="manual-stake">
{t('rewards.enterStakeAmount')} ({networkData.api.unit}):
</label>
<input
id="manual-stake"
type="number"
step="0.01"
value={manualStake ?? ''}
onChange={(e) =>
setManualStake(Number(e.target.value) || null)
}
placeholder={t('rewards.stakePlaceholder')}
style={{
width: '100%',
padding: '0.5rem',
marginTop: '0.5rem',
border: '1px solid var(--border-primary-color)',
borderRadius: '0.5rem',
}}
/>
</div>
</CardWrapper>
</motion.div>
</PageRow>
)}
</AnimatePresence>

<PageRow>
<CardWrapper>
<CardHeader>
<h3>{t('rewards.projectedRewards')}</h3>
</CardHeader>

<RewardsGrid>
<div className="header">
<span>{t('rewards.period')}</span>
<span>{networkData.unit}</span>
<span>USDT</span>
</div>

<div className="reward-row">
<span>{t('rewards.daily')}</span>
<span>
{dailyReward.toLocaleString('en-US', {
minimumFractionDigits: 3,
maximumFractionDigits: 3,
})}
</span>
<span>
$
{(dailyReward * dotPrice).toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</span>
</div>

<div className="reward-row">
<span>{t('rewards.monthly')}</span>
<span>
{monthlyReward.toLocaleString('en-US', {
minimumFractionDigits: 3,
maximumFractionDigits: 3,
})}
</span>
<span>
$
{(monthlyReward * dotPrice).toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</span>
</div>

<div className="reward-row">
<span>{t('rewards.annual')}</span>
<span>
{annualReward.toLocaleString('en-US', {
minimumFractionDigits: 3,
maximumFractionDigits: 3,
})}
</span>
<span>
$
{(annualReward * dotPrice).toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</span>
</div>
</RewardsGrid>
</CardWrapper>
</PageRow>
</>
)
}
Loading
Loading