From 791d8a21ac33f480de0ad41472a6af4c35235a91 Mon Sep 17 00:00:00 2001 From: John Guilding <54913924+JohnGuilding@users.noreply.github.com> Date: Thu, 29 Aug 2024 09:50:34 +0100 Subject: [PATCH] Feat/submit votes (#18) * submit application & use useSmartAccount hook * Use Hex & Address types * Sumbit votes * Remove unnecessary todos --- packages/interface/package.json | 1 + packages/interface/src/contexts/Maci.tsx | 6 +- packages/interface/src/layouts/BaseLayout.tsx | 2 +- packages/interface/src/pages/ballot/index.tsx | 13 +- packages/interface/src/utils/publishBatch.ts | 165 ++++++++++++++++++ pnpm-lock.yaml | 3 + 6 files changed, 175 insertions(+), 15 deletions(-) create mode 100644 packages/interface/src/utils/publishBatch.ts diff --git a/packages/interface/package.json b/packages/interface/package.json index 5dd1bfa..12c7eea 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -42,6 +42,7 @@ "lowdb": "^1.0.0", "lucide-react": "^0.316.0", "maci-cli": "^2.2.0", + "maci-crypto": "^2.2.0", "maci-domainobjs": "^2.2.0", "next": "^14.1.0", "next-auth": "^4.24.5", diff --git a/packages/interface/src/contexts/Maci.tsx b/packages/interface/src/contexts/Maci.tsx index d56582d..4cacf66 100644 --- a/packages/interface/src/contexts/Maci.tsx +++ b/packages/interface/src/contexts/Maci.tsx @@ -4,7 +4,6 @@ import { isAfter } from "date-fns"; import { type Signer, BrowserProvider } from "ethers"; import { isRegisteredUser, - publishBatch, type TallyData, type IGetPollData, getPoll, @@ -26,6 +25,7 @@ import { getSemaphoreProof } from "~/utils/semaphore"; import type { IVoteArgs, MaciContextType, MaciProviderProps } from "./types"; import type { Attestation } from "~/utils/types"; import signUp from "~/utils/signUp"; +import { publishBatch } from "~/utils/publishBatch"; export const MaciContext = createContext(undefined); @@ -252,7 +252,7 @@ export const MaciProvider: React.FC = ({ children }: MaciProv // function to be used to vote on a poll const onVote = useCallback( async (votes: IVoteArgs[], onError: () => Promise, onSuccess: () => Promise) => { - if (!signer || !stateIndex || !pollData) { + if (!signer || !smartAccount || !smartAccountClient || !stateIndex || !pollData) { return; } @@ -279,6 +279,8 @@ export const MaciProvider: React.FC = ({ children }: MaciProv privateKey: maciPrivKey!, pollId: BigInt(pollData.id), signer, + smartAccount, + smartAccountClient }) .then(() => onSuccess()) .catch((err: Error) => { diff --git a/packages/interface/src/layouts/BaseLayout.tsx b/packages/interface/src/layouts/BaseLayout.tsx index 00bb967..7d6786f 100644 --- a/packages/interface/src/layouts/BaseLayout.tsx +++ b/packages/interface/src/layouts/BaseLayout.tsx @@ -83,7 +83,7 @@ export const BaseLayout = ({ const { isRegistered } = useMaci(); const manageDisplay = useCallback(() => { - if ((requireAuth && !address) || (requireRegistration && !isRegistered)) { + if (requireRegistration && !isRegistered) { router.push("/"); } }, [requireAuth, address, requireRegistration, isRegistered, router]); diff --git a/packages/interface/src/pages/ballot/index.tsx b/packages/interface/src/pages/ballot/index.tsx index 66404ef..ac1d2aa 100644 --- a/packages/interface/src/pages/ballot/index.tsx +++ b/packages/interface/src/pages/ballot/index.tsx @@ -1,7 +1,6 @@ import clsx from "clsx"; import Link from "next/link"; -import { useRouter } from "next/router"; -import { useEffect, useState, useMemo, useCallback } from "react"; +import { useState, useMemo, useCallback } from "react"; import { useFormContext } from "react-hook-form"; import { Button } from "~/components/ui/Button"; @@ -12,7 +11,6 @@ import { useBallot } from "~/contexts/Ballot"; import { useMaci } from "~/contexts/Maci"; import { AllocationFormWrapper } from "~/features/ballot/components/AllocationFormWrapper"; import { BallotSchema } from "~/features/ballot/types"; -import useSmartAccount from "~/hooks/useSmartAccount"; import { LayoutWithSidebar } from "~/layouts/DefaultLayout"; import { formatNumber } from "~/utils/formatNumber"; import { useAppState } from "~/utils/state"; @@ -124,17 +122,8 @@ const BallotAllocationForm = (): JSX.Element => { }; const BallotPage = (): JSX.Element => { - const { address } = useSmartAccount(); const { ballot, sumBallot } = useBallot(); - const router = useRouter(); const appState = useAppState(); - - useEffect(() => { - if (!address) { - router.push("/"); - } - }, [address, router]); - const handleSubmit = useCallback(() => { sumBallot(); }, [sumBallot]); diff --git a/packages/interface/src/utils/publishBatch.ts b/packages/interface/src/utils/publishBatch.ts new file mode 100644 index 0000000..0b2b0b3 --- /dev/null +++ b/packages/interface/src/utils/publishBatch.ts @@ -0,0 +1,165 @@ +import { + IPublishBatchArgs, + IPublishBatchData, + PubKey, + MACI__factory as MACIFactory, + Poll__factory as PollFactory, +} from "maci-cli/sdk"; +import { + IG1ContractParams, + IMessageContractParams, + Keypair, + PCommand, + PrivKey, +} from "maci-domainobjs"; +import { genRandomSalt } from "maci-crypto"; +import { publicClient } from "./permissionless"; +import { SmartAccountClient } from "permissionless"; +import { EntryPoint } from "permissionless/types"; +import { KernelEcdsaSmartAccount } from "permissionless/accounts"; +import { Address, HttpTransport, Chain } from "viem"; + +const MESSAGE_TREE_ARITY = 5; + +type ISmartAccountPublishBatchArgs = IPublishBatchArgs & { + smartAccount: KernelEcdsaSmartAccount; + smartAccountClient: SmartAccountClient; +}; + + +/** + * @notice copied from maci-cli/sdk to add sponsorship + * Batch publish new messages to a MACI Poll contract + * @param {IPublishBatchArgs} args - The arguments for the publish command + * @returns {IPublishBatchData} The ephemeral private key used to encrypt the message, transaction hash + */ +export const publishBatch = async ({ + messages, + pollId, + maciAddress, + publicKey, + privateKey, + signer, + quiet = true, + smartAccount, + smartAccountClient, +}: ISmartAccountPublishBatchArgs): Promise => { + if (!PubKey.isValidSerializedPubKey(publicKey)) { + throw new Error("invalid MACI public key"); + } + + if (!PrivKey.isValidSerializedPrivKey(privateKey)) { + throw new Error("invalid MACI private key"); + } + + if (pollId < 0n) { + throw new Error(`invalid poll id ${pollId}`); + } + + const userMaciPubKey = PubKey.deserialize(publicKey); + const userMaciPrivKey = PrivKey.deserialize(privateKey); + const maciContract = MACIFactory.connect(maciAddress, signer); + const pollContracts = await maciContract.getPoll(pollId); + + const pollContract = PollFactory.connect(pollContracts.poll, signer); + + const [treeDepths, coordinatorPubKeyResult] = await Promise.all([ + pollContract.treeDepths(), + pollContract.coordinatorPubKey(), + ]); + const maxVoteOptions = Number( + BigInt(MESSAGE_TREE_ARITY) ** treeDepths.voteOptionTreeDepth + ); + + // validate the vote options index against the max leaf index on-chain + messages.forEach(({ stateIndex, voteOptionIndex, salt, nonce }) => { + if (voteOptionIndex < 0 || maxVoteOptions < voteOptionIndex) { + throw new Error("invalid vote option index"); + } + + // check < 1 cause index zero is a blank state leaf + if (stateIndex < 1) { + throw new Error("invalid state index"); + } + + if (nonce < 0) { + throw new Error("invalid nonce"); + } + }); + + const coordinatorPubKey = new PubKey([ + BigInt(coordinatorPubKeyResult.x.toString()), + BigInt(coordinatorPubKeyResult.y.toString()), + ]); + + const encryptionKeypair = new Keypair(); + const sharedKey = Keypair.genEcdhSharedKey( + encryptionKeypair.privKey, + coordinatorPubKey + ); + + const payload: [IMessageContractParams, IG1ContractParams][] = messages.map( + ({ salt, stateIndex, voteOptionIndex, newVoteWeight, nonce }) => { + const userSalt = salt ? BigInt(salt) : genRandomSalt(); + + // create the command object + const command = new PCommand( + stateIndex, + userMaciPubKey, + voteOptionIndex, + newVoteWeight, + nonce, + BigInt(pollId), + userSalt + ); + + // sign the command with the user private key + const signature = command.sign(userMaciPrivKey); + + const message = command.encrypt(signature, sharedKey); + + return [ + message.asContractParam(), + encryptionKeypair.pubKey.asContractParam(), + ]; + } + ); + + const preparedMessages = payload.map(([message]) => message); + const preparedKeys = payload.map(([, key]) => key); + + // TODO: (merge-ok) make this type casting/handling nicer + const reversedMessages = preparedMessages.reverse().map((item) => ({ + data: item.data.map((val) => BigInt(val)) as [ + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, + ], + })); + const reversedKeys = preparedKeys.reverse() as readonly { + x: bigint; + y: bigint; + }[]; + + const { request } = await publicClient.simulateContract({ + account: smartAccount, + address: pollContracts.poll as Address, + abi: PollFactory.abi, + functionName: "publishMessageBatch", + args: [reversedMessages, reversedKeys], + }); + const txHash = await smartAccountClient.writeContract(request); + + return { + hash: txHash, + encryptedMessages: preparedMessages, + privateKey: encryptionKeypair.privKey.serialize(), + }; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 445e81e..7ce101e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -184,6 +184,9 @@ importers: maci-cli: specifier: ^2.2.0 version: 2.2.1(5ocd64qtuox4mobu5be27w2nau) + maci-crypto: + specifier: ^2.2.0 + version: 2.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) maci-domainobjs: specifier: ^2.2.0 version: 2.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)