From 44ea6296ce324eb8ec80c1c3882f23e4b18b3e90 Mon Sep 17 00:00:00 2001 From: DinizSa <39380608+DinizSa@users.noreply.github.com> Date: Wed, 20 Nov 2024 11:28:27 +0200 Subject: [PATCH] TOK-412: List all activated builders (#369) * feat: list all the activated builders * chore: apply small fixes and leave placeholder for future changes * chore: remove duplicate code * style: apply lint rules * feat: allow table sort * chore: small ui improvements * fix: change arrow orintation * fix: remove mocked data * fix: lint * chore: feedback improvements * fix: last cycle rewards --------- Co-authored-by: Antonio Co-authored-by: Francisco Tobar --- .../leaderboard/BuildersLeaderBoardTable.tsx | 419 ++++++++++++++++++ .../leaderboard/LeaderBoard.tsx | 246 ---------- .../leaderboard/hooks/index.ts | 1 - .../hooks/useGetBuildersRewards.ts | 71 --- .../collective-rewards/leaderboard/index.ts | 3 +- src/app/collective-rewards/page.tsx | 4 +- .../rewards/builders/EstimatedRewards.tsx | 2 + .../rewards/builders/hooks/index.ts | 1 + .../builders/hooks/useGetBuildersRewards.ts | 175 ++++++++ .../collective-rewards/rewards/hooks/index.ts | 4 + .../hooks/useGetBuilderRewardPercentage.ts | 36 ++ .../hooks/useGetBuildersRewardPercentage.ts | 46 ++ .../rewards/hooks/useGetGaugesNotifyReward.ts | 21 +- .../rewards/hooks/useGetGaugesRewardShares.ts | 42 ++ .../rewards/hooks/useGetTotalAllocations.ts | 43 ++ .../rewards/utils/getNotifyRewardAmount.ts | 44 ++ .../rewards/utils/getPercentageData.ts | 22 + .../collective-rewards/rewards/utils/index.ts | 2 + .../rewards/utils/toPercentage.ts | 3 + src/app/collective-rewards/types.ts | 1 + .../Button/BecomeABuilderButton.test.tsx | 1 + .../user/context/BuilderContext.tsx | 4 +- .../user/hooks/useGetBuilders.ts | 12 +- .../utils/getMostAdvancedProposal.test.tsx | 4 + .../whitelist/hooks/useGetFilteredBuilders.ts | 1 + src/components/Collapsible/Collapsible.tsx | 2 +- src/components/ProgressBar/ProgressBar.tsx | 17 + src/lib/constants.ts | 1 + src/shared/hooks/usePagination.ts | 2 +- src/shared/hooks/usePaginationUi.tsx | 51 ++- 30 files changed, 951 insertions(+), 330 deletions(-) create mode 100644 src/app/collective-rewards/leaderboard/BuildersLeaderBoardTable.tsx delete mode 100644 src/app/collective-rewards/leaderboard/LeaderBoard.tsx delete mode 100644 src/app/collective-rewards/leaderboard/hooks/index.ts delete mode 100644 src/app/collective-rewards/leaderboard/hooks/useGetBuildersRewards.ts create mode 100644 src/app/collective-rewards/rewards/builders/hooks/useGetBuildersRewards.ts create mode 100644 src/app/collective-rewards/rewards/hooks/useGetBuilderRewardPercentage.ts create mode 100644 src/app/collective-rewards/rewards/hooks/useGetBuildersRewardPercentage.ts create mode 100644 src/app/collective-rewards/rewards/hooks/useGetGaugesRewardShares.ts create mode 100644 src/app/collective-rewards/rewards/hooks/useGetTotalAllocations.ts create mode 100644 src/app/collective-rewards/rewards/utils/getNotifyRewardAmount.ts create mode 100644 src/app/collective-rewards/rewards/utils/getPercentageData.ts create mode 100644 src/app/collective-rewards/rewards/utils/toPercentage.ts create mode 100644 src/components/ProgressBar/ProgressBar.tsx diff --git a/src/app/collective-rewards/leaderboard/BuildersLeaderBoardTable.tsx b/src/app/collective-rewards/leaderboard/BuildersLeaderBoardTable.tsx new file mode 100644 index 00000000..18679746 --- /dev/null +++ b/src/app/collective-rewards/leaderboard/BuildersLeaderBoardTable.tsx @@ -0,0 +1,419 @@ +import { CycleContextProvider } from '@/app/collective-rewards/metrics' +import { BuilderRewardPercentage, useGetBuildersRewards } from '@/app/collective-rewards/rewards' +import { BuilderContextProviderWithPrices } from '@/app/collective-rewards/user' +import { useHandleErrors } from '@/app/collective-rewards/utils' +import { AddressOrAliasWithCopy } from '@/components/Address' +import { Button } from '@/components/Button' +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/Collapsible' +import { Jdenticon } from '@/components/Header/Jdenticon' +import { LoadingSpinner } from '@/components/LoadingSpinner' +import { Popover } from '@/components/Popover' +import { ProgressBar } from '@/components/ProgressBar/ProgressBar' +import { TableBody, TableCell, TableCore, TableHead, TableRow } from '@/components/Table' +import { HeaderTitle, Label, Typography } from '@/components/Typography' +import { RBTC, RIF } from '@/lib/constants' +import { cn, formatCurrency, shortAddress, toFixed } from '@/lib/utils' +import { useBasicPaginationUi } from '@/shared/hooks/usePaginationUi' +import { rbtcIconSrc } from '@/shared/rbtcIconSrc' +import Image from 'next/image' +import { FC, memo, useMemo, useState } from 'react' +import { FaRegQuestionCircle } from 'react-icons/fa' +import { FaArrowDown, FaArrowUp } from 'react-icons/fa6' +import { LiaSortUpSolid } from 'react-icons/lia' +import { RiArrowUpSFill } from 'react-icons/ri' +import { Address, isAddress } from 'viem' + +type Currency = { + value: number + symbol: string +} + +export type Reward = { + crypto: Currency + fiat: Currency +} + +type RewardCellProps = { + rewards: Reward[] +} + +export function getFormattedCurrency(value: number, symbol: string) { + const formattedCurrency = formatCurrency(value, symbol) + return `${formattedCurrency.substring(0, 1)}${symbol} ${formattedCurrency.substring(1)}` +} + +export const rbtcIcon = ( + rBTC Logo +) +export const rifIcon = RIF Logo + +export const RewardCell: FC = ({ rewards }) => ( +
+ {rewards && + rewards.map(({ crypto: { value, symbol }, fiat: { value: fiatValue, symbol: fiatSymbol } }) => ( +
+ {/* TODO: if the value is very small, should we show it in Gwei/wei? */} + + + +
+ ))} +
+) + +export const LazyRewardCell = memo(RewardCell, ({ rewards: prevReward }, { rewards: nextReward }) => + prevReward.every((reward, key) => reward.fiat.value === nextReward[key].fiat.value), +) + +export const BuilderNameCell = ({ builderName, address }: { builderName: string; address: string }) => { + const shortenAddress = shortAddress(address as Address) + return ( + +
+ + +

{shortenAddress}

+
+ } + size="small" + trigger="hover" + disabled={!builderName || isAddress(builderName)} + > + + + + + +
+ ) +} + +export const BackerRewardsPercentage = ({ + rewardPercentage, +}: { + rewardPercentage: BuilderRewardPercentage | null +}) => { + const renderDelta = useMemo(() => { + if (!rewardPercentage) return null + + const deltaPercentage = rewardPercentage.next - rewardPercentage.current + if (deltaPercentage > 0) { + const colorGreen = '#1bc47d' + return ( +
+ +
+{deltaPercentage}
+
+ ) + } + if (deltaPercentage < 0) { + const colorRed = '#f14722' + return ( +
+ +
{deltaPercentage}
+
+ ) + } + return null + }, [rewardPercentage]) + return ( + +
+
{rewardPercentage?.current}
+ {renderDelta} +
+
+ ) +} + +export const LastCycleRewardCell = ({ rewards }: { rewards: Reward[] }) => { + return ( + + + + ) +} + +const EstimatedRewardCell = ({ rewards }: { rewards: Reward[] }) => { + return ( + + + + ) +} + +type TotalAllocationCellProps = { + // a percentage without decimals + totalAllocationPercentage: bigint +} + +const TotalAllocationCell = ({ totalAllocationPercentage }: TotalAllocationCellProps) => { + return ( + +
+ + +
+
+ ) +} +const ActionCell = () => { + /* TODO: manage the button status + - disabled when the backer cannot vote on the Builder + - variant=primary when the builder is selected and text changed to "Selected" + - variant=secondary by default and text is "Select" + */ + /* TODO: add the onClick event + * - it needs to interact with the allocation context to add the builder to the selected builders + */ + return ( + + + + ) +} + +enum RewardsColumnKeyEnum { + builder = 'builder', + lastCycleRewards = 'lastCycleRewards', + estimatedRewards = 'estimatedRewards', + rewardPercentage = 'rewardPercentage', + totalAllocationPercentage = 'totalAllocationPercentage', +} +const tableHeaders = [ + { label: 'Builder', className: 'w-[14%]', key: RewardsColumnKeyEnum.builder }, + { label: 'Backer Rewards %', className: 'w-[10%]', key: RewardsColumnKeyEnum.rewardPercentage }, + { label: 'Last Cycle Rewards', className: 'w-[22%]', key: RewardsColumnKeyEnum.lastCycleRewards }, + { label: 'Est. Backers Rewards', className: 'w-[22%]', key: RewardsColumnKeyEnum.estimatedRewards }, + { + label: 'Total Allocations', + className: 'w-[18%]', + // eslint-disable-next-line quotes + tooltip: "The Builder's share of the total allocations", + key: RewardsColumnKeyEnum.totalAllocationPercentage, + }, + // TODO: text-center isn't applied + { label: 'Actions', className: 'w-[14%]' }, +] + +type ISortConfig = { + key: RewardsColumnKeyEnum + direction: 'asc' | 'desc' +} + +const BuildersLeaderBoardTable = () => { + const { data: rewardsData, isLoading, error: rewardsError } = useGetBuildersRewards() + const [sortConfig, setSortConfig] = useState({ + key: RewardsColumnKeyEnum.totalAllocationPercentage, + direction: 'asc', + }) + + // pagination + const buildersPerPage = 10 + const tableDataLength = useMemo(() => Object.keys(rewardsData).length, [rewardsData]) + const maxPages = useMemo(() => Math.ceil(tableDataLength / buildersPerPage), [tableDataLength]) + const { paginationUi, currentPage } = useBasicPaginationUi(maxPages) + + useHandleErrors({ error: rewardsError, title: 'Error loading builder rewards' }) + + type IRewardData = (typeof rewardsData)[number] + const sortedRewardsData = useMemo( + () => + Object.values(rewardsData).toSorted((a: IRewardData, b: IRewardData) => { + const { key, direction } = sortConfig + if (!key) return 0 + + let aValue: number | string + let bValue: number | string + switch (key) { + case RewardsColumnKeyEnum.builder: + aValue = a.builderName || a.address + bValue = b.builderName || b.address + break + case RewardsColumnKeyEnum.totalAllocationPercentage: + aValue = Number(a.totalAllocationPercentage) + bValue = Number(b.totalAllocationPercentage) + break + case RewardsColumnKeyEnum.rewardPercentage: + if (!a.rewardPercentage || !b.rewardPercentage) return 0 + aValue = a.rewardPercentage.current + bValue = b.rewardPercentage.current + break + case RewardsColumnKeyEnum.lastCycleRewards: + aValue = a.lastCycleReward.RIF.crypto.value + a.lastCycleReward.RBTC.crypto.value + bValue = b.lastCycleReward.RIF.crypto.value + b.lastCycleReward.RBTC.crypto.value + break + case RewardsColumnKeyEnum.estimatedRewards: + aValue = a.estimatedReward.RIF.crypto.value + a.estimatedReward.RBTC.crypto.value + bValue = b.estimatedReward.RIF.crypto.value + b.estimatedReward.RBTC.crypto.value + break + default: + return 0 + } + + if (typeof aValue === 'string' && typeof bValue === 'string') { + return direction === 'asc' + ? a.builderName.localeCompare(b.builderName) + : b.builderName.localeCompare(a.builderName) + } + + if (typeof aValue === 'number' && typeof bValue === 'number') { + return direction === 'asc' ? aValue - bValue : bValue - aValue + } + + return 0 + }), + [rewardsData, sortConfig], + ) + + const paginatedRewardsData = useMemo( + () => sortedRewardsData.slice(currentPage * buildersPerPage, (currentPage + 1) * buildersPerPage), + [currentPage, sortedRewardsData], + ) + + if (isLoading) { + return + } + + if (!tableDataLength) { + return + } + + const handleSort = (key: RewardsColumnKeyEnum) => { + setSortConfig(prevSortConfig => { + if (prevSortConfig?.key === key) { + // Toggle direction if the same column is clicked + return { + key, + direction: prevSortConfig.direction === 'asc' ? 'desc' : 'asc', + } + } + // Set initial sort direction to ascending + return { key, direction: 'asc' } + }) + } + + return ( +
+ + + + {tableHeaders.map(header => ( + +
+ {header.tooltip && ( + + + + )} + {header.label} + {header.key && ( + + )} +
+
+ ))} +
+
+ + {Object.values(paginatedRewardsData).map( + ({ + address, + builderName, + lastCycleReward, + estimatedReward, + totalAllocationPercentage, + rewardPercentage, + }) => ( + + + + + + + + + ), + )} + +
+
{paginationUi}
+
+ ) +} + +export const BuildersLeaderBoard = () => { + const onManageAllocations = () => { + // TODO: fill the allocation context if necessary and change the route + console.log('Manage allocations') + } + return ( + <> + + +
+ Rewards leaderboard + +
+
+ + + +
+ +
+
+
+
+
+ + ) +} + +const EmptyLeaderboard = () => ( +
+ no builders yet + + Builders are joining soon... + +
+) diff --git a/src/app/collective-rewards/leaderboard/LeaderBoard.tsx b/src/app/collective-rewards/leaderboard/LeaderBoard.tsx deleted file mode 100644 index abc1d6c0..00000000 --- a/src/app/collective-rewards/leaderboard/LeaderBoard.tsx +++ /dev/null @@ -1,246 +0,0 @@ -import { useGetBuildersRewards } from './hooks' -import { useAlertContext } from '@/app/providers' -import { AddressOrAliasWithCopy } from '@/components/Address' -import { LoadingSpinner } from '@/components/LoadingSpinner' -import { TableBody, TableCell, TableCore, TableHead, TableRow } from '@/components/Table' -import { HeaderTitle, Label, Typography } from '@/components/Typography' -import { tokenContracts } from '@/lib/contracts' -import { cn, formatCurrency, shortAddress, toFixed } from '@/lib/utils' -import { FC, memo, useEffect } from 'react' -import { Jdenticon } from '@/components/Header/Jdenticon' -import { BuilderContextProviderWithPrices } from '@/app/collective-rewards/user' -import { Address, isAddress } from 'viem' -import { Popover } from '@/components/Popover' - -type Currency = { - value: number - symbol: string -} - -export type Reward = { - crypto: Currency - fiat: Currency -} - -type TableData = { - [address: string]: { - builderName: string - lastCycleReward: Reward[] - projectedReward: Reward[] - share: bigint - } -} - -type RewardCellProps = { - rewards: Reward[] -} - -export const RewardCell: FC = ({ rewards }) => ( -
- {rewards && - rewards.map(({ crypto: { value, symbol }, fiat: { value: fiatValue, symbol: fiatSymbol } }) => ( -
- {/* TODO: if the value is very small, should we show it in Gwei/wei? */} - -
- -
- ))} -
-) - -export const LazyRewardCell = memo(RewardCell, ({ rewards: prevReward }, { rewards: nextReward }) => - prevReward.every((reward, key) => reward.fiat.value === nextReward[key].fiat.value), -) - -export const BuilderNameCell = ({ builderName, address }: { builderName: string; address: string }) => { - const shortenAddress = shortAddress(address as Address) - return ( - -
- - -

{shortenAddress}

-
- } - size="small" - trigger="hover" - disabled={!builderName || isAddress(builderName)} - > - - - - - -
- ) -} - -export const LastCycleRewardCell = ({ rewards }: { rewards: Reward[] }) => { - return ( - - - - ) -} - -const ProjectedRewardCell = ({ rewards }: { rewards: Reward[] }) => { - return ( - - - - ) -} - -const ShareCell = ({ share }: { share: bigint }) => { - return ( - - - - ) -} - -const getShareTextColour = (share: bigint) => { - if (share >= 50n) return 'text-[#1BC47D]' - if (share >= 20n) return 'text-[#E56B1A]' - return 'text-[#F24822]' -} - -const tableHeaders = [ - { label: 'Builder', width: 'w-[38%]' }, - { label: 'Last cycle Rewards', width: 'w-[24%]' }, - { label: 'Projected Rewards', width: 'w-[24%]' }, - { label: 'Share %', width: 'w-[14%]', text_position: 'text-center' }, -] - -const tableBodyCellClasses = 'font-normal text-base leading-none text-text-primary font-rootstock-sans' - -const LeaderBoardTable = () => { - const { - data: rbtcData, - isLoading: rbtcLoading, - error: rbtcRewardsError, - } = useGetBuildersRewards(tokenContracts.RBTC, 'RBTC') - const { - data: rifData, - isLoading: rifLoading, - error: rifRewardsError, - } = useGetBuildersRewards(tokenContracts.RIF, 'RIF') - const { setMessage: setErrorMessage } = useAlertContext() - - const data = [...rbtcData, ...rifData] - const tableData = data.reduce( - (acc, { address, builderName, lastCycleReward, projectedReward, share }) => { - const currentShare = acc[address]?.share ?? 0n - acc[address] = { - builderName, - lastCycleReward: [...(acc[address]?.lastCycleReward ?? []), lastCycleReward], - projectedReward: [...(acc[address]?.projectedReward ?? []), projectedReward], - share: share > currentShare ? share : currentShare, - } - - return acc - }, - {}, - ) - const tableDataLength = Object.keys(tableData).length - - const isLoading = rbtcLoading || rifLoading - - useEffect(() => { - if (rbtcRewardsError) { - setErrorMessage({ - severity: 'error', - title: 'Error loading RBTC rewards', - content: rbtcRewardsError.message, - }) - console.error('🐛 rbtcRewardsError:', rbtcRewardsError) - } - }, [rbtcRewardsError, setErrorMessage]) - - useEffect(() => { - if (rifRewardsError) { - setErrorMessage({ - severity: 'error', - title: 'Error loading RIF rewards', - content: rifRewardsError.message, - }) - console.error('🐛 rifRewardsError:', rifRewardsError) - } - }, [rifRewardsError, setErrorMessage]) - - if (isLoading) { - return - } - - if (!tableDataLength) { - return - } - - return ( - - - - {tableHeaders.map(header => ( - - {header.label} - - ))} - - - - {Object.entries(tableData).map( - ([address, { builderName, lastCycleReward, projectedReward, share }]) => ( - - - - - - - ), - )} - - - ) -} - -export const LeaderBoard = () => { - return ( - <> - Rewards leaderboard - - - - - ) -} - -const EmptyLeaderboard = () => ( -
- no builders yet - - Builders are joining soon... - -
-) diff --git a/src/app/collective-rewards/leaderboard/hooks/index.ts b/src/app/collective-rewards/leaderboard/hooks/index.ts deleted file mode 100644 index f2eac8c8..00000000 --- a/src/app/collective-rewards/leaderboard/hooks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useGetBuildersRewards' diff --git a/src/app/collective-rewards/leaderboard/hooks/useGetBuildersRewards.ts b/src/app/collective-rewards/leaderboard/hooks/useGetBuildersRewards.ts deleted file mode 100644 index 0d45dead..00000000 --- a/src/app/collective-rewards/leaderboard/hooks/useGetBuildersRewards.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { formatBalanceToHuman } from '@/app/user/Balances/balanceUtils' -import { usePricesContext } from '@/shared/context/PricesContext' -import { useGetTokenProjectedReward, useGetRewardDistributedLogs } from '@/app/collective-rewards/rewards' -import { Address, isAddressEqual } from 'viem' -import { getLastCycleRewards } from '@/app/collective-rewards/utils/getLastCycleRewards' -import { useBuilderContext } from '@/app/collective-rewards/user' -import { BuilderStatusActive } from '@/app/collective-rewards/types' - -export const useGetBuildersRewards = (rewardToken: Address, rewardTokenSymbol?: string, currency = 'USD') => { - const { data: builders, isLoading: buildersLoading, error: buildersError } = useBuilderContext() - - const { - data: rewardDistributedLogs, - isLoading: logsLoading, - error: logsError, - } = useGetRewardDistributedLogs(rewardToken) - const { - data: { share, projectedReward }, - isLoading: tokenLoading, - error: tokenError, - } = useGetTokenProjectedReward(rewardToken) - const tokenSymbol = rewardTokenSymbol ?? '' - - const projectedRewardInHuman = Number(formatBalanceToHuman(projectedReward)) - - const isLoading = buildersLoading || logsLoading || tokenLoading - const error = buildersError ?? logsError ?? tokenError - const whitelistedBuilders = builders.filter(builder => builder.status === BuilderStatusActive) - - const { prices } = usePricesContext() - - return { - data: whitelistedBuilders.map(({ address, builderName }) => { - const builderEvents = rewardDistributedLogs.filter(event => - isAddressEqual(event.args.builder_, address), - ) - const lastCycleRewards = getLastCycleRewards(builderEvents) - const lastCycleRewardsInHuman = Number(formatBalanceToHuman(lastCycleRewards)) - - const price = prices[tokenSymbol]?.price ?? 0 - - return { - address, - builderName, - lastCycleReward: { - crypto: { - value: lastCycleRewardsInHuman, - symbol: tokenSymbol, - }, - fiat: { - value: price * lastCycleRewardsInHuman, - symbol: currency, - }, - }, - projectedReward: { - crypto: { - value: projectedRewardInHuman, - symbol: tokenSymbol, - }, - fiat: { - value: price * projectedRewardInHuman, - symbol: currency, - }, - }, - share, - } - }), - isLoading, - error, - } -} diff --git a/src/app/collective-rewards/leaderboard/index.ts b/src/app/collective-rewards/leaderboard/index.ts index 39ca9caf..c3119541 100644 --- a/src/app/collective-rewards/leaderboard/index.ts +++ b/src/app/collective-rewards/leaderboard/index.ts @@ -1,2 +1 @@ -export * from './hooks' -export * from './LeaderBoard' +export * from './BuildersLeaderBoardTable' diff --git a/src/app/collective-rewards/page.tsx b/src/app/collective-rewards/page.tsx index 5dc477bd..eccdbd8f 100644 --- a/src/app/collective-rewards/page.tsx +++ b/src/app/collective-rewards/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { LeaderBoard } from '@/app/collective-rewards/leaderboard' +import { BuildersLeaderBoard } from '@/app/collective-rewards/leaderboard' import { Metrics } from '@/app/collective-rewards/metrics' import { WhitelistContextProviderWithBuilders, WhitelistSection } from '@/app/collective-rewards/whitelist' import { MainContainer } from '@/components/MainContainer/MainContainer' @@ -13,7 +13,7 @@ export default function BuildersIncentiveMarket() { - + ) diff --git a/src/app/collective-rewards/rewards/builders/EstimatedRewards.tsx b/src/app/collective-rewards/rewards/builders/EstimatedRewards.tsx index 1de5d886..89bf36c5 100644 --- a/src/app/collective-rewards/rewards/builders/EstimatedRewards.tsx +++ b/src/app/collective-rewards/rewards/builders/EstimatedRewards.tsx @@ -65,6 +65,8 @@ const TokenRewards: FC = ({ builder, gauge, token: { id, symb const rewardsAmount = rewardPercentage ? applyPrecision(rewards * rewardPercentage) : 0n + // rewardsAmountGauge = rewardsAmount * rewardShares / totalPotentialRewards + // rewardsAmountBuilder = rewardsAmount * (1 - rewardPercentage) / totalPotentialRewards const estimatedRewards = rewardShares && totalPotentialRewards ? (rewardShares * rewardsAmount) / totalPotentialRewards : 0n const estimatedRewardsInHuman = Number(formatBalanceToHuman(estimatedRewards)) diff --git a/src/app/collective-rewards/rewards/builders/hooks/index.ts b/src/app/collective-rewards/rewards/builders/hooks/index.ts index 4e0ddd0c..3f108204 100644 --- a/src/app/collective-rewards/rewards/builders/hooks/index.ts +++ b/src/app/collective-rewards/rewards/builders/hooks/index.ts @@ -2,3 +2,4 @@ export * from './useClaimBuilderRewards' export * from './useGetBuilderRewards' export * from './useGetBuilderRewardsClaimedLogs' export * from './useGetRewardPercentageToApply' +export * from './useGetBuildersRewards' diff --git a/src/app/collective-rewards/rewards/builders/hooks/useGetBuildersRewards.ts b/src/app/collective-rewards/rewards/builders/hooks/useGetBuildersRewards.ts new file mode 100644 index 00000000..1dc4698b --- /dev/null +++ b/src/app/collective-rewards/rewards/builders/hooks/useGetBuildersRewards.ts @@ -0,0 +1,175 @@ +import { useCycleContext } from '@/app/collective-rewards/metrics' +import { + getNotifyRewardAmount, + useGetBuildersRewardPercentage, + useGetGaugesNotifyReward, + useGetGaugesRewardShares, + useGetRewardsCoinbase, + useGetRewardsERC20, + useGetTotalAllocations, + useGetTotalPotentialReward, +} from '@/app/collective-rewards/rewards' +import { useGetFilteredBuilders } from '@/app/collective-rewards/whitelist' +import { formatBalanceToHuman } from '@/app/user/Balances/balanceUtils' +import { RBTC, RIF, USD } from '@/lib/constants' +import { usePricesContext } from '@/shared/context/PricesContext' + +export const useGetBuildersRewards = () => { + const { + data: activeBuilders, + isLoading: activeBuildersLoading, + error: activeBuildersError, + } = useGetFilteredBuilders({ builderName: '', status: 'Active' }) + + const { + data: totalPotentialRewards, + isLoading: totalPotentialRewardsLoading, + error: totalPotentialRewardsError, + } = useGetTotalPotentialReward() + + const buildersAddress = activeBuilders?.map(({ address }) => address) + const { + data: buildersRewardsPercentage, + isLoading: rewardsPercentageLoading, + error: rewardsPercentageError, + } = useGetBuildersRewardPercentage(buildersAddress) + + const gauges = activeBuilders?.map(({ gauge }) => gauge) + const { + data: { allocationsPercentage }, + isLoading: allocationsLoading, + error: allocationsError, + } = useGetTotalAllocations(gauges) + + const { + data: rewardShares, + isLoading: rewardSharesLoading, + error: rewardSharesError, + } = useGetGaugesRewardShares(gauges) + + const { + data: rewardsERC20, + isLoading: rewardsERC20Loading, + error: rewardsERC20Error, + } = useGetRewardsERC20() + + const { + data: rewardsCoinbase, + isLoading: rewardsCoinbaseLoading, + error: rewardsCoinbaseError, + } = useGetRewardsCoinbase() + + const { data: cycle, isLoading: cycleLoading, error: cycleError } = useCycleContext() + const { cycleDuration, cycleStart, endDistributionWindow } = cycle + const distributionWindow = endDistributionWindow.diff(cycleStart) + const lastCycleStart = cycleStart.minus({ millisecond: cycleDuration.as('millisecond') }) + const lastCycleAfterDistribution = lastCycleStart.plus({ millisecond: +distributionWindow }) + + const { + data: notifyRewardEventLastCycle, + isLoading: logsLoading, + error: logsError, + } = useGetGaugesNotifyReward( + gauges, + undefined, + lastCycleAfterDistribution.toSeconds(), + endDistributionWindow.toSeconds(), + ) + + const buildersRewardsAmount = getNotifyRewardAmount(gauges, notifyRewardEventLastCycle) + + const isLoading = + rewardSharesLoading || + activeBuildersLoading || + allocationsLoading || + logsLoading || + rewardsPercentageLoading || + rewardsERC20Loading || + rewardsCoinbaseLoading || + cycleLoading || + totalPotentialRewardsLoading + + const error = + rewardSharesError ?? + activeBuildersError ?? + allocationsError ?? + logsError ?? + rewardsPercentageError ?? + rewardsERC20Error ?? + rewardsCoinbaseError ?? + cycleError ?? + totalPotentialRewardsError + + const { prices } = usePricesContext() + + const priceRif = prices[RIF]?.price ?? 0 + const priceRbtc = prices[RBTC]?.price ?? 0 + + return { + data: activeBuilders.map(({ address, builderName }, i) => { + const builderRewardShares = rewardShares ? rewardShares[i] : 0n + const rewardPercentage = buildersRewardsPercentage?.[i] ?? null + const currentRewardPercentage = rewardPercentage?.current ?? 0 + + // calculate rif estimated rewards + const rewardRif = rewardsERC20 ?? 0n + const rewardsAmountRif = totalPotentialRewards + ? rewardRif * (builderRewardShares / totalPotentialRewards) + : 0n + const estimatedRifInHuman = + Number(formatBalanceToHuman(rewardsAmountRif)) * (currentRewardPercentage / 100) + + // calculate rbtc estimated rewards + const rewardRbtc = rewardsCoinbase ?? 0n + const rewardsAmountRbtc = totalPotentialRewards + ? rewardRbtc * (builderRewardShares / totalPotentialRewards) + : 0n + const estimatedRbtcInHuman = + Number(formatBalanceToHuman(rewardsAmountRbtc)) * (currentRewardPercentage / 100) + + const totalAllocationPercentage = allocationsPercentage?.[i] ?? 0n + const builderRewardsAmount = buildersRewardsAmount[gauges[i]] + + return { + address, + builderName, + totalAllocationPercentage, + rewardPercentage, + lastCycleReward: { + RIF: { + crypto: { value: builderRewardsAmount.RIF, symbol: RIF }, + fiat: { + value: priceRif * builderRewardsAmount.RIF, + symbol: USD, + }, + }, + RBTC: { + crypto: { value: builderRewardsAmount.RBTC, symbol: RBTC }, + fiat: { + value: priceRbtc * builderRewardsAmount.RBTC, + symbol: USD, + }, + }, + }, + estimatedReward: { + RIF: { + crypto: { value: estimatedRifInHuman, symbol: RIF }, + fiat: { + value: priceRif * estimatedRifInHuman, + symbol: USD, + }, + }, + RBTC: { + crypto: { value: estimatedRbtcInHuman, symbol: RBTC }, + fiat: { + value: priceRbtc * estimatedRbtcInHuman, + symbol: USD, + }, + }, + }, + } + }), + isLoading, + error, + } +} diff --git a/src/app/collective-rewards/rewards/hooks/index.ts b/src/app/collective-rewards/rewards/hooks/index.ts index 28203926..4e8b9bc4 100644 --- a/src/app/collective-rewards/rewards/hooks/index.ts +++ b/src/app/collective-rewards/rewards/hooks/index.ts @@ -8,3 +8,7 @@ export * from './useGetRewardsERC20' export * from './useGetPerTokenRewards' export * from './useGetGaugesEvents' export * from './useGetGaugesNotifyReward' +export * from './useGetBuilderRewardPercentage' +export * from './useGetBuildersRewardPercentage' +export * from './useGetGaugesRewardShares' +export * from './useGetTotalAllocations' diff --git a/src/app/collective-rewards/rewards/hooks/useGetBuilderRewardPercentage.ts b/src/app/collective-rewards/rewards/hooks/useGetBuilderRewardPercentage.ts new file mode 100644 index 00000000..19ee080a --- /dev/null +++ b/src/app/collective-rewards/rewards/hooks/useGetBuilderRewardPercentage.ts @@ -0,0 +1,36 @@ +import { BuilderRewardPercentage, getPercentageData } from '@/app/collective-rewards/rewards/utils' +import { BackersManagerAbi } from '@/lib/abis/v2/BackersManagerAbi' +import { AVERAGE_BLOCKTIME } from '@/lib/constants' +import { BackersManagerAddress } from '@/lib/contracts' +import { useEffect, useState } from 'react' +import { Address } from 'viem' +import { useReadContract } from 'wagmi' + +export const useGetBuilderRewardPercentage = (builder: Address) => { + const [rewardPercentageData, setRewardPercentageData] = useState() + const { data, isLoading, error } = useReadContract({ + address: BackersManagerAddress, + abi: BackersManagerAbi, + functionName: 'builderRewardPercentage', + args: [builder], + query: { + refetchInterval: AVERAGE_BLOCKTIME, + }, + }) + + useEffect(() => { + if (!data) return + + const [previous, next, cooldownEndTime] = data + + const percentageData = getPercentageData(previous, next, cooldownEndTime) + + setRewardPercentageData(percentageData) + }, [data]) + + return { + rewardPercentageData, + isLoading, + error, + } +} diff --git a/src/app/collective-rewards/rewards/hooks/useGetBuildersRewardPercentage.ts b/src/app/collective-rewards/rewards/hooks/useGetBuildersRewardPercentage.ts new file mode 100644 index 00000000..a102a2ae --- /dev/null +++ b/src/app/collective-rewards/rewards/hooks/useGetBuildersRewardPercentage.ts @@ -0,0 +1,46 @@ +import { getPercentageData } from '@/app/collective-rewards/rewards/utils' +import { BackersManagerAbi } from '@/lib/abis/v2/BackersManagerAbi' +import { AVERAGE_BLOCKTIME } from '@/lib/constants' +import { BackersManagerAddress } from '@/lib/contracts' +import { useMemo } from 'react' +import { Address } from 'viem' +import { useReadContracts } from 'wagmi' + +export const useGetBuildersRewardPercentage = (builders: Address[]) => { + const rewardPercentageCalls = useMemo( + () => + builders?.map( + builder => + ({ + address: BackersManagerAddress, + abi: BackersManagerAbi, + functionName: 'builderRewardPercentage', + args: [builder], + }) as const, + ), + [builders], + ) + const { + data: rewardSharesResult, + isLoading, + error, + } = useReadContracts<[bigint, bigint, bigint][]>({ + contracts: rewardPercentageCalls, + query: { + refetchInterval: AVERAGE_BLOCKTIME, + }, + }) + const rewardPercentage = useMemo( + () => + rewardSharesResult + ?.map(share => share.result as [bigint, bigint, bigint]) + .map(([previous, next, cooldownEndTime]) => getPercentageData(previous, next, cooldownEndTime)), + [rewardSharesResult], + ) + + return { + data: rewardPercentage, + isLoading, + error, + } +} diff --git a/src/app/collective-rewards/rewards/hooks/useGetGaugesNotifyReward.ts b/src/app/collective-rewards/rewards/hooks/useGetGaugesNotifyReward.ts index 2d7463c3..11d96534 100644 --- a/src/app/collective-rewards/rewards/hooks/useGetGaugesNotifyReward.ts +++ b/src/app/collective-rewards/rewards/hooks/useGetGaugesNotifyReward.ts @@ -2,7 +2,12 @@ import { Address, isAddressEqual } from 'viem' import { useMemo } from 'react' import { useGetGaugesEvents } from '@/app/collective-rewards/rewards' -export const useGetGaugesNotifyReward = (gauges: Address[], rewardToken?: Address) => { +export const useGetGaugesNotifyReward = ( + gauges: Address[], + rewardToken?: Address, + fromTimestamp?: number, + toTimestamp?: number, +) => { const { data: eventsData, isLoading, error } = useGetGaugesEvents(gauges, 'NotifyReward') const data = useMemo(() => { @@ -11,13 +16,25 @@ export const useGetGaugesNotifyReward = (gauges: Address[], rewardToken?: Addres if (rewardToken) { events = events.filter(event => isAddressEqual(event.args.rewardToken_, rewardToken)) } + if (fromTimestamp) { + events = events.filter(event => { + // @ts-ignore + return event.timeStamp >= fromTimestamp + }) + } + if (toTimestamp) { + events = events.filter(event => { + // @ts-ignore + return event.timeStamp <= toTimestamp + }) + } if (events.length > 0) { acc[key] = events } return acc }, {}) - }, [eventsData, rewardToken]) + }, [eventsData, rewardToken, fromTimestamp, toTimestamp]) return { data, diff --git a/src/app/collective-rewards/rewards/hooks/useGetGaugesRewardShares.ts b/src/app/collective-rewards/rewards/hooks/useGetGaugesRewardShares.ts new file mode 100644 index 00000000..921e4aa5 --- /dev/null +++ b/src/app/collective-rewards/rewards/hooks/useGetGaugesRewardShares.ts @@ -0,0 +1,42 @@ +import { GaugeAbi } from '@/lib/abis/v2/GaugeAbi' +import { AVERAGE_BLOCKTIME } from '@/lib/constants' +import { useEffect, useMemo } from 'react' +import { Address } from 'viem' +import { useReadContracts } from 'wagmi' + +export const useGetGaugesRewardShares = (gauges: Address[]) => { + const rewardSharesCalls = useMemo( + () => + gauges?.map( + gauge => + ({ + address: gauge, + abi: GaugeAbi, + functionName: 'rewardShares', + args: [], + }) as const, + ), + [gauges], + ) + const { + data: rewardSharesResult, + isLoading, + error, + } = useReadContracts({ + contracts: rewardSharesCalls, + query: { + refetchInterval: AVERAGE_BLOCKTIME, + }, + }) + + const rewardShares = useMemo( + () => rewardSharesResult?.map(share => share.result as bigint), + [rewardSharesResult], + ) + + return { + data: rewardShares, + isLoading, + error, + } +} diff --git a/src/app/collective-rewards/rewards/hooks/useGetTotalAllocations.ts b/src/app/collective-rewards/rewards/hooks/useGetTotalAllocations.ts new file mode 100644 index 00000000..210d81bd --- /dev/null +++ b/src/app/collective-rewards/rewards/hooks/useGetTotalAllocations.ts @@ -0,0 +1,43 @@ +import { GaugeAbi } from '@/lib/abis/v2/GaugeAbi' +import { AVERAGE_BLOCKTIME } from '@/lib/constants' +import { useMemo } from 'react' +import { Address } from 'viem' +import { useReadContracts } from 'wagmi' + +export const useGetTotalAllocations = (gauges: Address[]) => { + const totalAllocationCalls = useMemo( + () => + gauges?.map( + gauge => + ({ + address: gauge, + abi: GaugeAbi, + functionName: 'totalAllocation', + args: [], + }) as const, + ), + [gauges], + ) + const { + data: allocationsResult, + isLoading, + error, + } = useReadContracts({ + contracts: totalAllocationCalls, + query: { + refetchInterval: AVERAGE_BLOCKTIME, + }, + }) + const allocations = allocationsResult?.map(allocation => allocation.result as bigint) + const sum = useMemo(() => allocations?.reduce((acc, allocation) => acc + allocation, 0n), [allocations]) + const allocationsPercentage = useMemo( + () => (allocations && sum ? allocations?.map(allocation => (allocation / sum) * 100n) : []), + [allocations, sum], + ) + + return { + data: { allocations, allocationsPercentage, sum }, + isLoading, + error, + } +} diff --git a/src/app/collective-rewards/rewards/utils/getNotifyRewardAmount.ts b/src/app/collective-rewards/rewards/utils/getNotifyRewardAmount.ts new file mode 100644 index 00000000..012fdcb1 --- /dev/null +++ b/src/app/collective-rewards/rewards/utils/getNotifyRewardAmount.ts @@ -0,0 +1,44 @@ +import { + BackerRewardsClaimedEventLog, + GaugeNotifyRewardEventLog, + NotifyRewardEventLog, +} from '@/app/collective-rewards/rewards' +import { formatBalanceToHuman } from '@/app/user/Balances/balanceUtils' +import { RBTC, RIF } from '@/lib/constants' +import { tokenContracts } from '@/lib/contracts' +import { Address, getAddress, isAddressEqual } from 'viem' + +interface BuilderClaimedRewards { + [RIF]: number + [RBTC]: number +} +export const getNotifyRewardAmount = ( + gauges: Address[], + notifyRewardEvents: Record, +) => { + const buildersClaimedRifLastCycle: Record = {} + for (const gauge of gauges) { + const gaugeClaimedEvents = notifyRewardEvents[gauge] ?? [] + // Calculate total amount of RIF claimed by the backer + const gaugeRifClaimedTotal: bigint = gaugeClaimedEvents.reduce( + (acc, value) => + isAddressEqual(value.args.rewardToken_, getAddress(tokenContracts.RIF)) + ? acc + value.args.builderAmount_ + : acc, + 0n, + ) + // Calculate total amount of RBTC claimed by the backer + const gaugeRbtcClaimedTotal: bigint = gaugeClaimedEvents.reduce( + (acc, value) => + isAddressEqual(value.args.rewardToken_, getAddress(tokenContracts.RBTC)) + ? acc + value.args.builderAmount_ + : acc, + 0n, + ) + buildersClaimedRifLastCycle[gauge] = { + RIF: Number(formatBalanceToHuman(gaugeRifClaimedTotal)), + RBTC: Number(formatBalanceToHuman(gaugeRbtcClaimedTotal)), + } + } + return buildersClaimedRifLastCycle +} diff --git a/src/app/collective-rewards/rewards/utils/getPercentageData.ts b/src/app/collective-rewards/rewards/utils/getPercentageData.ts new file mode 100644 index 00000000..2a3e49a8 --- /dev/null +++ b/src/app/collective-rewards/rewards/utils/getPercentageData.ts @@ -0,0 +1,22 @@ +import { toPercentage } from './toPercentage' + +export interface BuilderRewardPercentage { + current: number + next: number + cooldownEndTime: bigint +} + +export const getPercentageData = (previous: bigint, next: bigint, cooldownEndTime: bigint) => { + const currentTimestamp = Math.floor(Date.now() / 1000) + const previousPercentage = toPercentage(previous) + const nextPercentage = toPercentage(next) + let currentPercentage = currentTimestamp < cooldownEndTime ? previousPercentage : nextPercentage + currentPercentage = Math.round(currentPercentage * 100) / 100 + + const percentageData: BuilderRewardPercentage = { + current: currentPercentage, + next: nextPercentage, + cooldownEndTime, + } + return percentageData +} diff --git a/src/app/collective-rewards/rewards/utils/index.ts b/src/app/collective-rewards/rewards/utils/index.ts index 52526a99..15bf22f2 100644 --- a/src/app/collective-rewards/rewards/utils/index.ts +++ b/src/app/collective-rewards/rewards/utils/index.ts @@ -1,2 +1,4 @@ export * from './formatMetrics' export * from './getLastCycleRewards' +export * from './getNotifyRewardAmount' +export * from './getPercentageData' diff --git a/src/app/collective-rewards/rewards/utils/toPercentage.ts b/src/app/collective-rewards/rewards/utils/toPercentage.ts new file mode 100644 index 00000000..1f316dac --- /dev/null +++ b/src/app/collective-rewards/rewards/utils/toPercentage.ts @@ -0,0 +1,3 @@ +import { parseEther } from 'viem' + +export const toPercentage = (value: bigint) => Number((value * 100n) / parseEther('1')) diff --git a/src/app/collective-rewards/types.ts b/src/app/collective-rewards/types.ts index fe435078..24d879cd 100644 --- a/src/app/collective-rewards/types.ts +++ b/src/app/collective-rewards/types.ts @@ -18,6 +18,7 @@ export type BuilderInfo = { address: Address status: BuilderStatus proposals: CreateBuilderProposalEventLog[] + gauge: Address } export type ProposalsToState = Record diff --git a/src/app/collective-rewards/user/components/Button/BecomeABuilderButton.test.tsx b/src/app/collective-rewards/user/components/Button/BecomeABuilderButton.test.tsx index d6541d99..cafd2927 100644 --- a/src/app/collective-rewards/user/components/Button/BecomeABuilderButton.test.tsx +++ b/src/app/collective-rewards/user/components/Button/BecomeABuilderButton.test.tsx @@ -44,6 +44,7 @@ describe('BecomeABuilderButton', () => { timeStamp: 1723309061, }, ] as CreateBuilderProposalEventLog[], + gauge: '0x01', } const buildersData = [builderData] const proposalsToStates = { diff --git a/src/app/collective-rewards/user/context/BuilderContext.tsx b/src/app/collective-rewards/user/context/BuilderContext.tsx index f29c6392..50a7a845 100644 --- a/src/app/collective-rewards/user/context/BuilderContext.tsx +++ b/src/app/collective-rewards/user/context/BuilderContext.tsx @@ -20,6 +20,7 @@ export type BuilderProposal = { proposalName: string proposalDescription: string joiningDate: string + gauge: Address } type ProposalByBuilder = Record @@ -58,7 +59,7 @@ export const BuilderContextProvider: FC = ({ children }) = const filteredBuilders = useMemo(() => { return builders.reduce((acc, builder) => { - const { status, address } = builder + const { status, address, gauge } = builder const proposal = getMostAdvancedProposal(builder, proposalsStateMap) if (proposal) { @@ -77,6 +78,7 @@ export const BuilderContextProvider: FC = ({ children }) = proposalName, proposalDescription, joiningDate, + gauge, } } diff --git a/src/app/collective-rewards/user/hooks/useGetBuilders.ts b/src/app/collective-rewards/user/hooks/useGetBuilders.ts index fd791b8e..7ddbdea9 100644 --- a/src/app/collective-rewards/user/hooks/useGetBuilders.ts +++ b/src/app/collective-rewards/user/hooks/useGetBuilders.ts @@ -61,6 +61,7 @@ export const useGetBuilders = (): BuildersLoader => { */ // get the gauges const { data: gauges, isLoading: gaugesLoading, error: gaugesError } = useGetGaugesArray('active') + // get the builders for each gauge const gaugeToBuilderCalls = gauges?.map( gauge => @@ -83,6 +84,14 @@ export const useGetBuilders = (): BuildersLoader => { }) const builders = buildersResult?.map(builder => builder.result) as Address[] + const builderToGauge = builders?.reduce( + (acc, builder, index) => { + acc[builder] = gauges![index] + return acc + }, + {} as Record, + ) + // get the builder state for each builder const builderStatesCalls = builders?.map( builder => @@ -124,8 +133,9 @@ export const useGetBuilders = (): BuildersLoader => { ? builderStatusMap[builder as Address] // V2 : BuilderStatusProposalCreatedMVP, // MVP proposals: Object.values(proposals), + gauge: builderToGauge?.[builder as Address], })) - }, [builderStatusMap, buildersProposalsMap]) + }, [builderStatusMap, buildersProposalsMap, builderToGauge]) const isLoading = builderProposalsMapLoading || builderStatesLoading || buildersLoading || gaugesLoading const error = builderProposalsMapError ?? builderStatesError ?? buildersError ?? gaugesError diff --git a/src/app/collective-rewards/utils/getMostAdvancedProposal.test.tsx b/src/app/collective-rewards/utils/getMostAdvancedProposal.test.tsx index 2435d4f3..8f5ea79d 100644 --- a/src/app/collective-rewards/utils/getMostAdvancedProposal.test.tsx +++ b/src/app/collective-rewards/utils/getMostAdvancedProposal.test.tsx @@ -24,6 +24,7 @@ describe('getValidProposal', () => { }, }, ] as CreateBuilderProposalEventLog[], + gauge: '0x01', }, { 1: ProposalState.Executed, @@ -48,6 +49,7 @@ describe('getValidProposal', () => { }, }, ] as CreateBuilderProposalEventLog[], + gauge: '0x01', }, { 1: ProposalState.Active, @@ -78,6 +80,7 @@ describe('getValidProposal', () => { }, }, ] as CreateBuilderProposalEventLog[], + gauge: '0x01', }, { 1: ProposalState.Active, @@ -108,6 +111,7 @@ describe('getValidProposal', () => { }, }, ] as CreateBuilderProposalEventLog[], + gauge: '0x01', }, { 1: ProposalState.Canceled, diff --git a/src/app/collective-rewards/whitelist/hooks/useGetFilteredBuilders.ts b/src/app/collective-rewards/whitelist/hooks/useGetFilteredBuilders.ts index eff84149..f70b0ffe 100644 --- a/src/app/collective-rewards/whitelist/hooks/useGetFilteredBuilders.ts +++ b/src/app/collective-rewards/whitelist/hooks/useGetFilteredBuilders.ts @@ -31,6 +31,7 @@ export const useGetFilteredBuilders = ({ if (filterStatus !== 'all') { filteredBuilders = filteredBuilders.filter(builder => builder.status === filterStatus) } + setData(filteredBuilders) }, [builders, filterBuilderName, filterStatus]) diff --git a/src/components/Collapsible/Collapsible.tsx b/src/components/Collapsible/Collapsible.tsx index 113a03ab..8e67fb2c 100644 --- a/src/components/Collapsible/Collapsible.tsx +++ b/src/components/Collapsible/Collapsible.tsx @@ -13,7 +13,7 @@ const CollapsibleTrigger = React.forwardRef< const iconBaseCn = 'h-6 w-6 shrink-0' return (
-
+
diff --git a/src/components/ProgressBar/ProgressBar.tsx b/src/components/ProgressBar/ProgressBar.tsx new file mode 100644 index 00000000..f77f9c82 --- /dev/null +++ b/src/components/ProgressBar/ProgressBar.tsx @@ -0,0 +1,17 @@ +import React from 'react' + +type Props = { + progress: number // A number between 0 and 100 + color?: string +} + +export const ProgressBar: React.FC = ({ progress, color }) => { + return ( +
+
+
+ ) +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 727e9f55..48005315 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -34,4 +34,5 @@ export const AVERAGE_BLOCKTIME = 30_000 export const RIF = 'RIF' export const USD = 'USD' +export const RBTC = 'RBTC' export const USD_SYMBOL = '$' diff --git a/src/shared/hooks/usePagination.ts b/src/shared/hooks/usePagination.ts index df8d3876..fd9ce9a4 100644 --- a/src/shared/hooks/usePagination.ts +++ b/src/shared/hooks/usePagination.ts @@ -1,7 +1,7 @@ import { InfiniteData, useInfiniteQuery, UseInfiniteQueryResult } from '@tanstack/react-query' import { useState, useCallback, useMemo, useEffect } from 'react' -interface PaginatedResponse { +export interface PaginatedResponse { items: T[] [key: string]: any } diff --git a/src/shared/hooks/usePaginationUi.tsx b/src/shared/hooks/usePaginationUi.tsx index 627aaf6b..c0a1a8a5 100644 --- a/src/shared/hooks/usePaginationUi.tsx +++ b/src/shared/hooks/usePaginationUi.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useMemo } from 'react' +import { ReactNode, useEffect, useMemo, useState } from 'react' import { UsePaginatedQueryResult } from '@/shared/hooks/usePagination' import { Button } from '@/components/Button' import { ChevronLeft, ChevronRight } from 'lucide-react' @@ -22,8 +22,8 @@ export function usePaginationUi( } = paginationResult const paginationElement = useMemo(() => { + const maxVisiblePages = 5 const getPageNumbers = () => { - const maxVisiblePages = 5 const pages = [] const start = Math.max(0, Math.min(tablePage - 2, totalPages - maxVisiblePages)) const end = Math.min(start + maxVisiblePages, totalPages) @@ -66,6 +66,53 @@ export function usePaginationUi( } } +export function useBasicPaginationUi(totalPages: number) { + const [currentPage, setCurrentPage] = useState(0) + const maxVisiblePages = 5 + + const setPage = (nextPage: number) => { + if (nextPage < 0 || nextPage >= totalPages) return + if (nextPage === currentPage) return + setCurrentPage(nextPage) + } + + const getPageNumbers = () => { + const pages = [] + const start = Math.max(0, Math.min(currentPage - 2, totalPages - maxVisiblePages)) + const end = Math.min(start + maxVisiblePages, totalPages) + + for (let i = start; i < end; i++) { + pages.push(i) + } + return pages + } + + const paginationUi = ( +
+ } + onClick={() => setPage(currentPage - 1)} + disabled={currentPage === 0} + /> + {getPageNumbers().map(pageNumber => ( + setPage(pageNumber)} + text={pageNumber + 1} + isActive={pageNumber === currentPage} + /> + ))} + } + onClick={() => setPage(currentPage + 1)} + disabled={currentPage === totalPages - 1} + /> +
+ ) + + return { paginationUi, currentPage } +} + const PaginationButton = ({ text, onClick,