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;
}
}