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 (
+
+ {value === index && {children}}
+
+ );
+};
+
+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 = () => {