mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-02 12:53:10 +00:00
fix: hydration interface
This commit is contained in:
parent
de4acee1c1
commit
66802d7d78
@ -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",
|
||||
|
||||
@ -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<boolean> => {
|
||||
@ -142,6 +129,11 @@ const Header = () => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
console.log('currentUser', currentUser)
|
||||
}, [currentUser])
|
||||
|
||||
const getStatusIcon = () => {
|
||||
if (!isConnected) return <CircleSlash className="w-4 h-4" />;
|
||||
|
||||
@ -251,15 +243,6 @@ const Header = () => {
|
||||
align="end"
|
||||
className="w-56 bg-black/95 border-cyber-muted/30"
|
||||
>
|
||||
<div className="px-3 py-2 border-b border-cyber-muted/30">
|
||||
<div className="text-sm font-medium text-white">
|
||||
{currentUser?.displayName}
|
||||
</div>
|
||||
<div className="text-xs text-cyber-neutral">
|
||||
{address?.slice(0, 8)}...{address?.slice(-4)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
to="/profile"
|
||||
|
||||
@ -194,7 +194,7 @@ export function DelegationStep({
|
||||
{/* User Address */}
|
||||
{currentUser && (
|
||||
<div className="text-xs text-neutral-400">
|
||||
<div className="font-mono break-all">{currentUser.address}</div>
|
||||
<div className="font-mono break-all">{currentUser.displayName}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -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({
|
||||
</p>
|
||||
{verificationResult.details && (
|
||||
<div className="text-xs text-neutral-400">
|
||||
{walletType === 'bitcoin' ? (
|
||||
{currentUser?.walletType === 'bitcoin' ? (
|
||||
<p>
|
||||
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 (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-1 space-y-4">
|
||||
@ -247,8 +225,8 @@ export function VerificationStep({
|
||||
</p>
|
||||
{currentUser && (
|
||||
<div className="text-xs text-neutral-400">
|
||||
{walletType === 'bitcoin' && <p>Ordinal ID: Verified</p>}
|
||||
{walletType === 'ethereum' && <p>ENS Name: Verified</p>}
|
||||
{currentUser?.walletType === 'bitcoin' && <p>Ordinal ID: Verified</p>}
|
||||
{currentUser?.walletType === 'ethereum' && <p>ENS Name: Verified</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -294,7 +272,7 @@ export function VerificationStep({
|
||||
</span>
|
||||
</div>
|
||||
<ul className="text-xs text-neutral-400 space-y-1">
|
||||
{walletType === 'bitcoin' ? (
|
||||
{currentUser?.walletType === 'bitcoin' ? (
|
||||
<>
|
||||
<li>• We'll check your wallet for Bitcoin Ordinal ownership</li>
|
||||
<li>• If found, you'll get full posting and voting access</li>
|
||||
@ -319,10 +297,10 @@ export function VerificationStep({
|
||||
<div className="mt-auto space-y-3">
|
||||
<Button
|
||||
onClick={handleVerify}
|
||||
disabled={isLoading || isAuthenticating}
|
||||
disabled={isLoading}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
{isLoading || isAuthenticating ? (
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Verifying...
|
||||
@ -336,7 +314,7 @@ export function VerificationStep({
|
||||
onClick={onBack}
|
||||
variant="outline"
|
||||
className="w-full border-neutral-600 text-neutral-400 hover:bg-neutral-800"
|
||||
disabled={isLoading || isAuthenticating}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
@ -194,7 +194,7 @@ export function WalletConnectionDialog({
|
||||
</p>
|
||||
<p className="text-sm text-neutral-300 mb-2">Address:</p>
|
||||
<p className="text-xs font-mono text-neutral-400 break-all">
|
||||
{activeAddress}
|
||||
{activeAddress ? `${activeAddress.slice(0, 6)}...${activeAddress.slice(-4)}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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] = {
|
||||
|
||||
@ -24,6 +24,8 @@ export class MessageService implements MessageServiceInterface {
|
||||
this.delegationManager = delegationManager;
|
||||
}
|
||||
|
||||
// ===== PUBLIC METHODS =====
|
||||
|
||||
/**
|
||||
* Sign and send a message to the Waku network
|
||||
*/
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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<string, Promise<string | null>> = new Map();
|
||||
private debounceTimers: Map<string, NodeJS.Timeout> = 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<UserIdentity | null> {
|
||||
// Debounce rapid calls to the same address
|
||||
async getIdentity(
|
||||
address: string,
|
||||
opts?: { fresh?: boolean }
|
||||
): Promise<UserIdentity | null> {
|
||||
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<UserIdentity | null> {
|
||||
// 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<UserIdentity | null> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
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<UserIdentity | null> {
|
||||
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<string | null> {
|
||||
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<UserIdentity> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<MessageManager> {
|
||||
const manager = new MessageManager();
|
||||
await manager.initialize();
|
||||
return manager;
|
||||
}
|
||||
|
||||
// ===== PUBLIC INSTANCE METHODS =====
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
try {
|
||||
this.nodeManager = await WakuNodeManager.create();
|
||||
@ -72,54 +118,6 @@ class MessageManager {
|
||||
this.messageService?.updateReliableMessaging(null);
|
||||
}
|
||||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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;
|
||||
|
||||
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<boolean> - True if signature is valid
|
||||
*/
|
||||
static async verifySignature(
|
||||
message: string,
|
||||
signature: string,
|
||||
walletAddress: string,
|
||||
walletType: 'bitcoin' | 'ethereum'
|
||||
): Promise<boolean> {
|
||||
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<boolean> - True if signature is valid
|
||||
*/
|
||||
static async verifySignature(
|
||||
message: string,
|
||||
signature: string,
|
||||
walletAddress: string,
|
||||
walletType: 'bitcoin' | 'ethereum'
|
||||
): Promise<boolean> {
|
||||
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
|
||||
*/
|
||||
|
||||
@ -177,5 +177,6 @@ export interface UserIdentityCache {
|
||||
displayPreference: EDisplayPreference;
|
||||
lastUpdated: number;
|
||||
verificationStatus: EVerificationStatus;
|
||||
displayName: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -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<boolean> => {
|
||||
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<boolean> => {
|
||||
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 } }));
|
||||
|
||||
@ -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<UserDisplayInfo>({
|
||||
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;
|
||||
}
|
||||
@ -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<string>(),
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user