From 6f7cbb4b45f9fe235b8e73e53b09b090bae12292 Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Wed, 13 Aug 2025 12:00:40 +0530 Subject: [PATCH] chore: key delegation for 7 days / 30 days --- .env.example | 1 + README.md | 2 +- src/components/ui/delegation-step.tsx | 201 +++++++++--------- src/components/ui/wallet-wizard.tsx | 8 +- src/contexts/AuthContext.tsx | 56 +++-- src/lib/identity/services/AuthService.ts | 6 +- src/lib/identity/signatures/key-delegation.ts | 26 ++- .../identity/wallets/ReOwnWalletService.ts | 9 +- 8 files changed, 185 insertions(+), 124 deletions(-) diff --git a/.env.example b/.env.example index c7deb4a..e74110b 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ +VITE_REOWN_SECRETVITE_REOWN_SECRET # Mock/bypass settings for development VITE_OPCHAN_MOCK_ORDINAL_CHECK=false \ No newline at end of file diff --git a/README.md b/README.md index 3389d85..130ac02 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/components/ui/delegation-step.tsx b/src/components/ui/delegation-step.tsx index 1e6673d..675f86e 100644 --- a/src/components/ui/delegation-step.tsx +++ b/src/components/ui/delegation-step.tsx @@ -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('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({
- +
+ + + +
-

- Delegate Signing Key -

+

Key Delegation

- Create a browser-based signing key + Delegate signing authority to your browser for convenient forum interactions

- - {/* Delegation Status */} -
-
-
- - Key Delegation -
- {currentUser?.walletType === 'bitcoin' ? ( -
+ +
+ {/* Status */} +
+ {isDelegationValid() ? ( + ) : ( -
Ξ
+ + )} + + {isDelegationValid() ? 'Delegated' : 'Required'} + + {isDelegationValid() && ( + + {Math.floor(delegationTimeRemaining() / (1000 * 60 * 60))}h {Math.floor((delegationTimeRemaining() % (1000 * 60 * 60)) / (1000 * 60))}m remaining + )}
-
- {/* Status */} -
- {isDelegationValid() ? ( - - ) : ( - - )} - - {isDelegationValid() ? 'Delegated' : 'Required'} - - {isDelegationValid() && ( - - {Math.floor(delegationTimeRemaining() / (1000 * 60 * 60))}h {Math.floor((delegationTimeRemaining() % (1000 * 60 * 60)) / (1000 * 60))}m remaining - - )} + {/* Duration Selection */} + {!isDelegationValid() && ( +
+ +
+ + +
- - {/* Delegated Browser Public Key */} - {isDelegationValid() && currentUser?.browserPubKey && ( -
-
- {currentUser.browserPubKey} -
+ )} + + {/* Delegated Browser Public Key */} + {isDelegationValid() && currentUser?.browserPubKey && ( +
+
+ {currentUser.browserPubKey}
- )} - - {/* Wallet Address */} - {currentUser && ( -
-
- {currentUser.address} -
+
+ )} + + {/* Wallet Address */} + {currentUser && ( +
+
+ {currentUser.address}
- )} - - {/* Delete Button for Active Delegations */} - {isDelegationValid() && ( -
- -
- )} -
+
+ )} + + {/* Delete Button for Active Delegations */} + {isDelegationValid() && ( +
+ +
+ )}
- + {/* Action Buttons */} -
- {!isDelegationValid() && ( - - )} - - {isDelegationValid() && ( +
+ {isDelegationValid() ? ( + ) : ( + )} diff --git a/src/components/ui/wallet-wizard.tsx b/src/components/ui/wallet-wizard.tsx index ead8cb9..a601096 100644 --- a/src/components/ui/wallet-wizard.tsx +++ b/src/components/ui/wallet-wizard.tsx @@ -29,10 +29,11 @@ export function WalletWizard({ const [currentStep, setCurrentStep] = React.useState(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) { diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 91fa631..045fe5e 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -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; + disconnectWallet: () => void; verifyOwnership: () => Promise; - delegateKey: () => Promise; - clearDelegation: () => void; + delegateKey: (duration?: DelegationDuration) => Promise; isDelegationValid: () => boolean; delegationTimeRemaining: () => number; - isWalletAvailable: () => boolean; - messageSigning: { - signMessage: (message: OpchanMessage) => Promise; - verifyMessage: (message: OpchanMessage) => boolean; - }; + clearDelegation: () => void; + signMessage: (message: OpchanMessage) => Promise; + verifyMessage: (message: OpchanMessage) => boolean; } const AuthContext = createContext(undefined); @@ -105,6 +105,25 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { } }, [isConnected, address, isBitcoinConnected, isEthereumConnected, toast]); + const { disconnect } = useDisconnect(); + + const connectWallet = async (): Promise => { + 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 => { - if (!currentUser || !currentUser.address) { + const delegateKey = async (duration: DelegationDuration = '7days'): Promise => { + 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 ( diff --git a/src/lib/identity/services/AuthService.ts b/src/lib/identity/services/AuthService.ts index c417efb..71bbf42 100644 --- a/src/lib/identity/services/AuthService.ts +++ b/src/lib/identity/services/AuthService.ts @@ -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 { + async delegateKey(user: User, duration: DelegationDuration = '7days'): Promise { 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 { diff --git a/src/lib/identity/signatures/key-delegation.ts b/src/lib/identity/signatures/key-delegation.ts index 678bd10..0ec916c 100644 --- a/src/lib/identity/signatures/key-delegation.ts +++ b/src/lib/identity/signatures/key-delegation.ts @@ -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 { diff --git a/src/lib/identity/wallets/ReOwnWalletService.ts b/src/lib/identity/wallets/ReOwnWalletService.ts index 6b5e4bd..98c7506 100644 --- a/src/lib/identity/wallets/ReOwnWalletService.ts +++ b/src/lib/identity/wallets/ReOwnWalletService.ts @@ -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 { + async createKeyDelegation(walletType: 'bitcoin' | 'ethereum', duration: DelegationDuration = '7days'): Promise { 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 );