diff --git a/apps/web/src/views/GaugesVoting/components/Table/VoteTable/TableRow.tsx b/apps/web/src/views/GaugesVoting/components/Table/VoteTable/TableRow.tsx index 5fa9dcfb81e83..0504097ea2876 100644 --- a/apps/web/src/views/GaugesVoting/components/Table/VoteTable/TableRow.tsx +++ b/apps/web/src/views/GaugesVoting/components/Table/VoteTable/TableRow.tsx @@ -27,12 +27,19 @@ export const TableRow: React.FC = ({ data, vote = { ...DEFAULT_VOTE }, const { cakeLockedAmount } = useCakeLockStatus() const cakeLocked = useMemo(() => cakeLockedAmount > 0n, [cakeLockedAmount]) const userVote = useUserVote(data) - const { currentVoteWeight, currentVotePercent, previewVoteWeight, voteValue, voteLocked, willUnlock } = - useRowVoteState({ - data, - vote, - onChange, - }) + const { + currentVoteWeight, + currentVotePercent, + previewVoteWeight, + voteValue, + voteLocked, + willUnlock, + proxyVeCakeBalance, + } = useRowVoteState({ + data, + vote, + onChange, + }) const onMax = () => { onChange(vote, true) } @@ -48,12 +55,13 @@ export const TableRow: React.FC = ({ data, vote = { ...DEFAULT_VOTE }, { ...userVote, currentTimestamp: debugFormat(currentTimestamp), - nativeLasVoteTime: debugFormat(userVote?.lastVoteTime), + nativeLasVoteTime: debugFormat(userVote?.nativeLastVoteTime), proxyLastVoteTime: debugFormat(userVote?.proxyLastVoteTime), lastVoteTime: debugFormat(userVote?.lastVoteTime), end: debugFormat(userVote?.end), proxyEnd: debugFormat(userVote?.proxyEnd), nativeEnd: debugFormat(userVote?.nativeEnd), + proxyVeCakeBalance: proxyVeCakeBalance?.toString(), }, undefined, 2, diff --git a/apps/web/src/views/GaugesVoting/components/Table/VoteTable/hooks/useGaugeRows.ts b/apps/web/src/views/GaugesVoting/components/Table/VoteTable/hooks/useGaugeRows.ts index e3db38f78897f..982cd3f92ae9d 100644 --- a/apps/web/src/views/GaugesVoting/components/Table/VoteTable/hooks/useGaugeRows.ts +++ b/apps/web/src/views/GaugesVoting/components/Table/VoteTable/hooks/useGaugeRows.ts @@ -11,22 +11,25 @@ export const useGaugeRows = () => { const previousAccount = usePreviousValue(account) const { data: prevVotedGauges, refetch } = useUserVoteGauges() const [selectRowsHash, setSelectRowsHash] = useState([]) + const [initialed, setInitialed] = useState(false) const rows = useMemo(() => { return gauges?.filter((gauge) => selectRowsHash.includes(gauge.hash)) }, [gauges, selectRowsHash]) useEffect(() => { if (account !== previousAccount) { + setInitialed(false) setSelectRowsHash([]) } }, [account, previousAccount, selectRowsHash]) // add all gauges to selectRows when user has voted gauges useEffect(() => { - if (prevVotedGauges?.length && !selectRowsHash.length) { + if (prevVotedGauges?.length && !selectRowsHash.length && !initialed) { setSelectRowsHash(prevVotedGauges.map((gauge) => gauge.hash)) + setInitialed(true) } - }, [prevVotedGauges, selectRowsHash.length]) + }, [initialed, prevVotedGauges, selectRowsHash.length]) const onRowSelect = useCallback( (hash: Gauge['hash']) => { @@ -40,6 +43,7 @@ export const useGaugeRows = () => { ) return { + gauges, rows, isLoading, onRowSelect, diff --git a/apps/web/src/views/GaugesVoting/components/Table/VoteTable/hooks/useRowVoteState.ts b/apps/web/src/views/GaugesVoting/components/Table/VoteTable/hooks/useRowVoteState.ts index 7ee1e0dd05bab..f2f01c5fbad31 100644 --- a/apps/web/src/views/GaugesVoting/components/Table/VoteTable/hooks/useRowVoteState.ts +++ b/apps/web/src/views/GaugesVoting/components/Table/VoteTable/hooks/useRowVoteState.ts @@ -5,14 +5,16 @@ import { useVeCakeBalance } from 'hooks/useTokenBalance' import { useEffect, useMemo } from 'react' import { Hex } from 'viem' import { useCurrentBlockTimestamp } from 'views/CakeStaking/hooks/useCurrentBlockTimestamp' +import { useProxyVeCakeBalance } from 'views/CakeStaking/hooks/useProxyVeCakeBalance' import { useEpochVotePower } from 'views/GaugesVoting/hooks/useEpochVotePower' import { useUserVote } from 'views/GaugesVoting/hooks/useUserVote' -import { RowProps } from '../types' +import { DEFAULT_VOTE, RowProps } from '../types' export const useRowVoteState = ({ data, vote, onChange }: RowProps) => { const userVote = useUserVote(data) const voteLocked = userVote?.voteLocked const { balance: veCakeBalance } = useVeCakeBalance() + const { balance: proxyVeCakeBalance } = useProxyVeCakeBalance() const currentTimestamp = useCurrentBlockTimestamp() const epochVotePower = useEpochVotePower() const willUnlock = useMemo( @@ -52,18 +54,27 @@ export const useRowVoteState = ({ data, vote, onChange }: RowProps) => { const voteValue = useMemo(() => { if (voteLocked) return currentVotePercent ?? '' - return willUnlock ? '0' : vote?.power ?? '' + if (willUnlock) return '0' + if (vote?.power === DEFAULT_VOTE.power) return currentVotePercent ?? '' + return vote?.power ?? '' }, [voteLocked, currentVotePercent, willUnlock, vote?.power]) const previewVoteWeight = useMemo(() => { const p = Number(voteValue || 0) * 100 // const powerBN = new BN(epochVotePower.toString()) - const amount = getBalanceNumber(veCakeBalance.times(p).div(10000)) + let balance = veCakeBalance + if (userVote?.ignoredSide === 'proxy') { + balance = veCakeBalance.minus(proxyVeCakeBalance) + } + if (userVote?.ignoredSide === 'native') { + balance = proxyVeCakeBalance + } + const amount = getBalanceNumber(balance.times(p).div(10000)) if (amount === 0) return 0 if (amount < 1) return amount.toPrecision(2) return amount < 1000 ? amount.toFixed(2) : formatLocalisedCompactNumber(amount, true) - }, [veCakeBalance, voteValue]) + }, [voteValue, veCakeBalance, userVote?.ignoredSide, proxyVeCakeBalance]) // init vote value if still default useEffect(() => { @@ -83,5 +94,6 @@ export const useRowVoteState = ({ data, vote, onChange }: RowProps) => { voteValue, voteLocked, willUnlock, + proxyVeCakeBalance, } } diff --git a/apps/web/src/views/GaugesVoting/components/Table/VoteTable/index.tsx b/apps/web/src/views/GaugesVoting/components/Table/VoteTable/index.tsx index 7614e64799050..1a159c0b9caf6 100644 --- a/apps/web/src/views/GaugesVoting/components/Table/VoteTable/index.tsx +++ b/apps/web/src/views/GaugesVoting/components/Table/VoteTable/index.tsx @@ -21,6 +21,7 @@ import { useGaugesVotingCount } from 'views/CakeStaking/hooks/useGaugesVotingCou import { useCakeLockStatus } from 'views/CakeStaking/hooks/useVeCakeUserInfo' import { useEpochOnTally } from 'views/GaugesVoting/hooks/useEpochTime' import { useEpochVotePower } from 'views/GaugesVoting/hooks/useEpochVotePower' +import { useUserVoteSlopes } from 'views/GaugesVoting/hooks/useUserVoteGauges' import { useWriteGaugesVoteCallback } from 'views/GaugesVoting/hooks/useWriteGaugesVoteCallback' import { RemainingVotePower } from '../../RemainingVotePower' import { AddGaugeModal } from '../AddGauge/AddGaugeModal' @@ -31,6 +32,10 @@ import { ExpandRow, TableRow } from './TableRow' import { useGaugeRows } from './hooks/useGaugeRows' import { UserVote } from './types' +type GaugeWithDelta = Gauge & { + delta: bigint +} + const Scrollable = styled.div.withConfig({ shouldForwardProp: (prop) => !['expanded'].includes(prop) })<{ expanded: boolean }>` @@ -57,7 +62,8 @@ export const VoteTable = () => { return Object.values(votes).reduce((acc, cur) => acc + (cur?.locked ? Number(cur?.power) : 0), 0) }, [votes]) - const { rows, onRowSelect, refetch, isLoading } = useGaugeRows() + const { gauges, rows, onRowSelect, refetch, isLoading } = useGaugeRows() + const { data: slopes } = useUserVoteSlopes() const { isDesktop } = useMatchBreakpoints() const rowsWithLock = useMemo(() => { return rows?.map((row) => { @@ -101,8 +107,10 @@ export const VoteTable = () => { const { writeVote, isPending } = useWriteGaugesVoteCallback() const disabled = useMemo(() => { - const lockedSum = Object.values(votes).reduce((acc, cur) => acc + (cur?.locked ? Number(cur?.power) : 0), 0) - const newAddSum = Object.values(votes).reduce((acc, cur) => acc + (!cur?.locked ? Number(cur?.power) : 0), 0) + let lockedSum = Object.values(votes).reduce((acc, cur) => acc + (cur?.locked ? Number(cur?.power) : 0), 0) + let newAddSum = Object.values(votes).reduce((acc, cur) => acc + (!cur?.locked ? Number(cur?.power) : 0), 0) + lockedSum = Number(Number(lockedSum).toFixed(2)) + newAddSum = Number(Number(newAddSum).toFixed(2)) // voting ended if (onTally) return true @@ -120,26 +128,45 @@ export const VoteTable = () => { return Number(gaugesCount) - (rows?.length || 0) }, [gaugesCount, rows]) - const submitVote = useCallback(async () => { - const voteGauges = Object.values(votes) - .map((vote) => { - if (!vote.locked && Number(vote.power)) { - const row = rows?.find((r) => r.hash === vote.hash) + const sortedSubmitVotes = useMemo(() => { + const voteGauges = slopes + .map((slope) => { + const vote = votes[slope.hash] + // update vote power + if (vote && !vote?.locked) { + const row = gauges?.find((r) => r.hash === slope.hash) if (!row) return undefined + const currentPower = BigInt((Number(vote.power) * 100).toFixed(0)) + const { nativePower = 0, proxyPower = 0 } = slope || {} return { ...row, - weight: BigInt((Number(vote.power) * 100).toFixed(0)), + delta: currentPower - (BigInt(nativePower) + BigInt(proxyPower)), + weight: currentPower, + } + } + // vote deleted + if (!vote && (slope.proxyPower > 0 || slope.nativePower > 0)) { + const row = gauges?.find((r) => r.hash === slope.hash) + if (!row) return undefined + return { + ...row, + delta: 0n - (BigInt(slope.nativePower) + BigInt(slope.proxyPower)), + weight: 0n, } } return undefined }) - .filter((gauge: Gauge | undefined): gauge is Gauge => Boolean(gauge)) + .filter((gauge: GaugeWithDelta | undefined): gauge is GaugeWithDelta => Boolean(gauge)) + .sort((a, b) => (b.delta < a.delta ? 1 : -1)) + return voteGauges + }, [slopes, votes, gauges]) - await writeVote(voteGauges) + const submitVote = useCallback(async () => { + await writeVote(sortedSubmitVotes) await refetch() - }, [refetch, rows, votes, writeVote]) + }, [refetch, sortedSubmitVotes, writeVote]) - const gauges = isDesktop ? ( + const gaugesTable = isDesktop ? ( <> @@ -220,7 +247,7 @@ export const VoteTable = () => { onDismiss={() => setIsOpen(false)} /> - {gauges} + {gaugesTable} {rowsWithLock?.length && epochPower <= 0n && cakeLockedAmount > 0n ? ( diff --git a/apps/web/src/views/GaugesVoting/components/Table/VoteTable/types.ts b/apps/web/src/views/GaugesVoting/components/Table/VoteTable/types.ts index 343d43aec0eff..c6e3403261388 100644 --- a/apps/web/src/views/GaugesVoting/components/Table/VoteTable/types.ts +++ b/apps/web/src/views/GaugesVoting/components/Table/VoteTable/types.ts @@ -15,7 +15,7 @@ export type RowProps = { } & SpaceProps export const DEFAULT_VOTE: UserVote = { - power: '0', + power: 'DEFAULT', locked: false, hash: '0x', } diff --git a/apps/web/src/views/GaugesVoting/hooks/useUserVote.ts b/apps/web/src/views/GaugesVoting/hooks/useUserVote.ts index 4fce1af72f35d..d8f96c1d5fd89 100644 --- a/apps/web/src/views/GaugesVoting/hooks/useUserVote.ts +++ b/apps/web/src/views/GaugesVoting/hooks/useUserVote.ts @@ -7,6 +7,7 @@ import { Address, Hex, isAddressEqual, zeroAddress } from 'viem' import { useCurrentBlockTimestamp } from 'views/CakeStaking/hooks/useCurrentBlockTimestamp' import { useVeCakeUserInfo } from 'views/CakeStaking/hooks/useVeCakeUserInfo' import { usePublicClient } from 'wagmi' +import { useNextEpochStart } from './useEpochTime' export type VotedSlope = { hash: string @@ -28,6 +29,9 @@ export type VotedSlope = { end: bigint lastVoteTime: bigint voteLocked: boolean + ignoredSide?: 'native' | 'proxy' + ignoredSlope?: bigint + ignoredPower?: bigint } const max = (a: bigint, b: bigint) => (a > b ? a : b) @@ -39,7 +43,7 @@ export const useUserVote = (gauge?: Gauge, useProxyPool: boolean = true) => { const contract = useGaugesVotingContract() const { data: userInfo } = useVeCakeUserInfo() const currentTimestamp = useCurrentBlockTimestamp() - // const nextEpochStart = useNextEpochStart() + const nextEpochStart = useNextEpochStart() const { data } = useQuery( ['/vecake/userVoteSlopes', contract.address, gauge?.hash, account], @@ -76,9 +80,9 @@ export const useUserVote = (gauge?: Gauge, useProxyPool: boolean = true) => { allowFailure: false, }) const [ - [proxySlope, proxyPower, proxyEnd], + [_proxySlope, _proxyPower, proxyEnd], proxyLastVoteTime, - [nativeSlope, nativePower, nativeEnd], + [_nativeSlope, _nativePower, nativeEnd], nativeLastVoteTime, ] = response const proxyVoteLocked = dayjs @@ -89,6 +93,49 @@ export const useUserVote = (gauge?: Gauge, useProxyPool: boolean = true) => { .unix(Number(nativeLastVoteTime)) .add(10, 'day') .isAfter(dayjs.unix(currentTimestamp)) + let [nativeSlope, nativePower, proxySlope, proxyPower] = [_nativeSlope, _nativePower, _proxySlope, _proxyPower] + let ignoredSlope = 0n + let ignoredPower = 0n + let ignoredSide: 'native' | 'proxy' | undefined + // when native slope will expire before current epochEnd + // use proxy slope only + if (nativeEnd < nextEpochStart && proxyEnd > nextEpochStart) { + ignoredSlope = nativeSlope + ignoredPower = nativePower + ignoredSide = 'native' + nativeSlope = 0n + nativePower = 0n + } + // when proxy slope will expire before current epochEnd + // use native slope only + if (proxyEnd < nextEpochStart && nativeEnd > nextEpochStart) { + ignoredSlope = proxySlope + ignoredPower = proxyPower + ignoredSide = 'proxy' + proxySlope = 0n + proxyPower = 0n + } + + // when both slopes will expire before current epochEnd + // use max of both slopes + if (nativeEnd < nextEpochStart && proxyEnd < nextEpochStart) { + const nativeWeight = _nativeSlope * (nativeEnd - BigInt(currentTimestamp)) + const proxyWeight = _proxySlope * (proxyEnd - BigInt(currentTimestamp)) + if (nativeWeight > proxyWeight) { + ignoredPower = proxyPower + ignoredSlope = proxySlope + ignoredSide = 'proxy' + proxySlope = 0n + proxyPower = 0n + } else { + ignoredPower = nativePower + ignoredSlope = nativeSlope + ignoredSide = 'native' + nativeSlope = 0n + nativePower = 0n + } + } + return { hash: gauge?.hash as Hex, proxyPower, @@ -107,6 +154,9 @@ export const useUserVote = (gauge?: Gauge, useProxyPool: boolean = true) => { end: max(nativeEnd, nativeEnd), voteLocked: proxyVoteLocked || nativeVoteLocked, lastVoteTime: proxyLastVoteTime < nativeLastVoteTime ? nativeLastVoteTime : proxyLastVoteTime, + ignoredPower, + ignoredSlope, + ignoredSide, } } const response = await publicClient.multicall({