chore: key delegation for 7 days / 30 days

This commit is contained in:
Danish Arora 2025-08-13 12:00:40 +05:30
parent 3158b60aed
commit 6f7cbb4b45
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
8 changed files with 185 additions and 124 deletions

View File

@ -1,2 +1,3 @@
VITE_REOWN_SECRETVITE_REOWN_SECRET
# Mock/bypass settings for development
VITE_OPCHAN_MOCK_ORDINAL_CHECK=false

View File

@ -78,7 +78,7 @@ OpChan uses a two-tier authentication system:
1. **Wallet Connection**: Initial connection to Phantom wallet
2. **Key Delegation**: Optional browser key generation for improved UX
- Reduces wallet signature prompts
- 24-hour validity period
- Configurable duration: 1 week or 30 days
- Can be regenerated anytime
### Network & Performance

View File

@ -1,7 +1,8 @@
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Key, Loader2, CheckCircle, AlertCircle, Trash2 } from "lucide-react";
import { useAuth } from "@/contexts/useAuth";
import React from 'react';
import { Button } from './button';
import { useAuth } from '@/contexts/useAuth';
import { CheckCircle, AlertCircle, Trash2 } from 'lucide-react';
import { DelegationDuration } from '@/lib/identity/signatures/key-delegation';
interface DelegationStepProps {
onComplete: () => void;
@ -25,6 +26,7 @@ export function DelegationStep({
clearDelegation
} = useAuth();
const [selectedDuration, setSelectedDuration] = React.useState<DelegationDuration>('7days');
const [delegationResult, setDelegationResult] = React.useState<{
success: boolean;
message: string;
@ -38,12 +40,12 @@ export function DelegationStep({
setDelegationResult(null);
try {
const success = await delegateKey();
const success = await delegateKey(selectedDuration);
if (success) {
const expiryDate = currentUser.delegationExpiry
? new Date(currentUser.delegationExpiry).toLocaleString()
: '24 hours from now';
: `${selectedDuration === '7days' ? '1 week' : '30 days'} from now`;
setDelegationResult({
success: true,
@ -127,120 +129,129 @@ export function DelegationStep({
<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 className="w-16 h-16 bg-neutral-800 rounded-full flex items-center justify-center">
<svg className="w-8 h-8 text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
</div>
</div>
<h3 className="text-lg font-semibold text-white">
Delegate Signing Key
</h3>
<h3 className="text-lg font-semibold text-neutral-100">Key Delegation</h3>
<p className="text-sm text-neutral-400">
Create a browser-based signing key
Delegate signing authority to your browser for convenient forum interactions
</p>
</div>
{/* 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="space-y-3">
{/* Status */}
<div className="flex items-center gap-2">
{isDelegationValid() ? (
<CheckCircle className="h-4 w-4 text-green-500" />
) : (
<div className="text-blue-500 text-sm">Ξ</div>
<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>
<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>
)}
{/* Duration Selection */}
{!isDelegationValid() && (
<div className="space-y-3">
<label className="text-sm font-medium text-neutral-300">Delegation Duration:</label>
<div className="space-y-2">
<label className="flex items-center space-x-2 cursor-pointer">
<input
type="radio"
name="duration"
value="7days"
checked={selectedDuration === '7days'}
onChange={(e) => setSelectedDuration(e.target.value as DelegationDuration)}
className="w-4 h-4 text-green-600 bg-neutral-800 border-neutral-600 focus:ring-green-500 focus:ring-2"
/>
<span className="text-sm text-neutral-300">1 Week</span>
</label>
<label className="flex items-center space-x-2 cursor-pointer">
<input
type="radio"
name="duration"
value="30days"
checked={selectedDuration === '30days'}
onChange={(e) => setSelectedDuration(e.target.value as DelegationDuration)}
className="w-4 h-4 text-green-600 bg-neutral-800 border-neutral-600 focus:ring-green-500 focus:ring-2"
/>
<span className="text-sm text-neutral-300">30 Days</span>
</label>
</div>
</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>
)}
{/* 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>
)}
{/* Wallet Address */}
{currentUser && (
<div className="text-xs text-neutral-400">
<div className="font-mono break-all">
{currentUser.address}
</div>
</div>
)}
{/* Wallet Address */}
{currentUser && (
<div className="text-xs text-neutral-400">
<div className="font-mono break-all">
{currentUser.address}
</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>
)}
{/* Delete Button for Active Delegations */}
{isDelegationValid() && (
<div className="flex justify-end">
<Button
onClick={clearDelegation}
variant="outline"
size="sm"
className="text-red-400 border-red-400/30 hover:bg-red-400/10"
>
<Trash2 className="w-4 h-4 mr-1" />
Clear Delegation
</Button>
</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() && (
<div className="mt-auto space-y-2">
{isDelegationValid() ? (
<Button
onClick={handleComplete}
className="w-full bg-green-600 hover:bg-green-700 text-white"
disabled={isLoading}
>
Complete Setup
Continue
</Button>
) : (
<Button
onClick={handleDelegate}
className="w-full bg-green-600 hover:bg-green-700 text-white"
disabled={isLoading || isAuthenticating}
>
{isAuthenticating ? 'Delegating...' : 'Delegate Key'}
</Button>
)}
<Button
onClick={onBack}
variant="outline"
className="w-full border-neutral-600 text-neutral-400 hover:bg-neutral-800"
disabled={isLoading || isAuthenticating}
className="w-full border-neutral-600 text-neutral-300 hover:bg-neutral-800"
disabled={isLoading}
>
Back
</Button>

View File

@ -29,10 +29,11 @@ export function WalletWizard({
const [currentStep, setCurrentStep] = React.useState<WizardStep>(1);
const [isLoading, setIsLoading] = React.useState(false);
const { currentUser, isAuthenticated, verificationStatus, isDelegationValid } = useAuth();
const hasInitialized = React.useRef(false);
// Reset wizard when opened and determine starting step
React.useEffect(() => {
if (open) {
if (open && !hasInitialized.current) {
// Determine the appropriate starting step based on current state
if (!isAuthenticated) {
setCurrentStep(1); // Start at connection step if not authenticated
@ -44,8 +45,11 @@ export function WalletWizard({
setCurrentStep(3); // Default to step 3 if everything is complete
}
setIsLoading(false);
hasInitialized.current = true;
} else if (!open) {
hasInitialized.current = false;
}
}, [open, isAuthenticated, verificationStatus, isDelegationValid]); // Include all dependencies to properly determine step
}, [open, isAuthenticated, verificationStatus, isDelegationValid]);
const handleStepComplete = (step: WizardStep) => {
if (step < 3) {

View File

@ -4,24 +4,24 @@ import { User } from '@/types';
import { AuthService, AuthResult } from '@/lib/identity/services/AuthService';
import { OpchanMessage } from '@/types';
import { useAppKitAccount, useDisconnect, modal } from '@reown/appkit/react';
import { DelegationDuration } from '@/lib/identity/signatures/key-delegation';
export type VerificationStatus = 'unverified' | 'verified-none' | 'verified-owner' | 'verifying';
interface AuthContextType {
currentUser: User | null;
isAuthenticated: boolean;
isAuthenticating: boolean;
isAuthenticated: boolean;
verificationStatus: VerificationStatus;
connectWallet: () => Promise<boolean>;
disconnectWallet: () => void;
verifyOwnership: () => Promise<boolean>;
delegateKey: () => Promise<boolean>;
clearDelegation: () => void;
delegateKey: (duration?: DelegationDuration) => Promise<boolean>;
isDelegationValid: () => boolean;
delegationTimeRemaining: () => number;
isWalletAvailable: () => boolean;
messageSigning: {
signMessage: (message: OpchanMessage) => Promise<OpchanMessage | null>;
verifyMessage: (message: OpchanMessage) => boolean;
};
clearDelegation: () => void;
signMessage: (message: OpchanMessage) => Promise<OpchanMessage | null>;
verifyMessage: (message: OpchanMessage) => boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
@ -105,6 +105,25 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}
}, [isConnected, address, isBitcoinConnected, isEthereumConnected, toast]);
const { disconnect } = useDisconnect();
const connectWallet = async (): Promise<boolean> => {
try {
if (modal) {
await modal.open();
return true;
}
return false;
} catch (error) {
console.error('Error connecting wallet:', error);
return false;
}
};
const disconnectWallet = (): void => {
disconnect();
};
const getVerificationStatus = (user: User): VerificationStatus => {
if (user.walletType === 'bitcoin') {
return user.ordinalOwnership ? 'verified-owner' : 'verified-none';
@ -191,10 +210,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}
};
const delegateKey = async (): Promise<boolean> => {
if (!currentUser || !currentUser.address) {
const delegateKey = async (duration: DelegationDuration = '7days'): Promise<boolean> => {
if (!currentUser) {
toast({
title: "Not Connected",
title: "No User Found",
description: "Please connect your wallet first.",
variant: "destructive",
});
@ -204,12 +223,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setIsAuthenticating(true);
try {
const durationText = duration === '7days' ? '1 week' : '30 days';
toast({
title: "Starting Key Delegation",
description: "This will let you post, comment, and vote without approving each action for 24 hours.",
description: `This will let you post, comment, and vote without approving each action for ${durationText}.`,
});
const result: AuthResult = await authServiceRef.current.delegateKey(currentUser);
const result: AuthResult = await authServiceRef.current.delegateKey(currentUser, duration);
if (!result.success) {
throw new Error(result.error);
@ -292,16 +312,18 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const value: AuthContextType = {
currentUser,
isAuthenticated: Boolean(currentUser && isConnected),
isAuthenticating,
isAuthenticated: Boolean(currentUser && isConnected),
verificationStatus,
connectWallet,
disconnectWallet,
verifyOwnership,
delegateKey,
clearDelegation,
isDelegationValid,
delegationTimeRemaining,
isWalletAvailable,
messageSigning
clearDelegation,
signMessage: messageSigning.signMessage,
verifyMessage: messageSigning.verifyMessage
};
return (

View File

@ -4,7 +4,7 @@ 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 { KeyDelegation, DelegationDuration } from '../signatures/key-delegation';
import { OpchanMessage } from '@/types';
export interface AuthResult {
@ -234,7 +234,7 @@ export class AuthService {
/**
* Set up key delegation for the user
*/
async delegateKey(user: User): Promise<AuthResult> {
async delegateKey(user: User, duration: DelegationDuration = '7days'): Promise<AuthResult> {
try {
const walletType = user.walletType;
const isAvailable = this.walletService.isWalletAvailable(walletType);
@ -246,7 +246,7 @@ export class AuthService {
};
}
const success = await this.walletService.createKeyDelegation(walletType);
const success = await this.walletService.createKeyDelegation(walletType, duration);
if (!success) {
return {

View File

@ -13,11 +13,32 @@ import { DelegationInfo } from './types';
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
export type DelegationDuration = '7days' | '30days';
export class KeyDelegation {
private static readonly DEFAULT_EXPIRY_HOURS = 24;
private static readonly STORAGE_KEY = LOCAL_STORAGE_KEYS.KEY_DELEGATION;
// Duration options in hours
private static readonly DURATION_HOURS = {
'7days': 24 * 7, // 168 hours
'30days': 24 * 30 // 720 hours
} as const;
/**
* Get the number of hours for a given duration
*/
static getDurationHours(duration: DelegationDuration): number {
return KeyDelegation.DURATION_HOURS[duration];
}
/**
* Get available duration options
*/
static getAvailableDurations(): DelegationDuration[] {
return Object.keys(KeyDelegation.DURATION_HOURS) as DelegationDuration[];
}
/**
* Generates a new browser-based keypair for signing messages
* @returns Promise with keypair object containing hex-encoded public and private keys
@ -56,7 +77,7 @@ export class KeyDelegation {
* @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 duration The duration of the delegation ('1week' or '30days')
* @param walletType The type of wallet (bitcoin or ethereum)
* @returns DelegationInfo object
*/
@ -65,9 +86,10 @@ export class KeyDelegation {
signature: string,
browserPublicKey: string,
browserPrivateKey: string,
expiryHours: number = KeyDelegation.DEFAULT_EXPIRY_HOURS,
duration: DelegationDuration = '7days',
walletType: 'bitcoin' | 'ethereum'
): DelegationInfo {
const expiryHours = KeyDelegation.getDurationHours(duration);
const expiryTimestamp = Date.now() + (expiryHours * 60 * 60 * 1000);
return {

View File

@ -1,5 +1,5 @@
import { UseAppKitAccountReturn } from '@reown/appkit/react';
import { KeyDelegation } from '../signatures/key-delegation';
import { KeyDelegation, DelegationDuration } from '../signatures/key-delegation';
import { AppKit } from '@reown/appkit';
import { getEnsName } from '@wagmi/core';
import { ChainNamespace } from '@reown/appkit-common';
@ -122,7 +122,7 @@ export class ReOwnWalletService {
/**
* Create a key delegation for the connected wallet
*/
async createKeyDelegation(walletType: 'bitcoin' | 'ethereum'): Promise<boolean> {
async createKeyDelegation(walletType: 'bitcoin' | 'ethereum', duration: DelegationDuration = '7days'): Promise<boolean> {
try {
const account = this.getActiveAccount(walletType);
if (!account?.address) {
@ -133,7 +133,8 @@ export class ReOwnWalletService {
const keypair = this.keyDelegation.generateKeypair();
// Create delegation message with expiry
const expiryTimestamp = Date.now() + (24 * 60 * 60 * 1000); // 24 hours
const expiryHours = KeyDelegation.getDurationHours(duration);
const expiryTimestamp = Date.now() + (expiryHours * 60 * 60 * 1000);
const delegationMessage = this.keyDelegation.createDelegationMessage(
keypair.publicKey,
account.address,
@ -151,7 +152,7 @@ export class ReOwnWalletService {
signature,
keypair.publicKey,
keypair.privateKey,
24, // 24 hours
duration,
walletType
);