Skip to content

Commit

Permalink
TOK-512: last cycle rewards timestamps (#433)
Browse files Browse the repository at this point in the history
* refactor: last cycle rewards timestamps

* refactor: pr comments
  • Loading branch information
franciscotobar authored Jan 7, 2025
1 parent 0be8800 commit 85854e0
Show file tree
Hide file tree
Showing 13 changed files with 247 additions and 79 deletions.
9 changes: 9 additions & 0 deletions src/app/collective-rewards/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
fetchBuilderRewardsClaimedLogsByAddress,
fetchGaugeNotifyRewardLogsByAddress,
fetchNotifyRewardLogsByAddress,
fetchRewardDistributionFinishedLogsByAddress,
} from '@/lib/endpoints'

export const fetchNotifyRewardLogs = (fromBlock = 0) => {
Expand Down Expand Up @@ -39,3 +40,11 @@ export const fetchBackerRewardsClaimed = (gaugeAddress: Address, fromBlock = 0)
.replace('{{fromBlock}}', fromBlock.toString()),
)
}

export const fetchRewardDistributionFinished = (fromBlock = 0) => {
return axiosInstance.get(
fetchRewardDistributionFinishedLogsByAddress
.replace('{{address}}', BackersManagerAddress)
.replace('{{fromBlock}}', fromBlock.toString()),
)
}
19 changes: 13 additions & 6 deletions src/app/collective-rewards/rewards/builders/LastCycleRewards.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { useCycleContext } from '@/app/collective-rewards/metrics/context/CycleContext'
import {
formatMetrics,
getLastCycleRewards,
MetricsCard,
MetricsCardTitle,
TokenMetricsCardRow,
useGetGaugeNotifyRewardLogs,
Token,
BuilderRewardDetails,
getNotifyRewardAmount,
useGetLastCycleDistribution,
} from '@/app/collective-rewards/rewards'
import { useHandleErrors } from '@/app/collective-rewards/utils'
import { withSpinner } from '@/components/LoadingSpinner/withLoadingSpinner'
Expand All @@ -27,28 +28,34 @@ const TokenRewardsMetrics: FC<TokenRewardsMetricsProps> = ({
currency = 'USD',
}) => {
const { data: cycle, isLoading: cycleLoading, error: cycleError } = useCycleContext()
const {
data: { fromTimestamp, toTimestamp },
isLoading: lastCycleRewardsLoading,
error: lastCycleRewardsError,
} = useGetLastCycleDistribution(cycle)

const {
data: rewardsPerToken,
isLoading: logsLoading,
error: rewardsError,
} = useGetGaugeNotifyRewardLogs(gauge)
} = useGetGaugeNotifyRewardLogs(gauge, address, fromTimestamp, toTimestamp)

const error = cycleError ?? rewardsError
const error = cycleError ?? lastCycleRewardsError ?? rewardsError
useHandleErrors({ error, title: 'Error loading last cycle rewards' })

const { prices } = usePricesContext()

const lastCycleRewards = getLastCycleRewards(cycle, rewardsPerToken[address])
const lastCycleRewards = getNotifyRewardAmount(rewardsPerToken, address, 'builderAmount_')
const price = prices[symbol]?.price ?? 0
const { amount, fiatAmount } = formatMetrics(lastCycleRewards.builderAmount, price, symbol, currency)
const { amount, fiatAmount } = formatMetrics(lastCycleRewards[address] ?? 0n, price, symbol, currency)

return withSpinner(
TokenMetricsCardRow,
'min-h-0 grow-0',
)({
amount,
fiatAmount,
isLoading: cycleLoading || logsLoading,
isLoading: cycleLoading || lastCycleRewardsLoading || logsLoading,
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
RbtcSvg,
TokenRewards,
BackerRewardPercentage,
useGetLastCycleDistribution,
} from '@/app/collective-rewards/rewards'
import { usePricesContext } from '@/shared/context/PricesContext'
import { useGaugesGetFunction } from '@/app/collective-rewards/shared'
Expand Down Expand Up @@ -83,31 +84,35 @@ export const useGetBuildersRewards = ({ rif, rbtc }: { [token: string]: Token },
error: rewardsCoinbaseError,
} = useGetRewardsCoinbase()

const { cycleDuration, cycleStart, endDistributionWindow, cycleNext } = cycle
const distributionWindow = endDistributionWindow.diff(cycleStart)
const lastCycleStart = cycleStart.minus({ seconds: cycleDuration.as('seconds') })
const lastCycleAfterDistribution = lastCycleStart.plus({ seconds: distributionWindow.as('seconds') })
const {
data: { fromTimestamp, toTimestamp },
isLoading: lastCycleRewardsLoading,
error: lastCycleRewardsError,
} = useGetLastCycleDistribution(cycle)

const {
data: notifyRewardEventLastCycle,
isLoading: logsLoading,
error: logsError,
} = useGetGaugesNotifyReward(
gauges,
undefined,
lastCycleAfterDistribution.toSeconds(),
endDistributionWindow.toSeconds(),
} = useGetGaugesNotifyReward(gauges, undefined, fromTimestamp, toTimestamp)
const rifBuildersRewardsAmount = getNotifyRewardAmount(
notifyRewardEventLastCycle,
rif.address,
'backersAmount_',
)
const rbtcBuildersRewardsAmount = getNotifyRewardAmount(
notifyRewardEventLastCycle,
rbtc.address,
'backersAmount_',
)
const rifBuildersRewardsAmount = getNotifyRewardAmount(notifyRewardEventLastCycle, rif, 'backersAmount_')
const rbtcBuildersRewardsAmount = getNotifyRewardAmount(notifyRewardEventLastCycle, rbtc, 'backersAmount_')

// get the backer reward percentage for each builder we want to show
const buildersAddress = builders.map(({ address }) => address)
const {
data: backersRewardsPct,
isLoading: backersRewardsPctLoading,
error: backersRewardsPctError,
} = useGetBackersRewardPercentage(buildersAddress, cycleNext.toSeconds())
} = useGetBackersRewardPercentage(buildersAddress, cycle.cycleNext.toSeconds())

