mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-07 15:23:05 +00:00
fix: wallet reconnection
This commit is contained in:
parent
2140e41144
commit
1155948d0d
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,6 +2,8 @@
|
|||||||
comparison.md
|
comparison.md
|
||||||
.giga/
|
.giga/
|
||||||
|
|
||||||
|
furps.md
|
||||||
|
|
||||||
README-task-master.md
|
README-task-master.md
|
||||||
.cursor
|
.cursor
|
||||||
scripts
|
scripts
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user