Skip to content

Commit

Permalink
Feat/submit votes (#18)
Browse files Browse the repository at this point in the history
* submit application & use useSmartAccount hook

* Use Hex & Address types

* Sumbit votes

* Remove unnecessary todos
  • Loading branch information
JohnGuilding authored Aug 29, 2024
1 parent a6787b5 commit 791d8a2
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 15 deletions.
1 change: 1 addition & 0 deletions packages/interface/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 4 additions & 2 deletions packages/interface/src/contexts/Maci.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { isAfter } from "date-fns";
import { type Signer, BrowserProvider } from "ethers";
import {
isRegisteredUser,
publishBatch,
type TallyData,
type IGetPollData,
getPoll,
Expand All @@ -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<MaciContextType | undefined>(undefined);

Expand Down Expand Up @@ -252,7 +252,7 @@ export const MaciProvider: React.FC<MaciProviderProps> = ({ children }: MaciProv
// function to be used to vote on a poll
const onVote = useCallback(
async (votes: IVoteArgs[], onError: () => Promise<void>, onSuccess: () => Promise<void>) => {
if (!signer || !stateIndex || !pollData) {
if (!signer || !smartAccount || !smartAccountClient || !stateIndex || !pollData) {
return;
}

Expand All @@ -279,6 +279,8 @@ export const MaciProvider: React.FC<MaciProviderProps> = ({ children }: MaciProv
privateKey: maciPrivKey!,
pollId: BigInt(pollData.id),
signer,
smartAccount,
smartAccountClient
})
.then(() => onSuccess())
.catch((err: Error) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/interface/src/layouts/BaseLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
13 changes: 1 addition & 12 deletions packages/interface/src/pages/ballot/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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]);
Expand Down
165 changes: 165 additions & 0 deletions packages/interface/src/utils/publishBatch.ts
Original file line number Diff line number Diff line change
@@ -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<EntryPoint, HttpTransport, Chain>;
smartAccountClient: SmartAccountClient<EntryPoint, HttpTransport, Chain>;
};


/**
* @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<IPublishBatchData> => {
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(),
};
};
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 791d8a2

Please sign in to comment.