Skip to content

Commit

Permalink
fix(ui): recomputation of stakers chart data (#102)
Browse files Browse the repository at this point in the history
When switching between pools in StakingDetails, the aggregation logic for "all pools" that calculates each stakers' total balance was not correctly isolated. If a staker was in multiple pools the total balance was compounding each time and would get bigger and bigger each time the selected pool changed. This fix properly resets the aggregation logic.

It also refactors the logic into a custom hook, `useStakersChartData`, which simplifies the component and isolates the logic so it can be tested.
  • Loading branch information
drichar authored Apr 26, 2024
1 parent 3bf4d0b commit 7d9f052
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 57 deletions.
67 changes: 10 additions & 57 deletions ui/src/components/ValidatorDetails/StakingDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { AlgoAmount } from '@algorandfoundation/algokit-utils/types/amount'
import { useQueries, useQuery } from '@tanstack/react-query'
import { BarList, EventProps, ProgressBar } from '@tremor/react'
import { useWallet } from '@txnlab/use-wallet-react'
import { Copy } from 'lucide-react'
import * as React from 'react'
import { stakedInfoQueryOptions, validatorPoolsQueryOptions } from '@/api/queries'
import { AddStakeModal } from '@/components/AddStakeModal'
import { AlgoDisplayAmount } from '@/components/AlgoDisplayAmount'
import { Loading } from '@/components/Loading'
Expand All @@ -20,7 +18,8 @@ import {
} from '@/components/ui/select'
import { UnstakeModal } from '@/components/UnstakeModal'
import { PoolsChart } from '@/components/ValidatorDetails/PoolsChart'
import { StakedInfo, StakerValidatorData } from '@/interfaces/staking'
import { useStakersChartData } from '@/hooks/useStakersChartData'
import { StakerValidatorData } from '@/interfaces/staking'
import { Constraints, Validator } from '@/interfaces/validator'
import { isStakingDisabled, isUnstakingDisabled } from '@/utils/contracts'
import { copyToClipboard } from '@/utils/copyToClipboard'
Expand Down Expand Up @@ -52,58 +51,11 @@ export function StakingDetails({ validator, constraints, stakesByValidator }: St
value: convertFromBaseUnits(Number(pool.totalAlgoStaked || 1n), 6),
})) || []

const poolsInfoQuery = useQuery(validatorPoolsQueryOptions(validator.id))
const poolsInfo = poolsInfoQuery.data || []

const allStakedInfo = useQueries({
queries: poolsInfo.map((pool) => stakedInfoQueryOptions(pool.poolAppId)),
const { stakersChartData, poolsInfo, isLoading, isError } = useStakersChartData({
selectedPool,
validatorId: validator.id,
})

const isLoading = poolsInfoQuery.isLoading || allStakedInfo.some((query) => query.isLoading)
const isError = poolsInfoQuery.isError || allStakedInfo.some((query) => query.isError)

const chartData = React.useMemo(() => {
if (!allStakedInfo) {
return []
}

const stakedInfo = allStakedInfo
.map((query) => query.data || [])
.reduce((acc, stakers, i) => {
if (selectedPool !== 'all' && Number(selectedPool) !== i) {
return acc
}

// Temporary fix to handle duplicate staker bug
const poolStakers: StakedInfo[] = []
for (const staker of stakers) {
const stakerIndex = poolStakers.findIndex((s) => s.account === staker.account)
if (stakerIndex > -1) {
staker.account += ' ' // add space to make it unique
}
poolStakers.push(staker)
}

for (const staker of poolStakers) {
const stakerIndex = acc.findIndex((s) => s.account === staker.account)
if (stakerIndex > -1) {
acc[stakerIndex].balance += staker.balance
acc[stakerIndex].totalRewarded += staker.totalRewarded
acc[stakerIndex].rewardTokenBalance += staker.rewardTokenBalance
} else {
acc.push(staker)
}
}
return acc
}, [] as StakedInfo[])

return stakedInfo.map((staker) => ({
name: staker.account,
value: Number(staker.balance),
href: ExplorerLink.account(staker.account).trim(), // trim to remove trailing whitespace
}))
}, [allStakedInfo, selectedPool])

const valueFormatter = (v: number) => (
<AlgoDisplayAmount
amount={v}
Expand Down Expand Up @@ -379,16 +331,17 @@ export function StakingDetails({ validator, constraints, stakesByValidator }: St
</div>
<div className="flex items-center">{renderPoolInfo()}</div>
</div>
{chartData.length > 0 && (

{stakersChartData.length > 0 && (
<ScrollArea
className={cn('rounded-lg border', {
'h-64': chartData.length > 6,
'sm:h-96': chartData.length > 9,
'h-64': stakersChartData.length > 6,
'sm:h-96': stakersChartData.length > 9,
})}
>
<div className="p-2 pr-6">
<BarList
data={chartData}
data={stakersChartData}
valueFormatter={valueFormatter}
className="font-mono"
showAnimation
Expand Down
69 changes: 69 additions & 0 deletions ui/src/hooks/useStakersChartData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useQueries, useQuery } from '@tanstack/react-query'
import * as React from 'react'
import { stakedInfoQueryOptions, validatorPoolsQueryOptions } from '@/api/queries'
import { StakedInfo } from '@/interfaces/staking'
import { ExplorerLink } from '@/utils/explorer'

interface UseChartDataProps {
selectedPool: string
validatorId: number
}

export function useStakersChartData({ selectedPool, validatorId }: UseChartDataProps) {
const poolsInfoQuery = useQuery(validatorPoolsQueryOptions(validatorId))
const poolsInfo = poolsInfoQuery.data || []

const allStakedInfo = useQueries({
queries: poolsInfo.map((pool) => stakedInfoQueryOptions(pool.poolAppId)),
})

const isLoading = poolsInfoQuery.isLoading || allStakedInfo.some((query) => query.isLoading)
const isError = poolsInfoQuery.isError || allStakedInfo.some((query) => query.isError)
const isSuccess = poolsInfoQuery.isSuccess && allStakedInfo.every((query) => query.isSuccess)

const stakersChartData = React.useMemo(() => {
if (!allStakedInfo) {
return []
}

const stakerTotals: Record<string, StakedInfo> = {}

allStakedInfo.forEach((query, i) => {
if (selectedPool !== 'all' && Number(selectedPool) !== i) {
return
}

const stakers = query.data || []

stakers.forEach((staker) => {
const id = staker.account

if (!stakerTotals[id]) {
stakerTotals[id] = {
...staker,
balance: BigInt(0),
totalRewarded: BigInt(0),
rewardTokenBalance: BigInt(0),
}
}
stakerTotals[id].balance += staker.balance
stakerTotals[id].totalRewarded += staker.totalRewarded
stakerTotals[id].rewardTokenBalance += staker.rewardTokenBalance
})
})

return Object.values(stakerTotals).map((staker) => ({
name: staker.account,
value: Number(staker.balance),
href: ExplorerLink.account(staker.account),
}))
}, [allStakedInfo, selectedPool])

return {
stakersChartData,
poolsInfo,
isLoading,
isError,
isSuccess,
}
}

0 comments on commit 7d9f052

Please sign in to comment.