From 66802d7d78b7bf7d4759ccd2e09ad09135192177 Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Thu, 25 Sep 2025 15:39:54 +0530 Subject: [PATCH] fix: hydration interface --- app/package.json | 6 +- app/src/components/Header.tsx | 27 +- app/src/components/ui/delegation-step.tsx | 2 +- app/src/components/ui/verification-step.tsx | 66 +- app/src/components/ui/wallet-dialog.tsx | 2 +- app/src/pages/ProfilePage.tsx | 13 - package-lock.json | 2 +- .../core/src/lib/database/LocalDatabase.ts | 4 +- .../core/src/lib/services/MessageService.ts | 2 + packages/core/src/lib/services/Ordinals.ts | 4 + .../src/lib/services/UserIdentityService.ts | 588 +++++++----------- .../src/lib/waku/core/ReliableMessaging.ts | 73 +-- packages/core/src/lib/waku/index.ts | 115 ++-- .../src/lib/waku/services/MessageService.ts | 32 +- packages/core/src/lib/wallet/index.ts | 88 +-- packages/core/src/types/waku.ts | 1 + packages/react/src/v1/hooks/useAuth.ts | 52 +- packages/react/src/v1/hooks/useUserDisplay.ts | 54 +- .../react/src/v1/provider/StoreWiring.tsx | 72 +-- 19 files changed, 483 insertions(+), 720 deletions(-) diff --git a/app/package.json b/app/package.json index 5140dd6..382af2b 100644 --- a/app/package.json +++ b/app/package.json @@ -15,11 +15,11 @@ "test:ui": "vitest --ui" }, "dependencies": { - "@opchan/react": "file:../packages/react", - "@opchan/core": "file:../packages/core", "@hookform/resolvers": "^3.9.0", "@noble/ed25519": "^2.2.3", "@noble/hashes": "^1.8.0", + "@opchan/core": "file:../packages/core", + "@opchan/react": "file:../packages/react", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-aspect-ratio": "^1.1.0", @@ -91,7 +91,7 @@ "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@types/uuid": "^10.0.0", - "@vitejs/plugin-react-swc": "^3.5.0", + "@vitejs/plugin-react-swc": "^3.11.0", "@vitest/ui": "^3.2.4", "autoprefixer": "^10.4.20", "eslint": "^9.9.0", diff --git a/app/src/components/Header.tsx b/app/src/components/Header.tsx index 77797e7..e23812b 100644 --- a/app/src/components/Header.tsx +++ b/app/src/components/Header.tsx @@ -61,22 +61,9 @@ const Header = () => { const isConnected = bitcoinAccount.isConnected || ethereumAccount.isConnected; - - - // Use currentUser address (which has ENS details) instead of raw AppKit address - const address = currentUser?.address || (isConnected - ? bitcoinAccount.isConnected - ? bitcoinAccount.address - : ethereumAccount.address - : undefined); - const [walletWizardOpen, setWalletWizardOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); - useEffect(() => { - console.log({currentUser}) - - }, [currentUser]) // Use LocalDatabase to persist wizard state across navigation const getHasShownWizard = async (): Promise => { @@ -142,6 +129,11 @@ const Header = () => { } }; + + useEffect(() => { + console.log('currentUser', currentUser) + }, [currentUser]) + const getStatusIcon = () => { if (!isConnected) return ; @@ -251,15 +243,6 @@ const Header = () => { align="end" className="w-56 bg-black/95 border-cyber-muted/30" > -
-
- {currentUser?.displayName} -
-
- {address?.slice(0, 8)}...{address?.slice(-4)} -
-
- -
{currentUser.address}
+
{currentUser.displayName}
)} diff --git a/app/src/components/ui/verification-step.tsx b/app/src/components/ui/verification-step.tsx index 0d2e1b6..3ef55ea 100644 --- a/app/src/components/ui/verification-step.tsx +++ b/app/src/components/ui/verification-step.tsx @@ -10,7 +10,6 @@ import { } from 'lucide-react'; import { useAuth } from '@/hooks'; import { EVerificationStatus } from '@opchan/core'; -import { useAppKitAccount } from '@reown/appkit/react'; import { OrdinalDetails, EnsDetails } from '@opchan/core'; interface VerificationStepProps { @@ -26,19 +25,7 @@ export function VerificationStep({ isLoading, setIsLoading, }: VerificationStepProps) { - const { currentUser, verificationStatus, isAuthenticating, verifyOwnership } = useAuth(); - - // Get account info to determine wallet type - const bitcoinAccount = useAppKitAccount({ namespace: 'bip122' }); - const ethereumAccount = useAppKitAccount({ namespace: 'eip155' }); - - const isBitcoinConnected = bitcoinAccount.isConnected; - const isEthereumConnected = ethereumAccount.isConnected; - const walletType = isBitcoinConnected - ? 'bitcoin' - : isEthereumConnected - ? 'ethereum' - : undefined; + const { currentUser, verifyOwnership } = useAuth(); const [verificationResult, setVerificationResult] = React.useState<{ success: boolean; @@ -52,24 +39,17 @@ export function VerificationStep({ verificationResult?.success && verificationResult.message.includes('Checking ownership') ) { - // Check if actual ownership was verified - // Treat centralized verification status as source of truth - const isOwnerVerified = - verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED; - const hasOwnership = - walletType === 'bitcoin' - ? isOwnerVerified && !!currentUser?.ordinalDetails - : isOwnerVerified && !!currentUser?.ensDetails; + const hasOwnership = currentUser?.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED; if (hasOwnership) { setVerificationResult({ success: true, message: - walletType === 'bitcoin' + currentUser?.walletType === 'bitcoin' ? 'Ordinal ownership verified successfully!' : 'ENS ownership verified successfully!', details: - walletType === 'bitcoin' + currentUser?.walletType === 'bitcoin' ? currentUser?.ordinalDetails : currentUser?.ensDetails, }); @@ -77,13 +57,13 @@ export function VerificationStep({ setVerificationResult({ success: false, message: - walletType === 'bitcoin' + currentUser?.walletType === 'bitcoin' ? 'No Ordinal ownership found. You can still participate in the forum with your connected wallet!' : 'No ENS ownership found. You can still participate in the forum with your connected wallet!', }); } } - }, [currentUser, verificationResult, walletType, verificationStatus]); + }, [currentUser, verificationResult]); const handleVerify = async () => { console.log('🔘 Verify button clicked, currentUser:', currentUser); @@ -98,17 +78,15 @@ export function VerificationStep({ try { console.log('📞 Calling verifyWallet()...'); - const success = await verifyOwnership(); - console.log('📊 verifyWallet returned:', success); - - if (success) { + await verifyOwnership(); + if (currentUser?.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED) { // For now, just show success - the actual ownership check will be done // by the useEffect when the user state updates console.log('✅ Verification successful, setting result'); setVerificationResult({ success: true, message: - walletType === 'bitcoin' + currentUser?.walletType === 'bitcoin' ? 'Verification process completed. Checking ownership...' : 'Verification process completed. Checking ownership...', details: undefined, @@ -118,7 +96,7 @@ export function VerificationStep({ setVerificationResult({ success: false, message: - walletType === 'bitcoin' + currentUser?.walletType === 'bitcoin' ? 'No Ordinal ownership found. You can still participate in the forum with your connected wallet!' : 'No ENS ownership found. You can still participate in the forum with your connected wallet!', }); @@ -140,19 +118,19 @@ export function VerificationStep({ }; const getVerificationType = () => { - return walletType === 'bitcoin' ? 'Bitcoin Ordinal' : 'Ethereum ENS'; + return currentUser?.walletType === 'bitcoin' ? 'Bitcoin Ordinal' : 'Ethereum ENS'; }; const getVerificationIcon = () => { - return walletType === 'bitcoin' ? Bitcoin : Coins; + return currentUser?.walletType === 'bitcoin' ? Bitcoin : Coins; }; const getVerificationColor = () => { - return walletType === 'bitcoin' ? 'text-orange-500' : 'text-blue-500'; + return currentUser?.walletType === 'bitcoin' ? 'text-orange-500' : 'text-blue-500'; }; const getVerificationDescription = () => { - if (walletType === 'bitcoin') { + if (currentUser?.walletType === 'bitcoin') { return "Verify your Bitcoin Ordinal ownership to unlock premium features. If you don't own any Ordinals, you can still participate in the forum with your connected wallet."; } else { return "Verify your Ethereum ENS ownership to unlock premium features. If you don't own any ENS, you can still participate in the forum with your connected wallet."; @@ -194,7 +172,7 @@ export function VerificationStep({

{verificationResult.details && (
- {walletType === 'bitcoin' ? ( + {currentUser?.walletType === 'bitcoin' ? (

Ordinal ID:{' '} {typeof verificationResult.details === 'object' && @@ -231,7 +209,7 @@ export function VerificationStep({ } // Show verification status - if (verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED) { + if (currentUser?.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED) { return (

@@ -247,8 +225,8 @@ export function VerificationStep({

{currentUser && (
- {walletType === 'bitcoin' &&

Ordinal ID: Verified

} - {walletType === 'ethereum' &&

ENS Name: Verified

} + {currentUser?.walletType === 'bitcoin' &&

Ordinal ID: Verified

} + {currentUser?.walletType === 'ethereum' &&

ENS Name: Verified

}
)}
@@ -294,7 +272,7 @@ export function VerificationStep({
    - {walletType === 'bitcoin' ? ( + {currentUser?.walletType === 'bitcoin' ? ( <>
  • • We'll check your wallet for Bitcoin Ordinal ownership
  • • If found, you'll get full posting and voting access
  • @@ -319,10 +297,10 @@ export function VerificationStep({
    diff --git a/app/src/components/ui/wallet-dialog.tsx b/app/src/components/ui/wallet-dialog.tsx index 4b8a1a0..6aa424c 100644 --- a/app/src/components/ui/wallet-dialog.tsx +++ b/app/src/components/ui/wallet-dialog.tsx @@ -194,7 +194,7 @@ export function WalletConnectionDialog({

    Address:

    - {activeAddress} + {activeAddress ? `${activeAddress.slice(0, 6)}...${activeAddress.slice(-4)}` : ''}

    diff --git a/app/src/pages/ProfilePage.tsx b/app/src/pages/ProfilePage.tsx index b1a647d..66caad9 100644 --- a/app/src/pages/ProfilePage.tsx +++ b/app/src/pages/ProfilePage.tsx @@ -41,20 +41,7 @@ export default function ProfilePage() { // Get current user from auth context for the address const { currentUser, delegationInfo } = useAuth(); - const address = currentUser?.address; - // Debug current user ENS info - console.log('📋 Profile page debug:', { - address, - currentUser: currentUser - ? { - address: currentUser.address, - callSign: currentUser.callSign, - ensDetails: currentUser.ensDetails, - verificationStatus: currentUser.verificationStatus, - } - : null - }); const [isEditing, setIsEditing] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); diff --git a/package-lock.json b/package-lock.json index eea6048..337950b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -103,7 +103,7 @@ "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@types/uuid": "^10.0.0", - "@vitejs/plugin-react-swc": "^3.5.0", + "@vitejs/plugin-react-swc": "^3.11.0", "@vitest/ui": "^3.2.4", "autoprefixer": "^10.4.20", "eslint": "^9.9.0", diff --git a/packages/core/src/lib/database/LocalDatabase.ts b/packages/core/src/lib/database/LocalDatabase.ts index ee2685c..3b9a794 100644 --- a/packages/core/src/lib/database/LocalDatabase.ts +++ b/packages/core/src/lib/database/LocalDatabase.ts @@ -235,8 +235,7 @@ export class LocalDatabase { displayPreference, lastUpdated: timestamp, verificationStatus: - existing?.verificationStatus ?? - EVerificationStatus.WALLET_UNCONNECTED, + existing?.verificationStatus } as UserIdentityCache[string]; this.cache.userIdentities[author] = nextRecord; @@ -658,6 +657,7 @@ export class LocalDatabase { displayPreference: EDisplayPreference.WALLET_ADDRESS, lastUpdated: 0, verificationStatus: EVerificationStatus.WALLET_UNCONNECTED, + displayName: address.slice(0, 6) + '...' + address.slice(-4), }; const merged: UserIdentityCache[string] = { diff --git a/packages/core/src/lib/services/MessageService.ts b/packages/core/src/lib/services/MessageService.ts index cd658a7..d651b60 100644 --- a/packages/core/src/lib/services/MessageService.ts +++ b/packages/core/src/lib/services/MessageService.ts @@ -24,6 +24,8 @@ export class MessageService implements MessageServiceInterface { this.delegationManager = delegationManager; } + // ===== PUBLIC METHODS ===== + /** * Sign and send a message to the Waku network */ diff --git a/packages/core/src/lib/services/Ordinals.ts b/packages/core/src/lib/services/Ordinals.ts index 5d6d31a..f540410 100644 --- a/packages/core/src/lib/services/Ordinals.ts +++ b/packages/core/src/lib/services/Ordinals.ts @@ -11,6 +11,8 @@ class Ordinals { this.ordiscan = ordiscan; } + // ===== PUBLIC STATIC METHODS ===== + static getInstance(): Ordinals { if (!Ordinals.instance) { const apiKey = environment.ordiscanApiKey; @@ -22,6 +24,8 @@ class Ordinals { return Ordinals.instance; } + // ===== PUBLIC INSTANCE METHODS ===== + /** * Get Ordinal details for a Bitcoin address */ diff --git a/packages/core/src/lib/services/UserIdentityService.ts b/packages/core/src/lib/services/UserIdentityService.ts index 47ee6b8..5c54323 100644 --- a/packages/core/src/lib/services/UserIdentityService.ts +++ b/packages/core/src/lib/services/UserIdentityService.ts @@ -6,9 +6,8 @@ import { UserIdentityCache, } from '../../types/waku'; import { MessageService } from './MessageService'; -import messageManager from '../waku'; import { localDatabase } from '../database/LocalDatabase'; -import { WalletManager } from '../wallet'; +import { walletManager, WalletManager } from '../wallet'; export interface UserIdentity { address: string; @@ -19,224 +18,58 @@ export interface UserIdentity { }; callSign?: string; displayPreference: EDisplayPreference; + displayName: string; lastUpdated: number; verificationStatus: EVerificationStatus; } export class UserIdentityService { private messageService: MessageService; - private userIdentityCache: UserIdentityCache = {}; private refreshListeners: Set<(address: string) => void> = new Set(); - private ensResolutionCache: Map> = new Map(); private debounceTimers: Map = new Map(); constructor(messageService: MessageService) { this.messageService = messageService; } + // ===== PUBLIC METHODS ===== + /** - * Get user identity from cache or resolve from sources with debouncing + * Unified identity getter. When opts.fresh === true, bypass caches. */ - async getUserIdentity(address: string): Promise { - // Debounce rapid calls to the same address + async getIdentity( + address: string, + opts?: { fresh?: boolean } + ): Promise { + if (opts?.fresh) { + return this.getUserIdentityFresh(address); + } + + // Debounce rapid calls for non-fresh path if (this.debounceTimers.has(address)) { clearTimeout(this.debounceTimers.get(address)!); } - + return new Promise((resolve) => { const timer = setTimeout(async () => { this.debounceTimers.delete(address); const result = await this.getUserIdentityInternal(address); resolve(result); - }, 100); // 100ms debounce - + }, 100); this.debounceTimers.set(address, timer); }); } - /** - * Internal method to get user identity without debouncing - */ - private async getUserIdentityInternal(address: string): Promise { - // Check internal cache first - if (this.userIdentityCache[address]) { - const cached = this.userIdentityCache[address]; - // Enrich with ENS name if missing and ETH address - if (!cached.ensName && address.startsWith('0x')) { - const ensName = await this.resolveENSName(address); - if (ensName) { - cached.ensName = ensName; - // Update verification status if ENS is found - if (cached.verificationStatus !== EVerificationStatus.ENS_ORDINAL_VERIFIED) { - cached.verificationStatus = EVerificationStatus.ENS_ORDINAL_VERIFIED; - // Persist the updated verification status to LocalDatabase - await localDatabase.upsertUserIdentity(address, { - ensName, - verificationStatus: EVerificationStatus.ENS_ORDINAL_VERIFIED, - lastUpdated: Date.now(), - }); - } - } - } - return { - address, - ensName: cached.ensName, - ordinalDetails: cached.ordinalDetails, - callSign: cached.callSign, - displayPreference: cached.displayPreference, - lastUpdated: cached.lastUpdated, - verificationStatus: this.mapVerificationStatus( - cached.verificationStatus - ), - }; - } - - // Check LocalDatabase first for persisted identities (warm start) - const persisted = localDatabase.cache.userIdentities[address]; - if (persisted) { - this.userIdentityCache[address] = { - ensName: persisted.ensName, - ordinalDetails: persisted.ordinalDetails, - callSign: persisted.callSign, - displayPreference: persisted.displayPreference, - lastUpdated: persisted.lastUpdated, - verificationStatus: persisted.verificationStatus, - }; - const result = { - address, - ensName: persisted.ensName, - ordinalDetails: persisted.ordinalDetails, - callSign: persisted.callSign, - displayPreference: persisted.displayPreference, - lastUpdated: persisted.lastUpdated, - verificationStatus: this.mapVerificationStatus( - persisted.verificationStatus - ), - } as UserIdentity; - // Enrich with ENS name if missing and ETH address - if (!result.ensName && address.startsWith('0x')) { - const ensName = await this.resolveENSName(address); - if (ensName) { - result.ensName = ensName; - this.userIdentityCache[address].ensName = ensName; - // Update verification status if ENS is found - if (result.verificationStatus !== EVerificationStatus.ENS_ORDINAL_VERIFIED) { - result.verificationStatus = EVerificationStatus.ENS_ORDINAL_VERIFIED; - this.userIdentityCache[address].verificationStatus = EVerificationStatus.ENS_ORDINAL_VERIFIED; - // Persist the updated verification status to LocalDatabase - await localDatabase.upsertUserIdentity(address, { - ensName, - verificationStatus: EVerificationStatus.ENS_ORDINAL_VERIFIED, - lastUpdated: Date.now(), - }); - } - } - } - return result; - } - - // Fallback: Check Waku message cache - const cacheServiceData = - messageManager.messageCache.userIdentities[address]; - - if (cacheServiceData) { - - // Store in internal cache for future use - this.userIdentityCache[address] = { - ensName: cacheServiceData.ensName, - ordinalDetails: cacheServiceData.ordinalDetails, - callSign: cacheServiceData.callSign, - displayPreference: cacheServiceData.displayPreference, - lastUpdated: cacheServiceData.lastUpdated, - verificationStatus: cacheServiceData.verificationStatus, - }; - - const result = { - address, - ensName: cacheServiceData.ensName, - ordinalDetails: cacheServiceData.ordinalDetails, - callSign: cacheServiceData.callSign, - displayPreference: cacheServiceData.displayPreference, - lastUpdated: cacheServiceData.lastUpdated, - verificationStatus: this.mapVerificationStatus( - cacheServiceData.verificationStatus - ), - } as UserIdentity; - // Enrich with ENS name if missing and ETH address - if (!result.ensName && address.startsWith('0x')) { - const ensName = await this.resolveENSName(address); - if (ensName) { - result.ensName = ensName; - this.userIdentityCache[address].ensName = ensName; - // Update verification status if ENS is found - if (result.verificationStatus !== EVerificationStatus.ENS_ORDINAL_VERIFIED) { - result.verificationStatus = EVerificationStatus.ENS_ORDINAL_VERIFIED; - this.userIdentityCache[address].verificationStatus = EVerificationStatus.ENS_ORDINAL_VERIFIED; - // Persist the updated verification status to LocalDatabase - await localDatabase.upsertUserIdentity(address, { - ensName, - verificationStatus: EVerificationStatus.ENS_ORDINAL_VERIFIED, - lastUpdated: Date.now(), - }); - } - } - } - return result; - } - - - // Try to resolve identity from various sources - const identity = await this.resolveUserIdentity(address); - if (identity) { - this.userIdentityCache[address] = { - ensName: identity.ensName, - ordinalDetails: identity.ordinalDetails, - callSign: identity.callSign, - displayPreference: identity.displayPreference, - lastUpdated: identity.lastUpdated, - verificationStatus: identity.verificationStatus, - }; - - // Persist the resolved identity to LocalDatabase for future use - await localDatabase.upsertUserIdentity(address, { - ensName: identity.ensName, - ordinalDetails: identity.ordinalDetails, - callSign: identity.callSign, - displayPreference: identity.displayPreference, - verificationStatus: identity.verificationStatus, - lastUpdated: identity.lastUpdated, - }); - } - - return identity; - } - /** * Force a fresh identity resolution bypassing caches and LocalDatabase. * Useful for explicit verification flows where we must hit upstream resolvers. */ async getUserIdentityFresh(address: string): Promise { const identity = await this.resolveUserIdentity(address); + if (identity) { - // Update in-memory cache to reflect the fresh result - this.userIdentityCache[address] = { - ensName: identity.ensName, - ordinalDetails: identity.ordinalDetails, - callSign: identity.callSign, - displayPreference: identity.displayPreference, - lastUpdated: identity.lastUpdated, - verificationStatus: identity.verificationStatus, - }; - // Persist the fresh identity to LocalDatabase - await localDatabase.upsertUserIdentity(address, { - ensName: identity.ensName, - ordinalDetails: identity.ordinalDetails, - callSign: identity.callSign, - displayPreference: identity.displayPreference, - verificationStatus: identity.verificationStatus, - lastUpdated: identity.lastUpdated, - }); + await localDatabase.upsertUserIdentity(address, identity); } return identity; } @@ -244,27 +77,32 @@ export class UserIdentityService { /** * Get all cached user identities */ - getAllUserIdentities(): UserIdentity[] { - return Object.entries(this.userIdentityCache).map(([address, cached]) => ({ + getAll(): UserIdentity[] { + return Object.entries(localDatabase.cache.userIdentities).map(([address, cached]) => ({ address, ensName: cached.ensName, ordinalDetails: cached.ordinalDetails, callSign: cached.callSign, displayPreference: cached.displayPreference, + displayName: this.getDisplayName(address), lastUpdated: cached.lastUpdated, verificationStatus: this.mapVerificationStatus(cached.verificationStatus), })); } /** - * Update user profile via Waku message + * New contract: return result and updated identity. */ - async updateUserProfile( + async updateProfile( address: string, - callSign: string | undefined, - displayPreference: EDisplayPreference - ): Promise { + updates: { callSign?: string; displayPreference?: EDisplayPreference } + ): Promise<{ ok: true; identity: UserIdentity } | { ok: false; error: Error }>{ try { + const callSign = updates.callSign?.trim() || undefined; + const displayPreference = + updates.displayPreference ?? + localDatabase.cache.userIdentities[address]?.displayPreference ?? + EDisplayPreference.WALLET_ADDRESS; const timestamp = Date.now(); const unsignedMessage: UnsignedUserProfileUpdateMessage = { @@ -274,66 +112,124 @@ export class UserIdentityService { author: address, displayPreference, }; - // Only include callSign if provided and non-empty - if (callSign && callSign.trim()) { - unsignedMessage.callSign = callSign.trim(); - } + if (callSign) unsignedMessage.callSign = callSign; - const signedMessage = - await this.messageService.signAndBroadcastMessage(unsignedMessage); + const signedMessage = await this.messageService.signAndBroadcastMessage(unsignedMessage); + if (!signedMessage) return { ok: false, error: new Error('Broadcast failed') }; - + const profileMessage: UserProfileUpdateMessage = { + id: unsignedMessage.id, + type: MessageType.USER_PROFILE_UPDATE, + timestamp, + author: address, + displayPreference, + signature: signedMessage.signature, + browserPubKey: signedMessage.browserPubKey, + delegationProof: signedMessage.delegationProof, + ...(callSign ? { callSign } : {}), + }; - // If broadcast was successful, immediately update local cache - if (signedMessage) { - this.updateUserIdentityFromMessage( - signedMessage as UserProfileUpdateMessage - ); + // Persist, notify + await localDatabase.applyMessage(profileMessage); + this.notifyRefreshListeners(address); - // Also update the local database cache immediately - if (this.userIdentityCache[address]) { - const updatedIdentity = { - ...this.userIdentityCache[address], - callSign: - callSign && callSign.trim() - ? callSign.trim() - : this.userIdentityCache[address].callSign, - displayPreference, - lastUpdated: timestamp, - }; - - localDatabase.cache.userIdentities[address] = updatedIdentity; - - // Persist to IndexedDB using the storeMessage method - const profileMessage: UserProfileUpdateMessage = { - id: unsignedMessage.id, - type: MessageType.USER_PROFILE_UPDATE, - timestamp, - author: address, - displayPreference, - signature: signedMessage.signature, - browserPubKey: signedMessage.browserPubKey, - delegationProof: signedMessage.delegationProof, - }; - if (callSign && callSign.trim()) { - profileMessage.callSign = callSign.trim(); - } - - // Apply the message to update the database - await localDatabase.applyMessage(profileMessage); - - // Notify listeners that the user identity has been updated - this.notifyRefreshListeners(address); - } - } - - return !!signedMessage; + const identity = await this.getIdentity(address); + if (!identity) return { ok: false, error: new Error('Identity unavailable') }; + return { ok: true, identity }; } catch (error) { - console.error('Failed to update user profile:', error); - return false; + return { ok: false, error: error as Error }; } } + /** + * Update user identity from Waku message + */ + updateUserIdentityFromMessage(message: UserProfileUpdateMessage): void { + // No-op: LocalDatabase.applyMessage mutates the canonical cache. + // We only need to notify listeners to refresh their local views. + this.notifyRefreshListeners(message.author); + } + + /** + * Refresh user identity (force re-resolution) + */ + async refreshIdentity(address: string): Promise { + await this.getIdentity(address, { fresh: true }); + } + + /** + * Clear user identity cache + */ + clearCache(): void { + this.debounceTimers.forEach(timer => clearTimeout(timer)); + this.debounceTimers.clear(); + } + + /** + * Subscribe with identity payload + */ + subscribe( + listener: (address: string, identity: UserIdentity | null) => void + ): () => void { + const wrapped = async (address: string) => { + const record = localDatabase.cache.userIdentities[address]; + const identity = record + ? this.buildUserIdentityFromRecord(address, record) + : await this.getIdentity(address); + listener(address, identity); + }; + this.refreshListeners.add(wrapped); + return () => this.refreshListeners.delete(wrapped); + } + + /** + * Get display name for user based on their preferences + */ + getDisplayName(address: string): string { + const identity = localDatabase.cache.userIdentities[address]; + if (!identity) { + return `${address.slice(0, 6)}...${address.slice(-4)}`; + } + + if ( + identity.displayPreference === EDisplayPreference.CALL_SIGN && + identity.callSign + ) { + return identity.callSign; + } + + if (identity.ensName) { + return identity.ensName; + } + + return `${address.slice(0, 6)}...${address.slice(-4)}`; + } + + // ===== PRIVATE METHODS ===== + + /** + * Internal method to get user identity without debouncing + */ + private async getUserIdentityInternal(address: string): Promise { + const record = this.getCachedRecord(address); + if (record) { + let identity = this.buildUserIdentityFromRecord(address, record); + identity = await this.ensureEnsEnriched(address, identity); + return identity; + } + + // Try to resolve identity from various sources + const resolved = await this.resolveUserIdentity(address); + if (resolved) { + // Persist the resolved identity to LocalDatabase for future use + await localDatabase.upsertUserIdentity(address, resolved); + + return resolved; + } + + return null; + } + /** * Resolve user identity from various sources */ @@ -350,19 +246,24 @@ export class UserIdentityService { const defaultDisplayPreference: EDisplayPreference = EDisplayPreference.WALLET_ADDRESS; - // Default verification status based on what we can resolve - let verificationStatus: EVerificationStatus = - EVerificationStatus.WALLET_UNCONNECTED; + const isWalletConnected = WalletManager.hasInstance() + ? walletManager.getInstance().isConnected() + : false; + let verificationStatus: EVerificationStatus; if (ensName || ordinalDetails) { verificationStatus = EVerificationStatus.ENS_ORDINAL_VERIFIED; + } else { + verificationStatus = isWalletConnected ? EVerificationStatus.WALLET_CONNECTED : EVerificationStatus.WALLET_UNCONNECTED; } + return { address, ensName: ensName || undefined, ordinalDetails: ordinalDetails || undefined, callSign: undefined, // Will be populated from Waku messages displayPreference: defaultDisplayPreference, + displayName: this.getDisplayName(address), lastUpdated: Date.now(), verificationStatus, }; @@ -380,47 +281,13 @@ export class UserIdentityService { return null; // Not an Ethereum address } - // Check if we already have a pending resolution for this address - if (this.ensResolutionCache.has(address)) { - return this.ensResolutionCache.get(address)!; - } - - // Check if we already have this resolved in the cache and it's recent - const cached = this.userIdentityCache[address]; - if (cached?.ensName && cached.lastUpdated > Date.now() - 300000) { // 5 minutes cache + // Prefer previously persisted ENS if recent + const cached = localDatabase.cache.userIdentities[address]; + if (cached?.ensName && cached.lastUpdated > Date.now() - 300000) { return cached.ensName; } - // Create and cache the promise - const resolutionPromise = this.doResolveENSName(address); - this.ensResolutionCache.set(address, resolutionPromise); - - // Clean up the cache after resolution (successful or failed) - resolutionPromise.finally(() => { - // Remove from cache after 60 seconds to allow for re-resolution if needed - setTimeout(() => { - this.ensResolutionCache.delete(address); - }, 60000); - }); - - return resolutionPromise; - } - - private async doResolveENSName(address: string): Promise { - try { - // Import the ENS resolver from wagmi - const { getEnsName } = await import('@wagmi/core'); - const { config } = await import('../wallet/config'); - - const ensName = await getEnsName(config, { - address: address as `0x${string}`, - }); - - return ensName || null; - } catch (error) { - console.error('Failed to resolve ENS name:', error); - return null; - } + return this.doResolveENSName(address); } /** @@ -451,34 +318,80 @@ export class UserIdentityService { } /** - * Update user identity from Waku message + * Notify all listeners that user identity data has changed */ - updateUserIdentityFromMessage(message: UserProfileUpdateMessage): void { - const { author, callSign, displayPreference, timestamp } = message; + private notifyRefreshListeners(address: string): void { + this.refreshListeners.forEach(listener => listener(address)); + } - if (!this.userIdentityCache[author]) { - // Create new identity entry if it doesn't exist - this.userIdentityCache[author] = { - ensName: undefined, - ordinalDetails: undefined, - callSign: undefined, - displayPreference, - lastUpdated: timestamp, - verificationStatus: EVerificationStatus.WALLET_UNCONNECTED, - }; + // ===== HELPER METHODS ===== + + /** + * Normalize a cached identity record into a strongly-typed UserIdentity + */ + private buildUserIdentityFromRecord( + address: string, + record: UserIdentityCache[string] + ): UserIdentity { + return { + address, + ensName: record.ensName, + ordinalDetails: record.ordinalDetails, + callSign: record.callSign, + displayPreference: record.displayPreference, + displayName: this.getDisplayName(address), + lastUpdated: record.lastUpdated, + verificationStatus: this.mapVerificationStatus(record.verificationStatus), + }; + } + + /** + * Retrieve a cached identity record from memory, LocalDatabase, or Waku cache + * and hydrate in-memory cache for subsequent accesses. + */ + private getCachedRecord( + address: string + ): UserIdentityCache[string] | null { + return localDatabase.cache.userIdentities[address] || null; + } + + /** + * Ensure ENS is enriched if missing. Persists updates and keeps caches in sync. + */ + private async ensureEnsEnriched( + address: string, + identity: UserIdentity + ): Promise { + if (!identity.ensName && address.startsWith('0x')) { + const ensName = await this.resolveENSName(address); + if (ensName) { + const updated: UserIdentity = { + ...identity, + ensName, + verificationStatus: EVerificationStatus.ENS_ORDINAL_VERIFIED, + lastUpdated: Date.now(), + }; + + await localDatabase.upsertUserIdentity(address, { + ensName, + verificationStatus: EVerificationStatus.ENS_ORDINAL_VERIFIED, + lastUpdated: updated.lastUpdated, + }); + + return updated; + } } + return identity; + } - // Update only if this message is newer - if (timestamp > this.userIdentityCache[author].lastUpdated) { - this.userIdentityCache[author] = { - ...this.userIdentityCache[author], - callSign, - displayPreference, - lastUpdated: timestamp, - }; - - // Notify listeners that the user identity has been updated - this.notifyRefreshListeners(author); + private async doResolveENSName(address: string): Promise { + try { + // Resolve ENS via centralized WalletManager helper + const ensName = await WalletManager.resolveENS(address); + return ensName || null; + } catch (error) { + console.error('Failed to resolve ENS name:', error); + return null; } } @@ -507,61 +420,4 @@ export class UserIdentityService { return EVerificationStatus.WALLET_UNCONNECTED; } } - - /** - * Refresh user identity (force re-resolution) - */ - async refreshUserIdentity(address: string): Promise { - delete this.userIdentityCache[address]; - await this.getUserIdentity(address); - } - - /** - * Clear user identity cache - */ - clearUserIdentityCache(): void { - this.userIdentityCache = {}; - this.ensResolutionCache.clear(); - // Clear all debounce timers - this.debounceTimers.forEach(timer => clearTimeout(timer)); - this.debounceTimers.clear(); - } - - /** - * Add a refresh listener for when user identity data changes - */ - addRefreshListener(listener: (address: string) => void): () => void { - this.refreshListeners.add(listener); - return () => this.refreshListeners.delete(listener); - } - - /** - * Notify all listeners that user identity data has changed - */ - private notifyRefreshListeners(address: string): void { - this.refreshListeners.forEach(listener => listener(address)); - } - - /** - * Get display name for user based on their preferences - */ - getDisplayName(address: string): string { - const identity = this.userIdentityCache[address]; - if (!identity) { - return `${address.slice(0, 6)}...${address.slice(-4)}`; - } - - if ( - identity.displayPreference === EDisplayPreference.CALL_SIGN && - identity.callSign - ) { - return identity.callSign; - } - - if (identity.ensName) { - return identity.ensName; - } - - return `${address.slice(0, 6)}...${address.slice(-4)}`; - } } diff --git a/packages/core/src/lib/waku/core/ReliableMessaging.ts b/packages/core/src/lib/waku/core/ReliableMessaging.ts index 6679380..5d84127 100644 --- a/packages/core/src/lib/waku/core/ReliableMessaging.ts +++ b/packages/core/src/lib/waku/core/ReliableMessaging.ts @@ -27,6 +27,44 @@ export class ReliableMessaging { this.initializeChannel(node); } + // ===== PUBLIC METHODS ===== + + public async sendMessage( + message: OpchanMessage, + statusCallback?: MessageStatusCallback + ) { + if (!this.channel) { + throw new Error('Reliable channel not initialized'); + } + + const encodedMessage = this.codecManager.encodeMessage(message); + const messageId = ReliableChannel.getMessageId(encodedMessage); + + if (statusCallback) { + this.messageCallbacks.set(messageId, statusCallback); + } + + try { + return this.channel.send(encodedMessage); + } catch (error) { + this.messageCallbacks.delete(messageId); + throw error; + } + } + + public onMessage(callback: IncomingMessageCallback): () => void { + this.incomingMessageCallbacks.add(callback); + return () => this.incomingMessageCallbacks.delete(callback); + } + + public cleanup(): void { + this.messageCallbacks.clear(); + this.incomingMessageCallbacks.clear(); + this.channel = null; + } + + // ===== PRIVATE METHODS ===== + private async initializeChannel(node: LightNode): Promise { const encoder = this.codecManager.getEncoder(); const decoder = this.codecManager.getDecoder(); @@ -53,7 +91,6 @@ export class ReliableMessaging { ): void { channel.addEventListener("message-received", event => { try { - console.log("received a message, processing...", event.detail); const wakuMessage = event.detail; if (wakuMessage.payload) { const opchanMessage = this.codecManager.decodeMessage( @@ -96,38 +133,4 @@ export class ReliableMessaging { } ); } - - public async sendMessage( - message: OpchanMessage, - statusCallback?: MessageStatusCallback - ) { - if (!this.channel) { - throw new Error('Reliable channel not initialized'); - } - - const encodedMessage = this.codecManager.encodeMessage(message); - const messageId = ReliableChannel.getMessageId(encodedMessage); - - if (statusCallback) { - this.messageCallbacks.set(messageId, statusCallback); - } - - try { - return this.channel.send(encodedMessage); - } catch (error) { - this.messageCallbacks.delete(messageId); - throw error; - } - } - - public onMessage(callback: IncomingMessageCallback): () => void { - this.incomingMessageCallbacks.add(callback); - return () => this.incomingMessageCallbacks.delete(callback); - } - - public cleanup(): void { - this.messageCallbacks.clear(); - this.incomingMessageCallbacks.clear(); - this.channel = null; - } } diff --git a/packages/core/src/lib/waku/index.ts b/packages/core/src/lib/waku/index.ts index c84e205..ec7e673 100644 --- a/packages/core/src/lib/waku/index.ts +++ b/packages/core/src/lib/waku/index.ts @@ -11,18 +11,64 @@ export type { HealthChangeCallback, MessageStatusCallback }; class MessageManager { private nodeManager: WakuNodeManager | null = null; - // LocalDatabase eliminates the need for CacheService private messageService: MessageService | null = null; private reliableMessaging: ReliableMessaging | null = null; constructor() {} + // ===== PUBLIC STATIC METHODS ===== + public static async create(): Promise { const manager = new MessageManager(); await manager.initialize(); return manager; } + // ===== PUBLIC INSTANCE METHODS ===== + + public async stop(): Promise { + this.cleanupReliableMessaging(); + this.messageService?.cleanup(); + await this.nodeManager?.stop(); + } + + public get isReady(): boolean { + return this.nodeManager?.isReady ?? false; + } + + public get currentHealth(): HealthStatus { + return this.nodeManager?.currentHealth ?? HealthStatus.Unhealthy; + } + + public onHealthChange(callback: HealthChangeCallback): () => void { + if (!this.nodeManager) { + throw new Error('Node manager not initialized'); + } + return this.nodeManager.onHealthChange(callback); + } + + //TODO: return event handlers? + public async sendMessage( + message: OpchanMessage, + statusCallback?: MessageStatusCallback + ): Promise { + if (!this.messageService) { + throw new Error('MessageManager not fully initialized'); + } + this.messageService.sendMessage(message, statusCallback); + } + + public onMessageReceived( + callback: (message: OpchanMessage) => void + ): () => void { + if (!this.messageService) { + throw new Error('MessageManager not fully initialized'); + } + return this.messageService.onMessageReceived(callback); + } + + // ===== PRIVATE METHODS ===== + private async initialize(): Promise { try { this.nodeManager = await WakuNodeManager.create(); @@ -72,54 +118,6 @@ class MessageManager { this.messageService?.updateReliableMessaging(null); } } - - public async stop(): Promise { - this.cleanupReliableMessaging(); - this.messageService?.cleanup(); - await this.nodeManager?.stop(); - } - - public get isReady(): boolean { - return this.nodeManager?.isReady ?? false; - } - - public get currentHealth(): HealthStatus { - return this.nodeManager?.currentHealth ?? HealthStatus.Unhealthy; - } - - public onHealthChange(callback: HealthChangeCallback): () => void { - if (!this.nodeManager) { - throw new Error('Node manager not initialized'); - } - return this.nodeManager.onHealthChange(callback); - } - - //TODO: return event handlers? - public async sendMessage( - message: OpchanMessage, - statusCallback?: MessageStatusCallback - ): Promise { - if (!this.messageService) { - throw new Error('MessageManager not fully initialized'); - } - this.messageService.sendMessage(message, statusCallback); - } - - public onMessageReceived( - callback: (message: OpchanMessage) => void - ): () => void { - if (!this.messageService) { - throw new Error('MessageManager not fully initialized'); - } - return this.messageService.onMessageReceived(callback); - } - - public get messageCache() { - if (!this.messageService) { - throw new Error('MessageManager not fully initialized'); - } - return this.messageService.messageCache; - } } // Create a default instance that can be used synchronously but initialized asynchronously @@ -129,6 +127,8 @@ export class DefaultMessageManager { private _pendingHealthSubscriptions: HealthChangeCallback[] = []; private _pendingMessageSubscriptions: ((message: any) => void)[] = []; + // ===== PUBLIC METHODS ===== + // Initialize the manager asynchronously async initialize(): Promise { if (!this._initPromise) { @@ -149,23 +149,6 @@ export class DefaultMessageManager { this._pendingMessageSubscriptions = []; } - // Get the messageCache (most common usage) - get messageCache() { - if (!this._instance) { - // Return empty cache structure for compatibility during initialization - return { - cells: {}, - posts: {}, - comments: {}, - votes: {}, - moderations: {}, - userIdentities: {}, - bookmarks: {}, - }; - } - return this._instance.messageCache; - } - // Proxy other common methods get isReady(): boolean { return this._instance?.isReady ?? false; diff --git a/packages/core/src/lib/waku/services/MessageService.ts b/packages/core/src/lib/waku/services/MessageService.ts index 51aa58d..f03a54c 100644 --- a/packages/core/src/lib/waku/services/MessageService.ts +++ b/packages/core/src/lib/waku/services/MessageService.ts @@ -19,18 +19,7 @@ export class MessageService { this.setupMessageHandling(); } - private setupMessageHandling(): void { - if (this.reliableMessaging) { - this.reliableMessaging.onMessage(async message => { - localDatabase.setSyncing(true); - const isNew = await localDatabase.updateCache(message); - // Defensive: clear pending on inbound message to avoid stuck state - localDatabase.clearPending(message.id); - localDatabase.setSyncing(false); - if (isNew) this.messageReceivedCallbacks.forEach(cb => cb(message)); - }); - } - } + // ===== PUBLIC METHODS ===== public async sendMessage( message: OpchanMessage, @@ -94,12 +83,23 @@ export class MessageService { this.setupMessageHandling(); } - public get messageCache() { - return localDatabase.cache; - } - public cleanup(): void { this.messageReceivedCallbacks.clear(); this.reliableMessaging?.cleanup(); } + + // ===== PRIVATE METHODS ===== + + private setupMessageHandling(): void { + if (this.reliableMessaging) { + this.reliableMessaging.onMessage(async message => { + localDatabase.setSyncing(true); + const isNew = await localDatabase.updateCache(message); + // Defensive: clear pending on inbound message to avoid stuck state + localDatabase.clearPending(message.id); + localDatabase.setSyncing(false); + if (isNew) this.messageReceivedCallbacks.forEach(cb => cb(message)); + }); + } + } } diff --git a/packages/core/src/lib/wallet/index.ts b/packages/core/src/lib/wallet/index.ts index 905c034..7241d96 100644 --- a/packages/core/src/lib/wallet/index.ts +++ b/packages/core/src/lib/wallet/index.ts @@ -39,6 +39,8 @@ export class WalletManager { } } + // ===== PUBLIC STATIC METHODS ===== + /** * Create or get the singleton instance */ @@ -53,6 +55,7 @@ export class WalletManager { bitcoinAccount, ethereumAccount ); + return WalletManager.instance; } @@ -65,6 +68,9 @@ export class WalletManager { 'WalletManager not initialized. Call WalletManager.create() first.' ); } + + + return WalletManager.instance; } @@ -111,6 +117,48 @@ export class WalletManager { } } + /** + * Verify a message signature against a wallet address + * @param message - The original message that was signed + * @param signature - The signature to verify + * @param walletAddress - The expected signer's address + * @param walletType - The type of wallet (bitcoin/ethereum) + * @returns Promise - True if signature is valid + */ + static async verifySignature( + message: string, + signature: string, + walletAddress: string, + walletType: 'bitcoin' | 'ethereum' + ): Promise { + try { + if (walletType === 'ethereum') { + return await verifyEthereumMessage(config, { + address: walletAddress as `0x${string}`, + message, + signature: signature as `0x${string}`, + }); + } else if (walletType === 'bitcoin') { + //TODO: implement bitcoin signature verification + return true; + } + + console.error( + 'WalletManager.verifySignature - unknown wallet type:', + walletType + ); + return false; + } catch (error) { + console.error( + 'WalletManager.verifySignature - error verifying signature:', + error + ); + return false; + } + } + + // ===== PUBLIC INSTANCE METHODS ===== + /** * Get the currently active wallet */ @@ -185,46 +233,6 @@ export class WalletManager { } } - /** - * Verify a message signature against a wallet address - * @param message - The original message that was signed - * @param signature - The signature to verify - * @param walletAddress - The expected signer's address - * @param walletType - The type of wallet (bitcoin/ethereum) - * @returns Promise - True if signature is valid - */ - static async verifySignature( - message: string, - signature: string, - walletAddress: string, - walletType: 'bitcoin' | 'ethereum' - ): Promise { - try { - if (walletType === 'ethereum') { - return await verifyEthereumMessage(config, { - address: walletAddress as `0x${string}`, - message, - signature: signature as `0x${string}`, - }); - } else if (walletType === 'bitcoin') { - //TODO: implement bitcoin signature verification - return true; - } - - console.error( - 'WalletManager.verifySignature - unknown wallet type:', - walletType - ); - return false; - } catch (error) { - console.error( - 'WalletManager.verifySignature - error verifying signature:', - error - ); - return false; - } - } - /** * Get comprehensive wallet info including ENS resolution for Ethereum */ diff --git a/packages/core/src/types/waku.ts b/packages/core/src/types/waku.ts index ac6d0b3..a36c6c3 100644 --- a/packages/core/src/types/waku.ts +++ b/packages/core/src/types/waku.ts @@ -177,5 +177,6 @@ export interface UserIdentityCache { displayPreference: EDisplayPreference; lastUpdated: number; verificationStatus: EVerificationStatus; + displayName: string; }; } diff --git a/packages/react/src/v1/hooks/useAuth.ts b/packages/react/src/v1/hooks/useAuth.ts index f9f2675..896587a 100644 --- a/packages/react/src/v1/hooks/useAuth.ts +++ b/packages/react/src/v1/hooks/useAuth.ts @@ -21,11 +21,12 @@ export function useAuth() { const verificationStatus = useOpchanStore(s => s.session.verificationStatus); const delegation = useOpchanStore(s => s.session.delegation); + const connect = React.useCallback(async (input: ConnectInput): Promise => { const baseUser: User = { address: input.address, walletType: input.walletType, - displayName: input.address, + displayName: input.address.slice(0, 6) + '...' + input.address.slice(-4), displayPreference: EDisplayPreference.WALLET_ADDRESS, verificationStatus: EVerificationStatus.WALLET_CONNECTED, lastChecked: Date.now(), @@ -34,15 +35,16 @@ export function useAuth() { try { await client.database.storeUser(baseUser); // Prime identity service so display name/ens are cached - await client.userIdentityService.getUserIdentity(baseUser.address); - + const identity = await client.userIdentityService.getIdentity(baseUser.address); + if (!identity) return false; setOpchanState(prev => ({ ...prev, session: { ...prev.session, - currentUser: baseUser, - verificationStatus: baseUser.verificationStatus, - delegation: prev.session.delegation, + currentUser: { + ...baseUser, + ...identity, + }, }, })); return true; @@ -68,33 +70,37 @@ export function useAuth() { }, [client]); const verifyOwnership = React.useCallback(async (): Promise => { + console.log('verifyOwnership') const user = currentUser; if (!user) return false; try { - const identity = await client.userIdentityService.getUserIdentityFresh(user.address); - const nextStatus = identity?.verificationStatus ?? EVerificationStatus.WALLET_CONNECTED; + const identity = await client.userIdentityService.getIdentity(user.address, { fresh: true }); + if (!identity) { + console.error('verifyOwnership failed', 'identity not found'); + return false; + } + + console.log({user, identity}) const updated: User = { ...user, - verificationStatus: nextStatus, - displayName: identity?.displayPreference === EDisplayPreference.CALL_SIGN ? identity.callSign! : identity!.ensName!, - ensDetails: identity?.ensName ? { ensName: identity.ensName } : undefined, - ordinalDetails: identity?.ordinalDetails, + ...identity, }; await client.database.storeUser(updated); await client.database.upsertUserIdentity(user.address, { + displayName: identity.displayName, ensName: identity?.ensName || undefined, ordinalDetails: identity?.ordinalDetails, - verificationStatus: nextStatus, + verificationStatus: identity.verificationStatus, lastUpdated: Date.now(), }); setOpchanState(prev => ({ ...prev, - session: { ...prev.session, currentUser: updated, verificationStatus: nextStatus }, + session: { ...prev.session, currentUser: updated, verificationStatus: identity.verificationStatus }, })); - return nextStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED; + return identity.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED; } catch (e) { console.error('verifyOwnership failed', e); return false; @@ -151,19 +157,19 @@ export function useAuth() { const user = currentUser; if (!user) return false; try { - const ok = await client.userIdentityService.updateUserProfile( + const res = await client.userIdentityService.updateProfile( user.address, - updates.callSign, - updates.displayPreference ?? user.displayPreference, + { + callSign: updates.callSign, + displayPreference: updates.displayPreference ?? user.displayPreference, + } ); - if (!ok) return false; + if (!res.ok) return false; - await client.userIdentityService.refreshUserIdentity(user.address); - const fresh = await client.userIdentityService.getUserIdentity(user.address); + const identity = res.identity; const updated: User = { ...user, - callSign: fresh?.callSign ?? user.callSign, - displayPreference: fresh?.displayPreference ?? user.displayPreference, + ...identity, }; await client.database.storeUser(updated); setOpchanState(prev => ({ ...prev, session: { ...prev.session, currentUser: updated } })); diff --git a/packages/react/src/v1/hooks/useUserDisplay.ts b/packages/react/src/v1/hooks/useUserDisplay.ts index 68166ea..9dbe80a 100644 --- a/packages/react/src/v1/hooks/useUserDisplay.ts +++ b/packages/react/src/v1/hooks/useUserDisplay.ts @@ -1,14 +1,9 @@ import React from 'react'; import { useClient } from '../context/ClientContext'; import { EDisplayPreference, EVerificationStatus } from '@opchan/core'; +import { UserIdentity } from '@opchan/core/dist/lib/services/UserIdentityService'; -export interface UserDisplayInfo { - displayName: string; - callSign: string | null; - ensName: string | null; - ordinalDetails: string | null; - verificationLevel: EVerificationStatus; - displayPreference: EDisplayPreference | null; +export interface UserDisplayInfo extends UserIdentity { isLoading: boolean; error: string | null; } @@ -21,20 +16,18 @@ export function useUserDisplay(address: string): UserDisplayInfo { const client = useClient(); const [displayInfo, setDisplayInfo] = React.useState({ + address, displayName: `${address.slice(0, 6)}...${address.slice(-4)}`, - callSign: null, - ensName: null, - ordinalDetails: null, - verificationLevel: EVerificationStatus.WALLET_UNCONNECTED, - displayPreference: null, + lastUpdated: 0, + callSign: undefined, + ensName: undefined, + ordinalDetails: undefined, + verificationStatus: EVerificationStatus.WALLET_UNCONNECTED, + displayPreference: EDisplayPreference.WALLET_ADDRESS, isLoading: true, error: null, }); - const getDisplayName = React.useCallback((addr: string) => { - return client.userIdentityService.getDisplayName(addr); - }, [client]); - // Initial load and refresh listener React.useEffect(() => { if (!address) return; @@ -43,28 +36,16 @@ export function useUserDisplay(address: string): UserDisplayInfo { const loadUserDisplay = async () => { try { - const identity = await client.userIdentityService.getUserIdentity(address); + const identity = await client.userIdentityService.getIdentity(address); if (cancelled) return; if (identity) { setDisplayInfo({ - displayName: getDisplayName(address), - callSign: identity.callSign || null, - ensName: identity.ensName || null, - ordinalDetails: identity.ordinalDetails?.ordinalDetails || null, - verificationLevel: identity.verificationStatus, - displayPreference: identity.displayPreference || null, + ...identity, isLoading: false, error: null, }); - } else { - setDisplayInfo(prev => ({ - ...prev, - displayName: getDisplayName(address), - isLoading: false, - error: null, - })); } } catch (error) { if (cancelled) return; @@ -80,21 +61,16 @@ export function useUserDisplay(address: string): UserDisplayInfo { loadUserDisplay(); // Subscribe to identity service refresh events - const unsubscribe = client.userIdentityService.addRefreshListener(async (changedAddress) => { + const unsubscribe = client.userIdentityService.subscribe(async (changedAddress) => { if (changedAddress !== address || cancelled) return; try { - const identity = await client.userIdentityService.getUserIdentity(address); + const identity = await client.userIdentityService.getIdentity(address); if (!identity || cancelled) return; setDisplayInfo(prev => ({ ...prev, - displayName: getDisplayName(address), - callSign: identity.callSign || null, - ensName: identity.ensName || null, - ordinalDetails: identity.ordinalDetails?.ordinalDetails || null, - verificationLevel: identity.verificationStatus, - displayPreference: identity.displayPreference || null, + ...identity, isLoading: false, error: null, })); @@ -117,7 +93,7 @@ export function useUserDisplay(address: string): UserDisplayInfo { // Ignore unsubscribe errors } }; - }, [address, client, getDisplayName]); + }, [address, client]); return displayInfo; } \ No newline at end of file diff --git a/packages/react/src/v1/provider/StoreWiring.tsx b/packages/react/src/v1/provider/StoreWiring.tsx index c00402f..3a5735f 100644 --- a/packages/react/src/v1/provider/StoreWiring.tsx +++ b/packages/react/src/v1/provider/StoreWiring.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useClient } from '../context/ClientContext'; import { setOpchanState, getOpchanState } from '../store/opchanStore'; import type { OpchanMessage, User } from '@opchan/core'; -import { EVerificationStatus, EDisplayPreference } from '@opchan/core'; +import { EVerificationStatus } from '@opchan/core'; export const StoreWiring: React.FC = () => { const client = useClient(); @@ -23,9 +23,9 @@ export const StoreWiring: React.FC = () => { ...prev, content: { ...prev.content, - cells: Object.values(cache.cells), - posts: Object.values(cache.posts), - comments: Object.values(cache.comments), + cells: Object.values(cache.cells), + posts: Object.values(cache.posts), + comments: Object.values(cache.comments), bookmarks: Object.values(cache.bookmarks), lastSync: client.database.getSyncState().lastSync, pendingIds: new Set(), @@ -41,35 +41,12 @@ export const StoreWiring: React.FC = () => { loadedUser?.walletType, ); - // If we have a loaded user, enrich it with latest identity for display fields - let enrichedUser: User | null = loadedUser ?? null; - if (loadedUser) { - try { - const identity = await client.userIdentityService.getUserIdentity(loadedUser.address); - if (identity) { - const displayName = identity.displayPreference === EDisplayPreference.CALL_SIGN - ? (identity.callSign || loadedUser.displayName) - : (identity.ensName || loadedUser.displayName); - enrichedUser = { - ...loadedUser, - callSign: identity.callSign ?? loadedUser.callSign, - displayPreference: identity.displayPreference ?? loadedUser.displayPreference, - displayName, - ensDetails: identity.ensName ? { ensName: identity.ensName } : loadedUser.ensDetails, - ordinalDetails: identity.ordinalDetails ?? loadedUser.ordinalDetails, - verificationStatus: identity.verificationStatus ?? loadedUser.verificationStatus, - }; - try { await client.database.storeUser(enrichedUser); } catch { /* ignore persist error */ } - } - } catch { /* ignore identity enrich error */ } - } - setOpchanState(prev => ({ ...prev, session: { - currentUser: enrichedUser, + currentUser: loadedUser, verificationStatus: - enrichedUser?.verificationStatus ?? EVerificationStatus.WALLET_UNCONNECTED, + loadedUser?.verificationStatus ?? EVerificationStatus.WALLET_UNCONNECTED, delegation: delegationStatus ?? null, }, })); @@ -118,30 +95,29 @@ export const StoreWiring: React.FC = () => { }); // Reactively update session.currentUser when identity refreshes for the active user - unsubIdentity = client.userIdentityService.addRefreshListener(async (address: string) => { + unsubIdentity = client.userIdentityService.subscribe(async (address: string) => { try { const { session } = getOpchanState(); const active = session.currentUser; - if (!active || active.address !== address) return; + if (!active || active.address !== address) { + return; + } - const identity = await client.userIdentityService.getUserIdentity(address); - if (!identity) return; - - const displayName = identity.displayPreference === EDisplayPreference.CALL_SIGN - ? (identity.callSign || active.displayName) - : (identity.ensName || active.displayName); + const identity = await client.userIdentityService.getIdentity(address); + if (!identity) { + return; + } const updated: User = { ...active, - callSign: identity.callSign ?? active.callSign, - displayPreference: identity.displayPreference ?? active.displayPreference, - displayName, - ensDetails: identity.ensName ? { ensName: identity.ensName } : active.ensDetails, - ordinalDetails: identity.ordinalDetails ?? active.ordinalDetails, - verificationStatus: identity.verificationStatus ?? active.verificationStatus, + ...identity, }; - try { await client.database.storeUser(updated); } catch { /* ignore persist error */ } + try { + await client.database.storeUser(updated); + } catch (persistErr) { + console.warn('[StoreWiring] Failed to persist updated user after identity refresh:', persistErr); + } setOpchanState(prev => ({ ...prev, @@ -157,7 +133,9 @@ export const StoreWiring: React.FC = () => { }); }; - hydrate().then(wire); + hydrate().then(() => { + wire(); + }); return () => { unsubHealth?.(); @@ -167,6 +145,4 @@ export const StoreWiring: React.FC = () => { }, [client]); return null; -}; - - +}; \ No newline at end of file