mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-02 21:03:09 +00:00
chore: key delegation for 7 days / 30 days
This commit is contained in:
parent
3158b60aed
commit
6f7cbb4b45
@ -1,2 +1,3 @@
|
||||
VITE_REOWN_SECRETVITE_REOWN_SECRET
|
||||
# Mock/bypass settings for development
|
||||
VITE_OPCHAN_MOCK_ORDINAL_CHECK=false
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user