mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-07 15:23:05 +00:00
fix: hydration interface
This commit is contained in:
parent
de4acee1c1
commit
66802d7d78
@ -15,11 +15,11 @@
|
|||||||
"test:ui": "vitest --ui"
|
"test:ui": "vitest --ui"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opchan/react": "file:../packages/react",
|
|
||||||
"@opchan/core": "file:../packages/core",
|
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
"@noble/ed25519": "^2.2.3",
|
"@noble/ed25519": "^2.2.3",
|
||||||
"@noble/hashes": "^1.8.0",
|
"@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-accordion": "^1.2.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.0",
|
"@radix-ui/react-aspect-ratio": "^1.1.0",
|
||||||
@ -91,7 +91,7 @@
|
|||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/uuid": "^10.0.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",
|
"@vitest/ui": "^3.2.4",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^9.9.0",
|
"eslint": "^9.9.0",
|
||||||
|
|||||||
@ -61,22 +61,9 @@ const Header = () => {
|
|||||||
|
|
||||||
const isConnected = bitcoinAccount.isConnected || ethereumAccount.isConnected;
|
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 [walletWizardOpen, setWalletWizardOpen] = useState(false);
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log({currentUser})
|
|
||||||
|
|
||||||
}, [currentUser])
|
|
||||||
|
|
||||||
// Use LocalDatabase to persist wizard state across navigation
|
// Use LocalDatabase to persist wizard state across navigation
|
||||||
const getHasShownWizard = async (): Promise<boolean> => {
|
const getHasShownWizard = async (): Promise<boolean> => {
|
||||||
@ -142,6 +129,11 @@ const Header = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('currentUser', currentUser)
|
||||||
|
}, [currentUser])
|
||||||
|
|
||||||
const getStatusIcon = () => {
|
const getStatusIcon = () => {
|
||||||
if (!isConnected) return <CircleSlash className="w-4 h-4" />;
|
if (!isConnected) return <CircleSlash className="w-4 h-4" />;
|
||||||
|
|
||||||
@ -251,15 +243,6 @@ const Header = () => {
|
|||||||
align="end"
|
align="end"
|
||||||
className="w-56 bg-black/95 border-cyber-muted/30"
|
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>
|
<DropdownMenuItem asChild>
|
||||||
<Link
|
<Link
|
||||||
to="/profile"
|
to="/profile"
|
||||||
|
|||||||
@ -194,7 +194,7 @@ export function DelegationStep({
|
|||||||
{/* User Address */}
|
{/* User Address */}
|
||||||
{currentUser && (
|
{currentUser && (
|
||||||
<div className="text-xs text-neutral-400">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAuth } from '@/hooks';
|
import { useAuth } from '@/hooks';
|
||||||
import { EVerificationStatus } from '@opchan/core';
|
import { EVerificationStatus } from '@opchan/core';
|
||||||
import { useAppKitAccount } from '@reown/appkit/react';
|
|
||||||
import { OrdinalDetails, EnsDetails } from '@opchan/core';
|
import { OrdinalDetails, EnsDetails } from '@opchan/core';
|
||||||
|
|
||||||
interface VerificationStepProps {
|
interface VerificationStepProps {
|
||||||
@ -26,19 +25,7 @@ export function VerificationStep({
|
|||||||
isLoading,
|
isLoading,
|
||||||
setIsLoading,
|
setIsLoading,
|
||||||
}: VerificationStepProps) {
|
}: VerificationStepProps) {
|
||||||
const { currentUser, verificationStatus, isAuthenticating, verifyOwnership } = useAuth();
|
const { currentUser, 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 [verificationResult, setVerificationResult] = React.useState<{
|
const [verificationResult, setVerificationResult] = React.useState<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@ -52,24 +39,17 @@ export function VerificationStep({
|
|||||||
verificationResult?.success &&
|
verificationResult?.success &&
|
||||||
verificationResult.message.includes('Checking ownership')
|
verificationResult.message.includes('Checking ownership')
|
||||||
) {
|
) {
|
||||||
// Check if actual ownership was verified
|
const hasOwnership = currentUser?.verificationStatus === EVerificationStatus.ENS_ORDINAL_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;
|
|
||||||
|
|
||||||
if (hasOwnership) {
|
if (hasOwnership) {
|
||||||
setVerificationResult({
|
setVerificationResult({
|
||||||
success: true,
|
success: true,
|
||||||
message:
|
message:
|
||||||
walletType === 'bitcoin'
|
currentUser?.walletType === 'bitcoin'
|
||||||
? 'Ordinal ownership verified successfully!'
|
? 'Ordinal ownership verified successfully!'
|
||||||
: 'ENS ownership verified successfully!',
|
: 'ENS ownership verified successfully!',
|
||||||
details:
|
details:
|
||||||
walletType === 'bitcoin'
|
currentUser?.walletType === 'bitcoin'
|
||||||
? currentUser?.ordinalDetails
|
? currentUser?.ordinalDetails
|
||||||
: currentUser?.ensDetails,
|
: currentUser?.ensDetails,
|
||||||
});
|
});
|
||||||
@ -77,13 +57,13 @@ export function VerificationStep({
|
|||||||
setVerificationResult({
|
setVerificationResult({
|
||||||
success: false,
|
success: false,
|
||||||
message:
|
message:
|
||||||
walletType === 'bitcoin'
|
currentUser?.walletType === 'bitcoin'
|
||||||
? 'No Ordinal ownership found. You can still participate in the forum with your connected wallet!'
|
? '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!',
|
: 'No ENS ownership found. You can still participate in the forum with your connected wallet!',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [currentUser, verificationResult, walletType, verificationStatus]);
|
}, [currentUser, verificationResult]);
|
||||||
|
|
||||||
const handleVerify = async () => {
|
const handleVerify = async () => {
|
||||||
console.log('🔘 Verify button clicked, currentUser:', currentUser);
|
console.log('🔘 Verify button clicked, currentUser:', currentUser);
|
||||||
@ -98,17 +78,15 @@ export function VerificationStep({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('📞 Calling verifyWallet()...');
|
console.log('📞 Calling verifyWallet()...');
|
||||||
const success = await verifyOwnership();
|
await verifyOwnership();
|
||||||
console.log('📊 verifyWallet returned:', success);
|
if (currentUser?.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED) {
|
||||||
|
|
||||||
if (success) {
|
|
||||||
// For now, just show success - the actual ownership check will be done
|
// For now, just show success - the actual ownership check will be done
|
||||||
// by the useEffect when the user state updates
|
// by the useEffect when the user state updates
|
||||||
console.log('✅ Verification successful, setting result');
|
console.log('✅ Verification successful, setting result');
|
||||||
setVerificationResult({
|
setVerificationResult({
|
||||||
success: true,
|
success: true,
|
||||||
message:
|
message:
|
||||||
walletType === 'bitcoin'
|
currentUser?.walletType === 'bitcoin'
|
||||||
? 'Verification process completed. Checking ownership...'
|
? 'Verification process completed. Checking ownership...'
|
||||||
: 'Verification process completed. Checking ownership...',
|
: 'Verification process completed. Checking ownership...',
|
||||||
details: undefined,
|
details: undefined,
|
||||||
@ -118,7 +96,7 @@ export function VerificationStep({
|
|||||||
setVerificationResult({
|
setVerificationResult({
|
||||||
success: false,
|
success: false,
|
||||||
message:
|
message:
|
||||||
walletType === 'bitcoin'
|
currentUser?.walletType === 'bitcoin'
|
||||||
? 'No Ordinal ownership found. You can still participate in the forum with your connected wallet!'
|
? '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!',
|
: 'No ENS ownership found. You can still participate in the forum with your connected wallet!',
|
||||||
});
|
});
|
||||||
@ -140,19 +118,19 @@ export function VerificationStep({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getVerificationType = () => {
|
const getVerificationType = () => {
|
||||||
return walletType === 'bitcoin' ? 'Bitcoin Ordinal' : 'Ethereum ENS';
|
return currentUser?.walletType === 'bitcoin' ? 'Bitcoin Ordinal' : 'Ethereum ENS';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getVerificationIcon = () => {
|
const getVerificationIcon = () => {
|
||||||
return walletType === 'bitcoin' ? Bitcoin : Coins;
|
return currentUser?.walletType === 'bitcoin' ? Bitcoin : Coins;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getVerificationColor = () => {
|
const getVerificationColor = () => {
|
||||||
return walletType === 'bitcoin' ? 'text-orange-500' : 'text-blue-500';
|
return currentUser?.walletType === 'bitcoin' ? 'text-orange-500' : 'text-blue-500';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getVerificationDescription = () => {
|
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.";
|
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 {
|
} 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.";
|
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>
|
</p>
|
||||||
{verificationResult.details && (
|
{verificationResult.details && (
|
||||||
<div className="text-xs text-neutral-400">
|
<div className="text-xs text-neutral-400">
|
||||||
{walletType === 'bitcoin' ? (
|
{currentUser?.walletType === 'bitcoin' ? (
|
||||||
<p>
|
<p>
|
||||||
Ordinal ID:{' '}
|
Ordinal ID:{' '}
|
||||||
{typeof verificationResult.details === 'object' &&
|
{typeof verificationResult.details === 'object' &&
|
||||||
@ -231,7 +209,7 @@ export function VerificationStep({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show verification status
|
// Show verification status
|
||||||
if (verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED) {
|
if (currentUser?.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex-1 space-y-4">
|
<div className="flex-1 space-y-4">
|
||||||
@ -247,8 +225,8 @@ export function VerificationStep({
|
|||||||
</p>
|
</p>
|
||||||
{currentUser && (
|
{currentUser && (
|
||||||
<div className="text-xs text-neutral-400">
|
<div className="text-xs text-neutral-400">
|
||||||
{walletType === 'bitcoin' && <p>Ordinal ID: Verified</p>}
|
{currentUser?.walletType === 'bitcoin' && <p>Ordinal ID: Verified</p>}
|
||||||
{walletType === 'ethereum' && <p>ENS Name: Verified</p>}
|
{currentUser?.walletType === 'ethereum' && <p>ENS Name: Verified</p>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -294,7 +272,7 @@ export function VerificationStep({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ul className="text-xs text-neutral-400 space-y-1">
|
<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>• We'll check your wallet for Bitcoin Ordinal ownership</li>
|
||||||
<li>• If found, you'll get full posting and voting access</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">
|
<div className="mt-auto space-y-3">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleVerify}
|
onClick={handleVerify}
|
||||||
disabled={isLoading || isAuthenticating}
|
disabled={isLoading}
|
||||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
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" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
Verifying...
|
Verifying...
|
||||||
@ -336,7 +314,7 @@ export function VerificationStep({
|
|||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full border-neutral-600 text-neutral-400 hover:bg-neutral-800"
|
className="w-full border-neutral-600 text-neutral-400 hover:bg-neutral-800"
|
||||||
disabled={isLoading || isAuthenticating}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -194,7 +194,7 @@ export function WalletConnectionDialog({
|
|||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-neutral-300 mb-2">Address:</p>
|
<p className="text-sm text-neutral-300 mb-2">Address:</p>
|
||||||
<p className="text-xs font-mono text-neutral-400 break-all">
|
<p className="text-xs font-mono text-neutral-400 break-all">
|
||||||
{activeAddress}
|
{activeAddress ? `${activeAddress.slice(0, 6)}...${activeAddress.slice(-4)}` : ''}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -41,20 +41,7 @@ export default function ProfilePage() {
|
|||||||
|
|
||||||
// Get current user from auth context for the address
|
// Get current user from auth context for the address
|
||||||
const { currentUser, delegationInfo } = useAuth();
|
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 [isEditing, setIsEditing] = useState(false);
|
||||||
const [isSubmitting, setIsSubmitting] = 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": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/uuid": "^10.0.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",
|
"@vitest/ui": "^3.2.4",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^9.9.0",
|
"eslint": "^9.9.0",
|
||||||
|
|||||||
@ -235,8 +235,7 @@ export class LocalDatabase {
|
|||||||
displayPreference,
|
displayPreference,
|
||||||
lastUpdated: timestamp,
|
lastUpdated: timestamp,
|
||||||
verificationStatus:
|
verificationStatus:
|
||||||
existing?.verificationStatus ??
|
existing?.verificationStatus
|
||||||
EVerificationStatus.WALLET_UNCONNECTED,
|
|
||||||
} as UserIdentityCache[string];
|
} as UserIdentityCache[string];
|
||||||
|
|
||||||
this.cache.userIdentities[author] = nextRecord;
|
this.cache.userIdentities[author] = nextRecord;
|
||||||
@ -658,6 +657,7 @@ export class LocalDatabase {
|
|||||||
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
||||||
lastUpdated: 0,
|
lastUpdated: 0,
|
||||||
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
|
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
|
||||||
|
displayName: address.slice(0, 6) + '...' + address.slice(-4),
|
||||||
};
|
};
|
||||||
|
|
||||||
const merged: UserIdentityCache[string] = {
|
const merged: UserIdentityCache[string] = {
|
||||||
|
|||||||
@ -24,6 +24,8 @@ export class MessageService implements MessageServiceInterface {
|
|||||||
this.delegationManager = delegationManager;
|
this.delegationManager = delegationManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== PUBLIC METHODS =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sign and send a message to the Waku network
|
* Sign and send a message to the Waku network
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -11,6 +11,8 @@ class Ordinals {
|
|||||||
this.ordiscan = ordiscan;
|
this.ordiscan = ordiscan;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== PUBLIC STATIC METHODS =====
|
||||||
|
|
||||||
static getInstance(): Ordinals {
|
static getInstance(): Ordinals {
|
||||||
if (!Ordinals.instance) {
|
if (!Ordinals.instance) {
|
||||||
const apiKey = environment.ordiscanApiKey;
|
const apiKey = environment.ordiscanApiKey;
|
||||||
@ -22,6 +24,8 @@ class Ordinals {
|
|||||||
return Ordinals.instance;
|
return Ordinals.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== PUBLIC INSTANCE METHODS =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Ordinal details for a Bitcoin address
|
* Get Ordinal details for a Bitcoin address
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -6,9 +6,8 @@ import {
|
|||||||
UserIdentityCache,
|
UserIdentityCache,
|
||||||
} from '../../types/waku';
|
} from '../../types/waku';
|
||||||
import { MessageService } from './MessageService';
|
import { MessageService } from './MessageService';
|
||||||
import messageManager from '../waku';
|
|
||||||
import { localDatabase } from '../database/LocalDatabase';
|
import { localDatabase } from '../database/LocalDatabase';
|
||||||
import { WalletManager } from '../wallet';
|
import { walletManager, WalletManager } from '../wallet';
|
||||||
|
|
||||||
export interface UserIdentity {
|
export interface UserIdentity {
|
||||||
address: string;
|
address: string;
|
||||||
@ -19,224 +18,58 @@ export interface UserIdentity {
|
|||||||
};
|
};
|
||||||
callSign?: string;
|
callSign?: string;
|
||||||
displayPreference: EDisplayPreference;
|
displayPreference: EDisplayPreference;
|
||||||
|
displayName: string;
|
||||||
lastUpdated: number;
|
lastUpdated: number;
|
||||||
verificationStatus: EVerificationStatus;
|
verificationStatus: EVerificationStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserIdentityService {
|
export class UserIdentityService {
|
||||||
private messageService: MessageService;
|
private messageService: MessageService;
|
||||||
private userIdentityCache: UserIdentityCache = {};
|
|
||||||
private refreshListeners: Set<(address: string) => void> = new Set();
|
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();
|
private debounceTimers: Map<string, NodeJS.Timeout> = new Map();
|
||||||
|
|
||||||
constructor(messageService: MessageService) {
|
constructor(messageService: MessageService) {
|
||||||
this.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> {
|
async getIdentity(
|
||||||
// Debounce rapid calls to the same address
|
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)) {
|
if (this.debounceTimers.has(address)) {
|
||||||
clearTimeout(this.debounceTimers.get(address)!);
|
clearTimeout(this.debounceTimers.get(address)!);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const timer = setTimeout(async () => {
|
const timer = setTimeout(async () => {
|
||||||
this.debounceTimers.delete(address);
|
this.debounceTimers.delete(address);
|
||||||
const result = await this.getUserIdentityInternal(address);
|
const result = await this.getUserIdentityInternal(address);
|
||||||
resolve(result);
|
resolve(result);
|
||||||
}, 100); // 100ms debounce
|
}, 100);
|
||||||
|
|
||||||
this.debounceTimers.set(address, timer);
|
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.
|
* Force a fresh identity resolution bypassing caches and LocalDatabase.
|
||||||
* Useful for explicit verification flows where we must hit upstream resolvers.
|
* Useful for explicit verification flows where we must hit upstream resolvers.
|
||||||
*/
|
*/
|
||||||
async getUserIdentityFresh(address: string): Promise<UserIdentity | null> {
|
async getUserIdentityFresh(address: string): Promise<UserIdentity | null> {
|
||||||
const identity = await this.resolveUserIdentity(address);
|
const identity = await this.resolveUserIdentity(address);
|
||||||
|
|
||||||
if (identity) {
|
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
|
// Persist the fresh identity to LocalDatabase
|
||||||
await localDatabase.upsertUserIdentity(address, {
|
await localDatabase.upsertUserIdentity(address, identity);
|
||||||
ensName: identity.ensName,
|
|
||||||
ordinalDetails: identity.ordinalDetails,
|
|
||||||
callSign: identity.callSign,
|
|
||||||
displayPreference: identity.displayPreference,
|
|
||||||
verificationStatus: identity.verificationStatus,
|
|
||||||
lastUpdated: identity.lastUpdated,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return identity;
|
return identity;
|
||||||
}
|
}
|
||||||
@ -244,27 +77,32 @@ export class UserIdentityService {
|
|||||||
/**
|
/**
|
||||||
* Get all cached user identities
|
* Get all cached user identities
|
||||||
*/
|
*/
|
||||||
getAllUserIdentities(): UserIdentity[] {
|
getAll(): UserIdentity[] {
|
||||||
return Object.entries(this.userIdentityCache).map(([address, cached]) => ({
|
return Object.entries(localDatabase.cache.userIdentities).map(([address, cached]) => ({
|
||||||
address,
|
address,
|
||||||
ensName: cached.ensName,
|
ensName: cached.ensName,
|
||||||
ordinalDetails: cached.ordinalDetails,
|
ordinalDetails: cached.ordinalDetails,
|
||||||
callSign: cached.callSign,
|
callSign: cached.callSign,
|
||||||
displayPreference: cached.displayPreference,
|
displayPreference: cached.displayPreference,
|
||||||
|
displayName: this.getDisplayName(address),
|
||||||
lastUpdated: cached.lastUpdated,
|
lastUpdated: cached.lastUpdated,
|
||||||
verificationStatus: this.mapVerificationStatus(cached.verificationStatus),
|
verificationStatus: this.mapVerificationStatus(cached.verificationStatus),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update user profile via Waku message
|
* New contract: return result and updated identity.
|
||||||
*/
|
*/
|
||||||
async updateUserProfile(
|
async updateProfile(
|
||||||
address: string,
|
address: string,
|
||||||
callSign: string | undefined,
|
updates: { callSign?: string; displayPreference?: EDisplayPreference }
|
||||||
displayPreference: EDisplayPreference
|
): Promise<{ ok: true; identity: UserIdentity } | { ok: false; error: Error }>{
|
||||||
): Promise<boolean> {
|
|
||||||
try {
|
try {
|
||||||
|
const callSign = updates.callSign?.trim() || undefined;
|
||||||
|
const displayPreference =
|
||||||
|
updates.displayPreference ??
|
||||||
|
localDatabase.cache.userIdentities[address]?.displayPreference ??
|
||||||
|
EDisplayPreference.WALLET_ADDRESS;
|
||||||
|
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
const unsignedMessage: UnsignedUserProfileUpdateMessage = {
|
const unsignedMessage: UnsignedUserProfileUpdateMessage = {
|
||||||
@ -274,66 +112,124 @@ export class UserIdentityService {
|
|||||||
author: address,
|
author: address,
|
||||||
displayPreference,
|
displayPreference,
|
||||||
};
|
};
|
||||||
// Only include callSign if provided and non-empty
|
if (callSign) unsignedMessage.callSign = callSign;
|
||||||
if (callSign && callSign.trim()) {
|
|
||||||
unsignedMessage.callSign = callSign.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
const signedMessage =
|
const signedMessage = await this.messageService.signAndBroadcastMessage(unsignedMessage);
|
||||||
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
|
// Persist, notify
|
||||||
if (signedMessage) {
|
await localDatabase.applyMessage(profileMessage);
|
||||||
this.updateUserIdentityFromMessage(
|
this.notifyRefreshListeners(address);
|
||||||
signedMessage as UserProfileUpdateMessage
|
|
||||||
);
|
|
||||||
|
|
||||||
// Also update the local database cache immediately
|
const identity = await this.getIdentity(address);
|
||||||
if (this.userIdentityCache[address]) {
|
if (!identity) return { ok: false, error: new Error('Identity unavailable') };
|
||||||
const updatedIdentity = {
|
return { ok: true, identity };
|
||||||
...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;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update user profile:', error);
|
return { ok: false, error: error as Error };
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* Resolve user identity from various sources
|
||||||
*/
|
*/
|
||||||
@ -350,19 +246,24 @@ export class UserIdentityService {
|
|||||||
const defaultDisplayPreference: EDisplayPreference =
|
const defaultDisplayPreference: EDisplayPreference =
|
||||||
EDisplayPreference.WALLET_ADDRESS;
|
EDisplayPreference.WALLET_ADDRESS;
|
||||||
|
|
||||||
// Default verification status based on what we can resolve
|
const isWalletConnected = WalletManager.hasInstance()
|
||||||
let verificationStatus: EVerificationStatus =
|
? walletManager.getInstance().isConnected()
|
||||||
EVerificationStatus.WALLET_UNCONNECTED;
|
: false;
|
||||||
|
let verificationStatus: EVerificationStatus;
|
||||||
if (ensName || ordinalDetails) {
|
if (ensName || ordinalDetails) {
|
||||||
verificationStatus = EVerificationStatus.ENS_ORDINAL_VERIFIED;
|
verificationStatus = EVerificationStatus.ENS_ORDINAL_VERIFIED;
|
||||||
|
} else {
|
||||||
|
verificationStatus = isWalletConnected ? EVerificationStatus.WALLET_CONNECTED : EVerificationStatus.WALLET_UNCONNECTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
address,
|
address,
|
||||||
ensName: ensName || undefined,
|
ensName: ensName || undefined,
|
||||||
ordinalDetails: ordinalDetails || undefined,
|
ordinalDetails: ordinalDetails || undefined,
|
||||||
callSign: undefined, // Will be populated from Waku messages
|
callSign: undefined, // Will be populated from Waku messages
|
||||||
displayPreference: defaultDisplayPreference,
|
displayPreference: defaultDisplayPreference,
|
||||||
|
displayName: this.getDisplayName(address),
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
verificationStatus,
|
verificationStatus,
|
||||||
};
|
};
|
||||||
@ -380,47 +281,13 @@ export class UserIdentityService {
|
|||||||
return null; // Not an Ethereum address
|
return null; // Not an Ethereum address
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we already have a pending resolution for this address
|
// Prefer previously persisted ENS if recent
|
||||||
if (this.ensResolutionCache.has(address)) {
|
const cached = localDatabase.cache.userIdentities[address];
|
||||||
return this.ensResolutionCache.get(address)!;
|
if (cached?.ensName && cached.lastUpdated > Date.now() - 300000) {
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
return cached.ensName;
|
return cached.ensName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and cache the promise
|
return this.doResolveENSName(address);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -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 {
|
private notifyRefreshListeners(address: string): void {
|
||||||
const { author, callSign, displayPreference, timestamp } = message;
|
this.refreshListeners.forEach(listener => listener(address));
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.userIdentityCache[author]) {
|
// ===== HELPER METHODS =====
|
||||||
// Create new identity entry if it doesn't exist
|
|
||||||
this.userIdentityCache[author] = {
|
/**
|
||||||
ensName: undefined,
|
* Normalize a cached identity record into a strongly-typed UserIdentity
|
||||||
ordinalDetails: undefined,
|
*/
|
||||||
callSign: undefined,
|
private buildUserIdentityFromRecord(
|
||||||
displayPreference,
|
address: string,
|
||||||
lastUpdated: timestamp,
|
record: UserIdentityCache[string]
|
||||||
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
|
): 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
|
private async doResolveENSName(address: string): Promise<string | null> {
|
||||||
if (timestamp > this.userIdentityCache[author].lastUpdated) {
|
try {
|
||||||
this.userIdentityCache[author] = {
|
// Resolve ENS via centralized WalletManager helper
|
||||||
...this.userIdentityCache[author],
|
const ensName = await WalletManager.resolveENS(address);
|
||||||
callSign,
|
return ensName || null;
|
||||||
displayPreference,
|
} catch (error) {
|
||||||
lastUpdated: timestamp,
|
console.error('Failed to resolve ENS name:', error);
|
||||||
};
|
return null;
|
||||||
|
|
||||||
// Notify listeners that the user identity has been updated
|
|
||||||
this.notifyRefreshListeners(author);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -507,61 +420,4 @@ export class UserIdentityService {
|
|||||||
return EVerificationStatus.WALLET_UNCONNECTED;
|
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);
|
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> {
|
private async initializeChannel(node: LightNode): Promise<void> {
|
||||||
const encoder = this.codecManager.getEncoder();
|
const encoder = this.codecManager.getEncoder();
|
||||||
const decoder = this.codecManager.getDecoder();
|
const decoder = this.codecManager.getDecoder();
|
||||||
@ -53,7 +91,6 @@ export class ReliableMessaging {
|
|||||||
): void {
|
): void {
|
||||||
channel.addEventListener("message-received", event => {
|
channel.addEventListener("message-received", event => {
|
||||||
try {
|
try {
|
||||||
console.log("received a message, processing...", event.detail);
|
|
||||||
const wakuMessage = event.detail;
|
const wakuMessage = event.detail;
|
||||||
if (wakuMessage.payload) {
|
if (wakuMessage.payload) {
|
||||||
const opchanMessage = this.codecManager.decodeMessage(
|
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 {
|
class MessageManager {
|
||||||
private nodeManager: WakuNodeManager | null = null;
|
private nodeManager: WakuNodeManager | null = null;
|
||||||
// LocalDatabase eliminates the need for CacheService
|
|
||||||
private messageService: MessageService | null = null;
|
private messageService: MessageService | null = null;
|
||||||
private reliableMessaging: ReliableMessaging | null = null;
|
private reliableMessaging: ReliableMessaging | null = null;
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
|
// ===== PUBLIC STATIC METHODS =====
|
||||||
|
|
||||||
public static async create(): Promise<MessageManager> {
|
public static async create(): Promise<MessageManager> {
|
||||||
const manager = new MessageManager();
|
const manager = new MessageManager();
|
||||||
await manager.initialize();
|
await manager.initialize();
|
||||||
return manager;
|
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> {
|
private async initialize(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.nodeManager = await WakuNodeManager.create();
|
this.nodeManager = await WakuNodeManager.create();
|
||||||
@ -72,54 +118,6 @@ class MessageManager {
|
|||||||
this.messageService?.updateReliableMessaging(null);
|
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
|
// Create a default instance that can be used synchronously but initialized asynchronously
|
||||||
@ -129,6 +127,8 @@ export class DefaultMessageManager {
|
|||||||
private _pendingHealthSubscriptions: HealthChangeCallback[] = [];
|
private _pendingHealthSubscriptions: HealthChangeCallback[] = [];
|
||||||
private _pendingMessageSubscriptions: ((message: any) => void)[] = [];
|
private _pendingMessageSubscriptions: ((message: any) => void)[] = [];
|
||||||
|
|
||||||
|
// ===== PUBLIC METHODS =====
|
||||||
|
|
||||||
// Initialize the manager asynchronously
|
// Initialize the manager asynchronously
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
if (!this._initPromise) {
|
if (!this._initPromise) {
|
||||||
@ -149,23 +149,6 @@ export class DefaultMessageManager {
|
|||||||
this._pendingMessageSubscriptions = [];
|
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
|
// Proxy other common methods
|
||||||
get isReady(): boolean {
|
get isReady(): boolean {
|
||||||
return this._instance?.isReady ?? false;
|
return this._instance?.isReady ?? false;
|
||||||
|
|||||||
@ -19,18 +19,7 @@ export class MessageService {
|
|||||||
this.setupMessageHandling();
|
this.setupMessageHandling();
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupMessageHandling(): void {
|
// ===== PUBLIC METHODS =====
|
||||||
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 async sendMessage(
|
public async sendMessage(
|
||||||
message: OpchanMessage,
|
message: OpchanMessage,
|
||||||
@ -94,12 +83,23 @@ export class MessageService {
|
|||||||
this.setupMessageHandling();
|
this.setupMessageHandling();
|
||||||
}
|
}
|
||||||
|
|
||||||
public get messageCache() {
|
|
||||||
return localDatabase.cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
public cleanup(): void {
|
public cleanup(): void {
|
||||||
this.messageReceivedCallbacks.clear();
|
this.messageReceivedCallbacks.clear();
|
||||||
this.reliableMessaging?.cleanup();
|
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
|
* Create or get the singleton instance
|
||||||
*/
|
*/
|
||||||
@ -53,6 +55,7 @@ export class WalletManager {
|
|||||||
bitcoinAccount,
|
bitcoinAccount,
|
||||||
ethereumAccount
|
ethereumAccount
|
||||||
);
|
);
|
||||||
|
|
||||||
return WalletManager.instance;
|
return WalletManager.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,6 +68,9 @@ export class WalletManager {
|
|||||||
'WalletManager not initialized. Call WalletManager.create() first.'
|
'WalletManager not initialized. Call WalletManager.create() first.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return WalletManager.instance;
|
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
|
* 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
|
* Get comprehensive wallet info including ENS resolution for Ethereum
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -177,5 +177,6 @@ export interface UserIdentityCache {
|
|||||||
displayPreference: EDisplayPreference;
|
displayPreference: EDisplayPreference;
|
||||||
lastUpdated: number;
|
lastUpdated: number;
|
||||||
verificationStatus: EVerificationStatus;
|
verificationStatus: EVerificationStatus;
|
||||||
|
displayName: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,11 +21,12 @@ export function useAuth() {
|
|||||||
const verificationStatus = useOpchanStore(s => s.session.verificationStatus);
|
const verificationStatus = useOpchanStore(s => s.session.verificationStatus);
|
||||||
const delegation = useOpchanStore(s => s.session.delegation);
|
const delegation = useOpchanStore(s => s.session.delegation);
|
||||||
|
|
||||||
|
|
||||||
const connect = React.useCallback(async (input: ConnectInput): Promise<boolean> => {
|
const connect = React.useCallback(async (input: ConnectInput): Promise<boolean> => {
|
||||||
const baseUser: User = {
|
const baseUser: User = {
|
||||||
address: input.address,
|
address: input.address,
|
||||||
walletType: input.walletType,
|
walletType: input.walletType,
|
||||||
displayName: input.address,
|
displayName: input.address.slice(0, 6) + '...' + input.address.slice(-4),
|
||||||
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
||||||
verificationStatus: EVerificationStatus.WALLET_CONNECTED,
|
verificationStatus: EVerificationStatus.WALLET_CONNECTED,
|
||||||
lastChecked: Date.now(),
|
lastChecked: Date.now(),
|
||||||
@ -34,15 +35,16 @@ export function useAuth() {
|
|||||||
try {
|
try {
|
||||||
await client.database.storeUser(baseUser);
|
await client.database.storeUser(baseUser);
|
||||||
// Prime identity service so display name/ens are cached
|
// 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 => ({
|
setOpchanState(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
session: {
|
session: {
|
||||||
...prev.session,
|
...prev.session,
|
||||||
currentUser: baseUser,
|
currentUser: {
|
||||||
verificationStatus: baseUser.verificationStatus,
|
...baseUser,
|
||||||
delegation: prev.session.delegation,
|
...identity,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
return true;
|
return true;
|
||||||
@ -68,33 +70,37 @@ export function useAuth() {
|
|||||||
}, [client]);
|
}, [client]);
|
||||||
|
|
||||||
const verifyOwnership = React.useCallback(async (): Promise<boolean> => {
|
const verifyOwnership = React.useCallback(async (): Promise<boolean> => {
|
||||||
|
console.log('verifyOwnership')
|
||||||
const user = currentUser;
|
const user = currentUser;
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
try {
|
try {
|
||||||
const identity = await client.userIdentityService.getUserIdentityFresh(user.address);
|
const identity = await client.userIdentityService.getIdentity(user.address, { fresh: true });
|
||||||
const nextStatus = identity?.verificationStatus ?? EVerificationStatus.WALLET_CONNECTED;
|
if (!identity) {
|
||||||
|
console.error('verifyOwnership failed', 'identity not found');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log({user, identity})
|
||||||
|
|
||||||
const updated: User = {
|
const updated: User = {
|
||||||
...user,
|
...user,
|
||||||
verificationStatus: nextStatus,
|
...identity,
|
||||||
displayName: identity?.displayPreference === EDisplayPreference.CALL_SIGN ? identity.callSign! : identity!.ensName!,
|
|
||||||
ensDetails: identity?.ensName ? { ensName: identity.ensName } : undefined,
|
|
||||||
ordinalDetails: identity?.ordinalDetails,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await client.database.storeUser(updated);
|
await client.database.storeUser(updated);
|
||||||
await client.database.upsertUserIdentity(user.address, {
|
await client.database.upsertUserIdentity(user.address, {
|
||||||
|
displayName: identity.displayName,
|
||||||
ensName: identity?.ensName || undefined,
|
ensName: identity?.ensName || undefined,
|
||||||
ordinalDetails: identity?.ordinalDetails,
|
ordinalDetails: identity?.ordinalDetails,
|
||||||
verificationStatus: nextStatus,
|
verificationStatus: identity.verificationStatus,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
setOpchanState(prev => ({
|
setOpchanState(prev => ({
|
||||||
...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) {
|
} catch (e) {
|
||||||
console.error('verifyOwnership failed', e);
|
console.error('verifyOwnership failed', e);
|
||||||
return false;
|
return false;
|
||||||
@ -151,19 +157,19 @@ export function useAuth() {
|
|||||||
const user = currentUser;
|
const user = currentUser;
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
try {
|
try {
|
||||||
const ok = await client.userIdentityService.updateUserProfile(
|
const res = await client.userIdentityService.updateProfile(
|
||||||
user.address,
|
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 identity = res.identity;
|
||||||
const fresh = await client.userIdentityService.getUserIdentity(user.address);
|
|
||||||
const updated: User = {
|
const updated: User = {
|
||||||
...user,
|
...user,
|
||||||
callSign: fresh?.callSign ?? user.callSign,
|
...identity,
|
||||||
displayPreference: fresh?.displayPreference ?? user.displayPreference,
|
|
||||||
};
|
};
|
||||||
await client.database.storeUser(updated);
|
await client.database.storeUser(updated);
|
||||||
setOpchanState(prev => ({ ...prev, session: { ...prev.session, currentUser: updated } }));
|
setOpchanState(prev => ({ ...prev, session: { ...prev.session, currentUser: updated } }));
|
||||||
|
|||||||
@ -1,14 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useClient } from '../context/ClientContext';
|
import { useClient } from '../context/ClientContext';
|
||||||
import { EDisplayPreference, EVerificationStatus } from '@opchan/core';
|
import { EDisplayPreference, EVerificationStatus } from '@opchan/core';
|
||||||
|
import { UserIdentity } from '@opchan/core/dist/lib/services/UserIdentityService';
|
||||||
|
|
||||||
export interface UserDisplayInfo {
|
export interface UserDisplayInfo extends UserIdentity {
|
||||||
displayName: string;
|
|
||||||
callSign: string | null;
|
|
||||||
ensName: string | null;
|
|
||||||
ordinalDetails: string | null;
|
|
||||||
verificationLevel: EVerificationStatus;
|
|
||||||
displayPreference: EDisplayPreference | null;
|
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
@ -21,20 +16,18 @@ export function useUserDisplay(address: string): UserDisplayInfo {
|
|||||||
const client = useClient();
|
const client = useClient();
|
||||||
|
|
||||||
const [displayInfo, setDisplayInfo] = React.useState<UserDisplayInfo>({
|
const [displayInfo, setDisplayInfo] = React.useState<UserDisplayInfo>({
|
||||||
|
address,
|
||||||
displayName: `${address.slice(0, 6)}...${address.slice(-4)}`,
|
displayName: `${address.slice(0, 6)}...${address.slice(-4)}`,
|
||||||
callSign: null,
|
lastUpdated: 0,
|
||||||
ensName: null,
|
callSign: undefined,
|
||||||
ordinalDetails: null,
|
ensName: undefined,
|
||||||
verificationLevel: EVerificationStatus.WALLET_UNCONNECTED,
|
ordinalDetails: undefined,
|
||||||
displayPreference: null,
|
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
|
||||||
|
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const getDisplayName = React.useCallback((addr: string) => {
|
|
||||||
return client.userIdentityService.getDisplayName(addr);
|
|
||||||
}, [client]);
|
|
||||||
|
|
||||||
// Initial load and refresh listener
|
// Initial load and refresh listener
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!address) return;
|
if (!address) return;
|
||||||
@ -43,28 +36,16 @@ export function useUserDisplay(address: string): UserDisplayInfo {
|
|||||||
|
|
||||||
const loadUserDisplay = async () => {
|
const loadUserDisplay = async () => {
|
||||||
try {
|
try {
|
||||||
const identity = await client.userIdentityService.getUserIdentity(address);
|
const identity = await client.userIdentityService.getIdentity(address);
|
||||||
|
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
if (identity) {
|
if (identity) {
|
||||||
setDisplayInfo({
|
setDisplayInfo({
|
||||||
displayName: getDisplayName(address),
|
...identity,
|
||||||
callSign: identity.callSign || null,
|
|
||||||
ensName: identity.ensName || null,
|
|
||||||
ordinalDetails: identity.ordinalDetails?.ordinalDetails || null,
|
|
||||||
verificationLevel: identity.verificationStatus,
|
|
||||||
displayPreference: identity.displayPreference || null,
|
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
setDisplayInfo(prev => ({
|
|
||||||
...prev,
|
|
||||||
displayName: getDisplayName(address),
|
|
||||||
isLoading: false,
|
|
||||||
error: null,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
@ -80,21 +61,16 @@ export function useUserDisplay(address: string): UserDisplayInfo {
|
|||||||
loadUserDisplay();
|
loadUserDisplay();
|
||||||
|
|
||||||
// Subscribe to identity service refresh events
|
// 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;
|
if (changedAddress !== address || cancelled) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const identity = await client.userIdentityService.getUserIdentity(address);
|
const identity = await client.userIdentityService.getIdentity(address);
|
||||||
if (!identity || cancelled) return;
|
if (!identity || cancelled) return;
|
||||||
|
|
||||||
setDisplayInfo(prev => ({
|
setDisplayInfo(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
displayName: getDisplayName(address),
|
...identity,
|
||||||
callSign: identity.callSign || null,
|
|
||||||
ensName: identity.ensName || null,
|
|
||||||
ordinalDetails: identity.ordinalDetails?.ordinalDetails || null,
|
|
||||||
verificationLevel: identity.verificationStatus,
|
|
||||||
displayPreference: identity.displayPreference || null,
|
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
}));
|
}));
|
||||||
@ -117,7 +93,7 @@ export function useUserDisplay(address: string): UserDisplayInfo {
|
|||||||
// Ignore unsubscribe errors
|
// Ignore unsubscribe errors
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [address, client, getDisplayName]);
|
}, [address, client]);
|
||||||
|
|
||||||
return displayInfo;
|
return displayInfo;
|
||||||
}
|
}
|
||||||
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { useClient } from '../context/ClientContext';
|
import { useClient } from '../context/ClientContext';
|
||||||
import { setOpchanState, getOpchanState } from '../store/opchanStore';
|
import { setOpchanState, getOpchanState } from '../store/opchanStore';
|
||||||
import type { OpchanMessage, User } from '@opchan/core';
|
import type { OpchanMessage, User } from '@opchan/core';
|
||||||
import { EVerificationStatus, EDisplayPreference } from '@opchan/core';
|
import { EVerificationStatus } from '@opchan/core';
|
||||||
|
|
||||||
export const StoreWiring: React.FC = () => {
|
export const StoreWiring: React.FC = () => {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
@ -23,9 +23,9 @@ export const StoreWiring: React.FC = () => {
|
|||||||
...prev,
|
...prev,
|
||||||
content: {
|
content: {
|
||||||
...prev.content,
|
...prev.content,
|
||||||
cells: Object.values(cache.cells),
|
cells: Object.values(cache.cells),
|
||||||
posts: Object.values(cache.posts),
|
posts: Object.values(cache.posts),
|
||||||
comments: Object.values(cache.comments),
|
comments: Object.values(cache.comments),
|
||||||
bookmarks: Object.values(cache.bookmarks),
|
bookmarks: Object.values(cache.bookmarks),
|
||||||
lastSync: client.database.getSyncState().lastSync,
|
lastSync: client.database.getSyncState().lastSync,
|
||||||
pendingIds: new Set<string>(),
|
pendingIds: new Set<string>(),
|
||||||
@ -41,35 +41,12 @@ export const StoreWiring: React.FC = () => {
|
|||||||
loadedUser?.walletType,
|
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 => ({
|
setOpchanState(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
session: {
|
session: {
|
||||||
currentUser: enrichedUser,
|
currentUser: loadedUser,
|
||||||
verificationStatus:
|
verificationStatus:
|
||||||
enrichedUser?.verificationStatus ?? EVerificationStatus.WALLET_UNCONNECTED,
|
loadedUser?.verificationStatus ?? EVerificationStatus.WALLET_UNCONNECTED,
|
||||||
delegation: delegationStatus ?? null,
|
delegation: delegationStatus ?? null,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@ -118,30 +95,29 @@ export const StoreWiring: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Reactively update session.currentUser when identity refreshes for the active user
|
// 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 {
|
try {
|
||||||
const { session } = getOpchanState();
|
const { session } = getOpchanState();
|
||||||
const active = session.currentUser;
|
const active = session.currentUser;
|
||||||
if (!active || active.address !== address) return;
|
if (!active || active.address !== address) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const identity = await client.userIdentityService.getUserIdentity(address);
|
const identity = await client.userIdentityService.getIdentity(address);
|
||||||
if (!identity) return;
|
if (!identity) {
|
||||||
|
return;
|
||||||
const displayName = identity.displayPreference === EDisplayPreference.CALL_SIGN
|
}
|
||||||
? (identity.callSign || active.displayName)
|
|
||||||
: (identity.ensName || active.displayName);
|
|
||||||
|
|
||||||
const updated: User = {
|
const updated: User = {
|
||||||
...active,
|
...active,
|
||||||
callSign: identity.callSign ?? active.callSign,
|
...identity,
|
||||||
displayPreference: identity.displayPreference ?? active.displayPreference,
|
|
||||||
displayName,
|
|
||||||
ensDetails: identity.ensName ? { ensName: identity.ensName } : active.ensDetails,
|
|
||||||
ordinalDetails: identity.ordinalDetails ?? active.ordinalDetails,
|
|
||||||
verificationStatus: identity.verificationStatus ?? active.verificationStatus,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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 => ({
|
setOpchanState(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
@ -157,7 +133,9 @@ export const StoreWiring: React.FC = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
hydrate().then(wire);
|
hydrate().then(() => {
|
||||||
|
wire();
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubHealth?.();
|
unsubHealth?.();
|
||||||
@ -167,6 +145,4 @@ export const StoreWiring: React.FC = () => {
|
|||||||
}, [client]);
|
}, [client]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user