diff --git a/packages/app/modal/deploy-contract.sh b/packages/app/modal/deploy-contract.sh index acf4451..5942b5d 100644 --- a/packages/app/modal/deploy-contract.sh +++ b/packages/app/modal/deploy-contract.sh @@ -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 \ No newline at end of file diff --git a/packages/app/prisma/schema.prisma b/packages/app/prisma/schema.prisma index 6d618c6..e536d5f 100644 --- a/packages/app/prisma/schema.prisma +++ b/packages/app/prisma/schema.prisma @@ -1,7 +1,7 @@ // schema.prisma generator client { - provider = "prisma-client-js" + provider = "prisma-client-js" previewFeatures = ["fullTextSearch"] } @@ -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") } @@ -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") +} diff --git a/packages/app/src/app/try/[[...slug]]/action.ts b/packages/app/src/app/try/[[...slug]]/action.ts index f8664bc..d730ee0 100644 --- a/packages/app/src/app/try/[[...slug]]/action.ts +++ b/packages/app/src/app/try/[[...slug]]/action.ts @@ -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, + } + }) + } } \ No newline at end of file diff --git a/packages/app/src/app/try/[[...slug]]/content.tsx b/packages/app/src/app/try/[[...slug]]/content.tsx index fba9d1e..73d1aeb 100644 --- a/packages/app/src/app/try/[[...slug]]/content.tsx +++ b/packages/app/src/app/try/[[...slug]]/content.tsx @@ -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"; @@ -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 = { @@ -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(); const entry = props.entry; + const supportedChains = props.supportedChains; + const deployedContracts = props.deployedContracts; const { googleAuthToken, isGoogleAuthed, @@ -61,6 +75,8 @@ export function PageContent(props: ContentProps) { const [isRedeploying, setIsRedeploying] = useState(false); const [proofLogs, setProofLogs] = useState>({}); const [emailPageToken, setEmailPageToken] = useState("0"); + const [selectedChain, setSelectedChain] = useState("Ethereum Sepolia"); + const [selectedContracts, setSelectedContracts] = useState<(ContractDeployment|null)>(null); useEffect(() => { if (!inputWorkers[entry.slug]) { @@ -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) { @@ -162,7 +186,7 @@ export function PageContent(props: ContentProps) { } function redeployContractsHandler() { - redeployContracts(entry); + redeployContracts(entry, selectedChain); setIsRedeploying(true); setTimeout(() => {window.location.reload()}, 30000); } @@ -465,10 +489,24 @@ export function PageContent(props: ContentProps) {

- Step 4: Verify proofs on-chain (Sepolia) + Step 4: Verify proofs on-chain

+ +
+ {selectedContracts?.error &&

Error deploying contract: {selectedContracts.error}

} +
-

Verification Contract: {entry.contractAddress || "Not deployed yet"}

+

Verification Contract: {selectedContracts?.contractAddress || "Not deployed yet"}

