mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-06 23:03:07 +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 Dashboard from "./pages/Dashboard";
|
||||||
import Index from "./pages/Index";
|
import Index from "./pages/Index";
|
||||||
import { appkitConfig } from "./lib/identity/wallets/appkit";
|
import { appkitConfig } from "./lib/identity/wallets/appkit";
|
||||||
import { createAppKit } from "@reown/appkit";
|
|
||||||
import { WagmiProvider } from "wagmi";
|
import { WagmiProvider } from "wagmi";
|
||||||
import { config } from "./lib/identity/wallets/appkit";
|
import { config } from "./lib/identity/wallets/appkit";
|
||||||
import { AppKitProvider } from "@reown/appkit/react";
|
import { AppKitProvider } from "@reown/appkit/react";
|
||||||
@ -32,8 +31,6 @@ import { AppKitProvider } from "@reown/appkit/react";
|
|||||||
// Create a client
|
// Create a client
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
createAppKit(appkitConfig);
|
|
||||||
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
<WagmiProvider config={config}>
|
<WagmiProvider config={config}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Key, Loader2, CheckCircle, AlertCircle, Trash2 } from "lucide-react";
|
||||||
import { Key, Clock, Shield, Loader2, CheckCircle, AlertCircle } from "lucide-react";
|
|
||||||
import { useAuth } from "@/contexts/useAuth";
|
import { useAuth } from "@/contexts/useAuth";
|
||||||
|
|
||||||
interface DelegationStepProps {
|
interface DelegationStepProps {
|
||||||
@ -22,7 +21,8 @@ export function DelegationStep({
|
|||||||
delegateKey,
|
delegateKey,
|
||||||
isDelegationValid,
|
isDelegationValid,
|
||||||
delegationTimeRemaining,
|
delegationTimeRemaining,
|
||||||
isAuthenticating
|
isAuthenticating,
|
||||||
|
clearDelegation
|
||||||
} = useAuth();
|
} = useAuth();
|
||||||
|
|
||||||
const [delegationResult, setDelegationResult] = React.useState<{
|
const [delegationResult, setDelegationResult] = React.useState<{
|
||||||
@ -47,13 +47,13 @@ export function DelegationStep({
|
|||||||
|
|
||||||
setDelegationResult({
|
setDelegationResult({
|
||||||
success: true,
|
success: true,
|
||||||
message: "Key delegation successful! You can now interact with the forum without additional wallet approvals.",
|
message: "Key delegation successful!",
|
||||||
expiry: expiryDate
|
expiry: expiryDate
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setDelegationResult({
|
setDelegationResult({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Key delegation failed. You can still use the forum but will need to approve each action."
|
message: "Key delegation failed."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -70,155 +70,171 @@ export function DelegationStep({
|
|||||||
onComplete();
|
onComplete();
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatTimeRemaining = () => {
|
const handleRefresh = () => {
|
||||||
const remaining = delegationTimeRemaining();
|
window.location.reload();
|
||||||
if (remaining <= 0) return "Expired";
|
|
||||||
|
|
||||||
const hours = Math.floor(remaining / (1000 * 60 * 60));
|
|
||||||
const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
|
|
||||||
|
|
||||||
if (hours > 0) {
|
|
||||||
return `${hours}h ${minutes}m remaining`;
|
|
||||||
} else {
|
|
||||||
return `${minutes}m remaining`;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Show delegation result
|
// Show delegation result
|
||||||
if (delegationResult) {
|
if (delegationResult) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col h-full">
|
||||||
<div className={`p-4 rounded-lg border ${
|
<div className="flex-1 space-y-4">
|
||||||
delegationResult.success
|
<div className={`p-4 rounded-lg border ${
|
||||||
? 'bg-green-900/20 border-green-500/30'
|
delegationResult.success
|
||||||
: 'bg-yellow-900/20 border-yellow-500/30'
|
? 'bg-green-900/20 border-green-500/30'
|
||||||
}`}>
|
: 'bg-yellow-900/20 border-yellow-500/30'
|
||||||
<div className="flex items-center gap-2 mb-2">
|
}`}>
|
||||||
{delegationResult.success ? (
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<CheckCircle className="h-5 w-5 text-green-500" />
|
{delegationResult.success ? (
|
||||||
) : (
|
<CheckCircle className="h-5 w-5 text-green-500" />
|
||||||
<AlertCircle className="h-5 w-5 text-yellow-500" />
|
) : (
|
||||||
)}
|
<AlertCircle className="h-5 w-5 text-yellow-500" />
|
||||||
<span className={`font-medium ${
|
)}
|
||||||
delegationResult.success ? 'text-green-400' : 'text-yellow-400'
|
<span className={`font-medium ${
|
||||||
}`}>
|
delegationResult.success ? 'text-green-400' : 'text-yellow-400'
|
||||||
{delegationResult.success ? 'Delegation Complete' : 'Delegation Result'}
|
}`}>
|
||||||
</span>
|
{delegationResult.success ? 'Delegation Complete' : 'Delegation Result'}
|
||||||
</div>
|
</span>
|
||||||
<p className="text-sm text-neutral-300 mb-2">
|
|
||||||
{delegationResult.message}
|
|
||||||
</p>
|
|
||||||
{delegationResult.expiry && (
|
|
||||||
<div className="text-xs text-neutral-400">
|
|
||||||
<p>Expires: {delegationResult.expiry}</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<p className="text-sm text-neutral-300 mb-2">
|
||||||
</div>
|
{delegationResult.message}
|
||||||
|
</p>
|
||||||
<Button
|
{delegationResult.expiry && (
|
||||||
onClick={handleComplete}
|
<div className="text-xs text-neutral-400">
|
||||||
className="w-full bg-green-600 hover:bg-green-700 text-white"
|
<p>Expires: {delegationResult.expiry}</p>
|
||||||
disabled={isLoading}
|
</div>
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
Processing...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"Complete Setup"
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show existing delegation status
|
|
||||||
if (isDelegationValid()) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="p-4 bg-green-900/20 border border-green-500/30 rounded-lg">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<CheckCircle className="h-5 w-5 text-green-500" />
|
|
||||||
<span className="text-green-400 font-medium">Key Already Delegated</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-neutral-300 mb-2">
|
|
||||||
You already have an active key delegation.
|
|
||||||
</p>
|
|
||||||
<div className="text-xs text-neutral-400">
|
|
||||||
<p>Time remaining: {formatTimeRemaining()}</p>
|
|
||||||
{currentUser?.delegationExpiry && (
|
|
||||||
<p>Expires: {new Date(currentUser.delegationExpiry).toLocaleString()}</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
{/* Action Button */}
|
||||||
onClick={handleComplete}
|
<div className="mt-auto">
|
||||||
className="w-full bg-green-600 hover:bg-green-700 text-white"
|
<Button
|
||||||
disabled={isLoading}
|
onClick={handleComplete}
|
||||||
>
|
className="w-full bg-green-600 hover:bg-green-700 text-white"
|
||||||
Complete Setup
|
disabled={isLoading}
|
||||||
</Button>
|
>
|
||||||
|
Complete Setup
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show delegation form
|
// Show minimal delegation status
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col h-full">
|
||||||
<div className="text-center space-y-2">
|
<div className="flex-1 space-y-4">
|
||||||
<div className="flex justify-center">
|
<div className="text-center space-y-2">
|
||||||
<Key className="h-8 w-8 text-blue-500" />
|
<div className="flex justify-center">
|
||||||
</div>
|
<Key className="h-8 w-8 text-blue-500" />
|
||||||
<h3 className="text-lg font-semibold text-white">
|
|
||||||
Delegate Signing Key
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-neutral-400">
|
|
||||||
Create a browser-based signing key for better user experience
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4 bg-neutral-900/50 border border-neutral-700 rounded-lg">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<Shield className="h-4 w-4 text-blue-500" />
|
|
||||||
<span className="text-sm font-medium text-white">What is key delegation?</span>
|
|
||||||
</div>
|
|
||||||
<ul className="text-xs text-neutral-400 space-y-1">
|
|
||||||
<li>• Creates a browser-based signing key for 24 hours</li>
|
|
||||||
<li>• Allows posting, commenting, and voting without wallet approval</li>
|
|
||||||
<li>• Automatically expires for security</li>
|
|
||||||
<li>• Can be renewed anytime</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{currentUser?.browserPubKey && (
|
|
||||||
<div className="p-3 bg-neutral-900/30 border border-neutral-600 rounded-lg">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<Key className="h-4 w-4 text-green-500" />
|
|
||||||
<span className="text-sm font-medium text-white">Browser Key Generated</span>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-neutral-400 font-mono break-all">
|
<h3 className="text-lg font-semibold text-white">
|
||||||
{currentUser.browserPubKey.slice(0, 20)}...{currentUser.browserPubKey.slice(-20)}
|
Delegate Signing Key
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-neutral-400">
|
||||||
|
Create a browser-based signing key
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
{/* Delegation Status */}
|
||||||
<Button
|
<div className="p-4 bg-neutral-900/30 border border-neutral-700 rounded-lg">
|
||||||
onClick={handleDelegate}
|
<div className="flex items-center justify-between mb-3">
|
||||||
disabled={isLoading || isAuthenticating}
|
<div className="flex items-center gap-2">
|
||||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
<Key className="h-4 w-4 text-blue-500" />
|
||||||
>
|
<span className="text-sm font-medium text-white">Key Delegation</span>
|
||||||
{isLoading || isAuthenticating ? (
|
</div>
|
||||||
<>
|
{currentUser?.walletType === 'bitcoin' ? (
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<div className="text-orange-500 text-sm">₿</div>
|
||||||
Delegating Key...
|
) : (
|
||||||
</>
|
<div className="text-blue-500 text-sm">Ξ</div>
|
||||||
) : (
|
)}
|
||||||
"Delegate Signing Key"
|
</div>
|
||||||
)}
|
|
||||||
</Button>
|
<div className="space-y-3">
|
||||||
|
{/* Status */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{isDelegationValid() ? (
|
||||||
|
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<AlertCircle className="h-4 w-4 text-yellow-500" />
|
||||||
|
)}
|
||||||
|
<span className={`text-sm font-medium ${
|
||||||
|
isDelegationValid() ? 'text-green-400' : 'text-yellow-400'
|
||||||
|
}`}>
|
||||||
|
{isDelegationValid() ? 'Delegated' : 'Required'}
|
||||||
|
</span>
|
||||||
|
{isDelegationValid() && (
|
||||||
|
<span className="text-xs text-neutral-400">
|
||||||
|
{Math.floor(delegationTimeRemaining() / (1000 * 60 * 60))}h {Math.floor((delegationTimeRemaining() % (1000 * 60 * 60)) / (1000 * 60))}m remaining
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Delegated Browser Public Key */}
|
||||||
|
{isDelegationValid() && currentUser?.browserPubKey && (
|
||||||
|
<div className="text-xs text-neutral-400">
|
||||||
|
<div className="font-mono break-all bg-neutral-800 p-2 rounded">
|
||||||
|
{currentUser.browserPubKey}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Wallet Address */}
|
||||||
|
{currentUser && (
|
||||||
|
<div className="text-xs text-neutral-400">
|
||||||
|
<div className="font-mono break-all">
|
||||||
|
{currentUser.address}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Delete Button for Active Delegations */}
|
||||||
|
{isDelegationValid() && (
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
onClick={clearDelegation}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="border-red-600/50 text-red-400 hover:bg-red-600/20 hover:border-red-500 text-xs px-2 py-1 h-auto"
|
||||||
|
>
|
||||||
|
<Trash2 className="mr-1 h-3 w-3" />
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="mt-auto space-y-3">
|
||||||
|
{!isDelegationValid() && (
|
||||||
|
<Button
|
||||||
|
onClick={handleDelegate}
|
||||||
|
disabled={isLoading || isAuthenticating}
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
|
>
|
||||||
|
{isLoading || isAuthenticating ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Delegating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Delegate Key"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isDelegationValid() && (
|
||||||
|
<Button
|
||||||
|
onClick={handleComplete}
|
||||||
|
className="w-full bg-green-600 hover:bg-green-700 text-white"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Complete Setup
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
@ -226,14 +242,9 @@ export function DelegationStep({
|
|||||||
className="w-full border-neutral-600 text-neutral-400 hover:bg-neutral-800"
|
className="w-full border-neutral-600 text-neutral-400 hover:bg-neutral-800"
|
||||||
disabled={isLoading || isAuthenticating}
|
disabled={isLoading || isAuthenticating}
|
||||||
>
|
>
|
||||||
Back to Verification
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs text-neutral-500 text-center space-y-1">
|
|
||||||
<p>Key delegation is optional but recommended for better UX</p>
|
|
||||||
<p>You can still use the forum without delegation</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -103,52 +103,50 @@ export function VerificationStep({
|
|||||||
// Show verification result
|
// Show verification result
|
||||||
if (verificationResult) {
|
if (verificationResult) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col h-full">
|
||||||
<div className={`p-4 rounded-lg border ${
|
<div className="flex-1 space-y-4">
|
||||||
verificationResult.success
|
<div className={`p-4 rounded-lg border ${
|
||||||
? 'bg-green-900/20 border-green-500/30'
|
verificationResult.success
|
||||||
: 'bg-yellow-900/20 border-yellow-500/30'
|
? 'bg-green-900/20 border-green-500/30'
|
||||||
}`}>
|
: 'bg-yellow-900/20 border-yellow-500/30'
|
||||||
<div className="flex items-center gap-2 mb-2">
|
}`}>
|
||||||
{verificationResult.success ? (
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<ShieldCheck className="h-5 w-5 text-green-500" />
|
{verificationResult.success ? (
|
||||||
) : (
|
<ShieldCheck className="h-5 w-5 text-green-500" />
|
||||||
<AlertCircle className="h-5 w-5 text-yellow-500" />
|
|
||||||
)}
|
|
||||||
<span className={`font-medium ${
|
|
||||||
verificationResult.success ? 'text-green-400' : 'text-yellow-400'
|
|
||||||
}`}>
|
|
||||||
{verificationResult.success ? 'Verification Complete' : 'Verification Result'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-neutral-300 mb-2">
|
|
||||||
{verificationResult.message}
|
|
||||||
</p>
|
|
||||||
{verificationResult.details && (
|
|
||||||
<div className="text-xs text-neutral-400">
|
|
||||||
{walletType === 'bitcoin' ? (
|
|
||||||
<p>Ordinal ID: {verificationResult.details.id}</p>
|
|
||||||
) : (
|
) : (
|
||||||
<p>ENS Name: {verificationResult.details.ensName}</p>
|
<AlertCircle className="h-5 w-5 text-yellow-500" />
|
||||||
)}
|
)}
|
||||||
|
<span className={`font-medium ${
|
||||||
|
verificationResult.success ? 'text-green-400' : 'text-yellow-400'
|
||||||
|
}`}>
|
||||||
|
{verificationResult.success ? 'Verification Complete' : 'Verification Result'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<p className="text-sm text-neutral-300 mb-2">
|
||||||
|
{verificationResult.message}
|
||||||
|
</p>
|
||||||
|
{verificationResult.details && (
|
||||||
|
<div className="text-xs text-neutral-400">
|
||||||
|
{walletType === 'bitcoin' ? (
|
||||||
|
<p>Ordinal ID: {verificationResult.details.id}</p>
|
||||||
|
) : (
|
||||||
|
<p>ENS Name: {verificationResult.details.ensName}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
{/* Action Button */}
|
||||||
onClick={handleNext}
|
<div className="mt-auto">
|
||||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
<Button
|
||||||
disabled={isLoading}
|
onClick={handleNext}
|
||||||
>
|
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
{isLoading ? (
|
disabled={isLoading}
|
||||||
<>
|
>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
Next
|
||||||
Processing...
|
</Button>
|
||||||
</>
|
</div>
|
||||||
) : (
|
|
||||||
"Continue to Key Delegation"
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -156,78 +154,90 @@ export function VerificationStep({
|
|||||||
// Show verification status
|
// Show verification status
|
||||||
if (verificationStatus === 'verified-owner') {
|
if (verificationStatus === 'verified-owner') {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col h-full">
|
||||||
<div className="p-4 bg-green-900/20 border border-green-500/30 rounded-lg">
|
<div className="flex-1 space-y-4">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="p-4 bg-green-900/20 border border-green-500/30 rounded-lg">
|
||||||
<ShieldCheck className="h-5 w-5 text-green-500" />
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<span className="text-green-400 font-medium">Already Verified</span>
|
<ShieldCheck className="h-5 w-5 text-green-500" />
|
||||||
</div>
|
<span className="text-green-400 font-medium">Already Verified</span>
|
||||||
<p className="text-sm text-neutral-300 mb-2">
|
|
||||||
Your {getVerificationType()} ownership has been verified.
|
|
||||||
</p>
|
|
||||||
{currentUser && (
|
|
||||||
<div className="text-xs text-neutral-400">
|
|
||||||
{walletType === 'bitcoin' && currentUser.ordinalOwnership && (
|
|
||||||
<p>Ordinal ID: {typeof currentUser.ordinalOwnership === 'object' ? currentUser.ordinalOwnership.id : 'Verified'}</p>
|
|
||||||
)}
|
|
||||||
{walletType === 'ethereum' && currentUser.ensName && (
|
|
||||||
<p>ENS Name: {currentUser.ensName}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<p className="text-sm text-neutral-300 mb-2">
|
||||||
|
Your {getVerificationType()} ownership has been verified.
|
||||||
|
</p>
|
||||||
|
{currentUser && (
|
||||||
|
<div className="text-xs text-neutral-400">
|
||||||
|
{walletType === 'bitcoin' && currentUser.ordinalOwnership && (
|
||||||
|
<p>Ordinal ID: {typeof currentUser.ordinalOwnership === 'object' ? currentUser.ordinalOwnership.id : 'Verified'}</p>
|
||||||
|
)}
|
||||||
|
{walletType === 'ethereum' && currentUser.ensName && (
|
||||||
|
<p>ENS Name: {currentUser.ensName}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
{/* Action Button */}
|
||||||
onClick={handleNext}
|
<div className="mt-auto">
|
||||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
<Button
|
||||||
disabled={isLoading}
|
onClick={handleNext}
|
||||||
>
|
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
Continue to Key Delegation
|
disabled={isLoading}
|
||||||
</Button>
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show verification form
|
// Show verification form
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col h-full">
|
||||||
<div className="text-center space-y-2">
|
<div className="flex-1 space-y-4">
|
||||||
<div className="flex justify-center">
|
<div className="text-center space-y-2">
|
||||||
{React.createElement(getVerificationIcon(), {
|
<div className="flex justify-center">
|
||||||
className: `h-8 w-8 ${getVerificationColor()}`
|
{React.createElement(getVerificationIcon(), {
|
||||||
})}
|
className: `h-8 w-8 ${getVerificationColor()}`
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-white">
|
||||||
|
Verify {getVerificationType()} Ownership
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-neutral-400">
|
||||||
|
{getVerificationDescription()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-neutral-900/50 border border-neutral-700 rounded-lg">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Shield className="h-4 w-4 text-blue-500" />
|
||||||
|
<span className="text-sm font-medium text-white">What happens during verification?</span>
|
||||||
|
</div>
|
||||||
|
<ul className="text-xs text-neutral-400 space-y-1">
|
||||||
|
{walletType === 'bitcoin' ? (
|
||||||
|
<>
|
||||||
|
<li>• We'll check your wallet for Bitcoin Ordinal ownership</li>
|
||||||
|
<li>• If found, you'll get full posting and voting access</li>
|
||||||
|
<li>• If not found, you'll have read-only access</li>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<li>• We'll check your wallet for ENS domain ownership</li>
|
||||||
|
<li>• If found, you'll get full posting and voting access</li>
|
||||||
|
<li>• If not found, you'll have read-only access</li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs text-neutral-500 text-center">
|
||||||
|
Verification is required to access posting and voting features
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-semibold text-white">
|
|
||||||
Verify {getVerificationType()} Ownership
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-neutral-400">
|
|
||||||
{getVerificationDescription()}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 bg-neutral-900/50 border border-neutral-700 rounded-lg">
|
{/* Action Buttons */}
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="mt-auto space-y-3">
|
||||||
<Shield className="h-4 w-4 text-blue-500" />
|
|
||||||
<span className="text-sm font-medium text-white">What happens during verification?</span>
|
|
||||||
</div>
|
|
||||||
<ul className="text-xs text-neutral-400 space-y-1">
|
|
||||||
{walletType === 'bitcoin' ? (
|
|
||||||
<>
|
|
||||||
<li>• We'll check your wallet for Bitcoin Ordinal ownership</li>
|
|
||||||
<li>• If found, you'll get full posting and voting access</li>
|
|
||||||
<li>• If not found, you'll have read-only access</li>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<li>• We'll check your wallet for ENS domain ownership</li>
|
|
||||||
<li>• If found, you'll get full posting and voting access</li>
|
|
||||||
<li>• If not found, you'll have read-only access</li>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleVerify}
|
onClick={handleVerify}
|
||||||
disabled={isLoading || isAuthenticating}
|
disabled={isLoading || isAuthenticating}
|
||||||
@ -249,13 +259,9 @@ export function VerificationStep({
|
|||||||
className="w-full border-neutral-600 text-neutral-400 hover:bg-neutral-800"
|
className="w-full border-neutral-600 text-neutral-400 hover:bg-neutral-800"
|
||||||
disabled={isLoading || isAuthenticating}
|
disabled={isLoading || isAuthenticating}
|
||||||
>
|
>
|
||||||
Back to Wallet Connection
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs text-neutral-500 text-center">
|
|
||||||
Verification is required to access posting and voting features
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -50,7 +50,6 @@ export function WalletConnectionStep({
|
|||||||
view: "Connect",
|
view: "Connect",
|
||||||
namespace: "bip122"
|
namespace: "bip122"
|
||||||
});
|
});
|
||||||
// The wizard will automatically advance when connection is detected
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error connecting Bitcoin wallet:', error);
|
console.error('Error connecting Bitcoin wallet:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@ -70,7 +69,6 @@ export function WalletConnectionStep({
|
|||||||
view: "Connect",
|
view: "Connect",
|
||||||
namespace: "eip155"
|
namespace: "eip155"
|
||||||
});
|
});
|
||||||
// The wizard will automatically advance when connection is detected
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error connecting Ethereum wallet:', error);
|
console.error('Error connecting Ethereum wallet:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@ -85,7 +83,7 @@ export function WalletConnectionStep({
|
|||||||
// Show loading state if AppKit is not initialized
|
// Show loading state if AppKit is not initialized
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center py-8 space-y-4">
|
<div className="flex flex-col items-center justify-center h-full space-y-4">
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
|
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
|
||||||
<p className="text-neutral-400 text-center">
|
<p className="text-neutral-400 text-center">
|
||||||
Initializing wallet connection...
|
Initializing wallet connection...
|
||||||
@ -97,22 +95,28 @@ export function WalletConnectionStep({
|
|||||||
// Show connected state
|
// Show connected state
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col h-full">
|
||||||
<div className="p-4 bg-green-900/20 border border-green-500/30 rounded-lg">
|
<div className="flex-1 space-y-4">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="p-4 bg-green-900/20 border border-green-500/30 rounded-lg">
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<span className="text-green-400 font-medium">Wallet Connected</span>
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
|
<span className="text-green-400 font-medium">Wallet Connected</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-neutral-300 mb-2">
|
||||||
|
Connected to {activeChain} with {activeAddress?.slice(0, 6)}...{activeAddress?.slice(-4)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-neutral-300 mb-2">
|
|
||||||
Connected to {activeChain} with {activeAddress?.slice(0, 6)}...{activeAddress?.slice(-4)}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-neutral-400">
|
|
||||||
Proceeding to verification step...
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-center">
|
{/* Action Button */}
|
||||||
<Loader2 className="h-6 w-6 animate-spin text-blue-500" />
|
<div className="mt-auto">
|
||||||
|
<Button
|
||||||
|
onClick={handleNext}
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -120,95 +124,97 @@ export function WalletConnectionStep({
|
|||||||
|
|
||||||
// Show connection options
|
// Show connection options
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col h-full">
|
||||||
<p className="text-sm text-neutral-400 text-center">
|
<div className="flex-1 space-y-4">
|
||||||
Choose a network and wallet to connect to OpChan
|
<p className="text-sm text-neutral-400 text-center">
|
||||||
</p>
|
Choose a network and wallet to connect to OpChan
|
||||||
|
</p>
|
||||||
|
|
||||||
{/* Bitcoin Section */}
|
{/* Bitcoin Section */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Bitcoin className="h-5 w-5 text-orange-500" />
|
<Bitcoin className="h-5 w-5 text-orange-500" />
|
||||||
<h3 className="font-semibold text-white">Bitcoin</h3>
|
<h3 className="font-semibold text-white">Bitcoin</h3>
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge variant="secondary" className="text-xs">
|
||||||
Ordinal Verification Required
|
Ordinal Verification Required
|
||||||
</Badge>
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleBitcoinConnect}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white"
|
||||||
|
style={{
|
||||||
|
height: '44px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: 'none',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '600',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '8px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
Connecting...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Connect Bitcoin Wallet"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
onClick={handleBitcoinConnect}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="w-full bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white"
|
|
||||||
style={{
|
|
||||||
height: '44px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: 'none',
|
|
||||||
fontSize: '14px',
|
|
||||||
fontWeight: '600',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: '8px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
Connecting...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"Connect Bitcoin Wallet"
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute inset-0 flex items-center">
|
<div className="absolute inset-0 flex items-center">
|
||||||
<span className="w-full border-t border-neutral-700" />
|
<span className="w-full border-t border-neutral-700" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-xs uppercase">
|
||||||
|
<span className="bg-black px-2 text-neutral-500">or</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex justify-center text-xs uppercase">
|
|
||||||
<span className="bg-black px-2 text-neutral-500">or</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Ethereum Section */}
|
{/* Ethereum Section */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Coins className="h-5 w-5 text-blue-500" />
|
<Coins className="h-5 w-5 text-blue-500" />
|
||||||
<h3 className="font-semibold text-white">Ethereum</h3>
|
<h3 className="font-semibold text-white">Ethereum</h3>
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge variant="secondary" className="text-xs">
|
||||||
ENS Ownership Required
|
ENS Ownership Required
|
||||||
</Badge>
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleEthereumConnect}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white"
|
||||||
|
style={{
|
||||||
|
height: '44px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: 'none',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '600',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '8px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
Connecting...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Connect Ethereum Wallet"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
onClick={handleEthereumConnect}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white"
|
|
||||||
style={{
|
|
||||||
height: '44px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: 'none',
|
|
||||||
fontSize: '14px',
|
|
||||||
fontWeight: '600',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: '8px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
Connecting...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"Connect Ethereum Wallet"
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-xs text-neutral-500 text-center pt-2">
|
<div className="text-xs text-neutral-500 text-center pt-2">
|
||||||
Connect your wallet to use OpChan's features
|
Connect your wallet to use OpChan's features
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -33,25 +33,19 @@ export function WalletWizard({
|
|||||||
// Reset wizard when opened and determine starting step
|
// Reset wizard when opened and determine starting step
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
// Only auto-advance from step 1 to 2 when wallet connects during the session
|
// Determine the appropriate starting step based on current state
|
||||||
// Don't auto-advance to step 3 to allow manual navigation
|
if (!isAuthenticated) {
|
||||||
if (isAuthenticated && verificationStatus !== 'verified-owner') {
|
|
||||||
setCurrentStep(2); // Start at verification step if authenticated but not verified
|
|
||||||
} else if (!isAuthenticated) {
|
|
||||||
setCurrentStep(1); // Start at connection step if not authenticated
|
setCurrentStep(1); // Start at connection step if not authenticated
|
||||||
|
} else if (isAuthenticated && (verificationStatus === 'unverified' || verificationStatus === 'verifying')) {
|
||||||
|
setCurrentStep(2); // Start at verification step if authenticated but not verified
|
||||||
|
} else if (isAuthenticated && (verificationStatus === 'verified-owner' || verificationStatus === 'verified-none') && !isDelegationValid()) {
|
||||||
|
setCurrentStep(3); // Start at delegation step if verified but no valid delegation
|
||||||
} else {
|
} else {
|
||||||
setCurrentStep(1); // Default to step 1, let user navigate manually
|
setCurrentStep(3); // Default to step 3 if everything is complete
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [open, isAuthenticated, verificationStatus, isDelegationValid]);
|
}, [open, isAuthenticated, verificationStatus, isDelegationValid]); // Include all dependencies to properly determine step
|
||||||
|
|
||||||
// Auto-advance from step 1 to 2 only when wallet connects during the session
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (open && currentStep === 1 && isAuthenticated) {
|
|
||||||
setCurrentStep(2);
|
|
||||||
}
|
|
||||||
}, [open, currentStep, isAuthenticated]);
|
|
||||||
|
|
||||||
const handleStepComplete = (step: WizardStep) => {
|
const handleStepComplete = (step: WizardStep) => {
|
||||||
if (step < 3) {
|
if (step < 3) {
|
||||||
@ -150,8 +144,8 @@ export function WalletWizard({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Step Content */}
|
{/* Step Content - Fixed height container */}
|
||||||
<div className="min-h-[300px]">
|
<div className="h-[400px] flex flex-col">
|
||||||
{currentStep === 1 && (
|
{currentStep === 1 && (
|
||||||
<WalletConnectionStep
|
<WalletConnectionStep
|
||||||
onComplete={() => handleStepComplete(1)}
|
onComplete={() => handleStepComplete(1)}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { useToast } from '@/components/ui/use-toast';
|
|||||||
import { User } from '@/types';
|
import { User } from '@/types';
|
||||||
import { AuthService, AuthResult } from '@/lib/identity/services/AuthService';
|
import { AuthService, AuthResult } from '@/lib/identity/services/AuthService';
|
||||||
import { OpchanMessage } from '@/types';
|
import { OpchanMessage } from '@/types';
|
||||||
import { useAppKitAccount, useDisconnect } from '@reown/appkit/react';
|
import { useAppKitAccount, useDisconnect, modal } from '@reown/appkit/react';
|
||||||
|
|
||||||
export type VerificationStatus = 'unverified' | 'verified-none' | 'verified-owner' | 'verifying';
|
export type VerificationStatus = 'unverified' | 'verified-none' | 'verified-owner' | 'verifying';
|
||||||
|
|
||||||
@ -14,6 +14,7 @@ interface AuthContextType {
|
|||||||
verificationStatus: VerificationStatus;
|
verificationStatus: VerificationStatus;
|
||||||
verifyOwnership: () => Promise<boolean>;
|
verifyOwnership: () => Promise<boolean>;
|
||||||
delegateKey: () => Promise<boolean>;
|
delegateKey: () => Promise<boolean>;
|
||||||
|
clearDelegation: () => void;
|
||||||
isDelegationValid: () => boolean;
|
isDelegationValid: () => boolean;
|
||||||
delegationTimeRemaining: () => number;
|
delegationTimeRemaining: () => number;
|
||||||
isWalletAvailable: () => boolean;
|
isWalletAvailable: () => boolean;
|
||||||
@ -49,7 +50,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
// Create ref for AuthService so it persists between renders
|
// Create ref for AuthService so it persists between renders
|
||||||
const authServiceRef = useRef(new AuthService());
|
const authServiceRef = useRef(new AuthService());
|
||||||
|
|
||||||
// Set AppKit accounts in AuthService
|
// Set AppKit instance and accounts in AuthService
|
||||||
|
useEffect(() => {
|
||||||
|
if (modal) {
|
||||||
|
authServiceRef.current.setAppKit(modal);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
authServiceRef.current.setAccounts(bitcoinAccount, ethereumAccount);
|
authServiceRef.current.setAccounts(bitcoinAccount, ethereumAccount);
|
||||||
}, [bitcoinAccount, ethereumAccount]);
|
}, [bitcoinAccount, ethereumAccount]);
|
||||||
@ -250,6 +257,26 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
return authServiceRef.current.getDelegationTimeRemaining();
|
return authServiceRef.current.getDelegationTimeRemaining();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearDelegation = (): void => {
|
||||||
|
authServiceRef.current.clearDelegation();
|
||||||
|
|
||||||
|
// Update the current user to remove delegation info
|
||||||
|
if (currentUser) {
|
||||||
|
const updatedUser = {
|
||||||
|
...currentUser,
|
||||||
|
delegationExpiry: undefined,
|
||||||
|
browserPublicKey: undefined
|
||||||
|
};
|
||||||
|
setCurrentUser(updatedUser);
|
||||||
|
authServiceRef.current.saveUser(updatedUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Delegation Cleared",
|
||||||
|
description: "Your delegated signing key has been removed. You'll need to delegate a new key to continue posting and voting.",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const isWalletAvailable = (): boolean => {
|
const isWalletAvailable = (): boolean => {
|
||||||
return isConnected;
|
return isConnected;
|
||||||
};
|
};
|
||||||
@ -270,6 +297,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
verificationStatus,
|
verificationStatus,
|
||||||
verifyOwnership,
|
verifyOwnership,
|
||||||
delegateKey,
|
delegateKey,
|
||||||
|
clearDelegation,
|
||||||
isDelegationValid,
|
isDelegationValid,
|
||||||
delegationTimeRemaining,
|
delegationTimeRemaining,
|
||||||
isWalletAvailable,
|
isWalletAvailable,
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import { User } from '@/types';
|
import { User } from '@/types';
|
||||||
import { WalletService, AppKitAccount } from '../wallets/index';
|
import { WalletService } from '../wallets/index';
|
||||||
|
import { UseAppKitAccountReturn } from '@reown/appkit/react';
|
||||||
|
import { AppKit } from '@reown/appkit';
|
||||||
import { OrdinalAPI } from '../ordinal';
|
import { OrdinalAPI } from '../ordinal';
|
||||||
import { MessageSigning } from '../signatures/message-signing';
|
import { MessageSigning } from '../signatures/message-signing';
|
||||||
|
import { KeyDelegation } from '../signatures/key-delegation';
|
||||||
import { OpchanMessage } from '@/types';
|
import { OpchanMessage } from '@/types';
|
||||||
|
|
||||||
export interface AuthResult {
|
export interface AuthResult {
|
||||||
@ -14,44 +17,97 @@ export class AuthService {
|
|||||||
private walletService: WalletService;
|
private walletService: WalletService;
|
||||||
private ordinalApi: OrdinalAPI;
|
private ordinalApi: OrdinalAPI;
|
||||||
private messageSigning: MessageSigning;
|
private messageSigning: MessageSigning;
|
||||||
|
private keyDelegation: KeyDelegation;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.walletService = new WalletService();
|
this.walletService = new WalletService();
|
||||||
this.ordinalApi = new OrdinalAPI();
|
this.ordinalApi = new OrdinalAPI();
|
||||||
this.messageSigning = new MessageSigning(this.walletService.getKeyDelegation());
|
this.keyDelegation = new KeyDelegation();
|
||||||
|
this.messageSigning = new MessageSigning(this.keyDelegation);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set AppKit accounts for wallet service
|
* Set AppKit accounts for wallet service
|
||||||
*/
|
*/
|
||||||
setAccounts(bitcoinAccount: AppKitAccount, ethereumAccount: AppKitAccount) {
|
setAccounts(bitcoinAccount: UseAppKitAccountReturn, ethereumAccount: UseAppKitAccountReturn) {
|
||||||
this.walletService.setAccounts(bitcoinAccount, ethereumAccount);
|
this.walletService.setAccounts(bitcoinAccount, ethereumAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set AppKit instance for wallet service
|
||||||
|
*/
|
||||||
|
setAppKit(appKit: AppKit) {
|
||||||
|
this.walletService.setAppKit(appKit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the active wallet address
|
||||||
|
*/
|
||||||
|
private getActiveAddress(): string | null {
|
||||||
|
const isBitcoinConnected = this.walletService.isWalletAvailable('bitcoin');
|
||||||
|
const isEthereumConnected = this.walletService.isWalletAvailable('ethereum');
|
||||||
|
|
||||||
|
if (isBitcoinConnected) {
|
||||||
|
return this.walletService.getActiveAddress('bitcoin') || null;
|
||||||
|
} else if (isEthereumConnected) {
|
||||||
|
return this.walletService.getActiveAddress('ethereum') || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the active wallet type
|
||||||
|
*/
|
||||||
|
private getActiveWalletType(): 'bitcoin' | 'ethereum' | null {
|
||||||
|
if (this.walletService.isWalletAvailable('bitcoin')) {
|
||||||
|
return 'bitcoin';
|
||||||
|
} else if (this.walletService.isWalletAvailable('ethereum')) {
|
||||||
|
return 'ethereum';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to wallet and create user
|
* Connect to wallet and create user
|
||||||
*/
|
*/
|
||||||
async connectWallet(): Promise<AuthResult> {
|
async connectWallet(): Promise<AuthResult> {
|
||||||
try {
|
try {
|
||||||
const walletInfo = await this.walletService.getWalletInfo();
|
// Check which wallet is connected
|
||||||
if (!walletInfo) {
|
const isBitcoinConnected = this.walletService.isWalletAvailable('bitcoin');
|
||||||
|
const isEthereumConnected = this.walletService.isWalletAvailable('ethereum');
|
||||||
|
|
||||||
|
if (!isBitcoinConnected && !isEthereumConnected) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'No wallet connected'
|
error: 'No wallet connected'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine which wallet is active
|
||||||
|
const walletType = isBitcoinConnected ? 'bitcoin' : 'ethereum';
|
||||||
|
const address = this.getActiveAddress();
|
||||||
|
|
||||||
|
if (!address) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'No wallet address available'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const user: User = {
|
const user: User = {
|
||||||
address: walletInfo.address,
|
address: address,
|
||||||
walletType: walletInfo.walletType,
|
walletType: walletType,
|
||||||
verificationStatus: 'unverified',
|
verificationStatus: 'unverified',
|
||||||
lastChecked: Date.now(),
|
lastChecked: Date.now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add ENS info for Ethereum wallets
|
// Add ENS info for Ethereum wallets (if available)
|
||||||
if (walletInfo.walletType === 'ethereum' && walletInfo.ensName) {
|
if (walletType === 'ethereum') {
|
||||||
user.ensName = walletInfo.ensName;
|
// Note: ENS resolution would need to be implemented separately
|
||||||
user.ensOwnership = true;
|
// For now, we'll leave it as undefined
|
||||||
|
user.ensName = undefined;
|
||||||
|
user.ensOwnership = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -67,13 +123,23 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disconnect wallet and clear user data
|
* Disconnect wallet and clear stored data
|
||||||
*/
|
*/
|
||||||
async disconnectWallet(): Promise<void> {
|
async disconnectWallet(): Promise<void> {
|
||||||
const walletType = this.walletService.getActiveWalletType();
|
// Clear any existing delegations when disconnecting
|
||||||
if (walletType) {
|
this.keyDelegation.clearDelegation();
|
||||||
await this.walletService.disconnectWallet(walletType);
|
this.walletService.clearDelegation('bitcoin');
|
||||||
}
|
this.walletService.clearDelegation('ethereum');
|
||||||
|
|
||||||
|
// Clear stored user data
|
||||||
|
this.clearStoredUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear delegation for current wallet
|
||||||
|
*/
|
||||||
|
clearDelegation(): void {
|
||||||
|
this.keyDelegation.clearDelegation();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -124,13 +190,14 @@ export class AuthService {
|
|||||||
* Verify Ethereum ENS ownership
|
* Verify Ethereum ENS ownership
|
||||||
*/
|
*/
|
||||||
private async verifyEthereumENS(user: User): Promise<AuthResult> {
|
private async verifyEthereumENS(user: User): Promise<AuthResult> {
|
||||||
const walletInfo = await this.walletService.getWalletInfo();
|
// Note: ENS resolution would need to be implemented separately
|
||||||
const hasENS = walletInfo?.ensName && walletInfo.ensName.length > 0;
|
// For now, we'll assume no ENS ownership
|
||||||
|
const hasENS = false;
|
||||||
|
|
||||||
const updatedUser = {
|
const updatedUser = {
|
||||||
...user,
|
...user,
|
||||||
ensOwnership: hasENS,
|
ensOwnership: hasENS,
|
||||||
ensName: walletInfo?.ensName,
|
ensName: undefined,
|
||||||
lastChecked: Date.now(),
|
lastChecked: Date.now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -146,24 +213,35 @@ export class AuthService {
|
|||||||
async delegateKey(user: User): Promise<AuthResult> {
|
async delegateKey(user: User): Promise<AuthResult> {
|
||||||
try {
|
try {
|
||||||
const walletType = user.walletType;
|
const walletType = user.walletType;
|
||||||
const canConnect = await this.walletService.canConnectWallet(walletType);
|
const isAvailable = this.walletService.isWalletAvailable(walletType);
|
||||||
if (!canConnect) {
|
|
||||||
|
if (!isAvailable) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `${walletType} wallet is not available or cannot be connected. Please ensure it is installed and unlocked.`
|
error: `${walletType} wallet is not available or connected. Please ensure it is connected.`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const delegationInfo = await this.walletService.setupKeyDelegation(
|
const success = await this.walletService.createKeyDelegation(walletType);
|
||||||
user.address,
|
|
||||||
walletType
|
if (!success) {
|
||||||
);
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to create key delegation'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get delegation status to update user
|
||||||
|
const delegationStatus = this.walletService.getDelegationStatus(walletType);
|
||||||
|
|
||||||
|
// Get the actual browser public key from the delegation
|
||||||
|
const browserPublicKey = this.keyDelegation.getBrowserPublicKey();
|
||||||
|
|
||||||
const updatedUser = {
|
const updatedUser = {
|
||||||
...user,
|
...user,
|
||||||
browserPubKey: delegationInfo.browserPublicKey,
|
browserPubKey: browserPublicKey || undefined,
|
||||||
delegationSignature: delegationInfo.signature,
|
delegationSignature: delegationStatus.isValid ? 'valid' : undefined,
|
||||||
delegationExpiry: delegationInfo.expiryTimestamp,
|
delegationExpiry: delegationStatus.timeRemaining ? Date.now() + delegationStatus.timeRemaining : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -196,21 +274,49 @@ export class AuthService {
|
|||||||
* Check if delegation is valid
|
* Check if delegation is valid
|
||||||
*/
|
*/
|
||||||
isDelegationValid(): boolean {
|
isDelegationValid(): boolean {
|
||||||
return this.walletService.isDelegationValid();
|
// Only check the currently connected wallet type
|
||||||
|
const activeWalletType = this.getActiveWalletType();
|
||||||
|
if (!activeWalletType) return false;
|
||||||
|
|
||||||
|
const status = this.walletService.getDelegationStatus(activeWalletType);
|
||||||
|
return status.isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get delegation time remaining
|
* Get delegation time remaining
|
||||||
*/
|
*/
|
||||||
getDelegationTimeRemaining(): number {
|
getDelegationTimeRemaining(): number {
|
||||||
return this.walletService.getDelegationTimeRemaining();
|
// Only check the currently connected wallet type
|
||||||
|
const activeWalletType = this.getActiveWalletType();
|
||||||
|
if (!activeWalletType) return 0;
|
||||||
|
|
||||||
|
const status = this.walletService.getDelegationStatus(activeWalletType);
|
||||||
|
return status.timeRemaining || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current wallet info
|
* Get current wallet info
|
||||||
*/
|
*/
|
||||||
async getWalletInfo() {
|
async getWalletInfo() {
|
||||||
return await this.walletService.getWalletInfo();
|
// Return basic wallet info based on what's available
|
||||||
|
const isBitcoinConnected = this.walletService.isWalletAvailable('bitcoin');
|
||||||
|
const isEthereumConnected = this.walletService.isWalletAvailable('ethereum');
|
||||||
|
|
||||||
|
if (isBitcoinConnected) {
|
||||||
|
return {
|
||||||
|
address: this.getActiveAddress(),
|
||||||
|
walletType: 'bitcoin' as const,
|
||||||
|
isConnected: true
|
||||||
|
};
|
||||||
|
} else if (isEthereumConnected) {
|
||||||
|
return {
|
||||||
|
address: this.getActiveAddress(),
|
||||||
|
walletType: 'ethereum' as const,
|
||||||
|
isConnected: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -36,45 +36,47 @@ export class KeyDelegation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a delegation message to be signed by the Bitcoin wallet
|
* Creates a delegation message to be signed by the wallet
|
||||||
* @param browserPublicKey The browser-generated public key
|
* @param browserPublicKey The browser-generated public key
|
||||||
* @param bitcoinAddress The user's Bitcoin address
|
* @param walletAddress The user's wallet address
|
||||||
* @param expiryTimestamp When the delegation will expire
|
* @param expiryTimestamp When the delegation will expire
|
||||||
* @returns The message to be signed
|
* @returns The message to be signed
|
||||||
*/
|
*/
|
||||||
createDelegationMessage(
|
createDelegationMessage(
|
||||||
browserPublicKey: string,
|
browserPublicKey: string,
|
||||||
bitcoinAddress: string,
|
walletAddress: string,
|
||||||
expiryTimestamp: number
|
expiryTimestamp: number
|
||||||
): string {
|
): string {
|
||||||
return `I, ${bitcoinAddress}, delegate authority to this pubkey: ${browserPublicKey} until ${expiryTimestamp}`;
|
return `I, ${walletAddress}, delegate authority to this pubkey: ${browserPublicKey} until ${expiryTimestamp}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a delegation with the specified expiry time in hours
|
* Creates a delegation object from the signed message
|
||||||
* @param bitcoinAddress The Bitcoin wallet address
|
* @param walletAddress The wallet address that signed the delegation
|
||||||
* @param signature The signature from the Bitcoin wallet
|
* @param signature The signature from the wallet
|
||||||
* @param browserPublicKey The browser public key
|
* @param browserPublicKey The browser-generated public key
|
||||||
* @param browserPrivateKey The browser private key
|
* @param browserPrivateKey The browser-generated private key
|
||||||
* @param expiryHours How many hours the delegation should be valid (default: 24)
|
* @param expiryHours How many hours the delegation should last
|
||||||
* @returns The created delegation info
|
* @param walletType The type of wallet (bitcoin or ethereum)
|
||||||
|
* @returns DelegationInfo object
|
||||||
*/
|
*/
|
||||||
createDelegation(
|
createDelegation(
|
||||||
bitcoinAddress: string,
|
walletAddress: string,
|
||||||
signature: string,
|
signature: string,
|
||||||
browserPublicKey: string,
|
browserPublicKey: string,
|
||||||
browserPrivateKey: string,
|
browserPrivateKey: string,
|
||||||
expiryHours: number = KeyDelegation.DEFAULT_EXPIRY_HOURS
|
expiryHours: number = KeyDelegation.DEFAULT_EXPIRY_HOURS,
|
||||||
|
walletType: 'bitcoin' | 'ethereum'
|
||||||
): DelegationInfo {
|
): DelegationInfo {
|
||||||
const now = Date.now();
|
const expiryTimestamp = Date.now() + (expiryHours * 60 * 60 * 1000);
|
||||||
const expiryTimestamp = now + (expiryHours * 60 * 60 * 1000);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
signature,
|
signature,
|
||||||
expiryTimestamp,
|
expiryTimestamp,
|
||||||
browserPublicKey,
|
browserPublicKey,
|
||||||
browserPrivateKey,
|
browserPrivateKey,
|
||||||
bitcoinAddress
|
walletAddress,
|
||||||
|
walletType
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,15 +105,30 @@ export class KeyDelegation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a delegation is valid (exists and not expired)
|
* Checks if a delegation is valid (exists, not expired, and matches current wallet)
|
||||||
|
* @param currentAddress Optional current wallet address to validate against
|
||||||
|
* @param currentWalletType Optional current wallet type to validate against
|
||||||
* @returns boolean indicating if the delegation is valid
|
* @returns boolean indicating if the delegation is valid
|
||||||
*/
|
*/
|
||||||
isDelegationValid(): boolean {
|
isDelegationValid(currentAddress?: string, currentWalletType?: 'bitcoin' | 'ethereum'): boolean {
|
||||||
const delegation = this.retrieveDelegation();
|
const delegation = this.retrieveDelegation();
|
||||||
if (!delegation) return false;
|
if (!delegation) return false;
|
||||||
|
|
||||||
|
// Check if delegation has expired
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
return now < delegation.expiryTimestamp;
|
if (now >= delegation.expiryTimestamp) return false;
|
||||||
|
|
||||||
|
// If a current address is provided, validate it matches the delegation
|
||||||
|
if (currentAddress && delegation.walletAddress !== currentAddress) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a current wallet type is provided, validate it matches the delegation
|
||||||
|
if (currentWalletType && delegation.walletType !== currentWalletType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -166,7 +183,7 @@ export class KeyDelegation {
|
|||||||
getDelegatingAddress(): string | null {
|
getDelegatingAddress(): string | null {
|
||||||
const delegation = this.retrieveDelegation();
|
const delegation = this.retrieveDelegation();
|
||||||
if (!delegation || !this.isDelegationValid()) return null;
|
if (!delegation || !this.isDelegationValid()) return null;
|
||||||
return delegation.bitcoinAddress;
|
return delegation.walletAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
export interface DelegationSignature {
|
export interface DelegationSignature {
|
||||||
signature: string; // Signature from Bitcoin wallet
|
signature: string; // Signature from wallet
|
||||||
expiryTimestamp: number; // When this delegation expires
|
expiryTimestamp: number; // When this delegation expires
|
||||||
browserPublicKey: string; // Browser-generated public key that was delegated to
|
browserPublicKey: string; // Browser-generated public key that was delegated to
|
||||||
bitcoinAddress: string; // Bitcoin address that signed the delegation
|
walletAddress: string; // Wallet address that signed the delegation
|
||||||
|
walletType: 'bitcoin' | 'ethereum'; // Type of wallet that created the delegation
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DelegationInfo extends DelegationSignature {
|
export interface DelegationInfo extends DelegationSignature {
|
||||||
|
|||||||
@ -1,28 +1,14 @@
|
|||||||
|
import { UseAppKitAccountReturn } from '@reown/appkit/react';
|
||||||
import { KeyDelegation } from '../signatures/key-delegation';
|
import { KeyDelegation } from '../signatures/key-delegation';
|
||||||
import { bytesToHex } from '@/lib/utils';
|
import { AppKit } from '@reown/appkit';
|
||||||
import { getEnsName } from '@wagmi/core';
|
import { ChainNamespace } from '@reown/appkit-common';
|
||||||
import { config } from './appkit';
|
import { Provider} from '@reown/appkit-controllers';
|
||||||
import { UseAppKitAccountReturn } from '@reown/appkit';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface WalletInfo {
|
|
||||||
address: string;
|
|
||||||
walletType: 'bitcoin' | 'ethereum';
|
|
||||||
ensName?: string;
|
|
||||||
isConnected: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DelegationInfo {
|
|
||||||
browserPublicKey: string;
|
|
||||||
signature: string;
|
|
||||||
expiryTimestamp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ReOwnWalletService {
|
export class ReOwnWalletService {
|
||||||
private keyDelegation: KeyDelegation;
|
private keyDelegation: KeyDelegation;
|
||||||
private bitcoinAccount?: UseAppKitAccountReturn;
|
private bitcoinAccount?: UseAppKitAccountReturn;
|
||||||
private ethereumAccount?: UseAppKitAccountReturn;
|
private ethereumAccount?: UseAppKitAccountReturn;
|
||||||
|
private appKit?: AppKit;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.keyDelegation = new KeyDelegation();
|
this.keyDelegation = new KeyDelegation();
|
||||||
@ -36,190 +22,189 @@ export class ReOwnWalletService {
|
|||||||
this.ethereumAccount = ethereumAccount;
|
this.ethereumAccount = ethereumAccount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the AppKit instance for accessing adapters
|
||||||
|
*/
|
||||||
|
setAppKit(appKit: AppKit) {
|
||||||
|
this.appKit = appKit;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a wallet type is available and connected
|
* Check if a wallet type is available and connected
|
||||||
*/
|
*/
|
||||||
isWalletAvailable(walletType: 'bitcoin' | 'ethereum'): boolean {
|
isWalletAvailable(walletType: 'bitcoin' | 'ethereum'): boolean {
|
||||||
if (walletType === 'bitcoin') {
|
if (walletType === 'bitcoin') {
|
||||||
return this.bitcoinAccount?.isConnected || false;
|
return this.bitcoinAccount?.isConnected ?? false;
|
||||||
} else {
|
} else {
|
||||||
return this.ethereumAccount?.isConnected || false;
|
return this.ethereumAccount?.isConnected ?? false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if wallet can be connected
|
* Get the active account based on wallet type
|
||||||
*/
|
*/
|
||||||
async canConnectWallet(walletType: 'bitcoin' | 'ethereum'): Promise<boolean> {
|
private getActiveAccount(walletType: 'bitcoin' | 'ethereum'): UseAppKitAccountReturn | undefined {
|
||||||
// For ReOwn, we assume connection is always possible if AppKit is initialized
|
return walletType === 'bitcoin' ? this.bitcoinAccount : this.ethereumAccount;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get wallet connection info with ENS resolution for Ethereum
|
* Get the active address for a given wallet type
|
||||||
*/
|
*/
|
||||||
async getWalletInfo(): Promise<WalletInfo | null> {
|
getActiveAddress(walletType: 'bitcoin' | 'ethereum'): string | undefined {
|
||||||
if (this.bitcoinAccount?.isConnected) {
|
const account = this.getActiveAccount(walletType);
|
||||||
return {
|
return account?.address;
|
||||||
address: this.bitcoinAccount.address,
|
}
|
||||||
walletType: 'bitcoin',
|
|
||||||
isConnected: true
|
/**
|
||||||
};
|
* Get the appropriate namespace for the wallet type
|
||||||
} else if (this.ethereumAccount?.isConnected) {
|
*/
|
||||||
// Use Wagmi to resolve ENS name
|
private getNamespace(walletType: 'bitcoin' | 'ethereum'): ChainNamespace {
|
||||||
let ensName: string | undefined;
|
return walletType === 'bitcoin' ? 'bip122' : 'eip155';
|
||||||
try {
|
}
|
||||||
const resolvedName = await getEnsName(config, {
|
|
||||||
address: this.ethereumAccount.address as `0x${string}`
|
/**
|
||||||
});
|
* Sign a message using the appropriate adapter
|
||||||
ensName = resolvedName || undefined;
|
*/
|
||||||
} catch (error) {
|
async signMessage(messageBytes: Uint8Array, walletType: 'bitcoin' | 'ethereum'): Promise<string> {
|
||||||
console.warn('Failed to resolve ENS name:', error);
|
if (!this.appKit) {
|
||||||
// Continue without ENS name
|
throw new Error('AppKit instance not set. Call setAppKit() first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const account = this.getActiveAccount(walletType);
|
||||||
|
if (!account?.address) {
|
||||||
|
throw new Error(`No ${walletType} wallet connected`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const namespace = this.getNamespace(walletType);
|
||||||
|
|
||||||
|
// Convert message bytes to string for signing
|
||||||
|
const messageString = new TextDecoder().decode(messageBytes);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Access the adapter through the appKit instance
|
||||||
|
// The adapter is available through the appKit's chainAdapters property
|
||||||
|
const adapter = this.appKit.chainAdapters?.[namespace];
|
||||||
|
|
||||||
|
if (!adapter) {
|
||||||
|
throw new Error(`No adapter found for namespace: ${namespace}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
// Get the provider for the current connection
|
||||||
address: this.ethereumAccount.address,
|
const provider = this.appKit.getProvider(namespace);
|
||||||
walletType: 'ethereum',
|
|
||||||
ensName,
|
if (!provider) {
|
||||||
isConnected: true
|
throw new Error(`No provider found for namespace: ${namespace}`);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
// Call the adapter's signMessage method
|
||||||
|
const result = await adapter.signMessage({
|
||||||
|
message: messageString,
|
||||||
|
address: account.address,
|
||||||
|
provider: provider as Provider
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.signature;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error signing message with ${walletType} wallet:`, error);
|
||||||
|
throw new Error(`Failed to sign message with ${walletType} wallet: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the active wallet address
|
* Create a key delegation for the connected wallet
|
||||||
*/
|
*/
|
||||||
getActiveAddress(): string | null {
|
async createKeyDelegation(walletType: 'bitcoin' | 'ethereum'): Promise<boolean> {
|
||||||
if (this.bitcoinAccount?.isConnected) {
|
try {
|
||||||
return this.bitcoinAccount.address;
|
const account = this.getActiveAccount(walletType);
|
||||||
} else if (this.ethereumAccount?.isConnected) {
|
if (!account?.address) {
|
||||||
return this.ethereumAccount.address;
|
throw new Error(`No ${walletType} wallet connected`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a new browser keypair
|
||||||
|
const keypair = this.keyDelegation.generateKeypair();
|
||||||
|
|
||||||
|
// Create delegation message with expiry
|
||||||
|
const expiryTimestamp = Date.now() + (24 * 60 * 60 * 1000); // 24 hours
|
||||||
|
const delegationMessage = this.keyDelegation.createDelegationMessage(
|
||||||
|
keypair.publicKey,
|
||||||
|
account.address,
|
||||||
|
expiryTimestamp
|
||||||
|
);
|
||||||
|
|
||||||
|
const messageBytes = new TextEncoder().encode(delegationMessage);
|
||||||
|
|
||||||
|
// Sign the delegation message
|
||||||
|
const signature = await this.signMessage(messageBytes, walletType);
|
||||||
|
|
||||||
|
// Create and store the delegation
|
||||||
|
const delegationInfo = this.keyDelegation.createDelegation(
|
||||||
|
account.address,
|
||||||
|
signature,
|
||||||
|
keypair.publicKey,
|
||||||
|
keypair.privateKey,
|
||||||
|
24, // 24 hours
|
||||||
|
walletType
|
||||||
|
);
|
||||||
|
|
||||||
|
this.keyDelegation.storeDelegation(delegationInfo);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error creating key delegation for ${walletType}:`, error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the active wallet type
|
* Sign a message using the delegated key (if available) or fall back to wallet signing
|
||||||
*/
|
*/
|
||||||
getActiveWalletType(): 'bitcoin' | 'ethereum' | null {
|
async signMessageWithDelegation(messageBytes: Uint8Array, walletType: 'bitcoin' | 'ethereum'): Promise<string> {
|
||||||
if (this.bitcoinAccount?.isConnected) {
|
const account = this.getActiveAccount(walletType);
|
||||||
return 'bitcoin';
|
if (!account?.address) {
|
||||||
} else if (this.ethereumAccount?.isConnected) {
|
throw new Error(`No ${walletType} wallet connected`);
|
||||||
return 'ethereum';
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
// Check if we have a valid delegation for this specific wallet
|
||||||
|
if (this.keyDelegation.isDelegationValid(account.address, walletType)) {
|
||||||
|
// Use delegated key for signing
|
||||||
|
const messageString = new TextDecoder().decode(messageBytes);
|
||||||
|
const signature = this.keyDelegation.signMessage(messageString);
|
||||||
|
|
||||||
|
if (signature) {
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to wallet signing
|
||||||
|
return this.signMessage(messageBytes, walletType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup key delegation for the connected wallet
|
* Get delegation status for the connected wallet
|
||||||
*/
|
*/
|
||||||
async setupKeyDelegation(
|
getDelegationStatus(walletType: 'bitcoin' | 'ethereum'): {
|
||||||
address: string,
|
hasDelegation: boolean;
|
||||||
walletType: 'bitcoin' | 'ethereum'
|
isValid: boolean;
|
||||||
): Promise<DelegationInfo> {
|
timeRemaining?: number;
|
||||||
// Generate browser keypair
|
} {
|
||||||
const keypair = this.keyDelegation.generateKeypair();
|
const account = this.getActiveAccount(walletType);
|
||||||
|
const currentAddress = account?.address;
|
||||||
|
|
||||||
// Create delegation message with chain-specific format
|
const hasDelegation = this.keyDelegation.retrieveDelegation() !== null;
|
||||||
const expiryTimestamp = Date.now() + (24 * 60 * 60 * 1000); // 24 hours
|
const isValid = this.keyDelegation.isDelegationValid(currentAddress, walletType);
|
||||||
const delegationMessage = this.createDelegationMessage(
|
const timeRemaining = this.keyDelegation.getDelegationTimeRemaining();
|
||||||
keypair.publicKey,
|
|
||||||
address,
|
|
||||||
walletType,
|
|
||||||
expiryTimestamp
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get the appropriate account for signing
|
|
||||||
const account = walletType === 'bitcoin' ? this.bitcoinAccount : this.ethereumAccount;
|
|
||||||
if (!account?.isConnected) {
|
|
||||||
throw new Error(`${walletType} wallet is not connected`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign the delegation message
|
|
||||||
const signature = await this.signMessage(delegationMessage, walletType);
|
|
||||||
|
|
||||||
// Create and store delegation
|
|
||||||
const delegationInfo = this.keyDelegation.createDelegation(
|
|
||||||
address,
|
|
||||||
signature,
|
|
||||||
keypair.publicKey,
|
|
||||||
keypair.privateKey,
|
|
||||||
24
|
|
||||||
);
|
|
||||||
|
|
||||||
this.keyDelegation.storeDelegation(delegationInfo);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
browserPublicKey: keypair.publicKey,
|
hasDelegation,
|
||||||
signature,
|
isValid,
|
||||||
expiryTimestamp
|
timeRemaining: timeRemaining > 0 ? timeRemaining : undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create chain-specific delegation message
|
* Clear delegation for the connected wallet
|
||||||
*/
|
*/
|
||||||
private createDelegationMessage(
|
clearDelegation(walletType: 'bitcoin' | 'ethereum'): void {
|
||||||
browserPublicKey: string,
|
|
||||||
address: string,
|
|
||||||
walletType: 'bitcoin' | 'ethereum',
|
|
||||||
expiryTimestamp: number
|
|
||||||
): string {
|
|
||||||
const chainName = walletType === 'bitcoin' ? 'Bitcoin' : 'Ethereum';
|
|
||||||
const expiryDate = new Date(expiryTimestamp).toISOString();
|
|
||||||
|
|
||||||
return `I, ${address} (${chainName}), delegate authority to this pubkey: ${browserPublicKey} until ${expiryDate}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sign a message with the appropriate wallet
|
|
||||||
*/
|
|
||||||
private async signMessage(message: string, walletType: 'bitcoin' | 'ethereum'): Promise<string> {
|
|
||||||
const account = walletType === 'bitcoin' ? this.bitcoinAccount : this.ethereumAccount;
|
|
||||||
if (!account?.isConnected) {
|
|
||||||
throw new Error(`${walletType} wallet is not connected`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert message to bytes for signing
|
|
||||||
const messageBytes = new TextEncoder().encode(message);
|
|
||||||
|
|
||||||
// Sign with the appropriate wallet
|
|
||||||
const signature = await account.signMessage({ message: messageBytes });
|
|
||||||
|
|
||||||
// Return hex-encoded signature
|
|
||||||
return bytesToHex(signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disconnect wallet (handled by AppKit)
|
|
||||||
*/
|
|
||||||
async disconnectWallet(walletType: 'bitcoin' | 'ethereum'): Promise<void> {
|
|
||||||
// Clear stored delegation
|
|
||||||
this.keyDelegation.clearDelegation();
|
this.keyDelegation.clearDelegation();
|
||||||
|
|
||||||
// Note: Actual disconnection is handled by AppKit's useDisconnect hook
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if delegation is valid
|
|
||||||
*/
|
|
||||||
isDelegationValid(): boolean {
|
|
||||||
return this.keyDelegation.isDelegationValid();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get delegation time remaining
|
|
||||||
*/
|
|
||||||
getDelegationTimeRemaining(): number {
|
|
||||||
return this.keyDelegation.getDelegationTimeRemaining();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the key delegation instance
|
|
||||||
*/
|
|
||||||
getKeyDelegation(): KeyDelegation {
|
|
||||||
return this.keyDelegation;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,2 +1 @@
|
|||||||
export { ReOwnWalletService as WalletService } from './ReOwnWalletService';
|
export { ReOwnWalletService as WalletService } from './ReOwnWalletService';
|
||||||
export type { WalletInfo, DelegationInfo, AppKitAccount } from './ReOwnWalletService';
|
|
||||||
Loading…
x
Reference in New Issue
Block a user