verify and store on chain

This commit is contained in:
Václav Pavlín 2025-09-19 15:13:41 +02:00
parent 0d357ad64a
commit 7a9e8ccd78
No known key found for this signature in database
GPG Key ID: B378FB31BB6D89A5
5 changed files with 805 additions and 451 deletions

View File

@ -0,0 +1,114 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
struct ProofVerificationParams {
bytes32 vkeyHash;
bytes proof;
bytes32[] publicInputs;
bytes committedInputs;
uint256[] committedInputCounts;
uint256 validityPeriodInSeconds;
string domain;
string scope;
bool devMode;
}
interface IZKPassportVerifier {
// Verify the proof
function verifyProof(ProofVerificationParams calldata params) external returns (bool verified, bytes32 uniqueIdentifier);
function verifyScopes(bytes32[] calldata publicInputs, string calldata domain, string calldata scope) external view returns (bool);
}
interface IZKVerifier {
function verifyProof(ProofVerificationParams calldata params) external returns (bool verified, bytes32 uniqueIdentifier);
}
/**
* @title ZKPassportVerifier
* @notice Simplified contract to store verification outputs for adult status, country, and gender
*/
contract ZKPassportVerifier {
// Structure to store user verification data
struct Verification {
bool adult; // Whether user is 18+
string country; // User's country of nationality
string gender; // User's gender
}
// Mapping from user address to their verification data
mapping(address => Verification) public verifications;
// Mapping to track used unique identifiers
mapping(bytes32 => bool) public usedUniqueIdentifiers;
// Address of the ZKVerifier contract
IZKVerifier public zkVerifier;
/**
* @notice Constructor that sets the ZKVerifier contract address
* @param _zkVerifier Address of the ZKVerifier contract
*/
constructor(address _zkVerifier) {
require(_zkVerifier != address(0), "Invalid ZKVerifier address");
zkVerifier = IZKVerifier(_zkVerifier);
}
// Event emitted when verification data is updated
event VerificationUpdated(
address indexed user,
bool adult,
string country,
string gender
);
/**
* @notice Update verification data for the sender
* @param adult Whether the sender is 18+
* @param country The sender's country of nationality
* @param gender The sender's gender
* @param params Proof verification parameters
*/
function setVerification(
bool adult,
string calldata country,
string calldata gender,
ProofVerificationParams calldata params
) external {
// Verify the proof first using the ZKVerifier contract
(bool verified, bytes32 uniqueIdentifier) = zkVerifier.verifyProof(params);
// Revert if proof is not valid
require(verified, "Proof verification failed");
// Check if this unique identifier has already been used
require(!usedUniqueIdentifiers[uniqueIdentifier], "Unique identifier already used");
// Mark this unique identifier as used
usedUniqueIdentifiers[uniqueIdentifier] = true;
// Store the verification data
verifications[msg.sender] = Verification({
adult: adult,
country: country,
gender: gender
});
emit VerificationUpdated(msg.sender, adult, country, gender);
}
/**
* @notice Get verification data for a user
* @param user The address of the user
* @return adult Whether the user is 18+
* @return country The user's country of nationality
* @return gender The user's gender
*/
function getVerification(address user)
external view
returns (bool, string memory, string memory)
{
Verification storage verification = verifications[user];
return (verification.adult, verification.country, verification.gender);
}
}

25
hardhat.config.js Normal file
View File

@ -0,0 +1,25 @@
import "@nomicfoundation/hardhat-toolbox";
import dotenv from "dotenv";
dotenv.config();
const config = {
solidity: "0.8.20",
networks: {
sepolia: {
url: "https://eth-sepolia.api.onfinality.io/public",
chainId: 11155111,
accounts: [`${process.env.PRIVATE_KEY}`]
}
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY
},
paths: {
sources: "./contracts",
tests: "./test",
cache: "./cache",
artifacts: "./artifacts"
},
};
export default config;

View File

