chore: clean up services

This commit is contained in:
Danish Arora 2025-08-28 18:44:35 +05:30
parent 62d6800f0b
commit 9b6a1c48d7
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
12 changed files with 279 additions and 297 deletions

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Button } from './button'; import { Button } from './button';
import { useAuth } from '@/contexts/useAuth'; import { useAuth } from '@/contexts/useAuth';
import { CheckCircle, AlertCircle, Trash2 } from 'lucide-react'; import { CheckCircle, AlertCircle, Trash2 } from 'lucide-react';
import { DelegationDuration } from '@/lib/identity/signatures/key-delegation'; import { DelegationDuration } from '@/lib/identity/services/CryptoService';
interface DelegationStepProps { interface DelegationStepProps {
onComplete: () => void; onComplete: () => void;

View File

@ -1,10 +1,9 @@
import React, { createContext, useContext, useState, useEffect, useRef } from 'react'; import React, { createContext, useContext, useState, useEffect, useRef } from 'react';
import { useToast } from '@/components/ui/use-toast'; import { useToast } from '@/components/ui/use-toast';
import { User } from '@/types'; import { User, OpchanMessage, EVerificationStatus } from '@/types/forum';
import { AuthService, AuthResult } from '@/lib/identity/services/AuthService'; import { AuthService, CryptoService, MessageService, DelegationDuration } from '@/lib/identity/services';
import { OpchanMessage } from '@/types'; import { AuthResult } from '@/lib/identity/services/AuthService';
import { useAppKitAccount, useDisconnect, modal } from '@reown/appkit/react'; import { useAppKitAccount, useDisconnect, modal } from '@reown/appkit/react';
import { DelegationDuration } from '@/lib/identity/signatures/key-delegation';
export type VerificationStatus = 'unverified' | 'verified-none' | 'verified-basic' | 'verified-owner' | 'verifying'; export type VerificationStatus = 'unverified' | 'verified-none' | 'verified-basic' | 'verified-owner' | 'verifying';
@ -47,8 +46,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const activeAccount = isBitcoinConnected ? bitcoinAccount : ethereumAccount; const activeAccount = isBitcoinConnected ? bitcoinAccount : ethereumAccount;
const address = activeAccount.address; const address = activeAccount.address;
// Create ref for AuthService so it persists between renders // Create service instances that persist between renders
const authServiceRef = useRef(new AuthService()); const cryptoServiceRef = useRef(new CryptoService());
const authServiceRef = useRef(new AuthService(cryptoServiceRef.current));
const messageServiceRef = useRef(new MessageService(authServiceRef.current, cryptoServiceRef.current));
// Set AppKit instance and accounts in AuthService // Set AppKit instance and accounts in AuthService
useEffect(() => { useEffect(() => {
@ -76,7 +77,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const newUser: User = { const newUser: User = {
address, address,
walletType: isBitcoinConnected ? 'bitcoin' : 'ethereum', walletType: isBitcoinConnected ? 'bitcoin' : 'ethereum',
verificationStatus: 'verified-basic', // Connected wallets get basic verification by default verificationStatus: EVerificationStatus.VERIFIED_BASIC, // Connected wallets get basic verification by default
lastChecked: Date.now(), lastChecked: Date.now(),
}; };
@ -88,7 +89,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
...newUser, ...newUser,
ensOwnership: true, ensOwnership: true,
ensName: walletInfo.ensName, ensName: walletInfo.ensName,
verificationStatus: 'verified-owner' as const, verificationStatus: EVerificationStatus.VERIFIED_OWNER,
}; };
setCurrentUser(updatedUser); setCurrentUser(updatedUser);
setVerificationStatus('verified-owner'); setVerificationStatus('verified-owner');
@ -296,15 +297,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}; };
const isDelegationValid = (): boolean => { const isDelegationValid = (): boolean => {
return authServiceRef.current.isDelegationValid(); return cryptoServiceRef.current.isDelegationValid();
}; };
const delegationTimeRemaining = (): number => { const delegationTimeRemaining = (): number => {
return authServiceRef.current.getDelegationTimeRemaining(); return cryptoServiceRef.current.getDelegationTimeRemaining();
}; };
const clearDelegation = (): void => { const clearDelegation = (): void => {
authServiceRef.current.clearDelegation(); cryptoServiceRef.current.clearDelegation();
// Update the current user to remove delegation info // Update the current user to remove delegation info
if (currentUser) { if (currentUser) {
@ -329,10 +330,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const messageSigning = { const messageSigning = {
signMessage: async (message: OpchanMessage): Promise<OpchanMessage | null> => { signMessage: async (message: OpchanMessage): Promise<OpchanMessage | null> => {
return authServiceRef.current.signMessage(message); return cryptoServiceRef.current.signMessage(message);
}, },
verifyMessage: (message: OpchanMessage): boolean => { verifyMessage: (message: OpchanMessage): boolean => {
return authServiceRef.current.verifyMessage(message); return cryptoServiceRef.current.verifyMessage(message);
} }
}; };

View File

@ -1,5 +1,5 @@
import { RelevanceCalculator } from '../relevance'; import { RelevanceCalculator } from '../relevance';
import { Post, Comment, User, UserVerificationStatus } from '@/types/forum'; import { Post, Comment, User, UserVerificationStatus, EVerificationStatus } from '@/types/forum';
import { VoteMessage, MessageType } from '@/lib/waku/types'; import { VoteMessage, MessageType } from '@/lib/waku/types';
import { expect, describe, beforeEach, it } from 'vitest'; import { expect, describe, beforeEach, it } from 'vitest';
@ -58,7 +58,7 @@ describe('RelevanceCalculator', () => {
const verifiedUser: User = { const verifiedUser: User = {
address: 'user1', address: 'user1',
walletType: 'ethereum', walletType: 'ethereum',
verificationStatus: 'verified-owner', verificationStatus: EVerificationStatus.VERIFIED_OWNER,
ensOwnership: true, ensOwnership: true,
ensName: 'test.eth', ensName: 'test.eth',
lastChecked: Date.now() lastChecked: Date.now()
@ -72,7 +72,7 @@ describe('RelevanceCalculator', () => {
const verifiedUser: User = { const verifiedUser: User = {
address: 'user3', address: 'user3',
walletType: 'bitcoin', walletType: 'bitcoin',
verificationStatus: 'verified-owner', verificationStatus: EVerificationStatus.VERIFIED_OWNER,
ordinalOwnership: true, ordinalOwnership: true,
lastChecked: Date.now() lastChecked: Date.now()
}; };
@ -85,7 +85,7 @@ describe('RelevanceCalculator', () => {
const unverifiedUser: User = { const unverifiedUser: User = {
address: 'user2', address: 'user2',
walletType: 'ethereum', walletType: 'ethereum',
verificationStatus: 'unverified', verificationStatus: EVerificationStatus.UNVERIFIED,
ensOwnership: false, ensOwnership: false,
lastChecked: Date.now() lastChecked: Date.now()
}; };
@ -184,7 +184,7 @@ describe('RelevanceCalculator', () => {
{ {
address: 'user1', address: 'user1',
walletType: 'ethereum', walletType: 'ethereum',
verificationStatus: 'verified-owner', verificationStatus: EVerificationStatus.VERIFIED_OWNER,
ensOwnership: true, ensOwnership: true,
ensName: 'test.eth', ensName: 'test.eth',
lastChecked: Date.now() lastChecked: Date.now()
@ -192,7 +192,7 @@ describe('RelevanceCalculator', () => {
{ {
address: 'user2', address: 'user2',
walletType: 'bitcoin', walletType: 'bitcoin',
verificationStatus: 'unverified', verificationStatus: EVerificationStatus.UNVERIFIED,
ordinalOwnership: false, ordinalOwnership: false,
lastChecked: Date.now() lastChecked: Date.now()
} }

View File

@ -9,8 +9,7 @@ import {
} from '@/lib/waku/types'; } from '@/lib/waku/types';
import { Cell, Comment, Post, User } from '@/types/forum'; import { Cell, Comment, Post, User } from '@/types/forum';
import { transformCell, transformComment, transformPost } from './transformers'; import { transformCell, transformComment, transformPost } from './transformers';
import { MessageService } from '@/lib/identity/services/MessageService'; import { MessageService, AuthService, CryptoService } from '@/lib/identity/services';
import { AuthService } from '@/lib/identity/services/AuthService';
type ToastFunction = (props: { type ToastFunction = (props: {
title: string; title: string;
@ -70,8 +69,9 @@ export const createPost = async (
author: currentUser.address, author: currentUser.address,
}; };
const messageService = new MessageService(authService!); const cryptoService = new CryptoService();
const result = await messageService.signAndSendMessage(postMessage); const messageService = new MessageService(authService!, cryptoService);
const result = await messageService.sendMessage(postMessage);
if (!result.success) { if (!result.success) {
toast({ toast({
title: 'Post Failed', title: 'Post Failed',
@ -141,8 +141,9 @@ export const createComment = async (
author: currentUser.address, author: currentUser.address,
}; };
const messageService = new MessageService(authService!); const cryptoService = new CryptoService();
const result = await messageService.signAndSendMessage(commentMessage); const messageService = new MessageService(authService!, cryptoService);
const result = await messageService.sendMessage(commentMessage);
if (!result.success) { if (!result.success) {
toast({ toast({
title: 'Comment Failed', title: 'Comment Failed',
@ -199,8 +200,9 @@ export const createCell = async (
author: currentUser.address, author: currentUser.address,
}; };
const messageService = new MessageService(authService!); const cryptoService = new CryptoService();
const result = await messageService.signAndSendMessage(cellMessage); const messageService = new MessageService(authService!, cryptoService);
const result = await messageService.sendMessage(cellMessage);
if (!result.success) { if (!result.success) {
toast({ toast({
title: 'Cell Failed', title: 'Cell Failed',
@ -275,8 +277,9 @@ export const vote = async (
author: currentUser.address, author: currentUser.address,
}; };
const messageService = new MessageService(authService!); const cryptoService = new CryptoService();
const result = await messageService.signAndSendMessage(voteMessage); const messageService = new MessageService(authService!, cryptoService);
const result = await messageService.sendMessage(voteMessage);
if (!result.success) { if (!result.success) {
toast({ toast({
title: 'Vote Failed', title: 'Vote Failed',
@ -340,8 +343,9 @@ export const moderatePost = async (
timestamp: Date.now(), timestamp: Date.now(),
author: currentUser.address, author: currentUser.address,
}; };
const messageService = new MessageService(authService!); const cryptoService = new CryptoService();
const result = await messageService.signAndSendMessage(modMsg); const messageService = new MessageService(authService!, cryptoService);
const result = await messageService.sendMessage(modMsg);
if (!result.success) { if (!result.success) {
toast({ title: 'Moderation Failed', description: result.error || 'Failed to moderate post. Please try again.', variant: 'destructive' }); toast({ title: 'Moderation Failed', description: result.error || 'Failed to moderate post. Please try again.', variant: 'destructive' });
return false; return false;
@ -389,8 +393,9 @@ export const moderateComment = async (
timestamp: Date.now(), timestamp: Date.now(),
author: currentUser.address, author: currentUser.address,
}; };
const messageService = new MessageService(authService!); const cryptoService = new CryptoService();
const result = await messageService.signAndSendMessage(modMsg); const messageService = new MessageService(authService!, cryptoService);
const result = await messageService.sendMessage(modMsg);
if (!result.success) { if (!result.success) {
toast({ title: 'Moderation Failed', description: result.error || 'Failed to moderate comment. Please try again.', variant: 'destructive' }); toast({ title: 'Moderation Failed', description: result.error || 'Failed to moderate comment. Please try again.', variant: 'destructive' });
return false; return false;
@ -437,8 +442,9 @@ export const moderateUser = async (
signature: '', signature: '',
browserPubKey: currentUser.browserPubKey, browserPubKey: currentUser.browserPubKey,
}; };
const messageService = new MessageService(authService!); const cryptoService = new CryptoService();
const result = await messageService.signAndSendMessage(modMsg); const messageService = new MessageService(authService!, cryptoService);
const result = await messageService.sendMessage(modMsg);
if (!result.success) { if (!result.success) {
toast({ title: 'Moderation Failed', description: result.error || 'Failed to moderate user. Please try again.', variant: 'destructive' }); toast({ title: 'Moderation Failed', description: result.error || 'Failed to moderate user. Please try again.', variant: 'destructive' });
return false; return false;

View File

@ -1,11 +1,9 @@
import { User } from '@/types';
import { WalletService } from '../wallets/index'; import { WalletService } from '../wallets/index';
import { UseAppKitAccountReturn } from '@reown/appkit/react'; import { UseAppKitAccountReturn } from '@reown/appkit/react';
import { AppKit } from '@reown/appkit'; import { AppKit } from '@reown/appkit';
import { OrdinalAPI } from '../ordinal'; import { OrdinalAPI } from '../ordinal';
import { MessageSigning } from '../signatures/message-signing'; import { CryptoService, DelegationDuration } from './CryptoService';
import { KeyDelegation, DelegationDuration } from '../signatures/key-delegation'; import { EVerificationStatus, User } from '@/types/forum';
import { OpchanMessage } from '@/types';
export interface AuthResult { export interface AuthResult {
success: boolean; success: boolean;
@ -13,17 +11,37 @@ export interface AuthResult {
error?: string; error?: string;
} }
export class AuthService { export interface AuthServiceInterface {
// Wallet operations
setAccounts(bitcoinAccount: UseAppKitAccountReturn, ethereumAccount: UseAppKitAccountReturn): void;
setAppKit(appKit: AppKit): void;
connectWallet(): Promise<AuthResult>;
disconnectWallet(): Promise<void>;
// Verification
verifyOwnership(user: User): Promise<AuthResult>;
// Delegation setup
delegateKey(user: User, duration?: DelegationDuration): Promise<AuthResult>;
// User persistence
loadStoredUser(): User | null;
saveUser(user: User): void;
clearStoredUser(): void;
// Wallet info
getWalletInfo(): Promise<any>;
}
export class AuthService implements AuthServiceInterface {
private walletService: WalletService; private walletService: WalletService;
private ordinalApi: OrdinalAPI; private ordinalApi: OrdinalAPI;
private messageSigning: MessageSigning; private cryptoService: CryptoService;
private keyDelegation: KeyDelegation;
constructor() { constructor(cryptoService: CryptoService) {
this.walletService = new WalletService(); this.walletService = new WalletService();
this.ordinalApi = new OrdinalAPI(); this.ordinalApi = new OrdinalAPI();
this.keyDelegation = new KeyDelegation(); this.cryptoService = cryptoService;
this.messageSigning = new MessageSigning(this.keyDelegation);
} }
/** /**
@ -98,7 +116,7 @@ export class AuthService {
const user: User = { const user: User = {
address: address, address: address,
walletType: walletType, walletType: walletType,
verificationStatus: 'unverified', verificationStatus: EVerificationStatus.UNVERIFIED,
lastChecked: Date.now(), lastChecked: Date.now(),
}; };
@ -132,7 +150,7 @@ export class AuthService {
*/ */
async disconnectWallet(): Promise<void> { async disconnectWallet(): Promise<void> {
// Clear any existing delegations when disconnecting // Clear any existing delegations when disconnecting
this.keyDelegation.clearDelegation(); this.cryptoService.clearDelegation();
this.walletService.clearDelegation('bitcoin'); this.walletService.clearDelegation('bitcoin');
this.walletService.clearDelegation('ethereum'); this.walletService.clearDelegation('ethereum');
@ -140,13 +158,6 @@ export class AuthService {
this.clearStoredUser(); this.clearStoredUser();
} }
/**
* Clear delegation for current wallet
*/
clearDelegation(): void {
this.keyDelegation.clearDelegation();
}
/** /**
* Verify ordinal ownership for Bitcoin users or ENS ownership for Ethereum users * Verify ordinal ownership for Bitcoin users or ENS ownership for Ethereum users
*/ */
@ -182,7 +193,7 @@ export class AuthService {
const updatedUser = { const updatedUser = {
...user, ...user,
ordinalOwnership: hasOperators, ordinalOwnership: hasOperators,
verificationStatus: hasOperators ? 'verified-owner' : 'verified-basic', verificationStatus: hasOperators ? EVerificationStatus.VERIFIED_OWNER : EVerificationStatus.VERIFIED_BASIC,
lastChecked: Date.now(), lastChecked: Date.now(),
}; };
@ -207,7 +218,7 @@ export class AuthService {
...user, ...user,
ensOwnership: hasENS, ensOwnership: hasENS,
ensName: ensName, ensName: ensName,
verificationStatus: hasENS ? 'verified-owner' : 'verified-basic', verificationStatus: hasENS ? EVerificationStatus.VERIFIED_OWNER : EVerificationStatus.VERIFIED_BASIC,
lastChecked: Date.now(), lastChecked: Date.now(),
}; };
@ -223,7 +234,7 @@ export class AuthService {
...user, ...user,
ensOwnership: false, ensOwnership: false,
ensName: undefined, ensName: undefined,
verificationStatus: 'verified-basic', verificationStatus: EVerificationStatus.VERIFIED_BASIC,
lastChecked: Date.now(), lastChecked: Date.now(),
}; };
@ -262,7 +273,7 @@ export class AuthService {
const delegationStatus = this.walletService.getDelegationStatus(walletType); const delegationStatus = this.walletService.getDelegationStatus(walletType);
// Get the actual browser public key from the delegation // Get the actual browser public key from the delegation
const browserPublicKey = this.keyDelegation.getBrowserPublicKey(); const browserPublicKey = this.cryptoService.getBrowserPublicKey();
const updatedUser = { const updatedUser = {
...user, ...user,
@ -283,44 +294,6 @@ export class AuthService {
} }
} }
/**
* Sign a message using delegated key
*/
async signMessage(message: OpchanMessage): Promise<OpchanMessage | null> {
return this.messageSigning.signMessage(message);
}
/**
* Verify a message signature
*/
verifyMessage(message: OpchanMessage): boolean {
return this.messageSigning.verifyMessage(message);
}
/**
* Check if delegation is valid
*/
isDelegationValid(): boolean {
// 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
*/
getDelegationTimeRemaining(): number {
// 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
*/ */

View File

@ -1,22 +1,57 @@
/** /**
* Key delegation for Bitcoin wallets * CryptoService - Unified cryptographic operations
* *
* This module handles the creation of browser-based keypairs and * Combines key delegation and message signing functionality into a single,
* delegation of signing authority from Bitcoin wallets to these keypairs. * cohesive service focused on all cryptographic operations.
*/ */
import * as ed from '@noble/ed25519'; import * as ed from '@noble/ed25519';
import { sha512 } from '@noble/hashes/sha512'; import { sha512 } from '@noble/hashes/sha512';
import { bytesToHex, hexToBytes } from '@/lib/utils'; import { bytesToHex, hexToBytes } from '@/lib/utils';
import { LOCAL_STORAGE_KEYS } from '@/lib/waku/constants'; import { LOCAL_STORAGE_KEYS } from '@/lib/waku/constants';
import { DelegationInfo } from './types'; import { OpchanMessage } from '@/types/forum';
export interface DelegationSignature {
signature: string; // Signature from wallet
expiryTimestamp: number; // When this delegation expires
browserPublicKey: string; // Browser-generated public key that was delegated to
walletAddress: string; // Wallet address that signed the delegation
walletType: 'bitcoin' | 'ethereum'; // Type of wallet that created the delegation
}
export interface DelegationInfo extends DelegationSignature {
browserPrivateKey: string;
}
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m)); ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
export type DelegationDuration = '7days' | '30days'; export type DelegationDuration = '7days' | '30days';
export class KeyDelegation { export interface CryptoServiceInterface {
private static readonly DEFAULT_EXPIRY_HOURS = 24; // Delegation management
createDelegation(
walletAddress: string,
signature: string,
browserPublicKey: string,
browserPrivateKey: string,
duration: DelegationDuration,
walletType: 'bitcoin' | 'ethereum'
): void;
isDelegationValid(currentAddress?: string, currentWalletType?: 'bitcoin' | 'ethereum'): boolean;
getDelegationTimeRemaining(): number;
getBrowserPublicKey(): string | null;
clearDelegation(): void;
// Keypair generation
generateKeypair(): { publicKey: string; privateKey: string };
createDelegationMessage(browserPublicKey: string, walletAddress: string, expiryTimestamp: number): string;
// Message operations
signMessage<T extends OpchanMessage>(message: T): T | null;
verifyMessage(message: OpchanMessage): boolean;
}
export class CryptoService implements CryptoServiceInterface {
private static readonly STORAGE_KEY = LOCAL_STORAGE_KEYS.KEY_DELEGATION; private static readonly STORAGE_KEY = LOCAL_STORAGE_KEYS.KEY_DELEGATION;
// Duration options in hours // Duration options in hours
@ -24,24 +59,27 @@ export class KeyDelegation {
'7days': 24 * 7, // 168 hours '7days': 24 * 7, // 168 hours
'30days': 24 * 30 // 720 hours '30days': 24 * 30 // 720 hours
} as const; } as const;
/** /**
* Get the number of hours for a given duration * Get the number of hours for a given duration
*/ */
static getDurationHours(duration: DelegationDuration): number { static getDurationHours(duration: DelegationDuration): number {
return KeyDelegation.DURATION_HOURS[duration]; return CryptoService.DURATION_HOURS[duration];
} }
/** /**
* Get available duration options * Get available duration options
*/ */
static getAvailableDurations(): DelegationDuration[] { static getAvailableDurations(): DelegationDuration[] {
return Object.keys(KeyDelegation.DURATION_HOURS) as DelegationDuration[]; return Object.keys(CryptoService.DURATION_HOURS) as DelegationDuration[];
} }
// ============================================================================
// KEYPAIR GENERATION
// ============================================================================
/** /**
* Generates a new browser-based keypair for signing messages * Generates a new browser-based keypair for signing messages
* @returns Promise with keypair object containing hex-encoded public and private keys
*/ */
generateKeypair(): { publicKey: string; privateKey: string } { generateKeypair(): { publicKey: string; privateKey: string } {
const privateKey = ed.utils.randomPrivateKey(); const privateKey = ed.utils.randomPrivateKey();
@ -55,13 +93,9 @@ export class KeyDelegation {
publicKey: publicKeyHex publicKey: publicKeyHex
}; };
} }
/** /**
* Creates a delegation message to be signed by the wallet * Creates a delegation message to be signed by the wallet
* @param browserPublicKey The browser-generated public key
* @param walletAddress The user's wallet address
* @param expiryTimestamp When the delegation will expire
* @returns The message to be signed
*/ */
createDelegationMessage( createDelegationMessage(
browserPublicKey: string, browserPublicKey: string,
@ -71,15 +105,12 @@ export class KeyDelegation {
return `I, ${walletAddress}, delegate authority to this pubkey: ${browserPublicKey} until ${expiryTimestamp}`; return `I, ${walletAddress}, delegate authority to this pubkey: ${browserPublicKey} until ${expiryTimestamp}`;
} }
// ============================================================================
// DELEGATION MANAGEMENT
// ============================================================================
/** /**
* Creates a delegation object from the signed message * Creates and stores a delegation
* @param walletAddress The wallet address that signed the delegation
* @param signature The signature from the wallet
* @param browserPublicKey The browser-generated public key
* @param browserPrivateKey The browser-generated private key
* @param duration The duration of the delegation ('1week' or '30days')
* @param walletType The type of wallet (bitcoin or ethereum)
* @returns DelegationInfo object
*/ */
createDelegation( createDelegation(
walletAddress: string, walletAddress: string,
@ -88,11 +119,11 @@ export class KeyDelegation {
browserPrivateKey: string, browserPrivateKey: string,
duration: DelegationDuration = '7days', duration: DelegationDuration = '7days',
walletType: 'bitcoin' | 'ethereum' walletType: 'bitcoin' | 'ethereum'
): DelegationInfo { ): void {
const expiryHours = KeyDelegation.getDurationHours(duration); const expiryHours = CryptoService.getDurationHours(duration);
const expiryTimestamp = Date.now() + (expiryHours * 60 * 60 * 1000); const expiryTimestamp = Date.now() + (expiryHours * 60 * 60 * 1000);
return { const delegationInfo: DelegationInfo = {
signature, signature,
expiryTimestamp, expiryTimestamp,
browserPublicKey, browserPublicKey,
@ -100,22 +131,15 @@ export class KeyDelegation {
walletAddress, walletAddress,
walletType walletType
}; };
localStorage.setItem(CryptoService.STORAGE_KEY, JSON.stringify(delegationInfo));
} }
/**
* Stores delegation information in local storage
* @param delegationInfo The delegation information to store
*/
storeDelegation(delegationInfo: DelegationInfo): void {
localStorage.setItem(KeyDelegation.STORAGE_KEY, JSON.stringify(delegationInfo));
}
/** /**
* Retrieves delegation information from local storage * Retrieves delegation information from local storage
* @returns The stored delegation information or null if not found
*/ */
retrieveDelegation(): DelegationInfo | null { private retrieveDelegation(): DelegationInfo | null {
const delegationJson = localStorage.getItem(KeyDelegation.STORAGE_KEY); const delegationJson = localStorage.getItem(CryptoService.STORAGE_KEY);
if (!delegationJson) return null; if (!delegationJson) return null;
try { try {
@ -125,12 +149,9 @@ export class KeyDelegation {
return null; return null;
} }
} }
/** /**
* Checks if a delegation is valid (exists, not expired, and matches current wallet) * Checks if a delegation is valid
* @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
*/ */
isDelegationValid(currentAddress?: string, currentWalletType?: 'bitcoin' | 'ethereum'): boolean { isDelegationValid(currentAddress?: string, currentWalletType?: 'bitcoin' | 'ethereum'): boolean {
const delegation = this.retrieveDelegation(); const delegation = this.retrieveDelegation();
@ -152,13 +173,42 @@ export class KeyDelegation {
return true; return true;
} }
/** /**
* Signs a message using the browser-generated private key * Gets the time remaining on the current delegation
* @param message The message to sign
* @returns Promise resolving to the signature as a hex string, or null if no valid delegation
*/ */
signMessage(message: string): string | null { getDelegationTimeRemaining(): number {
const delegation = this.retrieveDelegation();
if (!delegation) return 0;
const now = Date.now();
return Math.max(0, delegation.expiryTimestamp - now);
}
/**
* Gets the browser public key from the current delegation
*/
getBrowserPublicKey(): string | null {
const delegation = this.retrieveDelegation();
if (!delegation) return null;
return delegation.browserPublicKey;
}
/**
* Clears the stored delegation
*/
clearDelegation(): void {
localStorage.removeItem(CryptoService.STORAGE_KEY);
}
// ============================================================================
// MESSAGE SIGNING & VERIFICATION
// ============================================================================
/**
* Signs a raw string message using the browser-generated private key
*/
signRawMessage(message: string): string | null {
const delegation = this.retrieveDelegation(); const delegation = this.retrieveDelegation();
if (!delegation || !this.isDelegationValid()) return null; if (!delegation || !this.isDelegationValid()) return null;
@ -166,22 +216,18 @@ export class KeyDelegation {
const privateKeyBytes = hexToBytes(delegation.browserPrivateKey); const privateKeyBytes = hexToBytes(delegation.browserPrivateKey);
const messageBytes = new TextEncoder().encode(message); const messageBytes = new TextEncoder().encode(message);
const signature = ed.sign(messageBytes, privateKeyBytes); const signature = ed.sign(messageBytes, privateKeyBytes);
return bytesToHex(signature); return bytesToHex(signature);
} catch (error) { } catch (error) {
console.error('Error signing with browser key:', error); console.error('Error signing with browser key:', error);
return null; return null;
} }
} }
/** /**
* Verifies a signature made with the browser key * Verifies a signature made with the browser key
* @param message The original message
* @param signature The signature to verify (hex string)
* @param publicKey The public key to verify against (hex string)
* @returns Promise resolving to a boolean indicating if the signature is valid
*/ */
verifySignature( private verifyRawSignature(
message: string, message: string,
signature: string, signature: string,
publicKey: string publicKey: string
@ -197,43 +243,66 @@ export class KeyDelegation {
return false; return false;
} }
} }
/** /**
* Gets the current delegation's Bitcoin address, if available * Signs an OpchanMessage with the delegated browser key
* @returns The Bitcoin address or null if no valid delegation exists
*/ */
getDelegatingAddress(): string | null { signMessage<T extends OpchanMessage>(message: T): T | null {
const delegation = this.retrieveDelegation(); if (!this.isDelegationValid()) {
if (!delegation || !this.isDelegationValid()) return null; console.error('No valid key delegation found. Cannot sign message.');
return delegation.walletAddress; return null;
} }
/**
* Gets the browser public key from the current delegation
* @returns The browser public key or null if no valid delegation exists
*/
getBrowserPublicKey(): string | null {
const delegation = this.retrieveDelegation(); const delegation = this.retrieveDelegation();
if (!delegation) return null; if (!delegation) return null;
return delegation.browserPublicKey;
}
/**
* Clears the stored delegation
*/
clearDelegation(): void {
localStorage.removeItem(KeyDelegation.STORAGE_KEY);
}
/**
* Gets the time remaining on the current delegation
* @returns Time remaining in milliseconds, or 0 if expired/no delegation
*/
getDelegationTimeRemaining(): number {
const delegation = this.retrieveDelegation();
if (!delegation) return 0;
const now = Date.now(); // Create the message content to sign (without signature fields)
return Math.max(0, delegation.expiryTimestamp - now); const messageToSign = JSON.stringify({
...message,
signature: undefined,
browserPubKey: undefined
});
const signature = this.signRawMessage(messageToSign);
if (!signature) return null;
return {
...message,
signature,
browserPubKey: delegation.browserPublicKey
};
} }
}
/**
* Verifies an OpchanMessage signature
*/
verifyMessage(message: OpchanMessage): boolean {
// Check for required signature fields
if (!message.signature || !message.browserPubKey) {
const messageId = 'id' in message ? message.id : `${message.type}-${message.timestamp}`;
console.warn('Message is missing signature information', messageId);
return false;
}
// Reconstruct the original signed content
const signedContent = JSON.stringify({
...message,
signature: undefined,
browserPubKey: undefined
});
// Verify the signature
const isValid = this.verifyRawSignature(
signedContent,
message.signature,
message.browserPubKey
);
if (!isValid) {
const messageId = 'id' in message ? message.id : `${message.type}-${message.timestamp}`;
console.warn(`Invalid signature for message ${messageId}`);
}
return isValid;
}
}

View File

@ -1,5 +1,6 @@
import { OpchanMessage } from '@/types'; import { OpchanMessage } from '@/types/forum';
import { AuthService } from './AuthService'; import { AuthService } from './AuthService';
import { CryptoService } from './CryptoService';
import messageManager from '@/lib/waku'; import messageManager from '@/lib/waku';
export interface MessageResult { export interface MessageResult {
@ -8,23 +9,30 @@ export interface MessageResult {
error?: string; error?: string;
} }
export class MessageService { export interface MessageServiceInterface {
private authService: AuthService; sendMessage(message: OpchanMessage): Promise<MessageResult>;
verifyMessage(message: OpchanMessage): boolean;
}
constructor(authService: AuthService) { export class MessageService implements MessageServiceInterface {
private authService: AuthService;
private cryptoService: CryptoService;
constructor(authService: AuthService, cryptoService: CryptoService) {
this.authService = authService; this.authService = authService;
this.cryptoService = cryptoService;
} }
/** /**
* Sign and send a message to the Waku network * Sign and send a message to the Waku network
*/ */
async signAndSendMessage(message: OpchanMessage): Promise<MessageResult> { async sendMessage(message: OpchanMessage): Promise<MessageResult> {
try { try {
const signedMessage = await this.authService.signMessage(message); const signedMessage = this.cryptoService.signMessage(message);
if (!signedMessage) { if (!signedMessage) {
// Check if delegation exists but is expired // Check if delegation exists but is expired
const isDelegationExpired = this.authService.isDelegationValid() === false; const isDelegationExpired = this.cryptoService.isDelegationValid() === false;
return { return {
success: false, success: false,
@ -66,6 +74,6 @@ export class MessageService {
* Verify a message signature * Verify a message signature
*/ */
verifyMessage(message: OpchanMessage): boolean { verifyMessage(message: OpchanMessage): boolean {
return this.authService.verifyMessage(message); return this.cryptoService.verifyMessage(message);
} }
} }

View File

@ -1,2 +1,3 @@
export { AuthService } from './AuthService'; export { AuthService, type AuthServiceInterface } from './AuthService';
export { MessageService } from './MessageService'; export { MessageService, type MessageServiceInterface } from './MessageService';
export { CryptoService, type CryptoServiceInterface, type DelegationDuration } from './CryptoService';

View File

@ -1,71 +0,0 @@
import { OpchanMessage } from '@/types';
import { KeyDelegation } from './key-delegation';
export class MessageSigning {
private keyDelegation: KeyDelegation;
constructor(keyDelegation: KeyDelegation) {
this.keyDelegation = keyDelegation;
}
signMessage<T extends OpchanMessage>(message: T): T | null {
if (!this.keyDelegation.isDelegationValid()) {
console.error('No valid key delegation found. Cannot sign message.');
return null;
}
const delegation = this.keyDelegation.retrieveDelegation();
if (!delegation) return null;
const messageToSign = JSON.stringify({
...message,
signature: undefined,
browserPubKey: undefined
});
const signature = this.keyDelegation.signMessage(messageToSign);
if (!signature) return null;
return {
...message,
signature,
browserPubKey: delegation.browserPublicKey
};
}
verifyMessage(message: OpchanMessage): boolean {
// Check for required signature fields
if (!message.signature || !message.browserPubKey) {
const messageId = 'id' in message ? message.id : `${message.type}-${message.timestamp}`;
console.warn('Message is missing signature information', messageId);
return false;
}
// Reconstruct the original signed content
const signedContent = JSON.stringify({
...message,
signature: undefined,
browserPubKey: undefined
});
// Verify the signature
const isValid = this.keyDelegation.verifySignature(
signedContent,
message.signature,
message.browserPubKey
);
if (!isValid) {
const messageId = 'id' in message ? message.id : `${message.type}-${message.timestamp}`;
console.warn(`Invalid signature for message ${messageId}`);
}
return isValid;
}
}

View File

@ -1,11 +0,0 @@
export interface DelegationSignature {
signature: string; // Signature from wallet
expiryTimestamp: number; // When this delegation expires
browserPublicKey: string; // Browser-generated public key that was delegated to
walletAddress: string; // Wallet address that signed the delegation
walletType: 'bitcoin' | 'ethereum'; // Type of wallet that created the delegation
}
export interface DelegationInfo extends DelegationSignature {
browserPrivateKey: string;
}

View File

@ -1,5 +1,5 @@
import { UseAppKitAccountReturn } from '@reown/appkit/react'; import { UseAppKitAccountReturn } from '@reown/appkit/react';
import { KeyDelegation, DelegationDuration } from '../signatures/key-delegation'; import { CryptoService, DelegationDuration } from '../services/CryptoService';
import { AppKit } from '@reown/appkit'; import { AppKit } from '@reown/appkit';
import { getEnsName } from '@wagmi/core'; import { getEnsName } from '@wagmi/core';
import { ChainNamespace } from '@reown/appkit-common'; import { ChainNamespace } from '@reown/appkit-common';
@ -14,13 +14,13 @@ export interface WalletInfo {
} }
export class ReOwnWalletService { export class ReOwnWalletService {
private keyDelegation: KeyDelegation; private cryptoService: CryptoService;
private bitcoinAccount?: UseAppKitAccountReturn; private bitcoinAccount?: UseAppKitAccountReturn;
private ethereumAccount?: UseAppKitAccountReturn; private ethereumAccount?: UseAppKitAccountReturn;
private appKit?: AppKit; private appKit?: AppKit;
constructor() { constructor() {
this.keyDelegation = new KeyDelegation(); this.cryptoService = new CryptoService();
} }
/** /**
@ -130,12 +130,12 @@ export class ReOwnWalletService {
} }
// Generate a new browser keypair // Generate a new browser keypair
const keypair = this.keyDelegation.generateKeypair(); const keypair = this.cryptoService.generateKeypair();
// Create delegation message with expiry // Create delegation message with expiry
const expiryHours = KeyDelegation.getDurationHours(duration); const expiryHours = CryptoService.getDurationHours(duration);
const expiryTimestamp = Date.now() + (expiryHours * 60 * 60 * 1000); const expiryTimestamp = Date.now() + (expiryHours * 60 * 60 * 1000);
const delegationMessage = this.keyDelegation.createDelegationMessage( const delegationMessage = this.cryptoService.createDelegationMessage(
keypair.publicKey, keypair.publicKey,
account.address, account.address,
expiryTimestamp expiryTimestamp
@ -147,7 +147,7 @@ export class ReOwnWalletService {
const signature = await this.signMessage(messageBytes, walletType); const signature = await this.signMessage(messageBytes, walletType);
// Create and store the delegation // Create and store the delegation
const delegationInfo = this.keyDelegation.createDelegation( this.cryptoService.createDelegation(
account.address, account.address,
signature, signature,
keypair.publicKey, keypair.publicKey,
@ -155,8 +155,6 @@ export class ReOwnWalletService {
duration, duration,
walletType walletType
); );
this.keyDelegation.storeDelegation(delegationInfo);
return true; return true;
} catch (error) { } catch (error) {
@ -175,10 +173,10 @@ export class ReOwnWalletService {
} }
// Check if we have a valid delegation for this specific wallet // Check if we have a valid delegation for this specific wallet
if (this.keyDelegation.isDelegationValid(account.address, walletType)) { if (this.cryptoService.isDelegationValid(account.address, walletType)) {
// Use delegated key for signing // Use delegated key for signing
const messageString = new TextDecoder().decode(messageBytes); const messageString = new TextDecoder().decode(messageBytes);
const signature = this.keyDelegation.signMessage(messageString); const signature = this.cryptoService.signRawMessage(messageString);
if (signature) { if (signature) {
return signature; return signature;
@ -200,9 +198,9 @@ export class ReOwnWalletService {
const account = this.getActiveAccount(walletType); const account = this.getActiveAccount(walletType);
const currentAddress = account?.address; const currentAddress = account?.address;
const hasDelegation = this.keyDelegation.retrieveDelegation() !== null; const hasDelegation = this.cryptoService.getBrowserPublicKey() !== null;
const isValid = this.keyDelegation.isDelegationValid(currentAddress, walletType); const isValid = this.cryptoService.isDelegationValid(currentAddress, walletType);
const timeRemaining = this.keyDelegation.getDelegationTimeRemaining(); const timeRemaining = this.cryptoService.getDelegationTimeRemaining();
return { return {
hasDelegation, hasDelegation,
@ -215,7 +213,7 @@ export class ReOwnWalletService {
* Clear delegation for the connected wallet * Clear delegation for the connected wallet
*/ */
clearDelegation(walletType: 'bitcoin' | 'ethereum'): void { clearDelegation(walletType: 'bitcoin' | 'ethereum'): void {
this.keyDelegation.clearDelegation(); this.cryptoService.clearDelegation();
} }
/** /**

View File

@ -14,7 +14,7 @@ export interface User {
ensAvatar?: string; ensAvatar?: string;
ensOwnership?: boolean; ensOwnership?: boolean;
verificationStatus: 'unverified' | 'verified-none' | 'verified-basic' | 'verified-owner' | 'verifying'; verificationStatus: EVerificationStatus;
signature?: string; signature?: string;
lastChecked?: number; lastChecked?: number;
@ -23,6 +23,14 @@ export interface User {
delegationExpiry?: number; // When the delegation expires delegationExpiry?: number; // When the delegation expires
} }
export enum EVerificationStatus {
UNVERIFIED = 'unverified',
VERIFIED_NONE = 'verified-none',
VERIFIED_BASIC = 'verified-basic',
VERIFIED_OWNER = 'verified-owner',
VERIFYING = 'verifying',
}
export interface Cell { export interface Cell {
id: string; id: string;
name: string; name: string;