Skip to content

Commit

Permalink
Merge pull request #62 from zkemail/feat/multi-chain
Browse files Browse the repository at this point in the history
Feat/multi chain
  • Loading branch information
javiersuweijie authored Oct 31, 2024
2 parents f79dfb9 + b138861 commit 0d01cc6
Show file tree
Hide file tree
Showing 11 changed files with 213 additions and 66 deletions.
2 changes: 1 addition & 1 deletion packages/app/modal/deploy-contract.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ fi
# install deps and compile contract
cd $input_dir
yarn install
forge script Deploy.s.sol --rpc-url $rpc_url --broadcast --chain-id $chain_id --verify
forge script Deploy.s.sol --rpc-url $rpc_url --broadcast --verify
rm -rf node_modules
60 changes: 44 additions & 16 deletions packages/app/prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// schema.prisma

generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
previewFeatures = ["fullTextSearch"]
}

Expand All @@ -12,21 +12,22 @@ datasource db {
}

model Entry {
id String @id @default(cuid())
title String
slug String @unique
description String
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
createdBy String
tags String[]
status String @default("PENDING")
parameters Json
ProofJob ProofJob[]
emailQuery String @default("")
verifierContractAddress String?
contractAddress String?
withModal Boolean @default(false)
id String @id @default(cuid())
title String
slug String @unique
description String
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
createdBy String
tags String[]
status String @default("PENDING")
parameters Json
ProofJob ProofJob[]
emailQuery String @default("")
verifierContractAddress String?
contractAddress String?
withModal Boolean @default(false)
ContractDeployment ContractDeployment[]
@@map("entries")
}
Expand Down Expand Up @@ -56,3 +57,30 @@ model ApiKey {
@@map("api_keys")
}

model ContractDeployment {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
status String @default("PENDING")
verifierContractAddress String?
contractAddress String?
chainName String
entryId String
entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade)
error String?
@@map("contract_deployments")
}

model Chain {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
chainId String
chainName String
dkimContractAddress String
rpcUrl String
@@map("chains")
}
28 changes: 20 additions & 8 deletions packages/app/src/app/try/[[...slug]]/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,26 @@
import prisma from "@/lib/prisma";
import { Entry } from "@prisma/client";

export async function redeployContracts(entry: Entry) {
await prisma.entry.update({
export async function redeployContracts(entry: Entry, chainName: string) {
const deployment = await prisma.contractDeployment.findFirst({
where: {
slug: entry.slug,
},
data: {
verifierContractAddress: null,
contractAddress: null
entryId: entry.id, chainName
}
})
});
if (deployment) {
await prisma.contractDeployment.update({
where: { id: deployment.id }, data: {
status: "PENDING"
}
})
} else {

await prisma.contractDeployment.create({
data: {
chainName,
status: "PENDING",
entryId: entry.id,
}
})
}
}
50 changes: 44 additions & 6 deletions packages/app/src/app/try/[[...slug]]/content.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table";
import { Entry } from "@prisma/client";
import { Chain, ContractDeployment, Entry } from "@prisma/client";
import { CheckedState } from "@radix-ui/react-checkbox";
import { useState, useEffect, FormEvent } from "react";
import { useGoogleAuth, fetchEmailList, fetchEmailsRaw, useZkEmailSDK } from "@zk-email/zk-email-sdk";
Expand All @@ -18,9 +18,14 @@ import { SimpleDialog } from "@/components/simple-dialog";
import { calculateSignalLength } from "@/lib/code-gen/utils";
import { redeployContracts } from "./action";
import { getProofLogs } from "@/lib/models/logs";
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectValue } from "@/components/ui/select";
import { SelectTrigger } from "@radix-ui/react-select";
import { FormControl, FormField } from "@/components/ui/form";

export interface ContentProps {
entry: Entry
entry: Entry,
supportedChains: Chain[],
deployedContracts: ContractDeployment[],
}

