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 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 = () => (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>

View File

@ -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 (
<div className="space-y-4">
<div className={`p-4 rounded-lg border ${
delegationResult.success
? '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 ? (
<CheckCircle className="h-5 w-5 text-green-500" />
) : (
<AlertCircle className="h-5 w-5 text-yellow-500" />
)}
<span className={`font-medium ${
delegationResult.success ? 'text-green-400' : 'text-yellow-400'
}`}>
{delegationResult.success ? 'Delegation Complete' : 'Delegation Result'}
</span>
</div>
<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 className="flex flex-col h-full">
<div className="flex-1 space-y-4">
<div className={`p-4 rounded-lg border ${
delegationResult.success
? '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 ? (
<CheckCircle className="h-5 w-5 text-green-500" />
) : (
<AlertCircle className="h-5 w-5 text-yellow-500" />
)}
<span className={`font-medium ${
delegationResult.success ? 'text-green-400' : 'text-yellow-400'
}`}>
{delegationResult.success ? 'Delegation Complete' : 'Delegation Result'}
</span>
</div>
)}
</div>
<Button
onClick={handleComplete}
className="w-full bg-green-600 hover:bg-green-700 text-white"
disabled={isLoading}
>
{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>
<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>
</div>
<Button
onClick={handleComplete}
className="w-full bg-green-600 hover:bg-green-700 text-white"
disabled={isLoading}
>
Complete Setup
</Button>
{/* Action Button */}
<div className="mt-auto">
<Button
onClick={handleComplete}
className="w-full bg-green-600 hover:bg-green-700 text-white"
disabled={isLoading}
>
Complete Setup
</Button>
</div>
</div>
);
}
// Show delegation form
// Show minimal delegation status
return (
<div className="space-y-4">
<div className="text-center space-y-2">
<div className="flex justify-center">
<Key className="h-8 w-8 text-blue-500" />
</div>
<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 className="flex flex-col h-full">
<div className="flex-1 space-y-4">
<div className="text-center space-y-2">
<div className="flex justify-center">
<Key className="h-8 w-8 text-blue-500" />
</div>
<p className="text-xs text-neutral-400 font-mono break-all">
{currentUser.browserPubKey.slice(0, 20)}...{currentUser.browserPubKey.slice(-20)}
<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
</p>
</div>
)}
<div className="space-y-3">
<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 Key...
</>
) : (
"Delegate Signing Key"
)}
</Button>
{/* Delegation Status */}
<div className="p-4 bg-neutral-900/30 border border-neutral-700 rounded-lg">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Key className="h-4 w-4 text-blue-500" />
<span className="text-sm font-medium text-white">Key Delegation</span>
</div>
{currentUser?.walletType === 'bitcoin' ? (
<div className="text-orange-500 text-sm"></div>
) : (
<div className="text-blue-500 text-sm">Ξ</div>
)}
</div>
<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
onClick={onBack}
@ -226,14 +242,9 @@ export function DelegationStep({
className="w-full border-neutral-600 text-neutral-400 hover:bg-neutral-800"
disabled={isLoading || isAuthenticating}
>
Back to Verification
Back
</Button>
</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>
);
}

View File

