diff --git a/src/App.tsx b/src/App.tsx index 27d7e22..f603341 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,7 +24,6 @@ import NotFound from "./pages/NotFound"; import Dashboard from "./pages/Dashboard"; import Index from "./pages/Index"; import { appkitConfig } from "./lib/identity/wallets/appkit"; -import { createAppKit } from "@reown/appkit"; import { WagmiProvider } from "wagmi"; import { config } from "./lib/identity/wallets/appkit"; import { AppKitProvider } from "@reown/appkit/react"; @@ -32,8 +31,6 @@ import { AppKitProvider } from "@reown/appkit/react"; // Create a client const queryClient = new QueryClient(); -createAppKit(appkitConfig); - const App = () => ( diff --git a/src/components/ui/delegation-step.tsx b/src/components/ui/delegation-step.tsx index 2388454..1e6673d 100644 --- a/src/components/ui/delegation-step.tsx +++ b/src/components/ui/delegation-step.tsx @@ -1,7 +1,6 @@ import * as React from "react"; import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { Key, Clock, Shield, Loader2, CheckCircle, AlertCircle } from "lucide-react"; +import { Key, Loader2, CheckCircle, AlertCircle, Trash2 } from "lucide-react"; import { useAuth } from "@/contexts/useAuth"; interface DelegationStepProps { @@ -22,7 +21,8 @@ export function DelegationStep({ delegateKey, isDelegationValid, delegationTimeRemaining, - isAuthenticating + isAuthenticating, + clearDelegation } = useAuth(); const [delegationResult, setDelegationResult] = React.useState<{ @@ -47,13 +47,13 @@ export function DelegationStep({ setDelegationResult({ success: true, - message: "Key delegation successful! You can now interact with the forum without additional wallet approvals.", + message: "Key delegation successful!", expiry: expiryDate }); } else { setDelegationResult({ success: false, - message: "Key delegation failed. You can still use the forum but will need to approve each action." + message: "Key delegation failed." }); } } catch (error) { @@ -70,155 +70,171 @@ export function DelegationStep({ onComplete(); }; - const formatTimeRemaining = () => { - const remaining = delegationTimeRemaining(); - if (remaining <= 0) return "Expired"; - - const hours = Math.floor(remaining / (1000 * 60 * 60)); - const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60)); - - if (hours > 0) { - return `${hours}h ${minutes}m remaining`; - } else { - return `${minutes}m remaining`; - } + const handleRefresh = () => { + window.location.reload(); }; // Show delegation result if (delegationResult) { return ( -
-
-
- {delegationResult.success ? ( - - ) : ( - - )} - - {delegationResult.success ? 'Delegation Complete' : 'Delegation Result'} - -
-

- {delegationResult.message} -

- {delegationResult.expiry && ( -
-

Expires: {delegationResult.expiry}

+
+
+
+
+ {delegationResult.success ? ( + + ) : ( + + )} + + {delegationResult.success ? 'Delegation Complete' : 'Delegation Result'} +
- )} -
- - -
- ); - } - - // Show existing delegation status - if (isDelegationValid()) { - return ( -
-
-
- - Key Already Delegated -
-

- You already have an active key delegation. -

-
-

Time remaining: {formatTimeRemaining()}

- {currentUser?.delegationExpiry && ( -

Expires: {new Date(currentUser.delegationExpiry).toLocaleString()}

+

+ {delegationResult.message} +

+ {delegationResult.expiry && ( +
+

Expires: {delegationResult.expiry}

+
)}
- + {/* Action Button */} +
+ +
); } - // Show delegation form + // Show minimal delegation status return ( -
-
-
- -
-

- Delegate Signing Key -

-

- Create a browser-based signing key for better user experience -

-
- -
-
- - What is key delegation? -
-
    -
  • • Creates a browser-based signing key for 24 hours
  • -
  • • Allows posting, commenting, and voting without wallet approval
  • -
  • • Automatically expires for security
  • -
  • • Can be renewed anytime
  • -
-
- - {currentUser?.browserPubKey && ( -
-
- - Browser Key Generated +
+
+
+
+
-

- {currentUser.browserPubKey.slice(0, 20)}...{currentUser.browserPubKey.slice(-20)} +

+ Delegate Signing Key +

+

+ Create a browser-based signing key

- )} -
- + {/* Delegation Status */} +
+
+
+ + Key Delegation +
+ {currentUser?.walletType === 'bitcoin' ? ( +
+ ) : ( +
Ξ
+ )} +
+ +
+ {/* Status */} +
+ {isDelegationValid() ? ( + + ) : ( + + )} + + {isDelegationValid() ? 'Delegated' : 'Required'} + + {isDelegationValid() && ( + + {Math.floor(delegationTimeRemaining() / (1000 * 60 * 60))}h {Math.floor((delegationTimeRemaining() % (1000 * 60 * 60)) / (1000 * 60))}m remaining + + )} +
+ + {/* Delegated Browser Public Key */} + {isDelegationValid() && currentUser?.browserPubKey && ( +
+
+ {currentUser.browserPubKey} +
+
+ )} + + {/* Wallet Address */} + {currentUser && ( +
+
+ {currentUser.address} +
+
+ )} + + {/* Delete Button for Active Delegations */} + {isDelegationValid() && ( +
+ +
+ )} +
+
+
+ + {/* Action Buttons */} +
+ {!isDelegationValid() && ( + + )} + + {isDelegationValid() && ( + + )}
- -
-

