From 0009b4b6a927ed945a76d53acd0774744ed747a5 Mon Sep 17 00:00:00 2001 From: Jimmy Chu <898091+jimmychu0807@users.noreply.github.com> Date: Fri, 29 Aug 2025 19:05:26 +0800 Subject: [PATCH 1/3] updated --- .github/workflows/ci.yml | 2 - packages/contracts/package.json | 5 +- .../script/DeploySelfVerification.s.sol | 2 +- .../script/TestVerificationCompletedEvent.sol | 28 ++ .../contracts/{lib => script}/listenEvents.ts | 10 +- packages/contracts/src/SelfVerification.sol | 9 + packages/web/package.json | 3 +- packages/web/src/abi/SelfVerification.json | 401 ++++++++++++++++++ .../src/app/api/verify-successful/route.ts | 9 + packages/web/src/app/layout.tsx | 3 +- packages/web/src/app/page.tsx | 64 ++- packages/web/src/components/Web3Provider.tsx | 7 +- packages/web/src/config.ts | 10 + pnpm-lock.yaml | 3 + 14 files changed, 536 insertions(+), 20 deletions(-) create mode 100644 packages/contracts/script/TestVerificationCompletedEvent.sol rename packages/contracts/{lib => script}/listenEvents.ts (71%) create mode 100644 packages/web/src/abi/SelfVerification.json create mode 100644 packages/web/src/app/api/verify-successful/route.ts create mode 100644 packages/web/src/config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dff9218..1046202 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,9 +11,7 @@ env: ID_HUB_ADDR: "0x68c931C9a534D37aa78094877F46fE46a49F1A51" SELF_SCOPE: "8881796546691635711476357071947099946348416895373566002268674093031321389112" SELF_CONFIG_ID: "0xc52f992ebee4435b00b65d2c74b12435e96359d1ccf408041528414e6ea687bc" - NEXT_PUBLIC_SELF_APP_NAME: "DHK dao Identity Verification" NEXT_PUBLIC_SELF_SCOPE: "8881796546691635711476357071947099946348416895373566002268674093031321389112" - NEXT_PUBLIC_SELF_ENDPOINT: "https://burro-concise-regularly.ngrok-free.app/api/verify" jobs: ci-check: diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 7a2df7d..8f8f462 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -6,10 +6,11 @@ "scripts": { "build": "forge build", "dev": "pnpm build && conc -n 'node,deploy' -c auto 'pnpm node' 'pnpm deploy:dev'", - "node": "anvil -f https://alfajores-forno.celo-testnet.org/", + "node": "anvil", "test": "forge test", "deploy:dev": "wait-on -t 10s tcp:localhost:8545 && forge script script/DeploySelfVerification.s.sol --rpc-url http://localhost:8545 --broadcast", - "listenEvents": "tsx lib/listenEvents.ts" + "script:listenEvents": "tsx script/listenEvents.ts", + "script:testVerificationCompletedEvent": "forge script script/TestVerificationCompletedEvent.sol --rpc-url http://localhost:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --broadcast --sig 'run(string,string,string)'" }, "keywords": [], "author": "", diff --git a/packages/contracts/script/DeploySelfVerification.s.sol b/packages/contracts/script/DeploySelfVerification.s.sol index ca5ae1b..188bbf7 100644 --- a/packages/contracts/script/DeploySelfVerification.s.sol +++ b/packages/contracts/script/DeploySelfVerification.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; +pragma solidity ^0.8.28; import {Script, console} from "forge-std/Script.sol"; import {SelfVerification} from "../src/SelfVerification.sol"; diff --git a/packages/contracts/script/TestVerificationCompletedEvent.sol b/packages/contracts/script/TestVerificationCompletedEvent.sol new file mode 100644 index 0000000..1883a31 --- /dev/null +++ b/packages/contracts/script/TestVerificationCompletedEvent.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {SelfVerification} from "../src/SelfVerification.sol"; + +contract TestVerificationCompletedEvent is Script { + SelfVerification public sv; + + function run( + string memory userId, + string memory nationality, + string memory userData + ) public { + address contractAddr = vm.envAddress("NEXT_PUBLIC_VERIFICATION_DEPLOYED_ADDR"); + sv = SelfVerification(contractAddr); + + vm.startBroadcast(); + + sv.testVerificationCompletedEvent( + vm.parseAddress(userId), + nationality, + bytes(userData) + ); + + vm.stopBroadcast(); + } +} diff --git a/packages/contracts/lib/listenEvents.ts b/packages/contracts/script/listenEvents.ts similarity index 71% rename from packages/contracts/lib/listenEvents.ts rename to packages/contracts/script/listenEvents.ts index 09b3230..5ebe849 100644 --- a/packages/contracts/lib/listenEvents.ts +++ b/packages/contracts/script/listenEvents.ts @@ -17,7 +17,13 @@ console.log(`Listening to on-chain events\nrpc-url: ${RPC_URL}, address: ${contr const unwatch = publicClient.watchEvent({ address: contractAddress, events: parseAbi([ - 'event VerificationCompleted(address indexed sender, string indexed nationality, bytes userData)' + 'event VerificationCompleted(address indexed sender, string indexed nationality, uint32 times, bytes userData)' ]), - onLogs: (logs) => console.log(logs) + onLogs: (logs) => { + for (const [i, log] of logs.entries()) { + console.log(`-- event ${i} --`); + console.log(`name: ${log.eventName}`); + console.log(`args:`, log.args); + } + } }); diff --git a/packages/contracts/src/SelfVerification.sol b/packages/contracts/src/SelfVerification.sol index 36357e9..500dee0 100644 --- a/packages/contracts/src/SelfVerification.sol +++ b/packages/contracts/src/SelfVerification.sol @@ -51,6 +51,15 @@ contract SelfVerification is SelfVerificationRoot, Ownable { // - etc. } + function testVerificationCompletedEvent( + address userId, + string memory nationality, + bytes memory userData + ) external onlyOwner { + verifiedHumans[userId] += 1; + emit VerificationCompleted(userId, nationality, verifiedHumans[userId], userData); + } + function getConfigId( bytes32 /* _destinationChainId */, bytes32 /* _userIdentifier */, diff --git a/packages/web/package.json b/packages/web/package.json index 5d8d31a..fa529ab 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -7,8 +7,6 @@ "build": "next build", "start": "next start", "lint": "tsc && prettier -c . && next lint", - "ngrok": "ngrok http --url=burro-concise-regularly.ngrok-free.app 3000", - "tunnel": "concurrently -n 'next,ngrok' -c auto 'pnpm dev' 'pnpm ngrok'", "prettier:fix": "prettier -w ." }, "dependencies": { @@ -33,6 +31,7 @@ "@types/react": "^18", "@types/react-dom": "^18", "concurrently": "catalog:", + "dotenv": "catalog:", "eslint": "^8", "eslint-config-next": "13.2.4", "pino-pretty": "^13.1.1", diff --git a/packages/web/src/abi/SelfVerification.json b/packages/web/src/abi/SelfVerification.json new file mode 100644 index 0000000..df6083c --- /dev/null +++ b/packages/web/src/abi/SelfVerification.json @@ -0,0 +1,401 @@ +{ +"abi": [ + { + "type": "constructor", + "inputs": [ + { + "name": "_identityVerificationHubV2Address", + "type": "address", + "internalType": "address" + }, + { + "name": "_scope", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "_verificationConfigId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "cancelOwnershipHandover", + "inputs": [], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "completeOwnershipHandover", + "inputs": [ + { + "name": "pendingOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "getConfigId", + "inputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isVerifiedHuman", + "inputs": [ + { + "name": "user", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "onVerificationSuccess", + "inputs": [ + { + "name": "output", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "userData", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "result", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "ownershipHandoverExpiresAt", + "inputs": [ + { + "name": "pendingOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "result", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "renounceOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "requestOwnershipHandover", + "inputs": [], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "scope", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "setConfigId", + "inputs": [ + { + "name": "configId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setScope", + "inputs": [ + { + "name": "newScope", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "testVerificationCompletedEvent", + "inputs": [ + { + "name": "userId", + "type": "address", + "internalType": "address" + }, + { + "name": "nationality", + "type": "string", + "internalType": "string" + }, + { + "name": "userData", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "verificationConfigId", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "verifiedHumans", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint32", + "internalType": "uint32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "verifySelfProof", + "inputs": [ + { + "name": "proofPayload", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "userContextData", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "OwnershipHandoverCanceled", + "inputs": [ + { + "name": "pendingOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipHandoverRequested", + "inputs": [ + { + "name": "pendingOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "oldOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ScopeUpdated", + "inputs": [ + { + "name": "newScope", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "VerificationCompleted", + "inputs": [ + { + "name": "sender", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "nationality", + "type": "string", + "indexed": true, + "internalType": "string" + }, + { + "name": "times", + "type": "uint32", + "indexed": false, + "internalType": "uint32" + }, + { + "name": "userData", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AlreadyInitialized", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidDataFormat", + "inputs": [] + }, + { + "type": "error", + "name": "NewOwnerIsZeroAddress", + "inputs": [] + }, + { + "type": "error", + "name": "NoHandoverRequest", + "inputs": [] + }, + { + "type": "error", + "name": "Unauthorized", + "inputs": [] + }, + { + "type": "error", + "name": "UnauthorizedCaller", + "inputs": [] + } + ] +} diff --git a/packages/web/src/app/api/verify-successful/route.ts b/packages/web/src/app/api/verify-successful/route.ts new file mode 100644 index 0000000..5cbe61a --- /dev/null +++ b/packages/web/src/app/api/verify-successful/route.ts @@ -0,0 +1,9 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function POST(req: NextRequest) { + console.log("Received request"); + console.log(req); + + // 1. get the account address + // 2. check on OP that it has DHK token >= 1 +} diff --git a/packages/web/src/app/layout.tsx b/packages/web/src/app/layout.tsx index a61edfe..142a67a 100644 --- a/packages/web/src/app/layout.tsx +++ b/packages/web/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; -import { GeistSans, GeistMono } from "geist/font"; +import { GeistSans } from "geist/font/sans"; +import { GeistMono } from "geist/font/mono"; import { Web3Provider } from "@/components/Web3Provider"; import "./globals.css"; diff --git a/packages/web/src/app/page.tsx b/packages/web/src/app/page.tsx index 737b60e..e455862 100644 --- a/packages/web/src/app/page.tsx +++ b/packages/web/src/app/page.tsx @@ -9,7 +9,8 @@ import { type SelfApp, } from "@selfxyz/qrcode"; import { clsx } from "clsx"; -import { useAccount } from "wagmi"; +import { useAccount, useConfig } from "wagmi"; +import { watchContractEvent } from "wagmi/actions"; import { Button, Field, @@ -19,6 +20,8 @@ import { DialogTitle, } from "@headlessui/react"; +import { appName, selfVerificationContract } from "@/config"; + export default function Home() { return (
@@ -34,7 +37,9 @@ function VerificationComponent() { const [selfApp, setSelfApp] = useState(); const [universalLink, setUniversalLink] = useState(""); const [message, setMessage] = useState(""); - const [isOpenDialog, setOpenDialog] = useState(false); + const [isDialogOpen, setDialogOpen] = useState(false); + const [unwatch, setUnwatch] = useState<() => void>(); + const config = useConfig(); const openDialog = () => { if (!account.address) return; @@ -48,7 +53,7 @@ function VerificationComponent() { version: 2, // app details - appName: process.env.NEXT_PUBLIC_SELF_APP_NAME || "", + appName, scope: process.env.NEXT_PUBLIC_SELF_SCOPE_SEED || "", logoBase64: "https://i.postimg.cc/mrmVf9hm/self.png", userId: account.address, @@ -72,24 +77,69 @@ function VerificationComponent() { setSelfApp(app); setUniversalLink(getUniversalLink(app)); - setOpenDialog(true); } catch (error) { console.error("Failed to initialize Self app:", error); + return; } + + setDialogOpen(true); + + // watch for VerificationCompleted event + console.log("watching..."); + console.log("address:", account.address); + console.log("config:", config); + + const { abi, address } = selfVerificationContract; + const unwatch = watchContractEvent(config, { + abi, + address, + eventName: "VerificationCompleted", + args: { + sender: account.address, + }, + onLogs(logs) { + console.log("Event received", logs); + }, + }); + + setUnwatch(unwatch); }; const closeDialog = () => { - setOpenDialog(false); + setDialogOpen(false); setTimeout(() => { setSelfApp(undefined); setUniversalLink(""); + if (unwatch) { + unwatch(); + setUnwatch(undefined); + } }, 1500); }; - const handleSuccessfulVerification = () => { + const handleSuccessfulVerification = async () => { console.log("Verification successful!"); closeDialog(); + // call backend function check api + try { + const res = await fetch("/api/verify-successful", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + address: account.address, + message, + }), + }); + + if (!res.ok) throw new Error("API request failed"); + + console.log("result:", res.json()); + } catch (err) { + console.error("Error calling verify-successful API:", err); + } }; const openSelfApp = () => { @@ -143,7 +193,7 @@ function VerificationComponent() {
diff --git a/packages/web/src/components/Web3Provider.tsx b/packages/web/src/components/Web3Provider.tsx index d505649..06a0907 100644 --- a/packages/web/src/components/Web3Provider.tsx +++ b/packages/web/src/components/Web3Provider.tsx @@ -2,15 +2,16 @@ import React from "react"; import { WagmiProvider, createConfig, webSocket } from "wagmi"; -import { celoAlfajores } from "wagmi/chains"; +import { celoAlfajores, foundry } from "wagmi/chains"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ConnectKitProvider, getDefaultConfig } from "connectkit"; +import { appName } from "@/config"; export const ckConfig = getDefaultConfig({ // Your dApps chains chains: [celoAlfajores], transports: { - // RPC URL for each chain + [foundry.id]: webSocket("http://localhost:8545"), [celoAlfajores.id]: webSocket( `https://celo-alfajores.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_ID}`, ), @@ -21,7 +22,7 @@ export const ckConfig = getDefaultConfig({ process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || "", // Required App Info - appName: "DHK dao self-prototype", + appName, // Optional App Info appDescription: "DHK dao self-prototype", diff --git a/packages/web/src/config.ts b/packages/web/src/config.ts new file mode 100644 index 0000000..a404a37 --- /dev/null +++ b/packages/web/src/config.ts @@ -0,0 +1,10 @@ +import type { Abi, Address } from "viem"; +import SelfVerificationABI from "@/abi/SelfVerification.json"; + +export const selfVerificationContract = { + address: (process.env.NEXT_PUBLIC_VERIFICATION_DEPLOYED_ADDR || + "0x") as Address, + abi: SelfVerificationABI.abi as Abi, +}; + +export const appName = "DHK dao Identity Verification" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb09908..77d8d7a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -123,6 +123,9 @@ importers: concurrently: specifier: 'catalog:' version: 9.2.1 + dotenv: + specifier: 'catalog:' + version: 17.2.1 eslint: specifier: ^8 version: 8.57.1 From 28880a811aa67f46216c6b72ca0010bf5140f38c Mon Sep 17 00:00:00 2001 From: Jimmy Chu <898091+jimmychu0807@users.noreply.github.com> Date: Sat, 30 Aug 2025 12:03:26 +0800 Subject: [PATCH 2/3] updated --- packages/web/package.json | 1 + packages/web/src/abi/SelfVerification.json | 716 +++++++++--------- packages/web/src/app/page.tsx | 228 +----- .../src/components/VerificationComponent.tsx | 243 ++++++ packages/web/src/components/Web3Provider.tsx | 2 +- packages/web/src/config.ts | 2 +- pnpm-lock.yaml | 92 ++- 7 files changed, 670 insertions(+), 614 deletions(-) create mode 100644 packages/web/src/components/VerificationComponent.tsx diff --git a/packages/web/package.json b/packages/web/package.json index fa529ab..33478e6 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -15,6 +15,7 @@ "@selfxyz/core": "^1.0.8", "@selfxyz/qrcode": "^1.0.11", "@tanstack/react-query": "^5.85.5", + "abitype": "^1.0.9", "clsx": "^2.1.1", "connectkit": "^1.9.1", "geist": "^1.4.2", diff --git a/packages/web/src/abi/SelfVerification.json b/packages/web/src/abi/SelfVerification.json index df6083c..1e9e1aa 100644 --- a/packages/web/src/abi/SelfVerification.json +++ b/packages/web/src/abi/SelfVerification.json @@ -1,401 +1,401 @@ { -"abi": [ - { - "type": "constructor", - "inputs": [ - { - "name": "_identityVerificationHubV2Address", - "type": "address", - "internalType": "address" - }, - { - "name": "_scope", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "_verificationConfigId", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "stateMutability": "nonpayable" - }, + "abi": [ + { + "type": "constructor", + "inputs": [ { - "type": "function", - "name": "cancelOwnershipHandover", - "inputs": [], - "outputs": [], - "stateMutability": "payable" + "name": "_identityVerificationHubV2Address", + "type": "address", + "internalType": "address" }, { - "type": "function", - "name": "completeOwnershipHandover", - "inputs": [ - { - "name": "pendingOwner", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "payable" + "name": "_scope", + "type": "uint256", + "internalType": "uint256" }, { - "type": "function", - "name": "getConfigId", - "inputs": [ - { - "name": "", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [ - { - "name": "", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "stateMutability": "view" - }, + "name": "_verificationConfigId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "cancelOwnershipHandover", + "inputs": [], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "completeOwnershipHandover", + "inputs": [ { - "type": "function", - "name": "isVerifiedHuman", - "inputs": [ - { - "name": "user", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, + "name": "pendingOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "getConfigId", + "inputs": [ { - "type": "function", - "name": "onVerificationSuccess", - "inputs": [ - { - "name": "output", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "userData", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [], - "stateMutability": "nonpayable" + "name": "", + "type": "bytes32", + "internalType": "bytes32" }, { - "type": "function", - "name": "owner", - "inputs": [], - "outputs": [ - { - "name": "result", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" + "name": "", + "type": "bytes32", + "internalType": "bytes32" }, { - "type": "function", - "name": "ownershipHandoverExpiresAt", - "inputs": [ - { - "name": "pendingOwner", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "result", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ { - "type": "function", - "name": "renounceOwnership", - "inputs": [], - "outputs": [], - "stateMutability": "payable" - }, + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isVerifiedHuman", + "inputs": [ { - "type": "function", - "name": "requestOwnershipHandover", - "inputs": [], - "outputs": [], - "stateMutability": "payable" - }, + "name": "user", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ { - "type": "function", - "name": "scope", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "onVerificationSuccess", + "inputs": [ { - "type": "function", - "name": "setConfigId", - "inputs": [ - { - "name": "configId", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "outputs": [], - "stateMutability": "nonpayable" + "name": "output", + "type": "bytes", + "internalType": "bytes" }, { - "type": "function", - "name": "setScope", - "inputs": [ - { - "name": "newScope", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, + "name": "userData", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ { - "type": "function", - "name": "testVerificationCompletedEvent", - "inputs": [ - { - "name": "userId", - "type": "address", - "internalType": "address" - }, - { - "name": "nationality", - "type": "string", - "internalType": "string" - }, - { - "name": "userData", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, + "name": "result", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "ownershipHandoverExpiresAt", + "inputs": [ { - "type": "function", - "name": "transferOwnership", - "inputs": [ - { - "name": "newOwner", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "payable" - }, + "name": "pendingOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ { - "type": "function", - "name": "verificationConfigId", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "stateMutability": "view" - }, + "name": "result", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "renounceOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "requestOwnershipHandover", + "inputs": [], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "scope", + "inputs": [], + "outputs": [ { - "type": "function", - "name": "verifiedHumans", - "inputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "uint32", - "internalType": "uint32" - } - ], - "stateMutability": "view" - }, + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "setConfigId", + "inputs": [ { - "type": "function", - "name": "verifySelfProof", - "inputs": [ - { - "name": "proofPayload", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "userContextData", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, + "name": "configId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setScope", + "inputs": [ { - "type": "event", - "name": "OwnershipHandoverCanceled", - "inputs": [ - { - "name": "pendingOwner", - "type": "address", - "indexed": true, - "internalType": "address" - } - ], - "anonymous": false - }, + "name": "newScope", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "testVerificationCompletedEvent", + "inputs": [ { - "type": "event", - "name": "OwnershipHandoverRequested", - "inputs": [ - { - "name": "pendingOwner", - "type": "address", - "indexed": true, - "internalType": "address" - } - ], - "anonymous": false + "name": "userId", + "type": "address", + "internalType": "address" }, { - "type": "event", - "name": "OwnershipTransferred", - "inputs": [ - { - "name": "oldOwner", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "newOwner", - "type": "address", - "indexed": true, - "internalType": "address" - } - ], - "anonymous": false + "name": "nationality", + "type": "string", + "internalType": "string" }, { - "type": "event", - "name": "ScopeUpdated", - "inputs": [ - { - "name": "newScope", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - } - ], - "anonymous": false - }, + "name": "userData", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ { - "type": "event", - "name": "VerificationCompleted", - "inputs": [ - { - "name": "sender", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "nationality", - "type": "string", - "indexed": true, - "internalType": "string" - }, - { - "name": "times", - "type": "uint32", - "indexed": false, - "internalType": "uint32" - }, - { - "name": "userData", - "type": "bytes", - "indexed": false, - "internalType": "bytes" - } - ], - "anonymous": false - }, + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "verificationConfigId", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "verifiedHumans", + "inputs": [ { - "type": "error", - "name": "AlreadyInitialized", - "inputs": [] + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint32", + "internalType": "uint32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "verifySelfProof", + "inputs": [ + { + "name": "proofPayload", + "type": "bytes", + "internalType": "bytes" }, { - "type": "error", - "name": "InvalidDataFormat", - "inputs": [] + "name": "userContextData", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "OwnershipHandoverCanceled", + "inputs": [ + { + "name": "pendingOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipHandoverRequested", + "inputs": [ + { + "name": "pendingOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "oldOwner", + "type": "address", + "indexed": true, + "internalType": "address" }, { - "type": "error", - "name": "NewOwnerIsZeroAddress", - "inputs": [] + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ScopeUpdated", + "inputs": [ + { + "name": "newScope", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "VerificationCompleted", + "inputs": [ + { + "name": "sender", + "type": "address", + "indexed": true, + "internalType": "address" }, { - "type": "error", - "name": "NoHandoverRequest", - "inputs": [] + "name": "nationality", + "type": "string", + "indexed": true, + "internalType": "string" }, { - "type": "error", - "name": "Unauthorized", - "inputs": [] + "name": "times", + "type": "uint32", + "indexed": false, + "internalType": "uint32" }, { - "type": "error", - "name": "UnauthorizedCaller", - "inputs": [] + "name": "userData", + "type": "bytes", + "indexed": false, + "internalType": "bytes" } - ] + ], + "anonymous": false + }, + { + "type": "error", + "name": "AlreadyInitialized", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidDataFormat", + "inputs": [] + }, + { + "type": "error", + "name": "NewOwnerIsZeroAddress", + "inputs": [] + }, + { + "type": "error", + "name": "NoHandoverRequest", + "inputs": [] + }, + { + "type": "error", + "name": "Unauthorized", + "inputs": [] + }, + { + "type": "error", + "name": "UnauthorizedCaller", + "inputs": [] + } + ] } diff --git a/packages/web/src/app/page.tsx b/packages/web/src/app/page.tsx index e455862..af9508c 100644 --- a/packages/web/src/app/page.tsx +++ b/packages/web/src/app/page.tsx @@ -1,26 +1,6 @@ "use client"; -import React, { useState } from "react"; -import { getUniversalLink } from "@selfxyz/core"; -import { ConnectKitButton } from "connectkit"; -import { - SelfAppBuilder, - SelfQRcodeWrapper, - type SelfApp, -} from "@selfxyz/qrcode"; -import { clsx } from "clsx"; -import { useAccount, useConfig } from "wagmi"; -import { watchContractEvent } from "wagmi/actions"; -import { - Button, - Field, - Input, - Dialog, - DialogPanel, - DialogTitle, -} from "@headlessui/react"; - -import { appName, selfVerificationContract } from "@/config"; +import VerificationComponent from "@/components/VerificationComponent"; export default function Home() { return ( @@ -31,209 +11,3 @@ export default function Home() { ); } - -function VerificationComponent() { - const account = useAccount(); - const [selfApp, setSelfApp] = useState(); - const [universalLink, setUniversalLink] = useState(""); - const [message, setMessage] = useState(""); - const [isDialogOpen, setDialogOpen] = useState(false); - const [unwatch, setUnwatch] = useState<() => void>(); - const config = useConfig(); - - const openDialog = () => { - if (!account.address) return; - - try { - const app = new SelfAppBuilder({ - // Contract integration settings - endpoint: process.env.NEXT_PUBLIC_VERIFICATION_DEPLOYED_ADDR, - endpointType: "staging_celo", - userIdType: "hex", - version: 2, - - // app details - appName, - scope: process.env.NEXT_PUBLIC_SELF_SCOPE_SEED || "", - logoBase64: "https://i.postimg.cc/mrmVf9hm/self.png", - userId: account.address, - userDefinedData: message, - disclosures: { - /* 1. what you want to verify from users' identity */ - minimumAge: 18, - ofac: false, - excludedCountries: [], - - /* 2. what you want users to reveal */ - // name: false, - // issuing_state: true, - nationality: true, - // date_of_birth: true, - // passport_number: false, - // gender: true, - // expiry_date: false, - }, - }).build(); - - setSelfApp(app); - setUniversalLink(getUniversalLink(app)); - } catch (error) { - console.error("Failed to initialize Self app:", error); - return; - } - - setDialogOpen(true); - - // watch for VerificationCompleted event - console.log("watching..."); - console.log("address:", account.address); - console.log("config:", config); - - const { abi, address } = selfVerificationContract; - const unwatch = watchContractEvent(config, { - abi, - address, - eventName: "VerificationCompleted", - args: { - sender: account.address, - }, - onLogs(logs) { - console.log("Event received", logs); - }, - }); - - setUnwatch(unwatch); - }; - - const closeDialog = () => { - setDialogOpen(false); - setTimeout(() => { - setSelfApp(undefined); - setUniversalLink(""); - if (unwatch) { - unwatch(); - setUnwatch(undefined); - } - }, 1500); - }; - - const handleSuccessfulVerification = async () => { - console.log("Verification successful!"); - closeDialog(); - - // call backend function check api - try { - const res = await fetch("/api/verify-successful", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - address: account.address, - message, - }), - }); - - if (!res.ok) throw new Error("API request failed"); - - console.log("result:", res.json()); - } catch (err) { - console.error("Error calling verify-successful API:", err); - } - }; - - const openSelfApp = () => { - if (universalLink) { - window.open(universalLink, "_blank"); - } - }; - - return ( - <> -
-

DHK dao Self Prototype

- -
-

1. Sign in with your wallet

- -
- - {account.address && ( -
-

2. Write your custom message

- - setMessage(e.target.value)} - /> - -
- )} - - {account.address && ( -
-

3. Verify yourself

- -
- )} -
- -
-
- - - Scan Self QR Code - - - {selfApp && SelfQRcodeWrapper ? ( -
- { - console.error("Error: Failed to verify identity"); - }} - /> - -
- ) : ( -
Loading QR Code...
- )} -
-
-
-
- - ); -} diff --git a/packages/web/src/components/VerificationComponent.tsx b/packages/web/src/components/VerificationComponent.tsx new file mode 100644 index 0000000..305c48a --- /dev/null +++ b/packages/web/src/components/VerificationComponent.tsx @@ -0,0 +1,243 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { getUniversalLink } from "@selfxyz/core"; +import { ConnectKitButton } from "connectkit"; +import { + SelfAppBuilder, + SelfQRcodeWrapper, + type SelfApp, +} from "@selfxyz/qrcode"; +import { clsx } from "clsx"; +import type { Log } from "viem"; +import { useAccount, useConfig, useWatchContractEvent } from "wagmi"; +import { watchContractEvent } from "wagmi/actions"; +import { + Button, + Field, + Input, + Dialog, + DialogPanel, + DialogTitle, +} from "@headlessui/react"; + +import { appName, selfVerificationContract } from "@/config"; +import type { Address } from "viem"; + +type VerificationCompletedEventArgs = { + sender: Address; + nationality: string; + times: number; + userData: string; +}; + +type EventLog = Log & { args: any }; + +export default function VerificationComponent() { + const account = useAccount(); + const config = useConfig(); + + const [selfApp, setSelfApp] = useState(); + const [universalLink, setUniversalLink] = useState(""); + const [message, setMessage] = useState(""); + const [isDialogOpen, setDialogOpen] = useState(false); + const [isFNCallback, setFNCallback] = useState(false); + const [evReceived, setEvReceived] = + useState(); + + // watch for VerificationCompleted event + useWatchContractEvent({ + address: selfVerificationContract.address, + abi: selfVerificationContract.abi, + eventName: "VerificationCompleted", + onLogs(logs: Log[]) { + // Use the last log only + if (logs.length > 1) { + console.error( + "Unexpected more than one VerificationCompleted event:", + logs.length, + ); + } + + // Extract the event arguments out + const args = (logs[logs.length - 1] as unknown as EventLog) + .args as VerificationCompletedEventArgs; + setEvReceived(args); + }, + }); + + const openDialog = () => { + if (!account.address) return; + + try { + const app = new SelfAppBuilder({ + // Contract integration settings + endpoint: process.env.NEXT_PUBLIC_VERIFICATION_DEPLOYED_ADDR, + endpointType: "staging_celo", + userIdType: "hex", + version: 2, + + // app details + appName, + scope: process.env.NEXT_PUBLIC_SELF_SCOPE_SEED || "", + logoBase64: "https://i.postimg.cc/mrmVf9hm/self.png", + userId: account.address, + userDefinedData: message, + disclosures: { + /* 1. what you want to verify from users' identity */ + minimumAge: 18, + ofac: false, + excludedCountries: [], + + /* 2. what you want users to reveal */ + // name: false, + // issuing_state: true, + nationality: true, + // date_of_birth: true, + // passport_number: false, + // gender: true, + // expiry_date: false, + }, + }).build(); + + setSelfApp(app); + setUniversalLink(getUniversalLink(app)); + } catch (error) { + console.error("Failed to initialize Self app:", error); + return; + } + + setDialogOpen(true); + }; + + const closeDialog = () => { + setDialogOpen(false); + setTimeout(() => { + setSelfApp(undefined); + setUniversalLink(""); + }, 1500); + }; + + // If the two states are set, call the backend /api/verify-successful + useEffect(() => { + if (!isFNCallback || !evReceived) return; + + (async () => { + const ev = { ...evReceived }; + + setFNCallback(false); + setEvReceived(undefined); + + // call backend function check api + try { + const res = await fetch("/api/verify-successful", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(ev), + }); + + if (!res.ok) throw new Error("API request failed"); + + console.log("result:", res.json()); + } catch (err) { + console.error("Error calling verify-successful API:", err); + } + })(); + }, [isFNCallback, evReceived]); + + return ( + <> +
+

DHK dao Self Prototype

+ +
+

1. Sign in with your wallet

+ +
+ + {account.address && ( +
+

2. Write your custom message

+ + setMessage(e.target.value)} + /> + +
+ )} + + {account.address && ( +
+

3. Verify yourself

+ +
+ )} +
+ +
+
+ + + Scan Self QR Code + + + {selfApp && SelfQRcodeWrapper ? ( +
+ { + setFNCallback(true); + closeDialog(); + }} + onError={() => { + console.error("Error: Failed to verify identity"); + }} + /> + +
+ ) : ( +
Loading QR Code...
+ )} +
+
+
+
+ + ); +} diff --git a/packages/web/src/components/Web3Provider.tsx b/packages/web/src/components/Web3Provider.tsx index 06a0907..50917bc 100644 --- a/packages/web/src/components/Web3Provider.tsx +++ b/packages/web/src/components/Web3Provider.tsx @@ -9,7 +9,7 @@ import { appName } from "@/config"; export const ckConfig = getDefaultConfig({ // Your dApps chains - chains: [celoAlfajores], + chains: [foundry, celoAlfajores], transports: { [foundry.id]: webSocket("http://localhost:8545"), [celoAlfajores.id]: webSocket( diff --git a/packages/web/src/config.ts b/packages/web/src/config.ts index a404a37..a321391 100644 --- a/packages/web/src/config.ts +++ b/packages/web/src/config.ts @@ -7,4 +7,4 @@ export const selfVerificationContract = { abi: SelfVerificationABI.abi as Abi, }; -export const appName = "DHK dao Identity Verification" +export const appName = "DHK dao Identity Verification"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77d8d7a..9d7949c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: '@tanstack/react-query': specifier: ^5.85.5 version: 5.85.5(react@18.3.1) + abitype: + specifier: ^1.0.9 + version: 1.0.9(typescript@5.9.2)(zod@3.22.4) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -1763,6 +1766,17 @@ packages: zod: optional: true + abitype@1.0.9: + resolution: {integrity: sha512-oN0S++TQmlwWuB+rkA6aiEefLv3SP+2l/tC5mux/TLj6qdA6rF15Vbpex4fHovLsMkwLwTIRj8/Q8vXCS3GfOg==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3 >=3.22.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -5313,10 +5327,10 @@ snapshots: '@babel/helpers': 7.28.3 '@babel/parser': 7.28.3 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.3(supports-color@5.5.0) + '@babel/traverse': 7.28.3 '@babel/types': 7.28.2 convert-source-map: 2.0.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -5345,6 +5359,13 @@ snapshots: '@babel/helper-globals@7.28.0': {} + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-imports@7.27.1(supports-color@5.5.0)': dependencies: '@babel/traverse': 7.28.3(supports-color@5.5.0) @@ -5355,9 +5376,9 @@ snapshots: '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.3)': dependencies: '@babel/core': 7.28.3 - '@babel/helper-module-imports': 7.27.1(supports-color@5.5.0) + '@babel/helper-module-imports': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.3(supports-color@5.5.0) + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color @@ -5391,6 +5412,18 @@ snapshots: '@babel/parser': 7.28.3 '@babel/types': 7.28.2 + '@babel/traverse@7.28.3': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.3 + '@babel/template': 7.27.2 + '@babel/types': 7.28.2 + debug: 4.4.1(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + '@babel/traverse@7.28.3(supports-color@5.5.0)': dependencies: '@babel/code-frame': 7.27.1 @@ -5633,7 +5666,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -5990,7 +6023,7 @@ snapshots: '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -6143,7 +6176,7 @@ snapshots: bufferutil: 4.0.9 cross-fetch: 4.1.0 date-fns: 2.30.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eciesjs: 0.4.15 eventemitter2: 6.4.9 readable-stream: 3.6.2 @@ -6167,7 +6200,7 @@ snapshots: '@paulmillr/qr': 0.2.1 bowser: 2.12.1 cross-fetch: 4.1.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eciesjs: 0.4.15 eth-rpc-errors: 4.0.3 eventemitter2: 6.4.9 @@ -6193,7 +6226,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@types/debug': 4.1.12 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) lodash.memoize: 4.1.2 pony-cause: 2.1.11 semver: 7.7.2 @@ -6205,7 +6238,7 @@ snapshots: dependencies: '@ethereumjs/tx': 4.2.0 '@types/debug': 4.1.12 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) semver: 7.7.2 superstruct: 1.0.4 transitivePeerDependencies: @@ -6218,7 +6251,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@types/debug': 4.1.12 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) pony-cause: 2.1.11 semver: 7.7.2 uuid: 9.0.1 @@ -6232,7 +6265,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@types/debug': 4.1.12 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) pony-cause: 2.1.11 semver: 7.7.2 uuid: 9.0.1 @@ -7262,7 +7295,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.9.2) - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.1 optionalDependencies: typescript: 5.9.2 @@ -7280,7 +7313,7 @@ snapshots: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 @@ -7984,6 +8017,11 @@ snapshots: typescript: 5.9.2 zod: 3.22.4 + abitype@1.0.9(typescript@5.9.2)(zod@3.22.4): + optionalDependencies: + typescript: 5.9.2 + zod: 3.22.4 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -7998,7 +8036,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -8992,7 +9030,7 @@ snapshots: eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) @@ -9014,7 +9052,7 @@ snapshots: eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.1 get-tsconfig: 4.10.1 is-bun-module: 2.0.0 @@ -9022,7 +9060,7 @@ snapshots: tinyglobby: 0.2.14 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -9037,7 +9075,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -9131,7 +9169,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -9435,7 +9473,7 @@ snapshots: follow-redirects@1.15.11(debug@4.4.1): optionalDependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) for-each@0.3.5: dependencies: @@ -9664,7 +9702,7 @@ snapshots: boxen: 5.1.2 chokidar: 4.0.3 ci-info: 2.0.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) enquirer: 2.4.1 env-paths: 2.2.1 ethereum-cryptography: 1.2.0 @@ -9761,7 +9799,7 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -10586,7 +10624,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.0.8(typescript@5.9.2)(zod@3.22.4) + abitype: 1.0.9(typescript@5.9.2)(zod@3.22.4) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.2 @@ -10600,7 +10638,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.0.8(typescript@5.9.2)(zod@3.22.4) + abitype: 1.0.9(typescript@5.9.2)(zod@3.22.4) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.2 @@ -10615,7 +10653,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.0.8(typescript@5.9.2)(zod@3.22.4) + abitype: 1.0.9(typescript@5.9.2)(zod@3.22.4) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.2 @@ -11524,7 +11562,7 @@ snapshots: cac: 6.7.14 chokidar: 4.0.3 consola: 3.4.2 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.1(supports-color@8.1.1) esbuild: 0.25.9 fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 From 7d7973b557196360ef9eb242d7c160448ea90d4b Mon Sep 17 00:00:00 2001 From: Jimmy Chu <898091+jimmychu0807@users.noreply.github.com> Date: Sat, 30 Aug 2025 14:31:36 +0800 Subject: [PATCH 3/3] completed on listening onchain events and wait for FN event --- packages/contracts/src/ERC20Token.sol | 16 + packages/web/src/abi/ERC20.json | 341 ++++++++++++++++++ .../src/app/api/verify-successful/route.ts | 72 +++- .../src/components/VerificationComponent.tsx | 12 +- packages/web/src/config.ts | 3 + 5 files changed, 436 insertions(+), 8 deletions(-) create mode 100644 packages/contracts/src/ERC20Token.sol create mode 100644 packages/web/src/abi/ERC20.json diff --git a/packages/contracts/src/ERC20Token.sol b/packages/contracts/src/ERC20Token.sol new file mode 100644 index 0000000..9b9ba0c --- /dev/null +++ b/packages/contracts/src/ERC20Token.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {ERC20} from "solady/tokens/ERC20.sol"; + +/** + * @dev Interface of the ERC-20 standard as defined in the ERC. + */ +contract ERC20Token is ERC20 { + function name() public pure override returns (string memory) { + return "ERC20 Token"; + } + function symbol() public pure override returns (string memory) { + return "TOKEN"; + } +} diff --git a/packages/web/src/abi/ERC20.json b/packages/web/src/abi/ERC20.json new file mode 100644 index 0000000..7a25e18 --- /dev/null +++ b/packages/web/src/abi/ERC20.json @@ -0,0 +1,341 @@ +{ + "abi": [ + { + "type": "function", + "name": "DOMAIN_SEPARATOR", + "inputs": [], + "outputs": [ + { + "name": "result", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "allowance", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + }, + { + "name": "spender", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "result", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "approve", + "inputs": [ + { + "name": "spender", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "balanceOf", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "result", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "decimals", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint8", + "internalType": "uint8" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "nonces", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "result", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "permit", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + }, + { + "name": "spender", + "type": "address", + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "deadline", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "v", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "r", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "s", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "symbol", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "totalSupply", + "inputs": [], + "outputs": [ + { + "name": "result", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transfer", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferFrom", + "inputs": [ + { + "name": "from", + "type": "address", + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "Approval", + "inputs": [ + { + "name": "owner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "spender", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Transfer", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AllowanceOverflow", + "inputs": [] + }, + { + "type": "error", + "name": "AllowanceUnderflow", + "inputs": [] + }, + { + "type": "error", + "name": "InsufficientAllowance", + "inputs": [] + }, + { + "type": "error", + "name": "InsufficientBalance", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidPermit", + "inputs": [] + }, + { + "type": "error", + "name": "Permit2AllowanceIsFixedAtInfinity", + "inputs": [] + }, + { + "type": "error", + "name": "PermitExpired", + "inputs": [] + }, + { + "type": "error", + "name": "TotalSupplyOverflow", + "inputs": [] + } + ] +} diff --git a/packages/web/src/app/api/verify-successful/route.ts b/packages/web/src/app/api/verify-successful/route.ts index 5cbe61a..0745cba 100644 --- a/packages/web/src/app/api/verify-successful/route.ts +++ b/packages/web/src/app/api/verify-successful/route.ts @@ -1,9 +1,73 @@ import { NextRequest, NextResponse } from "next/server"; +import { + createPublicClient, + http, + getContract, + type Address, + type Abi, +} from "viem"; +import { optimism } from "viem/chains"; +import { erc20Abi } from "@/config"; export async function POST(req: NextRequest) { - console.log("Received request"); - console.log(req); + const body = await req.json(); + console.log("request:", body); - // 1. get the account address - // 2. check on OP that it has DHK token >= 1 + const { txHash, sender, nationality, times, userData } = body; + + try { + // 1. verify the event from the txHash + if (!sender) throw new Error("Sender undefined"); + if (!txHash) throw new Error("txHash undefined"); + + // TODO: verify verificationCompleted event happen on the txHash with the sender + + // 2. check on OP that it has DHK token >= 1 + const dhkTokenPublicClient = createPublicClient({ + chain: optimism, + transport: http( + `https://opt-mainnet.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_ID}`, + ), + }); + + const dhkTokenContract = getContract({ + address: (process.env.NEXT_PUBLIC_DHKTOKEN_ADDR || "") as Address, + abi: erc20Abi, + client: dhkTokenPublicClient, + }); + + const decimals = BigInt((await dhkTokenContract.read.decimals()) as number); + const balance = BigInt( + (await dhkTokenContract.read.balanceOf([sender])) as bigint, + ); + const voteThreshold = + BigInt(process.env.NEXT_PUBLIC_DHKTOKEN_THRESHOLD || "1") * decimals; + + // TODO: 3. check user has a DHK subname in https://app.namespace.ninja + const senderHasSubname = true; + + const aboveTokenThreshold = balance >= voteThreshold; + const result = aboveTokenThreshold && senderHasSubname; + + return NextResponse.json({ + status: "success", + sender, + result, + aboveTokenThreshold, + hasSubname: senderHasSubname, + }); + } catch (err) { + console.log("Error verifying proof:", err); + + return NextResponse.json( + { + status: "error", + result: false, + message: err instanceof Error ? err.message : "Unknown error", + }, + { + status: 500, + }, + ); + } } diff --git a/packages/web/src/components/VerificationComponent.tsx b/packages/web/src/components/VerificationComponent.tsx index 305c48a..c68ba26 100644 --- a/packages/web/src/components/VerificationComponent.tsx +++ b/packages/web/src/components/VerificationComponent.tsx @@ -9,7 +9,7 @@ import { type SelfApp, } from "@selfxyz/qrcode"; import { clsx } from "clsx"; -import type { Log } from "viem"; +import type { Log, Hash } from "viem"; import { useAccount, useConfig, useWatchContractEvent } from "wagmi"; import { watchContractEvent } from "wagmi/actions"; import { @@ -25,6 +25,7 @@ import { appName, selfVerificationContract } from "@/config"; import type { Address } from "viem"; type VerificationCompletedEventArgs = { + txHash: Hash; sender: Address; nationality: string; times: number; @@ -60,8 +61,11 @@ export default function VerificationComponent() { } // Extract the event arguments out - const args = (logs[logs.length - 1] as unknown as EventLog) - .args as VerificationCompletedEventArgs; + const lastLog = logs[logs.length - 1] as unknown as EventLog; + const args = { + txHash: lastLog.transactionHash, + ...lastLog.args, + }; setEvReceived(args); }, }); @@ -140,7 +144,7 @@ export default function VerificationComponent() { if (!res.ok) throw new Error("API request failed"); - console.log("result:", res.json()); + console.log("result:", await res.json()); } catch (err) { console.error("Error calling verify-successful API:", err); } diff --git a/packages/web/src/config.ts b/packages/web/src/config.ts index a321391..734d648 100644 --- a/packages/web/src/config.ts +++ b/packages/web/src/config.ts @@ -1,5 +1,6 @@ import type { Abi, Address } from "viem"; import SelfVerificationABI from "@/abi/SelfVerification.json"; +import ERC20ABI_JSON from "@/abi/ERC20.json"; export const selfVerificationContract = { address: (process.env.NEXT_PUBLIC_VERIFICATION_DEPLOYED_ADDR || @@ -7,4 +8,6 @@ export const selfVerificationContract = { abi: SelfVerificationABI.abi as Abi, }; +export const erc20Abi = ERC20ABI_JSON.abi as Abi; + export const appName = "DHK dao Identity Verification";