From 1155948d0dabdf29191af17b01df51247518368a Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Wed, 30 Jul 2025 15:55:13 +0530 Subject: [PATCH] fix: wallet reconnection --- .gitignore | 2 + src/components/Header.tsx | 24 +++++-- src/contexts/AuthContext.tsx | 10 +++ src/lib/identity/services/AuthService.ts | 8 +++ src/lib/identity/wallets/index.ts | 44 +++++++++++++ src/lib/identity/wallets/phantom.ts | 80 +++++++++++++++++++++++- 6 files changed, 163 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 327d05a..6c9f2eb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ comparison.md .giga/ +furps.md + README-task-master.md .cursor scripts diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 560dece..26f7ed2 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -4,8 +4,9 @@ import { useAuth } from '@/contexts/useAuth'; import { useForum } from '@/contexts/useForum'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; -import { ShieldCheck, LogOut, Terminal, Wifi, WifiOff, AlertTriangle, CheckCircle, Key, RefreshCw, CircleSlash } from 'lucide-react'; +import { LogOut, Terminal, Wifi, WifiOff, AlertTriangle, CheckCircle, Key, RefreshCw, CircleSlash } from 'lucide-react'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { useToast } from '@/components/ui/use-toast'; const Header = () => { const { @@ -17,9 +18,11 @@ const Header = () => { verifyOrdinal, delegateKey, isDelegationValid, - delegationTimeRemaining + delegationTimeRemaining, + isWalletAvailable } = useAuth(); const { isNetworkConnected, isRefreshing } = useForum(); + const { toast } = useToast(); const handleConnect = async () => { await connectWallet(); @@ -34,7 +37,20 @@ const Header = () => { }; const handleDelegateKey = async () => { - await delegateKey(); + try { + if (!isWalletAvailable()) { + toast({ + title: "Wallet Not Available", + description: "Phantom wallet is not installed or not available. Please install Phantom wallet and try again.", + variant: "destructive", + }); + return; + } + + await delegateKey(); + } catch (error) { + console.error('Error in handleDelegateKey:', error); + } }; const formatDelegationTime = () => { @@ -72,7 +88,7 @@ const Header = () => { {hasValidDelegation ? (

Browser key active for ~{timeRemaining}. Wallet signatures not needed for most actions.

) : ( -

Delegate a browser key for 24h to avoid constant wallet signing.

+

Delegate a browser key for 24h to avoid constant wallet signing. If your wallet is disconnected, it will be reconnected automatically.

)} diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 5c142d7..a156b25 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -17,6 +17,7 @@ interface AuthContextType { delegateKey: () => Promise; isDelegationValid: () => boolean; delegationTimeRemaining: () => number; + isWalletAvailable: () => boolean; messageSigning: { signMessage: (message: OpchanMessage) => Promise; verifyMessage: (message: OpchanMessage) => boolean; @@ -220,6 +221,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { errorMessage = "You declined the signature request. Key delegation is optional but improves your experience."; } else if (error.message.includes("timeout")) { errorMessage = "Wallet request timed out. Please try again and approve the signature promptly."; + } else if (error.message.includes("Failed to connect wallet")) { + errorMessage = "Unable to connect to Phantom wallet. Please ensure it's installed and unlocked, then try again."; + } else if (error.message.includes("Wallet is not connected")) { + errorMessage = "Wallet connection was lost. Please reconnect your wallet and try again."; } else { errorMessage = error.message; } @@ -245,6 +250,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { return authServiceRef.current.getDelegationTimeRemaining(); }; + const isWalletAvailable = (): boolean => { + return authServiceRef.current.getWalletInfo()?.type === 'phantom'; + }; + return ( authServiceRef.current.signMessage(message), verifyMessage: (message: OpchanMessage) => authServiceRef.current.verifyMessage(message), diff --git a/src/lib/identity/services/AuthService.ts b/src/lib/identity/services/AuthService.ts index 4ec1973..e494ba8 100644 --- a/src/lib/identity/services/AuthService.ts +++ b/src/lib/identity/services/AuthService.ts @@ -92,6 +92,14 @@ export class AuthService { */ async delegateKey(user: User): Promise { try { + const canConnect = await this.walletService.canConnectWallet('phantom'); + if (!canConnect) { + return { + success: false, + error: 'Phantom wallet is not available or cannot be connected. Please ensure it is installed and unlocked.' + }; + } + const delegationInfo = await this.walletService.setupKeyDelegation( user.address, 'phantom' diff --git a/src/lib/identity/wallets/index.ts b/src/lib/identity/wallets/index.ts index 113beab..9899d2f 100644 --- a/src/lib/identity/wallets/index.ts +++ b/src/lib/identity/wallets/index.ts @@ -32,6 +32,23 @@ export class WalletService { public isWalletAvailable(type: WalletType): boolean { return this.phantomAdapter.isInstalled(); } + + /** + * Check if wallet is available and can be connected + */ + public async canConnectWallet(type: WalletType = 'phantom'): Promise { + if (!this.isWalletAvailable(type)) { + return false; + } + + try { + const isConnected = await this.phantomAdapter.isConnected(); + return isConnected; + } catch (error) { + console.debug('WalletService: Cannot connect wallet:', error); + return false; + } + } /** * Connect to a specific wallet type @@ -87,8 +104,31 @@ export class WalletService { walletType: WalletType, validityPeriod: number = WalletService.DEFAULT_DELEGATION_PERIOD ): Promise> { + console.debug('WalletService: Starting key delegation for address:', bitcoinAddress); + + let isConnected = await this.phantomAdapter.isConnected(); + console.debug('WalletService: Initial wallet connection check result:', isConnected); + + if (!isConnected) { + console.debug('WalletService: Wallet not connected, attempting to connect automatically'); + try { + await this.phantomAdapter.connect(); + isConnected = await this.phantomAdapter.isConnected(); + console.debug('WalletService: Auto-connection result:', isConnected); + } catch (error) { + console.error('WalletService: Failed to auto-connect wallet:', error); + throw new Error('Failed to connect wallet. Please ensure Phantom wallet is installed and try again.'); + } + } + + if (!isConnected) { + console.error('WalletService: Wallet is still not connected after auto-connection attempt'); + throw new Error('Wallet is not connected. Please connect your wallet first.'); + } + // Generate browser keypair const keypair = this.keyDelegation.generateKeypair(); + console.debug('WalletService: Generated browser keypair'); // Calculate expiry in hours const expiryHours = validityPeriod / (60 * 60 * 1000); @@ -99,9 +139,12 @@ export class WalletService { bitcoinAddress, Date.now() + validityPeriod ); + console.debug('WalletService: Created delegation message'); // Sign the delegation message with the Bitcoin wallet + console.debug('WalletService: Requesting signature from wallet'); const signature = await this.phantomAdapter.signMessage(delegationMessage); + console.debug('WalletService: Received signature from wallet'); // Create and store the delegation const delegationInfo = this.keyDelegation.createDelegation( @@ -113,6 +156,7 @@ export class WalletService { ); this.keyDelegation.storeDelegation(delegationInfo); + console.debug('WalletService: Stored delegation info'); // Return delegation info (excluding private key) return { diff --git a/src/lib/identity/wallets/phantom.ts b/src/lib/identity/wallets/phantom.ts index 452383d..0f8bd48 100644 --- a/src/lib/identity/wallets/phantom.ts +++ b/src/lib/identity/wallets/phantom.ts @@ -16,13 +16,71 @@ export class PhantomWalletAdapter { constructor() { this.checkWalletAvailability(); + this.restoreConnectionState(); } + /** + * Restore connection state from existing wallet connection + */ + private async restoreConnectionState(): Promise { + if (typeof window === 'undefined' || !window?.phantom?.bitcoin) { + console.debug('PhantomWalletAdapter: No wallet available for connection restoration'); + return; + } + + try { + console.debug('PhantomWalletAdapter: Attempting to restore connection state'); + this.provider = window.phantom; + this.btcProvider = window.phantom.bitcoin; + + // Check if wallet is already connected by trying to get accounts + if (this.btcProvider?.requestAccounts) { + const btcAccounts = await this.btcProvider.requestAccounts(); + + if (btcAccounts && btcAccounts.length > 0) { + const ordinalAccount = btcAccounts.find(acc => acc.purpose === 'ordinals'); + const account = ordinalAccount || btcAccounts[0]; + + this.currentAccount = account.address; + this.connectionStatus = WalletConnectionStatus.Connected; + console.debug('PhantomWalletAdapter: Successfully restored connection for account:', account.address); + } else { + console.debug('PhantomWalletAdapter: No accounts found during connection restoration'); + } + } else { + console.debug('PhantomWalletAdapter: requestAccounts method not available'); + } + } catch (error) { + // If we can't restore the connection, that's okay - user will need to reconnect + console.debug('PhantomWalletAdapter: Could not restore existing wallet connection:', error); + this.connectionStatus = WalletConnectionStatus.Disconnected; + } + } public getStatus(): WalletConnectionStatus { return this.connectionStatus; } + /** + * Check if the wallet is actually connected by attempting to get accounts + */ + public async isConnected(): Promise { + if (!this.btcProvider) { + return false; + } + + try { + if (this.btcProvider.requestAccounts) { + const accounts = await this.btcProvider.requestAccounts(); + return accounts && accounts.length > 0; + } + return false; + } catch (error) { + console.debug('Error checking wallet connection:', error); + return false; + } + } + public isInstalled(): boolean { if (typeof window === 'undefined') { return false; @@ -88,11 +146,30 @@ export class PhantomWalletAdapter { } async signMessage(message: string): Promise { + console.debug('PhantomWalletAdapter: signMessage called, btcProvider:', !!this.btcProvider, 'currentAccount:', this.currentAccount); + + if (!this.btcProvider && window?.phantom?.bitcoin) { + console.debug('PhantomWalletAdapter: Attempting to restore connection before signing'); + await this.restoreConnectionState(); + } + + if (!this.btcProvider || !this.currentAccount) { + console.debug('PhantomWalletAdapter: Wallet not connected, attempting to connect automatically'); + try { + await this.connect(); + } catch (error) { + console.error('PhantomWalletAdapter: Failed to auto-connect wallet:', error); + throw new Error('Failed to connect wallet. Please ensure Phantom wallet is installed and try again.'); + } + } + if (!this.btcProvider) { + console.error('PhantomWalletAdapter: Wallet is not connected - no btcProvider'); throw new Error('Wallet is not connected'); } if (!this.currentAccount) { + console.error('PhantomWalletAdapter: No active account to sign with'); throw new Error('No active account to sign with'); } @@ -101,6 +178,7 @@ export class PhantomWalletAdapter { throw new Error('signMessage method not available on wallet provider'); } + console.debug('PhantomWalletAdapter: Signing message for account:', this.currentAccount); const messageBytes = new TextEncoder().encode(message); const { signature } = await this.btcProvider.signMessage( @@ -115,7 +193,7 @@ export class PhantomWalletAdapter { return String(signature); } catch (error) { - console.error('Error signing message:', error); + console.error('PhantomWalletAdapter: Error signing message:', error); throw error; } }