diff --git a/contracts/ZKPassportVerifier.sol b/contracts/ZKPassportVerifier.sol
index f1979e0..ee640b6 100644
--- a/contracts/ZKPassportVerifier.sol
+++ b/contracts/ZKPassportVerifier.sol
@@ -31,6 +31,7 @@ interface IZKVerifier {
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
@@ -39,8 +40,9 @@ contract ZKPassportVerifier {
// Mapping from user address to their verification data
mapping(address => Verification) public verifications;
- // Mapping to track used unique identifiers
- mapping(bytes32 => bool) public usedUniqueIdentifiers;
+ // 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;
@@ -81,14 +83,47 @@ contract ZKPassportVerifier {
// Revert if proof is not valid
require(verified, "Proof verification failed");
- // Check if this unique identifier has already been used
- require(!usedUniqueIdentifiers[uniqueIdentifier], "Unique identifier already used");
+ // Always enforce wallet-scoped uniqueness
+ require(!walletUniqueIdentifiers[msg.sender][uniqueIdentifier], "Unique identifier already used by this wallet");
- // Mark this unique identifier as used
- usedUniqueIdentifiers[uniqueIdentifier] = true;
+ // 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
@@ -106,9 +141,9 @@ contract ZKPassportVerifier {
*/
function getVerification(address user)
external view
- returns (bool, string memory, string memory)
+ returns (bool, bool, string memory, string memory)
{
Verification storage verification = verifications[user];
- return (verification.adult, verification.country, verification.gender);
+ return (verification.initialized, verification.adult, verification.country, verification.gender);
}
}
\ No newline at end of file
diff --git a/src/components/ui/author-display.tsx b/src/components/ui/author-display.tsx
index 04b0bf7..5328496 100644
--- a/src/components/ui/author-display.tsx
+++ b/src/components/ui/author-display.tsx
@@ -13,7 +13,7 @@ export function AuthorDisplay({
className = '',
showBadge = true,
}: AuthorDisplayProps) {
- const { displayName, callSign, ensName, ordinalDetails } =
+ const { displayName, callSign, ensName, ordinalDetails, countryFlag, ageEmoji, genderEmoji } =
useUserDisplay(address);
// Only show a badge if the author has ENS, Ordinal, or Call Sign
@@ -21,7 +21,12 @@ export function AuthorDisplay({
return (
-
{displayName}
+
+ {displayName}
+ {countryFlag && {countryFlag}}
+ {ageEmoji && {ageEmoji}}
+ {genderEmoji && {genderEmoji}}
+
{shouldShowBadge && (
'🇺🇸'
+ return countryCode
+ .toUpperCase()
+ .replace(/./g, char =>
+ String.fromCodePoint(127397 + char.charCodeAt(0))
+ );
+ }
+
return displayInfo;
}
diff --git a/src/lib/services/UserIdentityService.ts b/src/lib/services/UserIdentityService.ts
index 663d512..0d07b31 100644
--- a/src/lib/services/UserIdentityService.ts
+++ b/src/lib/services/UserIdentityService.ts
@@ -9,6 +9,7 @@ import { MessageService } from './MessageService';
import messageManager from '@/lib/waku';
import { localDatabase } from '@/lib/database/LocalDatabase';
import { WalletManager } from '@/lib/wallet';
+import { getVerification } from '../zkPassport';
export interface UserIdentity {
address: string;
@@ -393,6 +394,7 @@ export class UserIdentityService {
displayPreference,
lastUpdated: timestamp,
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
+ identityProviders: []
};
}
@@ -711,4 +713,187 @@ export class UserIdentityService {
return `${address.slice(0, 6)}...${address.slice(-4)}`;
}
+
+ /**
+ * Get ZKPassport claims with multi-layer cache support
+ */
+ async getZKPassportClaims(address: string): Promise {
+ // 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 {
+ return this.resolveZKPassportClaims(address);
+ }
+
+ /**
+ * Resolve ZKPassport claims from blockchain contract with TTL caching
+ */
+ private async resolveZKPassportClaims(address: string): Promise {
+ 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