mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-02 12:53:10 +00:00
initial prep for on-chain ZKPassport verification
This commit is contained in:
parent
fefe7608ad
commit
0d357ad64a
4965
package-lock.json
generated
4965
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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[];
|
||||
}
|
||||
|
||||
@ -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[];
|
||||
};
|
||||
}
|
||||
|
||||
@ -21,4 +21,8 @@ export default defineConfig(() => ({
|
||||
build: {
|
||||
target: 'es2022',
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['@aztec/bb.js'],
|
||||
include: ['pino']
|
||||
}
|
||||
}));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user