type RawEmailResponse = {
Expand All @@ -31,9 +36,18 @@ type RawEmailResponse = {

type Email = RawEmailResponse & { selected: boolean, inputs?: any, error?: string, body?: string };

const chains = {
"Ethereum": "",
"ETH Sepolia": "",
"Arbitrum Testnet": "",
"Arbitrum": ""
}

export function PageContent(props: ContentProps) {
const workers = new Map<string, boolean>();
const entry = props.entry;
const supportedChains = props.supportedChains;
const deployedContracts = props.deployedContracts;
const {
googleAuthToken,
isGoogleAuthed,
Expand Down Expand Up @@ -61,6 +75,8 @@ export function PageContent(props: ContentProps) {
const [isRedeploying, setIsRedeploying] = useState<boolean>(false);
const [proofLogs, setProofLogs] = useState<Record<string, string>>({});
const [emailPageToken, setEmailPageToken] = useState<string|undefined>("0");
const [selectedChain, setSelectedChain] = useState<string>("Ethereum Sepolia");
const [selectedContracts, setSelectedContracts] = useState<(ContractDeployment|null)>(null);

useEffect(() => {
if (!inputWorkers[entry.slug]) {
Expand All @@ -69,6 +85,14 @@ export function PageContent(props: ContentProps) {
filterEmails(entry.emailQuery)
}, [googleAuthToken, inputWorkers])

useEffect(() => {
const deployed = deployedContracts.find(d => d.chainName == selectedChain)
if (!deployed) {
return setSelectedContracts(null);
}
setSelectedContracts(deployed)
}, [selectedChain])

function filterEmails(query: string) {
const fetchData = async () => {
if (emailPageToken === undefined) {
Expand Down Expand Up @@ -162,7 +186,7 @@ export function PageContent(props: ContentProps) {
}

function redeployContractsHandler() {
redeployContracts(entry);
redeployContracts(entry, selectedChain);
setIsRedeploying(true);
setTimeout(() => {window.location.reload()}, 30000);
}
Expand Down Expand Up @@ -465,10 +489,24 @@ export function PageContent(props: ContentProps) {
</div>
<div className="mb-4">
<h4 className="text-2xl md:text-2xl tracking-tighter max-w-xl text-left font-extrabold mb-4">
Step 4: Verify proofs on-chain (Sepolia)
Step 4: Verify proofs on-chain
</h4>
<Select value={selectedChain} onValueChange={setSelectedChain}>
<SelectTrigger>
Deploy to <b className="font-bold outline-1 outline-black outline pl-2 pr-2 ml-2">{selectedChain}</b>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Chain</SelectLabel>
{supportedChains.map(e => (<SelectItem key={e.id} value={e.chainName}>{e.chainName}</SelectItem>))}
</SelectGroup>
</SelectContent>
</Select>
<div className="flex flex-row items-center">
{selectedContracts?.error && <p>Error deploying contract: {selectedContracts.error}</p>}
</div>
<div className="flex flex-row items-center">
<p><b className="font-extrabold">Verification Contract:</b> {entry.contractAddress || "Not deployed yet"}</p>
<p><b className="font-extrabold">Verification Contract:</b> {selectedContracts?.contractAddress || "Not deployed yet"}</p>
<SimpleDialog title="Verification Contract" trigger={<Button className="font-extrabold" variant="link">View ABI</Button>}>
<code className="text-xs">
<pre>
Expand Down Expand Up @@ -504,7 +542,7 @@ export function PageContent(props: ContentProps) {
</code>
</SimpleDialog>
</div>
<p><b className="font-bold">Groth16 Contract:</b> {entry.verifierContractAddress || "Not deployed yet"}</p>
<p><b className="font-bold">Groth16 Contract:</b> {selectedContracts?.verifierContractAddress || "Not deployed yet"}</p>
<div className="flex flex-row">
<ConnectButton />
<Button className="ml-4" variant="outline" onClick={redeployContractsHandler}>{isRedeploying ? <><LoaderCircle className="animate-spin" /> Reloading in ~30s...</> : "Redeploy Contracts"}</Button>
Expand Down
5 changes: 4 additions & 1 deletion packages/app/src/app/try/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { getEntryBySlug } from '@/lib/models/entry';
import TryPage from './wrapper';
import prisma from '@/lib/prisma';

export default async function Page({params}: {params: {slug: string[]}}) {
const slug = params.slug.join('/');
const entry = await getEntryBySlug(slug);
if (!entry) {
return (<h1>Entry not found</h1>)
}
const supportedChains = await prisma.chain.findMany();
const deployedContracts = await prisma.contractDeployment.findMany({where: {entryId: entry?.id, OR: [{status: "COMPLETED"}, {status: "ERROR"}]}});
return (
<TryPage entry={entry}/>
<TryPage entry={entry} supportedChains={supportedChains} deployedContracts={deployedContracts}/>
);
}
52 changes: 38 additions & 14 deletions packages/app/src/lib/contract-deploy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,25 @@ import { verify } from "crypto";

import { bytesToHex, createPublicClient, createWalletClient, getAddress, Hex, http, isHex, parseAbi, toBytes, toHex } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { sepolia, foundry, optimismSepolia } from 'viem/chains';
import { sepolia, foundry, optimismSepolia, arbitrumSepolia, arbitrum, optimism, Chain, mainnet, scrollSepolia, scroll } from 'viem/chains';
import { pki } from "node-forge";
import { toCircomBigIntBytes } from "@zk-email/helpers";
import { usePublicClient } from "wagmi";
import prisma from "../prisma";

const CONTRACT_OUT_DIR = "./output/contract";
const CODE_OUT_DIR = "./output/code";
const CHAIN_ID = process.env.CHAIN_ID || "1";
const CHAIN = sepolia;
export const CHAINS: { [key: string]: Chain } = {
"Ethereum": mainnet,
"Ethereum Sepolia": sepolia,
"Arbitrum Sepolia": arbitrumSepolia,
"Arbitrum": arbitrum,
"Optimism Sepolia": optimismSepolia,
"Optimism": optimism,
"Scroll Sepolia": scrollSepolia,
"Scroll": scroll,
}

export async function deployContract(entry: Entry): Promise<void> {
const contractDir = path.join(CODE_OUT_DIR, entry.slug, 'contract')
Expand All @@ -41,12 +51,16 @@ export async function deployContract(entry: Entry): Promise<void> {
})
}

export async function deployContractWithModal(entry: Entry): Promise<void> {
export async function deployContractWithModal(entry: Entry, chainName: string): Promise<void> {
const contractDir = path.join(CODE_OUT_DIR, entry.slug, 'contract');

const chain = await prisma.chain.findFirstOrThrow({where: {chainName}})

// Environment variables needed for deployment
const env = {
...process.env,
RPC_URL: chain.rpcUrl,
CHAIN_ID: chain.chainId,
DKIM_REGISTRY: chain.dkimContractAddress,
PROJECT_SLUG: entry.slug,
CIRCUIT_NAME: (entry.parameters as any).name as string,
CONTRACT_PATH: contractDir,
Expand Down Expand Up @@ -127,7 +141,14 @@ export async function readContractAddresses(entry: Entry): Promise<{ verifier: s
return result;
}

export async function addDkimEntry(entry: Entry): Promise<void> {
export async function addDkimEntry(entry: Entry, chainName: string): Promise<void> {
const chain = await prisma.chain.findFirstOrThrow({where: {chainName}});
if (!chain.dkimContractAddress) {
throw new Error("DKIM registry not found for chain")
}
if (!CHAINS[chainName]) {
throw new Error(`Chain ${chainName} not found`)
}
const domain = (entry.parameters as any).senderDomain as string;
const selector = (entry.parameters as any).dkimSelector as string;
const res = await fetch(`https://archive.prove.email/api/key?domain=${domain}`);
Expand All @@ -150,7 +171,7 @@ export async function addDkimEntry(entry: Entry): Promise<void> {
const chunkedKey = toCircomBigIntBytes(BigInt(pubkey.n.toString()));
const hashedKey = BigInt(await pubKeyHasher(chunkedKey));

const isDKIMPublicKeyHashValid = await checkDKIMPublicKeyHash(domain, hashedKey);
const isDKIMPublicKeyHashValid = await checkDKIMPublicKeyHash(domain, hashedKey, chainName);
if (isDKIMPublicKeyHashValid) {
console.log(`DKIM key already exists for domain ${domain} selector ${selector}`);
return;
Expand All @@ -163,13 +184,13 @@ export async function addDkimEntry(entry: Entry): Promise<void> {
const account = privateKeyToAccount(privateKey as Hex);
const client = createWalletClient({
account,
chain: CHAIN,
chain: CHAINS[chainName],
transport: http()
});


// Prepare contract interaction
const dkimContract = process.env.DKIM_REGISTRY;
const dkimContract = chain.dkimContractAddress;
if (!dkimContract) {
throw new Error('DKIM_REGISTRY not found in environment variables');
}
Expand All @@ -194,16 +215,19 @@ export async function addDkimEntry(entry: Entry): Promise<void> {
console.log(`Transaction sent: ${hash}`);
}

export async function checkDKIMPublicKeyHash(domain: string, pubkeyHash: bigint): Promise<boolean> {
const dkimContract = process.env.DKIM_REGISTRY;
if (!dkimContract) {
throw new Error('DKIM_REGISTRY not found in environment variables');
export async function checkDKIMPublicKeyHash(domain: string, pubkeyHash: bigint, chainName: string): Promise<boolean> {
const chain = await prisma.chain.findFirstOrThrow({where: {chainName}});
if (!chain.dkimContractAddress) {
throw new Error("DKIM registry not found for chain")
}
const contractAddress = getAddress(dkimContract as Hex); // Replace 'X' with the actual contract address
if (!CHAINS[chainName]) {
throw new Error(`Chain ${chainName} not found`)
}
const contractAddress = getAddress(chain.dkimContractAddress as Hex); // Replace 'X' with the actual contract address
const abi = parseAbi(["function isDKIMPublicKeyHashValid( string memory domainName, bytes32 publicKeyHash) external view returns (bool)"]);
// use viem to read the contract function
const publicClient = createPublicClient({
chain: CHAIN,
chain: CHAINS[chainName],
transport: http()
});

Expand Down
4 changes: 2 additions & 2 deletions packages/app/src/lib/contract/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { foundry, mainnet, sepolia } from 'wagmi/chains'
import { arbitrum, arbitrumSepolia, foundry, mainnet, sepolia } from 'wagmi/chains'
import {
getDefaultConfig,
} from '@rainbow-me/rainbowkit';
Expand All @@ -13,7 +13,7 @@ import { Entry } from '@prisma/client';
export const config = getDefaultConfig({
appName: 'ZK Email SDK Regsitry',
projectId: '7a5727ef2bfa0be0186ec17111b106b0',
chains: [sepolia, foundry],
chains: [sepolia, arbitrum, arbitrumSepolia],
wallets: [{groupName: 'default', wallets: [metaMaskWallet, rabbyWallet, rainbowWallet]}],
ssr: true, // If your dApp uses server side rendering (SSR)
});
Expand Down
5 changes: 0 additions & 5 deletions packages/app/src/lib/models/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,6 @@ export const getRandomPendingEntry = async () => {
return entries[Math.floor(Math.random() * entries.length)];
}

export const getFirstUndeployedEntry = async () => {
const entry = await prisma.entry.findFirst({where: {status: "COMPLETED", verifierContractAddress: null}, orderBy: {createdAt: 'asc'}});
return entry
}

export const updateEntry = async (entry: Entry) => {
await prisma.entry.update({
where: {
Expand Down
Loading

0 comments on commit 0d01cc6

Please sign in to comment.