initial prep for on-chain ZKPassport verification

This commit is contained in:
Václav Pavlín 2025-09-19 11:33:12 +02:00
parent fefe7608ad
commit 0d357ad64a
No known key found for this signature in database
GPG Key ID: B378FB31BB6D89A5
10 changed files with 5816 additions and 119 deletions

4965
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -59,12 +59,14 @@
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"dotenv": "^17.2.2",
"embla-carousel-react": "^8.3.0",
"input-otp": "^1.2.4",
"lucide-react": "^0.462.0",
"next-themes": "^0.3.0",
"ordiscan": "^1.3.0",
"re-resizable": "6.11.2",
"pino": "^9.9.4",
"qrcode.react": "^4.2.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
@ -88,6 +90,12 @@
"devDependencies": {
"@eslint/js": "^9.9.0",
"@tailwindcss/typography": "^0.5.16",
"@nomicfoundation/hardhat-chai-matchers": "^2.1.0",
"@nomicfoundation/hardhat-ethers": "^3.1.0",
"@nomicfoundation/hardhat-network-helpers": "^1.1.0",
"@nomicfoundation/hardhat-toolbox": "^6.1.0",
"@nomicfoundation/hardhat-verify": "^2.1.1",
"@tailwindcss/typography": "^0.5.15",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
@ -102,6 +110,7 @@
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
"hardhat": "^2.26.3",
"jsdom": "^26.1.0",
"postcss": "^8.4.47",
"prettier": "^3.6.2",

View File

@ -1,6 +1,6 @@
import { useState, useEffect, useMemo } from 'react';
import { useForum } from '@/contexts/useForum';
import { EDisplayPreference, EVerificationStatus } from '@/types/identity';
import { EDisplayPreference, EVerificationStatus, IdentityProvider } from '@/types/identity';
export interface UserDisplayInfo {
displayName: string;
@ -11,6 +11,7 @@ export interface UserDisplayInfo {
displayPreference: EDisplayPreference | null;
isLoading: boolean;
error: string | null;
identityProviders: IdentityProvider[] | null;
}
/**
@ -27,6 +28,7 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
displayPreference: null,
isLoading: true,
error: null,
identityProviders: null,
});
const [refreshTrigger, setRefreshTrigger] = useState(0);
@ -104,6 +106,7 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
displayPreference: identity.displayPreference || null,
isLoading: false,
error: null,
identityProviders: identity.identityProviders || null,
});
} else {
setDisplayInfo({
@ -117,6 +120,7 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
displayPreference: null,
isLoading: false,
error: null,
identityProviders: null,
});
}
} catch (error) {

View File

@ -1,4 +1,4 @@
import { EVerificationStatus, EDisplayPreference } from '@/types/identity';
import { EVerificationStatus, EDisplayPreference, IdentityProvider, Claim } from '@/types/identity';
import {
UnsignedUserProfileUpdateMessage,
UserProfileUpdateMessage,
@ -21,6 +21,7 @@ export interface UserIdentity {
displayPreference: EDisplayPreference;
lastUpdated: number;
verificationStatus: EVerificationStatus;
identityProviders?: IdentityProvider[];
}
export class UserIdentityService {
@ -59,6 +60,7 @@ export class UserIdentityService {
verificationStatus: this.mapVerificationStatus(
cached.verificationStatus
),
identityProviders: cached.identityProviders
};
}
@ -191,6 +193,7 @@ export class UserIdentityService {
displayPreference: cached.displayPreference,
lastUpdated: cached.lastUpdated,
verificationStatus: this.mapVerificationStatus(cached.verificationStatus),
identityProviders: cached.identityProviders
}));
}
@ -250,6 +253,7 @@ export class UserIdentityService {
: this.userIdentityCache[address].callSign,
displayPreference,
lastUpdated: timestamp,
identityProviders: this.userIdentityCache[address].identityProviders
};
localDatabase.cache.userIdentities[address] = updatedIdentity;
@ -315,6 +319,7 @@ export class UserIdentityService {
displayPreference: defaultDisplayPreference,
lastUpdated: Date.now(),
verificationStatus,
identityProviders: []
};
} catch (error) {
console.error('Failed to resolve user identity:', error);
@ -405,6 +410,218 @@ export class UserIdentityService {
}
}
/**
* Update user identity with ZKPassport adulthood verification
*/
updateUserIdentityWithAdulthood(
address: string,
uniqueIdentifier: string,
isAdult: boolean
): void {
const timestamp = Date.now();
if (!this.userIdentityCache[address]) {
// Create new identity entry if it doesn't exist
this.userIdentityCache[address] = {
ensName: undefined,
ordinalDetails: undefined,
callSign: undefined,
displayPreference: EDisplayPreference.WALLET_ADDRESS,
lastUpdated: timestamp,
verificationStatus: EVerificationStatus.WALLET_CONNECTED,
};
}
// Create or update ZKPassport provider
if (!this.userIdentityCache[address].identityProviders) {
this.userIdentityCache[address].identityProviders = [];
}
const provider = this.userIdentityCache[address].identityProviders!.find(p => p.type === 'zkpassport');
const claims: Claim[] = provider?.claims || [];
// Update or add adulthood claim
const existingClaimIndex = claims.findIndex(c => c.key === 'adult');
const claim: Claim = {
key: 'adult',
value: isAdult,
verified: true
};
if (existingClaimIndex >= 0) {
claims[existingClaimIndex] = claim;
} else {
claims.push(claim);
}
// Create or update provider
const zkPassportProvider: IdentityProvider = {
type: 'zkpassport',
verifiedAt: timestamp,
uniqueIdentifier,
claims
};
const existingProviderIndex = this.userIdentityCache[address].identityProviders!.findIndex(
p => p.type === 'zkpassport'
);
if (existingProviderIndex >= 0) {
this.userIdentityCache[address].identityProviders![existingProviderIndex] = zkPassportProvider;
} else {
this.userIdentityCache[address].identityProviders!.push(zkPassportProvider);
}
// Update last updated timestamp
this.userIdentityCache[address].lastUpdated = timestamp;
// Update verification status if user is verified as adult
if (isAdult) {
this.userIdentityCache[address].verificationStatus = EVerificationStatus.ENS_ORDINAL_VERIFIED;
}
// Notify listeners that the user identity has been updated
this.notifyRefreshListeners(address);
}
/**
* Update user identity with ZKPassport country disclosure
*/
updateUserIdentityWithCountry(
address: string,
uniqueIdentifier: string,
country: string
): void {
const timestamp = Date.now();
if (!this.userIdentityCache[address]) {
// Create new identity entry if it doesn't exist
this.userIdentityCache[address] = {
ensName: undefined,
ordinalDetails: undefined,
callSign: undefined,
displayPreference: EDisplayPreference.WALLET_ADDRESS,
lastUpdated: timestamp,
verificationStatus: EVerificationStatus.WALLET_CONNECTED,
};
}
// Create or update ZKPassport provider
if (!this.userIdentityCache[address].identityProviders) {
this.userIdentityCache[address].identityProviders = [];
}
const provider = this.userIdentityCache[address].identityProviders!.find(p => p.type === 'zkpassport');
const claims: Claim[] = provider?.claims || [];
// Update or add country claim
const existingClaimIndex = claims.findIndex(c => c.key === 'country');
const claim: Claim = {
key: 'country',
value: country,
verified: true
};
if (existingClaimIndex >= 0) {
claims[existingClaimIndex] = claim;
} else {
claims.push(claim);
}
// Create or update provider
const zkPassportProvider: IdentityProvider = {
type: 'zkpassport',
verifiedAt: timestamp,
uniqueIdentifier,
claims
};
const existingProviderIndex = this.userIdentityCache[address].identityProviders!.findIndex(
p => p.type === 'zkpassport'
);
if (existingProviderIndex >= 0) {
this.userIdentityCache[address].identityProviders![existingProviderIndex] = zkPassportProvider;
} else {
this.userIdentityCache[address].identityProviders!.push(zkPassportProvider);
}
// Update last updated timestamp
this.userIdentityCache[address].lastUpdated = timestamp;
// Notify listeners that the user identity has been updated
this.notifyRefreshListeners(address);
}
/**
* Update user identity with ZKPassport gender disclosure
*/
updateUserIdentityWithGender(
address: string,
uniqueIdentifier: string,
gender: string
): void {
const timestamp = Date.now();
if (!this.userIdentityCache[address]) {
// Create new identity entry if it doesn't exist
this.userIdentityCache[address] = {
ensName: undefined,
ordinalDetails: undefined,
callSign: undefined,
displayPreference: EDisplayPreference.WALLET_ADDRESS,
lastUpdated: timestamp,
verificationStatus: EVerificationStatus.WALLET_CONNECTED,
};
}
// Create or update ZKPassport provider
if (!this.userIdentityCache[address].identityProviders) {
this.userIdentityCache[address].identityProviders = [];
}
const provider = this.userIdentityCache[address].identityProviders!.find(p => p.type === 'zkpassport');
const claims: Claim[] = provider?.claims || [];
// Update or add gender claim
const existingClaimIndex = claims.findIndex(c => c.key === 'gender');
const claim: Claim = {
key: 'gender',
value: gender,
verified: true
};
if (existingClaimIndex >= 0) {
claims[existingClaimIndex] = claim;
} else {
claims.push(claim);
}
// Create or update provider
const zkPassportProvider: IdentityProvider = {
type: 'zkpassport',
verifiedAt: timestamp,
uniqueIdentifier,
claims
};
const existingProviderIndex = this.userIdentityCache[address].identityProviders!.findIndex(
p => p.type === 'zkpassport'
);
if (existingProviderIndex >= 0) {
this.userIdentityCache[address].identityProviders![existingProviderIndex] = zkPassportProvider;
} else {
this.userIdentityCache[address].identityProviders!.push(zkPassportProvider);
}
// Update last updated timestamp
this.userIdentityCache[address].lastUpdated = timestamp;
// Notify listeners that the user identity has been updated
this.notifyRefreshListeners(address);
}
/**
* Map verification status string to enum
*/
@ -435,8 +652,19 @@ export class UserIdentityService {
* Refresh user identity (force re-resolution)
*/
async refreshUserIdentity(address: string): Promise<void> {
// Preserve identity providers when refreshing
const preservedProviders = this.userIdentityCache[address]?.identityProviders;
delete this.userIdentityCache[address];
// Get fresh identity
await this.getUserIdentity(address);
// Restore identity providers if they existed
if (preservedProviders && this.userIdentityCache[address]) {
this.userIdentityCache[address].identityProviders = preservedProviders;
this.userIdentityCache[address].lastUpdated = Date.now();
}
}
/**

View File

@ -2,9 +2,9 @@ import { AppKitOptions } from '@reown/appkit';
import { BitcoinAdapter } from '@reown/appkit-adapter-bitcoin';
import { WagmiAdapter } from '@reown/appkit-adapter-wagmi';
import { createStorage } from 'wagmi';
import { mainnet, bitcoin, AppKitNetwork } from '@reown/appkit/networks';
import { mainnet, sepolia, bitcoin, AppKitNetwork } from '@reown/appkit/networks';
const networks: [AppKitNetwork, ...AppKitNetwork[]] = [mainnet, bitcoin];
const networks: [AppKitNetwork, ...AppKitNetwork[]] = [mainnet, sepolia, bitcoin];
const projectId =
process.env.VITE_REOWN_SECRET || '2ead96ea166a03e5ab50e5c190532e72';

View File

@ -1,83 +1,448 @@
import { BrowserProvider, Contract } from 'ethers';
import { config } from '@/lib/wallet/config';
// Contract configuration - these should be moved to environment variables in production
const CONTRACT_ADDRESS = "0x971B0B5de23C63160602a3fbe68e96166Fc11D1A"; // Hardhat default deploy address
const CONTRACT_ABI = [
{
"inputs": [
{
"internalType": "bool",
"name": "adult",
"type": "bool"
},
{
"internalType": "string",
"name": "country",
"type": "string"
},
{
"internalType": "string",
"name": "gender",
"type": "string"
}
],
"name": "setVerification",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "user",
"type": "address"
}
],
"name": "getVerification",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
},
{
"internalType": "string",
"name": "",
"type": "string"
},
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
}
];
import { EU_COUNTRIES, ZKPassport } from '@zkpassport/sdk';
import { Claim } from '@/types/identity';
export const verifyAge = async (setProgress: (status:string) => void, setUrl: (url:string) => void): Promise<boolean> => {
const zkPassport = new ZKPassport();
export interface ZKPassportVerificationResult {
verified: boolean;
uniqueIdentifier: string;
claims: Claim[];
}
const queryBuilder = await zkPassport.request({
/**
* Verify that the user is an adult (18+ years old)
*/
export const verifyAdulthood = 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: "Prove you are 18+ years old",
scope: "adult",
});
});
const {
url,
onResult,
onGeneratingProof,
onError,
onProofGenerated,
onReject,
onRequestReceived
} = queryBuilder.gte("age", 18).done();
const {
url,
onResult,
onGeneratingProof,
onError,
onProofGenerated,
onReject,
onRequestReceived
} = queryBuilder.gte("age", 18).done();
setUrl(url);
setUrl(url);
return new Promise((resolve, reject) => {
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("Starting age 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(false);
});
onError((error) => {
setProgress(`Age verification error: ${error}`);
console.error("Age verification error", error);
resolve(false);
});
onResult(({ verified, uniqueIdentifier, result }) => {
console.log("Age verification callback", verified, uniqueIdentifier, result);
try {
console.log("Age verification result", verified, result);
if (verified) {
const isOver18 = result.age?.gte?.result;
setProgress("Age verification completed successfully");
resolve(isOver18 || false);
console.log("User is 18+ years old", isOver18);
} else {
setProgress("Age verification failed");
resolve(false);
}
} catch (error) {
console.error("Age verification result processing error", error);
setProgress(`Age verification result processing error: ${error}`);
resolve(false);
} finally {
setUrl('');
setProgress('');
}
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("Age verification exception", error);
setProgress(`Age verification exception: ${error}`);
reject(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
*/
export const discloseCountry = 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 country of nationality",
scope: "country",
});
const {
url,
onResult,
onGeneratingProof,
onError,
onProofGenerated,
onReject,
onRequestReceived
} = queryBuilder.disclose("nationality").done();
setUrl(url);
return new Promise((resolve, reject) => {
try {
console.log("Starting country disclosure with zkPassport");
onRequestReceived(() => {
setProgress("Request received, preparing for country disclosure");
console.log("Request received, preparing for country disclosure");
});
onGeneratingProof(() => {
setProgress("Generating cryptographic proof of country");
console.log("Generating cryptographic proof of country");
});
onProofGenerated(() => {
setProgress("Country proof generated successfully");
console.log("Country proof generated successfully");
});
onReject(() => {
setProgress("Country disclosure request was rejected");
console.log("Country disclosure request was rejected by the user");
resolve(null);
});
onError((error) => {
setProgress(`Country disclosure error: ${error}`);
console.error("Country disclosure error", error);
resolve(null);
});
onResult(({ verified, uniqueIdentifier, result }) => {
try {
console.log("Country disclosure result", verified, result);
if (verified && result.nationality?.disclose?.result) {
const claims: Claim[] = [
{
key: "country",
value: result.nationality.disclose.result,
verified: true
}
];
resolve({
verified: true,
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",
value: result.gender.disclose.result,
verified: true
}
];
resolve({
verified: true,
uniqueIdentifier: uniqueIdentifier || '',
claims
});
console.log("User gender disclosed", claims);
} else {
setProgress("Gender disclosure failed");
resolve(null);
}
} catch (error) {
console.error("Gender disclosure result processing error", error);
setProgress(`Gender disclosure result processing error: ${error}`);
resolve(null);
} finally {
setUrl('');
setProgress('');
}
});
} catch (error) {
console.error("Gender disclosure exception", error);
setProgress(`Gender disclosure exception: ${error}`);
reject(error);
}
});
}
/**
* Get a signer from the current wallet connection
* @returns Promise resolving to an ethers Signer or null if unavailable
*/
const getSigner = async (): Promise<any | null> => {
try {
// Get the provider from wagmi config
const provider = new BrowserProvider(window.ethereum as any, {name: "sepolia", chainId: 11155111});
// Request account access
await provider.send('eth_requestAccounts', []);
// Explicitly switch to Sepolia network
try {
await provider.send('wallet_switchEthereumChain', [
{ chainId: '0x' + (11155111).toString(16) }
]);
} catch (switchError: any) {
// If the network isn't added, add it
if (switchError.code === 4902) {
await provider.send('wallet_addEthereumChain', [
{
chainId: '0x' + (11155111).toString(16),
chainName: 'Sepolia Test Network',
nativeCurrency: {
name: 'Ethereum',
symbol: 'ETH',
decimals: 18
},
rpcUrls: ['https://eth-sepolia.api.onfinality.io/public'],
blockExplorerUrls: ['https://sepolia.etherscan.io']
}
]);
} else {
throw switchError;
}
}
return await provider.getSigner();
} catch (error) {
console.error('Failed to get signer:', error);
return null;
}
};
/**
* Submit verification data to the blockchain contract
* @param adult Whether the user is 18+
* @param country The user's country of nationality
* @param gender The user's gender
* @param setProgress Function to update progress status
* @returns Promise resolving to transaction hash on success, null on failure
*/
export const submitVerificationToContract = async (
adult: boolean,
country: string,
gender: string,
setProgress: (status: string) => void
): Promise<string | null> => {
setProgress('Initializing blockchain connection...');
try {
const signer = await getSigner();
if (!signer) {
setProgress('Failed to connect to wallet');
return null;
}
setProgress('Connecting to contract...');
if (!signer) {
setProgress('Failed to get signer');
return null;
}
const contract = new Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signer) as unknown as {
setVerification: (adult: boolean, country: string, gender: string) => Promise<any>;
};
setProgress('Submitting verification data to blockchain...');
const tx = await contract.setVerification(adult, country, gender);
setProgress('Waiting for blockchain confirmation...');
const receipt = await tx.wait();
if (receipt && receipt.hash) {
setProgress('Verification successfully recorded on blockchain!');
return receipt.hash;
} else {
setProgress('Transaction completed but no hash received');
return null;
}
} catch (error: any) {
console.error('Error submitting verification:', error);
if (error.message) {
setProgress(`Error: ${error.message}`);
} else {
setProgress('Failed to submit verification to contract');
}
return null;
}
};

