From 5166f6ac332c9f7321dea3227de975d62d710bcb Mon Sep 17 00:00:00 2001 From: Antonio Date: Wed, 11 Dec 2024 16:00:25 +0100 Subject: [PATCH] feat(ABI): annual backers incentives --- .../collective-rewards/metrics/Metrics.tsx | 14 ++- .../metrics/TotalActiveBuildersMetrics.tsx | 3 +- .../metrics/TotalAllocationsMetrics.tsx | 7 +- .../components/ABIMetrics/ABIMetrics.tsx | 43 +++++++ .../components/ABIMetrics/hooks/useGetABI.ts | 119 ++++++++++++++++++ .../metrics/components/ABIMetrics/index.ts | 1 + .../metrics/hooks/useGetTotalAllocation.ts | 12 ++ .../collective-rewards/rewards/MyRewards.tsx | 10 +- .../rewards/backers/ClaimableRewards.tsx | 1 - .../backers/hooks/useGetBackerRewards.ts | 4 +- .../rewards/builders/EstimatedRewards.tsx | 11 +- .../builders/hooks/useGetBuildersRewards.ts | 27 ++-- .../rewards/components/Tooltip.tsx | 4 +- src/app/collective-rewards/types.ts | 2 + .../utils/isBuilderOperational.ts | 10 ++ src/components/Popover/Popover.tsx | 5 +- 16 files changed, 232 insertions(+), 41 deletions(-) create mode 100644 src/app/collective-rewards/metrics/components/ABIMetrics/ABIMetrics.tsx create mode 100644 src/app/collective-rewards/metrics/components/ABIMetrics/hooks/useGetABI.ts create mode 100644 src/app/collective-rewards/metrics/components/ABIMetrics/index.ts create mode 100644 src/app/collective-rewards/metrics/hooks/useGetTotalAllocation.ts diff --git a/src/app/collective-rewards/metrics/Metrics.tsx b/src/app/collective-rewards/metrics/Metrics.tsx index 6e5e7505..b00ebcd0 100644 --- a/src/app/collective-rewards/metrics/Metrics.tsx +++ b/src/app/collective-rewards/metrics/Metrics.tsx @@ -1,4 +1,4 @@ -import { useGetGaugesArrayByType, withBuilderButton } from '@/app/collective-rewards/user' +import { useGetGaugesArray, withBuilderButton } from '@/app/collective-rewards/user' import { HeaderTitle } from '@/components/Typography' import { TotalAllocationsMetrics, @@ -11,12 +11,13 @@ import { getAddress } from 'viem' import { tokenContracts } from '@/lib/contracts' import { getCoinbaseAddress } from '@/app/collective-rewards/utils' import { PricesContextProvider } from '@/shared/context/PricesContext' +import { ABIMetrics } from './components/ABIMetrics' const HeaderWithBuilderButton = withBuilderButton(HeaderTitle) export const Metrics = () => { - const { data: activeGauges } = useGetGaugesArrayByType('active') - const gauges = activeGauges ?? [] + const { data: allGauges } = useGetGaugesArray() + const gauges = allGauges ?? [] const tokens = { rif: { @@ -35,14 +36,17 @@ export const Metrics = () => {
-
+
-
+
+
+ +
diff --git a/src/app/collective-rewards/metrics/TotalActiveBuildersMetrics.tsx b/src/app/collective-rewards/metrics/TotalActiveBuildersMetrics.tsx index 893603ef..db168f43 100644 --- a/src/app/collective-rewards/metrics/TotalActiveBuildersMetrics.tsx +++ b/src/app/collective-rewards/metrics/TotalActiveBuildersMetrics.tsx @@ -2,14 +2,13 @@ import { useGetBuildersByState } from '@/app/collective-rewards/user' import { MetricsCard, MetricsCardTitle, TokenMetricsCardRow } from '@/app/collective-rewards/rewards' import { withSpinner } from '@/components/LoadingSpinner/withLoadingSpinner' import { useHandleErrors } from '@/app/collective-rewards/utils' -import { Builder } from '../types' export const TotalActiveBuildersMetrics = () => { const { data: activatedBuilders, isLoading, error, - } = useGetBuildersByState>({ + } = useGetBuildersByState({ activated: true, communityApproved: true, kycApproved: true, diff --git a/src/app/collective-rewards/metrics/TotalAllocationsMetrics.tsx b/src/app/collective-rewards/metrics/TotalAllocationsMetrics.tsx index f3bfba49..af626f29 100644 --- a/src/app/collective-rewards/metrics/TotalAllocationsMetrics.tsx +++ b/src/app/collective-rewards/metrics/TotalAllocationsMetrics.tsx @@ -12,6 +12,7 @@ import { } from '@/app/collective-rewards/rewards' import { withSpinner } from '@/components/LoadingSpinner/withLoadingSpinner' import { useHandleErrors } from '@/app/collective-rewards/utils' +import { useGetTotalAllocation } from './hooks/useGetTotalAllocation' type TotalAllocationsProps = { gauges: Address[] @@ -25,13 +26,11 @@ export const TotalAllocationsMetrics: FC = ({ currency = 'USD', }) => { const { prices } = usePricesContext() - const { data, isLoading, error } = useGaugesGetFunction(gauges, 'totalAllocation') + const { data: totalAllocations, isLoading, error } = useGetTotalAllocation(gauges) useHandleErrors({ error, title: 'Error loading total allocations' }) + const totalAllocationsInHuman = Number(formatOnchainFraction(totalAllocations)) const price = prices[symbol]?.price ?? 0 - - const totalAllocations = Object.values(data).reduce((acc, allocation) => acc + allocation, 0n) - const totalAllocationsInHuman = Number(formatOnchainFraction(totalAllocations)) const fiatAmount = `= ${currency} ${formatCurrency(totalAllocationsInHuman * price, currency)}` return ( diff --git a/src/app/collective-rewards/metrics/components/ABIMetrics/ABIMetrics.tsx b/src/app/collective-rewards/metrics/components/ABIMetrics/ABIMetrics.tsx new file mode 100644 index 00000000..f2e67ccd --- /dev/null +++ b/src/app/collective-rewards/metrics/components/ABIMetrics/ABIMetrics.tsx @@ -0,0 +1,43 @@ +import { MetricsCard, MetricsCardTitle, TokenMetricsCardRow } from '@/app/collective-rewards/rewards' +import { withSpinner } from '@/components/LoadingSpinner/withLoadingSpinner' +import { useGetABI } from './hooks/useGetABI' + +export const ABIMetrics = () => { + const { data: abiPct, isLoading } = useGetABI() + return ( + <> + + + The Annual Backers Incentives (%) represents an estimate of the annualized percentage of + rewards that backers could receive based on their backing allocations. +
+
+ The calculation follows the formula: (1 + Rewards per stRIF per Cycle / RIF price)^26 - 1. +
+
+ This estimation is dynamic and may vary based on total rewards and user activity. This data is + for informational purposes only.{' '} + + ), + popoverProps: { + size: 'medium', + position: 'left-bottom', + }, + }} + /> + {withSpinner( + TokenMetricsCardRow, + 'min-h-0 grow-0', + )({ + amount: `${abiPct.toFixed(0)}%`, + isLoading, + })} +
+ + ) +} diff --git a/src/app/collective-rewards/metrics/components/ABIMetrics/hooks/useGetABI.ts b/src/app/collective-rewards/metrics/components/ABIMetrics/hooks/useGetABI.ts new file mode 100644 index 00000000..e94a8ec7 --- /dev/null +++ b/src/app/collective-rewards/metrics/components/ABIMetrics/hooks/useGetABI.ts @@ -0,0 +1,119 @@ +import { + useGetBackersRewardPercentage, + useGetRewardsCoinbase, + useGetRewardsERC20, +} from '@/app/collective-rewards/rewards' +import { usePricesContext } from '@/shared/context/PricesContext' +import { useGetBuildersByState } from '@/app/collective-rewards/user' +import { RequiredBuilder } from '@/app/collective-rewards/types' +import { useGaugesGetFunction } from '@/app/collective-rewards/shared' +import { useCycleContext } from '@/app/collective-rewards/metrics' +import { formatEther } from 'viem' +import { useMemo } from 'react' + +export const useGetABI = () => { + const { + data: builders, + isLoading: buildersLoading, + error: buildersError, + } = useGetBuildersByState({ + activated: true, + communityApproved: true, + kycApproved: true, + revoked: false, + }) + + const { data: rifRewards, isLoading: rifRewardsLoading, error: rifRewardsError } = useGetRewardsERC20() + const { + data: rbtcRewards, + isLoading: rbtcRewardsLoading, + error: rbtcRewardsError, + } = useGetRewardsCoinbase() + + const { + data: { cycleNext }, + isLoading: cycleLoading, + error: cycleError, + } = useCycleContext() + + const gauges = builders.map(({ gauge }) => gauge) + const { + data: totalAllocation, + isLoading: totalAllocationLoading, + error: totalAllocationError, + } = useGaugesGetFunction(gauges, 'totalAllocation') + + const buildersAddress = builders.map(({ address }) => address) + const { + data: backersRewardsPct, + isLoading: backersRewardsPctLoading, + error: backersRewardsPctError, + } = useGetBackersRewardPercentage(buildersAddress, cycleNext.toSeconds()) + + const { prices } = usePricesContext() + + const abi = useMemo(() => { + const sumTotalAllocation = Object.values(totalAllocation).reduce((acc, value) => acc + (value ?? 0n), 0n) + + if (!sumTotalAllocation) { + return 0 + } + + const rifPrice = prices.RIF?.price ?? 0 + const rbtcPrice = prices.RBTC?.price ?? 0 + const rifAmount = Number(formatEther(rifRewards ?? 0n)) + const rbtcAmount = Number(formatEther(rbtcRewards ?? 0n)) + const cyclePayout = rifAmount * rifPrice + rbtcAmount * rbtcPrice + + if (!rifPrice) { + return 0 + } + + const topFiveBuilders = builders + .reduce>((acc, builder) => { + const allocation = totalAllocation[builder.gauge] + const rewardPct = backersRewardsPct[builder.address] + if (allocation && rewardPct) { + acc.push({ allocation, current: rewardPct.current }) + } + return acc + }, []) + .sort((a, b) => (a.allocation > b.allocation ? -1 : 1)) + .slice(0, 5) + + const weightedAverageBuilderRewardsPct = + topFiveBuilders.reduce( + (acc, { allocation, current }) => + acc + Number(((allocation * 100n) / sumTotalAllocation) * BigInt(current)), + 0, + ) / 100 + + const totalAllocationInEther = Number(formatEther(sumTotalAllocation)) + const rewardsPerStRIFPerCycle = + (cyclePayout * (weightedAverageBuilderRewardsPct / totalAllocationInEther / rifPrice)) / 100 + + return (Math.pow(1 + rewardsPerStRIFPerCycle, 26) - 1) * 100 + }, [backersRewardsPct, builders, prices, rbtcRewards, rifRewards, totalAllocation]) + + const isLoading = + buildersLoading || + rifRewardsLoading || + rbtcRewardsLoading || + cycleLoading || + totalAllocationLoading || + backersRewardsPctLoading + + const error = + buildersError ?? + rifRewardsError ?? + rbtcRewardsError ?? + totalAllocationError ?? + cycleError ?? + backersRewardsPctError + + return { + data: abi, + isLoading, + error, + } +} diff --git a/src/app/collective-rewards/metrics/components/ABIMetrics/index.ts b/src/app/collective-rewards/metrics/components/ABIMetrics/index.ts new file mode 100644 index 00000000..17d2c5bf --- /dev/null +++ b/src/app/collective-rewards/metrics/components/ABIMetrics/index.ts @@ -0,0 +1 @@ +export * from './ABIMetrics' diff --git a/src/app/collective-rewards/metrics/hooks/useGetTotalAllocation.ts b/src/app/collective-rewards/metrics/hooks/useGetTotalAllocation.ts new file mode 100644 index 00000000..b6dcc9ab --- /dev/null +++ b/src/app/collective-rewards/metrics/hooks/useGetTotalAllocation.ts @@ -0,0 +1,12 @@ +import { useGaugesGetFunction } from '../../shared' + +export const useGetTotalAllocation = (gauges: any[]) => { + const { data, isLoading, error } = useGaugesGetFunction(gauges, 'totalAllocation') + + const totalAllocations = Object.values(data).reduce((acc, allocation) => acc + allocation, 0n) + return { + data: totalAllocations, + isLoading, + error, + } +} diff --git a/src/app/collective-rewards/rewards/MyRewards.tsx b/src/app/collective-rewards/rewards/MyRewards.tsx index 217b951b..2c332eb9 100644 --- a/src/app/collective-rewards/rewards/MyRewards.tsx +++ b/src/app/collective-rewards/rewards/MyRewards.tsx @@ -13,7 +13,7 @@ import { FC } from 'react' import { Address, getAddress, zeroAddress } from 'viem' import { useRouter } from 'next/navigation' import { Link } from '@/components/Link' -import { Builder } from '../types' +import { RequiredBuilder } from '@/app/collective-rewards/types' import { useCanManageAllocations } from '@/app/collective-rewards/allocations/hooks' const SubText = () => { @@ -37,11 +37,9 @@ const SubText = () => { export const Rewards: FC<{ builder: Address }> = ({ builder }) => { const router = useRouter() - const { data: activatedBuilders, error: activatedBuildersError } = useGetBuildersByState>( - { - activated: true, - }, - ) + const { data: activatedBuilders, error: activatedBuildersError } = useGetBuildersByState({ + activated: true, + }) const activatedGauges = activatedBuilders?.map(({ gauge }) => gauge) ?? [] const { data: gauge, error: gaugeError } = useGetBuilderToGauge(builder) const canManageAllocations = useCanManageAllocations() diff --git a/src/app/collective-rewards/rewards/backers/ClaimableRewards.tsx b/src/app/collective-rewards/rewards/backers/ClaimableRewards.tsx index 5dc5523d..e786aaaa 100644 --- a/src/app/collective-rewards/rewards/backers/ClaimableRewards.tsx +++ b/src/app/collective-rewards/rewards/backers/ClaimableRewards.tsx @@ -23,7 +23,6 @@ type TokenRewardsMetricsProps = { } const TokenRewardsMetrics: FC = ({ - gauges, token: { address, symbol }, currency = 'USD', }) => { diff --git a/src/app/collective-rewards/rewards/backers/hooks/useGetBackerRewards.ts b/src/app/collective-rewards/rewards/backers/hooks/useGetBackerRewards.ts index 4066d5fa..e8f96904 100644 --- a/src/app/collective-rewards/rewards/backers/hooks/useGetBackerRewards.ts +++ b/src/app/collective-rewards/rewards/backers/hooks/useGetBackerRewards.ts @@ -13,7 +13,7 @@ import { Address } from 'viem' import { usePricesContext } from '@/shared/context/PricesContext' import { formatBalanceToHuman } from '@/app/user/Balances/balanceUtils' import { useGetBuildersByState } from '@/app/collective-rewards//user' -import { Builder, BuilderStateFlags } from '@/app/collective-rewards/types' +import { BuilderStateFlags, RequiredBuilder } from '@/app/collective-rewards/types' import { useMemo } from 'react' export type BackerRewards = { @@ -53,7 +53,7 @@ export const useGetBackerRewards = ( data: builders, isLoading: buildersLoading, error: buildersError, - } = useGetBuildersByState>() + } = useGetBuildersByState() const buildersAddress = builders.map(({ address }) => address) const { data: backersRewardsPct, diff --git a/src/app/collective-rewards/rewards/builders/EstimatedRewards.tsx b/src/app/collective-rewards/rewards/builders/EstimatedRewards.tsx index f860dbfa..d0398d5a 100644 --- a/src/app/collective-rewards/rewards/builders/EstimatedRewards.tsx +++ b/src/app/collective-rewards/rewards/builders/EstimatedRewards.tsx @@ -10,13 +10,14 @@ import { BuilderRewardDetails, useGetBackerRewardPercentage, } from '@/app/collective-rewards/rewards' -import { useHandleErrors } from '@/app/collective-rewards/utils' +import { isBuilderActive, isBuilderRewarded, useHandleErrors } from '@/app/collective-rewards/utils' import { formatBalanceToHuman } from '@/app/user/Balances/balanceUtils' import { usePricesContext } from '@/shared/context/PricesContext' import { FC, useEffect, useState } from 'react' import { Address } from 'viem' import { withSpinner } from '@/components/LoadingSpinner/withLoadingSpinner' import { useCycleContext } from '@/app/collective-rewards/metrics/context/CycleContext' +import { useBuilderContext } from '@/app/collective-rewards/user' type TokenRewardsProps = { builder: Address @@ -66,6 +67,10 @@ const TokenRewards: FC = ({ builder, gauge, token: { id, symb const rewardPercentageToApply = backerRewardsPct?.current ?? 0 + const { getBuilderByAddress } = useBuilderContext() + const claimingBuilder = getBuilderByAddress(builder) + const isRewarded = isBuilderRewarded(claimingBuilder?.stateFlags) + const error = rewardsError ?? totalPotentialRewardsError ?? rewardSharesError ?? backerRewardsPctError ?? cycleError useHandleErrors({ error, title: 'Error loading estimated rewards' }) @@ -73,7 +78,9 @@ const TokenRewards: FC = ({ builder, gauge, token: { id, symb const { prices } = usePricesContext() const rewardsAmount = - rewardShares && totalPotentialRewards ? (rewards * rewardShares) / totalPotentialRewards : 0n + isRewarded && rewardShares && totalPotentialRewards + ? (rewards * rewardShares) / totalPotentialRewards + : 0n // The complement of the reward percentage is applied to the estimated rewards since are from the builder's perspective const estimatedRewardsInHuman = Number(formatBalanceToHuman(rewardsAmount)) * (1 - rewardPercentageToApply / 100) diff --git a/src/app/collective-rewards/rewards/builders/hooks/useGetBuildersRewards.ts b/src/app/collective-rewards/rewards/builders/hooks/useGetBuildersRewards.ts index a199f38e..1b510c0d 100644 --- a/src/app/collective-rewards/rewards/builders/hooks/useGetBuildersRewards.ts +++ b/src/app/collective-rewards/rewards/builders/hooks/useGetBuildersRewards.ts @@ -15,13 +15,12 @@ import { import { formatBalanceToHuman } from '@/app/user/Balances/balanceUtils' import { usePricesContext } from '@/shared/context/PricesContext' import { useGaugesGetFunction } from '@/app/collective-rewards/shared' -import { Builder, BuilderStateFlags } from '@/app/collective-rewards/types' +import { BuilderStateFlags, RequiredBuilder } from '@/app/collective-rewards/types' import { useGetBuildersByState } from '@/app/collective-rewards/user' import { Address } from 'viem' import { Allocations, AllocationsContext } from '@/app/collective-rewards/allocations/context' import { useContext, useMemo } from 'react' - -type RequiredBuilder = Required +import { isBuilderRewarded } from '@/app/collective-rewards//utils' const isBuilderShown = ( { stateFlags: { kycApproved, revoked, communityApproved, paused }, address }: RequiredBuilder, @@ -67,11 +66,6 @@ export const useGetBuildersRewards = ({ rif, rbtc }: { [token: string]: Token }, error: totalAllocationError, } = useGaugesGetFunction(gauges, 'totalAllocation') - const sumTotalAllocation = Object.values(totalAllocation ?? {}).reduce( - (acc, value) => acc + (value ?? 0n), - 0n, - ) - const { data: rewardShares, isLoading: rewardSharesLoading, @@ -154,22 +148,26 @@ export const useGetBuildersRewards = ({ rif, rbtc }: { [token: string]: Token }, const rewardPercentage = backersRewardsPct[address] ?? null const rewardPercentageToApply = rewardPercentage?.current ?? 0 + const isRewarded = isBuilderRewarded(stateFlags) + // calculate rif estimated rewards const rewardRif = rewardsERC20 ?? 0n - const rewardsAmountRif = totalPotentialRewards - ? (rewardRif * builderRewardShares) / totalPotentialRewards - : 0n + const rewardsAmountRif = + isRewarded && totalPotentialRewards ? (rewardRif * builderRewardShares) / totalPotentialRewards : 0n const estimatedRifInHuman = Number(formatBalanceToHuman(rewardsAmountRif)) * (rewardPercentageToApply / 100) // calculate rbtc estimated rewards const rewardRbtc = rewardsCoinbase ?? 0n - const rewardsAmountRbtc = totalPotentialRewards - ? (rewardRbtc * builderRewardShares) / totalPotentialRewards - : 0n + const rewardsAmountRbtc = + isRewarded && totalPotentialRewards ? (rewardRbtc * builderRewardShares) / totalPotentialRewards : 0n const estimatedRbtcInHuman = Number(formatBalanceToHuman(rewardsAmountRbtc)) * (rewardPercentageToApply / 100) + const sumTotalAllocation = Object.values(totalAllocation).reduce( + (acc, value) => acc + (value ?? 0n), + 0n, + ) const totalAllocationPercentage = sumTotalAllocation ? (totalAllocation[gauge] * 100n) / sumTotalAllocation : 0n @@ -230,7 +228,6 @@ export const useGetBuildersRewards = ({ rif, rbtc }: { [token: string]: Token }, rewardShares, totalPotentialRewards, backersRewardsPct, - sumTotalAllocation, rifBuildersRewardsAmount, rbtcBuildersRewardsAmount, rifPrice, diff --git a/src/app/collective-rewards/rewards/components/Tooltip.tsx b/src/app/collective-rewards/rewards/components/Tooltip.tsx index a41c236d..9d925d21 100644 --- a/src/app/collective-rewards/rewards/components/Tooltip.tsx +++ b/src/app/collective-rewards/rewards/components/Tooltip.tsx @@ -1,4 +1,4 @@ -import { FC } from 'react' +import { FC, ReactNode } from 'react' import { Popover, PopoverProps } from '../../../../components/Popover' import { Button, ButtonProps } from '../../../../components/Button' @@ -22,7 +22,7 @@ const TooltipSvg = () => ( ) export type TooltipProps = { - text: string + text: ReactNode popoverProps?: Pick } diff --git a/src/app/collective-rewards/types.ts b/src/app/collective-rewards/types.ts index a436433e..4da3290d 100644 --- a/src/app/collective-rewards/types.ts +++ b/src/app/collective-rewards/types.ts @@ -42,3 +42,5 @@ export type ProposalByBuilder = Record export type ProposalsToState = Record export type BuilderState = 'active' | 'inProgress' + +export type RequiredBuilder = Required diff --git a/src/app/collective-rewards/utils/isBuilderOperational.ts b/src/app/collective-rewards/utils/isBuilderOperational.ts index 8dc6e252..b5d74c6f 100644 --- a/src/app/collective-rewards/utils/isBuilderOperational.ts +++ b/src/app/collective-rewards/utils/isBuilderOperational.ts @@ -17,6 +17,16 @@ export const isBuilderActive = (stateFlags?: BuilderStateFlags) => { ) } +export const isBuilderRewarded = (stateFlags?: BuilderStateFlags) => { + return !!( + stateFlags && + stateFlags.activated && + stateFlags.communityApproved && + stateFlags.kycApproved && + !stateFlags.revoked + ) +} + const inactiveStates = ['Deactivated', 'KYCRevoked', 'Revoked', 'Paused'] as const type InactiveState = (typeof inactiveStates)[number] export const getBuilderInactiveState = (state: BuilderStateFlags): InactiveState => { diff --git a/src/components/Popover/Popover.tsx b/src/components/Popover/Popover.tsx index c866755f..3546e6b4 100644 --- a/src/components/Popover/Popover.tsx +++ b/src/components/Popover/Popover.tsx @@ -8,7 +8,7 @@ export interface PopoverProps extends Omit, 'chil disabled?: boolean trigger?: 'click' | 'hover' background?: 'dark' | 'light' - position?: 'top' | 'bottom' | 'right' | 'left' + position?: 'top' | 'bottom' | 'right' | 'left' | 'left-bottom' size?: 'small' | 'medium' hasCaret?: boolean } @@ -73,6 +73,7 @@ export const Popover = ({ position === 'bottom' && 'top-full', position === 'right' && 'left-full bottom-full', position === 'left' && 'right-full bottom-full', + position === 'left-bottom' && 'right-full top-full', size === 'small' && 'w-36', size === 'medium' && 'w-96', )} @@ -92,7 +93,7 @@ export const Popover = ({ ) } -const PopoverCaret = ({ position }: { position: 'top' | 'bottom' | 'right' | 'left' }) => ( +const PopoverCaret = ({ position }: { position: 'top' | 'bottom' | 'right' | 'left' | 'left-bottom' }) => ( <> {position === 'top' && (