diff --git a/.env.testnet.local b/.env.testnet.local index 002d8ca5..baa2e961 100644 --- a/.env.testnet.local +++ b/.env.testnet.local @@ -17,6 +17,9 @@ NEXT_PUBLIC_GENERAL_BUCKET_ADDRESS=0x72Ed7d7b7835Ad62B1f9b6280bAd62618aA71461 NEXT_PUBLIC_CHAIN_ID=31 # CR-related env variables +# TODO: To be removed +NEXT_PUBLIC_SIMPLIFIED_REWARD_DISTRIBUTOR_ADDRESS=0xc469Cc2579De5C16210e9063B4E628bF8C46bA02 + NEXT_PUBLIC_BACKERS_MANAGER_ADDRESS=0xec0a29Df5180A6B04496dfAf2D827e36F4a0A52F NEXT_PUBLIC_REWARD_DISTRIBUTOR_ADDRESS=0xD476E4804551595687C1f6F0a9C22dd1Bbfa0319 NEXT_PUBLIC_GOVERNANCE_MANAGER_ADDRESS=0xb7C6918d6aE6df2e147FF464271a94EAfF027E5D diff --git a/src/app/collective-rewards/allocations/AllocationMetrics.tsx b/src/app/collective-rewards/allocations/AllocationMetrics.tsx deleted file mode 100644 index 4906fec2..00000000 --- a/src/app/collective-rewards/allocations/AllocationMetrics.tsx +++ /dev/null @@ -1,93 +0,0 @@ -'use client' - -import { formatBalanceToHuman, getTokenBalance } from '@/app/user/Balances/balanceUtils' -import { useGetAddressTokens } from '@/app/user/Balances/hooks/useGetAddressTokens' -import { withSpinner } from '@/components/LoadingSpinner/withLoadingSpinner' -import { Paragraph } from '@/components/Typography' -import { ethers } from 'ethers' -import { useAccount } from 'wagmi' -import { useHandleErrors } from '@/app/collective-rewards/utils' -import { useBackerTotalAllocation } from './useBackerTotalAllocation' - -type ValueProps = { - value: string -} - -type MetricsProps = ValueProps & { - name: string -} - -const Metric = ({ name, value }: MetricsProps) => { - return ( -
- {name} - {value} -
- ) -} - -const Column = ({ children }: { children: React.ReactNode }) => { - return
{children}
-} - -const Balance = ({ value }: ValueProps) => { - return -} -const BalanceWithSpinner = withSpinner(Balance) - -const AllocatedAmount = ({ value }: ValueProps) => { - return -} -const AllocatedAmountWithSpinner = withSpinner(AllocatedAmount) - -const UnallocatedAmount = ({ value }: ValueProps) => { - return -} -const UnallocatedAmountWithSpinner = withSpinner(UnallocatedAmount) - -export const AllocationMetrics = () => { - // TODO: we can move this logic to a custom hook or to a context - const { address, chainId } = useAccount() - let { - data, - isLoading: balanceLoading, - error: balanceError, - } = useGetAddressTokens(address!, chainId as number) - const stRIFBalance = getTokenBalance('stRIF', data) - const balanceValue = `${stRIFBalance.balance} ${stRIFBalance.symbol}` - balanceLoading = false - - let { - data: allocatedAmount, - isLoading: allocatedAmountLoading, - error: allocatedAmountError, - } = useBackerTotalAllocation(address!) - const allocatedAmountValue = `${allocatedAmount} ${stRIFBalance.symbol}` - allocatedAmountLoading = false - allocatedAmount = 0n - - useHandleErrors({ - error: balanceError ?? allocatedAmountError, - title: 'Failed to fetch balance and allocated amount', - }) - - const unformattedUnit = stRIFBalance.balance - const formattedUnit = ethers.parseEther(unformattedUnit) - const unallocatedAmount = formatBalanceToHuman(formattedUnit - (allocatedAmount || 0n)) - - const unallocatedAmountValue = `${unallocatedAmount} ${stRIFBalance.symbol}` - const unallocatedAmountLoading = balanceLoading || allocatedAmountLoading - return ( -
- - - - - - - - - -
- ) -} diff --git a/src/app/collective-rewards/allocations/BuilderAllocation.tsx b/src/app/collective-rewards/allocations/BuilderAllocation.tsx deleted file mode 100644 index 6fe5eaed..00000000 --- a/src/app/collective-rewards/allocations/BuilderAllocation.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { formatBalanceToHuman } from '@/app/user/Balances/balanceUtils' -import { Input } from '@/components/Input' -import { Slider } from '@/components/Slider' -import { Label } from '@/components/Typography' -import { useState } from 'react' -import { BuilderAllocationHeader } from './BuilderAllocationHeader' -import { BuilderAllocationProps } from './types' - -export const BuilderAllocation = (builder: BuilderAllocationProps) => { - const [sliderValue, setSliderValue] = useState(builder.currentAllocation) - /* TODO: when the cumulative amount exceeds the balance - * - hint is changed - * - Slider is hidden - */ - const hint = `Allocation left ${formatBalanceToHuman(builder.allocationLeft.toString())} stRIF` - // const hint = - const onInputChange = () => { - /* TODO: - * - reset all the sliders to 0 when the user changes the input - * - update the cumulative amount - * - if the cumulative amount exceeds the total allocation, show an error message in the current input - */ - } - return ( -
- - - - -
- ) -} diff --git a/src/app/collective-rewards/allocations/AllocationAmount.tsx b/src/app/collective-rewards/allocations/components/AllocationAmount.tsx similarity index 51% rename from src/app/collective-rewards/allocations/AllocationAmount.tsx rename to src/app/collective-rewards/allocations/components/AllocationAmount.tsx index a6d84450..4a9d1974 100644 --- a/src/app/collective-rewards/allocations/AllocationAmount.tsx +++ b/src/app/collective-rewards/allocations/components/AllocationAmount.tsx @@ -1,10 +1,12 @@ 'use client' -import { formatBalanceToHuman } from '@/app/user/Balances/balanceUtils' import { Button, ButtonProps } from '@/components/Button' import { Input } from '@/components/Input' import { cn } from '@/lib/utils' -import { useState } from 'react' +import { useContext, useState } from 'react' +import { AllocationsContext } from '@/app/collective-rewards/allocations/context' +import { formatEther, parseEther } from 'viem' +import { StakeHint } from './StakeHint' const PercentageButton = ({ children, variant, ...rest }: ButtonProps) => ( ) -type AllocationAmountProps = { - balance: bigint - errorMessage?: string - onPercentageSet?: (percentage: number) => void -} +const ALLOCATION_EXCEED_AMOUNT_ERROR = 'Builder allocations exceeds amount to allocate' -export const AllocationAmount = ({ balance, errorMessage, onPercentageSet }: AllocationAmountProps) => { - // TODO: hint is shown only when the allocated amount exceeds the balance - // const hint = - const hint = undefined +export const AllocationAmount = () => { + const { + state: { + backer: { balance, totalAllocation, cumulativeAllocation, allocationCount }, + }, + actions: { updateAllocations, updateTotalAllocation }, + } = useContext(AllocationsContext) const [activeButton, setActiveButton] = useState(null) - const [allocatedAmount, setAllocatedAmount] = useState('0') - const onChange = (value: string) => { - setAllocatedAmount(value) - } - const onPercentageButtonClicked = (percentage: number, index: number) => { - const percentageAmount = (BigInt(balance ?? 0n) * BigInt(percentage)) / BigInt(100) - setAllocatedAmount(formatBalanceToHuman(percentageAmount)) - onPercentageSet?.(percentage) + const newTotalAllocation = (BigInt(balance ?? 0n) * BigInt(percentage)) / BigInt(100) + updateTotalAllocation(newTotalAllocation) setActiveButton(index) + const allocationValue = allocationCount > 0 ? newTotalAllocation / BigInt(allocationCount) : 0n + + updateAllocations(Array(allocationCount).fill(allocationValue)) } + + const handleOnChange = (value: string) => { + updateTotalAllocation(parseEther(value)) + } + return (
@@ -49,10 +52,14 @@ export const AllocationAmount = ({ balance, errorMessage, onPercentageSet }: All labelProps={{ className: 'text-base leading-4 font-normal' }} name="allocated-amount" fullWidth - onChange={onChange} - value={allocatedAmount.toString()} - errorMessage={errorMessage} - hint={hint} + onChange={handleOnChange} + value={formatEther(totalAllocation)} + errorMessage={ + cumulativeAllocation > totalAllocation && cumulativeAllocation < balance + ? ALLOCATION_EXCEED_AMOUNT_ERROR + : '' + } + hint={Number(totalAllocation - cumulativeAllocation) < 0 ? : undefined} />
diff --git a/src/app/collective-rewards/allocations/components/AllocationMetrics.tsx b/src/app/collective-rewards/allocations/components/AllocationMetrics.tsx new file mode 100644 index 00000000..04b6529b --- /dev/null +++ b/src/app/collective-rewards/allocations/components/AllocationMetrics.tsx @@ -0,0 +1,68 @@ +'use client' + +import { Paragraph } from '@/components/Typography' +import { useContext } from 'react' +import { formatEther } from 'viem' +import { AllocationsContext } from '@/app/collective-rewards/allocations/context' + +type ValueProps = { + value: string +} + +type MetricsProps = ValueProps & { + name: string +} + +const Metric = ({ name, value }: MetricsProps) => { + return ( +
+ {name} + {value} +
+ ) +} + +const Column = ({ children }: { children: React.ReactNode }) => { + return
{children}
+} + +const Balance = ({ value }: ValueProps) => { + return +} + +const AllocatedAmount = ({ value }: ValueProps) => { + return +} + +const UnallocatedAmount = ({ value }: ValueProps) => { + return +} + +export const AllocationMetrics = () => { + const { + initialState: { + backer: { totalAllocation, balance }, + }, + } = useContext(AllocationsContext) + + const balanceValue = `${formatEther(balance)} stRIF` + + const allocatedAmountValue = `${formatEther(totalAllocation)} stRIF` + + const unallocatedAmount = formatEther(balance - totalAllocation) + + const unallocatedAmountValue = `${unallocatedAmount} stRIF` + return ( +
+ + + + + + + + + +
+ ) +} diff --git a/src/app/collective-rewards/allocations/components/BuilderAllocation.tsx b/src/app/collective-rewards/allocations/components/BuilderAllocation.tsx new file mode 100644 index 00000000..a6fa5048 --- /dev/null +++ b/src/app/collective-rewards/allocations/components/BuilderAllocation.tsx @@ -0,0 +1,51 @@ +import { AllocationsContext } from '@/app/collective-rewards/allocations/context' +import { Builder } from '@/app/collective-rewards/types' +import { Input } from '@/components/Input' +import { Slider } from '@/components/Slider' +import { Label } from '@/components/Typography' +import { useContext } from 'react' +import { formatEther, parseEther } from 'viem' +import { BuilderAllocationHeader, BuilderAllocationHeaderProps } from './BuilderAllocationHeader' + +export type BuilderAllocationProps = BuilderAllocationHeaderProps & + Pick & { + index: number + currentAllocation: bigint + } + +export const BuilderAllocation = (builder: BuilderAllocationProps) => { + const { + state: { + backer: { totalAllocation, cumulativeAllocation }, + }, + actions: { updateAllocation }, + } = useContext(AllocationsContext) + const allocationLeft = totalAllocation - cumulativeAllocation + const { currentAllocation, kickback, address } = builder + const onInputChange = (value: string) => { + updateAllocation(builder.index, parseEther(value)) + } + + const onSliderValueChange = (value: number[]) => { + updateAllocation(builder.index, BigInt(value[0])) + } + + return ( +
+ + + 0 ? formatEther(allocationLeft) : '0'} stRIF`} + onChange={onInputChange} + value={formatEther(currentAllocation)} + /> + +
+ ) +} diff --git a/src/app/collective-rewards/allocations/BuilderAllocationHeader.tsx b/src/app/collective-rewards/allocations/components/BuilderAllocationHeader.tsx similarity index 63% rename from src/app/collective-rewards/allocations/BuilderAllocationHeader.tsx rename to src/app/collective-rewards/allocations/components/BuilderAllocationHeader.tsx index 9437f299..05e1e22f 100644 --- a/src/app/collective-rewards/allocations/BuilderAllocationHeader.tsx +++ b/src/app/collective-rewards/allocations/components/BuilderAllocationHeader.tsx @@ -1,17 +1,19 @@ +import { Builder, BuilderStatusActive, BuilderStatusShown } from '@/app/collective-rewards/types' +import { crStatusColorClasses } from '@/app/collective-rewards/user' import { AddressOrAlias } from '@/components/Address' import { Badge } from '@/components/Badge' import { Jdenticon } from '@/components/Header/Jdenticon' import { Paragraph, Typography } from '@/components/Typography' -import { BuilderStatusActive } from '../types' -import { crStatusColorClasses } from '../user' -import { BuilderAllocationProps } from './types' +import { FC } from 'react' -export const BuilderAllocationHeader = ({ +export type BuilderAllocationHeaderProps = Pick + +export const BuilderAllocationHeader: FC = ({ address, builderName, status, joiningDate, -}: BuilderAllocationProps) => { +}) => { return (
@@ -20,7 +22,10 @@ export const BuilderAllocationHeader = ({ {status !== BuilderStatusActive && ( - + )} {status === BuilderStatusActive && ( Joined {joiningDate} diff --git a/src/app/collective-rewards/allocations/Header.tsx b/src/app/collective-rewards/allocations/components/Header.tsx similarity index 100% rename from src/app/collective-rewards/allocations/Header.tsx rename to src/app/collective-rewards/allocations/components/Header.tsx diff --git a/src/app/collective-rewards/allocations/StakeHint.tsx b/src/app/collective-rewards/allocations/components/StakeHint.tsx similarity index 100% rename from src/app/collective-rewards/allocations/StakeHint.tsx rename to src/app/collective-rewards/allocations/components/StakeHint.tsx diff --git a/src/app/collective-rewards/allocations/components/index.ts b/src/app/collective-rewards/allocations/components/index.ts new file mode 100644 index 00000000..e471eb16 --- /dev/null +++ b/src/app/collective-rewards/allocations/components/index.ts @@ -0,0 +1,5 @@ +export * from './AllocationAmount' +export * from './AllocationMetrics' +export * from './BuilderAllocation' +export * from './Header' +export * from './StakeHint' diff --git a/src/app/collective-rewards/allocations/context/AllocationsContext.tsx b/src/app/collective-rewards/allocations/context/AllocationsContext.tsx new file mode 100644 index 00000000..486c4aa7 --- /dev/null +++ b/src/app/collective-rewards/allocations/context/AllocationsContext.tsx @@ -0,0 +1,249 @@ +import { + useBackerTotalAllocation, + useGetAllAllocationOf, + useGetVotingPower, +} from '@/app/collective-rewards/allocations/hooks' +import { Builder, BuilderInfo } from '@/app/collective-rewards/types' +import { useBuilderContext, useGetBuilders } from '@/app/collective-rewards/user' +import { createContext, FC, ReactNode, useEffect, useMemo, useState } from 'react' +import { zeroAddress } from 'viem' +import { useAccount } from 'wagmi' +import { createActions } from './allocationsActions' + +export type Allocations = Record + +export interface Backer { + totalAllocation: bigint + balance: bigint + allocationCount: number + cumulativeAllocation: bigint +} + +type AllocationsContextValue = { + selections: number[] + allocations: Allocations + backer: Backer + isContextLoading: boolean + contextError: Error | null + getBuilder: (index: number) => Builder | null +} + +export type AllocationsActions = { + toggleSelectedBuilder: (builderIndex: number) => void + updateAllocation: (builderIndex: number, value: bigint) => void + updateAllocations: (values: bigint[]) => void + updateTotalAllocation: (value: bigint) => void + resetAllocations: () => void +} + +export type InitialState = Pick + +type AllocationsContext = { + initialState: InitialState + state: AllocationsContextValue + actions: AllocationsActions +} + +const DEFAULT_CONTEXT: AllocationsContext = { + initialState: { + backer: { + balance: BigInt(0), + totalAllocation: BigInt(0), + allocationCount: 0, + cumulativeAllocation: BigInt(0), + }, + allocations: {}, + }, + state: { + selections: [], + allocations: {}, + backer: { + balance: BigInt(0), + totalAllocation: BigInt(0), + allocationCount: 0, + cumulativeAllocation: BigInt(0), + }, + isContextLoading: true, + contextError: null, + getBuilder: () => null, + }, + actions: { + toggleSelectedBuilder: () => {}, + updateAllocation: () => {}, + updateAllocations: () => {}, + updateTotalAllocation: () => {}, + resetAllocations: () => {}, + }, +} + +export const AllocationsContext = createContext(DEFAULT_CONTEXT) + +export const AllocationsContextProvider: FC<{ children: ReactNode }> = ({ children }) => { + const { address: backerAddress } = useAccount() + + /** + * Selections are the indexes of the builders that the backer has selected + */ + const [selections, setSelections] = useState(DEFAULT_CONTEXT.state.selections) + + /** + * Allocations are the amounts that the backer has allocated to each builder either in the current session or in the past + * The key is the index of the builder + * The value is the amount allocated + */ + const [allocations, setAllocations] = useState(DEFAULT_CONTEXT.state.allocations) + + /** + * Cumulative allocation is the total amount that the backer has allocated to all builders in current session + */ + const [isContextLoading, setIsContextLoading] = useState(DEFAULT_CONTEXT.state.isContextLoading) + const [contextError, setContextError] = useState(DEFAULT_CONTEXT.state.contextError) + + const [backer, setBacker] = useState(DEFAULT_CONTEXT.state.backer) + + // TODO: review this part + const { + data: buildersFromContext, + isLoading: isLoadingBuilders, + error: buildersError, + } = useBuilderContext() + const builders = buildersFromContext.map( + ({ builderName, status, address, gauge, joiningDate }) => + ({ + builderName, + status, + address, + gauge, + joiningDate, + // TODO: retrieve the kickback + kickback: 0, + }) as Builder, + ) + + const { + data: allAllocations, + isLoading: isAllAllocationsLoading, + error: allAllocationsError, + } = useGetAllAllocationOf( + backerAddress ?? zeroAddress, + builders.map(builder => builder.gauge), + ) + + const { + data: totalAllocation, + isLoading: isTotalAllocationLoading, + error: totalAllocationError, + } = useBackerTotalAllocation(backerAddress ?? zeroAddress) + + const { data: votingPower, isLoading: isVotingPowerLoading, error: votingPowerError } = useGetVotingPower() + + useEffect(() => { + if (isContextLoading) { + return + } + if (!backerAddress || !allAllocations) { + return + } + + const [allocations, newCumulativeAllocation] = createInitialAllocations(allAllocations, selections) + + setAllocations(allocations) + setBacker(prevBacker => ({ + ...prevBacker, + allocationCount: builders.length, + cumulativeAllocation: newCumulativeAllocation, + })) + }, [allAllocations, backerAddress, selections, isContextLoading, builders.length]) + + useEffect(() => { + if (totalAllocation) { + setBacker(prevBacker => ({ + ...prevBacker, + totalAllocation, + })) + } + }, [totalAllocation]) + + useEffect(() => { + if (votingPower) { + setBacker(prevBacker => ({ + ...prevBacker, + balance: votingPower, + })) + } + }, [votingPower]) + + useEffect(() => { + setContextError(buildersError ?? allAllocationsError ?? totalAllocationError ?? votingPowerError) + }, [allAllocationsError, buildersError, totalAllocationError, votingPowerError]) + + useEffect(() => { + setIsContextLoading( + isLoadingBuilders || isAllAllocationsLoading || isTotalAllocationLoading || isVotingPowerLoading, + ) + }, [isLoadingBuilders, isAllAllocationsLoading, isTotalAllocationLoading, isVotingPowerLoading]) + + const initialState: InitialState = useMemo(() => { + if (isContextLoading) { + return { + backer: DEFAULT_CONTEXT.initialState.backer, + allocations: DEFAULT_CONTEXT.initialState.allocations, + } + } + + const [initialAllocations, initialCumulativeAllocations] = createInitialAllocations( + allAllocations || [], + selections, + ) + + return { + backer: { + balance: votingPower ?? BigInt(0), + totalAllocation: totalAllocation ?? BigInt(0), + allocationCount: builders.length, + cumulativeAllocation: initialCumulativeAllocations, + }, + allocations: initialAllocations, + } + }, [allAllocations, builders, totalAllocation, votingPower, isContextLoading, selections]) + + const state: AllocationsContextValue = { + selections, + allocations, + backer, + isContextLoading, + contextError, + getBuilder: (index: number) => (index >= 0 && index < builders.length ? builders[index] : null), + } + + const actions: AllocationsActions = useMemo( + () => createActions(setSelections, setAllocations, setBacker, initialState), + [initialState], + ) + + return ( + + {children} + + ) +} + +function createInitialAllocations(allAllocations: bigint[], selections: number[]): [Allocations, bigint] { + return allAllocations.reduce( + (acc, allocation, index) => { + if (allocation || selections.includes(index)) { + acc[0][index] = allocation + acc[1] += allocation + } + + return acc + }, + [{} as Allocations, BigInt(0)], + ) +} diff --git a/src/app/collective-rewards/allocations/context/allocationsActions.ts b/src/app/collective-rewards/allocations/context/allocationsActions.ts new file mode 100644 index 00000000..668ef777 --- /dev/null +++ b/src/app/collective-rewards/allocations/context/allocationsActions.ts @@ -0,0 +1,57 @@ +import { Dispatch, SetStateAction } from 'react' +import { Allocations, AllocationsActions, Backer, InitialState } from './AllocationsContext' + +export const createActions = ( + setSelections: Dispatch>, + setAllocations: Dispatch>>, + setBacker: Dispatch>, + initialState: InitialState, +): AllocationsActions => ({ + toggleSelectedBuilder: (builderIndex: number) => { + setSelections(prevSelections => + prevSelections.includes(builderIndex) + ? prevSelections.filter(index => index !== builderIndex) + : [...prevSelections, builderIndex], + ) + }, + updateAllocation: (builderIndex: number, value: bigint) => { + setAllocations(prevAllocations => { + const newAllocations = { ...prevAllocations, [builderIndex]: value } + + setBacker(prevBacker => ({ + ...prevBacker, + cumulativeAllocation: + prevBacker.cumulativeAllocation + value - (prevAllocations[builderIndex] ?? BigInt(0)), + })) + + return newAllocations + }) + }, + updateAllocations: (values: bigint[]) => { + const [newAllocations, newCumulativeAllocation] = values.reduce( + (acc, value, index) => { + acc[0][index] = value + acc[1] += value + + return acc + }, + [{} as Allocations, BigInt(0)], + ) + setAllocations(newAllocations) + + setBacker(prevBacker => ({ + ...prevBacker, + cumulativeAllocation: newCumulativeAllocation, + })) + }, + updateTotalAllocation: (value: bigint) => { + setBacker(prevBacker => ({ + ...prevBacker, + totalAllocation: value, + })) + }, + resetAllocations: () => { + setAllocations(initialState.allocations) + setBacker(initialState.backer) + }, +}) diff --git a/src/app/collective-rewards/allocations/context/index.ts b/src/app/collective-rewards/allocations/context/index.ts new file mode 100644 index 00000000..0697bbaa --- /dev/null +++ b/src/app/collective-rewards/allocations/context/index.ts @@ -0,0 +1 @@ +export * from './AllocationsContext' diff --git a/src/app/collective-rewards/allocations/hooks/index.ts b/src/app/collective-rewards/allocations/hooks/index.ts new file mode 100644 index 00000000..3dffbdee --- /dev/null +++ b/src/app/collective-rewards/allocations/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './useBackerTotalAllocation' +export * from './useGetVotingPower' diff --git a/src/app/collective-rewards/allocations/useBackerTotalAllocation.tsx b/src/app/collective-rewards/allocations/hooks/useBackerTotalAllocation.ts similarity index 51% rename from src/app/collective-rewards/allocations/useBackerTotalAllocation.tsx rename to src/app/collective-rewards/allocations/hooks/useBackerTotalAllocation.ts index b7239801..f686e97d 100644 --- a/src/app/collective-rewards/allocations/useBackerTotalAllocation.tsx +++ b/src/app/collective-rewards/allocations/hooks/useBackerTotalAllocation.ts @@ -1,8 +1,9 @@ import { BackersManagerAbi } from '@/lib/abis/v2/BackersManagerAbi' +import { GaugeAbi } from '@/lib/abis/v2/GaugeAbi' import { AVERAGE_BLOCKTIME } from '@/lib/constants' import { BackersManagerAddress } from '@/lib/contracts' import { Address } from 'viem' -import { useReadContract } from 'wagmi' +import { useReadContract, useReadContracts } from 'wagmi' export const useBackerTotalAllocation = (backer: Address) => { const { data, isLoading, error } = useReadContract({ @@ -22,3 +23,23 @@ export const useBackerTotalAllocation = (backer: Address) => { error, } } + +export const useGetAllAllocationOf = (backer: Address, gauges: Address[]) => { + const { data, isLoading, error } = useReadContracts({ + contracts: gauges.map(gauge => ({ + abi: GaugeAbi, + address: gauge, + functionName: 'allocationOf', + args: [backer], + })), + query: { + refetchInterval: AVERAGE_BLOCKTIME, + }, + }) + + return { + data: data?.map(({ result }) => result as bigint), + isLoading, + error, + } +} diff --git a/src/app/collective-rewards/allocations/hooks/useGetVotingPower.ts b/src/app/collective-rewards/allocations/hooks/useGetVotingPower.ts new file mode 100644 index 00000000..e46d2b88 --- /dev/null +++ b/src/app/collective-rewards/allocations/hooks/useGetVotingPower.ts @@ -0,0 +1,24 @@ +import { StRIFTokenAbi } from '@/lib/abis/StRIFTokenAbi' +import { AVERAGE_BLOCKTIME } from '@/lib/constants' +import { tokenContracts } from '@/lib/contracts' +import { useAccount, useReadContract, useReadContracts } from 'wagmi' + +export const useGetVotingPower = () => { + const { address } = useAccount() + + const { data, isLoading, error } = useReadContract({ + abi: StRIFTokenAbi, + address: tokenContracts.stRIF, + functionName: 'balanceOf', + args: [address!], + query: { + refetchInterval: AVERAGE_BLOCKTIME, + }, + }) + + return { + data, + isLoading, + error, + } +} diff --git a/src/app/collective-rewards/allocations/index.ts b/src/app/collective-rewards/allocations/index.ts new file mode 100644 index 00000000..1db759f2 --- /dev/null +++ b/src/app/collective-rewards/allocations/index.ts @@ -0,0 +1,2 @@ +export * from './context' +export * from './hooks' diff --git a/src/app/collective-rewards/allocations/page.tsx b/src/app/collective-rewards/allocations/page.tsx index 6323acda..70d1ec44 100644 --- a/src/app/collective-rewards/allocations/page.tsx +++ b/src/app/collective-rewards/allocations/page.tsx @@ -1,77 +1,36 @@ 'use client' +import { Button } from '@/components/Button' import { MainContainer } from '@/components/MainContainer/MainContainer' import { Typography } from '@/components/Typography' -import { AllocationAmount } from './AllocationAmount' -import { AllocationMetrics } from './AllocationMetrics' -import { BuilderAllocation } from './BuilderAllocation' -import { Header } from './Header' -import { BuilderAllocationProps } from './types' -import { useState } from 'react' - -const ALLOCATION_EXCEED_AMOUNT_ERROR = 'Builder allocations exceeds amount to allocate' +import { useRouter } from 'next/navigation' +import { useContext } from 'react' +import { Builder } from '../types' +import { + AllocationAmount, + AllocationMetrics, + BuilderAllocation, + BuilderAllocationProps, + Header, +} from './components' +import { AllocationsContext } from './context' export default function Allocations() { - /* TODO: Error message is set when - * - the cumulative amount exceeds the allocation amount (ALLOCATION_EXCEED_AMOUNT_ERROR) - */ - const [errorMessage, setErrorMessage] = useState('') - const builders: BuilderAllocationProps[] = [ - { - builderName: 'Builder 1', - address: '0x1234567890abcdef1234567890abcdef12345678', - status: 'Active', - joiningDate: '2021-10-01', - allocationLeft: 50000000000000000000n, - backerRewards: 10, - currentAllocation: 70, - }, - { - builderName: 'Builder 2', - address: '0x1234567890abcdef1234567890abcdef12345678', - status: 'Active', - joiningDate: '2021-10-01', - allocationLeft: 50000000000000000000n, - backerRewards: 8, - currentAllocation: 50, - }, - { - builderName: 'Builder 3', - address: '0x1234567890abcdef1234567890abcdef12345678', - status: 'Deactivated', - joiningDate: '2021-10-01', - allocationLeft: 50000000000000000000n, - backerRewards: 100, - currentAllocation: 20, - }, - { - builderName: 'Builder 4', - address: '0x1234567890abcdef1234567890abcdef12345678', - status: 'Paused', - joiningDate: '2021-10-01', - allocationLeft: 50000000000000000000n, - backerRewards: 99, - currentAllocation: 100, - }, - { - builderName: 'Builder 5', - address: '0x1234567890abcdef1234567890abcdef12345678', - status: 'Deactivated', - joiningDate: '2021-10-01', - allocationLeft: 50000000000000000000n, - backerRewards: 99, - currentAllocation: 100, - }, - { - builderName: 'Builder 6', - address: '0x1234567890abcdef1234567890abcdef12345678', - status: 'Active', - joiningDate: '2021-10-01', - allocationLeft: 50000000000000000000n, - backerRewards: 99, - currentAllocation: 100, - }, - ] + const router = useRouter() + const { + state: { allocations, getBuilder }, + actions: { resetAllocations }, + } = useContext(AllocationsContext) + + const saveAllocations = () => { + // TODO: save current allocations + } + + const cancel = () => { + resetAllocations() + router.back() + } + return (
@@ -80,20 +39,45 @@ export default function Allocations() {
- +
Selected Builders
- {builders.map((builder, index) => ( - - ))} + {Object.entries(allocations).map(([key, currentAllocation]) => { + const index = Number(key) + const builderInfo = getBuilder(index) as Builder + const builder: BuilderAllocationProps = { + ...builderInfo, + index, + currentAllocation, + } + return + })} +
+
+
+ {/* TODO: review disabled statuses */} + + +
+ +
diff --git a/src/app/collective-rewards/allocations/types.ts b/src/app/collective-rewards/allocations/types.ts deleted file mode 100644 index 9d2242ee..00000000 --- a/src/app/collective-rewards/allocations/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Address } from 'viem' -import { BuilderStatusShown } from '../types' - -export type BuilderStatus = BuilderStatusShown | 'Paused' | 'Deactivated' - -export type BuilderAllocationProps = { - builderName: string - address: Address - status: BuilderStatus - joiningDate: string - allocationLeft: BigInt - // TODO: what's the value we expect here? (e.g. 8% or 8.123456%) - backerRewards: number - currentAllocation: number -} diff --git a/src/app/collective-rewards/context/PaginatedDataContext.tsx b/src/app/collective-rewards/context/PaginatedDataContext.tsx new file mode 100644 index 00000000..0ba1ca86 --- /dev/null +++ b/src/app/collective-rewards/context/PaginatedDataContext.tsx @@ -0,0 +1,91 @@ +import { Context, createContext, FC, useContext, useEffect, useMemo, useState } from 'react' + +type PaginationConfig = { + data: T[] + pageSize: number + currentPage: number +} + +type State = PaginationConfig & { + pageCount: number +} + +type Actions = { + getDataIndex: (index: number) => number + getPage: (page?: number) => T[] + updateData: (data: T[]) => void + updatePageSize: (pageSize: number) => void + updateCurrentPage: (currentPage: number) => void +} + +type PageContext = State & Actions + +export const PaginatedDataContext = createContext>({ + data: [], + pageSize: 0, + currentPage: 0, + pageCount: 0, + getDataIndex: () => 0, + getPage: () => [], + updateData: () => {}, + updatePageSize: () => {}, + updateCurrentPage: () => {}, +}) + +export const usePaginatedDataContext = () => { + const context = useContext>(PaginatedDataContext) + if (!context) { + throw new Error('usePaginatedDataContext must be used within a PaginatedDataContextProvider') + } + return context +} + +type PaginatedDataContextProviderProps = { + children: React.ReactNode + config: PaginationConfig +} + +export const PaginatedDataContextProvider: FC = ({ children, config }) => { + const [data, setData] = useState(config.data) + const [pageSize, setPageSize] = useState(config.pageSize) + const [currentPage, setCurrentPage] = useState(config.currentPage) + + const state = { + data, + pageSize, + currentPage, + pageCount: useMemo(() => Math.ceil(data.length / pageSize), [data, pageSize]), + } + const actions = { + getDataIndex: (pagedIndex: number) => pagedIndex + pageSize * currentPage, + getPage: (page?: number) => { + const pageIndex = page ?? currentPage + const start = pageIndex * pageSize + const end = start + pageSize + + return data.slice(start, end) + }, + updateData: (newData: typeof data) => setData(newData), + updatePageSize: (newPageSize: number) => setPageSize(newPageSize), + updateCurrentPage: (newCurrentPage: number) => setCurrentPage(newCurrentPage), + } + + return ( + {children} + ) +} + +export const withPagination = (Component: FC) => { + const PaginatedComponent: FC< + PaginationConfig & { + props: TProps + } + > = ({ props, ...rest }) => { + return ( + + + + ) + } + return PaginatedComponent +} diff --git a/src/app/collective-rewards/leaderboard/BuildersLeaderBoardTable.tsx b/src/app/collective-rewards/leaderboard/BuildersLeaderBoardTable.tsx index 79934c85..da9dd9f9 100644 --- a/src/app/collective-rewards/leaderboard/BuildersLeaderBoardTable.tsx +++ b/src/app/collective-rewards/leaderboard/BuildersLeaderBoardTable.tsx @@ -21,6 +21,7 @@ import { } from '@/app/collective-rewards/shared' import { getAddress } from 'viem' import { tokenContracts } from '@/lib/contracts' +import { useRouter } from 'next/navigation' enum RewardsColumnKeyEnum { builder = 'builder', @@ -174,9 +175,10 @@ const BuildersLeaderBoardTable: FC = ({ tokens, c } export const BuildersLeaderBoard = () => { + const router = useRouter() + const onManageAllocations = () => { - // TODO: fill the allocation context if necessary and change the route - console.log('Manage allocations') + router.push('collective-rewards/allocations') } // TODO: check where to store this information diff --git a/src/app/collective-rewards/page.tsx b/src/app/collective-rewards/page.tsx index eccdbd8f..a50083ad 100644 --- a/src/app/collective-rewards/page.tsx +++ b/src/app/collective-rewards/page.tsx @@ -2,7 +2,7 @@ import { BuildersLeaderBoard } from '@/app/collective-rewards/leaderboard' import { Metrics } from '@/app/collective-rewards/metrics' -import { WhitelistContextProviderWithBuilders, WhitelistSection } from '@/app/collective-rewards/whitelist' +import { WhitelistContextProvider, WhitelistSection } from '@/app/collective-rewards/whitelist' import { MainContainer } from '@/components/MainContainer/MainContainer' export default function BuildersIncentiveMarket() { @@ -10,9 +10,9 @@ export default function BuildersIncentiveMarket() {
- + - +
diff --git a/src/app/collective-rewards/rewards/MyRewards.tsx b/src/app/collective-rewards/rewards/MyRewards.tsx index 8b1b52de..5a54cd69 100644 --- a/src/app/collective-rewards/rewards/MyRewards.tsx +++ b/src/app/collective-rewards/rewards/MyRewards.tsx @@ -48,7 +48,7 @@ export const Rewards: FC<{ builder: Address }> = ({ builder }) => { { - console.error('Not implemented') + router.push('/collective-rewards/allocations') }} title="Backer Rewards" subtext="Monitor your rewards balances and claim." diff --git a/src/app/collective-rewards/shared/components/Pagination.tsx b/src/app/collective-rewards/shared/components/Pagination.tsx new file mode 100644 index 00000000..7684ed9d --- /dev/null +++ b/src/app/collective-rewards/shared/components/Pagination.tsx @@ -0,0 +1,33 @@ +import { FC, useContext } from 'react' +import { PaginatedDataContext } from '@/app/collective-rewards/context/PaginatedDataContext' +import { PaginationButton } from './PaginationButton' +import { ChevronLeft, ChevronRight } from 'lucide-react' + +export const Pagination: FC = () => { + const { currentPage, pageCount, updateCurrentPage } = useContext(PaginatedDataContext) + + return ( +
+ } + onClick={() => updateCurrentPage(currentPage - 1)} + disabled={currentPage === 0} + /> + {Array(pageCount) + .fill(1) + .map((_, pageNumber) => ( + updateCurrentPage(pageNumber)} + text={pageNumber + 1} + isActive={pageNumber === currentPage} + /> + ))} + } + onClick={() => updateCurrentPage(currentPage + 1)} + disabled={currentPage === pageCount - 1} + /> +
+ ) +} diff --git a/src/app/collective-rewards/shared/components/PaginationButton.tsx b/src/app/collective-rewards/shared/components/PaginationButton.tsx new file mode 100644 index 00000000..2be3f259 --- /dev/null +++ b/src/app/collective-rewards/shared/components/PaginationButton.tsx @@ -0,0 +1,13 @@ +import { Button } from '@/components/Button' +import { FC, ReactNode } from 'react' + +export const PaginationButton: FC<{ + text: ReactNode + onClick: () => void + disabled?: boolean + isActive?: boolean +}> = ({ text, onClick, disabled, isActive }) => ( + +) diff --git a/src/app/collective-rewards/shared/components/index.ts b/src/app/collective-rewards/shared/components/index.ts index a86149cb..6a2dbd3a 100644 --- a/src/app/collective-rewards/shared/components/index.ts +++ b/src/app/collective-rewards/shared/components/index.ts @@ -1 +1,3 @@ +export * from './Pagination' +export * from './PaginationButton' export * from './Table' diff --git a/src/app/collective-rewards/types.ts b/src/app/collective-rewards/types.ts index 24d879cd..7580dff5 100644 --- a/src/app/collective-rewards/types.ts +++ b/src/app/collective-rewards/types.ts @@ -3,10 +3,14 @@ import { ProposalState } from '@/shared/types' import { Address } from 'viem' export const BuilderStatusActive = 'Active' +export const BuilderStatusPaused = 'Paused' +export const BuilderStatusDeactivated = 'Deactivated' export const BuilderStatusInProgress = 'In progress' export const BuilderStatusProposalCreatedMVP = 'In progress - mvp' export const builderStatusOptions = [ BuilderStatusActive, + BuilderStatusPaused, + BuilderStatusDeactivated, BuilderStatusInProgress, BuilderStatusProposalCreatedMVP, ] as const @@ -21,4 +25,14 @@ export type BuilderInfo = { gauge: Address } +// TODO: refactor BuilderInfo & BuilderProposal +export type Builder = { + address: Address + status: BuilderStatus + gauge: Address + kickback: number + builderName: string + joiningDate: string +} + export type ProposalsToState = Record diff --git a/src/app/collective-rewards/user/components/Button/BecomeABuilderButton.tsx b/src/app/collective-rewards/user/components/Button/BecomeABuilderButton.tsx index 5647a610..29693082 100644 --- a/src/app/collective-rewards/user/components/Button/BecomeABuilderButton.tsx +++ b/src/app/collective-rewards/user/components/Button/BecomeABuilderButton.tsx @@ -1,4 +1,3 @@ -import { BuilderStatus } from '@/app/collective-rewards/allocations/types' import { BuilderInfo, BuilderStatusShown } from '@/app/collective-rewards/types' import { BuilderContextProvider, useBuilderContext } from '@/app/collective-rewards/user' import { useAlertContext } from '@/app/providers/AlertProvider' @@ -25,7 +24,10 @@ const BuilderRegistrationButton = () => { ) } -export const crStatusColorClasses: Record['className']> = { +export const crStatusColorClasses: Record< + BuilderStatusShown, + HtmlHTMLAttributes['className'] +> = { Active: 'bg-[#DBFEE5] text-secondary', 'In progress': 'bg-[#4B5CF0] color-text-primary', Paused: 'bg-[#F9E1FF] text-secondary', @@ -45,6 +47,8 @@ const StatusBadge: FC = ({ builderStatus }) => { return { 'In progress': InProgressComponent, Active: WhitelistedComponent, + Paused: InProgressComponent, // TODO: Change to PausedComponent + Deactivated: InProgressComponent, // TODO: Change to DeactivatedComponent undefined: BuilderRegistrationButton, }[builderStatus as BuilderStatusShown] } @@ -79,9 +83,5 @@ export const BecomeABuilderHandler = ({ address }: { address: Address }) => { } export const BecomeABuilderButton = ({ address }: { address: Address }) => { - return ( - - - - ) + return } diff --git a/src/app/collective-rewards/user/context/BuilderContext.tsx b/src/app/collective-rewards/user/context/BuilderContext.tsx index b1ae7b35..cd97795d 100644 --- a/src/app/collective-rewards/user/context/BuilderContext.tsx +++ b/src/app/collective-rewards/user/context/BuilderContext.tsx @@ -1,6 +1,7 @@ import { createContext, FC, ReactNode, useContext, useMemo } from 'react' import { Address } from 'viem' import { + BuilderInfo, BuilderStatus, BuilderStatusProposalCreatedMVP, BuilderStatusShown, @@ -12,6 +13,7 @@ import { DateTime } from 'luxon' import { splitCombinedName } from '@/app/proposals/shared/utils' import { withPricesContextProvider } from '@/shared/context/PricesContext' +// TODO: rename BuilderProposal and perhaps rewrite the type to Modify export type BuilderProposal = { builderName: string status: BuilderStatusShown @@ -57,7 +59,7 @@ export const BuilderContextProvider: FC = ({ children }) = error: proposalsStateMapError, } = useGetProposalsState(buildersProposals) - const filteredBuilders = useMemo(() => { + const builderWithProposal = useMemo(() => { return builders.reduce((acc, builder) => { const { status, address, gauge } = builder const proposal = getMostAdvancedProposal(builder, proposalsStateMap) @@ -90,11 +92,11 @@ export const BuilderContextProvider: FC = ({ children }) = const error = buildersError ?? proposalsStateMapError const getBuilderByAddress = (address: Address): BuilderProposal | undefined => { - return filteredBuilders[address] + return builderWithProposal[address] } const valueOfContext: BuilderContextValue = { - data: Object.values(filteredBuilders), + data: Object.values(builderWithProposal), isLoading, error, getBuilderByAddress, diff --git a/src/app/collective-rewards/user/hooks/useGetBuilders.ts b/src/app/collective-rewards/user/hooks/useGetBuilders.ts index a34ed37f..bc25dce9 100644 --- a/src/app/collective-rewards/user/hooks/useGetBuilders.ts +++ b/src/app/collective-rewards/user/hooks/useGetBuilders.ts @@ -5,6 +5,8 @@ import { BuilderStatusInProgress, BuilderStatusProposalCreatedMVP, } from '@/app/collective-rewards/types' +import { useGetGaugesArray } from '@/app/collective-rewards/user/hooks/useGetGaugesArray' +import { BuilderStateStruct } from '@/app/collective-rewards/utils/getBuilderGauge' import { useFetchCreateBuilderProposals } from '@/app/proposals/hooks/useFetchLatestProposals' import { BuilderRegistryAbi } from '@/lib/abis/v2/BuilderRegistryAbi' import { AVERAGE_BLOCKTIME } from '@/lib/constants' @@ -12,8 +14,6 @@ import { BackersManagerAddress } from '@/lib/contracts' import { useMemo } from 'react' import { Address, getAddress } from 'viem' import { useReadContracts } from 'wagmi' -import { useGetGaugesArray } from '@/app/collective-rewards/user/hooks/useGetGaugesArray' -import { BuilderStateStruct } from '@/app/collective-rewards/utils' export type BuilderLoader = { data?: BuilderInfo @@ -50,7 +50,6 @@ const getCombinedBuilderStatus = (builderState: BuilderStateStruct): BuilderStat // Default case: used to filter out builders return EXCLUDED_BUILDER_STATUS } - export const useGetBuilders = (): BuildersLoader => { /* * get Gauges @@ -126,6 +125,7 @@ export const useGetBuilders = (): BuildersLoader => { } = useFetchCreateBuilderProposals() const data = useMemo(() => { + // we need to add the kickback and the joining date return Object.entries(buildersProposalsMap ?? {}).map(([builder, proposals]) => ({ address: getAddress(builder), status: diff --git a/src/app/collective-rewards/whitelist/context/WhitelistContext.tsx b/src/app/collective-rewards/whitelist/context/WhitelistContext.tsx index 0441f7bb..cee83b67 100644 --- a/src/app/collective-rewards/whitelist/context/WhitelistContext.tsx +++ b/src/app/collective-rewards/whitelist/context/WhitelistContext.tsx @@ -38,7 +38,7 @@ interface WhitelistProviderProps { children: ReactNode } -const WhitelistContextProvider: FC = ({ children }) => { +export const WhitelistContextProvider: FC = ({ children }) => { const [search, setSearch] = useState('') const [filterBy, setFilterBy] = useState(initialFilterByValue) const { data, isLoading, error } = useGetFilteredBuilders({ builderName: search, status: filterBy }) diff --git a/src/app/collective-rewards/whitelist/hooks/useGetFilteredBuilders.ts b/src/app/collective-rewards/whitelist/hooks/useGetFilteredBuilders.ts index f70b0ffe..5d68f82f 100644 --- a/src/app/collective-rewards/whitelist/hooks/useGetFilteredBuilders.ts +++ b/src/app/collective-rewards/whitelist/hooks/useGetFilteredBuilders.ts @@ -3,14 +3,14 @@ import { BuilderStatusFilter } from '@/app/collective-rewards/whitelist' import { BuilderProposal, useBuilderContext } from '@/app/collective-rewards/user' type FetchWhitelistedBuildersFilter = { - builderName: string + builderName?: string status: BuilderStatusFilter } const lowerCaseCompare = (a: string, b: string) => a?.toLowerCase().includes(b?.toLowerCase()) export const useGetFilteredBuilders = ({ - builderName: filterBuilderName, + builderName: filterBuilderName = '', status: filterStatus, }: FetchWhitelistedBuildersFilter) => { const [data, setData] = useState([]) diff --git a/src/app/providers/ContextProviders.tsx b/src/app/providers/ContextProviders.tsx index 17519f95..b758801e 100644 --- a/src/app/providers/ContextProviders.tsx +++ b/src/app/providers/ContextProviders.tsx @@ -5,6 +5,8 @@ import { ReactNode } from 'react' import { WagmiProvider } from 'wagmi' import { AlertProvider } from './AlertProvider' import ErrorBoundary from '@/components/ErrorPage/ErrorBoundary' +import { AllocationsContextProvider } from '../collective-rewards/allocations' +import { BuilderContextProviderWithPrices } from '../collective-rewards/user' interface Props { children: ReactNode @@ -16,7 +18,11 @@ export const ContextProviders = ({ children }: Props) => { - {children} + + + {children} + + diff --git a/src/components/Slider/Slider.tsx b/src/components/Slider/Slider.tsx index e04f3fa9..c4babe3c 100644 --- a/src/components/Slider/Slider.tsx +++ b/src/components/Slider/Slider.tsx @@ -7,21 +7,12 @@ export const Slider = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >((props, forwardedRef) => { - const initialValue = props.value || props.defaultValue - const [value, setValue] = React.useState(initialValue) - - const onPercentageClick = (percentage: number) => { - setValue([percentage]) - console.log('percentage', percentage) - } - return ( <> setValue(nextValue)} > @@ -32,14 +23,13 @@ export const Slider = React.forwardRef< onPercentageClick(v)} > {v}% ))} - {value?.map((_, i) => ( + {props.value?.map((_, i) => (