Skip to content

Commit

Permalink
AGORA-1426 As an OP Delegate, I should be able to vote on an approval…
Browse files Browse the repository at this point in the history
… proposal (#53)

* approval votes list to typescript

* cast vote button to typescript

* approval page to typescript

* approval votes to typescript + fix

* cleanup

* support new approval module

* abstract logic isOldApprovalModule

* refactor

---------

Co-authored-by: stepandel <stepandel@gmail.com>
  • Loading branch information
Dom-Mac and stepandel committed Jan 12, 2024
1 parent 400dc8a commit b9b1405
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 46 deletions.
@@ -1,7 +1,7 @@
"use client";

import { VStack } from "@/components/Layout/Stack";
import { AbiCoder, ethers } from "ethers";
import { AbiCoder } from "ethers";
import { cn } from "@/lib/utils";
import { useMemo, useState } from "react";
import {
Expand All @@ -13,7 +13,6 @@ import TokenAmountDisplay from "@/components/shared/TokenAmountDisplay";
import { CheckIcon } from "lucide-react";
import { Proposal } from "@/app/api/proposals/proposal";
import { ParsedProposalData } from "@/lib/proposalUtils";
import { ProposalType } from "@prisma/client";
import { OptimismContracts } from "@/lib/contracts/contracts";
import { useContractWrite } from "wagmi";
import styles from "./approvalCastVoteDialog.module.scss";
Expand Down
Expand Up @@ -8,6 +8,54 @@ import OptionsResultsPanel from "../OptionResultsPanel/OptionResultsPanel";
import ApprovalProposalVotesList from "@/components/Votes/ApprovalProposalVotesList/ApprovalProposalVotesList";
import ApprovalProposalCriteria from "../ApprovalProposalCriteria/ApprovalProposalCriteria";
import ApprovalCastVoteButton from "@/components/Votes/ApprovalCastVoteButton/ApprovalCastVoteButton";
import { Proposal } from "@/app/api/proposals/proposal";
import { Delegate } from "@/app/api/delegates/delegate";
import { Vote } from "@/app/api/votes/vote";

type Props = {
proposal: Proposal;
initialProposalVotes: {
meta: {
currentPage: number;
pageSize: number;
hasNextPage: boolean;
};
votes: Vote[];
};
fetchVotesForProposal: (
proposal_id: string,
page?: number
) => Promise<{
meta: {
currentPage: number;
pageSize: number;
hasNextPage: boolean;
};
votes: Vote[];
}>;
fetchVotingPower: (
addressOrENSName: string | `0x${string}`,
blockNumber: number
) => Promise<{ votingPower: string }>;
fetchAuthorityChains: (
address: string | `0x${string}`,
blockNumber: number
) => Promise<{ chains: string[][] }>;
fetchDelegate: (
addressOrENSName: string | `0x${string}`
) => Promise<Delegate>;
fetchVoteForProposalAndDelegate: (
proposal_id: string,
address: string | `0x${string}`
) => Promise<
| {
vote: undefined;
}
| {
vote: Vote;
}
>;
};

export default function ApprovalVotesPanel({
proposal,
Expand All @@ -17,10 +65,10 @@ export default function ApprovalVotesPanel({
fetchAuthorityChains,
fetchDelegate,
fetchVoteForProposalAndDelegate,
}) {
}: Props) {
const [activeTab, setActiveTab] = useState(1);
const [isPending, startTransition] = useTransition();
function handleTabsChange(index) {
function handleTabsChange(index: number) {
startTransition(() => {
setActiveTab(index);
});
Expand All @@ -32,20 +80,16 @@ export default function ApprovalVotesPanel({
animate={{ opacity: isPending ? 0.3 : 1 }}
transition={{ duration: 0.3, delay: isPending ? 0.3 : 0 }}
>
<VStack className={styles.approval_votes_panel}>
<VStack gap={1} className={styles.approval_votes_panel}>
{/* Tabs */}
<HStack className={styles.approval_vote_tab_container}>
<div onClick={() => handleTabsChange(1)}>
<span className={activeTab === 1 ? "text-black" : ""}>Results</span>
</div>
{initialProposalVotes.votes &&
initialProposalVotes.votes.length > 0 && (
<div onClick={() => handleTabsChange(2)}>
<span className={activeTab === 2 ? "text-black" : ""}>
Votes
</span>
</div>
)}
{["Results", "Votes"].map((tab, index) => (
<div key={index} onClick={() => handleTabsChange(index + 1)}>
<span className={activeTab === index + 1 ? "text-black" : ""}>
{tab}
</span>
</div>
))}
</HStack>
{activeTab === 1 ? (
<OptionsResultsPanel proposal={proposal} />
Expand All @@ -57,7 +101,6 @@ export default function ApprovalVotesPanel({
/>
)}
<ApprovalProposalCriteria proposal={proposal} />

<div className={styles.button_container}>
<ApprovalCastVoteButton
proposal={proposal}
Expand Down
Expand Up @@ -9,40 +9,53 @@ import {
import { getVotingPowerAtSnapshot } from "@/app/api/voting-power/getVotingPower";
import { getAuthorityChains } from "@/app/api/authority-chains/getAuthorityChains";
import { getDelegate } from "@/app/api/delegates/getDelegates";
import { Proposal } from "@/app/api/proposals/proposal";

async function fetchProposalVotes(proposal_id, page = 1) {
async function fetchProposalVotes(proposal_id: string, page = 1) {
"use server";

return getVotesForProposal({ proposal_id, page });
}

async function fetchVotingPower(address, blockNumber) {
async function fetchVotingPower(
addressOrENSName: string | `0x${string}`,
blockNumber: number
) {
"use server";

return {
votingPower: (
await getVotingPowerAtSnapshot({ blockNumber, addressOrENSName: address })
await getVotingPowerAtSnapshot({ blockNumber, addressOrENSName })
).totalVP,
};
}

async function fetchAuthorityChains(address, blockNumber) {
async function fetchAuthorityChains(
address: string | `0x${string}`,
blockNumber: number
) {
"use server";

return {
chains: await getAuthorityChains({ blockNumber, address }),
chains: await getAuthorityChains({
blockNumber,
address,
}),
};
}

async function fetchDelegate(addressOrENSName) {
async function fetchDelegate(addressOrENSName: string | `0x${string}`) {
"use server";

return await getDelegate({
addressOrENSName,
});
}

async function fetchVoteForProposalAndDelegate(proposal_id, address) {
async function fetchVoteForProposalAndDelegate(
proposal_id: string,
address: string | `0x${string}`
) {
"use server";

return await getVoteForProposalAndDelegate({
Expand All @@ -51,7 +64,11 @@ async function fetchVoteForProposalAndDelegate(proposal_id, address) {
});
}

export default async function OPProposalApprovalPage({ proposal }) {
export default async function OPProposalApprovalPage({
proposal,
}: {
proposal: Proposal;
}) {
const proposalVotes = await fetchProposalVotes(proposal.id);

return (
Expand Down
Expand Up @@ -8,30 +8,64 @@ import { Button } from "@/components/ui/button";
import { useModal } from "connectkit";
import { useOpenDialog } from "@/components/Dialogs/DialogProvider/DialogProvider";
import { useAgoraContext } from "@/contexts/AgoraContext";
import { Proposal } from "@/app/api/proposals/proposal";
import { Delegate } from "@/app/api/delegates/delegate";
import { Vote } from "@/app/api/votes/vote";

type Props = {
proposal: Proposal;
fetchVotingPower: (
addressOrENSName: string | `0x${string}`,
blockNumber: number
) => Promise<{ votingPower: string }>;
fetchAuthorityChains: (
address: string | `0x${string}`,
blockNumber: number
) => Promise<{ chains: string[][] }>;
fetchDelegate: (
addressOrENSName: string | `0x${string}`
) => Promise<Delegate>;
fetchVoteForProposalAndDelegate: (
proposal_id: string,
address: string | `0x${string}`
) => Promise<
| {
vote: undefined;
}
| {
vote: Vote;
}
>;
};

export default function ApprovalCastVoteButton({
proposal,
fetchVotingPower,
fetchAuthorityChains,
fetchDelegate,
fetchVoteForProposalAndDelegate,
}) {
}: Props) {
const [votingPower, setVotingPower] = useState("0");
const [delegate, setDelegate] = useState({});
const [chains, setChains] = useState([]);
const [vote, setVote] = useState({});
const [delegate, setDelegate] = useState<Delegate>();
const [chains, setChains] = useState<string[][]>([]);
const [vote, setVote] = useState<Vote>();
const [isReady, setIsReady] = useState(false);
const openDialog = useOpenDialog();

const { address } = useAccount();

const fetchData = useCallback(async () => {
try {
const promises = [
fetchVotingPower(address, proposal.snapshotBlockNumber),
fetchDelegate(address),
fetchAuthorityChains(address, proposal.snapshotBlockNumber),
fetchVoteForProposalAndDelegate(proposal.id, address),
const promises: [
Promise<{ votingPower: string }>,
Promise<Delegate>,
Promise<{ chains: string[][] }>,
Promise<{ vote?: Vote }>
] = [
fetchVotingPower(address!, proposal.snapshotBlockNumber),
fetchDelegate(address!),
fetchAuthorityChains(address!, proposal.snapshotBlockNumber),
fetchVoteForProposalAndDelegate(proposal.id, address!),
];

const [votingPowerResult, delegateResult, chainsResult, voteResult] =
Expand Down Expand Up @@ -69,7 +103,7 @@ export default function ApprovalCastVoteButton({
type: "APPROVAL_CAST_VOTE",
params: {
proposal: proposal,
hasStatement: !!delegate.statement,
hasStatement: !!delegate?.statement,
},
})
}
Expand All @@ -82,7 +116,17 @@ export default function ApprovalCastVoteButton({
);
}

function VoteButton({ onClick, proposalStatus, delegateVote, isReady }) {
function VoteButton({
onClick,
proposalStatus,
delegateVote,
isReady,
}: {
onClick: () => void;
proposalStatus: Proposal["status"];
delegateVote?: Vote;
isReady: boolean;
}) {
const { isConnected } = useAgoraContext();
const { setOpen } = useModal();

Expand Down Expand Up @@ -119,15 +163,15 @@ function VoteButton({ onClick, proposalStatus, delegateVote, isReady }) {
);
}

function CastButton({ onClick }) {
function CastButton({ onClick }: { onClick: () => void }) {
return (
<button className={styles.vote_button} onClick={onClick}>
Cast Vote
</button>
);
}

function DisabledVoteButton({ reason }) {
function DisabledVoteButton({ reason }: { reason: string }) {
return (
<button disabled className={styles.vote_button_disabled}>
{reason}
Expand Down
Expand Up @@ -7,19 +7,47 @@ import { VStack, HStack } from "@/components/Layout/Stack";
import HumanAddress from "@/components/shared/HumanAddress";
import TokenAmountDisplay from "@/components/shared/TokenAmountDisplay";
import Image from "next/image";
import VoteText from "../VoteText/VoteText";
import { useAccount } from "wagmi";
import { Vote } from "@/app/api/votes/vote";

type Props = {
initialProposalVotes: {
meta: {
currentPage: number;
pageSize: number;
hasNextPage: boolean;
};
votes: Vote[];
};
fetchVotesForProposal: (
proposal_id: string,
page?: number
) => Promise<{
meta: {
currentPage: number;
pageSize: number;
hasNextPage: boolean;
};
votes: Vote[];
}>;
proposal_id: string;
};

export default function ApprovalProposalVotesList({
initialProposalVotes,
fetchVotesForProposal,
proposal_id,
}) {
}: Props) {
const fetching = React.useRef(false);
const [pages, setPages] = React.useState([initialProposalVotes] || []);
const [meta, setMeta] = React.useState(initialProposalVotes.meta);

const loadMore = async (page) => {
const proposalVotes = pages.reduce(
(all: Vote[], page) => all.concat(page.votes),
[]
);

const loadMore = async (page: number) => {
if (!fetching.current && meta.hasNextPage) {
fetching.current = true;
const data = await fetchVotesForProposal(proposal_id, page);
Expand All @@ -33,10 +61,9 @@ export default function ApprovalProposalVotesList({
}
};

const proposalVotes = pages.reduce((all, page) => all.concat(page.votes), []);

return (
<div className={styles.vote_container}>
{/* @ts-ignore */}
<InfiniteScroll
hasMore={meta.hasNextPage}
pageStart={0}
Expand All @@ -56,7 +83,7 @@ export default function ApprovalProposalVotesList({
);
}

function SingleVote({ vote }) {
function SingleVote({ vote }: { vote: Vote }) {
const { address } = useAccount();
const { address: voterAddress, params, support, reason, weight } = vote;

Expand All @@ -77,7 +104,7 @@ function SingleVote({ vote }) {
</div>
</HStack>
<VStack className={styles.single_vote__content}>
{params?.map((option, index) => (
{params?.map((option: string, index: number) => (
<p
key={index}
className={"whitespace-nowrap text-ellipsis overflow-hidden"}
Expand Down

0 comments on commit b9b1405

Please sign in to comment.