Skip to content

Commit

Permalink
added mutliple poll types and batch publish message
Browse files Browse the repository at this point in the history
  • Loading branch information
yashgo0018 committed Apr 28, 2024
1 parent 7727656 commit 319470c
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 127 deletions.
10 changes: 5 additions & 5 deletions packages/hardhat/contracts/PollManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -47,7 +47,7 @@ contract PollManager is Params, DomainObjs {
MACI.PollContracts pollContracts,
string name,
string[] options,
string ipfsHash,
string metadata,
uint256 startTime,
uint256 endTime
);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -135,7 +135,7 @@ contract PollManager is Params, DomainObjs {
pollContracts,
_name,
_options,
_ipfsHash,
_metadata,
block.timestamp,
endTime
);
Expand Down
17 changes: 8 additions & 9 deletions packages/nextjs/app/admin/_components/CreatePollModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -163,7 +162,7 @@ export default function Example({
value={pollData.pollType}
onChange={handlePollTypeChange}
>
<option disabled selected value={PollType.NOT_SELECTED}>
<option disabled value={PollType.NOT_SELECTED}>
Select Poll Type
</option>
<option value={PollType.SINGLE_VOTE}>Single Candidate Select</option>
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ export default function AdminPage() {
<thead>
<tr className="text-lg font-extralight">
<th className="border border-slate-600 bg-primary">Poll Name</th>
<th className="border border-slate-600 bg-primary">End Time</th>
<th className="border border-slate-600 bg-primary">Start Time</th>
<th className="border border-slate-600 bg-primary">End Time</th>
<th className="border border-slate-600 bg-primary">Status</th>
</tr>
</thead>
Expand Down
183 changes: 118 additions & 65 deletions packages/nextjs/app/polls/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,75 +3,61 @@
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<number | null>(null);
const handleCardClick = (index: number) => {
setClickedIndex(clickedIndex === index ? null : index);
};
const [isVotesInvalid, setIsVotesInvalid] = useState<Record<number, boolean>>({});

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,
address: poll?.pollContracts.poll,
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<PubKey>();
Expand All @@ -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(),
);

Expand All @@ -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 <div>Loading...</div>;

if (error) return <div>Poll not found</div>;
console.log(poll);

return (
<div className="container mx-auto pt-10">
Expand All @@ -135,19 +186,21 @@ export default function PollDetail() {
<div className="text-2xl font-bold ">Vote for {poll?.name}</div>
</div>
{poll?.options.map((candidate, index) => (
<div className="pb-5" key={index}>
<VoteCard clicked={clickedIndex === index} onClick={() => handleCardClick(index)}>
<div>{candidate}</div>
</VoteCard>

{/* add a votes number input here */}
<div className="pb-5 flex" key={index}>
<VoteCard
index={index}
candidate={candidate}
clicked={false}
pollType={pollType}
onChange={(checked, votes) => voteUpdated(index, checked, votes)}
isInvalid={Boolean(isVotesInvalid[index])}
setIsInvalid={status => setIsVotesInvalid({ ...isVotesInvalid, [index]: status })}
/>
</div>
))}
<div className={`mt-2 ${clickedIndex !== null ? " shadow-2xl" : ""}`}>
<div className={`mt-2 shadow-2xl`}>
<button
onClick={() => {
castVote();
}}
onClick={castVote}
disabled={!true}
className="hover:border-black border-2 border-accent w-full text-lg text-center bg-accent py-3 rounded-xl font-bold"
>
Expand Down
4 changes: 2 additions & 2 deletions packages/nextjs/app/polls/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ export default function Polls() {
polls.length !== 0 ? (
<>
<div className="mb-3 flex flex-col gap-y-2">
{polls.map((poll: any) => (
{polls.map(poll => (
<HoverBorderCard key={poll.id} showArrow={true} click={() => router.push(`/polls/${poll.id}`)}>
<div className="flex">
<div className="flex-1 flex flex-col">
<h1 className="text-lg font-bold">
{poll.name} ({"Closed"})
{poll.name} ({poll.status})
</h1>
<h1 className="text-md text-sm">{poll.options.length} Candidates</h1>
</div>
Expand Down
Loading

0 comments on commit 319470c

Please sign in to comment.