From 915354032fc1e392c429bd7ebd826a466ebe6336 Mon Sep 17 00:00:00 2001 From: MrX-SNX Date: Wed, 21 Aug 2024 12:52:44 +0100 Subject: [PATCH] test: cross chain vote selection (#394) * test: cross chain vote selection * ref: handle loading state for stateForNetwork * feat: * * feat: improved my votes page * feat: testnet deployment snax chain * feat: snapshot contract update * feat: QA * fix: table * feat: implemented multicall * fix: nominate self mutation * feat: ui fixes * test: fix test * ref: provider url * ref: multicall hook * feat: added chain icon * ref: added return statement * feat: modal prevent leaving without saving * feat: added longpress behaviour to my votes card * feat: added long press button * feat: open vote card when user selects candidate * fix: table * feat: rounded table * feat: updated ci, contracts and abis * ci: implemented new testnet * feat: fix voting * ref: fixing deps * feat: added release governance workflow * fix: reverted gov workflow --- .circleci/config.yml | 6 +- .../e2e/Councils - Administration.e2e.js | 10 +- .../cypress/e2e/Councils - Nomination.e2e.js | 7 + .../cypress/e2e/Councils - Voting.e2e.js | 14 +- governance/cypress/cypress/support/e2e.js | 3 - .../cypress/tasks/prepareVotingPower.js | 18 +- governance/cypress/package.json | 3 +- .../subgraph/tests/election-module.test.ts | 1 - governance/ui/.env.example | 6 +- .../components/CouncilCard/CouncilCard.tsx | 2 +- .../components/CouncilImage/CouncilImage.tsx | 6 +- .../CouncilInformation/CouncilInformation.tsx | 35 +- .../CouncilMembers/CouncilMembers.tsx | 65 ++-- .../CouncilNominees/CouncilNominees.tsx | 5 +- .../components/CouncilTabs/CouncilSelect.tsx | 23 +- .../components/CouncilTabs/CouncilTabs.tsx | 48 ++- .../components/CouncilUser/CouncilUser.tsx | 8 +- .../EditNomination/EditNomination.tsx | 20 +- .../EditNominationConfirmation.tsx | 37 +- .../EditNominationContainer.tsx | 4 +- .../EditNomination/EditNominationSelect.tsx | 36 +- .../ui/src/components/Header/Header.tsx | 53 +-- .../components/Header/NetworkController.tsx | 27 +- .../ui/src/components/MyVoteRow/MyVoteRow.tsx | 45 ++- .../src/components/MyVotesBox/MyVotesBox.tsx | 9 +- .../MyVotesSummary/MyVotesSummary.tsx | 107 +++++- .../components/NominateSelf/NominateSelf.tsx | 26 +- .../NominateSelf/NominateSelfContainer.tsx | 4 +- .../UserActionBox/UserActionBox.tsx | 2 - .../components/UserListItem/UserListItem.tsx | 18 +- .../ProfilePicture/ProfilePicture.tsx | 13 +- .../UserProfileCard/SelectedContainer.tsx | 2 +- .../UserProfileCard/UserProfileCard.tsx | 2 +- .../UserProfileCard/UserProfileDetails.tsx | 89 +++-- .../UserProfileEditPreview.tsx | 16 +- .../UserProfileForm/UserProfileForm.tsx | 109 ++++-- .../UserTableView/UserTableView.tsx | 137 ++++--- governance/ui/src/context/VoteContext.tsx | 61 ++- governance/ui/src/hooks/useMulticall.tsx | 6 + governance/ui/src/mutations/useCastVotes.ts | 72 ++-- .../ui/src/mutations/useDeclareVotingPower.ts | 7 +- .../ui/src/mutations/useEditNomination.ts | 58 +-- .../ui/src/mutations/useNominateSelf.ts | 16 +- governance/ui/src/pages/Admin.tsx | 361 ++++++++++-------- governance/ui/src/pages/App.tsx | 2 +- governance/ui/src/pages/Councils.tsx | 8 +- governance/ui/src/pages/MyProfile.tsx | 3 +- governance/ui/src/pages/MyVotes.tsx | 84 ++-- governance/ui/src/queries/index.ts | 1 - governance/ui/src/queries/useGetCastVotes.ts | 61 --- .../ui/src/queries/useGetCouncilMembers.ts | 19 +- .../ui/src/queries/useGetCouncilNominees.ts | 23 +- .../ui/src/queries/useGetIsNominated.ts | 3 +- governance/ui/src/queries/useGetUserBallot.ts | 2 +- .../ui/src/queries/useGetUserVotingPower.ts | 12 +- .../ui/src/queries/useGetVotingCandidates.ts | 13 - governance/ui/src/state/vote-card.ts | 6 + governance/ui/src/utils/abi.ts | 283 ++++++++++---- governance/ui/src/utils/contracts.ts | 66 ++-- governance/ui/src/utils/graph.ts | 16 - governance/ui/src/utils/localstorage.ts | 43 ++- governance/ui/src/utils/onboard.ts | 17 +- governance/ui/src/utils/providers.ts | 11 +- .../icons/SNXChainIcon/SNXChainIcon.tsx | 25 ++ .../components/icons/SNXChainIcon/index.tsx | 1 + liquidity/components/icons/index.ts | 1 + liquidity/lib/useBlockchain/useBlockchain.tsx | 39 +- yarn.lock | 1 - 68 files changed, 1369 insertions(+), 968 deletions(-) create mode 100644 governance/ui/src/hooks/useMulticall.tsx delete mode 100644 governance/ui/src/queries/useGetCastVotes.ts delete mode 100644 governance/ui/src/queries/useGetVotingCandidates.ts create mode 100644 governance/ui/src/state/vote-card.ts delete mode 100644 governance/ui/src/utils/graph.ts create mode 100644 liquidity/components/icons/SNXChainIcon/SNXChainIcon.tsx create mode 100644 liquidity/components/icons/SNXChainIcon/index.tsx diff --git a/.circleci/config.yml b/.circleci/config.yml index 42b25d57c..4919be37d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -141,7 +141,7 @@ jobs: - run: name: Run anvil localhost:8545 - command: 'anvil --fork-url https://optimism-sepolia.infura.io/v3/$INFURA_KEY --fork-block-number 15053625' + command: 'anvil --fork-url https://testnet.snaxchain.io' background: true - run: @@ -308,7 +308,7 @@ workflows: - tests - liquidity-cy - governance-e2e: - name: governance-e2e-op-sepolia + name: governance-e2e-snax-testnet - liquidity-e2e: name: liquidity-e2e-base-mainnet chainId: 8453 @@ -323,7 +323,7 @@ workflows: requires: [ tests, liquidity-cy, - governance-e2e-op-sepolia, + governance-e2e-snax-testnet, liquidity-e2e-base-mainnet, #liquidity-e2e-sepolia, ] diff --git a/governance/cypress/cypress/e2e/Councils - Administration.e2e.js b/governance/cypress/cypress/e2e/Councils - Administration.e2e.js index d70d77177..f146ccafe 100644 --- a/governance/cypress/cypress/e2e/Councils - Administration.e2e.js +++ b/governance/cypress/cypress/e2e/Councils - Administration.e2e.js @@ -1,5 +1,7 @@ it('Councils - Administration', () => { cy.task('changePeriod', { council: 'spartan', period: 'admin' }); + cy.task('changePeriod', { council: 'ambassador', period: 'admin' }); + cy.task('changePeriod', { council: 'treasury', period: 'admin' }); cy.task('mineBlock'); cy.connectWallet(); cy.viewport(1300, 900); @@ -13,10 +15,11 @@ it('Councils - Administration', () => { cy.get('[data-cy="council-tab-vote-circle"]').should('not.exist'); cy.get('[data-cy="view-council-button-spartan"]').click(); cy.get('[data-cy="election-closed-tag"]').should('exist'); + cy.get('[data-cy="council-image-council-tabs-spartan"]').should('have.css', 'width', '28px'); cy.get('[data-cy="user-action-box-unselected"]') .should('have.css', 'top', '105px') .and('have.css', 'position', 'sticky'); - cy.get('[data-cy="council-image-council-tabs-spartan"]') + cy.get('[data-cy="council-image-council-tabs-spartan-circle"]') .should('have.css', 'width', '40px') .and('have.css', 'height', '40px'); cy.get('[data-cy="council-information-spartan"]').should('have.css', 'gap', '8px'); @@ -38,5 +41,8 @@ it('Councils - Administration', () => { cy.get('[data-cy="sort-arrow-down"]').should('exist'); cy.viewport(600, 500); cy.visit('#/councils'); - cy.get('[data-cy="my-votes-summary-text"]').should('have.css', 'font-size', '14px'); + cy.get('[data-cy="my-votes-summary-text"]').should('have.css', 'font-size', '12px'); + cy.get('[data-cy="council-select-mobile"]').should('have.css', 'font-size', '16px'); + cy.get('[data-cy="menu-button-flex-council-select"]').should('have.css', 'height', '48px'); + cy.get('[data-cy="my-votes-button"]').should('have.css', 'height', '48px'); }); diff --git a/governance/cypress/cypress/e2e/Councils - Nomination.e2e.js b/governance/cypress/cypress/e2e/Councils - Nomination.e2e.js index ac7d10688..494fefeb9 100644 --- a/governance/cypress/cypress/e2e/Councils - Nomination.e2e.js +++ b/governance/cypress/cypress/e2e/Councils - Nomination.e2e.js @@ -1,5 +1,7 @@ it('Councils - Administration', () => { cy.task('changePeriod', { council: 'spartan', period: 'nomination' }); + cy.task('changePeriod', { council: 'ambassador', period: 'nomination' }); + cy.task('changePeriod', { council: 'treasury', period: 'nomination' }); cy.task('mineBlock'); cy.connectWallet(); cy.viewport(1300, 900); @@ -33,6 +35,9 @@ it('Councils - Administration', () => { ); cy.get('[data-cy="council-nomination-select-spartan"]').click(); cy.get('[data-cy="nominate-self-cast-nomination-button"]').click(); + cy.get('[data-cy="nominate-self-modal"]') + .should('have.css', 'top', '105px') + .and('have.css', 'position', 'sticky'); cy.get('[data-cy="nominate-self-done-button"]').click(); cy.get('[data-cy="empty-state-user-action-box"]').contains( 'Click on a nominee to see their profile details' @@ -46,6 +51,7 @@ it('Councils - Administration', () => { cy.get('[data-cy="nominate-self-button-user-profile-details"]').click(); cy.get('[data-cy="withdraw-vote-select"]').click(); cy.get('[data-cy="edit-nomination-button"]').click(); + cy.get('[data-cy="edit-nomination"]').should('have.css', 'height', '612px'); cy.get('[data-cy="confirm-edit-nomination-button"]').click(); cy.get('[data-cy="empty-state-user-action-box"]').contains( 'Click on a nominee to see their profile details' @@ -53,4 +59,5 @@ it('Councils - Administration', () => { cy.get('[data-cy="user-table-view-button-0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"]').should( 'not.exist' ); + cy.get('[data-cy="user-list-item-button-nomination"]').should('not.exist'); }); diff --git a/governance/cypress/cypress/e2e/Councils - Voting.e2e.js b/governance/cypress/cypress/e2e/Councils - Voting.e2e.js index d9ceddcdb..d34c6ec9a 100644 --- a/governance/cypress/cypress/e2e/Councils - Voting.e2e.js +++ b/governance/cypress/cypress/e2e/Councils - Voting.e2e.js @@ -1,7 +1,9 @@ it('Councils - Administration', () => { - cy.task('changePeriod', { council: 'spartan', period: 'admin' }); cy.task('prepareVotingPower', { council: 'spartan' }); - // cy.task('changePeriod', { council: 'spartan', period: 'voting' }); + cy.task('mineBlock'); + cy.task('prepareVotingPower', { council: 'ambassador' }); + cy.task('mineBlock'); + cy.task('prepareVotingPower', { council: 'treasury' }); cy.task('mineBlock'); cy.connectWallet(); cy.viewport(1300, 900); @@ -10,7 +12,7 @@ it('Councils - Administration', () => { cy.get('[data-cy="council-period-badge"]').contains('Voting Open'); cy.get('[data-cy="account-menu-button"]').click(); cy.get('[data-cy="network-controller-switch"]').click(); - cy.get('[data-cy="network-menu-button-11155420"]').click(); + cy.get('[data-cy="network-menu-button-13001"]').click(); cy.get('[data-cy="council-tab-vote-circle"]').should('not.exist'); cy.get('[data-cy="vote-council-button-spartan"]').click(); cy.get('[data-cy="period-countdown"]').should('exist'); @@ -39,7 +41,7 @@ it('Councils - Administration', () => { '[data-cy="user-blockies-council-tabs-0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"]' ).should('exist'); cy.wait(3000); - cy.get('[data-cy="my-votes-voting-power"]').contains('100.00'); + cy.get('[data-cy="my-votes-voting-power"]').contains('30.00'); cy.get('[data-cy="cast-my-vote-button"]').click(); cy.wait(3000); cy.window().then((win) => { @@ -50,8 +52,8 @@ it('Councils - Administration', () => { '[data-cy="user-blockies-council-tabs-0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"]' ).should('exist'); cy.wait(3000); - cy.get('[data-cy="remove-vote-button"]').click(); + cy.get('[data-cy="remove-vote-button-spartan"]').click(); cy.get('[data-cy="cast-my-vote-button"]').click(); cy.wait(3000); - cy.get('[data-cy="council-tab-vote-circle"]').should('exist'); + cy.get('[data-cy="my-votes-total-votes"]').contains('0/3'); }); diff --git a/governance/cypress/cypress/support/e2e.js b/governance/cypress/cypress/support/e2e.js index d932c28c1..da6d0667f 100644 --- a/governance/cypress/cypress/support/e2e.js +++ b/governance/cypress/cypress/support/e2e.js @@ -1,11 +1,8 @@ import '@cypress/code-coverage/support'; -import { onLogAdded } from '@snx-cy/onLogAdded'; import { ethers } from 'ethers'; import { metamask } from '../lib/metamask'; beforeEach(() => { - cy.on('log:added', onLogAdded); - cy.intercept('https://analytics.synthetix.io/matomo.js', { statusCode: 204 }).as('matomo'); [ diff --git a/governance/cypress/cypress/tasks/prepareVotingPower.js b/governance/cypress/cypress/tasks/prepareVotingPower.js index 13cb5958d..2061463d6 100644 --- a/governance/cypress/cypress/tasks/prepareVotingPower.js +++ b/governance/cypress/cypress/tasks/prepareVotingPower.js @@ -8,8 +8,6 @@ export async function prepareVotingPower({ council }) { '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', provider ); - // already done with current fork - // await proxy.connect(signer).setSnapshotContract(SnapshotRecordContract(11155420)?.address, true); const block = await provider.getBlock('latest'); await proxy .connect(signer) @@ -20,12 +18,16 @@ export async function prepareVotingPower({ council }) { block.timestamp, block.timestamp + 10000 ); - // already done with current fork - // const id = await proxy - // .connect(signer) - // .takeVotePowerSnapshot(SnapshotRecordContract(11155420)?.address); - // console.log('ID', id); - SnapshotRecordContract(11155420) + + // let id = ''; + // try { + // id = await proxy + // .connect(signer) + // .takeVotePowerSnapshot(SnapshotRecordContract(2192, council)?.address); + // } catch (error) { + // console.info('takeVotePowerSnapshot failed'); + // } + await SnapshotRecordContract(13001, council) .connect(signer) .setBalanceOfOnPeriod(await signer.getAddress(), 100, 1); return null; diff --git a/governance/cypress/package.json b/governance/cypress/package.json index f56f8a38c..b29bec31b 100644 --- a/governance/cypress/package.json +++ b/governance/cypress/package.json @@ -4,13 +4,12 @@ "main": "index.ts", "version": "0.0.1", "scripts": { - "anvil:op-sepolia": "anvil --fork-url https://optimism-sepolia.infura.io/v3/$INFURA_KEY --fork-block-number 15053625", + "anvil:snax": "anvil --fork-url https://testnet.snaxchain.io --fork-block-number 296528", "e2e": "cypress open --e2e --browser chrome" }, "devDependencies": { "@chakra-ui/react": "^2.8.2", "@cypress/code-coverage": "^3.12.39", - "@snx-cy/onLogAdded": "workspace:*", "@snx-cy/printBrowserLogs": "workspace:*", "@synthetixio/v3-theme": "workspace:*", "cypress": "13.11.0", diff --git a/governance/subgraph/tests/election-module.test.ts b/governance/subgraph/tests/election-module.test.ts index bd3599f25..d98b35ad9 100644 --- a/governance/subgraph/tests/election-module.test.ts +++ b/governance/subgraph/tests/election-module.test.ts @@ -5,7 +5,6 @@ import { clearStore, beforeAll, afterAll, - logStore, } from 'matchstick-as/assembly/index'; import { Address, BigInt, Bytes } from '@graphprotocol/graph-ts'; import { diff --git a/governance/ui/.env.example b/governance/ui/.env.example index e4271c502..6fb4fad9a 100644 --- a/governance/ui/.env.example +++ b/governance/ui/.env.example @@ -1,6 +1,8 @@ INFURA_KEY=xxx WC_PROJECT_ID=xxx BOARDROOM_KEY=xxx -DEV=true -DEV_RPC_MOTHERSHIP=http://127.0.0.1:19000/ +DEV=false +DEV_RPC_MOTHERSHIP=http://127.0.0.1:19000 TESTNET=false +CI=true +CI_RPC_MOTHERSHIP=http://127.0.0.1:8545 diff --git a/governance/ui/src/components/CouncilCard/CouncilCard.tsx b/governance/ui/src/components/CouncilCard/CouncilCard.tsx index 3e3c2010d..bcbe9573e 100644 --- a/governance/ui/src/components/CouncilCard/CouncilCard.tsx +++ b/governance/ui/src/components/CouncilCard/CouncilCard.tsx @@ -95,7 +95,7 @@ export function CouncilCard({ council }: CouncilCardProps) { - 0000 + TODO diff --git a/governance/ui/src/components/CouncilImage/CouncilImage.tsx b/governance/ui/src/components/CouncilImage/CouncilImage.tsx index 4b4c576ba..b102dcdc7 100644 --- a/governance/ui/src/components/CouncilImage/CouncilImage.tsx +++ b/governance/ui/src/components/CouncilImage/CouncilImage.tsx @@ -3,9 +3,10 @@ import { FlexProps, Flex, Image, ImageProps } from '@chakra-ui/react'; interface CouncilImageProps extends FlexProps { imageUrl: string; imageProps?: ImageProps; + dataCy?: string; } -export const CouncilImage = ({ imageUrl, imageProps, ...props }: CouncilImageProps) => { +export const CouncilImage = ({ imageUrl, imageProps, dataCy, ...props }: CouncilImageProps) => { return ( - + ); }; diff --git a/governance/ui/src/components/CouncilInformation/CouncilInformation.tsx b/governance/ui/src/components/CouncilInformation/CouncilInformation.tsx index f3882f8e1..b1ee4b58a 100644 --- a/governance/ui/src/components/CouncilInformation/CouncilInformation.tsx +++ b/governance/ui/src/components/CouncilInformation/CouncilInformation.tsx @@ -1,8 +1,7 @@ -import { Flex, Heading, Text } from '@chakra-ui/react'; +import { Flex, Heading, Icon, Text } from '@chakra-ui/react'; import councils, { CouncilSlugs } from '../../utils/councils'; import { useGetCouncilMembers } from '../../queries/useGetCouncilMembers'; import { CouncilImage } from '../CouncilImage'; -import { ExternalLinkIcon } from '@chakra-ui/icons'; import { Link } from 'react-router-dom'; export default function CouncilInformation({ activeCouncil }: { activeCouncil: CouncilSlugs }) { @@ -10,7 +9,12 @@ export default function CouncilInformation({ activeCouncil }: { activeCouncil: C const council = councils.find((council) => council.slug === activeCouncil); return ( - + {council?.title} - + {council?.description} @@ -60,7 +58,7 @@ export default function CouncilInformation({ activeCouncil }: { activeCouncil: C > Stipends: {council?.stipends}/month - + - Learn more + Learn more{' '} + + + + + + + + + + diff --git a/governance/ui/src/components/CouncilMembers/CouncilMembers.tsx b/governance/ui/src/components/CouncilMembers/CouncilMembers.tsx index 2145ab569..0400066ec 100644 --- a/governance/ui/src/components/CouncilMembers/CouncilMembers.tsx +++ b/governance/ui/src/components/CouncilMembers/CouncilMembers.tsx @@ -6,6 +6,7 @@ import { Table, Tag, Tbody, + Td, Text, Th, Thead, @@ -97,7 +98,7 @@ export default function CouncilMembers({ activeCouncil }: { activeCouncil: Counc flexDirection="column" mt={6} > - + Election for {quarter} @@ -113,7 +114,7 @@ export default function CouncilMembers({ activeCouncil }: { activeCouncil: Counc - +
- - ) : ( - <> - {!!sortedNominees?.length && - sortedNominees.map((councilNominee, index) => { - return ( - - ); - })} - + !!sortedNominees?.length && + sortedNominees.map((councilNominee, index) => { + return ( + + ); + }) )}
+ - - + + - - + + - - + + - +
+ - - + + - - + + - - + + - +
diff --git a/governance/ui/src/components/CouncilNominees/CouncilNominees.tsx b/governance/ui/src/components/CouncilNominees/CouncilNominees.tsx index 846d07123..05601cc10 100644 --- a/governance/ui/src/components/CouncilNominees/CouncilNominees.tsx +++ b/governance/ui/src/components/CouncilNominees/CouncilNominees.tsx @@ -90,7 +90,7 @@ export default function CouncilNominees({ activeCouncil }: { activeCouncil: Coun }) .filter((nominee) => { if (utils.isAddress(search)) { - return nominee.address.toLowerCase() === search; + return nominee.address.toLowerCase().includes(search); } if (search) { return nominee.username.toLowerCase().includes(search); @@ -167,7 +167,7 @@ export default function CouncilNominees({ activeCouncil }: { activeCouncil: Coun onChange={(e) => setSearch(e.target.value.trim().toLowerCase())} />
- +
{councilPeriod === '2' && ( @@ -214,7 +214,6 @@ export default function CouncilNominees({ activeCouncil }: { activeCouncil: Coun )} - {councilPeriod === '2' && ( {councilIsInAdminOrVoting && ( - + + {place < 10 ? ( + + {place + 1} + + + ) : ( + '-' + )} + + )} - - {councilPeriod !== '0' && ( - + )} {councilIsInAdminOrVoting && ( - )} {councilPeriod !== '2' && councilPeriod !== '0' && ( - + )} {councilPeriod === '0' && ( - + + Your Vote TODO + + )} ); } + +const CrownIcon = () => ( + + + +); diff --git a/governance/ui/src/context/VoteContext.tsx b/governance/ui/src/context/VoteContext.tsx index 18941c779..a2993d353 100644 --- a/governance/ui/src/context/VoteContext.tsx +++ b/governance/ui/src/context/VoteContext.tsx @@ -1,20 +1,33 @@ import React, { createContext, useContext, useReducer, ReactNode } from 'react'; import { removeCandidate, setCandidate } from '../utils/localstorage'; +import { useNetwork } from '../queries'; -export interface VoteState { +export interface VoteStateForNetwork { spartan: string | undefined; ambassador: string | undefined; treasury: string | undefined; } -type Action = { type: string; payload: string | undefined }; +export interface VoteState { + [key: string]: VoteStateForNetwork; +} + +type Action = { + type: string; + payload: { action: string | undefined; network: string | undefined }; +}; const parsedState = JSON.parse(localStorage.getItem('voteSelection') || '{}'); -const initialState: VoteState = { - spartan: parsedState?.spartan || undefined, - ambassador: parsedState?.ambassador || undefined, - treasury: parsedState?.treasury || undefined, +const initialState = (chainId?: string) => { + const customChainId = chainId || Object.keys(parsedState)[0] || '2492'; + return { + [customChainId]: { + spartan: parsedState[customChainId]?.spartan || undefined, + ambassador: parsedState[customChainId]?.ambassador || undefined, + treasury: parsedState[customChainId]?.treasury || undefined, + }, + } as VoteState; }; const VoteContext = createContext< @@ -28,36 +41,45 @@ const VoteContext = createContext< const voteReducer = (state: VoteState, action: Action): VoteState => { switch (action.type) { case 'SPARTAN': { - if (action.payload) { - setCandidate(action.payload, 'spartan'); + if (action.payload.action && action.payload.action !== 'remove') { + setCandidate(action.payload.action, 'spartan', action.payload.network); } else { - removeCandidate('spartan'); + removeCandidate('spartan', action.payload.network); } return { ...state, - spartan: action.payload, + [action.payload.network!]: { + ...state[action.payload.network!], + spartan: action.payload.action, + }, }; } case 'AMBASSADOR': { - if (action.payload) { - setCandidate(action.payload, 'ambassador'); + if (action.payload.action && action.payload.action !== 'remove') { + setCandidate(action.payload.action, 'ambassador', action.payload.network); } else { - removeCandidate('ambassador'); + removeCandidate('ambassador', action.payload.network); } return { ...state, - ambassador: action.payload, + [action.payload.network!]: { + ...state[action.payload.network!], + ambassador: action.payload.action, + }, }; } case 'TREASURY': { - if (action.payload) { - setCandidate(action.payload, 'treasury'); + if (action.payload.action && action.payload.action !== 'remove') { + setCandidate(action.payload.action, 'treasury', action.payload.network); } else { - removeCandidate('treasury'); + removeCandidate('treasury', action.payload.network); } return { ...state, - treasury: action.payload, + [action.payload.network!]: { + ...state[action.payload.network!], + treasury: action.payload.action, + }, }; } default: @@ -66,7 +88,8 @@ const voteReducer = (state: VoteState, action: Action): VoteState => { }; const VoteProvider: React.FC<{ children: ReactNode }> = ({ children }) => { - const [state, dispatch] = useReducer(voteReducer, initialState); + const { network } = useNetwork(); + const [state, dispatch] = useReducer(voteReducer, initialState(network?.id.toString())); return {children}; }; diff --git a/governance/ui/src/hooks/useMulticall.tsx b/governance/ui/src/hooks/useMulticall.tsx new file mode 100644 index 000000000..bb3b5252c --- /dev/null +++ b/governance/ui/src/hooks/useMulticall.tsx @@ -0,0 +1,6 @@ +import { Contract } from 'ethers'; +import { multicallABI } from '../utils/abi'; + +export const useMulticall = () => { + return new Contract('0xE2C5658cC5C448B48141168f3e475dF8f65A1e3e', multicallABI); +}; diff --git a/governance/ui/src/mutations/useCastVotes.ts b/governance/ui/src/mutations/useCastVotes.ts index 0414cf24e..1b9a7b45b 100644 --- a/governance/ui/src/mutations/useCastVotes.ts +++ b/governance/ui/src/mutations/useCastVotes.ts @@ -1,16 +1,10 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { - useGetUserBallot, - useGetUserVotingPower, - useNetwork, - useSigner, - useWallet, -} from '../queries'; +import { useGetUserVotingPower, useNetwork, useSigner, useWallet } from '../queries'; import { CouncilSlugs } from '../utils/councils'; import { getCouncilContract, SnapshotRecordContract } from '../utils/contracts'; -import { BigNumber, Contract } from 'ethers'; -import { multicallABI } from '../utils/abi'; +import { BigNumber, utils } from 'ethers'; import { useVoteContext } from '../context/VoteContext'; +import { useMulticall } from '../hooks/useMulticall'; export function useCastVotes( councils: CouncilSlugs[], @@ -21,16 +15,11 @@ export function useCastVotes( const { network } = useNetwork(); const { activeWallet } = useWallet(); const { dispatch } = useVoteContext(); - const multicall = new Contract('0xE2C5658cC5C448B48141168f3e475dF8f65A1e3e', multicallABI); - + const multicall = useMulticall(); const { data: spartanVotingPower } = useGetUserVotingPower('spartan'); const { data: ambassadorVotingPower } = useGetUserVotingPower('ambassador'); const { data: treasuryVotingPower } = useGetUserVotingPower('treasury'); - const { data: spartanBallot } = useGetUserBallot('spartan'); - const { data: ambassadorBallot } = useGetUserBallot('ambassador'); - const { data: treasuryBallot } = useGetUserBallot('treasury'); - const getVotingPowerByCouncil = (council: CouncilSlugs) => { switch (council) { case 'spartan': @@ -44,24 +33,11 @@ export function useCastVotes( } }; - const getBallotByCouncil = (council: CouncilSlugs) => { - switch (council) { - case 'spartan': - return spartanBallot; - case 'ambassador': - return ambassadorBallot; - case 'treasury': - return treasuryBallot; - default: - return spartanBallot; - } - }; - return useMutation({ mutationKey: ['cast', councils.toString(), JSON.stringify(candidates)], mutationFn: async () => { if (signer && network && multicall) { - const isMotherchain = network.id === 11155420; + const isMotherchain = network.id === (process.env.CI === 'true' ? 13001 : 2192); try { const electionModules = councils.map((council) => getCouncilContract(council).connect(signer) @@ -74,20 +50,26 @@ export function useCastVotes( target: electionModules[0].address, callData: electionModules[0].interface.encodeFunctionData( 'prepareBallotWithSnapshot', - [SnapshotRecordContract(network.id)?.address, activeWallet?.address] + [ + SnapshotRecordContract(network.id, council)?.address, + activeWallet?.address, + ] ), } : { target: electionModules[0].address, callData: electionModules[0].interface.encodeFunctionData( 'prepareBallotWithSnapshot', - [SnapshotRecordContract(network.id)?.address, activeWallet?.address] + [ + SnapshotRecordContract(network.id, council)?.address, + activeWallet?.address, + ] ), value: 0, requireSuccess: true, }; } - return ''; + return null; }) .filter((call) => !!call); let quote: BigNumber = BigNumber.from(0); @@ -100,9 +82,7 @@ export function useCastVotes( ? { target: electionModules[0].address, callData: shouldWithdrawVote - ? electionModules[0].interface.encodeFunctionData('withdrawVote', [ - [getBallotByCouncil(council)?.votedCandidates[0]], - ]) + ? electionModules[0].interface.encodeFunctionData('withdrawVote', []) : electionModules[0].interface.encodeFunctionData('cast', [ [candidates[council]], [getVotingPowerByCouncil(council)?.power], @@ -111,21 +91,21 @@ export function useCastVotes( : { target: electionModules[0].address, callData: shouldWithdrawVote - ? electionModules[0].interface.encodeFunctionData('withdrawVote', [ - [getBallotByCouncil(council)?.votedCandidates[0]], - ]) + ? electionModules[0].interface.encodeFunctionData('withdrawVote', []) : electionModules[0].interface.encodeFunctionData('cast', [ [candidates[council]], [getVotingPowerByCouncil(council)?.power], ]), value: quote.add(quote.mul(25).div(100)), - requireSuccess: false, + requireSuccess: true, }; }); - await multicall .connect(signer) - [isMotherchain ? 'aggregate' : 'aggregate3Value']([...prepareBallotData, ...castData]); + [isMotherchain ? 'aggregate' : 'aggregate3Value']([...prepareBallotData, ...castData], { + maxPriorityFeePerGas: utils.parseUnits('1', 'gwei'), + maxFeePerGas: utils.parseUnits('2', 'gwei'), + }); } catch (error) { console.error(error); } @@ -137,8 +117,14 @@ export function useCastVotes( councils.map((council) => { const shouldWithdrawVote = candidates[council] === 'remove'; shouldWithdrawVote - ? dispatch({ type: council.toUpperCase(), payload: undefined }) - : dispatch({ type: council.toUpperCase(), payload: candidates[council] }); + ? dispatch({ + type: council.toUpperCase(), + payload: { action: undefined, network: network!.id.toString() }, + }) + : dispatch({ + type: council.toUpperCase(), + payload: { action: candidates[council], network: network!.id.toString() }, + }); }); await Promise.all( councils.map( diff --git a/governance/ui/src/mutations/useDeclareVotingPower.ts b/governance/ui/src/mutations/useDeclareVotingPower.ts index db69e96b5..e504140d3 100644 --- a/governance/ui/src/mutations/useDeclareVotingPower.ts +++ b/governance/ui/src/mutations/useDeclareVotingPower.ts @@ -1,19 +1,20 @@ import { useMutation } from '@tanstack/react-query'; import { CouncilSlugs } from '../utils/councils'; import { getCouncilContract, SnapshotRecordContract } from '../utils/contracts'; -import { useSigner } from '../queries/useWallet'; +import { useNetwork, useSigner } from '../queries/useWallet'; export default function useDeclareVotingPower(council: CouncilSlugs) { const signer = useSigner(); + const { network } = useNetwork(); return useMutation({ mutationFn: async () => { - if (signer) { + if (signer && network) { try { const electionModule = getCouncilContract(council).connect(signer); const voter = await signer.getAddress(); await electionModule.prepareBallotWithSnapshot( - SnapshotRecordContract(11155420)?.address, + SnapshotRecordContract(network.id, council)?.address, voter ); } catch (error) { diff --git a/governance/ui/src/mutations/useEditNomination.ts b/governance/ui/src/mutations/useEditNomination.ts index 63164c98d..9d956a183 100644 --- a/governance/ui/src/mutations/useEditNomination.ts +++ b/governance/ui/src/mutations/useEditNomination.ts @@ -4,34 +4,43 @@ import { getCouncilContract } from '../utils/contracts'; import { CouncilSlugs } from '../utils/councils'; import { CustomToast } from '../components/CustomToast'; import { useToast } from '@chakra-ui/react'; -import { devSigner } from '../utils/providers'; +import { utils } from 'ethers'; +import { useMulticall } from '../hooks/useMulticall'; export default function useEditNomination({ currentNomination, nextNomination, }: { currentNomination?: CouncilSlugs; - nextNomination?: CouncilSlugs; + nextNomination?: CouncilSlugs | null; }) { const query = useQueryClient(); const signer = useSigner(); const toast = useToast(); - + const multicall = useMulticall(); return useMutation({ mutationFn: async () => { if (signer) { + const txs = []; if ((nextNomination && currentNomination) || (!nextNomination && currentNomination)) { - const tx1 = await getCouncilContract(currentNomination) - .connect(process.env.DEV === 'true' ? devSigner : signer) - .withdrawNomination(); - await tx1.wait(); + txs.push({ + target: getCouncilContract(currentNomination).address, + callData: + getCouncilContract(currentNomination).interface.encodeFunctionData( + 'withdrawNomination' + ), + }); } if (nextNomination) { - const tx2 = await getCouncilContract(nextNomination) - .connect(process.env.DEV === 'true' ? devSigner : signer) - .nominate(); - await tx2.wait(); + txs.push({ + target: getCouncilContract(nextNomination).address, + callData: getCouncilContract(nextNomination).interface.encodeFunctionData('nominate'), + }); } + await multicall.connect(signer).aggregate(txs, { + maxPriorityFeePerGas: utils.parseUnits('1', 'gwei'), + maxFeePerGas: utils.parseUnits('2', 'gwei'), + }); } }, mutationKey: ['editNomination', currentNomination, nextNomination], @@ -41,17 +50,22 @@ export default function useEditNomination({ queryKey: ['isNominated', address?.toLowerCase()], exact: false, }); - await query.invalidateQueries({ queryKey: ['nominees', currentNomination] }); - await query.invalidateQueries({ queryKey: ['nominees', nextNomination] }); - await query.invalidateQueries({ queryKey: ['nomineesDetails', currentNomination] }); - await query.invalidateQueries({ queryKey: ['nomineesDetails', nextNomination] }); - await query.refetchQueries({ queryKey: ['nominees', currentNomination], exact: false }); - await query.refetchQueries({ queryKey: ['nominees', nextNomination], exact: false }); - await query.refetchQueries({ - queryKey: ['nomineesDetails', currentNomination], - exact: false, - }); - await query.refetchQueries({ queryKey: ['nomineesDetails', nextNomination], exact: false }); + await Promise.all([ + await query.invalidateQueries({ queryKey: ['nominees', currentNomination] }), + await query.invalidateQueries({ queryKey: ['nominees', nextNomination] }), + await query.invalidateQueries({ queryKey: ['nomineesDetails', currentNomination] }), + await query.invalidateQueries({ queryKey: ['nomineesDetails', nextNomination] }), + await query.refetchQueries({ queryKey: ['nominees', currentNomination], exact: false }), + await query.refetchQueries({ queryKey: ['nominees', nextNomination], exact: false }), + await query.refetchQueries({ + queryKey: ['nomineesDetails', currentNomination, address], + exact: false, + }), + await query.refetchQueries({ + queryKey: ['nomineesDetails', nextNomination, address], + exact: false, + }), + ]); toast({ description: 'Nomination successfully edited.', status: 'success', diff --git a/governance/ui/src/mutations/useNominateSelf.ts b/governance/ui/src/mutations/useNominateSelf.ts index faa243c8b..10b09af1e 100644 --- a/governance/ui/src/mutations/useNominateSelf.ts +++ b/governance/ui/src/mutations/useNominateSelf.ts @@ -4,7 +4,7 @@ import { getCouncilContract } from '../utils/contracts'; import { useSigner } from '../queries/useWallet'; import { useToast } from '@chakra-ui/react'; import { CustomToast } from '../components/CustomToast'; -import { devSigner } from '../utils/providers'; +import { utils } from 'ethers'; export default function useNominateSelf(council: CouncilSlugs, address?: string) { const query = useQueryClient(); @@ -15,8 +15,11 @@ export default function useNominateSelf(council: CouncilSlugs, address?: string) mutationFn: async () => { if (signer) { const tx = await getCouncilContract(council) - .connect(process.env.DEV === 'true' ? devSigner : signer) - .nominate(); + .connect(signer) + .nominate({ + maxPriorityFeePerGas: utils.parseUnits('1', 'gwei'), + maxFeePerGas: utils.parseUnits('2', 'gwei'), + }); await tx.wait(); } }, @@ -26,9 +29,12 @@ export default function useNominateSelf(council: CouncilSlugs, address?: string) queryKey: ['isNominated', address], }); await query.invalidateQueries({ queryKey: ['nominees', council], exact: false }); - await query.invalidateQueries({ queryKey: ['nomineesDetails', council], exact: false }); + await query.invalidateQueries({ + queryKey: ['nomineesDetails', council, address], + exact: false, + }); await query.refetchQueries({ queryKey: ['nominees', council], exact: false }); - await query.refetchQueries({ queryKey: ['nomineesDetails', council], exact: false }); + await query.refetchQueries({ queryKey: ['nomineesDetails', council, address], exact: false }); toast({ description: 'Successfully nominated yourself.', status: 'success', diff --git a/governance/ui/src/pages/Admin.tsx b/governance/ui/src/pages/Admin.tsx index 5d727bf72..e57603b0c 100644 --- a/governance/ui/src/pages/Admin.tsx +++ b/governance/ui/src/pages/Admin.tsx @@ -1,175 +1,218 @@ import { Button, Flex, Heading, Text } from '@chakra-ui/react'; -import { Wallet } from 'ethers'; +import { utils } from 'ethers'; import { useSigner } from '../queries/useWallet'; -import { SnapshotRecordContract, getCouncilContract } from '../utils/contracts'; -import { motherShipProvider } from '../utils/providers'; +import { getCouncilContract, SnapshotRecordContract } from '../utils/contracts'; export default function Admin() { const signer = useSigner(); - const wallet = new Wallet( - '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', - motherShipProvider - ); + + const allProxies = [ + getCouncilContract('spartan'), + getCouncilContract('ambassador'), + getCouncilContract('treasury'), + ]; return ( - - {[getCouncilContract('spartan')].map((proxy, index) => { - return ( - - Spartan - - Start Now Admin Period - - - - Start Now Nomination Period - + + + Start Now Nomination Period + - - - Start Now Voting Period - + + + Start Now Voting Period + - - - Start Now Eval Period - - + ) + ); + }); + }} + > + Now + + + + Start Now Eval Period + + - - Set Snapshot Record Mock for voting power - - + ) + ); + }} + > + LFG (only in Administration) + + */} - - Take vote power snapshot - - - - ); - })} + ) + ); + }} + > + LFG (only in Nomination & Voting) + + + + + Get yourself some voting Power + + ); } diff --git a/governance/ui/src/pages/App.tsx b/governance/ui/src/pages/App.tsx index 9b7b34990..f3937c172 100644 --- a/governance/ui/src/pages/App.tsx +++ b/governance/ui/src/pages/App.tsx @@ -17,7 +17,7 @@ function App() { {councils.map((council) => ( - + ))} diff --git a/governance/ui/src/pages/Councils.tsx b/governance/ui/src/pages/Councils.tsx index 3e521b491..0ee917288 100644 --- a/governance/ui/src/pages/Councils.tsx +++ b/governance/ui/src/pages/Councils.tsx @@ -16,15 +16,11 @@ export default function Councils() { return ( - + - My Profile - + Update your profile information below diff --git a/governance/ui/src/pages/MyVotes.tsx b/governance/ui/src/pages/MyVotes.tsx index ba3aa3e4c..87409a633 100644 --- a/governance/ui/src/pages/MyVotes.tsx +++ b/governance/ui/src/pages/MyVotes.tsx @@ -1,13 +1,12 @@ -import { Alert, Button, Flex, Heading, Text } from '@chakra-ui/react'; +import { Alert, Box, Button, Flex, Heading, Text } from '@chakra-ui/react'; import councils, { CouncilSlugs } from '../utils/councils'; import { useNavigate } from 'react-router-dom'; import { WarningIcon } from '@chakra-ui/icons'; -import { useGetVotingCandidates } from '../queries/useGetVotingCandidates'; import { useGetCurrentPeriod } from '../queries/useGetCurrentPeriod'; import { useGetEpochSchedule } from '../queries/useGetEpochSchedule'; import { Timer } from '../components/Timer'; import CouncilTabs from '../components/CouncilTabs/CouncilTabs'; -import { useGetUserVotingPower } from '../queries/'; +import { useGetUserVotingPower, useNetwork } from '../queries/'; import { useCastVotes } from '../mutations'; import { formatNumber } from '@snx-v3/formatters'; import MyVoteRow from '../components/MyVoteRow/MyVoteRow'; @@ -16,22 +15,23 @@ import { useVoteContext } from '../context/VoteContext'; export default function MyVotes() { const { data: period } = useGetCurrentPeriod('spartan'); const { data: schedule } = useGetEpochSchedule('spartan'); + const { network } = useNetwork(); + const networkForState = network?.id.toString() || process.env.CI === 'true' ? 13001 : 2192; const { data: votingPowerSpartan } = useGetUserVotingPower('spartan'); - // const { data: votingPowerAmbassador } = useGetUserVotingPower('ambassador'); - // const { data: votingPowerTreassury } = useGetUserVotingPower('treasury'); + const { data: votingPowerAmbassador } = useGetUserVotingPower('ambassador'); + const { data: votingPowerTreassury } = useGetUserVotingPower('treasury'); const { state } = useVoteContext(); - const councilToCastVote = Object.entries(state || {}) + const councilToCastVote = Object.entries(state[networkForState] || {}) .filter(([_, candidate]) => !!candidate) .map(([council]) => council) as CouncilSlugs[]; - const { mutateAsync } = useCastVotes(councilToCastVote, state || {}); + const { mutateAsync } = useCastVotes(councilToCastVote, state[networkForState] || {}); const navigate = useNavigate(); - const { data: votingCandidates } = useGetVotingCandidates(); return ( <> - + {period !== '2' && ( @@ -72,38 +71,55 @@ export default function MyVotes() { )} - + My Votes - - {Object.values(votingCandidates || {}).length}/{councils.length} + + {Object.values(state[networkForState] || {}).filter((council) => !!council).length}/ + {councils.length} - + You can cast 3 votes in one transaction. Continue voting if you want to add other nominee otherwise cast your vote to complete your voting. - {councils.map((council) => ( + {councils.map((council, index) => ( ))} - - - You can now cast all your votes in one unique transaction - + + + + You can now cast all your votes in one unique transaction + + - Cast Your Vote + + {period === '2' ? 'Cast Your Votes' : 'Voting Power'} + Your total voting powered is aggregated from all chains and used to vote on Optimism. It can take{' '} @@ -132,19 +152,17 @@ export default function MyVotes() { borderWidth="1px" borderStyle="solid" borderColor="gray.900" - mb="auto" + mb="12" > Total Voting Power {formatNumber( - votingPowerSpartan?.power - ? // && votingPowerAmbassador - // && votingPowerTreassury - votingPowerSpartan.power - // .add(votingPowerAmbassador) - // .add(votingPowerTreassury) + votingPowerSpartan?.power && votingPowerAmbassador && votingPowerTreassury + ? votingPowerSpartan.power + .add(votingPowerAmbassador.power) + .add(votingPowerTreassury.power) .toString() : 0 )} @@ -153,12 +171,12 @@ export default function MyVotes() { diff --git a/governance/ui/src/queries/index.ts b/governance/ui/src/queries/index.ts index bedf9dac4..b0c4a9a28 100644 --- a/governance/ui/src/queries/index.ts +++ b/governance/ui/src/queries/index.ts @@ -11,5 +11,4 @@ export * from './useGetNomineesDetails'; export * from './useGetUserBallot'; export * from './useGetUserDetailsQuery'; export * from './useGetUserVotingPower'; -export * from './useGetVotingCandidates'; export * from './useWallet'; diff --git a/governance/ui/src/queries/useGetCastVotes.ts b/governance/ui/src/queries/useGetCastVotes.ts deleted file mode 100644 index 6e5cf692e..000000000 --- a/governance/ui/src/queries/useGetCastVotes.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { getCouncilContract } from '../utils/contracts'; -import { CouncilSlugs } from '../utils/councils'; -import { useGetVotingCandidates } from './useGetVotingCandidates'; -import { motherShipProvider } from '../utils/providers'; -import { BigNumber, utils } from 'ethers'; -import { useWallet, useNetwork } from './useWallet'; - -export function useGetCastVotes() { - const { network } = useNetwork(); - const { activeWallet } = useWallet(); - - const { data: votingCandidates } = useGetVotingCandidates(); - - return useQuery({ - queryFn: async () => { - const councils = Object.keys(votingCandidates ? votingCandidates : {}); - try { - if (councils.length && votingCandidates && activeWallet?.address) { - const data = await Promise.all( - councils.map(async (council) => { - const electionModuleContract = getCouncilContract(council as CouncilSlugs).connect( - motherShipProvider - ); - const addressToVoteForOrWithdraw = utils.getAddress(votingCandidates[council]); - const epochIndex = await electionModuleContract.getEpochIndex(); - const ballot = (await electionModuleContract.getBallot( - activeWallet.address, - network?.id, - epochIndex - )) as { amounts: BigNumber[]; votedCandidates: string[]; votingPower: BigNumber }; - if (ballot.votedCandidates.includes(addressToVoteForOrWithdraw)) { - return { - council, - data: electionModuleContract.interface.encodeFunctionData('withdrawVote', [ - addressToVoteForOrWithdraw, - ]), - }; - } else { - return { - council, - data: electionModuleContract.interface.encodeFunctionData('cast', [ - [addressToVoteForOrWithdraw], - [ballot.votingPower], - ]), - }; - } - }) - ); - return data; - } - return []; - } catch (error) { - console.error(error); - } - }, - queryKey: ['castVotes', activeWallet?.address, network?.id], - enabled: !!activeWallet?.address, - staleTime: 900000, - }); -} diff --git a/governance/ui/src/queries/useGetCouncilMembers.ts b/governance/ui/src/queries/useGetCouncilMembers.ts index 933d59016..107dac5ec 100644 --- a/governance/ui/src/queries/useGetCouncilMembers.ts +++ b/governance/ui/src/queries/useGetCouncilMembers.ts @@ -2,23 +2,6 @@ import { useQuery } from '@tanstack/react-query'; import { CouncilSlugs } from '../utils/councils'; import { getCouncilContract } from '../utils/contracts'; import { motherShipProvider } from '../utils/providers'; -import { Wallet } from 'ethers'; - -// TODO @dev remove -const randomAddresses = [ - Wallet.createRandom().address, - Wallet.createRandom().address, - Wallet.createRandom().address, - Wallet.createRandom().address, - Wallet.createRandom().address, - Wallet.createRandom().address, - Wallet.createRandom().address, - Wallet.createRandom().address, - Wallet.createRandom().address, - Wallet.createRandom().address, - Wallet.createRandom().address, - Wallet.createRandom().address, -]; export function useGetCouncilMembers(council: CouncilSlugs) { return useQuery({ @@ -27,7 +10,7 @@ export function useGetCouncilMembers(council: CouncilSlugs) { const members = (await getCouncilContract(council) .connect(motherShipProvider) .getCouncilMembers()) as string[]; - return members.concat(randomAddresses); + return members; }, enabled: !!council, staleTime: 900000, diff --git a/governance/ui/src/queries/useGetCouncilNominees.ts b/governance/ui/src/queries/useGetCouncilNominees.ts index 125a8c0cb..a0c2c3845 100644 --- a/governance/ui/src/queries/useGetCouncilNominees.ts +++ b/governance/ui/src/queries/useGetCouncilNominees.ts @@ -2,31 +2,14 @@ import { useQuery } from '@tanstack/react-query'; import { CouncilSlugs } from '../utils/councils'; import { motherShipProvider } from '../utils/providers'; import { getCouncilContract } from '../utils/contracts'; -import { Wallet } from 'ethers'; - -// TODO @dev remove -const randomAddresses = [ - Wallet.createRandom().address, - Wallet.createRandom().address, - Wallet.createRandom().address, - Wallet.createRandom().address, - Wallet.createRandom().address, - Wallet.createRandom().address, - Wallet.createRandom().address, - Wallet.createRandom().address, - Wallet.createRandom().address, - Wallet.createRandom().address, - Wallet.createRandom().address, - Wallet.createRandom().address, -]; export function useGetCouncilNominees(council: CouncilSlugs) { return useQuery({ queryKey: ['nominees', council], queryFn: async () => { - return (await getCouncilContract(council).connect(motherShipProvider).getNominees()).concat( - randomAddresses - ) as string[]; + return (await getCouncilContract(council) + .connect(motherShipProvider) + .getNominees()) as string[]; }, enabled: !!council, staleTime: 900000, diff --git a/governance/ui/src/queries/useGetIsNominated.ts b/governance/ui/src/queries/useGetIsNominated.ts index 621eae15f..3edc8239e 100644 --- a/governance/ui/src/queries/useGetIsNominated.ts +++ b/governance/ui/src/queries/useGetIsNominated.ts @@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import { motherShipProvider } from '../utils/providers'; import councils from '../utils/councils'; import { getCouncilContract } from '../utils/contracts'; +import { utils } from 'ethers'; export function useGetIsNominated(address?: string) { return useQuery({ @@ -28,7 +29,7 @@ export function useGetIsNominated(address?: string) { : councils[2], }; }, - enabled: !!address, + enabled: utils.isAddress(address || ''), staleTime: 900000, }); } diff --git a/governance/ui/src/queries/useGetUserBallot.ts b/governance/ui/src/queries/useGetUserBallot.ts index 8e5226b3d..08e7990b0 100644 --- a/governance/ui/src/queries/useGetUserBallot.ts +++ b/governance/ui/src/queries/useGetUserBallot.ts @@ -62,6 +62,6 @@ async function getBallot( ballot = { ...temp, council }; } // @ts-ignore - // TODO @MF check why TS is acting up + // TODO @dev check why TS is acting up return ballot; } diff --git a/governance/ui/src/queries/useGetUserVotingPower.ts b/governance/ui/src/queries/useGetUserVotingPower.ts index 120905e0a..db1f60a49 100644 --- a/governance/ui/src/queries/useGetUserVotingPower.ts +++ b/governance/ui/src/queries/useGetUserVotingPower.ts @@ -17,11 +17,13 @@ export function useGetUserVotingPower(council: CouncilSlugs) { try { const electionModule = getCouncilContract(council).connect(motherShipProvider); - const isMothership = network.id === 11155420; + const isMotherchain = network.id === (process.env.CI === 'true' ? 13001 : 2192); + const electionId = await electionModule.getEpochIndex(); - const ballot = isMothership + const ballot = isMotherchain ? await electionModule.getBallot(activeWallet.address, network.id, electionId) : await electionModule.connect(provider).getPreparedBallot(activeWallet.address); + if (ballot) { if (ballot?.votingPower?.gt(0)) { return { power: ballot.votingPower as BigNumber, isDeclared: true }; @@ -29,14 +31,14 @@ export function useGetUserVotingPower(council: CouncilSlugs) { return { power: ballot as BigNumber, isDeclared: true }; } } - const votingPower: BigNumber = isMothership + const votingPower: BigNumber = isMotherchain ? await electionModule .connect(provider) .callStatic.prepareBallotWithSnapshot( - SnapshotRecordContract(network.id)?.address, + SnapshotRecordContract(network.id, council)?.address, activeWallet?.address ) - : await SnapshotRecordContract(network.id) + : await SnapshotRecordContract(network.id, council) ?.connect(provider) .balanceOfOnPeriod(activeWallet.address, 1); return { power: votingPower, isDeclared: false }; diff --git a/governance/ui/src/queries/useGetVotingCandidates.ts b/governance/ui/src/queries/useGetVotingCandidates.ts deleted file mode 100644 index 24005010f..000000000 --- a/governance/ui/src/queries/useGetVotingCandidates.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; - -export function useGetVotingCandidates() { - return useQuery({ - queryKey: ['voting-candidates'], - queryFn: () => { - const selection = localStorage.getItem('voteSelection'); - const parsedSelection = JSON.parse(selection ? selection : '{}'); - return parsedSelection as Record; - }, - staleTime: 900000, - }); -} diff --git a/governance/ui/src/state/vote-card.ts b/governance/ui/src/state/vote-card.ts new file mode 100644 index 000000000..1ed258706 --- /dev/null +++ b/governance/ui/src/state/vote-card.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const voteCardState = atom({ + key: 'voteCardState', + default: false, +}); diff --git a/governance/ui/src/utils/abi.ts b/governance/ui/src/utils/abi.ts index 27ffa14fb..020c9cb6b 100644 --- a/governance/ui/src/utils/abi.ts +++ b/governance/ui/src/utils/abi.ts @@ -874,25 +874,6 @@ export const multicallABI = [ ]; export const electionModuleABITest = [ - { - inputs: [ - { - internalType: 'address', - name: 'user', - type: 'address', - }, - ], - name: 'getPreparedBallot', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, { inputs: [ { @@ -1441,6 +1422,17 @@ export const electionModuleABITest = [ name: 'UnregisteredEmitter', type: 'error', }, + { + inputs: [ + { + internalType: 'uint64', + name: '', + type: 'uint64', + }, + ], + name: 'UnsupportedNetwork', + type: 'error', + }, { inputs: [], name: 'ValueAlreadyInSet', @@ -1696,6 +1688,19 @@ export const electionModuleABITest = [ name: 'EpochStarted', type: 'event', }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint256', + name: 'gasLimit', + type: 'uint256', + }, + ], + name: 'GasLimitSet', + type: 'event', + }, { anonymous: false, inputs: [ @@ -1752,19 +1757,6 @@ export const electionModuleABITest = [ name: 'MessageReceived', type: 'event', }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'string', - name: 'message', - type: 'string', - }, - ], - name: 'MessageSent', - type: 'event', - }, { anonymous: false, inputs: [ @@ -1867,12 +1859,6 @@ export const electionModuleABITest = [ name: 'epochId', type: 'uint256', }, - { - indexed: false, - internalType: 'address[]', - name: 'candidates', - type: 'address[]', - }, ], name: 'VoteWithdrawn', type: 'event', @@ -1886,29 +1872,10 @@ export const electionModuleABITest = [ name: 'sender', type: 'address', }, - { - indexed: false, - internalType: 'address[]', - name: 'candidates', - type: 'address[]', - }, ], name: 'VoteWithdrawnSent', type: 'event', }, - { - inputs: [ - { - internalType: 'string', - name: 'message', - type: 'string', - }, - ], - name: '_recMessage', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, { inputs: [ { @@ -2048,11 +2015,6 @@ export const electionModuleABITest = [ name: 'chainId', type: 'uint256', }, - { - internalType: 'address[]', - name: 'candidates', - type: 'address[]', - }, ], name: '_recvWithdrawVote', outputs: [], @@ -2342,6 +2304,19 @@ export const electionModuleABITest = [ stateMutability: 'view', type: 'function', }, + { + inputs: [], + name: 'getGasLimit', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, { inputs: [], name: 'getNextElectionSettings', @@ -2741,22 +2716,22 @@ export const electionModuleABITest = [ stateMutability: 'payable', type: 'function', }, - { - inputs: [], - name: 'resolve', - outputs: [], - stateMutability: 'payable', - type: 'function', - }, { inputs: [ { - internalType: 'string', - name: 'message', - type: 'string', + internalType: 'uint16[]', + name: 'chainIds', + type: 'uint16[]', }, ], - name: 'sendMessage', + name: 'removeRegisteredEmitters', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'resolve', outputs: [], stateMutability: 'payable', type: 'function', @@ -2861,13 +2836,7 @@ export const electionModuleABITest = [ type: 'function', }, { - inputs: [ - { - internalType: 'address[]', - name: 'candidates', - type: 'address[]', - }, - ], + inputs: [], name: 'withdrawVote', outputs: [], stateMutability: 'payable', @@ -2894,6 +2863,11 @@ export const electionModuleABITest = [ name: 'InvalidSnapshotContract', type: 'error', }, + { + inputs: [], + name: 'InvalidWeightType', + type: 'error', + }, { inputs: [ { @@ -2942,6 +2916,69 @@ export const electionModuleABITest = [ name: 'SnapshotNotTaken', type: 'error', }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'snapshotContract', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'scale', + type: 'uint256', + }, + ], + name: 'ScaleSet', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'snapshotContract', + type: 'address', + }, + { + indexed: true, + internalType: 'bool', + name: 'enabled', + type: 'bool', + }, + { + indexed: false, + internalType: 'enum SnapshotVotePower.WeightType', + name: 'weight', + type: 'uint8', + }, + ], + name: 'SnapshotContractSet', + type: 'event', + }, + { + inputs: [ + { + internalType: 'address', + name: 'voter', + type: 'address', + }, + ], + name: 'getPreparedBallot', + outputs: [ + { + internalType: 'uint256', + name: 'power', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, { inputs: [ { @@ -2983,7 +3020,7 @@ export const electionModuleABITest = [ outputs: [ { internalType: 'uint256', - name: 'power', + name: 'votingPower', type: 'uint256', }, ], @@ -2997,6 +3034,29 @@ export const electionModuleABITest = [ name: 'snapshotContract', type: 'address', }, + { + internalType: 'uint256', + name: 'scale', + type: 'uint256', + }, + ], + name: 'setScale', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'snapshotContract', + type: 'address', + }, + { + internalType: 'enum SnapshotVotePower.WeightType', + name: 'weight', + type: 'uint8', + }, { internalType: 'bool', name: 'enabled', @@ -3621,6 +3681,30 @@ export const electionModuleABITest = [ stateMutability: 'view', type: 'function', }, + { + inputs: [ + { + internalType: 'address', + name: '_load_snapshotContract', + type: 'address', + }, + { + internalType: 'uint256', + name: 'ballotBalance', + type: 'uint256', + }, + ], + name: 'SnapshotVotePower_calculateVotingPower', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, { inputs: [ { @@ -3640,6 +3724,25 @@ export const electionModuleABITest = [ stateMutability: 'view', type: 'function', }, + { + inputs: [ + { + internalType: 'address', + name: '_load_snapshotContract', + type: 'address', + }, + ], + name: 'SnapshotVotePower_get_scale', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, { inputs: [ { @@ -3658,6 +3761,24 @@ export const electionModuleABITest = [ stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [ + { + internalType: 'address', + name: '_load_snapshotContract', + type: 'address', + }, + { + internalType: 'uint256', + name: 'val', + type: 'uint256', + }, + ], + name: 'SnapshotVotePower_set_scale', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, { inputs: [ { diff --git a/governance/ui/src/utils/contracts.ts b/governance/ui/src/utils/contracts.ts index eecfd2b6d..8b119f924 100644 --- a/governance/ui/src/utils/contracts.ts +++ b/governance/ui/src/utils/contracts.ts @@ -3,17 +3,17 @@ import { electionModuleABITest } from './abi'; import { CouncilSlugs } from './councils'; const SpartanCouncilContract = new Contract( - '0x0AFb5ef6DBe62702142Fa018BE0D21196E666796', + '0xBC85F11300A8EF619592fD678418Ec4eF26FBdFD', process.env.DEV ? electionModuleABITest : electionModuleABITest ); const AmbassadorCouncilContract = new Contract( - '0xB5BBEa9D6c0d57cc0061ee5A005F0863c0a43aad', + '0xCdbEf5753cE3CEbF361e143117e345ADd7498F80', process.env.DEV ? electionModuleABITest : electionModuleABITest ); const TreasuryCouncilContract = new Contract( - '0x43028D9Cc7e3BD425b15Ba335059F64595c3E000', + '0xe3aB2C6F1C9E46Fb53eD6b297c6fff68e935B161', process.env.DEV ? electionModuleABITest : electionModuleABITest ); @@ -36,31 +36,41 @@ export function getCouncilContract(council: CouncilSlugs) { } } -export const SnapshotRecordContract = (chainId: number) => { +export const SnapshotRecordContract = (chainId: number, council: CouncilSlugs) => { switch (chainId) { - case 421614: - return new Contract( - process.env.DEV === 'true' - ? '0x854AeE030eFEB8f9C4c778999174A33921613A4F' - : process.env.TESTNET === 'true' - ? '0x652e3a72945eDC8d2784c320771ffE0d090fa949' - : '0x652e3a72945eDC8d2784c320771ffE0d090fa949', - [ - 'function balanceOfOnPeriod(address, uint256) view returns (uint256)', - 'function setBalanceOfOnPeriod(address, uint256, uint256) external', - ] - ); - case 11155420: - return new Contract( - process.env.DEV === 'true' - ? '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9' - : process.env.TESTNET === 'true' - ? '0x652e3a72945eDC8d2784c320771ffE0d090fa949' - : '0x652e3a72945eDC8d2784c320771ffE0d090fa949', - [ - 'function balanceOfOnPeriod(address, uint256) view returns (uint256)', - 'function setBalanceOfOnPeriod(address, uint256, uint256) external', - ] - ); + case 13001: { + switch (council) { + case 'spartan': + return new Contract( + process.env.DEV === 'true' + ? '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9' + : '0x552E469B7C88cd501C08e7759d35dC58f08C9648', + [ + 'function balanceOfOnPeriod(address, uint256) view returns (uint256)', + 'function setBalanceOfOnPeriod(address, uint256, uint256) external', + ] + ); + case 'ambassador': + return new Contract( + process.env.DEV === 'true' + ? '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9' + : '0x3a0186E03137B9b971EC911350A0F2D88D24FDF2', + [ + 'function balanceOfOnPeriod(address, uint256) view returns (uint256)', + 'function setBalanceOfOnPeriod(address, uint256, uint256) external', + ] + ); + case 'treasury': + return new Contract( + process.env.DEV === 'true' + ? '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9' + : '0xC0bFA9aC792cF691734F7b2BD252d1c2B9fBa343', + [ + 'function balanceOfOnPeriod(address, uint256) view returns (uint256)', + 'function setBalanceOfOnPeriod(address, uint256, uint256) external', + ] + ); + } + } } }; diff --git a/governance/ui/src/utils/graph.ts b/governance/ui/src/utils/graph.ts deleted file mode 100644 index fc517cc09..000000000 --- a/governance/ui/src/utils/graph.ts +++ /dev/null @@ -1,16 +0,0 @@ -export async function graphQuery(query: string) { - const response = await fetch( - 'https://api.thegraph.com/subgraphs/name/rickk137/synthetix-election', - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ query }), - } - ); - return response.json(); -} - -// https://subgraph.satsuma-prod.com/ce5e03f52f3b/synthetix/synthetixio-governance-subgraph-sepolia/version/v0.0.1/api -// https://subgraph.satsuma-prod.com/ce5e03f52f3b/synthetix/synthetixio-governance-subgraph/version/v0.0.1/api diff --git a/governance/ui/src/utils/localstorage.ts b/governance/ui/src/utils/localstorage.ts index 0cee966b7..9dfa3303a 100644 --- a/governance/ui/src/utils/localstorage.ts +++ b/governance/ui/src/utils/localstorage.ts @@ -1,31 +1,34 @@ import { CouncilSlugs } from './councils'; -export const setCandidate = (candidate?: string, council?: CouncilSlugs) => { - if (!candidate || !council) return; - const selection = localStorage.getItem('voteSelection'); - if (!selection) localStorage.setItem('voteSelection', ''); - const parsedSelection = JSON.parse(selection ? selection : '{}'); - parsedSelection[council] = candidate; - localStorage.setItem('voteSelection', JSON.stringify(parsedSelection)); -}; +const key = 'voteSelection'; -export const removeCandidate = (council: CouncilSlugs) => { +export const setCandidate = (candidate?: string, council?: CouncilSlugs, network?: string) => { try { - const selection = localStorage.getItem('voteSelection'); - const parsedSelection = JSON.parse(selection ? selection : '{}'); - delete parsedSelection[council]; - localStorage.setItem('voteSelection', JSON.stringify(parsedSelection)); - } catch (err) { - console.error('tried to remove address that wasnt present in local storage: ', err); + if (!candidate || !council || !network) return; + const parsedSelection = JSON.parse(localStorage.getItem(key) || '{}'); + if (parsedSelection[network]) { + parsedSelection[network][council] = candidate; + } else { + parsedSelection[network] = { [council]: candidate }; + } + localStorage.setItem(key, JSON.stringify(parsedSelection)); + } catch (error) { + console.error('tried to add address but wasnt possible', candidate, error); } }; -export const getCandidate = (council: CouncilSlugs) => { +export const removeCandidate = (council?: CouncilSlugs, network?: string) => { + if (!council || !network) return; try { - const selection = localStorage.getItem('voteSelection'); - const parsedSelection = JSON.parse(selection ? selection : '{}'); - return parsedSelection[council] as string; + const parsedSelection = JSON.parse(localStorage.getItem(key) || '{}'); + delete parsedSelection[network][council]; + localStorage.setItem(key, JSON.stringify(parsedSelection)); } catch (err) { - console.error('cant get candidate for provided council: ', council, err); + console.error( + 'tried to remove address that wasnt present in local storage: ', + council, + network, + err + ); } }; diff --git a/governance/ui/src/utils/onboard.ts b/governance/ui/src/utils/onboard.ts index 5a22ff701..f9a0b6772 100644 --- a/governance/ui/src/utils/onboard.ts +++ b/governance/ui/src/utils/onboard.ts @@ -7,23 +7,16 @@ import { init } from '@web3-onboard/react'; import trezorModule from '@web3-onboard/trezor'; import walletConnectModule from '@web3-onboard/walletconnect'; -export const supportedNetworks = [421614, 11155420, 84532]; +export const supportedNetworks = [2192, 13001]; -export const chains = NETWORKS.filter((network) => supportedNetworks.includes(network.id)) - .map((network) => ({ +export const chains = NETWORKS.filter((network) => supportedNetworks.includes(network.id)).map( + (network) => ({ id: network.id, label: network.label, rpcUrl: network.rpcUrl(), token: network.token, - })) - .concat([ - { - id: 2192, - label: 'SNX Chain', - rpcUrl: 'http://127.0.0.1:19000', - token: 'SNX', - }, - ]); + }) +); export const onboard = init({ wallets: [ diff --git a/governance/ui/src/utils/providers.ts b/governance/ui/src/utils/providers.ts index 378968e65..10a7fa56c 100644 --- a/governance/ui/src/utils/providers.ts +++ b/governance/ui/src/utils/providers.ts @@ -1,4 +1,4 @@ -import { Wallet, providers } from 'ethers'; +import { providers } from 'ethers'; export const motherShipProvider = new providers.JsonRpcProvider( process.env.DEV === 'true' @@ -6,11 +6,6 @@ export const motherShipProvider = new providers.JsonRpcProvider( : process.env.CI === 'true' ? process.env.CI_RPC_MOTHERSHIP : process.env.TESTNET === 'true' - ? `https://optimism-sepolia.infura.io/v3/${process.env.INFURA_KEY}` - : `https://optimism-mainnet.infura.io/v3/${process.env.INFURA_KEY}` -); - -export const devSigner = new Wallet( - '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', - motherShipProvider + ? 'https://testnet.snaxchain.io/' + : 'https://mainnet.snaxchain.io/' ); diff --git a/liquidity/components/icons/SNXChainIcon/SNXChainIcon.tsx b/liquidity/components/icons/SNXChainIcon/SNXChainIcon.tsx new file mode 100644 index 000000000..cb6357c40 --- /dev/null +++ b/liquidity/components/icons/SNXChainIcon/SNXChainIcon.tsx @@ -0,0 +1,25 @@ +import { Icon, IconProps } from '@chakra-ui/react'; + +export const SNXChainIcon = ({ ...props }: IconProps) => { + return ( + + + + + + + ); +}; diff --git a/liquidity/components/icons/SNXChainIcon/index.tsx b/liquidity/components/icons/SNXChainIcon/index.tsx new file mode 100644 index 000000000..e1ca8df4d --- /dev/null +++ b/liquidity/components/icons/SNXChainIcon/index.tsx @@ -0,0 +1 @@ +export * from './SNXChainIcon'; diff --git a/liquidity/components/icons/index.ts b/liquidity/components/icons/index.ts index e5aa5daf4..b8fed247e 100644 --- a/liquidity/components/icons/index.ts +++ b/liquidity/components/icons/index.ts @@ -22,3 +22,4 @@ export * from './GithubIcon'; export * from './WarpcastIcon'; export * from './YoutubeIcon'; export * from './Sparkles'; +export * from './SNXChainIcon'; diff --git a/liquidity/lib/useBlockchain/useBlockchain.tsx b/liquidity/lib/useBlockchain/useBlockchain.tsx index d719b2d84..404fe4821 100644 --- a/liquidity/lib/useBlockchain/useBlockchain.tsx +++ b/liquidity/lib/useBlockchain/useBlockchain.tsx @@ -7,7 +7,7 @@ import { LogoIcon, OptimismIcon, ArbitrumIcon, - SNXIcon, + SNXChainIcon, } from '@snx-v3/icons'; import { INFURA_KEY as DEFAULT_INFURA_KEY } from '@snx-v3/constants'; import SynthetixIcon from './SynthetixIcon.svg'; @@ -69,10 +69,13 @@ export const NetworkIcon = ({ networkId, size = '24px', ...props }: NetworkIconP return ; case 42161: return ; - case 999: - return ; - default: + case 2192: + return ; + case 13001: + return ; + default: { return ; + } } }; @@ -215,6 +218,32 @@ export const ARBTHETIX: Network = { isTestnet: false, }; +export const SNAX: Network = { + id: 2192, + preset: 'main', + hexId: `0x${Number(2192).toString(16)}`, + token: 'ETH', + name: 'SNAX', + rpcUrl: () => 'https://mainnet.snaxchain.io/', + label: 'SNAX CHAIN', + isSupported: true, + publicRpcUrl: 'https://mainnet.snaxchain.io/', + isTestnet: false, +}; + +export const SNAXTESTNET: Network = { + id: 13001, + preset: 'main', + hexId: `0x${Number(13001).toString(16)}`, + token: 'ETH', + name: 'SNAX', + rpcUrl: () => 'https://testnet.snaxchain.io/', + label: 'SNAX CHAIN', + isSupported: true, + publicRpcUrl: 'https://testnet.snaxchain.io/', + isTestnet: true, +}; + export const NETWORKS: Network[] = [ BASE_ANDROMEDA, MAINNET, @@ -226,6 +255,8 @@ export const NETWORKS: Network[] = [ ARBITRUM_SEPOLIA, ARBITRUM, ARBTHETIX, + SNAX, + SNAXTESTNET, ]; export const deploymentsWithERC7412: string[] = [ diff --git a/yarn.lock b/yarn.lock index 73c545a93..1b14e9108 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5412,7 +5412,6 @@ __metadata: dependencies: "@chakra-ui/react": "npm:^2.8.2" "@cypress/code-coverage": "npm:^3.12.39" - "@snx-cy/onLogAdded": "workspace:*" "@snx-cy/printBrowserLogs": "workspace:*" "@synthetixio/v3-theme": "workspace:*" cypress: "npm:13.11.0"
Role {({ isOpen }) => ( <> - + {slugToName(activeCouncil)} @@ -51,20 +52,12 @@ export const CouncilsSelect = ({ activeCouncil }: { activeCouncil: CouncilSlugs > {councils.map((council) => ( navigate(`/councils/${council.slug}`)} - bg="navy.700" > - - + + {/* If on my votes page, spartan council is active by default for navigation */} - + @@ -77,14 +87,16 @@ export default function CouncilTabs({ activeCouncil }: { activeCouncil?: Council py="4" px="6" justifyContent="center" - mb="4" + mb="5" position="sticky" top="0px" zIndex={99} > {councils.map((council, index) => { - const newVoteCast = state[council.slug]; + const newVoteCast = state[networkForState] + ? state[networkForState][council.slug] + : ''; return ( {council.title} @@ -117,6 +143,7 @@ export default function CouncilTabs({ activeCouncil }: { activeCouncil?: Council address={userInformation[index].userInformation?.address} size={7} newVoteCast={newVoteCast} + isCouncilTabs={true} /> ) : ( councilPeriod === '2' && ( @@ -153,6 +180,7 @@ export default function CouncilTabs({ activeCouncil }: { activeCouncil?: Council isLoading={isLoading} councilPeriod={councilPeriod} schedule={schedule} + isInMyVotesPage={isInMyVotesPage} /> diff --git a/governance/ui/src/components/CouncilUser/CouncilUser.tsx b/governance/ui/src/components/CouncilUser/CouncilUser.tsx index 2b1ecc3ba..04d8f6074 100644 --- a/governance/ui/src/components/CouncilUser/CouncilUser.tsx +++ b/governance/ui/src/components/CouncilUser/CouncilUser.tsx @@ -2,7 +2,7 @@ import { Box, Flex, Image, Text } from '@chakra-ui/react'; import { useGetUserDetailsQuery } from '../../queries'; import councils, { CouncilSlugs } from '../../utils/councils'; import { ProfilePicture } from '../UserProfileCard/ProfilePicture'; -import { truncateAddress } from '@snx-v3/formatters'; +import { prettyString } from '@snx-v3/format'; export default function CouncilUser({ address, @@ -51,17 +51,17 @@ export default function CouncilUser({ )} - + {council.title} - {user ? (user?.username ? user.username : truncateAddress(user?.address)) : 'No Vote'} + {user ? (user?.username ? user.username : prettyString(user?.address, 6, 4)) : 'No Vote'} diff --git a/governance/ui/src/components/EditNomination/EditNomination.tsx b/governance/ui/src/components/EditNomination/EditNomination.tsx index 1a5daf5d5..7faa3c3f7 100644 --- a/governance/ui/src/components/EditNomination/EditNomination.tsx +++ b/governance/ui/src/components/EditNomination/EditNomination.tsx @@ -1,7 +1,7 @@ import { Button, Flex, FlexProps, Heading, IconButton, Image, Text } from '@chakra-ui/react'; import councils, { CouncilSlugs } from '../../utils/councils'; import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import useEditNomination from '../../mutations/useEditNomination'; import { CloseIcon } from '@chakra-ui/icons'; import { useGetIsNominated } from '../../queries/useGetIsNominated'; @@ -14,7 +14,10 @@ interface EditNominationProps extends FlexProps { } export default function EditNomination({ activeCouncil, ...props }: EditNominationProps) { - const [selectedCouncil, setSelectedCouncil] = useState(undefined); + const location = useLocation(); + const [selectedCouncil, setSelectedCouncil] = useState( + location.pathname.includes(activeCouncil) ? null : activeCouncil + ); const [showConfirm, setShowConfirm] = useState(false); const navigate = useNavigate(); const { activeWallet } = useWallet(); @@ -30,13 +33,14 @@ export default function EditNomination({ activeCouncil, ...props }: EditNominati flexDirection="column" bg="navy.700" w="100%" - maxW="451px" - h="fit-content" + maxW={{ base: '100%', xl: '451px' }} + h="612px" borderColor="gray.900" borderWidth="1px" borderStyle="solid" rounded="base" p="6" + data-cy="edit-nomination" {...props} > - {!selectedCouncil && ( - council.slug === selectedCouncil)?.image} - w="6" - h="6" - /> - )} + {!selectedCouncil && } {!selectedCouncil diff --git a/governance/ui/src/components/EditNomination/EditNominationConfirmation.tsx b/governance/ui/src/components/EditNomination/EditNominationConfirmation.tsx index 7822929e2..ada18f43b 100644 --- a/governance/ui/src/components/EditNomination/EditNominationConfirmation.tsx +++ b/governance/ui/src/components/EditNomination/EditNominationConfirmation.tsx @@ -15,7 +15,7 @@ export default function EditNominationConfirmation({ activeCouncil, setShowConfirm, }: { - selectedCouncil?: CouncilSlugs; + selectedCouncil?: CouncilSlugs | null; activeCouncil: CouncilSlugs; setShowConfirm: Dispatch>; }) { @@ -24,7 +24,7 @@ export default function EditNominationConfirmation({ const { data: nominationInformation } = useGetIsNominated(activeWallet?.address); const { data: user } = useGetUserDetailsQuery(activeWallet?.address); - const { mutate, isPending, isSuccess } = useEditNomination({ + const { mutateAsync, isPending, isSuccess } = useEditNomination({ currentNomination: nominationInformation?.council.slug, nextNomination: selectedCouncil, }); @@ -32,9 +32,9 @@ export default function EditNominationConfirmation({ useEffect(() => { if (isSuccess) { setShowConfirm(false); - navigate(`/councils/${activeCouncil}?nominate=false`); + navigate(`/councils/${selectedCouncil}?nominate=false`); } - }, [isSuccess, setShowConfirm, navigate, activeCouncil]); + }, [isSuccess, setShowConfirm, navigate, selectedCouncil]); return ( <> @@ -54,13 +54,21 @@ export default function EditNominationConfirmation({ > - - {user?.ens || prettyString(user!.address)} + + {user?.username || prettyString(user?.address || '')} - Nomination Wallet: {prettyString(user!.address)} + Nomination Wallet: {prettyString(user?.address || '')} - + Chose which governing body you would like to represent if chosen as an elected member: @@ -161,16 +169,17 @@ export default function EditNominationConfirmation({ {isPending ? ( - - loading - - + ) : ( <> diff --git a/governance/ui/src/components/Header/Header.tsx b/governance/ui/src/components/Header/Header.tsx index 4079da911..6e1978560 100644 --- a/governance/ui/src/components/Header/Header.tsx +++ b/governance/ui/src/components/Header/Header.tsx @@ -1,65 +1,18 @@ import { Button, Flex, useColorMode, Show, Link } from '@chakra-ui/react'; import { useNavigate } from 'react-router-dom'; -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import PeriodCountdown from '../PeriodCountdown/PeriodCountdown'; -import { useGetUserBallot } from '../../queries'; -import { useQueryClient } from '@tanstack/react-query'; import councils from '../../utils/councils'; -import { useWallet, useNetwork } from '../../queries/useWallet'; +import { useWallet } from '../../queries/useWallet'; import { NetworkController } from './NetworkController'; import { SNXHeaderIcon, SNXHeaderIconSmall } from '../Icons'; export function Header() { const navigate = useNavigate(); - const queryClient = useQueryClient(); const { activeWallet, walletsInfo, connect } = useWallet(); - const { network } = useNetwork(); const { colorMode, toggleColorMode } = useColorMode(); - const [localStorageUpdated, setLocalStorageUpdated] = useState(false); - const [fetchedNetwork, setFetchedNetwork] = useState([]); - - const { data: ballots, isFetched } = useGetUserBallot(['spartan', 'ambassador', 'treasury']); - - useEffect(() => { - if ( - activeWallet?.address && - network?.id && - isFetched && - (!localStorageUpdated || !fetchedNetwork.includes(network?.id)) - ) { - setLocalStorageUpdated(true); - setFetchedNetwork([...fetchedNetwork, network?.id]); - const selection = localStorage.getItem('voteSelection'); - if (!selection) localStorage.setItem('voteSelection', ''); - const parsedSelection = JSON.parse(selection ? selection : '{}'); - ballots?.forEach((ballot, index) => { - const council = - index === 0 - ? 'spartan' - : index === 1 - ? 'ambassador' - : index === 2 - ? 'grants' - : 'treasury'; - parsedSelection[council] = ballot.votedCandidates[0] - ? ballot.votedCandidates[0] - : parsedSelection[council]; - }); - localStorage.setItem('voteSelection', JSON.stringify(parsedSelection)); - queryClient.refetchQueries({ queryKey: ['voting-candidates'] }); - } - }, [ - activeWallet?.address, - network?.id, - localStorageUpdated, - isFetched, - ballots, - queryClient, - fetchedNetwork, - ]); - useEffect(() => { if (colorMode === 'light') { toggleColorMode(); @@ -90,7 +43,7 @@ export function Header() { bg="navy.700" h="65px" alignItems="center" - px={{ base: '3', lg: 6 }} + px={{ base: '4', md: 6 }} py={{ base: '4' }} borderBottomWidth="1px" borderStyle="solid" diff --git a/governance/ui/src/components/Header/NetworkController.tsx b/governance/ui/src/components/Header/NetworkController.tsx index d040c38cf..8d95158d4 100644 --- a/governance/ui/src/components/Header/NetworkController.tsx +++ b/governance/ui/src/components/Header/NetworkController.tsx @@ -15,7 +15,7 @@ import { Text, useDisclosure, } from '@chakra-ui/react'; -import { NetworkIcon, useNetwork, useWallet, NETWORKS, Network } from '@snx-v3/useBlockchain'; +import { NetworkIcon, useNetwork, useWallet, NETWORKS } from '@snx-v3/useBlockchain'; import { prettyString } from '@snx-v3/format'; import { useLocalStorage } from '@snx-v3/useLocalStorage'; import { CopyIcon } from '@chakra-ui/icons'; @@ -27,25 +27,12 @@ import Blockies from 'react-blockies'; import '../../pages/index.css'; import { useGetUserDetailsQuery } from '../../queries'; -const SNXChain: Network = { - hexId: '999', - id: 999, - isSupported: true, - isTestnet: false, - label: 'SNX Chain', - name: 'SNXChain', - preset: '999-main', - publicRpcUrl: 'http://127.0.0.1:19000', - rpcUrl: () => 'http://127.0.0.1:19000', - token: 'SNX', -}; - -const mainnets = NETWORKS.filter(({ isSupported, isTestnet }) => isSupported && !isTestnet) - .filter((network) => supportedNetworks.includes(network.id)) - .concat(SNXChain); -const testnets = NETWORKS.filter(({ isTestnet }) => isTestnet) - .filter((network) => supportedNetworks.includes(network.id)) - .concat(SNXChain); +const mainnets = NETWORKS.filter(({ isSupported, isTestnet }) => isSupported && !isTestnet).filter( + (network) => supportedNetworks.includes(network.id) +); +const testnets = NETWORKS.filter(({ isTestnet }) => isTestnet).filter((network) => + supportedNetworks.includes(network.id) +); export function NetworkController() { const { isOpen, onOpen, onClose } = useDisclosure(); diff --git a/governance/ui/src/components/MyVoteRow/MyVoteRow.tsx b/governance/ui/src/components/MyVoteRow/MyVoteRow.tsx index c27b5db12..13003ec9f 100644 --- a/governance/ui/src/components/MyVoteRow/MyVoteRow.tsx +++ b/governance/ui/src/components/MyVoteRow/MyVoteRow.tsx @@ -4,18 +4,24 @@ import { AddIcon, ArrowForwardIcon, CloseIcon } from '@chakra-ui/icons'; import { useNavigate } from 'react-router-dom'; import CouncilUser from '../CouncilUser/CouncilUser'; import { useVoteContext } from '../../context/VoteContext'; -import { useGetUserBallot } from '../../queries'; +import { useGetUserBallot, useNetwork } from '../../queries'; export default function MyVoteRow({ councilSlug, period, + isLast, }: { councilSlug: CouncilSlugs; period?: string; + isLast: boolean; }) { const navigate = useNavigate(); const { data: ballot } = useGetUserBallot(councilSlug); + const { network } = useNetwork(); + const networkForState = network?.id.toString() || '2192'; const { dispatch, state } = useVoteContext(); + const stateForNetwork = + !!state && !!state[networkForState] ? state[networkForState][councilSlug] : undefined; return ( - - {ballot?.votedCandidates[0] && state[councilSlug] === 'remove' && ( + {ballot?.votedCandidates[0] && stateForNetwork === 'remove' && ( <> - + )} - {!state[councilSlug] && !ballot?.votedCandidates[0] ? ( + {!stateForNetwork && !ballot?.votedCandidates[0] ? ( } @@ -56,15 +62,28 @@ export default function MyVoteRow({ } - data-cy="remove-vote-button" + data-cy={`remove-vote-button-${councilSlug}`} variant="outlined" isDisabled={period !== '2'} onClick={(e) => { e.stopPropagation(); - dispatch({ - type: councilSlug.toUpperCase(), - payload: state[councilSlug] === 'remove' ? undefined : 'remove', - }); + if (!!ballot?.votedCandidates[0]) { + dispatch({ + type: councilSlug.toUpperCase(), + payload: { + action: 'remove', + network: networkForState, + }, + }); + } else { + dispatch({ + type: councilSlug.toUpperCase(), + payload: { + action: stateForNetwork === 'remove' ? 'remove' : undefined, + network: networkForState, + }, + }); + } }} /> )} diff --git a/governance/ui/src/components/MyVotesBox/MyVotesBox.tsx b/governance/ui/src/components/MyVotesBox/MyVotesBox.tsx index 27b641cd9..0068860fa 100644 --- a/governance/ui/src/components/MyVotesBox/MyVotesBox.tsx +++ b/governance/ui/src/components/MyVotesBox/MyVotesBox.tsx @@ -2,7 +2,7 @@ import { Button, Fade, Flex, Heading } from '@chakra-ui/react'; import councils from '../../utils/councils'; import { useNavigate } from 'react-router-dom'; import MyVoteRow from '../MyVoteRow/MyVoteRow'; -import { VoteState } from '../../context/VoteContext'; +import { VoteStateForNetwork } from '../../context/VoteContext'; export default function MyVotesBox({ closeCart, @@ -11,7 +11,7 @@ export default function MyVotesBox({ period, }: { closeCart: () => void; - votes?: VoteState; + votes?: VoteStateForNetwork; isMouseOnDropdown: (val: boolean) => void; period?: string; }) { @@ -40,15 +40,16 @@ export default function MyVotesBox({ > My Votes - + {Object.values(!!votes ? votes : {}).filter((vote) => !!vote).length}/{councils.length} - {councils.map((council) => ( + {councils.map((council, index) => ( ))} ) : ( ) : councilPeriod === '2' && isNominatedFetched ? ( diff --git a/governance/ui/src/components/UserProfileCard/ProfilePicture/ProfilePicture.tsx b/governance/ui/src/components/UserProfileCard/ProfilePicture/ProfilePicture.tsx index 31cb91249..4fee161a2 100644 --- a/governance/ui/src/components/UserProfileCard/ProfilePicture/ProfilePicture.tsx +++ b/governance/ui/src/components/UserProfileCard/ProfilePicture/ProfilePicture.tsx @@ -9,6 +9,7 @@ interface ProfilePictureInterface { mr?: string; ml?: string; newVoteCast?: string; + isCouncilTabs?: boolean; } export const ProfilePicture = ({ @@ -19,6 +20,7 @@ export const ProfilePicture = ({ mr, ml, newVoteCast, + isCouncilTabs, }: ProfilePictureInterface) => { return ( <> @@ -56,20 +58,13 @@ export const ProfilePicture = ({ borderRadius: '99999px', }, }} + filter={isCouncilTabs ? 'grayscale(1)' : ''} zIndex={10} position="relative" data-cy={`user-blockies-council-tabs-${address || newVoteCast}`} > {!!newVoteCast && ( - + )} - + { + const [_, setVoteCard] = useRecoilState(voteCardState); + const [tooltipLabel, setTooltipLabel] = useState('Copy Profile Link'); + const [isTooltipOpen, setIsTooltipOpen] = useState(false); + const { network } = useNetwork(); + const networkForState = network?.id.toString() || '2192'; const { dispatch, state } = useVoteContext(); const navigate = useNavigate(); const { data: ballot } = useGetUserBallot(activeCouncil); - const isSelected = state[activeCouncil]?.toLowerCase() === userData?.address?.toLowerCase(); + const isSelected = state[networkForState] + ? state[networkForState][activeCouncil]?.toLowerCase() === userData?.address?.toLowerCase() + : false; const isAlreadyVoted = !!ballot?.votedCandidates && @@ -51,19 +61,23 @@ export const UserProfileDetails = ({ position="absolute" top="0px" right="0px" + _hover={{}} /> - } - variant="ghost" - position="absolute" - top="4px" - right="32px" - aria-label="edit-profile" - onClick={() => navigate(`/profile`)} - data-cy="edit-icon-user-profile-details" - color="white" - /> + {isOwn && ( + } + variant="ghost" + position="absolute" + top="4px" + right="32px" + aria-label="edit-profile" + onClick={() => navigate(`/profile`)} + data-cy="edit-icon-user-profile-details" + color="white" + _hover={{}} + /> + )} @@ -98,11 +112,21 @@ export const UserProfileDetails = ({ github={userData?.github} twitter={userData?.twitter} /> - { - navigator.clipboard.writeText(window.location.href); - }} - /> + + {/* @dev charka icon tickery */} +
+ setIsTooltipOpen(true)} + onMouseLeave={() => setTimeout(() => setIsTooltipOpen(false), 3000)} + onClick={() => { + navigator.clipboard.writeText(window.location.href); + setTooltipLabel('Profile Link Copied'); + setTimeout(() => setTooltipLabel('Copy Profile Link'), 5000); + }} + /> +
+
@@ -142,6 +166,7 @@ export const UserProfileDetails = ({ {councilPeriod === '2' ? ( )} + + {!isOwn && councilPeriod !== '2' && ( + + )} ); diff --git a/governance/ui/src/components/UserProfileForm/UserProfileEditPreview.tsx b/governance/ui/src/components/UserProfileForm/UserProfileEditPreview.tsx index 024a7d970..f1fb40b2e 100644 --- a/governance/ui/src/components/UserProfileForm/UserProfileEditPreview.tsx +++ b/governance/ui/src/components/UserProfileForm/UserProfileEditPreview.tsx @@ -10,10 +10,12 @@ export default function UserProfileEditPreview({ activeWallet, isPending, onSave, + isDirty, }: { onSave: () => void; isPending: boolean; activeWallet?: string; + isDirty: boolean; userData: GetUserDetails; }) { return ( @@ -21,7 +23,7 @@ export default function UserProfileEditPreview({ border="1px solid" borderColor={{ base: 'navy.700', xl: 'cyan.500' }} rounded="base" - p="4" + p={{ base: 0, xl: '4' }} bg="navy.700" flexDir="column" > @@ -90,9 +92,7 @@ export default function UserProfileEditPreview({ )} -
diff --git a/governance/ui/src/components/UserProfileForm/UserProfileForm.tsx b/governance/ui/src/components/UserProfileForm/UserProfileForm.tsx index c03f8e69e..20c77df40 100644 --- a/governance/ui/src/components/UserProfileForm/UserProfileForm.tsx +++ b/governance/ui/src/components/UserProfileForm/UserProfileForm.tsx @@ -19,8 +19,8 @@ import { useForm } from 'react-hook-form'; import useUpdateUserDetailsMutation from '../../mutations/useUpdateUserDetailsMutation'; import { GetUserDetails, useGetUserDetailsQuery } from '../../queries/'; import { useEffect } from 'react'; +import { unstable_useBlocker as useBlocker } from 'react-router-dom'; import { useWallet } from '../../queries/useWallet'; -import { ProfilePicture } from '../UserProfileCard/ProfilePicture'; import UserProfileEditPreview from './UserProfileEditPreview'; export function UserProfileForm() { @@ -28,8 +28,22 @@ export function UserProfileForm() { const { activeWallet } = useWallet(); const { data: user, isLoading } = useGetUserDetailsQuery(activeWallet?.address); const mutation = useUpdateUserDetailsMutation(); + const { + isOpen: blockerIsOpen, + onClose: onBlockerClose, + onOpen: onBlockerOpen, + } = useDisclosure({ id: 'blocker' }); - const { register, getValues, setValue, watch } = useForm({ + const blocker = useBlocker(() => { + if (formState.isDirty) { + onBlockerOpen(); + return true; + } + onBlockerClose(); + return false; + }); + + const { register, getValues, setValue, watch, formState } = useForm({ defaultValues: { address: user?.about, username: user?.username, @@ -99,9 +113,12 @@ export function UserProfileForm() { rounded="base" border="1px solid" borderColor="gray.900" + mt="9" > - - + {/* + + + Avatar @@ -115,16 +132,16 @@ export function UserProfileForm() { })} placeholder="QmSHZw..." /> - - - + */} + + Username @@ -132,30 +149,31 @@ export function UserProfileForm() { -
+ Discord - -
-
+ + + Twitter - -
-
+ + + Github - -
+ +
Wallet Address @@ -190,6 +210,7 @@ export function UserProfileForm() { {...register('delegationPitch')} placeholder="eg: How am I going to make a difference at Synthetix" data-cy="governance-pitch-input" + h="253px" /> @@ -234,6 +256,7 @@ export function UserProfileForm() { activeWallet={activeWallet?.address} isPending={mutation.isPending} userData={userData} + isDirty={formState.isDirty} onSave={() => { handleOnFormSave(); }} @@ -250,12 +273,54 @@ export function UserProfileForm() { activeWallet={activeWallet?.address} isPending={mutation.isPending} userData={userData} + isDirty={formState.isDirty} onSave={() => { handleOnFormSave(); }} /> + {blocker.state === 'blocked' && ( + + + + + onBlockerClose()} + size="xs" + aria-label="close button" + icon={} + variant="ghost" + colorScheme="whiteAlpha" + color="white" + position="absolute" + top="10px" + right="10px" + /> + You have unsaved changes + + You have unsaved changes. Do you want to leave without saving? + + + + + + + )} ); } diff --git a/governance/ui/src/components/UserTableView/UserTableView.tsx b/governance/ui/src/components/UserTableView/UserTableView.tsx index ca6ae1178..d5684bc42 100644 --- a/governance/ui/src/components/UserTableView/UserTableView.tsx +++ b/governance/ui/src/components/UserTableView/UserTableView.tsx @@ -1,10 +1,9 @@ -import { Button, Flex, Spinner, Text, Th, Tr } from '@chakra-ui/react'; +import { Button, Flex, Icon, Text, Td, Tr } from '@chakra-ui/react'; import { GetUserDetails } from '../../queries/useGetUserDetailsQuery'; import { Badge } from '../Badge'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { useGetCurrentPeriod } from '../../queries/useGetCurrentPeriod'; import { CouncilSlugs } from '../../utils/councils'; -import { useGetElectionSettings } from '../../queries/useGetElectionSettings'; import { ProfilePicture } from '../UserProfileCard/ProfilePicture'; import { prettyString } from '@snx-v3/format'; @@ -22,11 +21,10 @@ export default function UserTableView({ const navigate = useNavigate(); const [searchParams] = useSearchParams(); const { data: councilPeriod } = useGetCurrentPeriod(activeCouncil); - const { data: councilSettings } = useGetElectionSettings(activeCouncil); + const isSelected = searchParams.get('view') === user.address; const councilIsInAdminOrVoting = councilPeriod === '2' || councilPeriod === '0'; - if (!user) return ; return (
- {place < 10 ? `#${place + 1}` : '-'} - - {' '} - - {user.username ? user.username : prettyString(user.address)} - - + {' '} + + {user.username ? user.username : prettyString(user.address)} + + + + {councilIsInAdminOrVoting && ( + - - {councilSettings && councilSettings?.epochSeatCount > place && Member} - - - )} - {councilIsInAdminOrVoting && ( - TODO - + TODO - + - - Your Vote TODO -