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() {