mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-03 21:33:09 +00:00
Merge 539e118e300aae1f94e078ebd44b140be50ebd35 into c91164dbde13258e6e6e34c56a7ac136ac0c2872
This commit is contained in:
commit
3d5ec907d7
149
contracts/ZKPassportVerifier.sol
Normal file
149
contracts/ZKPassportVerifier.sol
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
// 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 initialized; // Whether the user has set their verification data
|
||||||
|
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 per wallet
|
||||||
|
// This prevents cross-wallet correlation while maintaining Sybil resistance
|
||||||
|
mapping(address => mapping(bytes32 => bool)) public walletUniqueIdentifiers;
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
|
||||||
|
// Always enforce wallet-scoped uniqueness
|
||||||
|
require(!walletUniqueIdentifiers[msg.sender][uniqueIdentifier], "Unique identifier already used by this wallet");
|
||||||
|
|
||||||
|
// Mark this unique identifier as used by this wallet
|
||||||
|
walletUniqueIdentifiers[msg.sender][uniqueIdentifier] = true;
|
||||||
|
|
||||||
|
// Store the verification data
|
||||||
|
verifications[msg.sender] = Verification({
|
||||||
|
initialized: true,
|
||||||
|
adult: adult,
|
||||||
|
country: country,
|
||||||
|
gender: gender
|
||||||
|
});
|
||||||
|
|
||||||
|
emit VerificationUpdated(msg.sender, adult, country, gender);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Update verification data without requiring a new proof
|
||||||
|
* @param adult Whether the sender is 18+
|
||||||
|
* @param country The sender's country of nationality
|
||||||
|
* @param gender The sender's gender
|
||||||
|
*/
|
||||||
|
function updateVerification(
|
||||||
|
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");
|
||||||
|
|
||||||
|
// Always enforce wallet-scoped uniqueness
|
||||||
|
require(walletUniqueIdentifiers[msg.sender][uniqueIdentifier], "Unique identifier already used by this wallet");
|
||||||
|
|
||||||
|
// Update the verification data
|
||||||
|
verifications[msg.sender] = Verification({
|
||||||
|
initialized: true,
|
||||||
|
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, bool, string memory, string memory)
|
||||||
|
{
|
||||||
|
Verification storage verification = verifications[user];
|
||||||
|
return (verification.initialized, verification.adult, verification.country, verification.gender);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
hardhat.config.js
Normal file
25
hardhat.config.js
Normal 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;
|
||||||
7030
package-lock.json
generated
7030
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@ -50,18 +50,24 @@
|
|||||||
"@reown/appkit-adapter-wagmi": "^1.7.17",
|
"@reown/appkit-adapter-wagmi": "^1.7.17",
|
||||||
"@reown/appkit-wallet-button": "^1.7.17",
|
"@reown/appkit-wallet-button": "^1.7.17",
|
||||||
"@tanstack/react-query": "^5.84.1",
|
"@tanstack/react-query": "^5.84.1",
|
||||||
|
"@types/qrcode.react": "^1.0.5",
|
||||||
"@waku/sdk": "^0.0.35-67a7287.0",
|
"@waku/sdk": "^0.0.35-67a7287.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
|
"@zkpassport/sdk": "^0.8.3",
|
||||||
|
"bitcoinjs-message": "^2.2.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
|
"dotenv": "^17.2.2",
|
||||||
"embla-carousel-react": "^8.3.0",
|
"embla-carousel-react": "^8.3.0",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"ordiscan": "^1.3.0",
|
"ordiscan": "^1.3.0",
|
||||||
"re-resizable": "6.11.2",
|
"re-resizable": "6.11.2",
|
||||||
|
"pino": "^9.9.4",
|
||||||
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@ -84,6 +90,12 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.9.0",
|
"@eslint/js": "^9.9.0",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@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/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
@ -98,6 +110,7 @@
|
|||||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.9",
|
"eslint-plugin-react-refresh": "^0.4.9",
|
||||||
"globals": "^15.9.0",
|
"globals": "^15.9.0",
|
||||||
|
"hardhat": "^2.26.3",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export function AuthorDisplay({
|
|||||||
className = '',
|
className = '',
|
||||||
showBadge = true,
|
showBadge = true,
|
||||||
}: AuthorDisplayProps) {
|
}: AuthorDisplayProps) {
|
||||||
const { displayName, callSign, ensName, ordinalDetails } =
|
const { displayName, callSign, ensName, ordinalDetails, countryFlag, ageEmoji, genderEmoji } =
|
||||||
useUserDisplay(address);
|
useUserDisplay(address);
|
||||||
|
|
||||||
// Only show a badge if the author has ENS, Ordinal, or Call Sign
|
// Only show a badge if the author has ENS, Ordinal, or Call Sign
|
||||||
@ -21,7 +21,12 @@ export function AuthorDisplay({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex items-center gap-1.5 ${className}`}>
|
<div className={`flex items-center gap-1.5 ${className}`}>
|
||||||
<span className="text-xs text-muted-foreground">{displayName}</span>
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{displayName}
|
||||||
|
{countryFlag && <span className="ml-1">{countryFlag}</span>}
|
||||||
|
{ageEmoji && <span className="ml-1">{ageEmoji}</span>}
|
||||||
|
{genderEmoji && <span className="ml-1">{genderEmoji}</span>}
|
||||||
|
</span>
|
||||||
|
|
||||||
{shouldShowBadge && (
|
{shouldShowBadge && (
|
||||||
<Badge
|
<Badge
|
||||||
|
|||||||
92
src/components/ui/contract-verification-button.tsx
Normal file
92
src/components/ui/contract-verification-button.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { useForum } from '@/contexts/useForum';
|
import { useForum } from '@/contexts/useForum';
|
||||||
import { EDisplayPreference, EVerificationStatus } from '@/types/identity';
|
import { EDisplayPreference, EVerificationStatus, IdentityProvider } from '@/types/identity';
|
||||||
|
|
||||||
export interface UserDisplayInfo {
|
export interface UserDisplayInfo {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@ -11,6 +11,10 @@ export interface UserDisplayInfo {
|
|||||||
displayPreference: EDisplayPreference | null;
|
displayPreference: EDisplayPreference | null;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
identityProviders: IdentityProvider[] | null;
|
||||||
|
countryFlag: string | null;
|
||||||
|
ageEmoji: string | null;
|
||||||
|
genderEmoji: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -27,6 +31,10 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
|
|||||||
displayPreference: null,
|
displayPreference: null,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
error: null,
|
error: null,
|
||||||
|
identityProviders: null,
|
||||||
|
countryFlag: null,
|
||||||
|
ageEmoji: null,
|
||||||
|
genderEmoji: null,
|
||||||
});
|
});
|
||||||
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||||
|
|
||||||
@ -83,6 +91,10 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
|
|||||||
displayPreference: null,
|
displayPreference: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
identityProviders: null,
|
||||||
|
countryFlag: null,
|
||||||
|
ageEmoji: null,
|
||||||
|
genderEmoji: null,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -93,6 +105,32 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
|
|||||||
if (identity) {
|
if (identity) {
|
||||||
const displayName = userIdentityService.getDisplayName(address);
|
const displayName = userIdentityService.getDisplayName(address);
|
||||||
|
|
||||||
|
// Extract country, adult, and gender claims from identity providers
|
||||||
|
let countryFlag = null;
|
||||||
|
let ageEmoji = null;
|
||||||
|
let genderEmoji = null;
|
||||||
|
|
||||||
|
if (identity.identityProviders) {
|
||||||
|
for (const provider of identity.identityProviders) {
|
||||||
|
if (provider.type === 'zkpassport') {
|
||||||
|
for (const claim of provider.claims) {
|
||||||
|
if (claim.key === 'country' && claim.verified && typeof claim.value === 'string') {
|
||||||
|
// Convert country code to flag emoji
|
||||||
|
countryFlag = getCountryFlag(claim.value);
|
||||||
|
}
|
||||||
|
if (claim.key === 'adult' && claim.verified && typeof claim.value === 'boolean') {
|
||||||
|
ageEmoji = claim.value ? '🧓' : '👶';
|
||||||
|
}
|
||||||
|
if (claim.key === 'gender' && claim.verified && typeof claim.value === 'string') {
|
||||||
|
// Map gender to emoji
|
||||||
|
genderEmoji = claim.value.toLowerCase() === 'm' ? '♂️' :
|
||||||
|
claim.value.toLowerCase() === 'f' ? '♀️' : '⚧️';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setDisplayInfo({
|
setDisplayInfo({
|
||||||
displayName,
|
displayName,
|
||||||
callSign: identity.callSign || null,
|
callSign: identity.callSign || null,
|
||||||
@ -104,6 +142,10 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
|
|||||||
displayPreference: identity.displayPreference || null,
|
displayPreference: identity.displayPreference || null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
identityProviders: identity.identityProviders || null,
|
||||||
|
countryFlag,
|
||||||
|
ageEmoji,
|
||||||
|
genderEmoji,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setDisplayInfo({
|
setDisplayInfo({
|
||||||
@ -117,6 +159,10 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
|
|||||||
displayPreference: null,
|
displayPreference: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
identityProviders: null,
|
||||||
|
countryFlag: null,
|
||||||
|
ageEmoji: null,
|
||||||
|
genderEmoji: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -133,6 +179,10 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
|
|||||||
displayPreference: null,
|
displayPreference: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
identityProviders: null,
|
||||||
|
countryFlag: null,
|
||||||
|
ageEmoji: null,
|
||||||
|
genderEmoji: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -157,6 +207,17 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
|
|||||||
verificationInfo,
|
verificationInfo,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Helper function to convert country code to flag emoji
|
||||||
|
function getCountryFlag(countryCode: string): string {
|
||||||
|
// Convert country code to flag emoji using regional indicator symbols
|
||||||
|
// For example, 'US' -> '🇺🇸'
|
||||||
|
return countryCode
|
||||||
|
.toUpperCase()
|
||||||
|
.replace(/./g, char =>
|
||||||
|
String.fromCodePoint(127397 + char.charCodeAt(0))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return displayInfo;
|
return displayInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { EVerificationStatus, EDisplayPreference } from '@/types/identity';
|
import { EVerificationStatus, EDisplayPreference, IdentityProvider, Claim } from '@/types/identity';
|
||||||
import {
|
import {
|
||||||
UnsignedUserProfileUpdateMessage,
|
UnsignedUserProfileUpdateMessage,
|
||||||
UserProfileUpdateMessage,
|
UserProfileUpdateMessage,
|
||||||
@ -9,6 +9,7 @@ import { MessageService } from './MessageService';
|
|||||||
import messageManager from '@/lib/waku';
|
import messageManager from '@/lib/waku';
|
||||||
import { localDatabase } from '@/lib/database/LocalDatabase';
|
import { localDatabase } from '@/lib/database/LocalDatabase';
|
||||||
import { WalletManager } from '@/lib/wallet';
|
import { WalletManager } from '@/lib/wallet';
|
||||||
|
import { getVerification } from '../zkPassport';
|
||||||
|
|
||||||
export interface UserIdentity {
|
export interface UserIdentity {
|
||||||
address: string;
|
address: string;
|
||||||
@ -21,6 +22,7 @@ export interface UserIdentity {
|
|||||||
displayPreference: EDisplayPreference;
|
displayPreference: EDisplayPreference;
|
||||||
lastUpdated: number;
|
lastUpdated: number;
|
||||||
verificationStatus: EVerificationStatus;
|
verificationStatus: EVerificationStatus;
|
||||||
|
identityProviders?: IdentityProvider[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserIdentityService {
|
export class UserIdentityService {
|
||||||
@ -59,6 +61,7 @@ export class UserIdentityService {
|
|||||||
verificationStatus: this.mapVerificationStatus(
|
verificationStatus: this.mapVerificationStatus(
|
||||||
cached.verificationStatus
|
cached.verificationStatus
|
||||||
),
|
),
|
||||||
|
identityProviders: cached.identityProviders
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,6 +194,7 @@ export class UserIdentityService {
|
|||||||
displayPreference: cached.displayPreference,
|
displayPreference: cached.displayPreference,
|
||||||
lastUpdated: cached.lastUpdated,
|
lastUpdated: cached.lastUpdated,
|
||||||
verificationStatus: this.mapVerificationStatus(cached.verificationStatus),
|
verificationStatus: this.mapVerificationStatus(cached.verificationStatus),
|
||||||
|
identityProviders: cached.identityProviders
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,6 +254,7 @@ export class UserIdentityService {
|
|||||||
: this.userIdentityCache[address].callSign,
|
: this.userIdentityCache[address].callSign,
|
||||||
displayPreference,
|
displayPreference,
|
||||||
lastUpdated: timestamp,
|
lastUpdated: timestamp,
|
||||||
|
identityProviders: this.userIdentityCache[address].identityProviders
|
||||||
};
|
};
|
||||||
|
|
||||||
localDatabase.cache.userIdentities[address] = updatedIdentity;
|
localDatabase.cache.userIdentities[address] = updatedIdentity;
|
||||||
@ -315,6 +320,7 @@ export class UserIdentityService {
|
|||||||
displayPreference: defaultDisplayPreference,
|
displayPreference: defaultDisplayPreference,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
verificationStatus,
|
verificationStatus,
|
||||||
|
identityProviders: []
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to resolve user identity:', error);
|
console.error('Failed to resolve user identity:', error);
|
||||||
@ -388,6 +394,7 @@ export class UserIdentityService {
|
|||||||
displayPreference,
|
displayPreference,
|
||||||
lastUpdated: timestamp,
|
lastUpdated: timestamp,
|
||||||
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
|
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
|
||||||
|
identityProviders: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -405,6 +412,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
|
* Map verification status string to enum
|
||||||
*/
|
*/
|
||||||
@ -435,8 +654,19 @@ export class UserIdentityService {
|
|||||||
* Refresh user identity (force re-resolution)
|
* Refresh user identity (force re-resolution)
|
||||||
*/
|
*/
|
||||||
async refreshUserIdentity(address: string): Promise<void> {
|
async refreshUserIdentity(address: string): Promise<void> {
|
||||||
|
// Preserve identity providers when refreshing
|
||||||
|
const preservedProviders = this.userIdentityCache[address]?.identityProviders;
|
||||||
|
|
||||||
delete this.userIdentityCache[address];
|
delete this.userIdentityCache[address];
|
||||||
|
|
||||||
|
// Get fresh identity
|
||||||
await this.getUserIdentity(address);
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -483,4 +713,187 @@ export class UserIdentityService {
|
|||||||
|
|
||||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get ZKPassport claims with multi-layer cache support
|
||||||
|
*/
|
||||||
|
async getZKPassportClaims(address: string): Promise<Claim[] | null> {
|
||||||
|
// 1. Check in-memory cache first (fastest)
|
||||||
|
if (this.userIdentityCache[address]?.identityProviders) {
|
||||||
|
const zkPassportProvider = this.userIdentityCache[address].identityProviders?.find(p => p.type === 'zkpassport');
|
||||||
|
if (zkPassportProvider) {
|
||||||
|
return zkPassportProvider.claims;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check LocalDatabase persistence (warm start)
|
||||||
|
const persisted = localDatabase.cache.userIdentities[address];
|
||||||
|
if (persisted?.identityProviders) {
|
||||||
|
const zkPassportProvider = persisted.identityProviders.find(p => p.type === 'zkpassport');
|
||||||
|
if (zkPassportProvider) {
|
||||||
|
// Restore in memory cache
|
||||||
|
this.userIdentityCache[address] = {
|
||||||
|
...persisted,
|
||||||
|
identityProviders: persisted.identityProviders
|
||||||
|
};
|
||||||
|
return zkPassportProvider.claims;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check Waku message cache (network cache)
|
||||||
|
const cacheServiceData = messageManager.messageCache.userIdentities[address];
|
||||||
|
if (cacheServiceData?.identityProviders) {
|
||||||
|
const zkPassportProvider = cacheServiceData.identityProviders.find(p => p.type === 'zkpassport');
|
||||||
|
if (zkPassportProvider) {
|
||||||
|
// Store in internal cache for future use
|
||||||
|
this.userIdentityCache[address] = {
|
||||||
|
...cacheServiceData,
|
||||||
|
identityProviders: cacheServiceData.identityProviders
|
||||||
|
};
|
||||||
|
return zkPassportProvider.claims;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Fetch from blockchain (source of truth)
|
||||||
|
return this.resolveZKPassportClaims(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force fresh resolution of ZKPassport claims (bypass caches)
|
||||||
|
*/
|
||||||
|
async getZKPassportClaimsFresh(address: string): Promise<Claim[] | null> {
|
||||||
|
return this.resolveZKPassportClaims(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve ZKPassport claims from blockchain contract with TTL caching
|
||||||
|
*/
|
||||||
|
private async resolveZKPassportClaims(address: string): Promise<Claim[] | null> {
|
||||||
|
try {
|
||||||
|
// Check if we have a recent cached version
|
||||||
|
const cached = this.userIdentityCache[address]?.identityProviders?.find(p => p.type === 'zkpassport');
|
||||||
|
const now = Date.now();
|
||||||
|
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes TTL
|
||||||
|
|
||||||
|
// If we have a recent cache and it's still valid, return it
|
||||||
|
if (cached && cached.verifiedAt && (now - cached.verifiedAt) < CACHE_TTL) {
|
||||||
|
return cached.claims;
|
||||||
|
}
|
||||||
|
|
||||||
|
const claimsData = await getVerification(address);
|
||||||
|
if (!claimsData) return null;
|
||||||
|
|
||||||
|
const claims: Claim[] = [];
|
||||||
|
|
||||||
|
// Process adult claim
|
||||||
|
if (claimsData.adult !== undefined) {
|
||||||
|
claims.push({
|
||||||
|
key: 'adult',
|
||||||
|
value: claimsData.adult,
|
||||||
|
verified: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process country claim
|
||||||
|
if (claimsData.country) {
|
||||||
|
claims.push({
|
||||||
|
key: 'country',
|
||||||
|
value: claimsData.country,
|
||||||
|
verified: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process gender claim
|
||||||
|
if (claimsData.gender) {
|
||||||
|
claims.push({
|
||||||
|
key: 'gender',
|
||||||
|
value: claimsData.gender,
|
||||||
|
verified: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update identity with claims (this updates all cache layers)
|
||||||
|
this.updateUserIdentityWithZKPassportClaims(address, claims);
|
||||||
|
|
||||||
|
return claims;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to resolve ZKPassport claims:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user identity with ZKPassport claims (updates all cache layers)
|
||||||
|
*/
|
||||||
|
updateUserIdentityWithZKPassportClaims(address: string, claims: Claim[]): void {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
// Initialize identity if it doesn't exist
|
||||||
|
if (!this.userIdentityCache[address]) {
|
||||||
|
this.userIdentityCache[address] = {
|
||||||
|
ensName: undefined,
|
||||||
|
ordinalDetails: undefined,
|
||||||
|
callSign: undefined,
|
||||||
|
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
||||||
|
lastUpdated: timestamp,
|
||||||
|
verificationStatus: EVerificationStatus.WALLET_CONNECTED,
|
||||||
|
identityProviders: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or update ZKPassport provider with TTL
|
||||||
|
const zkPassportProvider: IdentityProvider = {
|
||||||
|
type: 'zkpassport',
|
||||||
|
verifiedAt: timestamp,
|
||||||
|
expiresAt: timestamp + 24 * 60 * 60 * 1000, // 24 hours validity
|
||||||
|
claims: [...claims]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Replace or add provider
|
||||||
|
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 has verified claims
|
||||||
|
if (claims.some(c => c.verified)) {
|
||||||
|
this.userIdentityCache[address].verificationStatus = EVerificationStatus.ENS_ORDINAL_VERIFIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify listeners that the user identity has been updated
|
||||||
|
this.notifyRefreshListeners(address);
|
||||||
|
|
||||||
|
// Persist to IndexedDB
|
||||||
|
localDatabase.upsertUserIdentity(address, {
|
||||||
|
identityProviders: this.userIdentityCache[address].identityProviders,
|
||||||
|
lastUpdated: timestamp
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch resolve multiple user identities for post processing
|
||||||
|
*/
|
||||||
|
async resolveMultipleUsers(addresses: string[]): Promise<Map<string, UserIdentity>> {
|
||||||
|
const result = new Map<string, UserIdentity>();
|
||||||
|
|
||||||
|
// Process all resolutions in parallel
|
||||||
|
await Promise.all(
|
||||||
|
addresses.map(async (address) => {
|
||||||
|
const identity = await this.getUserIdentity(address);
|
||||||
|
if (identity) {
|
||||||
|
result.set(address, identity);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import { AppKitOptions } from '@reown/appkit';
|
|||||||
import { BitcoinAdapter } from '@reown/appkit-adapter-bitcoin';
|
import { BitcoinAdapter } from '@reown/appkit-adapter-bitcoin';
|
||||||
import { WagmiAdapter } from '@reown/appkit-adapter-wagmi';
|
import { WagmiAdapter } from '@reown/appkit-adapter-wagmi';
|
||||||
import { createStorage } from '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 =
|
const projectId =
|
||||||
process.env.VITE_REOWN_SECRET || '2ead96ea166a03e5ab50e5c190532e72';
|
process.env.VITE_REOWN_SECRET || '2ead96ea166a03e5ab50e5c190532e72';
|
||||||
|
|||||||
711
src/lib/zkPassport.ts
Normal file
711
src/lib/zkPassport.ts
Normal file
@ -0,0 +1,711 @@
|
|||||||
|
import { BrowserProvider, Contract } from 'ethers';
|
||||||
|
import { config } from '@/lib/wallet/config';
|
||||||
|
// Contract configuration - these should be moved to environment variables in production
|
||||||
|
export const CONTRACT_ADDRESS = "0x1753dbd9f4bb6473ee2905b2db183760B95be475"; // Hardhat default deploy address
|
||||||
|
const CONTRACT_ABI = [
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "_zkVerifier",
|
||||||
|
"type": "address"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "constructor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"indexed": true,
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "user",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": false,
|
||||||
|
"internalType": "bool",
|
||||||
|
"name": "adult",
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": false,
|
||||||
|
"internalType": "string",
|
||||||
|
"name": "country",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": false,
|
||||||
|
"internalType": "string",
|
||||||
|
"name": "gender",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "VerificationUpdated",
|
||||||
|
"type": "event"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "user",
|
||||||
|
"type": "address"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "getVerification",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": "bool",
|
||||||
|
"name": "initialized",
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "bool",
|
||||||
|
"name": "adult",
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "string",
|
||||||
|
"name": "country",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "string",
|
||||||
|
"name": "gender",
|
||||||
|
"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": "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": "updateVerification",
|
||||||
|
"outputs": [],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"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": [
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "bytes32",
|
||||||
|
"name": "",
|
||||||
|
"type": "bytes32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "walletUniqueIdentifiers",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": "bool",
|
||||||
|
"name": "",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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 { V } from 'vitest/dist/chunks/environment.d.cL3nLXbE.js';
|
||||||
|
|
||||||
|
export interface ZKPassportVerificationResult {
|
||||||
|
verified: boolean;
|
||||||
|
uniqueIdentifier: string;
|
||||||
|
claims: Claim[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for ZKPassport verification
|
||||||
|
*/
|
||||||
|
export interface ZKPassportVerificationOptions {
|
||||||
|
verifyAdulthood: boolean;
|
||||||
|
verifyCountry: boolean;
|
||||||
|
verifyGender: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform ZKPassport verification with selective disclosure based on provided options
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 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({
|
||||||
|
name: "OpChan",
|
||||||
|
logo: "https://zkpassport.id/logo.png",
|
||||||
|
purpose,
|
||||||
|
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 {
|
||||||
|
url,
|
||||||
|
onResult,
|
||||||
|
onGeneratingProof,
|
||||||
|
onError,
|
||||||
|
onProofGenerated,
|
||||||
|
onReject,
|
||||||
|
onRequestReceived
|
||||||
|
} = queryBuilder.done();
|
||||||
|
|
||||||
|
setUrl(url);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
console.log("Starting ZKPassport verification with options:", options);
|
||||||
|
onRequestReceived(() => {
|
||||||
|
setProgress("Request received, preparing for verification");
|
||||||
|
console.log("Request received, preparing for verification");
|
||||||
|
});
|
||||||
|
|
||||||
|
onGeneratingProof(() => {
|
||||||
|
setProgress("Generating cryptographic proof");
|
||||||
|
console.log("Generating cryptographic proof");
|
||||||
|
});
|
||||||
|
|
||||||
|
onProofGenerated((proof: ProofResult) => {
|
||||||
|
setProgress("Proof generated successfully");
|
||||||
|
console.log("Proof generated successfully");
|
||||||
|
setProof(proof);
|
||||||
|
});
|
||||||
|
|
||||||
|
onReject(() => {
|
||||||
|
setProgress("Verification request was rejected");
|
||||||
|
console.log("Verification request was rejected by the user");
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
onError((error) => {
|
||||||
|
setProgress(`Verification error: ${error}`);
|
||||||
|
console.error("Verification error", error);
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
onResult(({ verified, uniqueIdentifier, result }) => {
|
||||||
|
try {
|
||||||
|
console.log("ZKPassport verification result", verified, result);
|
||||||
|
if (verified) {
|
||||||
|
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",
|
||||||
|
value: result.nationality.disclose.result,
|
||||||
|
verified: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.verifyGender && result.gender?.disclose?.result) {
|
||||||
|
claims.push({
|
||||||
|
key: "gender",
|
||||||
|
value: result.gender.disclose.result,
|
||||||
|
verified: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
verified: true,
|
||||||
|
uniqueIdentifier: uniqueIdentifier || '',
|
||||||
|
claims
|
||||||
|
});
|
||||||
|
console.log("User verified with claims", claims);
|
||||||
|
} else {
|
||||||
|
setProgress("Verification failed");
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Verification result processing error", error);
|
||||||
|
setProgress(`Verification result processing error: ${error}`);
|
||||||
|
resolve(null);
|
||||||
|
} finally {
|
||||||
|
setUrl('');
|
||||||
|
setProgress('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("ZKPassport verification exception", error);
|
||||||
|
setProgress(`ZKPassport verification 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record verified claims on the blockchain
|
||||||
|
* @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,
|
||||||
|
proof: ProofResult,
|
||||||
|
setProgress: (status: string) => void
|
||||||
|
): Promise<string | null> => {
|
||||||
|
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 {
|
||||||
|
const signer = await getSigner();
|
||||||
|
console.log(signer)
|
||||||
|
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, verifierParams: SolidityVerifierParameters) => Promise<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
setProgress('Submitting verification data to blockchain...');
|
||||||
|
const tx = await contract.setVerification(adult, country, gender, verifierParams);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, boolean, string, string]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [initialized, adult, country, gender] = await contract.getVerification(address);
|
||||||
|
if (!initialized) {
|
||||||
|
return null; // No verification data set for this user
|
||||||
|
}
|
||||||
|
return { adult, country, gender };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching verification data:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update verification data for a user without requiring a new proof
|
||||||
|
* @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 updateVerification = async (
|
||||||
|
adult: boolean,
|
||||||
|
country: string,
|
||||||
|
gender: string,
|
||||||
|
proof: ProofResult,
|
||||||
|
setProgress: (status: string) => void
|
||||||
|
): Promise<string | null> => {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
updateVerification: (adult: boolean, country: string, gender: string, verifierParams: SolidityVerifierParameters) => Promise<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
setProgress('Updating verification data on blockchain...');
|
||||||
|
const tx = await contract.updateVerification(adult, country, gender, verifierParams);
|
||||||
|
|
||||||
|
setProgress('Waiting for blockchain confirmation...');
|
||||||
|
const receipt = await tx.wait();
|
||||||
|
|
||||||
|
if (receipt && receipt.hash) {
|
||||||
|
setProgress('Verification successfully updated on blockchain!');
|
||||||
|
return receipt.hash;
|
||||||
|
} else {
|
||||||
|
setProgress('Transaction completed but no hash received');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error updating verification:', error);
|
||||||
|
if (error.message) {
|
||||||
|
setProgress(`Error: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
setProgress('Failed to update verification on contract');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch ZKPassport claims for a user with proper typing
|
||||||
|
* @param address The wallet address of the user to fetch claims for
|
||||||
|
* @returns Promise resolving to claims array or null if not found
|
||||||
|
*/
|
||||||
|
export const fetchZKPassportClaims = async (address: string): Promise<Claim[] | null> => {
|
||||||
|
try {
|
||||||
|
const claimsData = await getVerification(address);
|
||||||
|
if (!claimsData) return null;
|
||||||
|
|
||||||
|
const claims: Claim[] = [];
|
||||||
|
|
||||||
|
// Process adult claim
|
||||||
|
if (claimsData.adult !== undefined) {
|
||||||
|
claims.push({
|
||||||
|
key: 'adult',
|
||||||
|
value: claimsData.adult,
|
||||||
|
verified: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process country claim
|
||||||
|
if (claimsData.country) {
|
||||||
|
claims.push({
|
||||||
|
key: 'country',
|
||||||
|
value: claimsData.country,
|
||||||
|
verified: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process gender claim
|
||||||
|
if (claimsData.gender) {
|
||||||
|
claims.push({
|
||||||
|
key: 'gender',
|
||||||
|
value: claimsData.gender,
|
||||||
|
verified: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims.length > 0 ? claims : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching ZKPassport claims:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -7,6 +7,12 @@ import { DelegationFullStatus } from '@/lib/delegation';
|
|||||||
import { Button } from '@/components/ui/button';
|
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 { CONTRACT_ADDRESS, getVerification, submitVerificationToContract, updateVerification } from '@/lib/zkPassport';
|
||||||
|
import { verifyWithZKPassport, ZKPassportVerificationOptions } from '@/lib/zkPassport';
|
||||||
|
import { UserIdentityService } from '@/lib/services/UserIdentityService';
|
||||||
|
import { useForum } from '@/contexts/useForum';
|
||||||
|
import { QRCodeCanvas } from 'qrcode.react';
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@ -34,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();
|
||||||
@ -63,6 +70,52 @@ export default function ProfilePage() {
|
|||||||
EDisplayPreference.WALLET_ADDRESS
|
EDisplayPreference.WALLET_ADDRESS
|
||||||
);
|
);
|
||||||
const [walletWizardOpen, setWalletWizardOpen] = useState(false);
|
const [walletWizardOpen, setWalletWizardOpen] = useState(false);
|
||||||
|
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 [proof, setProof] = useState<ProofResult | null>(null);
|
||||||
|
const [verificationOptions, setVerificationOptions] = useState<ZKPassportVerificationOptions>({
|
||||||
|
verifyAdulthood: false,
|
||||||
|
verifyCountry: false,
|
||||||
|
verifyGender: false
|
||||||
|
});
|
||||||
|
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(() => {
|
||||||
@ -587,6 +640,235 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
{/* Identity Verification Section */}
|
||||||
|
<div className="max-w-4xl mx-auto mt-8 mb-8">
|
||||||
|
<Card className="content-card">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center justify-between text-white">
|
||||||
|
<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 Status */}
|
||||||
|
{userInfo.identityProviders && userInfo.identityProviders.some(p => p.type === 'zkpassport') && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-sm font-medium text-cyber-neutral uppercase tracking-wide">
|
||||||
|
Verified (and <a className="text-cyber-accent hover:underline" href={`https://sepolia.etherscan.io/address/${CONTRACT_ADDRESS}`} target='_blank'>recorded</a>) 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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column: Verification Controls */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Verification Toggles */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="verifyAdulthood"
|
||||||
|
checked={verificationOptions.verifyAdulthood}
|
||||||
|
onChange={(e) => setVerificationOptions({...verificationOptions, verifyAdulthood: 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="verifyAdulthood" className="text-sm text-cyber-neutral">
|
||||||
|
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');
|
||||||
|
|
||||||
|
// Check if verification already exists
|
||||||
|
const existingVerification = await getVerification(address!);
|
||||||
|
const hasExistingVerification = existingVerification && (
|
||||||
|
existingVerification.adult !== undefined ||
|
||||||
|
existingVerification.country !== '' ||
|
||||||
|
existingVerification.gender !== ''
|
||||||
|
);
|
||||||
|
console.log('Existing verification:', existingVerification, hasExistingVerification);
|
||||||
|
|
||||||
|
let tx;
|
||||||
|
if (hasExistingVerification) {
|
||||||
|
// Use updateVerification for existing verifications
|
||||||
|
tx = await updateVerification(
|
||||||
|
adulthoodClaim?.value as boolean || false,
|
||||||
|
countryClaim?.value as string || '',
|
||||||
|
genderClaim?.value as string || '',
|
||||||
|
proof,
|
||||||
|
setProgress
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Use submitVerificationToContract for new verifications
|
||||||
|
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>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
<footer className="page-footer">
|
<footer className="page-footer">
|
||||||
<p>OpChan - A decentralized forum built on Waku & Bitcoin Ordinals</p>
|
<p>OpChan - A decentralized forum built on Waku & Bitcoin Ordinals</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@ -37,3 +37,20 @@ export enum EDisplayPreference {
|
|||||||
CALL_SIGN = 'call-sign',
|
CALL_SIGN = 'call-sign',
|
||||||
WALLET_ADDRESS = 'wallet-address',
|
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';
|
import { DelegationProof } from '@/lib/delegation/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -177,5 +177,6 @@ export interface UserIdentityCache {
|
|||||||
displayPreference: EDisplayPreference;
|
displayPreference: EDisplayPreference;
|
||||||
lastUpdated: number;
|
lastUpdated: number;
|
||||||
verificationStatus: EVerificationStatus;
|
verificationStatus: EVerificationStatus;
|
||||||
|
identityProviders?: IdentityProvider[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,4 +21,8 @@ export default defineConfig(() => ({
|
|||||||
build: {
|
build: {
|
||||||
target: 'es2022',
|
target: 'es2022',
|
||||||
},
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ['@aztec/bb.js'],
|
||||||
|
include: ['pino']
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user