fix: delegation + signatures

This commit is contained in:
Danish Arora 2025-08-06 17:21:56 +05:30
parent be55804d91
commit 539c596974
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
11 changed files with 736 additions and 586 deletions

View File

@ -24,7 +24,6 @@ import NotFound from "./pages/NotFound";
import Dashboard from "./pages/Dashboard"; import Dashboard from "./pages/Dashboard";
import Index from "./pages/Index"; import Index from "./pages/Index";
import { appkitConfig } from "./lib/identity/wallets/appkit"; import { appkitConfig } from "./lib/identity/wallets/appkit";
import { createAppKit } from "@reown/appkit";
import { WagmiProvider } from "wagmi"; import { WagmiProvider } from "wagmi";
import { config } from "./lib/identity/wallets/appkit"; import { config } from "./lib/identity/wallets/appkit";
import { AppKitProvider } from "@reown/appkit/react"; import { AppKitProvider } from "@reown/appkit/react";
@ -32,8 +31,6 @@ import { AppKitProvider } from "@reown/appkit/react";
// Create a client // Create a client
const queryClient = new QueryClient(); const queryClient = new QueryClient();
createAppKit(appkitConfig);
const App = () => ( const App = () => (
<WagmiProvider config={config}> <WagmiProvider config={config}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>

View File

@ -1,7 +1,6 @@
import * as React from "react"; import * as React from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Key, Loader2, CheckCircle, AlertCircle, Trash2 } from "lucide-react";
import { Key, Clock, Shield, Loader2, CheckCircle, AlertCircle } from "lucide-react";
import { useAuth } from "@/contexts/useAuth"; import { useAuth } from "@/contexts/useAuth";
interface DelegationStepProps { interface DelegationStepProps {
@ -22,7 +21,8 @@ export function DelegationStep({
delegateKey, delegateKey,
isDelegationValid, isDelegationValid,
delegationTimeRemaining, delegationTimeRemaining,
isAuthenticating isAuthenticating,
clearDelegation
} = useAuth(); } = useAuth();
const [delegationResult, setDelegationResult] = React.useState<{ const [delegationResult, setDelegationResult] = React.useState<{
@ -47,13 +47,13 @@ export function DelegationStep({
setDelegationResult({ setDelegationResult({
success: true, success: true,
message: "Key delegation successful! You can now interact with the forum without additional wallet approvals.", message: "Key delegation successful!",
expiry: expiryDate expiry: expiryDate
}); });
} else { } else {
setDelegationResult({ setDelegationResult({
success: false, 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) { } catch (error) {
@ -70,155 +70,171 @@ export function DelegationStep({
onComplete(); onComplete();
}; };
const formatTimeRemaining = () => { const handleRefresh = () => {
const remaining = delegationTimeRemaining(); window.location.reload();
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`;
}
}; };
// Show delegation result // Show delegation result
if (delegationResult) { if (delegationResult) {
return ( return (
<div className="space-y-4"> <div className="flex flex-col h-full">
<div className={`p-4 rounded-lg border ${ <div className="flex-1 space-y-4">
delegationResult.success <div className={`p-4 rounded-lg border ${
? 'bg-green-900/20 border-green-500/30' delegationResult.success
: 'bg-yellow-900/20 border-yellow-500/30' ? 'bg-green-900/20 border-green-500/30'
}`}> : 'bg-yellow-900/20 border-yellow-500/30'
<div className="flex items-center gap-2 mb-2"> }`}>
{delegationResult.success ? ( <div className="flex items-center gap-2 mb-2">
<CheckCircle className="h-5 w-5 text-green-500" /> {delegationResult.success ? (
) : ( <CheckCircle className="h-5 w-5 text-green-500" />
<AlertCircle className="h-5 w-5 text-yellow-500" /> ) : (
)} <AlertCircle className="h-5 w-5 text-yellow-500" />
<span className={`font-medium ${ )}
delegationResult.success ? 'text-green-400' : 'text-yellow-400' <span className={`font-medium ${
}`}> delegationResult.success ? 'text-green-400' : 'text-yellow-400'
{delegationResult.success ? 'Delegation Complete' : 'Delegation Result'} }`}>
</span> {delegationResult.success ? 'Delegation Complete' : 'Delegation Result'}
</div> </span>
<p className="text-sm text-neutral-300 mb-2">
{delegationResult.message}
</p>
{delegationResult.expiry && (
<div className="text-xs text-neutral-400">
<p>Expires: {delegationResult.expiry}</p>
</div> </div>
)} <p className="text-sm text-neutral-300 mb-2">
</div> {delegationResult.message}
</p>
<Button {delegationResult.expiry && (
onClick={handleComplete} <div className="text-xs text-neutral-400">
className="w-full bg-green-600 hover:bg-green-700 text-white" <p>Expires: {delegationResult.expiry}</p>
disabled={isLoading} </div>
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Processing...
</>
) : (
"Complete Setup"
)}
</Button>
</div>
);
}
// Show existing delegation status
if (isDelegationValid()) {
return (
<div className="space-y-4">
<div className="p-4 bg-green-900/20 border border-green-500/30 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<CheckCircle className="h-5 w-5 text-green-500" />
<span className="text-green-400 font-medium">Key Already Delegated</span>
</div>
<p className="text-sm text-neutral-300 mb-2">
You already have an active key delegation.
</p>
<div className="text-xs text-neutral-400">
<p>Time remaining: {formatTimeRemaining()}</p>
{currentUser?.delegationExpiry && (
<p>Expires: {new Date(currentUser.delegationExpiry).toLocaleString()}</p>
)} )}
</div> </div>
</div> </div>
<Button {/* Action Button */}
onClick={handleComplete} <div className="mt-auto">
className="w-full bg-green-600 hover:bg-green-700 text-white" <Button
disabled={isLoading} onClick={handleComplete}
> className="w-full bg-green-600 hover:bg-green-700 text-white"
Complete Setup disabled={isLoading}
</Button> >
Complete Setup
</Button>
</div>
</div> </div>
); );
} }
// Show delegation form // Show minimal delegation status
return ( return (
<div className="space-y-4"> <div className="flex flex-col h-full">
<div className="text-center space-y-2"> <div className="flex-1 space-y-4">
<div className="flex justify-center"> <div className="text-center space-y-2">
<Key className="h-8 w-8 text-blue-500" /> <div className="flex justify-center">
</div> <Key className="h-8 w-8 text-blue-500" />
<h3 className="text-lg font-semibold text-white">
Delegate Signing Key
</h3>
<p className="text-sm text-neutral-400">
Create a browser-based signing key for better user experience
</p>
</div>
<div className="p-4 bg-neutral-900/50 border border-neutral-700 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Shield className="h-4 w-4 text-blue-500" />
<span className="text-sm font-medium text-white">What is key delegation?</span>
</div>
<ul className="text-xs text-neutral-400 space-y-1">
<li> Creates a browser-based signing key for 24 hours</li>
<li> Allows posting, commenting, and voting without wallet approval</li>
<li> Automatically expires for security</li>
<li> Can be renewed anytime</li>
</ul>
</div>
{currentUser?.browserPubKey && (
<div className="p-3 bg-neutral-900/30 border border-neutral-600 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Key className="h-4 w-4 text-green-500" />
<span className="text-sm font-medium text-white">Browser Key Generated</span>
</div> </div>
<p className="text-xs text-neutral-400 font-mono break-all"> <h3 className="text-lg font-semibold text-white">
{currentUser.browserPubKey.slice(0, 20)}...{currentUser.browserPubKey.slice(-20)} Delegate Signing Key
</h3>
<p className="text-sm text-neutral-400">
Create a browser-based signing key
</p> </p>
</div> </div>
)}
<div className="space-y-3"> {/* Delegation Status */}
<Button <div className="p-4 bg-neutral-900/30 border border-neutral-700 rounded-lg">
onClick={handleDelegate} <div className="flex items-center justify-between mb-3">
disabled={isLoading || isAuthenticating} <div className="flex items-center gap-2">
className="w-full bg-blue-600 hover:bg-blue-700 text-white" <Key className="h-4 w-4 text-blue-500" />
> <span className="text-sm font-medium text-white">Key Delegation</span>
{isLoading || isAuthenticating ? ( </div>
<> {currentUser?.walletType === 'bitcoin' ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <div className="text-orange-500 text-sm"></div>
Delegating Key... ) : (
</> <div className="text-blue-500 text-sm">Ξ</div>
) : ( )}
"Delegate Signing Key" </div>
)}
</Button> <div className="space-y-3">
{/* Status */}
<div className="flex items-center gap-2">
{isDelegationValid() ? (
<CheckCircle className="h-4 w-4 text-green-500" />
) : (
<AlertCircle className="h-4 w-4 text-yellow-500" />
)}
<span className={`text-sm font-medium ${
isDelegationValid() ? 'text-green-400' : 'text-yellow-400'
}`}>
{isDelegationValid() ? 'Delegated' : 'Required'}
</span>
{isDelegationValid() && (
<span className="text-xs text-neutral-400">
{Math.floor(delegationTimeRemaining() / (1000 * 60 * 60))}h {Math.floor((delegationTimeRemaining() % (1000 * 60 * 60)) / (1000 * 60))}m remaining
</span>
)}
</div>
{/* Delegated Browser Public Key */}
{isDelegationValid() && currentUser?.browserPubKey && (
<div className="text-xs text-neutral-400">
<div className="font-mono break-all bg-neutral-800 p-2 rounded">
{currentUser.browserPubKey}
</div>
</div>
)}
{/* Wallet Address */}
{currentUser && (
<div className="text-xs text-neutral-400">
<div className="font-mono break-all">
{currentUser.address}
</div>
</div>
)}
{/* Delete Button for Active Delegations */}
{isDelegationValid() && (
<div className="flex justify-end">
<Button
onClick={clearDelegation}
variant="outline"
size="sm"
className="border-red-600/50 text-red-400 hover:bg-red-600/20 hover:border-red-500 text-xs px-2 py-1 h-auto"
>
<Trash2 className="mr-1 h-3 w-3" />
Remove
</Button>
</div>
)}
</div>
</div>
</div>
{/* Action Buttons */}
<div className="mt-auto space-y-3">
{!isDelegationValid() && (
<Button
onClick={handleDelegate}
disabled={isLoading || isAuthenticating}
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
>
{isLoading || isAuthenticating ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Delegating...
</>
) : (
"Delegate Key"
)}
</Button>
)}
{isDelegationValid() && (
<Button
onClick={handleComplete}
className="w-full bg-green-600 hover:bg-green-700 text-white"
disabled={isLoading}
>
Complete Setup
</Button>
)}
<Button <Button
onClick={onBack} onClick={onBack}
@ -226,14 +242,9 @@ export function DelegationStep({
className="w-full border-neutral-600 text-neutral-400 hover:bg-neutral-800" className="w-full border-neutral-600 text-neutral-400 hover:bg-neutral-800"
disabled={isLoading || isAuthenticating} disabled={isLoading || isAuthenticating}
> >
Back to Verification Back
</Button> </Button>
</div> </div>
<div className="text-xs text-neutral-500 text-center space-y-1">
<p>Key delegation is optional but recommended for better UX</p>
<p>You can still use the forum without delegation</p>
</div>
</div> </div>
); );
} }

View File

@ -103,52 +103,50 @@ export function VerificationStep({
// Show verification result // Show verification result
if (verificationResult) { if (verificationResult) {
return ( return (
<div className="space-y-4"> <div className="flex flex-col h-full">
<div className={`p-4 rounded-lg border ${ <div className="flex-1 space-y-4">
verificationResult.success <div className={`p-4 rounded-lg border ${
? 'bg-green-900/20 border-green-500/30' verificationResult.success
: 'bg-yellow-900/20 border-yellow-500/30' ? 'bg-green-900/20 border-green-500/30'
}`}> : 'bg-yellow-900/20 border-yellow-500/30'
<div className="flex items-center gap-2 mb-2"> }`}>
{verificationResult.success ? ( <div className="flex items-center gap-2 mb-2">
<ShieldCheck className="h-5 w-5 text-green-500" /> {verificationResult.success ? (
) : ( <ShieldCheck className="h-5 w-5 text-green-500" />
<AlertCircle className="h-5 w-5 text-yellow-500" />
)}
<span className={`font-medium ${
verificationResult.success ? 'text-green-400' : 'text-yellow-400'
}`}>
{verificationResult.success ? 'Verification Complete' : 'Verification Result'}
</span>
</div>
<p className="text-sm text-neutral-300 mb-2">
{verificationResult.message}
</p>
{verificationResult.details && (
<div className="text-xs text-neutral-400">
{walletType === 'bitcoin' ? (
<p>Ordinal ID: {verificationResult.details.id}</p>
) : ( ) : (
<p>ENS Name: {verificationResult.details.ensName}</p> <AlertCircle className="h-5 w-5 text-yellow-500" />
)} )}
<span className={`font-medium ${
verificationResult.success ? 'text-green-400' : 'text-yellow-400'
}`}>
{verificationResult.success ? 'Verification Complete' : 'Verification Result'}
</span>
</div> </div>
)} <p className="text-sm text-neutral-300 mb-2">
{verificationResult.message}
</p>
{verificationResult.details && (
<div className="text-xs text-neutral-400">
{walletType === 'bitcoin' ? (
<p>Ordinal ID: {verificationResult.details.id}</p>
) : (
<p>ENS Name: {verificationResult.details.ensName}</p>
)}
</div>
)}
</div>
</div> </div>
<Button {/* Action Button */}
onClick={handleNext} <div className="mt-auto">
className="w-full bg-blue-600 hover:bg-blue-700 text-white" <Button
disabled={isLoading} onClick={handleNext}
> className="w-full bg-blue-600 hover:bg-blue-700 text-white"
{isLoading ? ( disabled={isLoading}
<> >
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Next
Processing... </Button>
</> </div>
) : (
"Continue to Key Delegation"
)}
</Button>
</div> </div>
); );
} }
@ -156,78 +154,90 @@ export function VerificationStep({
// Show verification status // Show verification status
if (verificationStatus === 'verified-owner') { if (verificationStatus === 'verified-owner') {
return ( return (
<div className="space-y-4"> <div className="flex flex-col h-full">
<div className="p-4 bg-green-900/20 border border-green-500/30 rounded-lg"> <div className="flex-1 space-y-4">
<div className="flex items-center gap-2 mb-2"> <div className="p-4 bg-green-900/20 border border-green-500/30 rounded-lg">
<ShieldCheck className="h-5 w-5 text-green-500" /> <div className="flex items-center gap-2 mb-2">
<span className="text-green-400 font-medium">Already Verified</span> <ShieldCheck className="h-5 w-5 text-green-500" />
</div> <span className="text-green-400 font-medium">Already Verified</span>
<p className="text-sm text-neutral-300 mb-2">
Your {getVerificationType()} ownership has been verified.
</p>
{currentUser && (
<div className="text-xs text-neutral-400">
{walletType === 'bitcoin' && currentUser.ordinalOwnership && (
<p>Ordinal ID: {typeof currentUser.ordinalOwnership === 'object' ? currentUser.ordinalOwnership.id : 'Verified'}</p>
)}
{walletType === 'ethereum' && currentUser.ensName && (
<p>ENS Name: {currentUser.ensName}</p>
)}
</div> </div>
)} <p className="text-sm text-neutral-300 mb-2">
Your {getVerificationType()} ownership has been verified.
</p>
{currentUser && (
<div className="text-xs text-neutral-400">
{walletType === 'bitcoin' && currentUser.ordinalOwnership && (
<p>Ordinal ID: {typeof currentUser.ordinalOwnership === 'object' ? currentUser.ordinalOwnership.id : 'Verified'}</p>
)}
{walletType === 'ethereum' && currentUser.ensName && (
<p>ENS Name: {currentUser.ensName}</p>
)}
</div>
)}
</div>
</div> </div>
<Button {/* Action Button */}
onClick={handleNext} <div className="mt-auto">
className="w-full bg-blue-600 hover:bg-blue-700 text-white" <Button
disabled={isLoading} onClick={handleNext}
> className="w-full bg-blue-600 hover:bg-blue-700 text-white"
Continue to Key Delegation disabled={isLoading}
</Button> >
Next
</Button>
</div>
</div> </div>
); );
} }
// Show verification form // Show verification form
return ( return (
<div className="space-y-4"> <div className="flex flex-col h-full">
<div className="text-center space-y-2"> <div className="flex-1 space-y-4">
<div className="flex justify-center"> <div className="text-center space-y-2">
{React.createElement(getVerificationIcon(), { <div className="flex justify-center">
className: `h-8 w-8 ${getVerificationColor()}` {React.createElement(getVerificationIcon(), {
})} className: `h-8 w-8 ${getVerificationColor()}`
})}
</div>
<h3 className="text-lg font-semibold text-white">
Verify {getVerificationType()} Ownership
</h3>
<p className="text-sm text-neutral-400">
{getVerificationDescription()}
</p>
</div>
<div className="p-4 bg-neutral-900/50 border border-neutral-700 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Shield className="h-4 w-4 text-blue-500" />
<span className="text-sm font-medium text-white">What happens during verification?</span>
</div>
<ul className="text-xs text-neutral-400 space-y-1">
{walletType === 'bitcoin' ? (
<>
<li> We'll check your wallet for Bitcoin Ordinal ownership</li>
<li> If found, you'll get full posting and voting access</li>
<li> If not found, you'll have read-only access</li>
</>
) : (
<>
<li> We'll check your wallet for ENS domain ownership</li>
<li> If found, you'll get full posting and voting access</li>
<li> If not found, you'll have read-only access</li>
</>
)}
</ul>
</div>
<div className="text-xs text-neutral-500 text-center">
Verification is required to access posting and voting features
</div> </div>
<h3 className="text-lg font-semibold text-white">
Verify {getVerificationType()} Ownership
</h3>
<p className="text-sm text-neutral-400">
{getVerificationDescription()}
</p>
</div> </div>
<div className="p-4 bg-neutral-900/50 border border-neutral-700 rounded-lg"> {/* Action Buttons */}
<div className="flex items-center gap-2 mb-2"> <div className="mt-auto space-y-3">
<Shield className="h-4 w-4 text-blue-500" />
<span className="text-sm font-medium text-white">What happens during verification?</span>
</div>
<ul className="text-xs text-neutral-400 space-y-1">
{walletType === 'bitcoin' ? (
<>
<li> We'll check your wallet for Bitcoin Ordinal ownership</li>
<li> If found, you'll get full posting and voting access</li>
<li> If not found, you'll have read-only access</li>
</>
) : (
<>
<li> We'll check your wallet for ENS domain ownership</li>
<li> If found, you'll get full posting and voting access</li>
<li> If not found, you'll have read-only access</li>
</>
)}
</ul>
</div>
<div className="space-y-3">
<Button <Button
onClick={handleVerify} onClick={handleVerify}
disabled={isLoading || isAuthenticating} disabled={isLoading || isAuthenticating}
@ -249,13 +259,9 @@ export function VerificationStep({
className="w-full border-neutral-600 text-neutral-400 hover:bg-neutral-800" className="w-full border-neutral-600 text-neutral-400 hover:bg-neutral-800"
disabled={isLoading || isAuthenticating} disabled={isLoading || isAuthenticating}
> >
Back to Wallet Connection Back
</Button> </Button>
</div> </div>
<div className="text-xs text-neutral-500 text-center">
Verification is required to access posting and voting features
</div>
</div> </div>
); );
} }

View File

@ -50,7 +50,6 @@ export function WalletConnectionStep({
view: "Connect", view: "Connect",
namespace: "bip122" namespace: "bip122"
}); });
// The wizard will automatically advance when connection is detected
} catch (error) { } catch (error) {
console.error('Error connecting Bitcoin wallet:', error); console.error('Error connecting Bitcoin wallet:', error);
} finally { } finally {
@ -70,7 +69,6 @@ export function WalletConnectionStep({
view: "Connect", view: "Connect",
namespace: "eip155" namespace: "eip155"
}); });
// The wizard will automatically advance when connection is detected
} catch (error) { } catch (error) {
console.error('Error connecting Ethereum wallet:', error); console.error('Error connecting Ethereum wallet:', error);
} finally { } finally {
@ -85,7 +83,7 @@ export function WalletConnectionStep({
// Show loading state if AppKit is not initialized // Show loading state if AppKit is not initialized
if (!initialized) { if (!initialized) {
return ( return (
<div className="flex flex-col items-center justify-center py-8 space-y-4"> <div className="flex flex-col items-center justify-center h-full space-y-4">
<Loader2 className="h-8 w-8 animate-spin text-blue-500" /> <Loader2 className="h-8 w-8 animate-spin text-blue-500" />
<p className="text-neutral-400 text-center"> <p className="text-neutral-400 text-center">
Initializing wallet connection... Initializing wallet connection...
@ -97,22 +95,28 @@ export function WalletConnectionStep({
// Show connected state // Show connected state
if (isConnected) { if (isConnected) {
return ( return (
<div className="space-y-4"> <div className="flex flex-col h-full">
<div className="p-4 bg-green-900/20 border border-green-500/30 rounded-lg"> <div className="flex-1 space-y-4">
<div className="flex items-center gap-2 mb-2"> <div className="p-4 bg-green-900/20 border border-green-500/30 rounded-lg">
<div className="w-2 h-2 bg-green-500 rounded-full"></div> <div className="flex items-center gap-2 mb-2">
<span className="text-green-400 font-medium">Wallet Connected</span> <div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span className="text-green-400 font-medium">Wallet Connected</span>
</div>
<p className="text-sm text-neutral-300 mb-2">
Connected to {activeChain} with {activeAddress?.slice(0, 6)}...{activeAddress?.slice(-4)}
</p>
</div> </div>
<p className="text-sm text-neutral-300 mb-2">
Connected to {activeChain} with {activeAddress?.slice(0, 6)}...{activeAddress?.slice(-4)}
</p>
<p className="text-xs text-neutral-400">
Proceeding to verification step...
</p>
</div> </div>
<div className="flex justify-center"> {/* Action Button */}
<Loader2 className="h-6 w-6 animate-spin text-blue-500" /> <div className="mt-auto">
<Button
onClick={handleNext}
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
disabled={isLoading}
>
Next
</Button>
</div> </div>
</div> </div>
); );
@ -120,95 +124,97 @@ export function WalletConnectionStep({
// Show connection options // Show connection options
return ( return (
<div className="space-y-4"> <div className="flex flex-col h-full">
<p className="text-sm text-neutral-400 text-center"> <div className="flex-1 space-y-4">
Choose a network and wallet to connect to OpChan <p className="text-sm text-neutral-400 text-center">
</p> Choose a network and wallet to connect to OpChan
</p>
{/* Bitcoin Section */} {/* Bitcoin Section */}
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Bitcoin className="h-5 w-5 text-orange-500" /> <Bitcoin className="h-5 w-5 text-orange-500" />
<h3 className="font-semibold text-white">Bitcoin</h3> <h3 className="font-semibold text-white">Bitcoin</h3>
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary" className="text-xs">
Ordinal Verification Required Ordinal Verification Required
</Badge> </Badge>
</div>
<Button
onClick={handleBitcoinConnect}
disabled={isLoading}
className="w-full bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white"
style={{
height: '44px',
borderRadius: '8px',
border: 'none',
fontSize: '14px',
fontWeight: '600',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px'
}}
>
{isLoading ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Connecting...
</>
) : (
"Connect Bitcoin Wallet"
)}
</Button>
</div> </div>
<Button
onClick={handleBitcoinConnect}
disabled={isLoading}
className="w-full bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white"
style={{
height: '44px',
borderRadius: '8px',
border: 'none',
fontSize: '14px',
fontWeight: '600',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px'
}}
>
{isLoading ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Connecting...
</>
) : (
"Connect Bitcoin Wallet"
)}
</Button>
</div>
{/* Divider */} {/* Divider */}
<div className="relative"> <div className="relative">
<div className="absolute inset-0 flex items-center"> <div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-neutral-700" /> <span className="w-full border-t border-neutral-700" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-black px-2 text-neutral-500">or</span>
</div>
</div> </div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-black px-2 text-neutral-500">or</span>
</div>
</div>
{/* Ethereum Section */} {/* Ethereum Section */}
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Coins className="h-5 w-5 text-blue-500" /> <Coins className="h-5 w-5 text-blue-500" />
<h3 className="font-semibold text-white">Ethereum</h3> <h3 className="font-semibold text-white">Ethereum</h3>
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary" className="text-xs">
ENS Ownership Required ENS Ownership Required
</Badge> </Badge>
</div>
<Button
onClick={handleEthereumConnect}
disabled={isLoading}
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white"
style={{
height: '44px',
borderRadius: '8px',
border: 'none',
fontSize: '14px',
fontWeight: '600',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px'
}}
>
{isLoading ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Connecting...
</>
) : (
"Connect Ethereum Wallet"
)}
</Button>
</div> </div>
<Button
onClick={handleEthereumConnect}
disabled={isLoading}
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white"
style={{
height: '44px',
borderRadius: '8px',
border: 'none',
fontSize: '14px',
fontWeight: '600',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px'
}}
>
{isLoading ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Connecting...
</>
) : (
"Connect Ethereum Wallet"
)}
</Button>
</div>
<div className="text-xs text-neutral-500 text-center pt-2"> <div className="text-xs text-neutral-500 text-center pt-2">
Connect your wallet to use OpChan's features Connect your wallet to use OpChan's features
</div>
</div> </div>
</div> </div>
); );

View File

@ -33,25 +33,19 @@ export function WalletWizard({
// Reset wizard when opened and determine starting step // Reset wizard when opened and determine starting step
React.useEffect(() => { React.useEffect(() => {
if (open) { if (open) {
// Only auto-advance from step 1 to 2 when wallet connects during the session // Determine the appropriate starting step based on current state
// Don't auto-advance to step 3 to allow manual navigation if (!isAuthenticated) {
if (isAuthenticated && verificationStatus !== 'verified-owner') {
setCurrentStep(2); // Start at verification step if authenticated but not verified
} else if (!isAuthenticated) {
setCurrentStep(1); // Start at connection step if not authenticated 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 { } else {
setCurrentStep(1); // Default to step 1, let user navigate manually setCurrentStep(3); // Default to step 3 if everything is complete
} }
setIsLoading(false); setIsLoading(false);
} }
}, [open, isAuthenticated, verificationStatus, isDelegationValid]); }, [open, isAuthenticated, verificationStatus, isDelegationValid]); // Include all dependencies to properly determine step
// 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]);
const handleStepComplete = (step: WizardStep) => { const handleStepComplete = (step: WizardStep) => {
if (step < 3) { if (step < 3) {
@ -150,8 +144,8 @@ export function WalletWizard({
))} ))}
</div> </div>
{/* Step Content */} {/* Step Content - Fixed height container */}
<div className="min-h-[300px]"> <div className="h-[400px] flex flex-col">
{currentStep === 1 && ( {currentStep === 1 && (
<WalletConnectionStep <WalletConnectionStep
onComplete={() => handleStepComplete(1)} onComplete={() => handleStepComplete(1)}

View File

@ -3,7 +3,7 @@ import { useToast } from '@/components/ui/use-toast';
import { User } from '@/types'; import { User } from '@/types';
import { AuthService, AuthResult } from '@/lib/identity/services/AuthService'; import { AuthService, AuthResult } from '@/lib/identity/services/AuthService';
import { OpchanMessage } from '@/types'; 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'; export type VerificationStatus = 'unverified' | 'verified-none' | 'verified-owner' | 'verifying';
@ -14,6 +14,7 @@ interface AuthContextType {
verificationStatus: VerificationStatus; verificationStatus: VerificationStatus;
verifyOwnership: () => Promise<boolean>; verifyOwnership: () => Promise<boolean>;
delegateKey: () => Promise<boolean>; delegateKey: () => Promise<boolean>;
clearDelegation: () => void;
isDelegationValid: () => boolean; isDelegationValid: () => boolean;
delegationTimeRemaining: () => number; delegationTimeRemaining: () => number;
isWalletAvailable: () => boolean; isWalletAvailable: () => boolean;
@ -49,7 +50,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
// Create ref for AuthService so it persists between renders // Create ref for AuthService so it persists between renders
const authServiceRef = useRef(new AuthService()); 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(() => { useEffect(() => {
authServiceRef.current.setAccounts(bitcoinAccount, ethereumAccount); authServiceRef.current.setAccounts(bitcoinAccount, ethereumAccount);
}, [bitcoinAccount, ethereumAccount]); }, [bitcoinAccount, ethereumAccount]);
@ -250,6 +257,26 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
return authServiceRef.current.getDelegationTimeRemaining(); 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 => { const isWalletAvailable = (): boolean => {
return isConnected; return isConnected;
}; };
@ -270,6 +297,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
verificationStatus, verificationStatus,
verifyOwnership, verifyOwnership,
delegateKey, delegateKey,
clearDelegation,
isDelegationValid, isDelegationValid,
delegationTimeRemaining, delegationTimeRemaining,
isWalletAvailable, isWalletAvailable,

View File

@ -1,7 +1,10 @@
import { User } from '@/types'; 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 { OrdinalAPI } from '../ordinal';
import { MessageSigning } from '../signatures/message-signing'; import { MessageSigning } from '../signatures/message-signing';
import { KeyDelegation } from '../signatures/key-delegation';
import { OpchanMessage } from '@/types'; import { OpchanMessage } from '@/types';
export interface AuthResult { export interface AuthResult {
@ -14,44 +17,97 @@ export class AuthService {
private walletService: WalletService; private walletService: WalletService;
private ordinalApi: OrdinalAPI; private ordinalApi: OrdinalAPI;
private messageSigning: MessageSigning; private messageSigning: MessageSigning;
private keyDelegation: KeyDelegation;
constructor() { constructor() {
this.walletService = new WalletService(); this.walletService = new WalletService();
this.ordinalApi = new OrdinalAPI(); 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 * Set AppKit accounts for wallet service
*/ */
setAccounts(bitcoinAccount: AppKitAccount, ethereumAccount: AppKitAccount) { setAccounts(bitcoinAccount: UseAppKitAccountReturn, ethereumAccount: UseAppKitAccountReturn) {
this.walletService.setAccounts(bitcoinAccount, ethereumAccount); 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 * Connect to wallet and create user
*/ */
async connectWallet(): Promise<AuthResult> { async connectWallet(): Promise<AuthResult> {
try { try {
const walletInfo = await this.walletService.getWalletInfo(); // Check which wallet is connected
if (!walletInfo) { const isBitcoinConnected = this.walletService.isWalletAvailable('bitcoin');
const isEthereumConnected = this.walletService.isWalletAvailable('ethereum');
if (!isBitcoinConnected && !isEthereumConnected) {
return { return {
success: false, success: false,
error: 'No wallet connected' 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 = { const user: User = {
address: walletInfo.address, address: address,
walletType: walletInfo.walletType, walletType: walletType,
verificationStatus: 'unverified', verificationStatus: 'unverified',
lastChecked: Date.now(), lastChecked: Date.now(),
}; };
// Add ENS info for Ethereum wallets // Add ENS info for Ethereum wallets (if available)
if (walletInfo.walletType === 'ethereum' && walletInfo.ensName) { if (walletType === 'ethereum') {
user.ensName = walletInfo.ensName; // Note: ENS resolution would need to be implemented separately
user.ensOwnership = true; // For now, we'll leave it as undefined
user.ensName = undefined;
user.ensOwnership = false;
} }
return { return {
@ -67,13 +123,23 @@ export class AuthService {
} }
/** /**
* Disconnect wallet and clear user data * Disconnect wallet and clear stored data
*/ */
async disconnectWallet(): Promise<void> { async disconnectWallet(): Promise<void> {
const walletType = this.walletService.getActiveWalletType(); // Clear any existing delegations when disconnecting
if (walletType) { this.keyDelegation.clearDelegation();
await this.walletService.disconnectWallet(walletType); 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 * Verify Ethereum ENS ownership
*/ */
private async verifyEthereumENS(user: User): Promise<AuthResult> { private async verifyEthereumENS(user: User): Promise<AuthResult> {
const walletInfo = await this.walletService.getWalletInfo(); // Note: ENS resolution would need to be implemented separately
const hasENS = walletInfo?.ensName && walletInfo.ensName.length > 0; // For now, we'll assume no ENS ownership
const hasENS = false;
const updatedUser = { const updatedUser = {
...user, ...user,
ensOwnership: hasENS, ensOwnership: hasENS,
ensName: walletInfo?.ensName, ensName: undefined,
lastChecked: Date.now(), lastChecked: Date.now(),
}; };
@ -146,24 +213,35 @@ export class AuthService {
async delegateKey(user: User): Promise<AuthResult> { async delegateKey(user: User): Promise<AuthResult> {
try { try {
const walletType = user.walletType; const walletType = user.walletType;
const canConnect = await this.walletService.canConnectWallet(walletType); const isAvailable = this.walletService.isWalletAvailable(walletType);
if (!canConnect) {
if (!isAvailable) {
return { return {
success: false, 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( const success = await this.walletService.createKeyDelegation(walletType);
user.address,
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 = { const updatedUser = {
...user, ...user,
browserPubKey: delegationInfo.browserPublicKey, browserPubKey: browserPublicKey || undefined,
delegationSignature: delegationInfo.signature, delegationSignature: delegationStatus.isValid ? 'valid' : undefined,
delegationExpiry: delegationInfo.expiryTimestamp, delegationExpiry: delegationStatus.timeRemaining ? Date.now() + delegationStatus.timeRemaining : undefined,
}; };
return { return {
@ -196,21 +274,49 @@ export class AuthService {
* Check if delegation is valid * Check if delegation is valid
*/ */
isDelegationValid(): boolean { 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 * Get delegation time remaining
*/ */
getDelegationTimeRemaining(): number { 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 * Get current wallet info
*/ */
async getWalletInfo() { 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;
} }
/** /**

View File

@ -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 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 * @param expiryTimestamp When the delegation will expire
* @returns The message to be signed * @returns The message to be signed
*/ */
createDelegationMessage( createDelegationMessage(
browserPublicKey: string, browserPublicKey: string,
bitcoinAddress: string, walletAddress: string,
expiryTimestamp: number expiryTimestamp: number
): string { ): 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 * Creates a delegation object from the signed message
* @param bitcoinAddress The Bitcoin wallet address * @param walletAddress The wallet address that signed the delegation
* @param signature The signature from the Bitcoin wallet * @param signature The signature from the wallet
* @param browserPublicKey The browser public key * @param browserPublicKey The browser-generated public key
* @param browserPrivateKey The browser private key * @param browserPrivateKey The browser-generated private key
* @param expiryHours How many hours the delegation should be valid (default: 24) * @param expiryHours How many hours the delegation should last
* @returns The created delegation info * @param walletType The type of wallet (bitcoin or ethereum)
* @returns DelegationInfo object
*/ */
createDelegation( createDelegation(
bitcoinAddress: string, walletAddress: string,
signature: string, signature: string,
browserPublicKey: string, browserPublicKey: string,
browserPrivateKey: string, browserPrivateKey: string,
expiryHours: number = KeyDelegation.DEFAULT_EXPIRY_HOURS expiryHours: number = KeyDelegation.DEFAULT_EXPIRY_HOURS,
walletType: 'bitcoin' | 'ethereum'
): DelegationInfo { ): DelegationInfo {
const now = Date.now(); const expiryTimestamp = Date.now() + (expiryHours * 60 * 60 * 1000);
const expiryTimestamp = now + (expiryHours * 60 * 60 * 1000);
return { return {
signature, signature,
expiryTimestamp, expiryTimestamp,
browserPublicKey, browserPublicKey,
browserPrivateKey, 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 * @returns boolean indicating if the delegation is valid
*/ */
isDelegationValid(): boolean { isDelegationValid(currentAddress?: string, currentWalletType?: 'bitcoin' | 'ethereum'): boolean {
const delegation = this.retrieveDelegation(); const delegation = this.retrieveDelegation();
if (!delegation) return false; if (!delegation) return false;
// Check if delegation has expired
const now = Date.now(); 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 { getDelegatingAddress(): string | null {
const delegation = this.retrieveDelegation(); const delegation = this.retrieveDelegation();
if (!delegation || !this.isDelegationValid()) return null; if (!delegation || !this.isDelegationValid()) return null;
return delegation.bitcoinAddress; return delegation.walletAddress;
} }
/** /**

View File

@ -1,8 +1,9 @@
export interface DelegationSignature { export interface DelegationSignature {
signature: string; // Signature from Bitcoin wallet signature: string; // Signature from wallet
expiryTimestamp: number; // When this delegation expires expiryTimestamp: number; // When this delegation expires
browserPublicKey: string; // Browser-generated public key that was delegated to 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 { export interface DelegationInfo extends DelegationSignature {

View File

@ -1,28 +1,14 @@
import { UseAppKitAccountReturn } from '@reown/appkit/react';
import { KeyDelegation } from '../signatures/key-delegation'; import { KeyDelegation } from '../signatures/key-delegation';
import { bytesToHex } from '@/lib/utils'; import { AppKit } from '@reown/appkit';
import { getEnsName } from '@wagmi/core'; import { ChainNamespace } from '@reown/appkit-common';
import { config } from './appkit'; import { Provider} from '@reown/appkit-controllers';
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;
}
export class ReOwnWalletService { export class ReOwnWalletService {
private keyDelegation: KeyDelegation; private keyDelegation: KeyDelegation;
private bitcoinAccount?: UseAppKitAccountReturn; private bitcoinAccount?: UseAppKitAccountReturn;
private ethereumAccount?: UseAppKitAccountReturn; private ethereumAccount?: UseAppKitAccountReturn;
private appKit?: AppKit;
constructor() { constructor() {
this.keyDelegation = new KeyDelegation(); this.keyDelegation = new KeyDelegation();
@ -36,190 +22,189 @@ export class ReOwnWalletService {
this.ethereumAccount = ethereumAccount; 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 * Check if a wallet type is available and connected
*/ */
isWalletAvailable(walletType: 'bitcoin' | 'ethereum'): boolean { isWalletAvailable(walletType: 'bitcoin' | 'ethereum'): boolean {
if (walletType === 'bitcoin') { if (walletType === 'bitcoin') {
return this.bitcoinAccount?.isConnected || false; return this.bitcoinAccount?.isConnected ?? false;
} else { } 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<boolean> { private getActiveAccount(walletType: 'bitcoin' | 'ethereum'): UseAppKitAccountReturn | undefined {
// For ReOwn, we assume connection is always possible if AppKit is initialized return walletType === 'bitcoin' ? this.bitcoinAccount : this.ethereumAccount;
return true;
} }
/** /**
* Get wallet connection info with ENS resolution for Ethereum * Get the active address for a given wallet type
*/ */
async getWalletInfo(): Promise<WalletInfo | null> { getActiveAddress(walletType: 'bitcoin' | 'ethereum'): string | undefined {
if (this.bitcoinAccount?.isConnected) { const account = this.getActiveAccount(walletType);
return { return account?.address;
address: this.bitcoinAccount.address, }
walletType: 'bitcoin',
isConnected: true /**
}; * Get the appropriate namespace for the wallet type
} else if (this.ethereumAccount?.isConnected) { */
// Use Wagmi to resolve ENS name private getNamespace(walletType: 'bitcoin' | 'ethereum'): ChainNamespace {
let ensName: string | undefined; return walletType === 'bitcoin' ? 'bip122' : 'eip155';
try { }
const resolvedName = await getEnsName(config, {
address: this.ethereumAccount.address as `0x${string}` /**
}); * Sign a message using the appropriate adapter
ensName = resolvedName || undefined; */
} catch (error) { async signMessage(messageBytes: Uint8Array, walletType: 'bitcoin' | 'ethereum'): Promise<string> {
console.warn('Failed to resolve ENS name:', error); if (!this.appKit) {
// Continue without ENS name 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 { // Get the provider for the current connection
address: this.ethereumAccount.address, const provider = this.appKit.getProvider(namespace);
walletType: 'ethereum',
ensName, if (!provider) {
isConnected: true 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 { async createKeyDelegation(walletType: 'bitcoin' | 'ethereum'): Promise<boolean> {
if (this.bitcoinAccount?.isConnected) { try {
return this.bitcoinAccount.address; const account = this.getActiveAccount(walletType);
} else if (this.ethereumAccount?.isConnected) { if (!account?.address) {
return this.ethereumAccount.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 { async signMessageWithDelegation(messageBytes: Uint8Array, walletType: 'bitcoin' | 'ethereum'): Promise<string> {
if (this.bitcoinAccount?.isConnected) { const account = this.getActiveAccount(walletType);
return 'bitcoin'; if (!account?.address) {
} else if (this.ethereumAccount?.isConnected) { throw new Error(`No ${walletType} wallet connected`);
return 'ethereum';
} }
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( getDelegationStatus(walletType: 'bitcoin' | 'ethereum'): {
address: string, hasDelegation: boolean;
walletType: 'bitcoin' | 'ethereum' isValid: boolean;
): Promise<DelegationInfo> { timeRemaining?: number;
// Generate browser keypair } {
const keypair = this.keyDelegation.generateKeypair(); const account = this.getActiveAccount(walletType);
const currentAddress = account?.address;
// Create delegation message with chain-specific format const hasDelegation = this.keyDelegation.retrieveDelegation() !== null;
const expiryTimestamp = Date.now() + (24 * 60 * 60 * 1000); // 24 hours const isValid = this.keyDelegation.isDelegationValid(currentAddress, walletType);
const delegationMessage = this.createDelegationMessage( const timeRemaining = this.keyDelegation.getDelegationTimeRemaining();
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);
return { return {
browserPublicKey: keypair.publicKey, hasDelegation,
signature, isValid,
expiryTimestamp timeRemaining: timeRemaining > 0 ? timeRemaining : undefined
}; };
} }
/** /**
* Create chain-specific delegation message * Clear delegation for the connected wallet
*/ */
private createDelegationMessage( clearDelegation(walletType: 'bitcoin' | 'ethereum'): void {
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<string> {
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<void> {
// Clear stored delegation
this.keyDelegation.clearDelegation(); 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;
} }
} }

View File

@ -1,2 +1 @@
export { ReOwnWalletService as WalletService } from './ReOwnWalletService'; export { ReOwnWalletService as WalletService } from './ReOwnWalletService';
export type { WalletInfo, DelegationInfo, AppKitAccount } from './ReOwnWalletService';