diff --git a/packages/interface/README.md b/packages/interface/README.md index 584255f..f59c3f6 100644 --- a/packages/interface/README.md +++ b/packages/interface/README.md @@ -11,18 +11,20 @@ ## Hacking PSE E2E setup instructions on Optimism Sepolia ### Mint top hat and create first hat + You'll need to mint a top hat and then create your first hat. The hat id of your first hat is the one we'll be assigning to vote participants. Learn more about hats at [the hats documentation](https://docs.hatsprotocol.xyz/) + 1. Go to [the hats contract on op scan](https://sepolia-optimism.etherscan.io/address/0x3bc1a0ad72417f2d411118085256fc53cbddd137#writeContract) and connect your wallet 2. Call `mintTopHat` - You can take inspriation from [this tx](https://sepolia-optimism.etherscan.io/tx/0x57d6562494335b9ae640e97c4e3fb7f3d80a0141352652a42da6c2d9ed662804) 3. Call `createHat` - Add the id of the `HatCreated` log from the `mintTopHat` transaction as the admin (`_admin (uint256)`) for this call. Add your own address for `_eligibility (address)` & `_toggle (address)`. You can take inspiration from [this tx](https://sepolia-optimism.etherscan.io/tx/0x45bc3410ceb95cd5c760dd11ffbc99cbd8f5c3b488240612788fcf3674124edb) 4. Take the hat id from the logs of `HatCreated`, add this hat id the the `.env` files in `backend`, `contracts` and `interface` - ### Deploy Semaphore contracts gated by Hats 1. Add a rpc url, deployer private key, op scan api key, hats address, and hat id to a `.env` file in `packages/contracts`. The hats address for optimism sepolia is `0x3bc1A0Ad72417f2d411118085256fC53CBdDd137`. That hats id is the value you got in the transaction logs when calling `createHat` 2. `cd packages/contracts` 3. Deploy contracts: + ```bash # Load the variables in the .env file source .env @@ -30,11 +32,13 @@ source .env # To deploy and verify the contract forge script --chain sepolia script/Hackathon.s.sol:DeploySemaphore_AndSetGate --rpc-url $SEPOLIA_RPC_URL --etherscan-api-key $ETHERSCAN_API_KEY --broadcast --verify -vvvv ``` -4. Add the semahore address to `.env` in `interface` +4. Add the semahore address to `.env` in `interface` ### Deploy MACI contracts -1. Follow installation instructions for MACI if this is your first time running a maci poll https://maci.pse.dev/docs/quick-start/installation. After installing the required dependencies to your machine, setup the maci monorepo: + +1. Follow installation instructions for MACI if this is your first time running a maci poll https://maci.pse.dev/docs/quick-start/installation. After installing the required dependencies to your machine, setup the maci monorepo: + ```bash # install dependencies and run pnpm build git clone https://github.com/privacy-scaling-explorations/maci.git && \ @@ -44,12 +48,14 @@ pnpm run build ``` 2. You won't need to compile new circuits, but you will need to download the zkeys + ```bash # download zkeys pnpm download-zkeys:test ``` 3. Generate and safely store a coordinator key pair https://maci.pse.dev/docs/quick-start/deployment#coordinator-key + ```bash # generate a coordinator key pair - save this for later cd packages/cli && \ @@ -57,46 +63,49 @@ node build/ts/index.js genMaciKeyPair ``` 4. `cp deploy-config-example.json deploy-config.json` - 1. go to the `optimism_sepolia` json object in `deploy-config.json` - 2. set the following values to ensure maci works with the semaphore gatekeeper you deployed. Set your desired poll duration as well - ```json - "optimism_sepolia": { - "FreeForAllGatekeeper": { - "deploy": false - }, - "SemaphoreGatekeeper": { - "deploy": true, - "semaphoreContract": "THE SEMAPHORE ADDRESS YOU DEPLOYED", - "groupId": 1 - }, - "MACI": { - "stateTreeDepth": 10, - "gatekeeper": "SemaphoreGatekeeper" - }, - "Poll": { - "pollDuration": 3600, - "coordinatorPubkey": "YOUR MACI PUBLIC KEY GENERATED FROM genMaciKeyPair", - "useQuadraticVoting": false - } - } - ``` + + 1. go to the `optimism_sepolia` json object in `deploy-config.json` + 2. set the following values to ensure maci works with the semaphore gatekeeper you deployed. Set your desired poll duration as well + + ```json + "optimism_sepolia": { + "FreeForAllGatekeeper": { + "deploy": false + }, + "SemaphoreGatekeeper": { + "deploy": true, + "semaphoreContract": "THE SEMAPHORE ADDRESS YOU DEPLOYED", + "groupId": 1 + }, + "MACI": { + "stateTreeDepth": 10, + "gatekeeper": "SemaphoreGatekeeper" + }, + "Poll": { + "pollDuration": 3600, + "coordinatorPubkey": "YOUR MACI PUBLIC KEY GENERATED FROM genMaciKeyPair", + "useQuadraticVoting": false + } + } + ``` 5. `cp default-deployed-contracts.json deployed-contracts.json` -Fill the `.env` file with the appropriate data (you will find an example in the .env.example file): - - your mnemonic - - an RPC key + Fill the `.env` file with the appropriate data (you will find an example in the .env.example file): - your mnemonic - an RPC key -5. `pnpm deploy:optimism-sepolia --incremental` -6. In this repo, add the maci address to `NEXT_PUBLIC_MACI_ADDRESS` in the interface `.env` +6. `pnpm deploy:optimism-sepolia --incremental` +7. In this repo, add the maci address to `NEXT_PUBLIC_MACI_ADDRESS` in the interface `.env` ### Deploy the subgraph + In the same MACI repo, deploy the subgraph + 1. `cd apps/subgraph` 2. Add the maci address and the block it was deployed in to `apps/subgraph/config/network.json` 3. `pnpm run build` 4. if you need to deploy again, remember to increment the version label each time + ```bash -graph deploy test-maci-vote \ +graph deploy test-maci-vote \ --version-label v0.0.1 \ --node https://subgraphs.alchemy.com/api/subgraphs/deploy \ --deploy-key YOUR_DEPLOY_KEY \ @@ -104,12 +113,16 @@ graph deploy test-maci-vote \ ``` ### Start frontend + backend + From the root of this repo + 1. `pnpm install && pnpm build` 2. `pnpm dev:server` 3. `pnpm dev:interface` +4. visit `https://0.0.0.0:3001/`, click "Advanced", then click "Proceed to 0.0.0.0 (unsafe)". This allows the frontend to fetch from the backend ### Deploy a poll + 1. in the maci repo, run `pnpm deploy-poll:optimism-sepolia` ## Supported Networks diff --git a/packages/interface/src/contexts/Maci.tsx b/packages/interface/src/contexts/Maci.tsx index da37f2f..d86f603 100644 --- a/packages/interface/src/contexts/Maci.tsx +++ b/packages/interface/src/contexts/Maci.tsx @@ -259,13 +259,13 @@ 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) => { + async (votes: IVoteArgs[], onError: (message: string) => Promise, onSuccess: () => Promise) => { if (!signer || !smartAccount || !smartAccountClient || !stateIndex || !pollData) { return; } if (!votes.length) { - onError(); + onError("No votes provided"); setError("No votes provided"); return; } @@ -293,7 +293,8 @@ export const MaciProvider: React.FC = ({ children }: MaciProv .then(() => onSuccess()) .catch((err: Error) => { setError(err.message); - return onError(); + console.error(err.message) + return onError(err.message); }) .finally(() => { setIsLoading(false); diff --git a/packages/interface/src/contexts/types.ts b/packages/interface/src/contexts/types.ts index 29abec1..c95508e 100644 --- a/packages/interface/src/contexts/types.ts +++ b/packages/interface/src/contexts/types.ts @@ -23,7 +23,7 @@ export interface MaciContextType { onSignup: (onError: () => void, onSuccess: () => void) => Promise; onVote: ( args: IVoteArgs[], - onError: () => void | Promise, + onError: (message: string) => void | Promise, onSuccess: () => void | Promise ) => Promise; updateEligibility: ( diff --git a/packages/interface/src/features/applications/components/ApplicationForm.tsx b/packages/interface/src/features/applications/components/ApplicationForm.tsx index 9e0a1b6..9f7144c 100644 --- a/packages/interface/src/features/applications/components/ApplicationForm.tsx +++ b/packages/interface/src/features/applications/components/ApplicationForm.tsx @@ -59,7 +59,7 @@ export const ApplicationForm = (): JSX.Element => { toast.error("Application create error", { description: err.reason ?? err.data?.message, }) - console.log(err) + console.error(err) }, }) diff --git a/packages/interface/src/features/applications/hooks/useApproveApplication.ts b/packages/interface/src/features/applications/hooks/useApproveApplication.ts index b999ca5..4a56eb2 100644 --- a/packages/interface/src/features/applications/hooks/useApproveApplication.ts +++ b/packages/interface/src/features/applications/hooks/useApproveApplication.ts @@ -40,9 +40,9 @@ export function useApproveApplication(opts?: { toast.success("Application approved successfully!"); opts?.onSuccess?.(); }, - onError: (err: { reason?: string; data?: { message: string } }) => + onError: (err: Error | { reason?: string; data?: { message: string } }) => toast.error("Application approve error", { - description: err.reason ?? err.data?.message, + description: err instanceof Error ? err.message : err.reason ?? err.data?.message }), }); } diff --git a/packages/interface/src/features/ballot/components/SubmitBallotButton.tsx b/packages/interface/src/features/ballot/components/SubmitBallotButton.tsx index 665219e..21bd5d5 100644 --- a/packages/interface/src/features/ballot/components/SubmitBallotButton.tsx +++ b/packages/interface/src/features/ballot/components/SubmitBallotButton.tsx @@ -20,8 +20,8 @@ export const SubmitBallotButton = (): JSX.Element => { [sumBallot, ballot, initialVoiceCredits], ); - const onVotingError = useCallback(() => { - toast.error("Voting error"); + const onVotingError = useCallback((message: string) => { + toast.error(`Voting error ${message}`); }, []); const handleSubmitBallot = useCallback(async () => { diff --git a/packages/interface/src/hooks/useEAS.ts b/packages/interface/src/hooks/useEAS.ts index 6ed9bee..6954f40 100644 --- a/packages/interface/src/hooks/useEAS.ts +++ b/packages/interface/src/hooks/useEAS.ts @@ -5,9 +5,9 @@ import { type DefaultError, type UseMutationResult, useMutation } from "@tanstac import { useEthersSigner } from "~/hooks/useEthersSigner"; import { createAttestation } from "~/lib/eas/createAttestation"; import useSmartAccount from "./useSmartAccount"; -import { Address, Hex } from "viem"; -import { publicClient } from "~/utils/permissionless"; +import { Address, encodeFunctionData, Hex } from "viem"; import { eas } from "~/config"; +import sendUserOperation from "~/utils/sendUserOperation"; export function useCreateAttestation(): UseMutationResult< AttestationRequest, @@ -60,17 +60,21 @@ export function useAttest(): UseMutationResult; }; - /** * @notice copied from maci-cli/sdk to add sponsorship * Batch publish new messages to a MACI Poll contract @@ -148,17 +147,23 @@ export const publishBatch = async ({ y: bigint; }[]; - const { request } = await publicClient.simulateContract({ - account: smartAccount, - address: pollContracts.poll as Address, + const to = pollContracts.poll as Address; + const calldata = encodeFunctionData({ abi: PollFactory.abi, functionName: "publishMessageBatch", args: [reversedMessages, reversedKeys], }); - const txHash = await smartAccountClient.writeContract(request); + + const userOpHash = await sendUserOperation( + to, + calldata, + smartAccount, + smartAccountClient + ); + console.log("publishMessageBatch userOpHash", userOpHash); return { - hash: txHash, + hash: userOpHash, encryptedMessages: preparedMessages, privateKey: encryptionKeypair.privKey.serialize(), }; diff --git a/packages/interface/src/utils/sendUserOperation.ts b/packages/interface/src/utils/sendUserOperation.ts new file mode 100644 index 0000000..e998186 --- /dev/null +++ b/packages/interface/src/utils/sendUserOperation.ts @@ -0,0 +1,79 @@ +import { + UserOperation, + EstimateUserOperationGasParameters, + ENTRYPOINT_ADDRESS_V07, + SmartAccountClient, +} from "permissionless"; +import { EntryPoint, ENTRYPOINT_ADDRESS_V07_TYPE } from "permissionless/types"; +import { Address, Chain, Hex, HttpTransport } from "viem"; +import { publicClient, pimlicoBundlerClient } from "./permissionless"; +import { KernelEcdsaSmartAccount } from "permissionless/accounts"; + +export default async function sendUserOperation( + to: Address, + calldata: Hex, + smartAccount: KernelEcdsaSmartAccount, + smartAccountClient: SmartAccountClient +) { + const estimateGasFees = await publicClient.estimateFeesPerGas(); + + const partialUserOperation: UserOperation<"v0.7"> = { + sender: smartAccount.address, + nonce: await smartAccount.getNonce(), + callData: await smartAccount.encodeCallData({ + to, + value: 0n, + data: calldata, + }), + callGasLimit: 0n, + verificationGasLimit: 0n, + preVerificationGas: 0n, + maxFeePerGas: estimateGasFees.maxFeePerGas, + maxPriorityFeePerGas: estimateGasFees.maxPriorityFeePerGas, + signature: "0x", + }; + + const dummySignature = + await smartAccount.getDummySignature(partialUserOperation); + + const estimateGasUserOperation: EstimateUserOperationGasParameters = + { + userOperation: { + ...partialUserOperation, + signature: dummySignature, + }, + entryPoint: ENTRYPOINT_ADDRESS_V07, + }; + const gasValues = await pimlicoBundlerClient.estimateUserOperationGas( + estimateGasUserOperation + ); + + // estimateUserOperationGas fails to accurately estimate the callGasLimit when sending a + // userOp with calldata over a certain size. Adding a buffer ensures the userOp succeeds + // in this case + const multiplier = 10n; // 10% + const callGasLimitBuffer = (gasValues.callGasLimit / 100n) * multiplier; + + const userOpHash = await smartAccountClient.sendUserOperation({ + account: smartAccount, + userOperation: { + ...partialUserOperation, + callGasLimit: gasValues.callGasLimit + callGasLimitBuffer, + verificationGasLimit: gasValues.verificationGasLimit, + preVerificationGas: gasValues.preVerificationGas, + }, + }); + + const userOpReceipt = await pimlicoBundlerClient.waitForUserOperationReceipt({ + hash: userOpHash, + timeout: 20000, + }); + + if (userOpReceipt && !userOpReceipt.success) { + throw new Error( + `User Operation reverted ${userOpHash}. ${userOpReceipt.reason ?? ""}` + ); + } + + return userOpHash; +}