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/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/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..33478e6 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": { @@ -17,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", @@ -33,6 +32,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/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/abi/SelfVerification.json b/packages/web/src/abi/SelfVerification.json new file mode 100644 index 0000000..1e9e1aa --- /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..0745cba --- /dev/null +++ b/packages/web/src/app/api/verify-successful/route.ts @@ -0,0 +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) { + const body = await req.json(); + console.log("request:", body); + + 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/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..af9508c 100644 --- a/packages/web/src/app/page.tsx +++ b/packages/web/src/app/page.tsx @@ -1,23 +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 } from "wagmi"; -import { - Button, - Field, - Input, - Dialog, - DialogPanel, - DialogTitle, -} from "@headlessui/react"; +import VerificationComponent from "@/components/VerificationComponent"; export default function Home() { return ( @@ -28,162 +11,3 @@ export default function Home() { ); } - -function VerificationComponent() { - const account = useAccount(); - const [selfApp, setSelfApp] = useState(); - const [universalLink, setUniversalLink] = useState(""); - const [message, setMessage] = useState(""); - const [isOpenDialog, setOpenDialog] = useState(false); - - 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: process.env.NEXT_PUBLIC_SELF_APP_NAME || "", - 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)); - setOpenDialog(true); - } catch (error) { - console.error("Failed to initialize Self app:", error); - } - }; - - const closeDialog = () => { - setOpenDialog(false); - setTimeout(() => { - setSelfApp(undefined); - setUniversalLink(""); - }, 1500); - }; - - const handleSuccessfulVerification = () => { - console.log("Verification successful!"); - closeDialog(); - // call backend function check api - }; - - 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..c68ba26 --- /dev/null +++ b/packages/web/src/components/VerificationComponent.tsx @@ -0,0 +1,247 @@ +"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, Hash } 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 = { + txHash: Hash; + 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 lastLog = logs[logs.length - 1] as unknown as EventLog; + const args = { + txHash: lastLog.transactionHash, + ...lastLog.args, + }; + 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:", await 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 d505649..50917bc 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], + chains: [foundry, 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..734d648 --- /dev/null +++ b/packages/web/src/config.ts @@ -0,0 +1,13 @@ +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 || + "0x") as Address, + abi: SelfVerificationABI.abi as Abi, +}; + +export const erc20Abi = ERC20ABI_JSON.abi as Abi; + +export const appName = "DHK dao Identity Verification"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb09908..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 @@ -123,6 +126,9 @@ importers: concurrently: specifier: 'catalog:' version: 9.2.1 + dotenv: + specifier: 'catalog:' + version: 17.2.1 eslint: specifier: ^8 version: 8.57.1 @@ -1760,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: @@ -5310,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 @@ -5342,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) @@ -5352,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 @@ -5388,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 @@ -5630,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 @@ -5987,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 @@ -6140,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 @@ -6164,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 @@ -6190,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 @@ -6202,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: @@ -6215,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 @@ -6229,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 @@ -7259,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 @@ -7277,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 @@ -7981,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 @@ -7995,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 @@ -8989,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) @@ -9011,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 @@ -9019,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 @@ -9034,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 @@ -9128,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 @@ -9432,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: @@ -9661,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 @@ -9758,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 @@ -10583,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 @@ -10597,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 @@ -10612,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 @@ -11521,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