@ -0,0 +1,92 @@
import { Button } from '@/components/ui/button';
import { Loader2, Send } from 'lucide-react';
import { useState } from 'react';
interface ContractVerificationButtonProps {
onVerify: () => Promise<string | null>;
isVerifying: boolean;
verificationType: 'adult' | 'country' | 'gender';
}
export function ContractVerificationButton({
onVerify,
isVerifying,
}: ContractVerificationButtonProps) {
const [txStatus, setTxStatus] = useState<'idle' | 'pending' | 'success' | 'error'>('idle');
const [txHash, setTxHash] = useState<string | null>(null);
const handleVerify = async () => {
setTxStatus('pending');
setTxHash(null);
try {
const hash = await onVerify();
console.log(hash)
if (hash) {
console.log("Setting TX hash")
setTxHash(hash);
setTxStatus('success');
} else {
setTxStatus('error');
}
setTimeout(() => {
setTxStatus('idle');
setTxHash(null);
}, 60000);
} catch (error) {
setTxStatus('error');
setTimeout(() => setTxStatus('idle'), 3000);
}
};
const getButtonText = () => {
if (txStatus === 'pending') return 'Recording...';
if (txStatus === 'success') return txHash ? 'Recorded on chain!' : 'Success!';
if (txStatus === 'error') return 'Error';
return `Record Verified Claims`;
};
return (
<div className="space-y-2">
<Button
onClick={handleVerify}
disabled={isVerifying || txStatus === 'pending'}
className="w-full bg-cyber-accent hover:bg-cyber-accent/80 text-black font-mono"
>
{txStatus === 'pending' ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{getButtonText()}
</>
) : txStatus === 'success' ? (
<>
<Send className="mr-2 h-4 w-4" />
{getButtonText()}
</>
) : txStatus === 'error' ? (
<>
<span className="mr-2"></span>
{getButtonText()}
</>
) : (
<>
<Send className="mr-2 h-4 w-4" />
{getButtonText()}
</>
)}
</Button>
{txStatus === 'success' && txHash && (
<div className="text-center text-xs text-cyber-neutral bg-cyber-dark/30 p-2 rounded border border-cyber-muted/30 font-mono">
TX: <a
href={`https://sepolia.etherscan.io/tx/${txHash}`}
target="_blank"
rel="noopener noreferrer"
className="text-cyber-accent hover:underline"
>
{txHash.slice(0, 20)}...{txHash.slice(-18)}
</a>
</div>
)}
</div>
);
}

View File

