chore: simplify DelegationManager

This commit is contained in:
Danish Arora 2025-09-02 11:06:18 +05:30
parent 4232878d39
commit 5e0622183e
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
3 changed files with 162 additions and 182 deletions

View File

@ -3,7 +3,7 @@ import { useToast } from '@/components/ui/use-toast';
import { OpchanMessage } from '@/types/forum'; import { OpchanMessage } from '@/types/forum';
import { User, EVerificationStatus, DisplayPreference } from '@/types/identity'; import { User, EVerificationStatus, DisplayPreference } from '@/types/identity';
import { WalletManager } from '@/lib/wallet'; import { WalletManager } from '@/lib/wallet';
import { DelegationManager, DelegationDuration } from '@/lib/delegation'; import { DelegationManager, DelegationDuration, DelegationFullStatus } from '@/lib/delegation';
import { useAppKitAccount, useDisconnect, modal } from '@reown/appkit/react'; import { useAppKitAccount, useDisconnect, modal } from '@reown/appkit/react';
export type VerificationStatus = export type VerificationStatus =
@ -22,8 +22,7 @@ interface AuthContextType {
disconnectWallet: () => void; disconnectWallet: () => void;
verifyOwnership: () => Promise<boolean>; verifyOwnership: () => Promise<boolean>;
delegateKey: (duration?: DelegationDuration) => Promise<boolean>; delegateKey: (duration?: DelegationDuration) => Promise<boolean>;
isDelegationValid: () => boolean; getDelegationStatus: () => DelegationFullStatus;
delegationTimeRemaining: () => number;
clearDelegation: () => void; clearDelegation: () => void;
signMessage: (message: OpchanMessage) => Promise<OpchanMessage | null>; signMessage: (message: OpchanMessage) => Promise<OpchanMessage | null>;
verifyMessage: (message: OpchanMessage) => boolean; verifyMessage: (message: OpchanMessage) => boolean;
@ -166,32 +165,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
); );
} }
// Generate new keypair // Use the simplified delegation method
const keypair = delegationManager.generateKeypair(); return await delegationManager.delegate(
// Create delegation message with expiry
const expiryHours = DelegationManager.getDurationHours(duration);
const expiryTimestamp = Date.now() + expiryHours * 60 * 60 * 1000;
const delegationMessage = delegationManager.createDelegationMessage(
keypair.publicKey,
user.address, user.address,
expiryTimestamp user.walletType,
);
// Sign the delegation message with wallet
const signature = await walletManager.signMessage(delegationMessage);
// Create and store the delegation
delegationManager.createDelegation(
user.address,
signature,
keypair.publicKey,
keypair.privateKey,
duration, duration,
user.walletType (message) => walletManager.signMessage(message)
); );
return true;
} catch (error) { } catch (error) {
console.error( console.error(
`Error creating key delegation for ${user.walletType}:`, `Error creating key delegation for ${user.walletType}:`,
@ -419,15 +399,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
} }
// Update user with delegation info // Update user with delegation info
const browserPublicKey = delegationManager.getBrowserPublicKey(); const delegationStatus = delegationManager.getStatus(
const delegationStatus = delegationManager.getDelegationStatus(
currentUser.address, currentUser.address,
currentUser.walletType currentUser.walletType
); );
const updatedUser = { const updatedUser = {
...currentUser, ...currentUser,
browserPubKey: browserPublicKey || undefined, browserPubKey: delegationStatus.publicKey || undefined,
delegationSignature: delegationStatus.isValid ? 'valid' : undefined, delegationSignature: delegationStatus.isValid ? 'valid' : undefined,
delegationExpiry: delegationStatus.timeRemaining delegationExpiry: delegationStatus.timeRemaining
? Date.now() + delegationStatus.timeRemaining ? Date.now() + delegationStatus.timeRemaining
@ -467,16 +446,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
} }
}; };
const isDelegationValid = (): boolean => { const getDelegationStatus = (): DelegationFullStatus => {
return delegationManager.isDelegationValid(); return delegationManager.getStatus(currentUser?.address, currentUser?.walletType);
};
const delegationTimeRemaining = (): number => {
return delegationManager.getDelegationTimeRemaining();
}; };
const clearDelegation = (): void => { const clearDelegation = (): void => {
delegationManager.clearDelegation(); delegationManager.clear();
// Update the current user to remove delegation info // Update the current user to remove delegation info
if (currentUser) { if (currentUser) {
@ -500,10 +475,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
signMessage: async ( signMessage: async (
message: OpchanMessage message: OpchanMessage
): Promise<OpchanMessage | null> => { ): Promise<OpchanMessage | null> => {
return delegationManager.signMessageWithDelegatedKey(message); return delegationManager.signMessage(message);
}, },
verifyMessage: (message: OpchanMessage): boolean => { verifyMessage: (message: OpchanMessage): boolean => {
return delegationManager.verifyMessage(message); return delegationManager.verify(message);
}, },
}; };
@ -516,8 +491,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
disconnectWallet, disconnectWallet,
verifyOwnership, verifyOwnership,
delegateKey, delegateKey,
isDelegationValid, getDelegationStatus,
delegationTimeRemaining,
clearDelegation, clearDelegation,
signMessage: messageSigning.signMessage, signMessage: messageSigning.signMessage,
verifyMessage: messageSigning.verifyMessage, verifyMessage: messageSigning.verifyMessage,

View File

@ -11,8 +11,7 @@ export const useDelegation = () => {
const { const {
delegateKey: contextDelegateKey, delegateKey: contextDelegateKey,
isDelegationValid: contextIsDelegationValid, getDelegationStatus: contextGetDelegationStatus,
delegationTimeRemaining: contextDelegationTimeRemaining,
clearDelegation: contextClearDelegation, clearDelegation: contextClearDelegation,
isAuthenticating, isAuthenticating,
} = context; } = context;
@ -29,17 +28,19 @@ export const useDelegation = () => {
}, [contextClearDelegation]); }, [contextClearDelegation]);
const delegationStatus = useMemo(() => { const delegationStatus = useMemo(() => {
const isValid = contextIsDelegationValid(); const status = contextGetDelegationStatus();
const timeRemaining = contextDelegationTimeRemaining();
return { return {
hasDelegation: timeRemaining > 0, hasDelegation: status.hasDelegation,
isValid, isValid: status.isValid,
timeRemaining: timeRemaining > 0 ? timeRemaining : undefined, timeRemaining: status.timeRemaining,
expiresAt: expiresAt:
timeRemaining > 0 ? new Date(Date.now() + timeRemaining) : undefined, status.timeRemaining ? new Date(Date.now() + status.timeRemaining) : undefined,
publicKey: status.publicKey,
address: status.address,
walletType: status.walletType,
}; };
}, [contextIsDelegationValid, contextDelegationTimeRemaining]); }, [contextGetDelegationStatus]);
const formatTimeRemaining = useCallback((timeMs: number): string => { const formatTimeRemaining = useCallback((timeMs: number): string => {
const hours = Math.floor(timeMs / (1000 * 60 * 60)); const hours = Math.floor(timeMs / (1000 * 60 * 60));

View File

@ -9,6 +9,13 @@ import { DelegationStorage } from './storage';
// Set up ed25519 with sha512 // Set up ed25519 with sha512
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m)); ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
// Enhanced status interface that consolidates all delegation information
export interface DelegationFullStatus extends DelegationStatus {
publicKey?: string;
address?: string;
walletType?: 'bitcoin' | 'ethereum';
}
export class DelegationManager { export class DelegationManager {
// Duration options in hours // Duration options in hours
private static readonly DURATION_HOURS = { private static readonly DURATION_HOURS = {
@ -24,166 +31,112 @@ export class DelegationManager {
} }
// ============================================================================ // ============================================================================
// KEYPAIR GENERATION // PUBLIC API
// ============================================================================ // ============================================================================
/** /**
* Generate a new browser-based keypair for signing messages * Create a complete delegation with a single method call
* @param address - Wallet address to delegate from
* @param walletType - Type of wallet (bitcoin/ethereum)
* @param duration - How long the delegation should last
* @param signFunction - Function to sign the delegation message with the wallet
* @returns Promise<boolean> - Success status
*/ */
generateKeypair(): { publicKey: string; privateKey: string } { async delegate(
const privateKey = ed.utils.randomPrivateKey(); address: string,
const privateKeyHex = bytesToHex(privateKey); walletType: 'bitcoin' | 'ethereum',
const publicKey = ed.getPublicKey(privateKey);
const publicKeyHex = bytesToHex(publicKey);
return {
privateKey: privateKeyHex,
publicKey: publicKeyHex,
};
}
/**
* Create a delegation message to be signed by the wallet
*/
createDelegationMessage(
browserPublicKey: string,
walletAddress: string,
expiryTimestamp: number
): string {
return `I, ${walletAddress}, delegate authority to this pubkey: ${browserPublicKey} until ${expiryTimestamp}`;
}
// ============================================================================
// DELEGATION LIFECYCLE
// ============================================================================
/**
* Create and store a delegation
*/
createDelegation(
walletAddress: string,
signature: string,
browserPublicKey: string,
browserPrivateKey: string,
duration: DelegationDuration = '7days', duration: DelegationDuration = '7days',
walletType: 'bitcoin' | 'ethereum' signFunction: (message: string) => Promise<string>
): void { ): Promise<boolean> {
const expiryHours = DelegationManager.getDurationHours(duration); try {
const expiryTimestamp = Date.now() + expiryHours * 60 * 60 * 1000; // Generate new keypair
const keypair = this.generateKeypair();
// Create delegation message with expiry
const expiryHours = DelegationManager.getDurationHours(duration);
const expiryTimestamp = Date.now() + expiryHours * 60 * 60 * 1000;
const delegationMessage = this.createDelegationMessage(
keypair.publicKey,
address,
expiryTimestamp
);
const delegationInfo: DelegationInfo = { // Sign the delegation message with wallet
signature, const signature = await signFunction(delegationMessage);
expiryTimestamp,
browserPublicKey,
browserPrivateKey,
walletAddress,
walletType,
};
DelegationStorage.store(delegationInfo); // Create and store the delegation
const delegationInfo: DelegationInfo = {
signature,
expiryTimestamp,
browserPublicKey: keypair.publicKey,
browserPrivateKey: keypair.privateKey,
walletAddress: address,
walletType,
};
DelegationStorage.store(delegationInfo);
return true;
} catch (error) {
console.error('Error creating delegation:', error);
return false;
}
} }
/** /**
* Check if a delegation is valid * Get comprehensive delegation status
* @param currentAddress - Optional address to validate against
* @param currentWalletType - Optional wallet type to validate against
* @returns Complete delegation status information
*/ */
isDelegationValid( getStatus(
currentAddress?: string, currentAddress?: string,
currentWalletType?: 'bitcoin' | 'ethereum' currentWalletType?: 'bitcoin' | 'ethereum'
): boolean { ): DelegationFullStatus {
const delegation = DelegationStorage.retrieve(); const delegation = DelegationStorage.retrieve();
if (!delegation) return false;
if (!delegation) {
return {
hasDelegation: false,
isValid: false,
};
}
// Check if delegation has expired // Check if delegation has expired
const now = Date.now(); const now = Date.now();
if (now >= delegation.expiryTimestamp) return false; const hasExpired = now >= delegation.expiryTimestamp;
// If a current address is provided, validate it matches the delegation // Check address/wallet type matching if provided
if (currentAddress && delegation.walletAddress !== currentAddress) { const addressMatches = !currentAddress || delegation.walletAddress === currentAddress;
return false; const walletTypeMatches = !currentWalletType || delegation.walletType === currentWalletType;
}
const isValid = !hasExpired && addressMatches && walletTypeMatches;
// If a current wallet type is provided, validate it matches the delegation const timeRemaining = Math.max(0, delegation.expiryTimestamp - now);
if (currentWalletType && delegation.walletType !== currentWalletType) {
return false;
}
return true;
}
/**
* Get the time remaining on the current delegation in milliseconds
*/
getDelegationTimeRemaining(): number {
const delegation = DelegationStorage.retrieve();
if (!delegation) return 0;
const now = Date.now();
return Math.max(0, delegation.expiryTimestamp - now);
}
/**
* Get the browser public key from the current delegation
*/
getBrowserPublicKey(): string | null {
const delegation = DelegationStorage.retrieve();
if (!delegation) return null;
return delegation.browserPublicKey;
}
/**
* Get delegation status
*/
getDelegationStatus(
currentAddress?: string,
currentWalletType?: 'bitcoin' | 'ethereum'
): DelegationStatus {
const hasDelegation = this.getBrowserPublicKey() !== null;
const isValid = this.isDelegationValid(currentAddress, currentWalletType);
const timeRemaining = this.getDelegationTimeRemaining();
return { return {
hasDelegation, hasDelegation: true,
isValid, isValid,
timeRemaining: timeRemaining > 0 ? timeRemaining : undefined, timeRemaining: timeRemaining > 0 ? timeRemaining : undefined,
publicKey: delegation.browserPublicKey,
address: delegation.walletAddress,
walletType: delegation.walletType,
}; };
} }
/** /**
* Clear the stored delegation * Clear the stored delegation
*/ */
clearDelegation(): void { clear(): void {
DelegationStorage.clear(); DelegationStorage.clear();
} }
// ============================================================================
// MESSAGE SIGNING & VERIFICATION
// ============================================================================
/** /**
* Sign a raw string message using the browser-generated private key * Sign a message with the delegated browser key
* @param message - Unsigned message to sign
* @returns Signed message or null if delegation invalid
*/ */
signRawMessage(message: string): string | null { signMessage(message: UnsignedMessage): OpchanMessage | null {
const delegation = DelegationStorage.retrieve(); const status = this.getStatus();
if (!delegation || !this.isDelegationValid()) return null; if (!status.isValid) {
try {
const privateKeyBytes = hexToBytes(delegation.browserPrivateKey);
const messageBytes = new TextEncoder().encode(message);
const signature = ed.sign(messageBytes, privateKeyBytes);
return bytesToHex(signature);
} catch (error) {
console.error('Error signing with browser key:', error);
return null;
}
}
/**
* Sign an unsigned message with the delegated browser key
*/
signMessageWithDelegatedKey(message: UnsignedMessage): OpchanMessage | null {
if (!this.isDelegationValid()) {
console.error('No valid key delegation found. Cannot sign message.'); console.error('No valid key delegation found. Cannot sign message.');
return null; return null;
} }
@ -198,7 +151,7 @@ export class DelegationManager {
browserPubKey: undefined, browserPubKey: undefined,
}); });
const signature = this.signRawMessage(messageToSign); const signature = this.signRaw(messageToSign);
if (!signature) return null; if (!signature) return null;
return { return {
@ -209,9 +162,11 @@ export class DelegationManager {
} }
/** /**
* Verify an OpchanMessage signature * Verify a message signature
* @param message - Signed message to verify
* @returns True if signature is valid
*/ */
verifyMessage(message: OpchanMessage): boolean { verify(message: OpchanMessage): boolean {
// Check for required signature fields // Check for required signature fields
if (!message.signature || !message.browserPubKey) { if (!message.signature || !message.browserPubKey) {
const messageId = message.id || `${message.type}-${message.timestamp}`; const messageId = message.id || `${message.type}-${message.timestamp}`;
@ -227,7 +182,7 @@ export class DelegationManager {
}); });
// Verify the signature // Verify the signature
const isValid = this.verifyRawSignature( const isValid = this.verifyRaw(
signedContent, signedContent,
message.signature, message.signature,
message.browserPubKey message.browserPubKey
@ -241,10 +196,60 @@ export class DelegationManager {
return isValid; return isValid;
} }
// ============================================================================
// PRIVATE HELPERS
// ============================================================================
/**
* Generate a new browser-based keypair for signing messages
*/
private generateKeypair(): { publicKey: string; privateKey: string } {
const privateKey = ed.utils.randomPrivateKey();
const privateKeyHex = bytesToHex(privateKey);
const publicKey = ed.getPublicKey(privateKey);
const publicKeyHex = bytesToHex(publicKey);
return {
privateKey: privateKeyHex,
publicKey: publicKeyHex,
};
}
/**
* Create a delegation message to be signed by the wallet
*/
private createDelegationMessage(
browserPublicKey: string,
walletAddress: string,
expiryTimestamp: number
): string {
return `I, ${walletAddress}, delegate authority to this pubkey: ${browserPublicKey} until ${expiryTimestamp}`;
}
/**
* Sign a raw string message using the browser-generated private key
*/
private signRaw(message: string): string | null {
const delegation = DelegationStorage.retrieve();
if (!delegation) return null;
try {
const privateKeyBytes = hexToBytes(delegation.browserPrivateKey);
const messageBytes = new TextEncoder().encode(message);
const signature = ed.sign(messageBytes, privateKeyBytes);
return bytesToHex(signature);
} catch (error) {
console.error('Error signing with browser key:', error);
return null;
}
}
/** /**
* Verify a signature made with the browser key * Verify a signature made with the browser key
*/ */
private verifyRawSignature( private verifyRaw(
message: string, message: string,
signature: string, signature: string,
publicKey: string publicKey: string