const isLoading =
rewardSharesLoading ||
Expand All @@ -118,7 +123,8 @@ export const useGetBuildersRewards = ({ rif, rbtc }: { [token: string]: Token },
rewardsERC20Loading ||
rewardsCoinbaseLoading ||
cycleLoading ||
totalPotentialRewardsLoading
totalPotentialRewardsLoading ||
lastCycleRewardsLoading

const error =
rewardSharesError ??
Expand All @@ -129,7 +135,8 @@ export const useGetBuildersRewards = ({ rif, rbtc }: { [token: string]: Token },
rewardsERC20Error ??
rewardsCoinbaseError ??
cycleError ??
totalPotentialRewardsError
totalPotentialRewardsError ??
lastCycleRewardsError

const { prices } = usePricesContext()

Expand Down
2 changes: 2 additions & 0 deletions src/app/collective-rewards/rewards/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ export * from './useGetBackerRewardPercentage'
export * from './useGetBackersRewardPercentage'
export * from './useGetBackerRewardPerTokenPaid'
export * from './useIsBuilderOrBacker'
export * from './useGetRewardDistributionFinishedLogs'
export * from './useGetLastCycleDistribution'
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Address, isAddressEqual } from 'viem'
import { useMemo } from 'react'
import { useGetGaugesEvents } from '@/app/collective-rewards/rewards'
import { GaugeNotifyRewardEventLog, useGetGaugesEvents } from '@/app/collective-rewards/rewards'

export const useGetGaugesNotifyReward = (
gauges: Address[],
Expand All @@ -10,25 +10,24 @@ export const useGetGaugesNotifyReward = (
) => {
const { data: eventsData, isLoading, error } = useGetGaugesEvents(gauges, 'NotifyReward')

type Log = GaugeNotifyRewardEventLog[number]
const data = useMemo(() => {
if (!eventsData) {
return {}
}

return Object.keys(eventsData).reduce((acc: { [key: string]: (typeof eventsData)[Address] }, key) => {
let events = eventsData[key as Address]
let events = eventsData[key as Address] as (Log & { timeStamp: number })[]
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
})
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
useGetRewardDistributionFinishedLogs,
useGetLastCycleDistribution,
RewardDistributionFinishedEventLog,
} from '@/app/collective-rewards/rewards'
import { describe, expect, it, vi } from 'vitest'
import { Cycle } from '@/app/collective-rewards/metrics'
import { DateTime, Duration } from 'luxon'

vi.mock('@/app/collective-rewards/rewards/hooks/useGetRewardDistributionFinishedLogs', () => {
return {
useGetRewardDistributionFinishedLogs: vi.fn(),
}
})

describe('useGetLastCycleRewardsTimestamps', () => {
type Log = RewardDistributionFinishedEventLog[number] & { timeStamp: number }
const startTimestamp = 1733011200 // 2024-12-01 00:00:00
const duration = 1209600 // 14 days
const distributionWindow = 3600 // 1 hour
const cycle: Cycle = {
cycleStart: DateTime.fromSeconds(startTimestamp),
cycleNext: DateTime.fromSeconds(startTimestamp + duration),
cycleDuration: Duration.fromObject({ seconds: duration }),
fistCycleStart: DateTime.fromSeconds(startTimestamp),
endDistributionWindow: DateTime.fromSeconds(distributionWindow),
}
const { cycleStart, cycleNext, cycleDuration } = cycle

it('should return (from,to) = to next cycle start if there are no events', () => {
vi.mocked(useGetRewardDistributionFinishedLogs).mockImplementation(() => {
return {
data: [],
isLoading: false,
error: null,
}
})

const { data } = useGetLastCycleDistribution(cycle)

expect(data).toEqual({
fromTimestamp: cycleNext.toSeconds(),
toTimestamp: cycleNext.toSeconds(),
})
})

it('should return (from,to) = to next cycle start if there were not distributions in the current cycle', () => {
const lastCycleStart = cycle.cycleStart.minus({ seconds: cycleDuration.as('seconds') })
vi.mocked(useGetRewardDistributionFinishedLogs).mockImplementation(() => {
return {
data: [
{
timeStamp: lastCycleStart.toSeconds(),
},
] as Log[],
isLoading: false,
error: null,
}
})

const { data } = useGetLastCycleDistribution(cycle)

expect(data).toEqual({
fromTimestamp: cycleNext.toSeconds(),
toTimestamp: cycleNext.toSeconds(),
})
})

it('should return (from = current cycle start, to = last event) if there were distributions in the current cycle', () => {
const endDistributionTime = cycleStart.plus({ seconds: 100 })
vi.mocked(useGetRewardDistributionFinishedLogs).mockImplementation(() => {
return {
data: [
{
timeStamp: endDistributionTime.toSeconds(),
},
] as Log[],
isLoading: false,
error: null,
}
})

const { data } = useGetLastCycleDistribution(cycle)

expect(data).toEqual({
fromTimestamp: cycleStart.toSeconds(),
toTimestamp: endDistributionTime.toSeconds(),
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Cycle } from '@/app/collective-rewards/metrics'
import {
RewardDistributionFinishedEventLog,
useGetRewardDistributionFinishedLogs,
} from '@/app/collective-rewards/rewards'

type Log = RewardDistributionFinishedEventLog[number] & { timeStamp: number }
export const useGetLastCycleDistribution = ({ cycleStart, cycleNext }: Cycle) => {
const { data: rewardDistributionFinished, isLoading, error } = useGetRewardDistributionFinishedLogs()

const [lastEvent] = rewardDistributionFinished.slice(-1) as Log[]

let fromTimestamp = cycleNext.toSeconds()
let toTimestamp = cycleNext.toSeconds()

if (lastEvent && lastEvent.timeStamp >= cycleStart.toSeconds()) {
fromTimestamp = cycleStart.toSeconds()
toTimestamp = lastEvent.timeStamp
}

return {
data: {
fromTimestamp,
toTimestamp,
},
isLoading,
error,
}
}
54 changes: 42 additions & 12 deletions src/app/collective-rewards/rewards/hooks/useGetNotifyRewardLogs.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useQuery } from '@tanstack/react-query'
import { fetchGaugeNotifyRewardLogs, fetchNotifyRewardLogs } from '@/app/collective-rewards/actions'
import { Address, getAddress, parseEventLogs } from 'viem'
import { Address, getAddress, isAddressEqual, parseEventLogs } from 'viem'
import { GaugeAbi } from '@/lib/abis/v2/GaugeAbi'
import { BackersManagerAbi } from '@/lib/abis/v2/BackersManagerAbi'
import { BackersManagerAddress } from '@/lib/contracts'
import { AVERAGE_BLOCKTIME } from '@/lib/constants'
import { useMemo } from 'react'

export type NotifyRewardEventLog = ReturnType<
typeof parseEventLogs<typeof BackersManagerAbi, true, 'NotifyReward'>
Expand Down Expand Up @@ -46,28 +47,57 @@ export type GaugeNotifyRewardEventLog = ReturnType<
>
export type GaugeNotifyRewardsPerToken = Record<Address, GaugeNotifyRewardEventLog>

export const useGetGaugeNotifyRewardLogs = (gauge: Address) => {
const { data, error, isLoading } = useQuery({
export const useGetGaugeNotifyRewardLogs = (
gauge: Address,
rewardToken?: Address,
fromTimestamp?: number,
toTimestamp?: number,
) => {
const {
data: events,
error,
isLoading,
} = useQuery({
queryFn: async () => {
const { data } = await fetchGaugeNotifyRewardLogs(gauge)

const events = parseEventLogs({
return parseEventLogs({
abi: GaugeAbi,
logs: data,
eventName: 'NotifyReward',
})

return events.reduce<GaugeNotifyRewardsPerToken>((acc, log) => {
const rewardToken = getAddress(log.args.rewardToken_)
acc[rewardToken] = [...(acc[rewardToken] || []), log]
return acc
}, {})
},
queryKey: ['notifyRewardLogs', gauge],
queryKey: ['notifyRewardLogs', gauge, rewardToken],
refetchInterval: AVERAGE_BLOCKTIME,
initialData: {},
initialData: [],
})

type Log = GaugeNotifyRewardEventLog[number]
const data = useMemo(() => {
return events.reduce<GaugeNotifyRewardsPerToken>((acc, log) => {
const {
timeStamp,
args: { rewardToken_ },
} = log as Log & { timeStamp: number }
const rewardTokenAddress = getAddress(rewardToken_)

if (rewardToken && !isAddressEqual(rewardToken, rewardTokenAddress)) {
return acc
}

if (fromTimestamp && timeStamp < fromTimestamp) {
return acc
}

if (toTimestamp && timeStamp > toTimestamp) {
return acc
}

acc[rewardTokenAddress] = [...(acc[rewardTokenAddress] || []), log]
return acc
}, {})
}, [events, rewardToken, fromTimestamp, toTimestamp])

return {
data,
error,
Expand Down
Loading

0 comments on commit 85854e0

Please sign in to comment.