From 7623c42d8bea6a6644b76178130d3e9d72b156da Mon Sep 17 00:00:00 2001 From: Atatakai Date: Wed, 4 Dec 2024 14:01:06 +0400 Subject: [PATCH 1/2] (govern) feat: add possibility to reset all votes --- .../Contracts/MyVotingWeight/Votes.tsx | 162 ++++++++++++++---- 1 file changed, 131 insertions(+), 31 deletions(-) diff --git a/apps/govern/components/Contracts/MyVotingWeight/Votes.tsx b/apps/govern/components/Contracts/MyVotingWeight/Votes.tsx index a8c60585..e5a73d08 100644 --- a/apps/govern/components/Contracts/MyVotingWeight/Votes.tsx +++ b/apps/govern/components/Contracts/MyVotingWeight/Votes.tsx @@ -1,5 +1,15 @@ import { InfoCircleOutlined } from '@ant-design/icons'; -import { Button, Flex, Space, Statistic, Table, Tooltip, Typography } from 'antd'; +import { + Button, + Flex, + Modal, + notification, + Space, + Statistic, + Table, + Tooltip, + Typography, +} from 'antd'; import { ColumnsType } from 'antd/es/table'; import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; @@ -10,7 +20,13 @@ import { CHAIN_NAMES, RETAINER_ADDRESS } from 'libs/util-constants/src'; import { getBytes32FromAddress } from 'common-util/functions/addresses'; import { NextWeekTooltip } from 'components/NextWeekTooltip'; -import { useAppSelector } from 'store/index'; +import { useAppDispatch, useAppSelector } from 'store/index'; +import { Address } from 'viem'; +import { useAccount } from 'wagmi'; +import { voteForNomineeWeights } from 'common-util/functions'; +import { queryClient } from 'context/Web3ModalProvider'; +import { INVALIDATE_AFTER_UPDATE_KEYS } from 'common-util/constants/scopeKeys'; +import { clearState } from 'store/govern'; const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000; const TEN_DAYS_IN_MS = 10 * ONE_DAY_IN_MS; @@ -93,10 +109,18 @@ type VotesProps = { }; export const Votes = ({ setIsUpdating, setAllocations }: VotesProps) => { + const dispatch = useAppDispatch(); const { stakingContracts } = useAppSelector((state) => state.govern); const { lastUserVote, userVotes } = useAppSelector((state) => state.govern); + const { address: account } = useAccount(); const [votesBlocked, setVotesBlocked] = useState(false); + const [isResetModalOpen, setIsResetModalOpen] = useState(false); + const [isResetLoading, setIsResetLoading] = useState(false); + + const showResetModal = () => setIsResetModalOpen(true); + const closeResetModal = () => setIsResetModalOpen(false); + useEffect(() => { setVotesBlocked(lastUserVote !== null ? lastUserVote + TEN_DAYS_IN_MS > Date.now() : false); }, [lastUserVote]); @@ -119,6 +143,57 @@ export const Votes = ({ setIsUpdating, setAllocations }: VotesProps) => { ); }; + const resetAllWeights = async () => { + if (!account) return; + + setIsResetLoading(true); + + const nominees: Address[] = []; + const chainIds: number[] = []; + const weights: string[] = []; + + Object.keys(userVotes).forEach((address) => { + const contract = stakingContracts.find((contract) => contract.address === address); + if (contract) { + // Set each staking contract's weight to 0 + nominees.push(contract.address); + chainIds.push(contract.chainId); + weights.push('0'); + } else if (address === getBytes32FromAddress(RETAINER_ADDRESS)) { + // set the retainer's weight to 0 + nominees.push(getBytes32FromAddress(RETAINER_ADDRESS)); + chainIds.push(1); + weights.push('0'); + } + }); + + // Vote + voteForNomineeWeights({ account, nominees, chainIds, weights }) + .then(() => { + closeResetModal(); + notification.success({ + message: 'Your votes have been reset', + }); + + // Reset previously saved data so it's re-fetched automatically + queryClient.removeQueries({ + predicate: (query) => + INVALIDATE_AFTER_UPDATE_KEYS.includes( + (query.queryKey[1] as Record)?.scopeKey, + ), + }); + dispatch(clearState()); + }) + .catch((error) => { + notification.error({ + message: error.message, + }); + }) + .finally(() => { + setIsResetLoading(false); + }); + }; + const unblockVoting = () => setVotesBlocked(false); const deadline = lastUserVote ? lastUserVote + TEN_DAYS_IN_MS : undefined; @@ -157,34 +232,59 @@ export const Votes = ({ setIsUpdating, setAllocations }: VotesProps) => { }, [userVotes, stakingContracts]); return ( - - - {votesBlocked && lastUserVote !== null && ( - - Cooldown period: } - format={ - deadline && deadline < Date.now() + ONE_DAY_IN_MS - ? 'H[h] m[m] s[s]' - : 'D[d] H[h] m[m]' - } - value={deadline} - onFinish={unblockVoting} - /> - - )} - - - - className="mt-16" - columns={columns} - dataSource={data} - pagination={false} - rowClassName={rowClassName} - rowKey={(record) => record.address || record.name} - /> - + <> + + + {votesBlocked && lastUserVote !== null && ( + + Cooldown period: } + format={ + deadline && deadline < Date.now() + ONE_DAY_IN_MS + ? 'H[h] m[m] s[s]' + : 'D[d] H[h] m[m]' + } + value={deadline} + onFinish={unblockVoting} + /> + + )} + + + + + + + className="mt-16" + columns={columns} + dataSource={data} + pagination={false} + rowClassName={rowClassName} + rowKey={(record) => record.address || record.name} + /> + + + + This will seize all your voting weight, including unallocated ones applied to the Rollover + Pool. + + + After you confirm, you’ll enter a 10 day cooldown period. You won't be able to update + your weights during that time. + + + ); }; From f16dd96f40041cb65f4259aae3054948f02cdbd2 Mon Sep 17 00:00:00 2001 From: Atatakai Date: Mon, 3 Mar 2025 12:58:01 +0400 Subject: [PATCH 2/2] chore: fix tests --- .../MyVotingWeight/MyVotingWeight.spec.tsx | 16 ++++------------ .../Contracts/MyVotingWeight/Votes.spec.tsx | 14 ++++++++++++++ apps/govern/jest.setup.js | 4 ++++ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/apps/govern/components/Contracts/MyVotingWeight/MyVotingWeight.spec.tsx b/apps/govern/components/Contracts/MyVotingWeight/MyVotingWeight.spec.tsx index cb5809dd..af4ecc0f 100644 --- a/apps/govern/components/Contracts/MyVotingWeight/MyVotingWeight.spec.tsx +++ b/apps/govern/components/Contracts/MyVotingWeight/MyVotingWeight.spec.tsx @@ -1,14 +1,13 @@ import '@testing-library/jest-dom'; import { render, screen } from '@testing-library/react'; import { Allocation } from 'types'; -import { useAccount } from 'wagmi'; - -import { useVotingPower } from 'hooks/index'; import { MyVotingWeight } from './MyVotingWeight'; -jest.mock('wagmi', () => ({ useAccount: jest.fn() })); -jest.mock('hooks/index', () => ({ useVotingPower: jest.fn() })); +jest.mock('wagmi', () => ({ + useAccount: jest.fn().mockReturnValue({ address: '0x1234', isConnected: true }), +})); +jest.mock('hooks/index', () => ({ useVotingPower: jest.fn().mockReturnValue({ data: '75.05' }) })); jest.mock('store/index', () => ({ useAppSelector: jest.fn().mockReturnValue({ userVotes: {}, @@ -33,13 +32,6 @@ const MyVotingWeightExample = () => { }; describe('', () => { - beforeEach(() => { - jest.clearAllMocks(); - - (useVotingPower as jest.Mock).mockReturnValue({ data: '75.05' }); - (useAccount as jest.Mock).mockReturnValue({ address: '0x1234', isConnected: true }); - }); - it('should display title and description', () => { render(); diff --git a/apps/govern/components/Contracts/MyVotingWeight/Votes.spec.tsx b/apps/govern/components/Contracts/MyVotingWeight/Votes.spec.tsx index 55687905..f7db5206 100644 --- a/apps/govern/components/Contracts/MyVotingWeight/Votes.spec.tsx +++ b/apps/govern/components/Contracts/MyVotingWeight/Votes.spec.tsx @@ -4,7 +4,21 @@ import React from 'react'; import { Votes } from './Votes'; +jest.mock('wagmi', () => ({ + useAccount: jest.fn().mockReturnValue({ address: '0x1234', isConnected: true }), +})); + +jest.mock('@wagmi/core', () => ({ + readContract: jest.fn(), + readContracts: jest.fn(), +})); + +jest.mock('context/Web3ModalProvider', () => ({ + queryClient: jest.fn(), +})); + jest.mock('store/index', () => ({ + useAppDispatch: jest.fn(), useAppSelector: jest.fn().mockReturnValue({ lastUserVote: Date.now(), userVotes: { diff --git a/apps/govern/jest.setup.js b/apps/govern/jest.setup.js index 151d1a25..79a09935 100644 --- a/apps/govern/jest.setup.js +++ b/apps/govern/jest.setup.js @@ -33,3 +33,7 @@ jest.mock('wagmi/chains', () => ({ celo, mode, })); + +jest.mock('common-util/config/wagmi', () => ({ + SUPPORTED_CHAINS: [{ name: 'ethereum', chainId: 1 }], +}));