Key delegation is optional but recommended for better UX

-

You can still use the forum without delegation

-
); } \ No newline at end of file diff --git a/src/components/ui/verification-step.tsx b/src/components/ui/verification-step.tsx index 7d1787c..f9b3984 100644 --- a/src/components/ui/verification-step.tsx +++ b/src/components/ui/verification-step.tsx @@ -103,52 +103,50 @@ export function VerificationStep({ // Show verification result if (verificationResult) { return ( -
-
-
- {verificationResult.success ? ( - - ) : ( - - )} - - {verificationResult.success ? 'Verification Complete' : 'Verification Result'} - -
-

- {verificationResult.message} -

- {verificationResult.details && ( -
- {walletType === 'bitcoin' ? ( -

Ordinal ID: {verificationResult.details.id}

+
+
+
+
+ {verificationResult.success ? ( + ) : ( -

ENS Name: {verificationResult.details.ensName}

+ )} + + {verificationResult.success ? 'Verification Complete' : 'Verification Result'} +
- )} +

+ {verificationResult.message} +

+ {verificationResult.details && ( +
+ {walletType === 'bitcoin' ? ( +

Ordinal ID: {verificationResult.details.id}

+ ) : ( +

ENS Name: {verificationResult.details.ensName}

+ )} +
+ )} +
- + {/* Action Button */} +
+ +
); } @@ -156,78 +154,90 @@ export function VerificationStep({ // Show verification status if (verificationStatus === 'verified-owner') { return ( -
-
-
- - Already Verified -
-

- Your {getVerificationType()} ownership has been verified. -

- {currentUser && ( -
- {walletType === 'bitcoin' && currentUser.ordinalOwnership && ( -

Ordinal ID: {typeof currentUser.ordinalOwnership === 'object' ? currentUser.ordinalOwnership.id : 'Verified'}

- )} - {walletType === 'ethereum' && currentUser.ensName && ( -

ENS Name: {currentUser.ensName}

- )} +
+
+
+
+ + Already Verified
- )} +

+ Your {getVerificationType()} ownership has been verified. +

+ {currentUser && ( +
+ {walletType === 'bitcoin' && currentUser.ordinalOwnership && ( +

Ordinal ID: {typeof currentUser.ordinalOwnership === 'object' ? currentUser.ordinalOwnership.id : 'Verified'}

+ )} + {walletType === 'ethereum' && currentUser.ensName && ( +

ENS Name: {currentUser.ensName}

+ )} +
+ )} +
- + {/* Action Button */} +
+ +
); } // Show verification form return ( -
-
-
- {React.createElement(getVerificationIcon(), { - className: `h-8 w-8 ${getVerificationColor()}` - })} +
+
+
+
+ {React.createElement(getVerificationIcon(), { + className: `h-8 w-8 ${getVerificationColor()}` + })} +
+

+ Verify {getVerificationType()} Ownership +

+

+ {getVerificationDescription()} +

+
+ +
+
+ + What happens during verification? +
+
    + {walletType === 'bitcoin' ? ( + <> +
  • • We'll check your wallet for Bitcoin Ordinal ownership
  • +
  • • If found, you'll get full posting and voting access
  • +
  • • If not found, you'll have read-only access
  • + + ) : ( + <> +
  • • We'll check your wallet for ENS domain ownership
  • +
  • • If found, you'll get full posting and voting access
  • +
  • • If not found, you'll have read-only access
  • + + )} +
+
+ +
+ Verification is required to access posting and voting features
-

- Verify {getVerificationType()} Ownership -

-

- {getVerificationDescription()} -

-
-
- - What happens during verification? -
-
    - {walletType === 'bitcoin' ? ( - <> -
  • • We'll check your wallet for Bitcoin Ordinal ownership
  • -
  • • If found, you'll get full posting and voting access
  • -
  • • If not found, you'll have read-only access
  • - - ) : ( - <> -
  • • We'll check your wallet for ENS domain ownership
  • -
  • • If found, you'll get full posting and voting access
  • -
  • • If not found, you'll have read-only access
  • - - )} -
-
- -
+ {/* Action Buttons */} +
- -
- Verification is required to access posting and voting features -
); } \ No newline at end of file diff --git a/src/components/ui/wallet-connection-step.tsx b/src/components/ui/wallet-connection-step.tsx index 54301b2..154fe6f 100644 --- a/src/components/ui/wallet-connection-step.tsx +++ b/src/components/ui/wallet-connection-step.tsx @@ -50,7 +50,6 @@ export function WalletConnectionStep({ view: "Connect", namespace: "bip122" }); - // The wizard will automatically advance when connection is detected } catch (error) { console.error('Error connecting Bitcoin wallet:', error); } finally { @@ -70,7 +69,6 @@ export function WalletConnectionStep({ view: "Connect", namespace: "eip155" }); - // The wizard will automatically advance when connection is detected } catch (error) { console.error('Error connecting Ethereum wallet:', error); } finally { @@ -85,7 +83,7 @@ export function WalletConnectionStep({ // Show loading state if AppKit is not initialized if (!initialized) { return ( -
+