@ -1,63 +1,222 @@
import { BrowserProvider, Contract } from 'ethers'; import { BrowserProvider, Contract } from 'ethers';
import { config } from '@/lib/wallet/config'; import { config } from '@/lib/wallet/config';
// Contract configuration - these should be moved to environment variables in production // Contract configuration - these should be moved to environment variables in production
const CONTRACT_ADDRESS = "0x971B0B5de23C63160602a3fbe68e96166Fc11D1A"; // Hardhat default deploy address export const CONTRACT_ADDRESS = "0xaA649E71A6d7347742e3642AAe209d580913f021"; // Hardhat default deploy address
const CONTRACT_ABI = [ const CONTRACT_ABI = [
{ {
"inputs": [ "inputs": [
{ {
"internalType": "bool", "internalType": "address",
"name": "adult", "name": "_zkVerifier",
"type": "bool" "type": "address"
}, }
{ ],
"internalType": "string", "stateMutability": "nonpayable",
"name": "country", "type": "constructor"
"type": "string" },
}, {
{ "anonymous": false,
"internalType": "string", "inputs": [
"name": "gender", {
"type": "string" "indexed": true,
} "internalType": "address",
], "name": "user",
"name": "setVerification", "type": "address"
"outputs": [], },
"stateMutability": "nonpayable", {
"type": "function" "indexed": false,
}, "internalType": "bool",
{ "name": "adult",
"inputs": [ "type": "bool"
{ },
"internalType": "address", {
"name": "user", "indexed": false,
"type": "address" "internalType": "string",
} "name": "country",
], "type": "string"
"name": "getVerification", },
"outputs": [ {
{ "indexed": false,
"internalType": "bool", "internalType": "string",
"name": "", "name": "gender",
"type": "bool" "type": "string"
}, }
{ ],
"internalType": "string", "name": "VerificationUpdated",
"name": "", "type": "event"
"type": "string" },
}, {
{ "inputs": [
"internalType": "string", {
"name": "", "internalType": "address",
"type": "string" "name": "user",
} "type": "address"
], }
"stateMutability": "view", ],
"type": "function" "name": "getVerification",
} "outputs": [
]; {
import { EU_COUNTRIES, ZKPassport } from '@zkpassport/sdk'; "internalType": "bool",
"name": "",
"type": "bool"
},
{
"internalType": "string",
"name": "",
"type": "string"
},
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bool",
"name": "adult",
"type": "bool"
},
{
"internalType": "string",
"name": "country",
"type": "string"
},
{
"internalType": "string",
"name": "gender",
"type": "string"
},
{
"components": [
{
"internalType": "bytes32",
"name": "vkeyHash",
"type": "bytes32"
},
{
"internalType": "bytes",
"name": "proof",
"type": "bytes"
},
{
"internalType": "bytes32[]",
"name": "publicInputs",
"type": "bytes32[]"
},
{
"internalType": "bytes",
"name": "committedInputs",
"type": "bytes"
},
{
"internalType": "uint256[]",
"name": "committedInputCounts",
"type": "uint256[]"
},
{
"internalType": "uint256",
"name": "validityPeriodInSeconds",
"type": "uint256"
},
{
"internalType": "string",
"name": "domain",
"type": "string"
},
{
"internalType": "string",
"name": "scope",
"type": "string"
},
{
"internalType": "bool",
"name": "devMode",
"type": "bool"
}
],
"internalType": "struct ProofVerificationParams",
"name": "params",
"type": "tuple"
}
],
"name": "setVerification",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"name": "usedUniqueIdentifiers",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "verifications",
"outputs": [
{
"internalType": "bool",
"name": "adult",
"type": "bool"
},
{
"internalType": "string",
"name": "country",
"type": "string"
},
{
"internalType": "string",
"name": "gender",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "zkVerifier",
"outputs": [
{
"internalType": "contract IZKVerifier",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
}
];
const devMode = true;
const proofMode = "compressed-evm"; //"fast"
import { EU_COUNTRIES, ProofResult, SolidityVerifierParameters, ZKPassport } from '@zkpassport/sdk';
import { Claim } from '@/types/identity'; import { Claim } from '@/types/identity';
import { V } from 'vitest/dist/chunks/environment.d.cL3nLXbE.js';
export interface ZKPassportVerificationResult { export interface ZKPassportVerificationResult {
verified: boolean; verified: boolean;
@ -66,112 +225,55 @@ export interface ZKPassportVerificationResult {
} }
/** /**
* Verify that the user is an adult (18+ years old) * Options for ZKPassport verification
*/ */
export const verifyAdulthood = async (setProgress: (status:string) => void, setUrl: (url:string) => void): Promise<ZKPassportVerificationResult | null> => { export interface ZKPassportVerificationOptions {
const zkPassport = new ZKPassport(); verifyAdulthood: boolean;
verifyCountry: boolean;
const queryBuilder = await zkPassport.request({ verifyGender: boolean;
name: "OpChan",
logo: "https://zkpassport.id/logo.png",
purpose: "Prove you are 18+ years old",
scope: "adult",
});
const {
url,
onResult,
onGeneratingProof,
onError,
onProofGenerated,
onReject,
onRequestReceived
} = queryBuilder.gte("age", 18).done();
setUrl(url);
return new Promise((resolve, reject) => {
try {
console.log("Starting adulthood verification with zkPassport");
onRequestReceived(() => {
setProgress("Request received, preparing for age verification");
console.log("Request received, preparing for age verification");
});
onGeneratingProof(() => {
setProgress("Generating cryptographic proof of age");
console.log("Generating cryptographic proof of age");
});
onProofGenerated(() => {
setProgress("Age proof generated successfully");
console.log("Age proof generated successfully");
});
onReject(() => {
setProgress("Age verification request was rejected");
console.log("Age verification request was rejected by the user");
resolve(null);
});
onError((error) => {
setProgress(`Age verification error: ${error}`);
console.error("Age verification error", error);
resolve(null);
});
onResult(({ verified, uniqueIdentifier, result }) => {
try {
console.log("Adulthood verification result", verified, result);
if (verified) {
const claims: Claim[] = [
{
key: "adult",
value: result.age?.gte?.result,
verified: true
}
];
resolve({
verified: true,
uniqueIdentifier: uniqueIdentifier || '',
claims
});
console.log("User is verified as adult", claims);
} else {
setProgress("Age verification failed");
resolve(null);
}
} catch (error) {
console.error("Adulthood verification result processing error", error);
setProgress(`Adulthood verification result processing error: ${error}`);
resolve(null);
} finally {
setUrl('');
setProgress('');
}
});
} catch (error) {
console.error("Adulthood verification exception", error);
setProgress(`Adulthood verification exception: ${error}`);
reject(error);
}
});
} }
/** /**
* Disclose the user's country of nationality * Perform ZKPassport verification with selective disclosure based on provided options
*/ */
export const discloseCountry = async (setProgress: (status:string) => void, setUrl: (url:string) => void): Promise<ZKPassportVerificationResult | null> => { export const verifyWithZKPassport = async (
options: ZKPassportVerificationOptions,
setProgress: (status: string) => void,
setUrl: (url: string) => void,
setProof: (proof: ProofResult) => void,
): Promise<ZKPassportVerificationResult | null> => {
const zkPassport = new ZKPassport(); const zkPassport = new ZKPassport();
// Build purpose message based on selected verifications
const selectedVerifications = [];
if (options.verifyAdulthood) selectedVerifications.push("being 18+");
if (options.verifyCountry) selectedVerifications.push("your country of nationality");
if (options.verifyGender) selectedVerifications.push("your gender");
const purpose = selectedVerifications.length > 0
? `Verify ${selectedVerifications.join(", ")}`
: "Verify your identity";
const queryBuilder = await zkPassport.request({ const queryBuilder = await zkPassport.request({
name: "OpChan", name: "OpChan",
logo: "https://zkpassport.id/logo.png", logo: "https://zkpassport.id/logo.png",
purpose: "Verify your country of nationality", purpose,
scope: "country", scope: "identity",
devMode: devMode,
mode: proofMode,
}); });
// Conditionally add verification requirements based on options
if (options.verifyAdulthood) {
queryBuilder.gte("age", 18);
}
if (options.verifyCountry) {
queryBuilder.disclose("nationality");
}
if (options.verifyGender) {
queryBuilder.disclose("gender");
}
const { const {
url, url,
onResult, onResult,
@ -180,159 +282,84 @@ export const discloseCountry = async (setProgress: (status:string) => void, setU
onProofGenerated, onProofGenerated,
onReject, onReject,
onRequestReceived onRequestReceived
} = queryBuilder.disclose("nationality").done(); } = queryBuilder.done();
setUrl(url); setUrl(url);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
console.log("Starting country disclosure with zkPassport"); console.log("Starting ZKPassport verification with options:", options);
onRequestReceived(() => { onRequestReceived(() => {
setProgress("Request received, preparing for country disclosure"); setProgress("Request received, preparing for verification");
console.log("Request received, preparing for country disclosure"); console.log("Request received, preparing for verification");
}); });
onGeneratingProof(() => { onGeneratingProof(() => {
setProgress("Generating cryptographic proof of country"); setProgress("Generating cryptographic proof");
console.log("Generating cryptographic proof of country"); console.log("Generating cryptographic proof");
}); });
onProofGenerated(() => { onProofGenerated((proof: ProofResult) => {
setProgress("Country proof generated successfully"); setProgress("Proof generated successfully");
console.log("Country proof generated successfully"); console.log("Proof generated successfully");
setProof(proof);
}); });
onReject(() => { onReject(() => {
setProgress("Country disclosure request was rejected"); setProgress("Verification request was rejected");
console.log("Country disclosure request was rejected by the user"); console.log("Verification request was rejected by the user");
resolve(null); resolve(null);
}); });
onError((error) => { onError((error) => {
setProgress(`Country disclosure error: ${error}`); setProgress(`Verification error: ${error}`);
console.error("Country disclosure error", error); console.error("Verification error", error);
resolve(null); resolve(null);
}); });
onResult(({ verified, uniqueIdentifier, result }) => { onResult(({ verified, uniqueIdentifier, result }) => {
try { try {
console.log("Country disclosure result", verified, result); console.log("ZKPassport verification result", verified, result);
if (verified && result.nationality?.disclose?.result) { if (verified) {
const claims: Claim[] = [ const claims: Claim[] = [];
{
if (options.verifyAdulthood && result.age?.gte?.result) {
claims.push({
key: "adult",
value: result.age.gte.result,
verified: true
});
}
if (options.verifyCountry && result.nationality?.disclose?.result) {
claims.push({
key: "country", key: "country",
value: result.nationality.disclose.result, value: result.nationality.disclose.result,
verified: true verified: true
} });
]; }
resolve({ if (options.verifyGender && result.gender?.disclose?.result) {
verified: true, claims.push({
uniqueIdentifier: uniqueIdentifier || '',
claims
});
console.log("User country disclosed", claims);
} else {
setProgress("Country disclosure failed");
resolve(null);
}
} catch (error) {
console.error("Country disclosure result processing error", error);
setProgress(`Country disclosure result processing error: ${error}`);
resolve(null);
} finally {
setUrl('');
setProgress('');
}
});
} catch (error) {
console.error("Country disclosure exception", error);
setProgress(`Country disclosure exception: ${error}`);
reject(error);
}
});
}
/**
* Disclose the user's gender
*/
export const discloseGender = async (setProgress: (status:string) => void, setUrl: (url:string) => void): Promise<ZKPassportVerificationResult | null> => {
const zkPassport = new ZKPassport();
const queryBuilder = await zkPassport.request({
name: "OpChan",
logo: "https://zkpassport.id/logo.png",
purpose: "Verify your gender",
scope: "gender",
});
const {
url,
onResult,
onGeneratingProof,
onError,
onProofGenerated,
onReject,
onRequestReceived
} = queryBuilder.disclose("gender").done();
setUrl(url);
return new Promise((resolve, reject) => {
try {
console.log("Starting gender disclosure with zkPassport");
onRequestReceived(() => {
setProgress("Request received, preparing for gender disclosure");
console.log("Request received, preparing for gender disclosure");
});
onGeneratingProof(() => {
setProgress("Generating cryptographic proof of gender");
console.log("Generating cryptographic proof of gender");
});
onProofGenerated(() => {
setProgress("Gender proof generated successfully");
console.log("Gender proof generated successfully");
});
onReject(() => {
setProgress("Gender disclosure request was rejected");
console.log("Gender disclosure request was rejected by the user");
resolve(null);
});
onError((error) => {
setProgress(`Gender disclosure error: ${error}`);
console.error("Gender disclosure error", error);
resolve(null);
});
onResult(({ verified, uniqueIdentifier, result }) => {
try {
console.log("Gender disclosure result", verified, result);
if (verified && result.gender?.disclose?.result) {
const claims: Claim[] = [
{
key: "gender", key: "gender",
value: result.gender.disclose.result, value: result.gender.disclose.result,
verified: true verified: true
} });
]; }
resolve({ resolve({
verified: true, verified: true,
uniqueIdentifier: uniqueIdentifier || '', uniqueIdentifier: uniqueIdentifier || '',
claims claims
}); });
console.log("User gender disclosed", claims); console.log("User verified with claims", claims);
} else { } else {
setProgress("Gender disclosure failed"); setProgress("Verification failed");
resolve(null); resolve(null);
} }
} catch (error) { } catch (error) {
console.error("Gender disclosure result processing error", error); console.error("Verification result processing error", error);
setProgress(`Gender disclosure result processing error: ${error}`); setProgress(`Verification result processing error: ${error}`);
resolve(null); resolve(null);
} finally { } finally {
setUrl(''); setUrl('');
@ -340,8 +367,8 @@ export const discloseGender = async (setProgress: (status:string) => void, setUr
} }
}); });
} catch (error) { } catch (error) {
console.error("Gender disclosure exception", error); console.error("ZKPassport verification exception", error);
setProgress(`Gender disclosure exception: ${error}`); setProgress(`ZKPassport verification exception: ${error}`);
reject(error); reject(error);
} }
}); });
@ -392,7 +419,7 @@ const getSigner = async (): Promise<any | null> => {
}; };
/** /**
* Submit verification data to the blockchain contract * Record verified claims on the blockchain
* @param adult Whether the user is 18+ * @param adult Whether the user is 18+
* @param country The user's country of nationality * @param country The user's country of nationality
* @param gender The user's gender * @param gender The user's gender
@ -403,9 +430,22 @@ export const submitVerificationToContract = async (
adult: boolean, adult: boolean,
country: string, country: string,
gender: string, gender: string,
proof: ProofResult,
setProgress: (status: string) => void setProgress: (status: string) => void
): Promise<string | null> => { ): Promise<string | null> => {
setProgress('Initializing blockchain connection...'); setProgress('Initializing blockchain connection...');
const zkPassport = new ZKPassport();
// Get verification parameters
const verifierParams = zkPassport.getSolidityVerifierParameters({
proof: proof,
// Use the same scope as the one you specified with the request function
scope: "identity",
// Enable dev mode if you want to use mock passports, otherwise keep it false
devMode: true,
});
try { try {
const signer = await getSigner(); const signer = await getSigner();
@ -420,11 +460,11 @@ export const submitVerificationToContract = async (
return null; return null;
} }
const contract = new Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signer) as unknown as { const contract = new Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signer) as unknown as {
setVerification: (adult: boolean, country: string, gender: string) => Promise<any>; setVerification: (adult: boolean, country: string, gender: string, verifierParams: SolidityVerifierParameters) => Promise<any>;
}; };
setProgress('Submitting verification data to blockchain...'); setProgress('Submitting verification data to blockchain...');
const tx = await contract.setVerification(adult, country, gender); const tx = await contract.setVerification(adult, country, gender, verifierParams);
setProgress('Waiting for blockchain confirmation...'); setProgress('Waiting for blockchain confirmation...');
const receipt = await tx.wait(); const receipt = await tx.wait();
@ -445,4 +485,24 @@ export const submitVerificationToContract = async (
} }
return null; return null;
} }
}
/**
* Fetch verification data for a user from the ZKPassport verifier contract
* @param address The wallet address of the user to fetch verification data for
* @returns Promise resolving to an object containing adult status, country, and gender, or null if not found
*/
export const getVerification = async (address: string): Promise<{ adult: boolean; country: string; gender: string } | null> => {
try {
const provider = new BrowserProvider(window.ethereum as any);
const contract = new Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider) as unknown as {
getVerification: (address: string) => Promise<[boolean, string, string]>;
};
const [adult, country, gender] = await contract.getVerification(address);
return { adult, country, gender };
} catch (error) {
console.error('Error fetching verification data:', error);
return null;
}
}; };

