Skip to content

Commit

Permalink
Fix/user op error handling (#42)
Browse files Browse the repository at this point in the history
* Manually set callGasLimit buffer on voting

* Add userOp error handling to useEAS
  • Loading branch information
JohnGuilding authored Sep 13, 2024
1 parent 335db7f commit 23a18e4
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 56 deletions.
77 changes: 45 additions & 32 deletions packages/interface/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,34 @@
## 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

# 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 && \
Expand All @@ -44,72 +48,81 @@ 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 && \
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 \
--ipfs https://ipfs.satsuma.xyz/
```

### 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
Expand Down
7 changes: 4 additions & 3 deletions packages/interface/src/contexts/Maci.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -259,13 +259,13 @@ 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>) => {
async (votes: IVoteArgs[], onError: (message: string) => Promise<void>, onSuccess: () => Promise<void>) => {
if (!signer || !smartAccount || !smartAccountClient || !stateIndex || !pollData) {
return;
}

if (!votes.length) {
onError();
onError("No votes provided");
setError("No votes provided");
return;
}
Expand Down Expand Up @@ -293,7 +293,8 @@ export const MaciProvider: React.FC<MaciProviderProps> = ({ children }: MaciProv
.then(() => onSuccess())
.catch((err: Error) => {
setError(err.message);
return onError();
console.error(err.message)
return onError(err.message);
})
.finally(() => {
setIsLoading(false);
Expand Down
2 changes: 1 addition & 1 deletion packages/interface/src/contexts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface MaciContextType {
onSignup: (onError: () => void, onSuccess: () => void) => Promise<void>;
onVote: (
args: IVoteArgs[],
onError: () => void | Promise<void>,
onError: (message: string) => void | Promise<void>,
onSuccess: () => void | Promise<void>
) => Promise<void>;
updateEligibility: (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}),
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
18 changes: 11 additions & 7 deletions packages/interface/src/hooks/useEAS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -60,17 +60,21 @@ export function useAttest(): UseMutationResult<Hex, DefaultError, MultiAttestati
}

try {
const { request } = await publicClient.simulateContract({
account: smartAccount,
address: eas.contracts.eas as Address,
const to = eas.contracts.eas as Address;
const calldata = encodeFunctionData({
abi: EASFactory.abi,
functionName: "multiAttest",
args: [multiAttestationRequests],
});
return await smartAccountClient.writeContract(request);
return await sendUserOperation(
to,
calldata,
smartAccount,
smartAccountClient
);
} catch (error: unknown) {
console.error(error);
throw new Error("Error attesting");
throw new Error(`Error attesting ${error instanceof Error ? `- ${error.message}` : ""}`);
}
},
});
Expand Down
21 changes: 13 additions & 8 deletions packages/interface/src/utils/publishBatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ import {
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";
import { Address, HttpTransport, Chain, encodeFunctionData } from "viem";
import sendUserOperation from "./sendUserOperation";

const MESSAGE_TREE_ARITY = 5;

Expand All @@ -26,7 +26,6 @@ type ISmartAccountPublishBatchArgs = IPublishBatchArgs & {
smartAccountClient: SmartAccountClient<EntryPoint, HttpTransport, Chain>;
};


/**
* @notice copied from maci-cli/sdk to add sponsorship
* Batch publish new messages to a MACI Poll contract
Expand Down Expand Up @@ -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(),
};
Expand Down
79 changes: 79 additions & 0 deletions packages/interface/src/utils/sendUserOperation.ts
Original file line number Diff line number Diff line change
@@ -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<EntryPoint, HttpTransport, Chain>,
smartAccountClient: SmartAccountClient<EntryPoint, HttpTransport, Chain>
) {
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<ENTRYPOINT_ADDRESS_V07_TYPE> =
{
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;
}

0 comments on commit 23a18e4

Please sign in to comment.