diff --git a/src/helpers/environment/Environment/Environment.ts b/src/helpers/environment/Environment/Environment.ts index e34213489..b088bcea8 100644 --- a/src/helpers/environment/Environment/Environment.ts +++ b/src/helpers/environment/Environment/Environment.ts @@ -68,6 +68,15 @@ export class Environment { }), ); + public static getGovernanceSubgraphUrl = (): string => { + const subgraphApiKey = this.getSubgraphApiKey(); + return this._get({ + first: true, + key: "VITE_GOVERNANCE_SUBGRAPH_URL", + fallback: `https://gateway.thegraph.com/api/${subgraphApiKey}/subgraphs/id/AQoLCXebY1Ga7DrqVaVQ85KMwS7iFof73tv9XMVGRtyJ`, + }); + }; + public static getNodeUrls = (networkId: NetworkId) => { switch (networkId) { case NetworkId.MAINNET: diff --git a/src/helpers/index.tsx b/src/helpers/index.tsx index 7beabc257..1e70ae65b 100644 --- a/src/helpers/index.tsx +++ b/src/helpers/index.tsx @@ -42,9 +42,10 @@ export function shorten(str: string) { } export function formatCurrency(c: number, precision = 0, currency = "USD") { + console.log(c, precision, currency); const formatted = new Intl.NumberFormat("en-US", { - style: currency === "USD" ? "currency" : undefined, - currency, + style: currency === "USD" || currency === "" ? "currency" : undefined, + currency: "USD", maximumFractionDigits: precision, minimumFractionDigits: precision, }).format(c); diff --git a/src/views/Governance/Components/Status.tsx b/src/views/Governance/Components/Status.tsx index 73b771191..ddebf01b3 100644 --- a/src/views/Governance/Components/Status.tsx +++ b/src/views/Governance/Components/Status.tsx @@ -1,14 +1,14 @@ -import { Box, Typography } from "@mui/material"; +import { Box, Tooltip, Typography } from "@mui/material"; import { Paper } from "@olympusdao/component-library"; import { useGetCanceledTime } from "src/views/Governance/hooks/useGetCanceledTime"; import { useGetExecutedTime } from "src/views/Governance/hooks/useGetExecutedTime"; import { useGetProposalDetails } from "src/views/Governance/hooks/useGetProposalDetails"; -import { useGetProposal } from "src/views/Governance/hooks/useGetProposals"; +import { useGetProposalFromSubgraph } from "src/views/Governance/hooks/useGetProposalFromSubgraph"; import { useGetQueuedTime } from "src/views/Governance/hooks/useGetQueuedTime"; import { useGetVetoedTime } from "src/views/Governance/hooks/useGetVetoedTime"; export const Status = ({ proposalId }: { proposalId: number }) => { - const { data: proposal } = useGetProposal({ proposalId }); + const { data: proposal } = useGetProposalFromSubgraph({ proposalId: proposalId.toString() }); const { data: proposalDetails } = useGetProposalDetails({ proposalId }); const { data: queueTime } = useGetQueuedTime({ proposalId }); const { data: executedTime } = useGetExecutedTime({ proposalId, status: proposalDetails?.status }); @@ -29,19 +29,23 @@ export const Status = ({ proposalId }: { proposalId: number }) => { {proposal?.createdAtBlock?.toLocaleString()} Published Onchain -
- {proposalDetails?.startDate?.toLocaleString()} - Voting Period Starts -
- {(proposalDetails?.endDate || placeholderEndDate) && ( +
- - {proposalDetails.endDate - ? proposalDetails?.endDate?.toLocaleString() - : placeholderEndDate?.toLocaleString()} - - Voting Period Ends + {proposalDetails?.startDate?.toLocaleString()} + Voting Period Starts
+
+ {(proposalDetails?.endDate || placeholderEndDate) && ( + +
+ + {proposalDetails.endDate + ? proposalDetails?.endDate?.toLocaleString() + : placeholderEndDate?.toLocaleString()} + + Voting Period Ends +
+
)} {proposalDetails?.status === "Queued" && ( <> @@ -52,7 +56,7 @@ export const Status = ({ proposalId }: { proposalId: number }) => { )}
- {proposalDetails.eta.toLocaleString()} + {proposalDetails.etaDate.toLocaleString()} Estimated Execution Time
diff --git a/src/views/Governance/Proposals/VoteDetails.tsx b/src/views/Governance/Proposals/VoteDetails.tsx new file mode 100644 index 000000000..e6c546e1d --- /dev/null +++ b/src/views/Governance/Proposals/VoteDetails.tsx @@ -0,0 +1,104 @@ +import { + Box, + Paper, + Tab, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Tabs, + Typography, +} from "@mui/material"; +import React, { useState } from "react"; +import { useParams } from "react-router-dom"; +import { useGetVotes } from "src/views/Governance/hooks/useGetVotes"; +import { VoteRow } from "src/views/Governance/Proposals/VoteRow"; + +// Data for the tables, including a 'reason' field for each row +const tablesData = [ + { + id: "For", + contractValue: 1, + }, + { + id: "Against", + contractValue: 0, + }, + { + id: "Abstain", + contractValue: 2, + }, +]; + +const TabPanel = (props: { children: React.ReactNode; value: number; index: number }) => { + const { children, value, index, ...other } = props; + return ( + + ); +}; + +export default function GovernanceTable() { + const { id } = useParams(); + const [tabIndex, setTabIndex] = useState(0); + const [supportValue, setSupportValue] = useState(1); + const { data: voteData } = useGetVotes({ proposalId: id, support: supportValue }); + + const handleTabChange = (event: React.SyntheticEvent, newIndex: number) => { + setSupportValue(tablesData[newIndex].contractValue); + setTabIndex(newIndex); + }; + + return ( + + is loading + TabIndicatorProps={{ style: { display: "none" } }} + centered + > + {" "} + {tablesData.map((table, index) => ( + + ))} + + + {tablesData.map((table, index) => ( + + + + + + {tablesData[tabIndex].id} + Votes + + + + {voteData?.length ? ( + voteData.map((row, i) => ( + + + + )) + ) : ( + + + No votes yet + + + )} + +
+
+
+ ))} +
+ ); +} diff --git a/src/views/Governance/Proposals/VoteRow.tsx b/src/views/Governance/Proposals/VoteRow.tsx new file mode 100644 index 000000000..771fa809a --- /dev/null +++ b/src/views/Governance/Proposals/VoteRow.tsx @@ -0,0 +1,37 @@ +import { Box, Link, TableCell, Tooltip, Typography } from "@mui/material"; +import { formatEther } from "ethers/lib/utils.js"; +import { abbreviatedNumber } from "src/helpers"; +import { truncateEthereumAddress } from "src/helpers/truncateAddress"; +import { useEnsName } from "wagmi"; + +export const VoteRow = ({ + voter, + reason, + votes, + tx, +}: { + voter: string; + reason?: string; + votes: string; + tx: string; +}) => { + const { data: ensName } = useEnsName({ address: voter as `0x${string}` }); + return ( + <> + + + + {ensName || truncateEthereumAddress(voter)} + + + {/* Render the reason if provided, and style it as a comment */} + {reason && ( + + "{reason}" + + )} + + {abbreviatedNumber.format(Number(formatEther(votes) || 0))} gOHM + + ); +}; diff --git a/src/views/Governance/Proposals/index.tsx b/src/views/Governance/Proposals/index.tsx index d4cffa21d..25976eea3 100644 --- a/src/views/Governance/Proposals/index.tsx +++ b/src/views/Governance/Proposals/index.tsx @@ -20,13 +20,14 @@ import { useActivateProposal } from "src/views/Governance/hooks/useActivatePropo import { useExecuteProposal } from "src/views/Governance/hooks/useExecuteProposal"; import { useGetCurrentBlockTime } from "src/views/Governance/hooks/useGetCurrentBlockTime"; import { useGetProposalDetails } from "src/views/Governance/hooks/useGetProposalDetails"; -import { useGetProposal } from "src/views/Governance/hooks/useGetProposals"; +import { useGetProposalFromSubgraph } from "src/views/Governance/hooks/useGetProposalFromSubgraph"; import { useQueueProposal } from "src/views/Governance/hooks/useQueueProposal"; +import VoteDetails from "src/views/Governance/Proposals/VoteDetails"; import { useEnsName, useNetwork, useSwitchNetwork } from "wagmi"; export const ProposalPage = () => { const { id } = useParams(); - const { data: proposal } = useGetProposal({ proposalId: Number(id) }); + const { data: proposal } = useGetProposalFromSubgraph({ proposalId: id }); const { data: proposalDetails } = useGetProposalDetails({ proposalId: Number(id) }); const { data: ensAddress } = useEnsName({ address: proposalDetails?.proposer as `0x${string}` }); const [voteModalOpen, setVoteModalOpen] = useState(false); @@ -160,7 +161,7 @@ export const ProposalPage = () => { > - {/* */} + @@ -197,11 +198,8 @@ export const ProposalPage = () => { )} {tabIndex === 2 && ( <> - - Comments - - No comments yet + )} diff --git a/src/views/Governance/helpers/normalizeProposal.ts b/src/views/Governance/helpers/normalizeProposal.ts new file mode 100644 index 000000000..a0cb77109 --- /dev/null +++ b/src/views/Governance/helpers/normalizeProposal.ts @@ -0,0 +1,20 @@ +import { Proposal } from "src/views/Governance/hooks/useGetProposalFromSubgraph"; + +// Normalizes the proposal data to match the onchain format +export const normalizeProposal = (proposal: Proposal) => { + return { + createdAtBlock: new Date(Number(proposal.blockTimestamp) * 1000), + details: { + id: proposal.proposalId, + proposer: proposal.proposer, + targets: proposal.targets, + values: proposal.values, + signatures: proposal.signatures, + calldatas: proposal.calldatas, + startBlock: proposal.startBlock, + description: proposal.description, + }, + title: proposal.description.split(/#+\s|\n/g)[1] || `${proposal.description.slice(0, 20)}...`, + txHash: proposal.transactionHash, + }; +}; diff --git a/src/views/Governance/hooks/useGetProposalDetails.tsx b/src/views/Governance/hooks/useGetProposalDetails.tsx index b9fcaee7b..c4f61cd32 100644 --- a/src/views/Governance/hooks/useGetProposalDetails.tsx +++ b/src/views/Governance/hooks/useGetProposalDetails.tsx @@ -15,8 +15,8 @@ export const useGetProposalDetails = ({ proposalId }: { proposalId: number }) => const blockTime = await archiveProvider.getBlock("latest"); const endDateBlockTimestamp = await archiveProvider.getBlock(Number(proposalDetails.endBlock)); const startDateBlockTimestamp = await archiveProvider.getBlock(Number(proposalDetails.startBlock)); - const startDate = getDateFromBlock(Number(proposalDetails.startBlock), blockTime.number, 15, blockTime.timestamp); - const endDate = getDateFromBlock(Number(proposalDetails.endBlock), blockTime.number, 15, blockTime.timestamp); + const startDate = getDateFromBlock(Number(proposalDetails.startBlock), blockTime.number, 12, blockTime.timestamp); + const endDate = getDateFromBlock(Number(proposalDetails.endBlock), blockTime.number, 12, blockTime.timestamp); return { id: proposalDetails.id.toNumber(), diff --git a/src/views/Governance/hooks/useGetProposalFromSubgraph.tsx b/src/views/Governance/hooks/useGetProposalFromSubgraph.tsx new file mode 100644 index 000000000..0038bb14b --- /dev/null +++ b/src/views/Governance/hooks/useGetProposalFromSubgraph.tsx @@ -0,0 +1,60 @@ +import { useQuery } from "@tanstack/react-query"; +import request, { gql } from "graphql-request"; +import { Environment } from "src/helpers/environment/Environment/Environment"; +import { normalizeProposal } from "src/views/Governance/helpers/normalizeProposal"; + +export type Proposal = { + proposalId: string; + proposer: string; + targets: string[]; + signatures: string[]; + calldatas: string[]; + transactionHash: string; + description: string; + blockTimestamp: string; + blockNumber: string; + startBlock: string; + values: string[]; +}; + +type ProposalResponse = { + proposalCreated: Proposal; +}; + +export const useGetProposalFromSubgraph = ({ proposalId }: { proposalId?: string }) => { + const query = gql` + query { + proposalCreated(id: ${proposalId}) { + proposalId + proposer + targets + signatures + calldatas + transactionHash + description + blockTimestamp + blockNumber + startBlock + values + } + } +`; + + return useQuery( + ["getProposal", proposalId], + async () => { + try { + const subgraphUrl = Environment.getGovernanceSubgraphUrl(); + const response = await request(subgraphUrl, query); + if (!response.proposalCreated) { + return null; + } + return normalizeProposal(response.proposalCreated); + } catch (error) { + console.error("useGetProposalFromSubgraph", error); + return null; + } + }, + { enabled: !!proposalId }, + ); +}; diff --git a/src/views/Governance/hooks/useGetProposals.tsx b/src/views/Governance/hooks/useGetProposals.tsx deleted file mode 100644 index 6ccffe0f3..000000000 --- a/src/views/Governance/hooks/useGetProposals.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { GOVERNANCE_CONTRACT } from "src/constants/contracts"; -import { Environment } from "src/helpers/environment/Environment/Environment"; -import { Providers } from "src/helpers/providers/Providers/Providers"; -import { NetworkId } from "src/networkDetails"; -import { OlympusGovernorBravo__factory } from "src/typechain"; -import { ProposalCreatedEventObject } from "src/typechain/OlympusGovernorBravo"; - -export const useGetProposals = () => { - const archiveProvider = Providers.getArchiveStaticProvider(NetworkId.MAINNET); - // const contract = GOVERNANCE_CONTRACT.getEthersContract(NetworkId.MAINNET); - const contractAddress = GOVERNANCE_CONTRACT.addresses[NetworkId.MAINNET]; - const contract = OlympusGovernorBravo__factory.connect(contractAddress, archiveProvider); - - return useQuery( - ["getProposals", NetworkId.MAINNET], - async () => { - const proposals: { createdAtBlock: Date; details: ProposalCreatedEventObject; title: string; txHash: string }[] = - []; - let blockNumber = await archiveProvider.getBlockNumber(); - const startBlock = Environment.getGovernanceStartBlock(); - const chunkSize = 10000; //RPC limit - // Fetch the last proposal ID - const lastProposalId = (await contract.proposalCount()).toNumber(); - - // If there are no proposals, return an empty array - if (lastProposalId === 0) { - console.log("No proposals found."); - return proposals; - } - let foundProposalId1 = false; - - while (blockNumber > startBlock && !foundProposalId1) { - const fromBlock = Math.max(blockNumber - chunkSize, startBlock); - - const proposalCreatedEvents = await contract.queryFilter( - contract.filters.ProposalCreated(), - fromBlock, - blockNumber, - ); - - console.log("ProposalCreated events found:", proposalCreatedEvents.length); - - // Process each event - for (const item of proposalCreatedEvents) { - const timestamp = (await archiveProvider.getBlock(item.blockNumber)).timestamp; - - if (item.decode) { - const details = { ...item.decode(item.data), values: item.args[3] } as ProposalCreatedEventObject; - - proposals.push({ - createdAtBlock: new Date(timestamp * 1000), - details, - title: details.description.split(/#+\s|\n/g)[1] || `${details.description.slice(0, 20)}...`, - txHash: item.transactionHash, - }); - - // Stop if we hit proposal ID 1 - if (Number(details.id) === 1) { - foundProposalId1 = true; - break; - } - } - } - - // Move to the next block range (going backwards) - blockNumber = fromBlock - 1; - } - console.log("Proposals found:", proposals.length); - - return proposals; - }, - - { enabled: !!archiveProvider && !!contract }, - ); -}; - -export const useGetProposal = ({ proposalId }: { proposalId: number }) => { - const proposals = useGetProposals(); - return useQuery( - ["getProposal", NetworkId.MAINNET, proposalId], - async () => { - return proposals.data?.find(item => Number(item?.details.id) === proposalId); - }, - { enabled: !!proposals.data }, - ); -}; diff --git a/src/views/Governance/hooks/useGetProposalsFromSubgraph.tsx b/src/views/Governance/hooks/useGetProposalsFromSubgraph.tsx new file mode 100644 index 000000000..dea244136 --- /dev/null +++ b/src/views/Governance/hooks/useGetProposalsFromSubgraph.tsx @@ -0,0 +1,41 @@ +import { useQuery } from "@tanstack/react-query"; +import request, { gql } from "graphql-request"; +import { Environment } from "src/helpers/environment/Environment/Environment"; +import { normalizeProposal } from "src/views/Governance/helpers/normalizeProposal"; +import { Proposal } from "src/views/Governance/hooks/useGetProposalFromSubgraph"; + +export const useGetProposalsFromSubgraph = () => { + const query = gql` + query { + proposalCreateds(orderBy: proposalId, orderDirection: desc) { + proposalId + proposer + targets + signatures + calldatas + transactionHash + description + blockTimestamp + blockNumber + startBlock + values + } + } + `; + + type Proposals = { + proposalCreateds: Proposal[]; + }; + + return useQuery(["getProposals"], async () => { + try { + const subgraphUrl = Environment.getGovernanceSubgraphUrl(); + const response = await request(subgraphUrl, query); + + return response.proposalCreateds.map(normalizeProposal); + } catch (error) { + console.error("useGetProposalsFromSubgraph", error); + return []; + } + }); +}; diff --git a/src/views/Governance/hooks/useGetVotes.tsx b/src/views/Governance/hooks/useGetVotes.tsx new file mode 100644 index 000000000..d2e848e5b --- /dev/null +++ b/src/views/Governance/hooks/useGetVotes.tsx @@ -0,0 +1,38 @@ +import { useQuery } from "@tanstack/react-query"; +import request, { gql } from "graphql-request"; +import { Environment } from "src/helpers/environment/Environment/Environment"; + +export const useGetVotes = ({ proposalId, support }: { proposalId?: string; support: number }) => { + return useQuery( + ["getVotes", proposalId, support], + async () => { + const query = gql` + query MyQuery { + voteCasts(orderBy: votes, orderDirection: desc, where: {proposalId: ${proposalId}, support: ${support} }) { + votes + voter + reason + support + transactionHash + } + } + `; + + type votesResponse = { + voteCasts: { + votes: string; + voter: string; + reason: string; + support: number; + transactionHash: string; + }[]; + }; + + const subgraphUrl = Environment.getGovernanceSubgraphUrl(); + const response = await request(subgraphUrl, query); + + return response.voteCasts || []; + }, + { enabled: !!proposalId && !!support }, + ); +}; diff --git a/src/views/Governance/index.tsx b/src/views/Governance/index.tsx index f1958cfad..f273680f3 100644 --- a/src/views/Governance/index.tsx +++ b/src/views/Governance/index.tsx @@ -19,11 +19,11 @@ import { ContractParameters } from "src/views/Governance/Components/ContractPara import { ProposalContainer } from "src/views/Governance/Components/ProposalContainer"; import { DelegationMessage } from "src/views/Governance/Delegation/DelegationMessage"; import { GovernanceDevTools } from "src/views/Governance/hooks/dev/GovernanceDevTools"; -import { useGetProposals } from "src/views/Governance/hooks/useGetProposals"; +import { useGetProposalsFromSubgraph } from "src/views/Governance/hooks/useGetProposalsFromSubgraph"; import { useGetVotingWeight } from "src/views/Governance/hooks/useGetVotingWeight"; export const Governance = () => { - const { data: proposals, isFetching } = useGetProposals(); + const { data: proposals, isFetching } = useGetProposalsFromSubgraph(); const { data: currentVotingWeight } = useGetVotingWeight({}); const theme = useTheme(); const [activeProposals, setActiveProposals] = useState([]); @@ -114,7 +114,7 @@ export const Governance = () => {