View ABI}>
@@ -504,7 +542,7 @@ export function PageContent(props: ContentProps) {
                                         
                                     
                                 
-

Groth16 Contract: {entry.verifierContractAddress || "Not deployed yet"}

+

Groth16 Contract: {selectedContracts?.verifierContractAddress || "Not deployed yet"}

diff --git a/packages/app/src/app/try/[[...slug]]/page.tsx b/packages/app/src/app/try/[[...slug]]/page.tsx index fb56bd7..a00e0b4 100644 --- a/packages/app/src/app/try/[[...slug]]/page.tsx +++ b/packages/app/src/app/try/[[...slug]]/page.tsx @@ -1,5 +1,6 @@ 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('/'); @@ -7,7 +8,9 @@ export default async function Page({params}: {params: {slug: string[]}}) { if (!entry) { return (

Entry not found

) } + const supportedChains = await prisma.chain.findMany(); + const deployedContracts = await prisma.contractDeployment.findMany({where: {entryId: entry?.id, OR: [{status: "COMPLETED"}, {status: "ERROR"}]}}); return ( - + ); } \ No newline at end of file diff --git a/packages/app/src/lib/contract-deploy/index.ts b/packages/app/src/lib/contract-deploy/index.ts index 56641ca..5e84ca1 100644 --- a/packages/app/src/lib/contract-deploy/index.ts +++ b/packages/app/src/lib/contract-deploy/index.ts @@ -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 { const contractDir = path.join(CODE_OUT_DIR, entry.slug, 'contract') @@ -41,12 +51,16 @@ export async function deployContract(entry: Entry): Promise { }) } -export async function deployContractWithModal(entry: Entry): Promise { +export async function deployContractWithModal(entry: Entry, chainName: string): Promise { 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, @@ -127,7 +141,14 @@ export async function readContractAddresses(entry: Entry): Promise<{ verifier: s return result; } -export async function addDkimEntry(entry: Entry): Promise { +export async function addDkimEntry(entry: Entry, chainName: string): Promise { + 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}`); @@ -150,7 +171,7 @@ export async function addDkimEntry(entry: Entry): Promise { 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; @@ -163,13 +184,13 @@ export async function addDkimEntry(entry: Entry): Promise { 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'); } @@ -194,16 +215,19 @@ export async function addDkimEntry(entry: Entry): Promise { console.log(`Transaction sent: ${hash}`); } -export async function checkDKIMPublicKeyHash(domain: string, pubkeyHash: bigint): Promise { - 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 { + 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() }); diff --git a/packages/app/src/lib/contract/index.ts b/packages/app/src/lib/contract/index.ts index 90687e5..a36d3cd 100644 --- a/packages/app/src/lib/contract/index.ts +++ b/packages/app/src/lib/contract/index.ts @@ -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'; @@ -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) }); diff --git a/packages/app/src/lib/models/entry.ts b/packages/app/src/lib/models/entry.ts index 44fcf5d..5d5bd0f 100644 --- a/packages/app/src/lib/models/entry.ts +++ b/packages/app/src/lib/models/entry.ts @@ -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: { diff --git a/packages/app/src/scripts/migrate_chains.ts b/packages/app/src/scripts/migrate_chains.ts new file mode 100644 index 0000000..d5d80ab --- /dev/null +++ b/packages/app/src/scripts/migrate_chains.ts @@ -0,0 +1,39 @@ +import { CHAINS } from "@/lib/contract-deploy"; +import prisma from "@/lib/prisma"; + +const DKIM_CONTRACTS = { + "Ethereum Sepolia": "0xCD0E07250d70f07BbA71419969E3Ab9840D00A0a", + // "Optimism Sepolia": "0x94d987d05732dFC3A98cD4B5d3B1d27677568745", + "Arbitrum": "0xA605d110572c3efD43662785721b3f5cBdcF7f66", + "Arbitrum Sepolia": "0xdFA0726F9DA74568d97aCbB3205Fcf40c5730Ada", +}; + +(async function() { + const entries = await prisma.entry.findMany(); + for (const entry of entries) { + if (entry.contractAddress && !entry.contractAddress.startsWith("0x")) { + console.log(`Entry ${entry.slug} has contract address ${entry.contractAddress}, skipping`) + await prisma.contractDeployment.create({ + data: { + chainName: "Ethereum Sepolia", + contractAddress: entry.contractAddress, + verifierContractAddress: entry.verifierContractAddress, + entryId: entry.id, + status: "COMPLETED" + } + }) + } + } + + for (const [chainName, dkimContractAddress] of Object.entries(DKIM_CONTRACTS)) { + const chain = CHAINS[chainName]; + await prisma.chain.create({ + data: { + chainId: ""+chain.id, + chainName, + dkimContractAddress, + rpcUrl:chain.rpcUrls.default.http[0] + } + }) + } +})() \ No newline at end of file diff --git a/packages/app/src/scripts/write_dkim_entry.ts b/packages/app/src/scripts/write_dkim_entry.ts index 2cf1b65..bf54963 100644 --- a/packages/app/src/scripts/write_dkim_entry.ts +++ b/packages/app/src/scripts/write_dkim_entry.ts @@ -10,7 +10,7 @@ import { prisma } from "@/lib/prisma"; for (const entry of entries) { console.log("Adding DKIM entry for entry: ", entry.slug) try { - await addDkimEntry(entry) + await addDkimEntry(entry, "Ethereum Sepolia") } catch { console.log("Error adding DKIM entry for: ", entry.slug); } diff --git a/packages/app/src/workers/contract-deployer.ts b/packages/app/src/workers/contract-deployer.ts index bc70eb5..6a0e36d 100644 --- a/packages/app/src/workers/contract-deployer.ts +++ b/packages/app/src/workers/contract-deployer.ts @@ -1,39 +1,47 @@ import { generateCodeLibrary } from "@/lib/code-gen/gen"; import { addDkimEntry, buildContract, deployContract, readContractAddresses, deployContractWithModal } from "@/lib/contract-deploy" -import { getFirstUndeployedEntry } from "@/lib/models/entry" import prisma from "@/lib/prisma" +import { error } from "console"; + +async function getUndeployedEntry() { + const deployment = await prisma.contractDeployment.findFirst({where: {status: "PENDING"}, include: {entry: true}}) + return deployment; +} async function startContractDeployerService() { while (true) { await new Promise(resolve => setTimeout(resolve, 1000)); - const entry = await getFirstUndeployedEntry(); - if (entry) { + const deployment = await getUndeployedEntry(); + if (deployment) { try { + const entry = deployment.entry; if (entry.withModal) { - await deployContractWithModal(entry); + await deployContractWithModal(entry, deployment.chainName); } else { await generateCodeLibrary(entry.parameters, entry.slug, entry.status); await deployContract(entry); } - await addDkimEntry(entry); + await addDkimEntry(entry, deployment.chainName); const addresses = await readContractAddresses(entry); - await prisma.entry.update({ + await prisma.contractDeployment.update({ where: { - id: entry.id + id: deployment.id }, data: { + status: "COMPLETED", verifierContractAddress: addresses.verifier, - contractAddress: addresses.contract + contractAddress: addresses.contract, + error: null, } }); } catch (error) { - await prisma.entry.update({ + await prisma.contractDeployment.update({ where: { - id: entry.id + id: deployment.id }, data: { - verifierContractAddress: "error:" + (error as Error).toString(), - contractAddress: "error:" + (error as Error).toString() + status: "ERROR", + error: "error:" + (error as Error).toString(), } }); }