View File

@ -8,8 +8,8 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { ContractVerificationButton } from '@/components/ui/contract-verification-button'; import { ContractVerificationButton } from '@/components/ui/contract-verification-button';
import { submitVerificationToContract } from '@/lib/zkPassport'; import { CONTRACT_ADDRESS, getVerification, submitVerificationToContract } from '@/lib/zkPassport';
import { verifyAdulthood, discloseCountry, discloseGender } from '@/lib/zkPassport'; import { verifyWithZKPassport, ZKPassportVerificationOptions } from '@/lib/zkPassport';
import { UserIdentityService } from '@/lib/services/UserIdentityService'; import { UserIdentityService } from '@/lib/services/UserIdentityService';
import { useForum } from '@/contexts/useForum'; import { useForum } from '@/contexts/useForum';
import { QRCodeCanvas } from 'qrcode.react'; import { QRCodeCanvas } from 'qrcode.react';
@ -40,6 +40,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { EDisplayPreference, EVerificationStatus } from '@/types/identity'; import { EDisplayPreference, EVerificationStatus } from '@/types/identity';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { ProofResult } from '@zkpassport/sdk';
export default function ProfilePage() { export default function ProfilePage() {
const { updateProfile } = useUserActions(); const { updateProfile } = useUserActions();
@ -73,8 +74,49 @@ export default function ProfilePage() {
const [progress, setProgress] = useState<string>(''); const [progress, setProgress] = useState<string>('');
const [isVerifying, setIsVerifying] = useState<boolean>(false); const [isVerifying, setIsVerifying] = useState<boolean>(false);
const [verificationType, setVerificationType] = useState<'adult' | 'country' | 'gender' | null>(null); const [verificationType, setVerificationType] = useState<'adult' | 'country' | 'gender' | null>(null);
const [proof, setProof] = useState<ProofResult | null>(null);
const [verificationOptions, setVerificationOptions] = useState<ZKPassportVerificationOptions>({
verifyAdulthood: false,
verifyCountry: false,
verifyGender: false
});
const { userIdentityService } = useForum(); const { userIdentityService } = useForum();
// Load verification data from contract on component mount
useEffect(() => {
const loadVerificationData = async () => {
if (address) {
const verificationData = await getVerification(address);
if (verificationData && userIdentityService) {
// Update user identity with data from contract
if (verificationData.adult) {
userIdentityService.updateUserIdentityWithAdulthood(
address,
'', // uniqueIdentifier not available from contract
verificationData.adult
);
}
if (verificationData.country) {
userIdentityService.updateUserIdentityWithCountry(
address,
'', // uniqueIdentifier not available from contract
verificationData.country
);
}
if (verificationData.gender) {
userIdentityService.updateUserIdentityWithGender(
address,
'', // uniqueIdentifier not available from contract
verificationData.gender
);
}
}
}
};
loadVerificationData();
}, [address, userIdentityService]);
// Initialize and update local state when user data changes // Initialize and update local state when user data changes
useEffect(() => { useEffect(() => {
if (currentUser) { if (currentUser) {
@ -599,190 +641,211 @@ export default function ProfilePage() {
</main> </main>
{/* Identity Verification Section */} {/* Identity Verification Section */}
<div className="max-w-md mx-auto mt-8 p-6 bg-cyber-muted/20 border border-cyber-muted/30 rounded-lg"> <div className="max-w-4xl mx-auto mt-8 mb-8">
<h2 className="text-xl font-bold text-white mb-4">Identity Verification</h2> <Card className="content-card">
<p className="text-cyber-neutral mb-4"> <CardHeader>
Verify your identity to enhance your profile with verifiable claims. <CardTitle className="flex items-center justify-between text-white">
</p> <div className="flex items-center gap-2">
<Shield className="h-5 w-5 text-cyber-accent" />
Identity Verification
</div>
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Left Column: Verification Status */}
<div className="space-y-6">
<p className="text-cyber-neutral">
Verify your identity to enhance your profile with verifiable claims.
</p>
{/* Verification Buttons */} {/* Verification Status */}
<div className="space-y-3 mb-6"> {userInfo.identityProviders && userInfo.identityProviders.some(p => p.type === 'zkpassport') && (
<div className="space-y-2"> <div className="space-y-3">
<Button <h3 className="text-sm font-medium text-cyber-neutral uppercase tracking-wide">
onClick={async () => { Verified (and <a className="text-cyber-accent hover:underline" href={`https://sepolia.etherscan.io/address/${CONTRACT_ADDRESS}`} target='_blank'>recorded</a>) Claims
setVerificationType('adult'); </h3>
setIsVerifying(true); <div className="space-y-2">
try { {userInfo.identityProviders.flatMap(p => p.claims).map((claim, index) => (
const result = await verifyAdulthood(setProgress, setUrl); <div key={index} className="flex items-center justify-between p-3 bg-cyber-dark/50 border border-cyber-muted/30 rounded-md">
if (result && result.claims && result.claims.length > 0 && userIdentityService) { <div className="flex items-center gap-2">
if (result.uniqueIdentifier && result.claims[0]?.value !== undefined) { <CheckCircle className="h-4 w-4 text-green-500" />
userIdentityService.updateUserIdentityWithAdulthood( <span className="text-sm text-cyber-light capitalize">
address!, {claim.key}
result.uniqueIdentifier, </span>
result.claims[0].value </div>
); <span className="text-sm text-cyber-accent font-mono">
} {typeof claim.value === 'boolean' ? (claim.value ? 'Yes' : 'No') : claim.value}
} </span>
} catch (error) { </div>
console.error('Adulthood verification failed:', error); ))}
} finally { </div>
setIsVerifying(false);
setVerificationType(null);
}
}}
disabled={isVerifying}
className="w-full bg-cyber-accent hover:bg-cyber-accent/80 text-black font-mono"
>
{isVerifying && verificationType === 'adult' ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Verifying...
</>
) : (
'Verify Adulthood (18+)'
)}
</Button>
{userInfo.identityProviders && userInfo.identityProviders.some(p => p.type === 'zkpassport') && (
<ContractVerificationButton
onVerify={async () => {
const adulthoodClaim = userInfo.identityProviders?.flatMap(p => p.claims).find(c => c.key === 'adult');
const countryClaim = userInfo.identityProviders?.flatMap(p => p.claims).find(c => c.key === 'country');
const genderClaim = userInfo.identityProviders?.flatMap(p => p.claims).find(c => c.key === 'gender');
if (adulthoodClaim) {
await submitVerificationToContract(
adulthoodClaim.value as boolean,
countryClaim?.value as string || '',
genderClaim?.value as string || '',
setProgress
);
}
}}
isVerifying={isVerifying}
verificationType="adult"
/>
)}
</div>
<Button
onClick={async () => {
setVerificationType('country');
setIsVerifying(true);
try {
const result = await discloseCountry(setProgress, setUrl);
if (result && result.claims && result.claims.length > 0 && userIdentityService) {
if (result.uniqueIdentifier && result.claims[0]?.value !== undefined) {
userIdentityService.updateUserIdentityWithCountry(
address!,
result.uniqueIdentifier,
result.claims[0].value
);
}
}
} catch (error) {
console.error('Country disclosure failed:', error);
} finally {
setIsVerifying(false);
setVerificationType(null);
}
}}
disabled={isVerifying}
className="w-full bg-cyber-accent hover:bg-cyber-accent/80 text-black font-mono"
>
{isVerifying && verificationType === 'country' ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Verifying...
</>
) : (
'Disclose Country'
)}
</Button>
<Button
onClick={async () => {
setVerificationType('gender');
setIsVerifying(true);
try {
const result = await discloseGender(setProgress, setUrl);
if (result && result.claims && result.claims.length > 0 && userIdentityService) {
if (result.uniqueIdentifier && result.claims[0]?.value !== undefined) {
userIdentityService.updateUserIdentityWithGender(
address!,
result.uniqueIdentifier,
result.claims[0].value
);
}
}
} catch (error) {
console.error('Gender disclosure failed:', error);
} finally {
setIsVerifying(false);
setVerificationType(null);
}
}}
disabled={isVerifying}
className="w-full bg-cyber-accent hover:bg-cyber-accent/80 text-black font-mono"
>
{isVerifying && verificationType === 'gender' ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Verifying...
</>
) : (
'Disclose Gender'
)}
</Button>
</div>
{/* Verification Status */}
{userInfo.identityProviders && userInfo.identityProviders.some(p => p.type === 'zkpassport') && (
<div className="space-y-3 mb-6">
<h3 className="text-sm font-medium text-cyber-neutral uppercase tracking-wide">
Verified Claims
</h3>
<div className="space-y-2">
{userInfo.identityProviders.flatMap(p => p.claims).map((claim, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-cyber-dark/50 border border-cyber-muted/30 rounded-md">
<div className="flex items-center gap-2">
<CheckCircle className="h-4 w-4 text-green-500" />
<span className="text-sm text-cyber-light capitalize">
{claim.key}
</span>
</div> </div>
<span className="text-sm text-cyber-accent font-mono"> )}
{typeof claim.value === 'boolean' ? (claim.value ? 'Yes' : 'No') : claim.value} </div>
</span>
</div>
))}
</div>
</div>
)}
{/* Progress and QR Code */} {/* Right Column: Verification Controls */}
{progress && ( <div className="space-y-6">
<p className="mt-4 text-sm text-cyber-neutral">{progress}</p> {/* Verification Toggles */}
)} <div className="space-y-4">
{url && ( <div className="flex items-center space-x-2">
<div className="mt-4 space-y-4"> <input
<div className="text-center"> type="checkbox"
<a id="verifyAdulthood"
href={url} checked={verificationOptions.verifyAdulthood}
target="_blank" onChange={(e) => setVerificationOptions({...verificationOptions, verifyAdulthood: e.target.checked})}
rel="noopener noreferrer" disabled={isVerifying}
className="text-cyber-accent hover:underline font-medium" className="w-4 h-4 text-cyber-accent bg-cyber-dark border-cyber-muted/30 rounded focus:ring-cyber-accent focus:ring-2"
> />
Open verification in new tab <label htmlFor="verifyAdulthood" className="text-sm text-cyber-neutral">
</a> Verify Adulthood (18+)
</label>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="verifyCountry"
checked={verificationOptions.verifyCountry}
onChange={(e) => setVerificationOptions({...verificationOptions, verifyCountry: e.target.checked})}
disabled={isVerifying}
className="w-4 h-4 text-cyber-accent bg-cyber-dark border-cyber-muted/30 rounded focus:ring-cyber-accent focus:ring-2"
/>
<label htmlFor="verifyCountry" className="text-sm text-cyber-neutral">
Disclose Country
</label>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="verifyGender"
checked={verificationOptions.verifyGender}
onChange={(e) => setVerificationOptions({...verificationOptions, verifyGender: e.target.checked})}
disabled={isVerifying}
className="w-4 h-4 text-cyber-accent bg-cyber-dark border-cyber-muted/30 rounded focus:ring-cyber-accent focus:ring-2"
/>
<label htmlFor="verifyGender" className="text-sm text-cyber-neutral">
Disclose Gender
</label>
</div>
</div>
{/* Verification Button */}
<div className="space-y-3">
<Button
onClick={async () => {
// Set verification type based on selected options
if (verificationOptions.verifyAdulthood) setVerificationType('adult');
else if (verificationOptions.verifyCountry) setVerificationType('country');
else if (verificationOptions.verifyGender) setVerificationType('gender');
setIsVerifying(true);
try {
const result = await verifyWithZKPassport(verificationOptions, setProgress, setUrl, setProof);
if (result && result.claims && result.claims.length > 0 && userIdentityService) {
// Update all verified claims
result.claims.forEach(claim => {
if (claim.key === 'adult' && claim.value !== undefined) {
userIdentityService.updateUserIdentityWithAdulthood(
address!,
result.uniqueIdentifier,
claim.value
);
} else if (claim.key === 'country' && claim.value !== undefined) {
userIdentityService.updateUserIdentityWithCountry(
address!,
result.uniqueIdentifier,
claim.value
);
} else if (claim.key === 'gender' && claim.value !== undefined) {
userIdentityService.updateUserIdentityWithGender(
address!,
result.uniqueIdentifier,
claim.value
);
}
});
}
} catch (error) {
console.error('Verification failed:', error);
} finally {
setIsVerifying(false);
setVerificationType(null);
}
}}
disabled={isVerifying || (!verificationOptions.verifyAdulthood && !verificationOptions.verifyCountry && !verificationOptions.verifyGender)}
className="w-full bg-cyber-accent hover:bg-cyber-accent/80 text-black font-mono"
>
{isVerifying ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Verifying...
</>
) : (
'Verify Selected Claims'
)}
</Button>
{/* Contract Verification Button - only show if any claims exist */}
{userInfo.identityProviders && proof && userInfo.identityProviders.some(p => p.type === 'zkpassport') && (
<ContractVerificationButton
onVerify={async () => {
const adulthoodClaim = userInfo.identityProviders?.flatMap(p => p.claims).find(c => c.key === 'adult');
const countryClaim = userInfo.identityProviders?.flatMap(p => p.claims).find(c => c.key === 'country');
const genderClaim = userInfo.identityProviders?.flatMap(p => p.claims).find(c => c.key === 'gender');
const tx = await submitVerificationToContract(
adulthoodClaim?.value as boolean || false,
countryClaim?.value as string || '',
genderClaim?.value as string || '',
proof,
setProgress
);
if (tx) {
toast({
title: 'Verification Submitted',
description: 'Your verification has been submitted to the contract.',
});
}
return tx;
}}
isVerifying={isVerifying}
verificationType="adult"
/>
)}
</div>
{/* Progress and QR Code */}
{progress && (
<p className="mt-4 text-sm text-cyber-neutral">{progress}</p>
)}
{url && (
<div className="mt-4 space-y-4">
<div className="text-center">
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="text-cyber-accent hover:underline font-medium"
>
Open verification in new tab
</a>
</div>
<div className="flex justify-center p-4 bg-white rounded-lg shadow-lg inline-block">
<QRCodeCanvas value={url} size={200} level="H" includeMargin={true} />
</div>
<p className="text-xs text-cyber-neutral text-center">
Scan this QR code to open the verification page on your mobile device
</p>
</div>
)}
</div>
</div> </div>
<div className="flex justify-center p-4 bg-white rounded-lg shadow-lg inline-block"> </CardContent>
<QRCodeCanvas value={url} size={200} level="H" includeMargin={true} /> </Card>
</div>
<p className="text-xs text-cyber-neutral text-center">
Scan this QR code to open the verification page on your mobile device
</p>
</div>
)}
</div> </div>
<footer className="page-footer"> <footer className="page-footer">