fix: wallet reconnection

This commit is contained in:
Danish Arora 2025-07-30 15:55:13 +05:30
parent 2140e41144
commit 1155948d0d
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
6 changed files with 163 additions and 5 deletions

2
.gitignore vendored
View File

@ -2,6 +2,8 @@
comparison.md comparison.md
.giga/ .giga/
furps.md
README-task-master.md README-task-master.md
.cursor .cursor
scripts scripts

View File

@ -4,8 +4,9 @@ import { useAuth } from '@/contexts/useAuth';
import { useForum } from '@/contexts/useForum'; import { useForum } from '@/contexts/useForum';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; 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 { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useToast } from '@/components/ui/use-toast';
const Header = () => { const Header = () => {
const { const {
@ -17,9 +18,11 @@ const Header = () => {
verifyOrdinal, verifyOrdinal,
delegateKey, delegateKey,
isDelegationValid, isDelegationValid,
delegationTimeRemaining delegationTimeRemaining,
isWalletAvailable
} = useAuth(); } = useAuth();
const { isNetworkConnected, isRefreshing } = useForum(); const { isNetworkConnected, isRefreshing } = useForum();
const { toast } = useToast();
const handleConnect = async () => { const handleConnect = async () => {
await connectWallet(); await connectWallet();
@ -34,7 +37,20 @@ const Header = () => {
}; };
const handleDelegateKey = async () => { 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 = () => { const formatDelegationTime = () => {
@ -72,7 +88,7 @@ const Header = () => {
{hasValidDelegation ? ( {hasValidDelegation ? (
<p>Browser key active for ~{timeRemaining}. Wallet signatures not needed for most actions.</p> <p>Browser key active for ~{timeRemaining}. Wallet signatures not needed for most actions.</p>
) : ( ) : (
<p>Delegate a browser key for 24h to avoid constant wallet signing.</p> <p>Delegate a browser key for 24h to avoid constant wallet signing. If your wallet is disconnected, it will be reconnected automatically.</p>
)} )}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>

View File

@ -17,6 +17,7 @@ interface AuthContextType {
delegateKey: () => Promise<boolean>; delegateKey: () => Promise<boolean>;
isDelegationValid: () => boolean; isDelegationValid: () => boolean;
delegationTimeRemaining: () => number; delegationTimeRemaining: () => number;
isWalletAvailable: () => boolean;
messageSigning: { messageSigning: {
signMessage: (message: OpchanMessage) => Promise<OpchanMessage | null>; signMessage: (message: OpchanMessage) => Promise<OpchanMessage | null>;
verifyMessage: (message: OpchanMessage) => boolean; 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."; errorMessage = "You declined the signature request. Key delegation is optional but improves your experience.";
} else if (error.message.includes("timeout")) { } else if (error.message.includes("timeout")) {
errorMessage = "Wallet request timed out. Please try again and approve the signature promptly."; 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 { } else {
errorMessage = error.message; errorMessage = error.message;
} }
@ -245,6 +250,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
return authServiceRef.current.getDelegationTimeRemaining(); return authServiceRef.current.getDelegationTimeRemaining();
}; };
const isWalletAvailable = (): boolean => {
return authServiceRef.current.getWalletInfo()?.type === 'phantom';
};
return ( return (
<AuthContext.Provider <AuthContext.Provider
value={{ value={{
@ -258,6 +267,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
delegateKey, delegateKey,
isDelegationValid, isDelegationValid,
delegationTimeRemaining, delegationTimeRemaining,
isWalletAvailable,
messageSigning: { messageSigning: {
signMessage: (message: OpchanMessage) => authServiceRef.current.signMessage(message), signMessage: (message: OpchanMessage) => authServiceRef.current.signMessage(message),
verifyMessage: (message: OpchanMessage) => authServiceRef.current.verifyMessage(message), verifyMessage: (message: OpchanMessage) => authServiceRef.current.verifyMessage(message),

View File

@ -92,6 +92,14 @@ export class AuthService {
*/ */
async delegateKey(user: User): Promise<AuthResult> { async delegateKey(user: User): Promise<AuthResult> {
try { 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( const delegationInfo = await this.walletService.setupKeyDelegation(
user.address, user.address,
'phantom' 'phantom'

View File

@ -32,6 +32,23 @@ export class WalletService {
public isWalletAvailable(type: WalletType): boolean { public isWalletAvailable(type: WalletType): boolean {
return this.phantomAdapter.isInstalled(); return this.phantomAdapter.isInstalled();
} }
/**
* Check if wallet is available and can be connected
*/
public async canConnectWallet(type: WalletType = 'phantom'): Promise<boolean> {
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 * Connect to a specific wallet type
@ -87,8 +104,31 @@ export class WalletService {
walletType: WalletType, walletType: WalletType,
validityPeriod: number = WalletService.DEFAULT_DELEGATION_PERIOD validityPeriod: number = WalletService.DEFAULT_DELEGATION_PERIOD
): Promise<Omit<DelegationInfo, 'browserPrivateKey'>> { ): Promise<Omit<DelegationInfo, 'browserPrivateKey'>> {
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 // Generate browser keypair
const keypair = this.keyDelegation.generateKeypair(); const keypair = this.keyDelegation.generateKeypair();
console.debug('WalletService: Generated browser keypair');
// Calculate expiry in hours // Calculate expiry in hours
const expiryHours = validityPeriod / (60 * 60 * 1000); const expiryHours = validityPeriod / (60 * 60 * 1000);
@ -99,9 +139,12 @@ export class WalletService {
bitcoinAddress, bitcoinAddress,
Date.now() + validityPeriod Date.now() + validityPeriod
); );
console.debug('WalletService: Created delegation message');
// Sign the delegation message with the Bitcoin wallet // Sign the delegation message with the Bitcoin wallet
console.debug('WalletService: Requesting signature from wallet');
const signature = await this.phantomAdapter.signMessage(delegationMessage); const signature = await this.phantomAdapter.signMessage(delegationMessage);
console.debug('WalletService: Received signature from wallet');
// Create and store the delegation // Create and store the delegation
const delegationInfo = this.keyDelegation.createDelegation( const delegationInfo = this.keyDelegation.createDelegation(
@ -113,6 +156,7 @@ export class WalletService {
); );
this.keyDelegation.storeDelegation(delegationInfo); this.keyDelegation.storeDelegation(delegationInfo);
console.debug('WalletService: Stored delegation info');
// Return delegation info (excluding private key) // Return delegation info (excluding private key)
return { return {

View File

@ -16,13 +16,71 @@ export class PhantomWalletAdapter {
constructor() { constructor() {
this.checkWalletAvailability(); this.checkWalletAvailability();
this.restoreConnectionState();
} }
/**
* Restore connection state from existing wallet connection
*/
private async restoreConnectionState(): Promise<void> {
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 { public getStatus(): WalletConnectionStatus {
return this.connectionStatus; return this.connectionStatus;
} }
/**
* Check if the wallet is actually connected by attempting to get accounts
*/
public async isConnected(): Promise<boolean> {
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 { public isInstalled(): boolean {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return false; return false;
@ -88,11 +146,30 @@ export class PhantomWalletAdapter {
} }
async signMessage(message: string): Promise<string> { async signMessage(message: string): Promise<string> {
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) { if (!this.btcProvider) {
console.error('PhantomWalletAdapter: Wallet is not connected - no btcProvider');
throw new Error('Wallet is not connected'); throw new Error('Wallet is not connected');
} }
if (!this.currentAccount) { if (!this.currentAccount) {
console.error('PhantomWalletAdapter: No active account to sign with');
throw new Error('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'); 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 messageBytes = new TextEncoder().encode(message);
const { signature } = await this.btcProvider.signMessage( const { signature } = await this.btcProvider.signMessage(
@ -115,7 +193,7 @@ export class PhantomWalletAdapter {
return String(signature); return String(signature);
} catch (error) { } catch (error) {
console.error('Error signing message:', error); console.error('PhantomWalletAdapter: Error signing message:', error);
throw error; throw error;
} }
} }