mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-02 21:03:09 +00:00
fix: delegation + signatures
This commit is contained in:
parent
be55804d91
commit
539c596974
@ -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}>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -1,2 +1 @@
|
||||
export { ReOwnWalletService as WalletService } from './ReOwnWalletService';
|
||||
export type { WalletInfo, DelegationInfo, AppKitAccount } from './ReOwnWalletService';
|
||||
export { ReOwnWalletService as WalletService } from './ReOwnWalletService';
|
||||
Loading…
x
Reference in New Issue
Block a user