View File

@ -7,7 +7,11 @@ import { DelegationFullStatus } from '@/lib/delegation';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { verifyAge } from '@/lib/zkPassport';
import { ContractVerificationButton } from '@/components/ui/contract-verification-button';
import { submitVerificationToContract } from '@/lib/zkPassport';
import { verifyAdulthood, discloseCountry, discloseGender } from '@/lib/zkPassport';
import { UserIdentityService } from '@/lib/services/UserIdentityService';
import { useForum } from '@/contexts/useForum';
import { QRCodeCanvas } from 'qrcode.react';
import {
Select,
@ -68,6 +72,8 @@ export default function ProfilePage() {
const [url, setUrl] = useState<string>('');
const [progress, setProgress] = useState<string>('');
const [isVerifying, setIsVerifying] = useState<boolean>(false);
const [verificationType, setVerificationType] = useState<'adult' | 'country' | 'gender' | null>(null);
const { userIdentityService } = useForum();
// Initialize and update local state when user data changes
useEffect(() => {
@ -592,36 +598,168 @@ export default function ProfilePage() {
</div>
</main>
{/* Age 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">
<h2 className="text-xl font-bold text-white mb-4">Age Verification</h2>
<h2 className="text-xl font-bold text-white mb-4">Identity Verification</h2>
<p className="text-cyber-neutral mb-4">
Verify your age to access restricted content.
Verify your identity to enhance your profile with verifiable claims.
</p>
<Button
onClick={async () => {
setIsVerifying(true);
try {
const result = await verifyAge(setProgress, setUrl);
console.log('Age verification result:', result);
} catch (error) {
console.error('Age verification failed:', error);
} finally {
setIsVerifying(false);
}
}}
disabled={isVerifying}
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 Age'
)}
</Button>
{/* Verification Buttons */}
<div className="space-y-3 mb-6">
<div className="space-y-2">
<Button
onClick={async () => {
setVerificationType('adult');
setIsVerifying(true);
try {
const result = await verifyAdulthood(setProgress, setUrl);
if (result && result.claims && result.claims.length > 0 && userIdentityService) {
if (result.uniqueIdentifier && result.claims[0]?.value !== undefined) {
userIdentityService.updateUserIdentityWithAdulthood(
address!,
result.uniqueIdentifier,
result.claims[0].value
);
}
}
} catch (error) {
console.error('Adulthood verification 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 === '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>
<span className="text-sm text-cyber-accent font-mono">
{typeof claim.value === 'boolean' ? (claim.value ? 'Yes' : 'No') : claim.value}
</span>
</div>
))}
</div>
</div>
)}
{/* Progress and QR Code */}
{progress && (
<p className="mt-4 text-sm text-cyber-neutral">{progress}</p>
)}

View File

@ -37,3 +37,20 @@ export enum EDisplayPreference {
CALL_SIGN = 'call-sign',
WALLET_ADDRESS = 'wallet-address',
}
// New interfaces for identity providers and claims
export interface Claim {
key: string;
value: any;
verified: boolean;
proof?: string;
expiresAt?: number;
}
export interface IdentityProvider {
type: string;
verifiedAt: number;
expiresAt?: number;
uniqueIdentifier?: string;
claims: Claim[];
}

View File

@ -1,4 +1,4 @@
import { EDisplayPreference, EVerificationStatus } from './identity';
import { EDisplayPreference, EVerificationStatus, IdentityProvider } from './identity';
import { DelegationProof } from '@/lib/delegation/types';
/**
@ -177,5 +177,6 @@ export interface UserIdentityCache {
displayPreference: EDisplayPreference;
lastUpdated: number;
verificationStatus: EVerificationStatus;
identityProviders?: IdentityProvider[];
};
}

View File

@ -21,4 +21,8 @@ export default defineConfig(() => ({
build: {
target: 'es2022',
},
optimizeDeps: {
exclude: ['@aztec/bb.js'],
include: ['pino']
}
}));