Initializing wallet connection... @@ -97,22 +95,28 @@ export function WalletConnectionStep({ // Show connected state if (isConnected) { return ( -

-
-
-
- Wallet Connected +
+
+
+
+
+ Wallet Connected +
+

+ Connected to {activeChain} with {activeAddress?.slice(0, 6)}...{activeAddress?.slice(-4)} +

-

- Connected to {activeChain} with {activeAddress?.slice(0, 6)}...{activeAddress?.slice(-4)} -

-

- Proceeding to verification step... -

-
- + {/* Action Button */} +
+
); @@ -120,95 +124,97 @@ export function WalletConnectionStep({ // Show connection options return ( -
-

- Choose a network and wallet to connect to OpChan -

+
+
+

+ Choose a network and wallet to connect to OpChan +

- {/* Bitcoin Section */} -
-
- -

Bitcoin

- - Ordinal Verification Required - + {/* Bitcoin Section */} +
+
+ +

Bitcoin

+ + Ordinal Verification Required + +
+
- -
- {/* Divider */} -
-
- + {/* Divider */} +
+
+ +
+
+ or +
-
- or -
-
- {/* Ethereum Section */} -
-
- -

Ethereum

- - ENS Ownership Required - + {/* Ethereum Section */} +
+
+ +

Ethereum

+ + ENS Ownership Required + +
+
- -
-
- Connect your wallet to use OpChan's features +
+ Connect your wallet to use OpChan's features +
); diff --git a/src/components/ui/wallet-wizard.tsx b/src/components/ui/wallet-wizard.tsx index 25a553e..ead8cb9 100644 --- a/src/components/ui/wallet-wizard.tsx +++ b/src/components/ui/wallet-wizard.tsx @@ -33,25 +33,19 @@ export function WalletWizard({ // Reset wizard when opened and determine starting step React.useEffect(() => { if (open) { - // Only auto-advance from step 1 to 2 when wallet connects during the session - // Don't auto-advance to step 3 to allow manual navigation - if (isAuthenticated && verificationStatus !== 'verified-owner') { - setCurrentStep(2); // Start at verification step if authenticated but not verified - } else if (!isAuthenticated) { + // Determine the appropriate starting step based on current state + if (!isAuthenticated) { setCurrentStep(1); // Start at connection step if not authenticated + } else if (isAuthenticated && (verificationStatus === 'unverified' || verificationStatus === 'verifying')) { + setCurrentStep(2); // Start at verification step if authenticated but not verified + } else if (isAuthenticated && (verificationStatus === 'verified-owner' || verificationStatus === 'verified-none') && !isDelegationValid()) { + setCurrentStep(3); // Start at delegation step if verified but no valid delegation } else { - setCurrentStep(1); // Default to step 1, let user navigate manually + setCurrentStep(3); // Default to step 3 if everything is complete } setIsLoading(false); } - }, [open, isAuthenticated, verificationStatus, isDelegationValid]); - - // Auto-advance from step 1 to 2 only when wallet connects during the session - React.useEffect(() => { - if (open && currentStep === 1 && isAuthenticated) { - setCurrentStep(2); - } - }, [open, currentStep, isAuthenticated]); + }, [open, isAuthenticated, verificationStatus, isDelegationValid]); // Include all dependencies to properly determine step const handleStepComplete = (step: WizardStep) => { if (step < 3) { @@ -150,8 +144,8 @@ export function WalletWizard({ ))}
- {/* Step Content */} -
+ {/* Step Content - Fixed height container */} +
{currentStep === 1 && ( handleStepComplete(1)} diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index d074d4c..91fa631 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -3,7 +3,7 @@ import { useToast } from '@/components/ui/use-toast'; import { User } from '@/types'; import { AuthService, AuthResult } from '@/lib/identity/services/AuthService'; import { OpchanMessage } from '@/types'; -import { useAppKitAccount, useDisconnect } from '@reown/appkit/react'; +import { useAppKitAccount, useDisconnect, modal } from '@reown/appkit/react'; export type VerificationStatus = 'unverified' | 'verified-none' | 'verified-owner' | 'verifying'; @@ -14,6 +14,7 @@ interface AuthContextType { verificationStatus: VerificationStatus; verifyOwnership: () => Promise; delegateKey: () => Promise; + clearDelegation: () => void; isDelegationValid: () => boolean; delegationTimeRemaining: () => number; isWalletAvailable: () => boolean; @@ -49,7 +50,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { // Create ref for AuthService so it persists between renders const authServiceRef = useRef(new AuthService()); - // Set AppKit accounts in AuthService + // Set AppKit instance and accounts in AuthService + useEffect(() => { + if (modal) { + authServiceRef.current.setAppKit(modal); + } + }, []); + useEffect(() => { authServiceRef.current.setAccounts(bitcoinAccount, ethereumAccount); }, [bitcoinAccount, ethereumAccount]); @@ -250,6 +257,26 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { return authServiceRef.current.getDelegationTimeRemaining(); }; + const clearDelegation = (): void => { + authServiceRef.current.clearDelegation(); + + // Update the current user to remove delegation info + if (currentUser) { + const updatedUser = { + ...currentUser, + delegationExpiry: undefined, + browserPublicKey: undefined + }; + setCurrentUser(updatedUser); + authServiceRef.current.saveUser(updatedUser); + } + + toast({ + title: "Delegation Cleared", + description: "Your delegated signing key has been removed. You'll need to delegate a new key to continue posting and voting.", + }); + }; + const isWalletAvailable = (): boolean => { return isConnected; }; @@ -270,6 +297,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { verificationStatus, verifyOwnership, delegateKey, + clearDelegation, isDelegationValid, delegationTimeRemaining, isWalletAvailable, diff --git a/src/lib/identity/services/AuthService.ts b/src/lib/identity/services/AuthService.ts index 040ab21..c2a1c74 100644 --- a/src/lib/identity/services/AuthService.ts +++ b/src/lib/identity/services/AuthService.ts @@ -1,7 +1,10 @@ import { User } from '@/types'; -import { WalletService, AppKitAccount } from '../wallets/index'; +import { WalletService } from '../wallets/index'; +import { UseAppKitAccountReturn } from '@reown/appkit/react'; +import { AppKit } from '@reown/appkit'; import { OrdinalAPI } from '../ordinal'; import { MessageSigning } from '../signatures/message-signing'; +import { KeyDelegation } from '../signatures/key-delegation'; import { OpchanMessage } from '@/types'; export interface AuthResult { @@ -14,44 +17,97 @@ export class AuthService { private walletService: WalletService; private ordinalApi: OrdinalAPI; private messageSigning: MessageSigning; + private keyDelegation: KeyDelegation; constructor() { this.walletService = new WalletService(); this.ordinalApi = new OrdinalAPI(); - this.messageSigning = new MessageSigning(this.walletService.getKeyDelegation()); + this.keyDelegation = new KeyDelegation(); + this.messageSigning = new MessageSigning(this.keyDelegation); } /** * Set AppKit accounts for wallet service */ - setAccounts(bitcoinAccount: AppKitAccount, ethereumAccount: AppKitAccount) { + setAccounts(bitcoinAccount: UseAppKitAccountReturn, ethereumAccount: UseAppKitAccountReturn) { this.walletService.setAccounts(bitcoinAccount, ethereumAccount); } + /** + * Set AppKit instance for wallet service + */ + setAppKit(appKit: AppKit) { + this.walletService.setAppKit(appKit); + } + + /** + * Get the active wallet address + */ + private getActiveAddress(): string | null { + const isBitcoinConnected = this.walletService.isWalletAvailable('bitcoin'); + const isEthereumConnected = this.walletService.isWalletAvailable('ethereum'); + + if (isBitcoinConnected) { + return this.walletService.getActiveAddress('bitcoin') || null; + } else if (isEthereumConnected) { + return this.walletService.getActiveAddress('ethereum') || null; + } + + return null; + } + + /** + * Get the active wallet type + */ + private getActiveWalletType(): 'bitcoin' | 'ethereum' | null { + if (this.walletService.isWalletAvailable('bitcoin')) { + return 'bitcoin'; + } else if (this.walletService.isWalletAvailable('ethereum')) { + return 'ethereum'; + } + return null; + } + /** * Connect to wallet and create user */ async connectWallet(): Promise { try { - const walletInfo = await this.walletService.getWalletInfo(); - if (!walletInfo) { + // Check which wallet is connected + const isBitcoinConnected = this.walletService.isWalletAvailable('bitcoin'); + const isEthereumConnected = this.walletService.isWalletAvailable('ethereum'); + + if (!isBitcoinConnected && !isEthereumConnected) { return { success: false, error: 'No wallet connected' }; } + // Determine which wallet is active + const walletType = isBitcoinConnected ? 'bitcoin' : 'ethereum'; + const address = this.getActiveAddress(); + + if (!address) { + return { + success: false, + error: 'No wallet address available' + }; + } + const user: User = { - address: walletInfo.address, - walletType: walletInfo.walletType, + address: address, + walletType: walletType, verificationStatus: 'unverified', lastChecked: Date.now(), }; - // Add ENS info for Ethereum wallets - if (walletInfo.walletType === 'ethereum' && walletInfo.ensName) { - user.ensName = walletInfo.ensName; - user.ensOwnership = true; + // Add ENS info for Ethereum wallets (if available) + if (walletType === 'ethereum') { + // Note: ENS resolution would need to be implemented separately + // For now, we'll leave it as undefined + user.ensName = undefined; + user.ensOwnership = false; } return { @@ -67,13 +123,23 @@ export class AuthService { } /** - * Disconnect wallet and clear user data + * Disconnect wallet and clear stored data */ async disconnectWallet(): Promise { - const walletType = this.walletService.getActiveWalletType(); - if (walletType) { - await this.walletService.disconnectWallet(walletType); - } + // Clear any existing delegations when disconnecting + this.keyDelegation.clearDelegation(); + this.walletService.clearDelegation('bitcoin'); + this.walletService.clearDelegation('ethereum'); + + // Clear stored user data + this.clearStoredUser(); + } + + /** + * Clear delegation for current wallet + */ + clearDelegation(): void { + this.keyDelegation.clearDelegation(); } /** @@ -124,13 +190,14 @@ export class AuthService { * Verify Ethereum ENS ownership */ private async verifyEthereumENS(user: User): Promise { - const walletInfo = await this.walletService.getWalletInfo(); - const hasENS = walletInfo?.ensName && walletInfo.ensName.length > 0; + // Note: ENS resolution would need to be implemented separately + // For now, we'll assume no ENS ownership + const hasENS = false; const updatedUser = { ...user, ensOwnership: hasENS, - ensName: walletInfo?.ensName, + ensName: undefined, lastChecked: Date.now(), }; @@ -146,24 +213,35 @@ export class AuthService { async delegateKey(user: User): Promise { try { const walletType = user.walletType; - const canConnect = await this.walletService.canConnectWallet(walletType); - if (!canConnect) { + const isAvailable = this.walletService.isWalletAvailable(walletType); + + if (!isAvailable) { return { success: false, - error: `${walletType} wallet is not available or cannot be connected. Please ensure it is installed and unlocked.` + error: `${walletType} wallet is not available or connected. Please ensure it is connected.` }; } - const delegationInfo = await this.walletService.setupKeyDelegation( - user.address, - walletType - ); + const success = await this.walletService.createKeyDelegation(walletType); + + if (!success) { + return { + success: false, + error: 'Failed to create key delegation' + }; + } + // Get delegation status to update user + const delegationStatus = this.walletService.getDelegationStatus(walletType); + + // Get the actual browser public key from the delegation + const browserPublicKey = this.keyDelegation.getBrowserPublicKey(); + const updatedUser = { ...user, - browserPubKey: delegationInfo.browserPublicKey, - delegationSignature: delegationInfo.signature, - delegationExpiry: delegationInfo.expiryTimestamp, + browserPubKey: browserPublicKey || undefined, + delegationSignature: delegationStatus.isValid ? 'valid' : undefined, + delegationExpiry: delegationStatus.timeRemaining ? Date.now() + delegationStatus.timeRemaining : undefined, }; return { @@ -196,21 +274,49 @@ export class AuthService { * Check if delegation is valid */ isDelegationValid(): boolean { - return this.walletService.isDelegationValid(); + // Only check the currently connected wallet type + const activeWalletType = this.getActiveWalletType(); + if (!activeWalletType) return false; + + const status = this.walletService.getDelegationStatus(activeWalletType); + return status.isValid; } /** * Get delegation time remaining */ getDelegationTimeRemaining(): number { - return this.walletService.getDelegationTimeRemaining(); + // Only check the currently connected wallet type + const activeWalletType = this.getActiveWalletType(); + if (!activeWalletType) return 0; + + const status = this.walletService.getDelegationStatus(activeWalletType); + return status.timeRemaining || 0; } /** * Get current wallet info */ async getWalletInfo() { - return await this.walletService.getWalletInfo(); + // Return basic wallet info based on what's available + const isBitcoinConnected = this.walletService.isWalletAvailable('bitcoin'); + const isEthereumConnected = this.walletService.isWalletAvailable('ethereum'); + + if (isBitcoinConnected) { + return { + address: this.getActiveAddress(), + walletType: 'bitcoin' as const, + isConnected: true + }; + } else if (isEthereumConnected) { + return { + address: this.getActiveAddress(), + walletType: 'ethereum' as const, + isConnected: true + }; + } + + return null; } /** diff --git a/src/lib/identity/signatures/key-delegation.ts b/src/lib/identity/signatures/key-delegation.ts index e7dd93e..678bd10 100644 --- a/src/lib/identity/signatures/key-delegation.ts +++ b/src/lib/identity/signatures/key-delegation.ts @@ -36,45 +36,47 @@ export class KeyDelegation { } /** - * Creates a delegation message to be signed by the Bitcoin wallet + * Creates a delegation message to be signed by the wallet * @param browserPublicKey The browser-generated public key - * @param bitcoinAddress The user's Bitcoin address + * @param walletAddress The user's wallet address * @param expiryTimestamp When the delegation will expire * @returns The message to be signed */ createDelegationMessage( browserPublicKey: string, - bitcoinAddress: string, + walletAddress: string, expiryTimestamp: number ): string { - return `I, ${bitcoinAddress}, delegate authority to this pubkey: ${browserPublicKey} until ${expiryTimestamp}`; + return `I, ${walletAddress}, delegate authority to this pubkey: ${browserPublicKey} until ${expiryTimestamp}`; } - + /** - * Creates a delegation with the specified expiry time in hours - * @param bitcoinAddress The Bitcoin wallet address - * @param signature The signature from the Bitcoin wallet - * @param browserPublicKey The browser public key - * @param browserPrivateKey The browser private key - * @param expiryHours How many hours the delegation should be valid (default: 24) - * @returns The created delegation info + * Creates a delegation object from the signed message + * @param walletAddress The wallet address that signed the delegation + * @param signature The signature from the wallet + * @param browserPublicKey The browser-generated public key + * @param browserPrivateKey The browser-generated private key + * @param expiryHours How many hours the delegation should last + * @param walletType The type of wallet (bitcoin or ethereum) + * @returns DelegationInfo object */ createDelegation( - bitcoinAddress: string, + walletAddress: string, signature: string, browserPublicKey: string, browserPrivateKey: string, - expiryHours: number = KeyDelegation.DEFAULT_EXPIRY_HOURS + expiryHours: number = KeyDelegation.DEFAULT_EXPIRY_HOURS, + walletType: 'bitcoin' | 'ethereum' ): DelegationInfo { - const now = Date.now(); - const expiryTimestamp = now + (expiryHours * 60 * 60 * 1000); + const expiryTimestamp = Date.now() + (expiryHours * 60 * 60 * 1000); return { signature, expiryTimestamp, browserPublicKey, browserPrivateKey, - bitcoinAddress + walletAddress, + walletType }; } @@ -103,15 +105,30 @@ export class KeyDelegation { } /** - * Checks if a delegation is valid (exists and not expired) + * Checks if a delegation is valid (exists, not expired, and matches current wallet) + * @param currentAddress Optional current wallet address to validate against + * @param currentWalletType Optional current wallet type to validate against * @returns boolean indicating if the delegation is valid */ - isDelegationValid(): boolean { + isDelegationValid(currentAddress?: string, currentWalletType?: 'bitcoin' | 'ethereum'): boolean { const delegation = this.retrieveDelegation(); if (!delegation) return false; + // Check if delegation has expired const now = Date.now(); - return now < delegation.expiryTimestamp; + if (now >= delegation.expiryTimestamp) return false; + + // If a current address is provided, validate it matches the delegation + if (currentAddress && delegation.walletAddress !== currentAddress) { + return false; + } + + // If a current wallet type is provided, validate it matches the delegation + if (currentWalletType && delegation.walletType !== currentWalletType) { + return false; + } + + return true; } /** @@ -166,7 +183,7 @@ export class KeyDelegation { getDelegatingAddress(): string | null { const delegation = this.retrieveDelegation(); if (!delegation || !this.isDelegationValid()) return null; - return delegation.bitcoinAddress; + return delegation.walletAddress; } /** diff --git a/src/lib/identity/signatures/types.ts b/src/lib/identity/signatures/types.ts index e73e3f3..62fa08a 100644 --- a/src/lib/identity/signatures/types.ts +++ b/src/lib/identity/signatures/types.ts @@ -1,8 +1,9 @@ export interface DelegationSignature { - signature: string; // Signature from Bitcoin wallet + signature: string; // Signature from wallet expiryTimestamp: number; // When this delegation expires browserPublicKey: string; // Browser-generated public key that was delegated to - bitcoinAddress: string; // Bitcoin address that signed the delegation + walletAddress: string; // Wallet address that signed the delegation + walletType: 'bitcoin' | 'ethereum'; // Type of wallet that created the delegation } export interface DelegationInfo extends DelegationSignature { diff --git a/src/lib/identity/wallets/ReOwnWalletService.ts b/src/lib/identity/wallets/ReOwnWalletService.ts index 1f00d57..e206c17 100644 --- a/src/lib/identity/wallets/ReOwnWalletService.ts +++ b/src/lib/identity/wallets/ReOwnWalletService.ts @@ -1,28 +1,14 @@ +import { UseAppKitAccountReturn } from '@reown/appkit/react'; import { KeyDelegation } from '../signatures/key-delegation'; -import { bytesToHex } from '@/lib/utils'; -import { getEnsName } from '@wagmi/core'; -import { config } from './appkit'; -import { UseAppKitAccountReturn } from '@reown/appkit'; - - - -export interface WalletInfo { - address: string; - walletType: 'bitcoin' | 'ethereum'; - ensName?: string; - isConnected: boolean; -} - -export interface DelegationInfo { - browserPublicKey: string; - signature: string; - expiryTimestamp: number; -} +import { AppKit } from '@reown/appkit'; +import { ChainNamespace } from '@reown/appkit-common'; +import { Provider} from '@reown/appkit-controllers'; export class ReOwnWalletService { private keyDelegation: KeyDelegation; private bitcoinAccount?: UseAppKitAccountReturn; private ethereumAccount?: UseAppKitAccountReturn; + private appKit?: AppKit; constructor() { this.keyDelegation = new KeyDelegation(); @@ -36,190 +22,189 @@ export class ReOwnWalletService { this.ethereumAccount = ethereumAccount; } + /** + * Set the AppKit instance for accessing adapters + */ + setAppKit(appKit: AppKit) { + this.appKit = appKit; + } + /** * Check if a wallet type is available and connected */ isWalletAvailable(walletType: 'bitcoin' | 'ethereum'): boolean { if (walletType === 'bitcoin') { - return this.bitcoinAccount?.isConnected || false; + return this.bitcoinAccount?.isConnected ?? false; } else { - return this.ethereumAccount?.isConnected || false; + return this.ethereumAccount?.isConnected ?? false; } } /** - * Check if wallet can be connected + * Get the active account based on wallet type */ - async canConnectWallet(walletType: 'bitcoin' | 'ethereum'): Promise { - // For ReOwn, we assume connection is always possible if AppKit is initialized - return true; + private getActiveAccount(walletType: 'bitcoin' | 'ethereum'): UseAppKitAccountReturn | undefined { + return walletType === 'bitcoin' ? this.bitcoinAccount : this.ethereumAccount; } /** - * Get wallet connection info with ENS resolution for Ethereum + * Get the active address for a given wallet type */ - async getWalletInfo(): Promise { - if (this.bitcoinAccount?.isConnected) { - return { - address: this.bitcoinAccount.address, - walletType: 'bitcoin', - isConnected: true - }; - } else if (this.ethereumAccount?.isConnected) { - // Use Wagmi to resolve ENS name - let ensName: string | undefined; - try { - const resolvedName = await getEnsName(config, { - address: this.ethereumAccount.address as `0x${string}` - }); - ensName = resolvedName || undefined; - } catch (error) { - console.warn('Failed to resolve ENS name:', error); - // Continue without ENS name + getActiveAddress(walletType: 'bitcoin' | 'ethereum'): string | undefined { + const account = this.getActiveAccount(walletType); + return account?.address; + } + + /** + * Get the appropriate namespace for the wallet type + */ + private getNamespace(walletType: 'bitcoin' | 'ethereum'): ChainNamespace { + return walletType === 'bitcoin' ? 'bip122' : 'eip155'; + } + + /** + * Sign a message using the appropriate adapter + */ + async signMessage(messageBytes: Uint8Array, walletType: 'bitcoin' | 'ethereum'): Promise { + if (!this.appKit) { + throw new Error('AppKit instance not set. Call setAppKit() first.'); + } + + const account = this.getActiveAccount(walletType); + if (!account?.address) { + throw new Error(`No ${walletType} wallet connected`); + } + + const namespace = this.getNamespace(walletType); + + // Convert message bytes to string for signing + const messageString = new TextDecoder().decode(messageBytes); + + try { + // Access the adapter through the appKit instance + // The adapter is available through the appKit's chainAdapters property + const adapter = this.appKit.chainAdapters?.[namespace]; + + if (!adapter) { + throw new Error(`No adapter found for namespace: ${namespace}`); } - return { - address: this.ethereumAccount.address, - walletType: 'ethereum', - ensName, - isConnected: true - }; + // Get the provider for the current connection + const provider = this.appKit.getProvider(namespace); + + if (!provider) { + throw new Error(`No provider found for namespace: ${namespace}`); + } + + // Call the adapter's signMessage method + const result = await adapter.signMessage({ + message: messageString, + address: account.address, + provider: provider as Provider + }); + + return result.signature; + } catch (error) { + console.error(`Error signing message with ${walletType} wallet:`, error); + throw new Error(`Failed to sign message with ${walletType} wallet: ${error instanceof Error ? error.message : 'Unknown error'}`); } - return null; } /** - * Get the active wallet address + * Create a key delegation for the connected wallet */ - getActiveAddress(): string | null { - if (this.bitcoinAccount?.isConnected) { - return this.bitcoinAccount.address; - } else if (this.ethereumAccount?.isConnected) { - return this.ethereumAccount.address; + async createKeyDelegation(walletType: 'bitcoin' | 'ethereum'): Promise { + try { + const account = this.getActiveAccount(walletType); + if (!account?.address) { + throw new Error(`No ${walletType} wallet connected`); + } + + // Generate a new browser keypair + const keypair = this.keyDelegation.generateKeypair(); + + // Create delegation message with expiry + const expiryTimestamp = Date.now() + (24 * 60 * 60 * 1000); // 24 hours + const delegationMessage = this.keyDelegation.createDelegationMessage( + keypair.publicKey, + account.address, + expiryTimestamp + ); + + const messageBytes = new TextEncoder().encode(delegationMessage); + + // Sign the delegation message + const signature = await this.signMessage(messageBytes, walletType); + + // Create and store the delegation + const delegationInfo = this.keyDelegation.createDelegation( + account.address, + signature, + keypair.publicKey, + keypair.privateKey, + 24, // 24 hours + walletType + ); + + this.keyDelegation.storeDelegation(delegationInfo); + + return true; + } catch (error) { + console.error(`Error creating key delegation for ${walletType}:`, error); + return false; } - return null; } /** - * Get the active wallet type + * Sign a message using the delegated key (if available) or fall back to wallet signing */ - getActiveWalletType(): 'bitcoin' | 'ethereum' | null { - if (this.bitcoinAccount?.isConnected) { - return 'bitcoin'; - } else if (this.ethereumAccount?.isConnected) { - return 'ethereum'; + async signMessageWithDelegation(messageBytes: Uint8Array, walletType: 'bitcoin' | 'ethereum'): Promise { + const account = this.getActiveAccount(walletType); + if (!account?.address) { + throw new Error(`No ${walletType} wallet connected`); } - return null; + + // Check if we have a valid delegation for this specific wallet + if (this.keyDelegation.isDelegationValid(account.address, walletType)) { + // Use delegated key for signing + const messageString = new TextDecoder().decode(messageBytes); + const signature = this.keyDelegation.signMessage(messageString); + + if (signature) { + return signature; + } + } + + // Fall back to wallet signing + return this.signMessage(messageBytes, walletType); } /** - * Setup key delegation for the connected wallet + * Get delegation status for the connected wallet */ - async setupKeyDelegation( - address: string, - walletType: 'bitcoin' | 'ethereum' - ): Promise { - // Generate browser keypair - const keypair = this.keyDelegation.generateKeypair(); + getDelegationStatus(walletType: 'bitcoin' | 'ethereum'): { + hasDelegation: boolean; + isValid: boolean; + timeRemaining?: number; + } { + const account = this.getActiveAccount(walletType); + const currentAddress = account?.address; - // Create delegation message with chain-specific format - const expiryTimestamp = Date.now() + (24 * 60 * 60 * 1000); // 24 hours - const delegationMessage = this.createDelegationMessage( - keypair.publicKey, - address, - walletType, - expiryTimestamp - ); - - // Get the appropriate account for signing - const account = walletType === 'bitcoin' ? this.bitcoinAccount : this.ethereumAccount; - if (!account?.isConnected) { - throw new Error(`${walletType} wallet is not connected`); - } - - // Sign the delegation message - const signature = await this.signMessage(delegationMessage, walletType); - - // Create and store delegation - const delegationInfo = this.keyDelegation.createDelegation( - address, - signature, - keypair.publicKey, - keypair.privateKey, - 24 - ); - - this.keyDelegation.storeDelegation(delegationInfo); + const hasDelegation = this.keyDelegation.retrieveDelegation() !== null; + const isValid = this.keyDelegation.isDelegationValid(currentAddress, walletType); + const timeRemaining = this.keyDelegation.getDelegationTimeRemaining(); return { - browserPublicKey: keypair.publicKey, - signature, - expiryTimestamp + hasDelegation, + isValid, + timeRemaining: timeRemaining > 0 ? timeRemaining : undefined }; } /** - * Create chain-specific delegation message + * Clear delegation for the connected wallet */ - private createDelegationMessage( - browserPublicKey: string, - address: string, - walletType: 'bitcoin' | 'ethereum', - expiryTimestamp: number - ): string { - const chainName = walletType === 'bitcoin' ? 'Bitcoin' : 'Ethereum'; - const expiryDate = new Date(expiryTimestamp).toISOString(); - - return `I, ${address} (${chainName}), delegate authority to this pubkey: ${browserPublicKey} until ${expiryDate}`; - } - - /** - * Sign a message with the appropriate wallet - */ - private async signMessage(message: string, walletType: 'bitcoin' | 'ethereum'): Promise { - const account = walletType === 'bitcoin' ? this.bitcoinAccount : this.ethereumAccount; - if (!account?.isConnected) { - throw new Error(`${walletType} wallet is not connected`); - } - - // Convert message to bytes for signing - const messageBytes = new TextEncoder().encode(message); - - // Sign with the appropriate wallet - const signature = await account.signMessage({ message: messageBytes }); - - // Return hex-encoded signature - return bytesToHex(signature); - } - - /** - * Disconnect wallet (handled by AppKit) - */ - async disconnectWallet(walletType: 'bitcoin' | 'ethereum'): Promise { - // Clear stored delegation + clearDelegation(walletType: 'bitcoin' | 'ethereum'): void { this.keyDelegation.clearDelegation(); - - // Note: Actual disconnection is handled by AppKit's useDisconnect hook - } - - /** - * Check if delegation is valid - */ - isDelegationValid(): boolean { - return this.keyDelegation.isDelegationValid(); - } - - /** - * Get delegation time remaining - */ - getDelegationTimeRemaining(): number { - return this.keyDelegation.getDelegationTimeRemaining(); - } - - /** - * Get the key delegation instance - */ - getKeyDelegation(): KeyDelegation { - return this.keyDelegation; } } \ No newline at end of file diff --git a/src/lib/identity/wallets/index.ts b/src/lib/identity/wallets/index.ts index 3b5b7c3..aad497c 100644 --- a/src/lib/identity/wallets/index.ts +++ b/src/lib/identity/wallets/index.ts @@ -1,2 +1 @@ -export { ReOwnWalletService as WalletService } from './ReOwnWalletService'; -export type { WalletInfo, DelegationInfo, AppKitAccount } from './ReOwnWalletService'; \ No newline at end of file +export { ReOwnWalletService as WalletService } from './ReOwnWalletService'; \ No newline at end of file