From 06aa9bfa666e2c53f4d664646413f6a0c4511e30 Mon Sep 17 00:00:00 2001 From: Benjamin A <97291322+Baoufa@users.noreply.github.com> Date: Sun, 16 Jul 2023 20:02:59 +0200 Subject: [PATCH 1/6] feat: wagmi logic in hook + nonce --- front/src/app/page.tsx | 155 +++----------------------- front/src/app/sismo-connect-config.ts | 4 +- front/src/utils/misc.ts | 42 +++++++ front/src/utils/useContract.tsx | 148 ++++++++++++++++++++++++ 4 files changed, 210 insertions(+), 139 deletions(-) create mode 100644 front/src/utils/useContract.tsx diff --git a/front/src/app/page.tsx b/front/src/app/page.tsx index 25e9449..18b9711 100644 --- a/front/src/app/page.tsx +++ b/front/src/app/page.tsx @@ -1,126 +1,55 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import Header from "./components/Header"; import { useAccount, - useConnect, - useContractWrite, - useDisconnect, useNetwork, - usePrepareContractWrite, useSwitchNetwork, } from "wagmi"; -import { waitForTransaction, readContract } from "@wagmi/core"; import { useConnectModal } from "@rainbow-me/rainbowkit"; -import { decodeEventLog, formatEther } from "viem"; -import { abi as AirdropABI } from "../../../abi/Airdrop.json"; -import { formatError, signMessage } from "@/utils/misc"; +import { getProofDataForAuth, getProofDataForClaim, getuserIdFromHex, signMessage } from "@/utils/misc"; import { mumbaiFork } from "@/utils/wagmi"; import { SismoConnectButton, // the Sismo Connect React button displayed below } from "@sismo-core/sismo-connect-react"; -import { transactions } from "../../../broadcast/Airdrop.s.sol/5151111/run-latest.json"; import { fundMyAccountOnLocalFork } from "@/utils/fundMyAccountOnLocalFork"; -import { errorsABI } from "@/utils/errorsABI"; import { AUTHS, CLAIMS, CONFIG, - VerifiedAuth, - VerifiedClaim, AuthType, ClaimType, } from "@/app/sismo-connect-config"; +import useContract from "@/utils/useContract"; /* ******************** Defines the chain to use *************************** */ const CHAIN = mumbaiFork; export default function Home() { - /* *********************** Application states *************************** */ - const [loading, setLoading] = useState(false); - const [error, setError] = useState(""); - const [pageState, setPageState] = useState("init"); - const [amountClaimed, setAmountClaimed] = useState(""); - const [responseBytes, setResponseBytes] = useState(""); - const [verifiedClaims, setVerifiedClaims] = useState(); - const [verifiedAuths, setVerifiedAuths] = useState(); - const [verifiedSignedMessage, setVerifiedSignedMessage] = useState(); - - /* *************** Wagmi hooks for wallet connection ******************** */ - const { disconnect } = useDisconnect(); - const { chain } = useNetwork(); + const [responseBytes, setResponseBytes] = useState(null); const { isConnected, address } = useAccount({ onConnect: async ({ address }) => address && (await fundMyAccountOnLocalFork(address)), }); - const { switchNetworkAsync, switchNetwork } = useSwitchNetwork(); + const { chain } = useNetwork(); + const { switchNetwork } = useSwitchNetwork(); const { openConnectModal, connectModalOpen } = useConnectModal(); - /* ************* Wagmi hooks for contract interaction ******************* */ - const contractCallInputs = - responseBytes && chain - ? { - address: transactions[0].contractAddress as `0x${string}}`, - abi: [...AirdropABI, ...errorsABI], - functionName: "claimWithSismo", - args: [responseBytes], - chain, - } - : {}; - - const { config, error: wagmiSimulateError } = usePrepareContractWrite(contractCallInputs); - const { writeAsync } = useContractWrite(config); - - /* ************* Handle simulateContract call & chain errors ************ */ - useEffect(() => { - if (chain?.id !== CHAIN.id) return setError(`Please switch to ${CHAIN.name} network`); - setError(""); - }, [chain]); - - useEffect(() => { - if (!wagmiSimulateError) return; - if (!isConnected) return; - return setError(formatError(wagmiSimulateError)); - }, [wagmiSimulateError, isConnected]); - - /* ************ Handle the airdrop claim button click ******************* */ - async function claimAirdrop() { - if (!address) return; - setError(""); - setLoading(true); - try { - // Switch to the selected network if not already on it - if (chain?.id !== CHAIN.id) await switchNetworkAsync?.(CHAIN.id); - setPageState("confirmingTransaction"); - const tx = await writeAsync?.(); - setPageState("verifying"); - const txReceipt = tx && (await waitForTransaction({ hash: tx.hash })); - if (txReceipt?.status === "success") { - setAmountClaimed( - formatEther((await readAirdropContract("balanceOf", [address])) as unknown as bigint) - ); - setVerifiedClaims((await readAirdropContract("getVerifiedClaims")) as VerifiedClaim[]); - setVerifiedAuths((await readAirdropContract("getVerifiedAuths")) as VerifiedAuth[]); - setVerifiedSignedMessage((await readAirdropContract("getVerifiedSignedMessage")) as string); - setPageState("verified"); - } - } catch (e: any) { - setError(formatError(e)); - } finally { - setLoading(false); - } - } + const { + claimAirdrop, + reset, + amountClaimed, + error, + pageState, + verifiedAuths, + verifiedClaims, + verifiedSignedMessage, + } = useContract({ responseBytes, chain: CHAIN }); /* ************************* Reset state **************************** */ function resetApp() { - disconnect(); - setAmountClaimed(""); + reset(); setResponseBytes(""); - setError(""); - setPageState("init"); - const url = new URL(window.location.href); - url.searchParams.delete("sismoConnectResponseCompressed"); - window.history.replaceState({}, "", url.toString()); } return ( @@ -159,7 +88,6 @@ export default function Home() { // onResponseBytes calls a 'setResponse' function with the responseBytes returned by the Sismo Vault onResponseBytes={(responseBytes: string) => { setResponseBytes(responseBytes); - setPageState("responseReceived"); }} // Some text to display on the button text={"Claim with Sismo"} @@ -354,52 +282,3 @@ export default function Home() { ); } -function readibleHex(userId: string, startLength = 6, endLength = 4, separator = "...") { - if (!userId.toString().startsWith("0x")) { - return userId; // Return the original string if it doesn't start with "0x" - } - return userId.substring(0, startLength) + separator + userId.substring(userId.length - endLength); -} - -function getProofDataForAuth(verifiedAuths: VerifiedAuth[], authType: AuthType): string | null { - for (const auth of verifiedAuths) { - if (auth.proofData && auth.authType === authType) { - return readibleHex("0x" + (auth.proofData as unknown as number).toString(16)); - } - } - - return null; // returns null if no matching authType is found -} - -function getProofDataForClaim( - verifiedClaims: VerifiedClaim[], - claimType: number, - groupId: string, - value: number -): string | null { - for (const claim of verifiedClaims) { - if (claim.proofData && claim.claimType === claimType && claim.groupId === groupId) { - return readibleHex("0x" + (claim.proofData as unknown as number).toString(16)); - } - } - - return null; // returns null if no matching authType is found -} - -function getuserIdFromHex(hexUserId: string) { - const index = hexUserId.lastIndexOf("000000"); - if (index !== -1) { - return hexUserId.substring(index + 6); - } else { - return hexUserId; // returns the original string if '00' is not found - } -} - -const readAirdropContract = async (functionName: string, args?: string[]) => { - return readContract({ - address: transactions[0].contractAddress as `0x${string}}`, - abi: AirdropABI, - functionName, - args: args || [], - }); -}; diff --git a/front/src/app/sismo-connect-config.ts b/front/src/app/sismo-connect-config.ts index a161c68..65614b1 100644 --- a/front/src/app/sismo-connect-config.ts +++ b/front/src/app/sismo-connect-config.ts @@ -7,8 +7,10 @@ import { VerifiedAuth, VerifiedClaim, } from "@sismo-core/sismo-connect-client"; +import { SignatureRequest } from "@sismo-core/sismo-connect-react"; -export { ClaimType, AuthType, VerifiedAuth, VerifiedClaim }; +export { ClaimType, AuthType }; +export type { VerifiedAuth, VerifiedClaim }; export const CONFIG: SismoConnectConfig = { appId: "0x32403ced4b65f2079eda77c84e7d2be6", vault: { diff --git a/front/src/utils/misc.ts b/front/src/utils/misc.ts index cd08f66..ffe7a9a 100644 --- a/front/src/utils/misc.ts +++ b/front/src/utils/misc.ts @@ -1,6 +1,7 @@ import { encodeAbiParameters } from "viem"; import { abi as AirdropABI } from "../../../abi/Airdrop.json"; import { errorsABI } from "./errorsABI"; +import { AuthType, VerifiedAuth, VerifiedClaim } from "@/app/sismo-connect-config"; declare global { interface Window { @@ -29,3 +30,44 @@ export const formatError = (error: Error | null) => { if (!error) return ""; return error?.message?.split("args:")?.[0]?.split("data:")?.[0]?.trim() || ""; }; + +export function readibleHex(userId: string, startLength = 6, endLength = 4, separator = "...") { + if (!userId.toString().startsWith("0x")) { + return userId; // Return the original string if it doesn't start with "0x" + } + return userId.substring(0, startLength) + separator + userId.substring(userId.length - endLength); +} + +export function getProofDataForAuth(verifiedAuths: VerifiedAuth[], authType: AuthType): string | null { + for (const auth of verifiedAuths) { + if (auth.proofData && auth.authType === authType) { + return readibleHex("0x" + (auth.proofData as unknown as number).toString(16)); + } + } + + return null; // returns null if no matching authType is found +} + +export function getProofDataForClaim( + verifiedClaims: VerifiedClaim[], + claimType: number, + groupId: string, + value: number +): string | null { + for (const claim of verifiedClaims) { + if (claim.proofData && claim.claimType === claimType && claim.groupId === groupId) { + return readibleHex("0x" + (claim.proofData as unknown as number).toString(16)); + } + } + + return null; // returns null if no matching authType is found +} + +export function getuserIdFromHex(hexUserId: string) { + const index = hexUserId.lastIndexOf("000000"); + if (index !== -1) { + return hexUserId.substring(index + 6); + } else { + return hexUserId; // returns the original string if '00' is not found + } +} \ No newline at end of file diff --git a/front/src/utils/useContract.tsx b/front/src/utils/useContract.tsx new file mode 100644 index 0000000..9d9e861 --- /dev/null +++ b/front/src/utils/useContract.tsx @@ -0,0 +1,148 @@ +import { useEffect, useState } from "react"; +import { Chain, formatEther } from "viem"; +import { + useAccount, + useContractWrite, + useNetwork, + usePrepareContractWrite, + usePublicClient, + useSwitchNetwork, +} from "wagmi"; +import { waitForTransaction, readContract } from "@wagmi/core"; +import { abi as AirdropABI } from "../../../abi/Airdrop.json"; +import { errorsABI } from "./errorsABI"; +import { formatError } from "./misc"; +import { VerifiedAuth, VerifiedClaim } from "@/app/sismo-connect-config"; +import { fundMyAccountOnLocalFork } from "./fundMyAccountOnLocalFork"; +import { transactions } from "../../../broadcast/Airdrop.s.sol/5151111/run-latest.json"; + +export type ContractClaim = { + claimAirdrop: () => Promise; + reset: () => void; + error: string; + amountClaimed: string; + pageState: string; + verifiedClaims: VerifiedClaim[] | undefined; + verifiedAuths: VerifiedAuth[] | undefined; + verifiedSignedMessage: string | undefined; +}; + +export default function useContract({ + responseBytes, + chain, +}: { + responseBytes: string | null; + chain: Chain; +}): ContractClaim { + const [error, setError] = useState(""); + const [pageState, setPageState] = useState("init"); + const [amountClaimed, setAmountClaimed] = useState(""); + const [verifiedClaims, setVerifiedClaims] = useState(); + const [verifiedAuths, setVerifiedAuths] = useState(); + const [verifiedSignedMessage, setVerifiedSignedMessage] = useState(); + const [nonce, setNonce] = useState(null); + + const publicClient = usePublicClient(); + const { chain: currentChain } = useNetwork(); + const { isConnected, address } = useAccount({ + onConnect: async ({ address }) => address && (await fundMyAccountOnLocalFork(address)), + }); + const { switchNetworkAsync } = useSwitchNetwork(); + const contractCallInputs = { + address: transactions[0].contractAddress as `0x${string}}`, + abi: [...AirdropABI, ...errorsABI], + functionName: "claimWithSismo", + args: [responseBytes], + chain, + enabled: Boolean(responseBytes) && Boolean(typeof nonce === "number"), + }; + const { config, error: wagmiSimulateError } = usePrepareContractWrite(contractCallInputs); + const { writeAsync } = useContractWrite(config); + + useEffect(() => { + if (!responseBytes) return; + setPageState("responseReceived"); + }, [responseBytes]); + + useEffect(() => { + if (!address) return; + if (currentChain?.id !== chain?.id) return; + + const fetchNonce = async () => { + const nonce = await publicClient.getTransactionCount({ + address: address || "0x00", + blockTag: "latest", + }); + setNonce(nonce); + }; + + fetchNonce(); + }, [address, chain, currentChain]); + + /* ************* Handle simulateContract call & chain errors ************ */ + useEffect(() => { + if (currentChain?.id !== chain.id) return setError(`Please switch to ${chain.name} network`); + setError(""); + }, [currentChain]); + + useEffect(() => { + if (!wagmiSimulateError) return; + if (!isConnected) return; + return setError(formatError(wagmiSimulateError)); + }, [wagmiSimulateError, isConnected]); + + /* ************ Handle the airdrop claim button click ******************* */ + async function claimAirdrop() { + if (!address) return; + setError(""); + try { + if (currentChain?.id !== chain.id) await switchNetworkAsync?.(chain.id); + setPageState("confirmingTransaction"); + const tx = await writeAsync?.(); + setPageState("verifying"); + const txReceipt = tx && (await waitForTransaction({ hash: tx.hash })); + if (txReceipt?.status === "success") { + setAmountClaimed( + formatEther((await readAirdropContract("balanceOf", [address])) as unknown as bigint) + ); + setVerifiedClaims((await readAirdropContract("getVerifiedClaims")) as VerifiedClaim[]); + setVerifiedAuths((await readAirdropContract("getVerifiedAuths")) as VerifiedAuth[]); + setVerifiedSignedMessage((await readAirdropContract("getVerifiedSignedMessage")) as string); + setPageState("verified"); + } + } catch (e: any) { + setError(formatError(e)); + } finally { + setNonce(null); + } + } + + const readAirdropContract = async (functionName: string, args?: string[]) => { + return readContract({ + address: transactions[0].contractAddress as `0x${string}}`, + abi: AirdropABI, + functionName, + args: args || [], + }); + }; + + function reset() { + setAmountClaimed(""); + setError(""); + setPageState("init"); + const url = new URL(window.location.href); + url.searchParams.delete("sismoConnectResponseCompressed"); + window.history.replaceState({}, "", url.toString()); + } + + return { + claimAirdrop, + reset, + error, + pageState, + amountClaimed, + verifiedClaims, + verifiedAuths, + verifiedSignedMessage, + }; +} From 3d6fd35bcedb219cc2ce8a7745adb20580d311a4 Mon Sep 17 00:00:00 2001 From: Benjamin A <97291322+Baoufa@users.noreply.github.com> Date: Sun, 16 Jul 2023 20:10:31 +0200 Subject: [PATCH 2/6] feat: add nonce to contract write --- front/src/utils/useContract.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/front/src/utils/useContract.tsx b/front/src/utils/useContract.tsx index 9d9e861..3553a9f 100644 --- a/front/src/utils/useContract.tsx +++ b/front/src/utils/useContract.tsx @@ -54,6 +54,7 @@ export default function useContract({ functionName: "claimWithSismo", args: [responseBytes], chain, + nonce: nonce || undefined, enabled: Boolean(responseBytes) && Boolean(typeof nonce === "number"), }; const { config, error: wagmiSimulateError } = usePrepareContractWrite(contractCallInputs); From e9cb9cb1f597a8e2f5558b43108047a3a172bd64 Mon Sep 17 00:00:00 2001 From: Benjamin A <97291322+Baoufa@users.noreply.github.com> Date: Sun, 16 Jul 2023 20:20:46 +0200 Subject: [PATCH 3/6] feat: add timeout --- front/src/app/page.tsx | 24 +++++++---------- front/src/utils/useContract.tsx | 46 +++++++++++++++------------------ 2 files changed, 30 insertions(+), 40 deletions(-) diff --git a/front/src/app/page.tsx b/front/src/app/page.tsx index 18b9711..ea7b384 100644 --- a/front/src/app/page.tsx +++ b/front/src/app/page.tsx @@ -2,25 +2,20 @@ import { useState } from "react"; import Header from "./components/Header"; -import { - useAccount, - useNetwork, - useSwitchNetwork, -} from "wagmi"; +import { useAccount, useNetwork, useSwitchNetwork } from "wagmi"; import { useConnectModal } from "@rainbow-me/rainbowkit"; -import { getProofDataForAuth, getProofDataForClaim, getuserIdFromHex, signMessage } from "@/utils/misc"; +import { + getProofDataForAuth, + getProofDataForClaim, + getuserIdFromHex, + signMessage, +} from "@/utils/misc"; import { mumbaiFork } from "@/utils/wagmi"; import { SismoConnectButton, // the Sismo Connect React button displayed below } from "@sismo-core/sismo-connect-react"; import { fundMyAccountOnLocalFork } from "@/utils/fundMyAccountOnLocalFork"; -import { - AUTHS, - CLAIMS, - CONFIG, - AuthType, - ClaimType, -} from "@/app/sismo-connect-config"; +import { AUTHS, CLAIMS, CONFIG, AuthType, ClaimType } from "@/app/sismo-connect-config"; import useContract from "@/utils/useContract"; /* ******************** Defines the chain to use *************************** */ @@ -116,7 +111,7 @@ export default function Home() { {isConnected && !amountClaimed && error && ( <> -

{error}

+

{error}

{error.slice(0, 16) === "Please switch to" && ( )} @@ -281,4 +276,3 @@ export default function Home() { ); } - diff --git a/front/src/utils/useContract.tsx b/front/src/utils/useContract.tsx index 3553a9f..7b32022 100644 --- a/front/src/utils/useContract.tsx +++ b/front/src/utils/useContract.tsx @@ -1,11 +1,10 @@ import { useEffect, useState } from "react"; -import { Chain, formatEther } from "viem"; +import { Chain, TransactionReceipt, formatEther } from "viem"; import { useAccount, useContractWrite, useNetwork, usePrepareContractWrite, - usePublicClient, useSwitchNetwork, } from "wagmi"; import { waitForTransaction, readContract } from "@wagmi/core"; @@ -40,9 +39,6 @@ export default function useContract({ const [verifiedClaims, setVerifiedClaims] = useState(); const [verifiedAuths, setVerifiedAuths] = useState(); const [verifiedSignedMessage, setVerifiedSignedMessage] = useState(); - const [nonce, setNonce] = useState(null); - - const publicClient = usePublicClient(); const { chain: currentChain } = useNetwork(); const { isConnected, address } = useAccount({ onConnect: async ({ address }) => address && (await fundMyAccountOnLocalFork(address)), @@ -54,8 +50,7 @@ export default function useContract({ functionName: "claimWithSismo", args: [responseBytes], chain, - nonce: nonce || undefined, - enabled: Boolean(responseBytes) && Boolean(typeof nonce === "number"), + enabled: Boolean(responseBytes), }; const { config, error: wagmiSimulateError } = usePrepareContractWrite(contractCallInputs); const { writeAsync } = useContractWrite(config); @@ -65,21 +60,6 @@ export default function useContract({ setPageState("responseReceived"); }, [responseBytes]); - useEffect(() => { - if (!address) return; - if (currentChain?.id !== chain?.id) return; - - const fetchNonce = async () => { - const nonce = await publicClient.getTransactionCount({ - address: address || "0x00", - blockTag: "latest", - }); - setNonce(nonce); - }; - - fetchNonce(); - }, [address, chain, currentChain]); - /* ************* Handle simulateContract call & chain errors ************ */ useEffect(() => { if (currentChain?.id !== chain.id) return setError(`Please switch to ${chain.name} network`); @@ -101,7 +81,25 @@ export default function useContract({ setPageState("confirmingTransaction"); const tx = await writeAsync?.(); setPageState("verifying"); - const txReceipt = tx && (await waitForTransaction({ hash: tx.hash })); + let txReceipt: TransactionReceipt | undefined; + if (chain.id === 5151111) { + const timeout = new Promise((_, reject) => + setTimeout( + () => + reject( + new Error( + "Local fork error: operation timed out after 15 seconds, if you are running a local fork on Anvil please make sure to reset your wallet nonce." + ) + ), + 15000 + ) + ); + const txReceiptPromise = tx && waitForTransaction({ hash: tx.hash }); + const race = await Promise.race([txReceiptPromise, timeout]); + txReceipt = race as TransactionReceipt; + } else { + txReceipt = tx && (await waitForTransaction({ hash: tx.hash })); + } if (txReceipt?.status === "success") { setAmountClaimed( formatEther((await readAirdropContract("balanceOf", [address])) as unknown as bigint) @@ -113,8 +111,6 @@ export default function useContract({ } } catch (e: any) { setError(formatError(e)); - } finally { - setNonce(null); } } From 1f588af0e1e65aa9434988677b42d25c81a5e6ed Mon Sep 17 00:00:00 2001 From: Benjamin A <97291322+Baoufa@users.noreply.github.com> Date: Sun, 16 Jul 2023 20:21:48 +0200 Subject: [PATCH 4/6] feat: remove typo --- front/src/utils/useContract.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/src/utils/useContract.tsx b/front/src/utils/useContract.tsx index 7b32022..8fa6730 100644 --- a/front/src/utils/useContract.tsx +++ b/front/src/utils/useContract.tsx @@ -45,7 +45,7 @@ export default function useContract({ }); const { switchNetworkAsync } = useSwitchNetwork(); const contractCallInputs = { - address: transactions[0].contractAddress as `0x${string}}`, + address: transactions[0].contractAddress as `0x${string}`, abi: [...AirdropABI, ...errorsABI], functionName: "claimWithSismo", args: [responseBytes], From c3b4576cce0fab108f8cb75135b2c2d53db73a11 Mon Sep 17 00:00:00 2001 From: Benjamin A <97291322+Baoufa@users.noreply.github.com> Date: Sun, 16 Jul 2023 20:33:20 +0200 Subject: [PATCH 5/6] fix: piut a 10sc timer on local --- front/src/utils/useContract.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/src/utils/useContract.tsx b/front/src/utils/useContract.tsx index 8fa6730..20b10d9 100644 --- a/front/src/utils/useContract.tsx +++ b/front/src/utils/useContract.tsx @@ -91,7 +91,7 @@ export default function useContract({ "Local fork error: operation timed out after 15 seconds, if you are running a local fork on Anvil please make sure to reset your wallet nonce." ) ), - 15000 + 10000 ) ); const txReceiptPromise = tx && waitForTransaction({ hash: tx.hash }); From d00e7d86757abda694afc50caa3d2a3107425cc5 Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes <35774097+dhadrien@users.noreply.github.com> Date: Mon, 17 Jul 2023 08:50:22 +0200 Subject: [PATCH 6/6] fix: cleaned app and contracts --- .gitignore | 2 +- abi/Airdrop.json | 103 ++++++++++++++++++++++++++ front/src/app/components/Header.tsx | 9 +++ front/src/app/globals.css | 7 +- front/src/app/page.tsx | 22 ++---- front/src/app/sismo-connect-config.ts | 5 +- front/src/utils/useContract.tsx | 17 ++--- src/Airdrop.sol | 86 ++++++++++----------- 8 files changed, 180 insertions(+), 71 deletions(-) diff --git a/.gitignore b/.gitignore index 2228abd..1b9c631 100644 --- a/.gitignore +++ b/.gitignore @@ -56,4 +56,4 @@ docs/ .env # Sismo connect config -sismo-connect-config.json \ No newline at end of file +sismo-connect-config.json diff --git a/abi/Airdrop.json b/abi/Airdrop.json index 7e07132..47a1b58 100644 --- a/abi/Airdrop.json +++ b/abi/Airdrop.json @@ -146,6 +146,109 @@ "name": "Approval", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "components": [ + { + "internalType": "enum AuthType", + "name": "authType", + "type": "uint8" + }, + { + "internalType": "bool", + "name": "isAnon", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "userId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "proofData", + "type": "bytes" + } + ], + "indexed": false, + "internalType": "struct VerifiedAuth", + "name": "verifiedAuth", + "type": "tuple" + } + ], + "name": "AuthVerified", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "components": [ + { + "internalType": "enum ClaimType", + "name": "claimType", + "type": "uint8" + }, + { + "internalType": "bytes16", + "name": "groupId", + "type": "bytes16" + }, + { + "internalType": "bytes16", + "name": "groupTimestamp", + "type": "bytes16" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "proofId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "proofData", + "type": "bytes" + } + ], + "indexed": false, + "internalType": "struct VerifiedClaim", + "name": "verifiedClaim", + "type": "tuple" + } + ], + "name": "ClaimVerified", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes", + "name": "verifiedSignedMessage", + "type": "bytes" + } + ], + "name": "SignedMessageVerified", + "type": "event" + }, { "anonymous": false, "inputs": [ diff --git a/front/src/app/components/Header.tsx b/front/src/app/components/Header.tsx index 0abd873..e373736 100644 --- a/front/src/app/components/Header.tsx +++ b/front/src/app/components/Header.tsx @@ -33,6 +33,15 @@ const Header: React.FC = () => { src/Airdrop.sol: Contract - verify Sismo Connect request, mint tokens and stores verified claims and auths

+

+ {" "} + Notes:
+ 1. If you are using metamask and transactions hang. Go to settings > advanced > clear activity and nonce data
+ 2. First ZK Proof generation takes longer time, especially with bad internet as there is a + zkey file to download once in the data vault connection
+ 3. The more proofs you request, the longer it takes to generate them (about 2 secs per + proof) +

); diff --git a/front/src/app/globals.css b/front/src/app/globals.css index 9707953..66f1a08 100644 --- a/front/src/app/globals.css +++ b/front/src/app/globals.css @@ -158,11 +158,12 @@ input:focus { .callout { - color: #d3d3d3; /* Light gray color */ + color: #000000; /* Black color text */ padding: 1rem; /* Adds some space around the text */ - border: 1px solid #d3d3d3; /* Adds a light gray border */ + border: 2px solid #000000; /* Increases border thickness and change to black */ border-radius: 5px; /* Rounds the corners of the border */ - background-color: rgba(0,0,0,0.1); /* Adds a very light black background to increase readability of gray text */ + background-color: rgba(255,255,255,0.6); /* Adds a semi-transparent white background to increase readability of black text */ + box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); /* Adds a slight shadow for a "lifted" effect */ } .verifying, .verified { diff --git a/front/src/app/page.tsx b/front/src/app/page.tsx index ea7b384..b6806ee 100644 --- a/front/src/app/page.tsx +++ b/front/src/app/page.tsx @@ -60,7 +60,7 @@ export default function Home() { <> <> {" "} - + {pageState != "init" && }

{`Chain: ${chain?.name} [${chain?.id}]`}
@@ -71,30 +71,22 @@ export default function Home() { <> { setResponseBytes(responseBytes); }} // Some text to display on the button text={"Claim with Sismo"} /> -

- {" "} - Notes:
- 1. First ZK Proof generation takes longer time, especially with bad internat as - there is a zkey file to download once in the data vault connection
- 2. The more proofs you request, the longer it takes to generate them (about 2 secs - per proof) -

)}
diff --git a/front/src/app/sismo-connect-config.ts b/front/src/app/sismo-connect-config.ts index 65614b1..1e5afa8 100644 --- a/front/src/app/sismo-connect-config.ts +++ b/front/src/app/sismo-connect-config.ts @@ -35,6 +35,7 @@ export const CONFIG: SismoConnectConfig = { // Sismo Connect Response in the vault instead of redirecting back to the app }; +// Request users to prove ownership of a Data Source (Wallet, Twitter, Github, Telegram, etc.) export const AUTHS: AuthRequest[] = [ // Anonymous identifier of the vault for this app // vaultId = hash(vaultSecret, appId). @@ -46,6 +47,7 @@ export const AUTHS: AuthRequest[] = [ // { authType: AuthType.TELEGRAM, userId: "875608110", isOptional: true }, ]; +// Request users to prove membership in a Data Group (e.g I own a wallet that is part of a DAO, owns an NFT, etc.) export const CLAIMS: ClaimRequest[] = [ { // claim on Sismo Hub GitHub Contributors Data Group membership: https://factory.sismo.io/groups-explorer?search=0xda1c3726426d5639f4c6352c2c976b87 @@ -71,11 +73,12 @@ export const CLAIMS: ClaimRequest[] = [ // request user to prove membership in the group with value = 10 groupId: "0xfae674b6cba3ff2f8ce2114defb200b1", claimType: ClaimType.EQ, - value: 10, // dhadrin.sismo.eth minted exactly 10, eligible + value: 10, // dhadrien.sismo.eth minted exactly 10, eligible isOptional: true, }, ]; +// Request users to sign a message export const SIGNATURE_REQUEST: SignatureRequest = { message: "I love Sismo!", isSelectableByUser: true, diff --git a/front/src/utils/useContract.tsx b/front/src/utils/useContract.tsx index 20b10d9..bb6777f 100644 --- a/front/src/utils/useContract.tsx +++ b/front/src/utils/useContract.tsx @@ -84,15 +84,14 @@ export default function useContract({ let txReceipt: TransactionReceipt | undefined; if (chain.id === 5151111) { const timeout = new Promise((_, reject) => - setTimeout( - () => - reject( - new Error( - "Local fork error: operation timed out after 15 seconds, if you are running a local fork on Anvil please make sure to reset your wallet nonce." - ) - ), - 10000 - ) + setTimeout(() => { + setPageState("responseReceived"); + reject( + new Error( + "Transaction timed-out: If you are running a local fork on Anvil please make sure to reset your wallet nonce. In metamask: Go to settings > advanced > clear activity and nonce data" + ) + ); + }, 10000) ); const txReceiptPromise = tx && waitForTransaction({ hash: tx.hash }); const race = await Promise.race([txReceiptPromise, timeout]); diff --git a/src/Airdrop.sol b/src/Airdrop.sol index 6711e5d..2cd593d 100644 --- a/src/Airdrop.sol +++ b/src/Airdrop.sol @@ -3,23 +3,30 @@ pragma solidity ^0.8.20; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "forge-std/console.sol"; -import "sismo-connect-solidity/SismoLib.sol"; // <--- add a Sismo Connect import +import "sismo-connect-solidity/SismoLib.sol"; /* * @title Airdrop * @author Sismo - * @dev Simple Airdrop contract that mints ERC20 tokens to the msg.sender - * This contract is used for tutorial purposes only - * It will be used to demonstrate how to integrate Sismo Connect + * @dev Simple Airdrop contract gated by Sismo Connect + * Application requests multiple zk proofs (auths and claims) and verify them + * The contract stores all verified results in storage */ contract Airdrop is ERC20, SismoConnect { error AlreadyClaimed(); + event AuthVerified(VerifiedAuth verifiedAuth); + event ClaimVerified(VerifiedClaim verifiedClaim); + event SignedMessageVerified(bytes verifiedSignedMessage); using SismoConnectHelper for SismoConnectVerifiedResult; mapping(uint256 => bool) public claimed; + // must correspond to requests defined in the app frontend + // Sismo Connect response's zk proofs will be checked against these requests. + // check Airdrop.s.sol to see how these requests are built and passed to the constructor AuthRequest[] private _authRequests; ClaimRequest[] private _claimRequests; + // Results of the verification of the Sismo Connect response. VerifiedAuth[] internal _verifiedAuths; VerifiedClaim[] internal _verifiedClaims; bytes internal _verifiedSignedMessage; @@ -36,66 +43,48 @@ contract Airdrop is ERC20, SismoConnect { _setClaims(claimRequests); } -// struct SismoConnectVerifiedResult { -// bytes16 appId; -// bytes16 namespace; -// bytes32 version; -// VerifiedAuth[] auths; -// VerifiedClaim[] claims; -// bytes signedMessage; -// } - function claimWithSismo(bytes memory response) public { SismoConnectVerifiedResult memory result = verify({ responseBytes: response, - // we want the user to prove that he owns a Sismo Vault - // we are recreating the auth request made in the frontend to be sure that - // the proofs provided in the response are valid with respect to this auth request + // checking response against requested auths auths: _authRequests, + // checking response against requested claims claims: _claimRequests, - // we also want to check if the signed message provided in the response is the signature of the user's address + // checking response against requested message signature signature: buildSignature({message: abi.encode(msg.sender)}) }); - for (uint256 i = 0; i < result.auths.length; i++) { - _verifiedAuths.push(result.auths[i]); - } - for (uint256 i = 0; i < result.claims.length; i++) { - _verifiedClaims.push(result.claims[i]); - } - - _verifiedSignedMessage =result.signedMessage; - - // if the proofs and signed message are valid, we take the userId from the verified result - // in this case the userId is the vaultId (since we used AuthType.VAULT in the auth request), // it is the anonymous identifier of a user's vault for a specific app // --> vaultId = hash(userVaultSecret, appId) + // used to avoid double claims uint256 vaultId = result.getUserId(AuthType.VAULT); - // we check if the user has already claimed the airdrop - // if (claimed[vaultId]) { - // revert AlreadyClaimed(); - // } + // checking if the user has already claimed + if (claimed[vaultId]) { + revert AlreadyClaimed(); + } - // we mark the user as claimed. We could also have stored more user airdrop information for a more complex airdrop system. But we keep it simple here. + // marking that the user has claimed claimed[vaultId] = true; + // airdrop amount = number of verified proofs uint256 airdropAmount = (result.auths.length + result.claims.length) * 10 ** 18; _mint(msg.sender, airdropAmount); - } - function _setAuths(AuthRequest[] memory auths) private { - for (uint256 i = 0; i < auths.length; i++) { - _authRequests.push(auths[i]); + // storing the result of the verification + for (uint256 i = 0; i < result.auths.length; i++) { + _verifiedAuths.push(result.auths[i]); + emit AuthVerified(result.auths[i]); } - } - - function _setClaims(ClaimRequest[] memory claims) private { - for (uint256 i = 0; i < claims.length; i++) { - _claimRequests.push(claims[i]); + for (uint256 i = 0; i < result.claims.length; i++) { + _verifiedClaims.push(result.claims[i]); + emit ClaimVerified(result.claims[i]); } + _verifiedSignedMessage =result.signedMessage; + emit SignedMessageVerified(result.signedMessage); } + function getVerifiedClaims() external view returns (VerifiedClaim[] memory) { return _verifiedClaims; } @@ -107,4 +96,17 @@ contract Airdrop is ERC20, SismoConnect { function getVerifiedSignedMessage() external view returns (bytes memory) { return _verifiedSignedMessage; } + + function _setAuths(AuthRequest[] memory auths) private { + for (uint256 i = 0; i < auths.length; i++) { + _authRequests.push(auths[i]); + } + } + + function _setClaims(ClaimRequest[] memory claims) private { + for (uint256 i = 0; i < claims.length; i++) { + _claimRequests.push(claims[i]); + } + } + }