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