@ -103,52 +103,50 @@ export function VerificationStep({
// Show verification result
if (verificationResult) {
return (
<div className="space-y-4">
<div className={`p-4 rounded-lg border ${
verificationResult.success
? '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 ? (
<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>
<div className="flex flex-col h-full">
<div className="flex-1 space-y-4">
<div className={`p-4 rounded-lg border ${
verificationResult.success
? '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 ? (
<ShieldCheck className="h-5 w-5 text-green-500" />
) : (
<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>
)}
<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>
<Button
onClick={handleNext}
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Processing...
</>
) : (
"Continue to Key Delegation"
)}
</Button>
{/* Action Button */}
<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>
);
}
@ -156,78 +154,90 @@ export function VerificationStep({
// Show verification status
if (verificationStatus === 'verified-owner') {
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">
<ShieldCheck className="h-5 w-5 text-green-500" />
<span className="text-green-400 font-medium">Already Verified</span>
</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 className="flex flex-col h-full">
<div className="flex-1 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">
<ShieldCheck className="h-5 w-5 text-green-500" />
<span className="text-green-400 font-medium">Already Verified</span>
</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>
<Button
onClick={handleNext}
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
disabled={isLoading}
>
Continue to Key Delegation
</Button>
{/* Action Button */}
<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>
);
}
// Show verification form
return (
<div className="space-y-4">
<div className="text-center space-y-2">
<div className="flex justify-center">
{React.createElement(getVerificationIcon(), {
className: `h-8 w-8 ${getVerificationColor()}`
})}
<div className="flex flex-col h-full">
<div className="flex-1 space-y-4">
<div className="text-center space-y-2">
<div className="flex justify-center">
{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>
<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="space-y-3">
{/* Action Buttons */}
<div className="mt-auto space-y-3">
<Button
onClick={handleVerify}
disabled={isLoading || isAuthenticating}
@ -249,13 +259,9 @@ export function VerificationStep({
className="w-full border-neutral-600 text-neutral-400 hover:bg-neutral-800"
disabled={isLoading || isAuthenticating}
>
Back to Wallet Connection
Back
</Button>
</div>
<div className="text-xs text-neutral-500 text-center">
Verification is required to access posting and voting features
</div>
</div>
);
}

View File

@ -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 (
<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" />
<p className="text-neutral-400 text-center">
Initializing wallet connection...
@ -97,22 +95,28 @@ export function WalletConnectionStep({
// Show connected state
if (isConnected) {
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">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span className="text-green-400 font-medium">Wallet Connected</span>
<div className="flex flex-col h-full">
<div className="flex-1 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">
<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>
<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 className="flex justify-center">
<Loader2 className="h-6 w-6 animate-spin text-blue-500" />
{/* Action Button */}
<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>
);
@ -120,95 +124,97 @@ export function WalletConnectionStep({
// Show connection options
return (
<div className="space-y-4">
<p className="text-sm text-neutral-400 text-center">
Choose a network and wallet to connect to OpChan
</p>
<div className="flex flex-col h-full">
<div className="flex-1 space-y-4">
<p className="text-sm text-neutral-400 text-center">
Choose a network and wallet to connect to OpChan
</p>
{/* Bitcoin Section */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<Bitcoin className="h-5 w-5 text-orange-500" />
<h3 className="font-semibold text-white">Bitcoin</h3>
<Badge variant="secondary" className="text-xs">
Ordinal Verification Required
</Badge>
{/* Bitcoin Section */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<Bitcoin className="h-5 w-5 text-orange-500" />
<h3 className="font-semibold text-white">Bitcoin</h3>
<Badge variant="secondary" className="text-xs">
Ordinal Verification Required
</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>
<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 */}
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-neutral-700" />
{/* Divider */}
<div className="relative">
<div className="absolute inset-0 flex items-center">
<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 className="relative flex justify-center text-xs uppercase">
<span className="bg-black px-2 text-neutral-500">or</span>
</div>
</div>
{/* Ethereum Section */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<Coins className="h-5 w-5 text-blue-500" />
<h3 className="font-semibold text-white">Ethereum</h3>
<Badge variant="secondary" className="text-xs">
ENS Ownership Required
</Badge>
{/* Ethereum Section */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<Coins className="h-5 w-5 text-blue-500" />
<h3 className="font-semibold text-white">Ethereum</h3>
<Badge variant="secondary" className="text-xs">
ENS Ownership Required
</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>
<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">
Connect your wallet to use OpChan's features
<div className="text-xs text-neutral-500 text-center pt-2">
Connect your wallet to use OpChan's features
</div>
</div>
</div>
);

View File

@ -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({
))}
</div>
{/* Step Content */}
<div className="min-h-[300px]">
{/* Step Content - Fixed height container */}
<div className="h-[400px] flex flex-col">
{currentStep === 1 && (
<WalletConnectionStep
onComplete={() => handleStepComplete(1)}

View File

@ -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<boolean>;
delegateKey: () => Promise<boolean>;
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,

View File

@ -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<AuthResult> {
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<void> {
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<AuthResult> {
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<AuthResult> {
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;
}
/**

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

View File

@ -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 {

View File

@ -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<boolean> {
// 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<WalletInfo | null> {
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<string> {
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<boolean> {
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<string> {
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<DelegationInfo> {
// 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<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
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;
}
}

View File

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