diff --git a/packages/hardhat/contracts/PollManager.sol b/packages/hardhat/contracts/PollManager.sol index 0e681d9..0044b76 100644 --- a/packages/hardhat/contracts/PollManager.sol +++ b/packages/hardhat/contracts/PollManager.sol @@ -18,7 +18,7 @@ contract PollManager is Params, DomainObjs { uint256 maciPollId; string name; bytes encodedOptions; - string ipfsHash; + string metadata; MACI.PollContracts pollContracts; uint256 startTime; uint256 endTime; @@ -47,7 +47,7 @@ contract PollManager is Params, DomainObjs { MACI.PollContracts pollContracts, string name, string[] options, - string ipfsHash, + string metadata, uint256 startTime, uint256 endTime ); @@ -89,7 +89,7 @@ contract PollManager is Params, DomainObjs { function createPoll( string calldata _name, string[] calldata _options, - string calldata _ipfsHash, + string calldata _metadata, uint256 _duration ) public onlyOwner { // TODO: check if the number of options are more than limit @@ -120,7 +120,7 @@ contract PollManager is Params, DomainObjs { name: _name, encodedOptions: encodedOptions, numOfOptions: _options.length, - ipfsHash: _ipfsHash, + metadata: _metadata, startTime: block.timestamp, endTime: endTime, pollContracts: pollContracts, @@ -135,7 +135,7 @@ contract PollManager is Params, DomainObjs { pollContracts, _name, _options, - _ipfsHash, + _metadata, block.timestamp, endTime ); diff --git a/packages/nextjs/app/admin/_components/CreatePollModal.tsx b/packages/nextjs/app/admin/_components/CreatePollModal.tsx index e9eef1e..41b4c2e 100644 --- a/packages/nextjs/app/admin/_components/CreatePollModal.tsx +++ b/packages/nextjs/app/admin/_components/CreatePollModal.tsx @@ -5,15 +5,9 @@ import { MdEdit } from "react-icons/md"; import { RxCross2 } from "react-icons/rx"; import Modal from "~~/components/Modal"; import { useScaffoldContractWrite } from "~~/hooks/scaffold-eth"; +import { PollType } from "~~/types/poll"; import { notification } from "~~/utils/scaffold-eth"; -enum PollType { - NOT_SELECTED, - SINGLE_VOTE, - MULTIPLE_VOTE, - WEIGHTED_MULTIPLE_VOTE, -} - export default function Example({ show, setOpen, @@ -68,7 +62,12 @@ export default function Example({ const { writeAsync } = useScaffoldContractWrite({ contractName: "PollManager", functionName: "createPoll", - args: [pollData?.title, pollData?.options || [], "", duration > 0 ? BigInt(duration) : 0n], + args: [ + pollData.title, + pollData.options || [], + JSON.stringify({ pollType: pollData.pollType }), + duration > 0 ? BigInt(duration) : 0n, + ], }); async function onSubmit() { @@ -163,7 +162,7 @@ export default function Example({ value={pollData.pollType} onChange={handlePollTypeChange} > - diff --git a/packages/nextjs/app/admin/page.tsx b/packages/nextjs/app/admin/page.tsx index 29d6a9e..a7f0c7e 100644 --- a/packages/nextjs/app/admin/page.tsx +++ b/packages/nextjs/app/admin/page.tsx @@ -46,8 +46,8 @@ export default function AdminPage() { Poll Name - End Time Start Time + End Time Status diff --git a/packages/nextjs/app/polls/[id]/page.tsx b/packages/nextjs/app/polls/[id]/page.tsx index 9f59bdd..3f09b82 100644 --- a/packages/nextjs/app/polls/[id]/page.tsx +++ b/packages/nextjs/app/polls/[id]/page.tsx @@ -3,59 +3,44 @@ import { useEffect, useState } from "react"; import { useParams } from "next/navigation"; import { genRandomSalt } from "@se-2/hardhat/maci-ts/crypto"; -import { Keypair, Message, PCommand, PubKey } from "@se-2/hardhat/maci-ts/domainobjs"; +import { Keypair, PCommand, PubKey } from "@se-2/hardhat/maci-ts/domainobjs"; import { useContractRead, useContractWrite } from "wagmi"; import PollAbi from "~~/abi/Poll"; import VoteCard from "~~/components/card/VoteCard"; import { useAuthContext } from "~~/contexts/AuthContext"; import { useAuthUserOnly } from "~~/hooks/useAuthUserOnly"; import { useFetchPoll } from "~~/hooks/useFetchPoll"; +import { PollType } from "~~/types/poll"; +import { notification } from "~~/utils/scaffold-eth"; export default function PollDetail() { const { id } = useParams<{ id: string }>(); const { data: poll, error, isLoading } = useFetchPoll(id); + const [pollType, setPollType] = useState(PollType.NOT_SELECTED); useAuthUserOnly({}); const { keypair, stateIndex } = useAuthContext(); - const [votes] = useState<{ index: number; votes: number; voteMessage: { message: Message; encKeyPair: Keypair } }[]>( - [], - ); + const [votes, setVotes] = useState<{ index: number; votes: number }[]>([]); - const [clickedIndex, setClickedIndex] = useState(null); - const handleCardClick = (index: number) => { - setClickedIndex(clickedIndex === index ? null : index); - }; + const [isVotesInvalid, setIsVotesInvalid] = useState>({}); - const castVote = async () => { - console.log("Voting for candidate", clickedIndex); - console.log("A", message?.message.asContractParam(), message?.encKeyPair.pubKey.asContractParam()); - // // navigate to the home page - // try { - // // setLoaderMessage("Casting the vote, please wait..."); - - // // router.push(`/voted-success?id=${clickedIndex}`); - // } catch (err) { - // console.log("err", err); - // // toast.error("Casting vote failed, please try again "); - // } - }; + const isAnyInvalid = Object.values(isVotesInvalid).some(v => v); useEffect(() => { - if (votes.length === 0) { + if (!poll || !poll.metadata) { return; } - (async () => { - try { - await writeAsync(); - } catch (err) { - console.log({ err }); - } - })(); - }, [votes]); + try { + const { pollType } = JSON.parse(poll.metadata); + setPollType(pollType); + } catch (err) { + console.log("err", err); + } + }, [poll]); const { data: coordinatorPubKeyResult } = useContractRead({ abi: PollAbi, @@ -63,15 +48,16 @@ export default function PollDetail() { functionName: "coordinatorPubKey", }); - const [message, setMessage] = useState<{ message: Message; encKeyPair: Keypair }>(); - - console.log("message", message); - - const { writeAsync } = useContractWrite({ + const { writeAsync: publishMessage } = useContractWrite({ abi: PollAbi, address: poll?.pollContracts.poll, functionName: "publishMessage", - args: message ? [message.message.asContractParam(), message.encKeyPair.pubKey.asContractParam()] : undefined, + }); + + const { writeAsync: publishMessageBatch } = useContractWrite({ + abi: PollAbi, + address: poll?.pollContracts.poll, + functionName: "publishMessageBatch", }); const [coordinatorPubKey, setCoordinatorPubKey] = useState(); @@ -89,28 +75,79 @@ export default function PollDetail() { setCoordinatorPubKey(coordinatorPubKey_); }, [coordinatorPubKeyResult]); - useEffect(() => { - if (clickedIndex === null || !coordinatorPubKey || !keypair || !stateIndex) { + const castVote = async () => { + if (!poll || stateIndex == null || !coordinatorPubKey || !keypair) return; + + // check if the votes are valid + if (isAnyInvalid) { + notification.error("Please enter a valid number of votes"); return; } - console.log( - stateIndex, // stateindex - keypair.pubKey, // userMaciPubKey - BigInt(clickedIndex), - 1n, - 1n, // nonce - BigInt(id), - genRandomSalt(), + // check if no votes are selected + if (votes.length === 0) { + notification.error("Please select at least one option to vote"); + return; + } + + // check if the poll is closed + // if (Number(poll.endTime) * 1000 < new Date().getTime()) { + // notification.error("Voting is closed for this poll"); + // return; + // } + + // TODO: check if the poll is not started + + const votesToMessage = votes.map((v, i) => + getMessageAndEncKeyPair( + stateIndex, + poll.id, + BigInt(v.index), + BigInt(v.votes), + BigInt(votes.length - i), + coordinatorPubKey, + keypair, + ), ); + try { + if (votesToMessage.length === 1) { + await publishMessage({ + args: [votesToMessage[0].message.asContractParam(), votesToMessage[0].encKeyPair.pubKey.asContractParam()], + }); + } else { + await publishMessageBatch({ + args: [ + votesToMessage.map(v => v.message.asContractParam()), + votesToMessage.map(v => v.encKeyPair.pubKey.asContractParam()), + ], + }); + } + + // // setLoaderMessage("Casting the vote, please wait..."); + // // router.push(`/voted-success?id=${clickedIndex}`); + } catch (err) { + console.log("err", err); + notification.error("Casting vote failed, please try again "); + } + }; + + function getMessageAndEncKeyPair( + stateIndex: bigint, + pollIndex: bigint, + candidateIndex: bigint, + weight: bigint, + nonce: bigint, + coordinatorPubKey: PubKey, + keypair: Keypair, + ) { const command: PCommand = new PCommand( - stateIndex, // stateindex - keypair.pubKey, // userMaciPubKey - BigInt(clickedIndex), - 1n, - 1n, // nonce - BigInt(id), + stateIndex, + keypair.pubKey, + candidateIndex, + weight, + nonce, + pollIndex, genRandomSalt(), ); @@ -120,13 +157,27 @@ export default function PollDetail() { const message = command.encrypt(signature, Keypair.genEcdhSharedKey(encKeyPair.privKey, coordinatorPubKey)); - setMessage({ message, encKeyPair }); - }, [id, clickedIndex, coordinatorPubKey, keypair, stateIndex]); + return { message, encKeyPair }; + } + + function voteUpdated(index: number, checked: boolean, voteCounts: number) { + if (pollType === PollType.SINGLE_VOTE) { + if (checked) { + setVotes([{ index, votes: voteCounts }]); + } + return; + } + + if (checked) { + setVotes([...votes.filter(v => v.index !== index), { index, votes: voteCounts }]); + } else { + setVotes(votes.filter(v => v.index !== index)); + } + } if (isLoading) return
Loading...
; if (error) return
Poll not found
; - console.log(poll); return (
@@ -135,19 +186,21 @@ export default function PollDetail() {
Vote for {poll?.name}
{poll?.options.map((candidate, index) => ( -
- handleCardClick(index)}> -
{candidate}
-
- - {/* add a votes number input here */} +
+ voteUpdated(index, checked, votes)} + isInvalid={Boolean(isVotesInvalid[index])} + setIsInvalid={status => setIsVotesInvalid({ ...isVotesInvalid, [index]: status })} + />
))} -
+