mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-02 21:03:09 +00:00
chore: clean up services
This commit is contained in:
parent
62d6800f0b
commit
9b6a1c48d7
@ -2,7 +2,7 @@ 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';
|
||||
import { DelegationDuration } from '@/lib/identity/services/CryptoService';
|
||||
|
||||
interface DelegationStepProps {
|
||||
onComplete: () => void;
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import React, { createContext, useContext, useState, useEffect, useRef } from 'react';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { User } from '@/types';
|
||||
import { AuthService, AuthResult } from '@/lib/identity/services/AuthService';
|
||||
import { OpchanMessage } from '@/types';
|
||||
import { User, OpchanMessage, EVerificationStatus } from '@/types/forum';
|
||||
import { AuthService, CryptoService, MessageService, DelegationDuration } from '@/lib/identity/services';
|
||||
import { AuthResult } from '@/lib/identity/services/AuthService';
|
||||
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';
|
||||
|
||||
@ -47,8 +46,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const activeAccount = isBitcoinConnected ? bitcoinAccount : ethereumAccount;
|
||||
const address = activeAccount.address;
|
||||
|
||||
// Create ref for AuthService so it persists between renders
|
||||
const authServiceRef = useRef(new AuthService());
|
||||
// Create service instances that persist between renders
|
||||
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
|
||||
useEffect(() => {
|
||||
@ -76,7 +77,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const newUser: User = {
|
||||
address,
|
||||
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(),
|
||||
};
|
||||
|
||||
@ -88,7 +89,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
...newUser,
|
||||
ensOwnership: true,
|
||||
ensName: walletInfo.ensName,
|
||||
verificationStatus: 'verified-owner' as const,
|
||||
verificationStatus: EVerificationStatus.VERIFIED_OWNER,
|
||||
};
|
||||
setCurrentUser(updatedUser);
|
||||
setVerificationStatus('verified-owner');
|
||||
@ -296,15 +297,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
};
|
||||
|
||||
const isDelegationValid = (): boolean => {
|
||||
return authServiceRef.current.isDelegationValid();
|
||||
return cryptoServiceRef.current.isDelegationValid();
|
||||
};
|
||||
|
||||
const delegationTimeRemaining = (): number => {
|
||||
return authServiceRef.current.getDelegationTimeRemaining();
|
||||
return cryptoServiceRef.current.getDelegationTimeRemaining();
|
||||
};
|
||||
|
||||
const clearDelegation = (): void => {
|
||||
authServiceRef.current.clearDelegation();
|
||||
cryptoServiceRef.current.clearDelegation();
|
||||
|
||||
// Update the current user to remove delegation info
|
||||
if (currentUser) {
|
||||
@ -329,10 +330,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
const messageSigning = {
|
||||
signMessage: async (message: OpchanMessage): Promise<OpchanMessage | null> => {
|
||||
return authServiceRef.current.signMessage(message);
|
||||
return cryptoServiceRef.current.signMessage(message);
|
||||
},
|
||||
verifyMessage: (message: OpchanMessage): boolean => {
|
||||
return authServiceRef.current.verifyMessage(message);
|
||||
return cryptoServiceRef.current.verifyMessage(message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { expect, describe, beforeEach, it } from 'vitest';
|
||||
|
||||
@ -58,7 +58,7 @@ describe('RelevanceCalculator', () => {
|
||||
const verifiedUser: User = {
|
||||
address: 'user1',
|
||||
walletType: 'ethereum',
|
||||
verificationStatus: 'verified-owner',
|
||||
verificationStatus: EVerificationStatus.VERIFIED_OWNER,
|
||||
ensOwnership: true,
|
||||
ensName: 'test.eth',
|
||||
lastChecked: Date.now()
|
||||
@ -72,7 +72,7 @@ describe('RelevanceCalculator', () => {
|
||||
const verifiedUser: User = {
|
||||
address: 'user3',
|
||||
walletType: 'bitcoin',
|
||||
verificationStatus: 'verified-owner',
|
||||
verificationStatus: EVerificationStatus.VERIFIED_OWNER,
|
||||
ordinalOwnership: true,
|
||||
lastChecked: Date.now()
|
||||
};
|
||||
@ -85,7 +85,7 @@ describe('RelevanceCalculator', () => {
|
||||
const unverifiedUser: User = {
|
||||
address: 'user2',
|
||||
walletType: 'ethereum',
|
||||
verificationStatus: 'unverified',
|
||||
verificationStatus: EVerificationStatus.UNVERIFIED,
|
||||
ensOwnership: false,
|
||||
lastChecked: Date.now()
|
||||
};
|
||||
@ -184,7 +184,7 @@ describe('RelevanceCalculator', () => {
|
||||
{
|
||||
address: 'user1',
|
||||
walletType: 'ethereum',
|
||||
verificationStatus: 'verified-owner',
|
||||
verificationStatus: EVerificationStatus.VERIFIED_OWNER,
|
||||
ensOwnership: true,
|
||||
ensName: 'test.eth',
|
||||
lastChecked: Date.now()
|
||||
@ -192,7 +192,7 @@ describe('RelevanceCalculator', () => {
|
||||
{
|
||||
address: 'user2',
|
||||
walletType: 'bitcoin',
|
||||
verificationStatus: 'unverified',
|
||||
verificationStatus: EVerificationStatus.UNVERIFIED,
|
||||
ordinalOwnership: false,
|
||||
lastChecked: Date.now()
|
||||
}
|
||||
|
||||
@ -9,8 +9,7 @@ import {
|
||||
} from '@/lib/waku/types';
|
||||
import { Cell, Comment, Post, User } from '@/types/forum';
|
||||
import { transformCell, transformComment, transformPost } from './transformers';
|
||||
import { MessageService } from '@/lib/identity/services/MessageService';
|
||||
import { AuthService } from '@/lib/identity/services/AuthService';
|
||||
import { MessageService, AuthService, CryptoService } from '@/lib/identity/services';
|
||||
|
||||
type ToastFunction = (props: {
|
||||
title: string;
|
||||
@ -70,8 +69,9 @@ export const createPost = async (
|
||||
author: currentUser.address,
|
||||
};
|
||||
|
||||
const messageService = new MessageService(authService!);
|
||||
const result = await messageService.signAndSendMessage(postMessage);
|
||||
const cryptoService = new CryptoService();
|
||||
const messageService = new MessageService(authService!, cryptoService);
|
||||
const result = await messageService.sendMessage(postMessage);
|
||||
if (!result.success) {
|
||||
toast({
|
||||
title: 'Post Failed',
|
||||
@ -141,8 +141,9 @@ export const createComment = async (
|
||||
author: currentUser.address,
|
||||
};
|
||||
|
||||
const messageService = new MessageService(authService!);
|
||||
const result = await messageService.signAndSendMessage(commentMessage);
|
||||
const cryptoService = new CryptoService();
|
||||
const messageService = new MessageService(authService!, cryptoService);
|
||||
const result = await messageService.sendMessage(commentMessage);
|
||||
if (!result.success) {
|
||||
toast({
|
||||
title: 'Comment Failed',
|
||||
@ -199,8 +200,9 @@ export const createCell = async (
|
||||
author: currentUser.address,
|
||||
};
|
||||
|
||||
const messageService = new MessageService(authService!);
|
||||
const result = await messageService.signAndSendMessage(cellMessage);
|
||||
const cryptoService = new CryptoService();
|
||||
const messageService = new MessageService(authService!, cryptoService);
|
||||
const result = await messageService.sendMessage(cellMessage);
|
||||
if (!result.success) {
|
||||
toast({
|
||||
title: 'Cell Failed',
|
||||
@ -275,8 +277,9 @@ export const vote = async (
|
||||
author: currentUser.address,
|
||||
};
|
||||
|
||||
const messageService = new MessageService(authService!);
|
||||
const result = await messageService.signAndSendMessage(voteMessage);
|
||||
const cryptoService = new CryptoService();
|
||||
const messageService = new MessageService(authService!, cryptoService);
|
||||
const result = await messageService.sendMessage(voteMessage);
|
||||
if (!result.success) {
|
||||
toast({
|
||||
title: 'Vote Failed',
|
||||
@ -340,8 +343,9 @@ export const moderatePost = async (
|
||||
timestamp: Date.now(),
|
||||
author: currentUser.address,
|
||||
};
|
||||
const messageService = new MessageService(authService!);
|
||||
const result = await messageService.signAndSendMessage(modMsg);
|
||||
const cryptoService = new CryptoService();
|
||||
const messageService = new MessageService(authService!, cryptoService);
|
||||
const result = await messageService.sendMessage(modMsg);
|
||||
if (!result.success) {
|
||||
toast({ title: 'Moderation Failed', description: result.error || 'Failed to moderate post. Please try again.', variant: 'destructive' });
|
||||
return false;
|
||||
@ -389,8 +393,9 @@ export const moderateComment = async (
|
||||
timestamp: Date.now(),
|
||||
author: currentUser.address,
|
||||
};
|
||||
const messageService = new MessageService(authService!);
|
||||
const result = await messageService.signAndSendMessage(modMsg);
|
||||
const cryptoService = new CryptoService();
|
||||
const messageService = new MessageService(authService!, cryptoService);
|
||||
const result = await messageService.sendMessage(modMsg);
|
||||
if (!result.success) {
|
||||
toast({ title: 'Moderation Failed', description: result.error || 'Failed to moderate comment. Please try again.', variant: 'destructive' });
|
||||
return false;
|
||||
@ -437,8 +442,9 @@ export const moderateUser = async (
|
||||
signature: '',
|
||||
browserPubKey: currentUser.browserPubKey,
|
||||
};
|
||||
const messageService = new MessageService(authService!);
|
||||
const result = await messageService.signAndSendMessage(modMsg);
|
||||
const cryptoService = new CryptoService();
|
||||
const messageService = new MessageService(authService!, cryptoService);
|
||||
const result = await messageService.sendMessage(modMsg);
|
||||
if (!result.success) {
|
||||
toast({ title: 'Moderation Failed', description: result.error || 'Failed to moderate user. Please try again.', variant: 'destructive' });
|
||||
return false;
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import { User } from '@/types';
|
||||
import { WalletService } from '../wallets/index';
|
||||
import { UseAppKitAccountReturn } from '@reown/appkit/react';
|
||||
import { AppKit } from '@reown/appkit';
|
||||
import { OrdinalAPI } from '../ordinal';
|
||||
import { MessageSigning } from '../signatures/message-signing';
|
||||
import { KeyDelegation, DelegationDuration } from '../signatures/key-delegation';
|
||||
import { OpchanMessage } from '@/types';
|
||||
import { CryptoService, DelegationDuration } from './CryptoService';
|
||||
import { EVerificationStatus, User } from '@/types/forum';
|
||||
|
||||
export interface AuthResult {
|
||||
success: boolean;
|
||||
@ -13,17 +11,37 @@ export interface AuthResult {
|
||||
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 ordinalApi: OrdinalAPI;
|
||||
private messageSigning: MessageSigning;
|
||||
private keyDelegation: KeyDelegation;
|
||||
private cryptoService: CryptoService;
|
||||
|
||||
constructor() {
|
||||
constructor(cryptoService: CryptoService) {
|
||||
this.walletService = new WalletService();
|
||||
this.ordinalApi = new OrdinalAPI();
|
||||
this.keyDelegation = new KeyDelegation();
|
||||
this.messageSigning = new MessageSigning(this.keyDelegation);
|
||||
this.cryptoService = cryptoService;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -98,7 +116,7 @@ export class AuthService {
|
||||
const user: User = {
|
||||
address: address,
|
||||
walletType: walletType,
|
||||
verificationStatus: 'unverified',
|
||||
verificationStatus: EVerificationStatus.UNVERIFIED,
|
||||
lastChecked: Date.now(),
|
||||
};
|
||||
|
||||
@ -132,7 +150,7 @@ export class AuthService {
|
||||
*/
|
||||
async disconnectWallet(): Promise<void> {
|
||||
// Clear any existing delegations when disconnecting
|
||||
this.keyDelegation.clearDelegation();
|
||||
this.cryptoService.clearDelegation();
|
||||
this.walletService.clearDelegation('bitcoin');
|
||||
this.walletService.clearDelegation('ethereum');
|
||||
|
||||
@ -140,13 +158,6 @@ export class AuthService {
|
||||
this.clearStoredUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear delegation for current wallet
|
||||
*/
|
||||
clearDelegation(): void {
|
||||
this.keyDelegation.clearDelegation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify ordinal ownership for Bitcoin users or ENS ownership for Ethereum users
|
||||
*/
|
||||
@ -182,7 +193,7 @@ export class AuthService {
|
||||
const updatedUser = {
|
||||
...user,
|
||||
ordinalOwnership: hasOperators,
|
||||
verificationStatus: hasOperators ? 'verified-owner' : 'verified-basic',
|
||||
verificationStatus: hasOperators ? EVerificationStatus.VERIFIED_OWNER : EVerificationStatus.VERIFIED_BASIC,
|
||||
lastChecked: Date.now(),
|
||||
};
|
||||
|
||||
@ -207,7 +218,7 @@ export class AuthService {
|
||||
...user,
|
||||
ensOwnership: hasENS,
|
||||
ensName: ensName,
|
||||
verificationStatus: hasENS ? 'verified-owner' : 'verified-basic',
|
||||
verificationStatus: hasENS ? EVerificationStatus.VERIFIED_OWNER : EVerificationStatus.VERIFIED_BASIC,
|
||||
lastChecked: Date.now(),
|
||||
};
|
||||
|
||||
@ -223,7 +234,7 @@ export class AuthService {
|
||||
...user,
|
||||
ensOwnership: false,
|
||||
ensName: undefined,
|
||||
verificationStatus: 'verified-basic',
|
||||
verificationStatus: EVerificationStatus.VERIFIED_BASIC,
|
||||
lastChecked: Date.now(),
|
||||
};
|
||||
|
||||
@ -262,7 +273,7 @@ export class AuthService {
|
||||
const delegationStatus = this.walletService.getDelegationStatus(walletType);
|
||||
|
||||
// Get the actual browser public key from the delegation
|
||||
const browserPublicKey = this.keyDelegation.getBrowserPublicKey();
|
||||
const browserPublicKey = this.cryptoService.getBrowserPublicKey();
|
||||
|
||||
const updatedUser = {
|
||||
...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
|
||||
*/
|
||||
|
||||
@ -1,22 +1,57 @@
|
||||
/**
|
||||
* Key delegation for Bitcoin wallets
|
||||
* CryptoService - Unified cryptographic operations
|
||||
*
|
||||
* This module handles the creation of browser-based keypairs and
|
||||
* delegation of signing authority from Bitcoin wallets to these keypairs.
|
||||
* Combines key delegation and message signing functionality into a single,
|
||||
* cohesive service focused on all cryptographic operations.
|
||||
*/
|
||||
|
||||
import * as ed from '@noble/ed25519';
|
||||
import { sha512 } from '@noble/hashes/sha512';
|
||||
import { bytesToHex, hexToBytes } from '@/lib/utils';
|
||||
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));
|
||||
|
||||
export type DelegationDuration = '7days' | '30days';
|
||||
|
||||
export class KeyDelegation {
|
||||
private static readonly DEFAULT_EXPIRY_HOURS = 24;
|
||||
export interface CryptoServiceInterface {
|
||||
// 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;
|
||||
|
||||
// Duration options in hours
|
||||
@ -24,24 +59,27 @@ export class KeyDelegation {
|
||||
'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];
|
||||
return CryptoService.DURATION_HOURS[duration];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get available duration options
|
||||
*/
|
||||
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
|
||||
* @returns Promise with keypair object containing hex-encoded public and private keys
|
||||
*/
|
||||
generateKeypair(): { publicKey: string; privateKey: string } {
|
||||
const privateKey = ed.utils.randomPrivateKey();
|
||||
@ -55,13 +93,9 @@ export class KeyDelegation {
|
||||
publicKey: publicKeyHex
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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(
|
||||
browserPublicKey: string,
|
||||
@ -71,15 +105,12 @@ export class KeyDelegation {
|
||||
return `I, ${walletAddress}, delegate authority to this pubkey: ${browserPublicKey} until ${expiryTimestamp}`;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DELEGATION MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Creates a delegation object from the signed message
|
||||
* @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
|
||||
* Creates and stores a delegation
|
||||
*/
|
||||
createDelegation(
|
||||
walletAddress: string,
|
||||
@ -88,11 +119,11 @@ export class KeyDelegation {
|
||||
browserPrivateKey: string,
|
||||
duration: DelegationDuration = '7days',
|
||||
walletType: 'bitcoin' | 'ethereum'
|
||||
): DelegationInfo {
|
||||
const expiryHours = KeyDelegation.getDurationHours(duration);
|
||||
): void {
|
||||
const expiryHours = CryptoService.getDurationHours(duration);
|
||||
const expiryTimestamp = Date.now() + (expiryHours * 60 * 60 * 1000);
|
||||
|
||||
return {
|
||||
const delegationInfo: DelegationInfo = {
|
||||
signature,
|
||||
expiryTimestamp,
|
||||
browserPublicKey,
|
||||
@ -100,22 +131,15 @@ export class KeyDelegation {
|
||||
walletAddress,
|
||||
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
|
||||
* @returns The stored delegation information or null if not found
|
||||
*/
|
||||
retrieveDelegation(): DelegationInfo | null {
|
||||
const delegationJson = localStorage.getItem(KeyDelegation.STORAGE_KEY);
|
||||
private retrieveDelegation(): DelegationInfo | null {
|
||||
const delegationJson = localStorage.getItem(CryptoService.STORAGE_KEY);
|
||||
if (!delegationJson) return null;
|
||||
|
||||
try {
|
||||
@ -125,12 +149,9 @@ export class KeyDelegation {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a delegation is valid (exists, not expired, and matches current wallet)
|
||||
* @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
|
||||
* Checks if a delegation is valid
|
||||
*/
|
||||
isDelegationValid(currentAddress?: string, currentWalletType?: 'bitcoin' | 'ethereum'): boolean {
|
||||
const delegation = this.retrieveDelegation();
|
||||
@ -152,13 +173,42 @@ export class KeyDelegation {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Signs a message using the browser-generated private key
|
||||
* @param message The message to sign
|
||||
* @returns Promise resolving to the signature as a hex string, or null if no valid delegation
|
||||
* Gets the time remaining on the current 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();
|
||||
if (!delegation || !this.isDelegationValid()) return null;
|
||||
|
||||
@ -166,22 +216,18 @@ export class KeyDelegation {
|
||||
const privateKeyBytes = hexToBytes(delegation.browserPrivateKey);
|
||||
const messageBytes = new TextEncoder().encode(message);
|
||||
|
||||
const signature = ed.sign(messageBytes, privateKeyBytes);
|
||||
const signature = ed.sign(messageBytes, privateKeyBytes);
|
||||
return bytesToHex(signature);
|
||||
} catch (error) {
|
||||
console.error('Error signing with browser key:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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,
|
||||
signature: string,
|
||||
publicKey: string
|
||||
@ -197,43 +243,66 @@ export class KeyDelegation {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the current delegation's Bitcoin address, if available
|
||||
* @returns The Bitcoin address or null if no valid delegation exists
|
||||
* Signs an OpchanMessage with the delegated browser key
|
||||
*/
|
||||
getDelegatingAddress(): string | null {
|
||||
const delegation = this.retrieveDelegation();
|
||||
if (!delegation || !this.isDelegationValid()) return null;
|
||||
return delegation.walletAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the browser public key from the current delegation
|
||||
* @returns The browser public key or null if no valid delegation exists
|
||||
*/
|
||||
getBrowserPublicKey(): string | null {
|
||||
signMessage<T extends OpchanMessage>(message: T): T | null {
|
||||
if (!this.isDelegationValid()) {
|
||||
console.error('No valid key delegation found. Cannot sign message.');
|
||||
return null;
|
||||
}
|
||||
|
||||
const delegation = this.retrieveDelegation();
|
||||
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();
|
||||
return Math.max(0, delegation.expiryTimestamp - now);
|
||||
// Create the message content to sign (without signature fields)
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import { OpchanMessage } from '@/types';
|
||||
import { OpchanMessage } from '@/types/forum';
|
||||
import { AuthService } from './AuthService';
|
||||
import { CryptoService } from './CryptoService';
|
||||
import messageManager from '@/lib/waku';
|
||||
|
||||
export interface MessageResult {
|
||||
@ -8,23 +9,30 @@ export interface MessageResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class MessageService {
|
||||
private authService: AuthService;
|
||||
export interface MessageServiceInterface {
|
||||
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.cryptoService = cryptoService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign and send a message to the Waku network
|
||||
*/
|
||||
async signAndSendMessage(message: OpchanMessage): Promise<MessageResult> {
|
||||
async sendMessage(message: OpchanMessage): Promise<MessageResult> {
|
||||
try {
|
||||
const signedMessage = await this.authService.signMessage(message);
|
||||
const signedMessage = this.cryptoService.signMessage(message);
|
||||
|
||||
if (!signedMessage) {
|
||||
// Check if delegation exists but is expired
|
||||
const isDelegationExpired = this.authService.isDelegationValid() === false;
|
||||
const isDelegationExpired = this.cryptoService.isDelegationValid() === false;
|
||||
|
||||
return {
|
||||
success: false,
|
||||
@ -66,6 +74,6 @@ export class MessageService {
|
||||
* Verify a message signature
|
||||
*/
|
||||
verifyMessage(message: OpchanMessage): boolean {
|
||||
return this.authService.verifyMessage(message);
|
||||
return this.cryptoService.verifyMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,2 +1,3 @@
|
||||
export { AuthService } from './AuthService';
|
||||
export { MessageService } from './MessageService';
|
||||
export { AuthService, type AuthServiceInterface } from './AuthService';
|
||||
export { MessageService, type MessageServiceInterface } from './MessageService';
|
||||
export { CryptoService, type CryptoServiceInterface, type DelegationDuration } from './CryptoService';
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
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 { getEnsName } from '@wagmi/core';
|
||||
import { ChainNamespace } from '@reown/appkit-common';
|
||||
@ -14,13 +14,13 @@ export interface WalletInfo {
|
||||
}
|
||||
|
||||
export class ReOwnWalletService {
|
||||
private keyDelegation: KeyDelegation;
|
||||
private cryptoService: CryptoService;
|
||||
private bitcoinAccount?: UseAppKitAccountReturn;
|
||||
private ethereumAccount?: UseAppKitAccountReturn;
|
||||
private appKit?: AppKit;
|
||||
|
||||
constructor() {
|
||||
this.keyDelegation = new KeyDelegation();
|
||||
this.cryptoService = new CryptoService();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -130,12 +130,12 @@ export class ReOwnWalletService {
|
||||
}
|
||||
|
||||
// Generate a new browser keypair
|
||||
const keypair = this.keyDelegation.generateKeypair();
|
||||
const keypair = this.cryptoService.generateKeypair();
|
||||
|
||||
// Create delegation message with expiry
|
||||
const expiryHours = KeyDelegation.getDurationHours(duration);
|
||||
const expiryHours = CryptoService.getDurationHours(duration);
|
||||
const expiryTimestamp = Date.now() + (expiryHours * 60 * 60 * 1000);
|
||||
const delegationMessage = this.keyDelegation.createDelegationMessage(
|
||||
const delegationMessage = this.cryptoService.createDelegationMessage(
|
||||
keypair.publicKey,
|
||||
account.address,
|
||||
expiryTimestamp
|
||||
@ -147,7 +147,7 @@ export class ReOwnWalletService {
|
||||
const signature = await this.signMessage(messageBytes, walletType);
|
||||
|
||||
// Create and store the delegation
|
||||
const delegationInfo = this.keyDelegation.createDelegation(
|
||||
this.cryptoService.createDelegation(
|
||||
account.address,
|
||||
signature,
|
||||
keypair.publicKey,
|
||||
@ -155,8 +155,6 @@ export class ReOwnWalletService {
|
||||
duration,
|
||||
walletType
|
||||
);
|
||||
|
||||
this.keyDelegation.storeDelegation(delegationInfo);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
@ -175,10 +173,10 @@ export class ReOwnWalletService {
|
||||
}
|
||||
|
||||
// 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
|
||||
const messageString = new TextDecoder().decode(messageBytes);
|
||||
const signature = this.keyDelegation.signMessage(messageString);
|
||||
const signature = this.cryptoService.signRawMessage(messageString);
|
||||
|
||||
if (signature) {
|
||||
return signature;
|
||||
@ -200,9 +198,9 @@ export class ReOwnWalletService {
|
||||
const account = this.getActiveAccount(walletType);
|
||||
const currentAddress = account?.address;
|
||||
|
||||
const hasDelegation = this.keyDelegation.retrieveDelegation() !== null;
|
||||
const isValid = this.keyDelegation.isDelegationValid(currentAddress, walletType);
|
||||
const timeRemaining = this.keyDelegation.getDelegationTimeRemaining();
|
||||
const hasDelegation = this.cryptoService.getBrowserPublicKey() !== null;
|
||||
const isValid = this.cryptoService.isDelegationValid(currentAddress, walletType);
|
||||
const timeRemaining = this.cryptoService.getDelegationTimeRemaining();
|
||||
|
||||
return {
|
||||
hasDelegation,
|
||||
@ -215,7 +213,7 @@ export class ReOwnWalletService {
|
||||
* Clear delegation for the connected wallet
|
||||
*/
|
||||
clearDelegation(walletType: 'bitcoin' | 'ethereum'): void {
|
||||
this.keyDelegation.clearDelegation();
|
||||
this.cryptoService.clearDelegation();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -14,7 +14,7 @@ export interface User {
|
||||
ensAvatar?: string;
|
||||
ensOwnership?: boolean;
|
||||
|
||||
verificationStatus: 'unverified' | 'verified-none' | 'verified-basic' | 'verified-owner' | 'verifying';
|
||||
verificationStatus: EVerificationStatus;
|
||||
|
||||
signature?: string;
|
||||
lastChecked?: number;
|
||||
@ -23,6 +23,14 @@ export interface User {
|
||||
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 {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user