chore: move out of services into hooks + simplified classes

This commit is contained in:
Danish Arora 2025-09-02 10:48:49 +05:30
parent 9ade3efb10
commit dc4468078e
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
20 changed files with 702 additions and 798 deletions

View File

@ -23,9 +23,9 @@ import PostPage from './pages/PostPage';
import NotFound from './pages/NotFound';
import Dashboard from './pages/Dashboard';
import Index from './pages/Index';
import { appkitConfig } from './lib/services/WalletService/config';
import { appkitConfig } from './lib/wallet/config';
import { WagmiProvider } from 'wagmi';
import { config } from './lib/services/WalletService/config';
import { config } from './lib/wallet/config';
import { AppKitProvider } from '@reown/appkit/react';
// Create a client

View File

@ -3,7 +3,7 @@ import { Badge } from '@/components/ui/badge';
import { Shield, Crown } from 'lucide-react';
import { UserVerificationStatus } from '@/types/forum';
import { getEnsName } from '@wagmi/core';
import { config } from '@/lib/services/WalletService/config';
import { config } from '@/lib/wallet/config';
import { OrdinalAPI } from '@/lib/services/Ordinal';
interface AuthorDisplayProps {

View File

@ -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/services/CryptoService';
import { DelegationDuration } from '@/lib/delegation';
interface DelegationStepProps {
onComplete: () => void;

View File

@ -1,9 +1,9 @@
import React, { createContext, useState, useEffect, useRef } from 'react';
import React, { createContext, useState, useEffect, useMemo } from 'react';
import { useToast } from '@/components/ui/use-toast';
import { OpchanMessage } from '@/types/forum';
import { User, EVerificationStatus, DisplayPreference } from '@/types/identity';
import { AuthService, CryptoService, DelegationDuration } from '@/lib/services';
import { AuthResult } from '@/lib/services/AuthService';
import { WalletManager } from '@/lib/wallet';
import { DelegationManager, DelegationDuration } from '@/lib/delegation';
import { useAppKitAccount, useDisconnect, modal } from '@reown/appkit/react';
export type VerificationStatus =
@ -53,26 +53,152 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const activeAccount = isBitcoinConnected ? bitcoinAccount : ethereumAccount;
const address = activeAccount.address;
// Create service instances that persist between renders
const cryptoServiceRef = useRef(new CryptoService());
const authServiceRef = useRef(new AuthService(cryptoServiceRef.current));
// Create manager instances that persist between renders
const walletManager = useMemo(() => new WalletManager(), []);
const delegationManager = useMemo(() => new DelegationManager(), []);
// Set AppKit instance and accounts in AuthService
// Set AppKit instance and accounts in WalletManager
useEffect(() => {
if (modal) {
authServiceRef.current.setAppKit(modal);
walletManager.setAppKit(modal);
}
}, []);
}, [walletManager]);
useEffect(() => {
authServiceRef.current.setAccounts(bitcoinAccount, ethereumAccount);
}, [bitcoinAccount, ethereumAccount]);
walletManager.setAccounts(bitcoinAccount, ethereumAccount);
}, [bitcoinAccount, ethereumAccount, walletManager]);
// Helper functions for user persistence
const loadStoredUser = (): User | null => {
const storedUser = localStorage.getItem('opchan-user');
if (!storedUser) return null;
try {
const user = JSON.parse(storedUser);
const lastChecked = user.lastChecked || 0;
const expiryTime = 24 * 60 * 60 * 1000;
if (Date.now() - lastChecked < expiryTime) {
return user;
} else {
localStorage.removeItem('opchan-user');
return null;
}
} catch (e) {
console.error('Failed to parse stored user data', e);
localStorage.removeItem('opchan-user');
return null;
}
};
const saveUser = (user: User): void => {
localStorage.setItem('opchan-user', JSON.stringify(user));
};
// Helper function for ownership verification
const verifyUserOwnership = async (user: User): Promise<User> => {
if (user.walletType === 'bitcoin') {
// TODO: revert when the API is ready
// const response = await ordinalApi.getOperatorDetails(user.address);
// const hasOperators = response.has_operators;
const hasOperators = true;
return {
...user,
ordinalDetails: hasOperators
? { ordinalId: 'mock', ordinalDetails: 'Mock ordinal for testing' }
: undefined,
verificationStatus: hasOperators
? EVerificationStatus.VERIFIED_OWNER
: EVerificationStatus.VERIFIED_BASIC,
lastChecked: Date.now(),
};
} else if (user.walletType === 'ethereum') {
try {
const walletInfo = await walletManager.getWalletInfo();
const hasENS = !!walletInfo?.ensName;
const ensName = walletInfo?.ensName;
return {
...user,
ensDetails: hasENS && ensName ? { ensName } : undefined,
verificationStatus: hasENS
? EVerificationStatus.VERIFIED_OWNER
: EVerificationStatus.VERIFIED_BASIC,
lastChecked: Date.now(),
};
} catch (error) {
console.error('Error verifying ENS ownership:', error);
return {
...user,
ensDetails: undefined,
verificationStatus: EVerificationStatus.VERIFIED_BASIC,
lastChecked: Date.now(),
};
}
} else {
throw new Error('Unknown wallet type');
}
};
// Helper function for key delegation
const createUserDelegation = async (
user: User,
duration: DelegationDuration = '7days'
): Promise<boolean> => {
try {
const walletType = user.walletType;
const isAvailable = walletManager.isWalletConnected(walletType);
if (!isAvailable) {
throw new Error(
`${walletType} wallet is not available or connected. Please ensure it is connected.`
);
}
// Generate new keypair
const keypair = delegationManager.generateKeypair();
// 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,
expiryTimestamp
);
// Sign the delegation message with wallet
const signature = await walletManager.signMessage(
delegationMessage,
walletType
);
// Create and store the delegation
delegationManager.createDelegation(
user.address,
signature,
keypair.publicKey,
keypair.privateKey,
duration,
walletType
);
return true;
} catch (error) {
console.error(
`Error creating key delegation for ${user.walletType}:`,
error
);
return false;
}
};
// Sync with AppKit wallet state
useEffect(() => {
if (isConnected && address) {
// Check if we have a stored user for this address
const storedUser = authServiceRef.current.loadStoredUser();
const storedUser = loadStoredUser();
if (storedUser && storedUser.address === address) {
// Use stored user data
@ -90,35 +216,34 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
// For Ethereum wallets, try to check ENS ownership immediately
if (isEthereumConnected) {
authServiceRef.current
walletManager
.getWalletInfo()
.then(walletInfo => {
if (walletInfo?.ensName) {
const updatedUser = {
...newUser,
ensOwnership: true,
ensName: walletInfo.ensName,
ensDetails: { ensName: walletInfo.ensName },
verificationStatus: EVerificationStatus.VERIFIED_OWNER,
};
setCurrentUser(updatedUser);
setVerificationStatus('verified-owner');
authServiceRef.current.saveUser(updatedUser);
saveUser(updatedUser);
} else {
setCurrentUser(newUser);
setVerificationStatus('verified-basic');
authServiceRef.current.saveUser(newUser);
saveUser(newUser);
}
})
.catch(() => {
// Fallback to basic verification if ENS check fails
setCurrentUser(newUser);
setVerificationStatus('verified-basic');
authServiceRef.current.saveUser(newUser);
saveUser(newUser);
});
} else {
setCurrentUser(newUser);
setVerificationStatus('verified-basic');
authServiceRef.current.saveUser(newUser);
saveUser(newUser);
}
const chainName = isBitcoinConnected ? 'Bitcoin' : 'Ethereum';
@ -142,7 +267,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setCurrentUser(null);
setVerificationStatus('unverified');
}
}, [isConnected, address, isBitcoinConnected, isEthereumConnected, toast]);
}, [
isConnected,
address,
isBitcoinConnected,
isEthereumConnected,
toast,
walletManager,
]);
const { disconnect } = useDisconnect();
@ -165,13 +297,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const getVerificationStatus = (user: User): VerificationStatus => {
if (user.walletType === 'bitcoin') {
return user.ordinalDetails
? EVerificationStatus.VERIFIED_OWNER
: EVerificationStatus.VERIFIED_BASIC;
return user.ordinalDetails ? 'verified-owner' : 'verified-basic';
} else if (user.walletType === 'ethereum') {
return user.ensDetails
? EVerificationStatus.VERIFIED_OWNER
: EVerificationStatus.VERIFIED_BASIC;
return user.ensDetails ? 'verified-owner' : 'verified-basic';
}
return 'unverified';
};
@ -197,16 +325,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
description: `Checking your wallet for ${verificationType} ownership...`,
});
const result: AuthResult =
await authServiceRef.current.verifyOwnership(currentUser);
if (!result.success) {
throw new Error(result.error);
}
const updatedUser = result.user!;
const updatedUser = await verifyUserOwnership(currentUser);
setCurrentUser(updatedUser);
authServiceRef.current.saveUser(updatedUser);
saveUser(updatedUser);
// Update verification status
setVerificationStatus(getVerificationStatus(updatedUser));
@ -284,18 +405,29 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
description: `This will let you post, comment, and vote without approving each action for ${durationText}.`,
});
const result: AuthResult = await authServiceRef.current.delegateKey(
currentUser,
duration
);
if (!result.success) {
throw new Error(result.error);
const success = await createUserDelegation(currentUser, duration);
if (!success) {
throw new Error('Failed to create key delegation');
}
const updatedUser = result.user!;
// Update user with delegation info
const browserPublicKey = delegationManager.getBrowserPublicKey();
const delegationStatus = delegationManager.getDelegationStatus(
currentUser.address,
currentUser.walletType
);
const updatedUser = {
...currentUser,
browserPubKey: browserPublicKey || undefined,
delegationSignature: delegationStatus.isValid ? 'valid' : undefined,
delegationExpiry: delegationStatus.timeRemaining
? Date.now() + delegationStatus.timeRemaining
: undefined,
};
setCurrentUser(updatedUser);
authServiceRef.current.saveUser(updatedUser);
saveUser(updatedUser);
// Format date for user-friendly display
const expiryDate = new Date(updatedUser.delegationExpiry!);
@ -328,15 +460,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
};
const isDelegationValid = (): boolean => {
return cryptoServiceRef.current.isDelegationValid();
return delegationManager.isDelegationValid();
};
const delegationTimeRemaining = (): number => {
return cryptoServiceRef.current.getDelegationTimeRemaining();
return delegationManager.getDelegationTimeRemaining();
};
const clearDelegation = (): void => {
cryptoServiceRef.current.clearDelegation();
delegationManager.clearDelegation();
// Update the current user to remove delegation info
if (currentUser) {
@ -346,7 +478,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
browserPublicKey: undefined,
};
setCurrentUser(updatedUser);
authServiceRef.current.saveUser(updatedUser);
saveUser(updatedUser);
}
toast({
@ -360,10 +492,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
signMessage: async (
message: OpchanMessage
): Promise<OpchanMessage | null> => {
return cryptoServiceRef.current.signMessage(message);
return delegationManager.signMessageWithDelegatedKey(message);
},
verifyMessage: (message: OpchanMessage): boolean => {
return cryptoServiceRef.current.verifyMessage(message);
return delegationManager.verifyMessage(message);
},
};

View File

@ -19,9 +19,9 @@ import messageManager from '@/lib/waku';
import { getDataFromCache } from '@/lib/forum/transformers';
import { RelevanceCalculator } from '@/lib/forum/RelevanceCalculator';
import { UserVerificationStatus } from '@/types/forum';
import { CryptoService } from '@/lib/services';
import { DelegationManager } from '@/lib/delegation';
import { getEnsName } from '@wagmi/core';
import { config } from '@/lib/services/WalletService/config';
import { config } from '@/lib/wallet/config';
interface ForumContextType {
cells: Cell[];
@ -98,17 +98,17 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
const { toast } = useToast();
const { currentUser, isAuthenticated } = useAuth();
const cryptoService = useMemo(() => new CryptoService(), []);
const delegationManager = useMemo(() => new DelegationManager(), []);
const forumActions = useMemo(
() => new ForumActions(cryptoService),
[cryptoService]
() => new ForumActions(delegationManager),
[delegationManager]
);
// Transform message cache data to the expected types
const updateStateFromCache = useCallback(() => {
// Use the verifyMessage function from cryptoService if available
// Use the verifyMessage function from delegationManager if available
const verifyFn = isAuthenticated
? (message: OpchanMessage) => cryptoService.verifyMessage(message)
? (message: OpchanMessage) => delegationManager.verifyMessage(message)
: undefined;
// Build user verification status for relevance calculation
@ -214,7 +214,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
setComments(transformed.comments);
setUserVerificationStatus(enrichedStatus);
})();
}, [cryptoService, isAuthenticated, currentUser]);
}, [delegationManager, isAuthenticated, currentUser]);
const handleRefreshData = async () => {
setIsRefreshing(true);

View File

@ -0,0 +1,70 @@
import { useCallback, useContext, useMemo } from 'react';
import { AuthContext } from '@/contexts/AuthContext';
import { DelegationDuration } from '@/lib/delegation';
export const useDelegation = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useDelegation must be used within an AuthProvider');
}
const {
delegateKey: contextDelegateKey,
isDelegationValid: contextIsDelegationValid,
delegationTimeRemaining: contextDelegationTimeRemaining,
clearDelegation: contextClearDelegation,
isAuthenticating,
} = context;
const createDelegation = useCallback(
async (duration?: DelegationDuration): Promise<boolean> => {
return contextDelegateKey(duration);
},
[contextDelegateKey]
);
const clearDelegation = useCallback((): void => {
contextClearDelegation();
}, [contextClearDelegation]);
const delegationStatus = useMemo(() => {
const isValid = contextIsDelegationValid();
const timeRemaining = contextDelegationTimeRemaining();
return {
hasDelegation: timeRemaining > 0,
isValid,
timeRemaining: timeRemaining > 0 ? timeRemaining : undefined,
expiresAt:
timeRemaining > 0 ? new Date(Date.now() + timeRemaining) : undefined,
};
}, [contextIsDelegationValid, contextDelegationTimeRemaining]);
const formatTimeRemaining = useCallback((timeMs: number): string => {
const hours = Math.floor(timeMs / (1000 * 60 * 60));
const minutes = Math.floor((timeMs % (1000 * 60 * 60)) / (1000 * 60));
if (hours > 24) {
const days = Math.floor(hours / 24);
return `${days} day${days === 1 ? '' : 's'}`;
} else if (hours > 0) {
return `${hours}h ${minutes}m`;
} else {
return `${minutes}m`;
}
}, []);
return {
// Delegation status
delegationStatus,
isCreatingDelegation: isAuthenticating,
// Delegation actions
createDelegation,
clearDelegation,
// Utilities
formatTimeRemaining,
};
};

View File

@ -0,0 +1,48 @@
import { useCallback, useContext } from 'react';
import { AuthContext } from '@/contexts/AuthContext';
import { OpchanMessage } from '@/types/forum';
export const useMessageSigning = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useMessageSigning must be used within an AuthProvider');
}
const {
signMessage: contextSignMessage,
verifyMessage: contextVerifyMessage,
isDelegationValid,
} = context;
const signMessage = useCallback(
async (message: OpchanMessage): Promise<OpchanMessage | null> => {
// Check if we have a valid delegation before attempting to sign
if (!isDelegationValid()) {
console.warn('No valid delegation found. Cannot sign message.');
return null;
}
return contextSignMessage(message);
},
[contextSignMessage, isDelegationValid]
);
const verifyMessage = useCallback(
(message: OpchanMessage): boolean => {
return contextVerifyMessage(message);
},
[contextVerifyMessage]
);
const canSignMessages = useCallback((): boolean => {
return isDelegationValid();
}, [isDelegationValid]);
return {
// Message signing
signMessage,
verifyMessage,
canSignMessages,
};
};

54
src/hooks/useWallet.ts Normal file
View File

@ -0,0 +1,54 @@
import { useCallback, useContext } from 'react';
import { AuthContext } from '@/contexts/AuthContext';
import { modal } from '@reown/appkit/react';
export const useWallet = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useWallet must be used within an AuthProvider');
}
const {
currentUser,
isAuthenticated,
verificationStatus,
connectWallet: contextConnectWallet,
disconnectWallet: contextDisconnectWallet,
} = context;
const connect = useCallback(async (): Promise<boolean> => {
return contextConnectWallet();
}, [contextConnectWallet]);
const disconnect = useCallback((): void => {
contextDisconnectWallet();
}, [contextDisconnectWallet]);
const openModal = useCallback(async (): Promise<void> => {
if (modal) {
await modal.open();
}
}, []);
const closeModal = useCallback((): void => {
if (modal) {
modal.close();
}
}, []);
return {
// Wallet state
isConnected: isAuthenticated,
address: currentUser?.address,
walletType: currentUser?.walletType,
verificationStatus,
currentUser,
// Wallet actions
connect,
disconnect,
openModal,
closeModal,
};
};

View File

@ -1,67 +1,15 @@
/**
* CryptoService - Unified cryptographic operations
*
* 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 { OpchanMessage } from '@/types/forum';
import { UnsignedMessage } from '@/types/waku';
import { DelegationDuration, DelegationInfo, DelegationStatus } from './types';
import { DelegationStorage } from './storage';
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;
}
// Set up ed25519 with sha512
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
export type DelegationDuration = '7days' | '30days';
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(message: UnsignedMessage): OpchanMessage | null;
verifyMessage(message: OpchanMessage): boolean;
}
export class CryptoService implements CryptoServiceInterface {
private static readonly STORAGE_KEY = LOCAL_STORAGE_KEYS.KEY_DELEGATION;
export class DelegationManager {
// Duration options in hours
private static readonly DURATION_HOURS = {
'7days': 24 * 7, // 168 hours
@ -72,7 +20,7 @@ export class CryptoService implements CryptoServiceInterface {
* Get the number of hours for a given duration
*/
static getDurationHours(duration: DelegationDuration): number {
return CryptoService.DURATION_HOURS[duration];
return DelegationManager.DURATION_HOURS[duration];
}
// ============================================================================
@ -80,7 +28,7 @@ export class CryptoService implements CryptoServiceInterface {
// ============================================================================
/**
* Generates a new browser-based keypair for signing messages
* Generate a new browser-based keypair for signing messages
*/
generateKeypair(): { publicKey: string; privateKey: string } {
const privateKey = ed.utils.randomPrivateKey();
@ -96,7 +44,7 @@ export class CryptoService implements CryptoServiceInterface {
}
/**
* Creates a delegation message to be signed by the wallet
* Create a delegation message to be signed by the wallet
*/
createDelegationMessage(
browserPublicKey: string,
@ -107,11 +55,11 @@ export class CryptoService implements CryptoServiceInterface {
}
// ============================================================================
// DELEGATION MANAGEMENT
// DELEGATION LIFECYCLE
// ============================================================================
/**
* Creates and stores a delegation
* Create and store a delegation
*/
createDelegation(
walletAddress: string,
@ -121,7 +69,7 @@ export class CryptoService implements CryptoServiceInterface {
duration: DelegationDuration = '7days',
walletType: 'bitcoin' | 'ethereum'
): void {
const expiryHours = CryptoService.getDurationHours(duration);
const expiryHours = DelegationManager.getDurationHours(duration);
const expiryTimestamp = Date.now() + expiryHours * 60 * 60 * 1000;
const delegationInfo: DelegationInfo = {
@ -133,35 +81,17 @@ export class CryptoService implements CryptoServiceInterface {
walletType,
};
localStorage.setItem(
CryptoService.STORAGE_KEY,
JSON.stringify(delegationInfo)
);
DelegationStorage.store(delegationInfo);
}
/**
* Retrieves delegation information from local storage
*/
private retrieveDelegation(): DelegationInfo | null {
const delegationJson = localStorage.getItem(CryptoService.STORAGE_KEY);
if (!delegationJson) return null;
try {
return JSON.parse(delegationJson);
} catch (e) {
console.error('Failed to parse delegation information', e);
return null;
}
}
/**
* Checks if a delegation is valid
* Check if a delegation is valid
*/
isDelegationValid(
currentAddress?: string,
currentWalletType?: 'bitcoin' | 'ethereum'
): boolean {
const delegation = this.retrieveDelegation();
const delegation = DelegationStorage.retrieve();
if (!delegation) return false;
// Check if delegation has expired
@ -182,10 +112,10 @@ export class CryptoService implements CryptoServiceInterface {
}
/**
* Gets the time remaining on the current delegation
* Get the time remaining on the current delegation in milliseconds
*/
getDelegationTimeRemaining(): number {
const delegation = this.retrieveDelegation();
const delegation = DelegationStorage.retrieve();
if (!delegation) return 0;
const now = Date.now();
@ -193,19 +123,37 @@ export class CryptoService implements CryptoServiceInterface {
}
/**
* Gets the browser public key from the current delegation
* Get the browser public key from the current delegation
*/
getBrowserPublicKey(): string | null {
const delegation = this.retrieveDelegation();
const delegation = DelegationStorage.retrieve();
if (!delegation) return null;
return delegation.browserPublicKey;
}
/**
* Clears the stored delegation
* 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 {
hasDelegation,
isValid,
timeRemaining: timeRemaining > 0 ? timeRemaining : undefined,
};
}
/**
* Clear the stored delegation
*/
clearDelegation(): void {
localStorage.removeItem(CryptoService.STORAGE_KEY);
DelegationStorage.clear();
}
// ============================================================================
@ -213,10 +161,10 @@ export class CryptoService implements CryptoServiceInterface {
// ============================================================================
/**
* Signs a raw string message using the browser-generated private key
* Sign a raw string message using the browser-generated private key
*/
signRawMessage(message: string): string | null {
const delegation = this.retrieveDelegation();
const delegation = DelegationStorage.retrieve();
if (!delegation || !this.isDelegationValid()) return null;
try {
@ -232,15 +180,15 @@ export class CryptoService implements CryptoServiceInterface {
}
/**
* Signs an unsigned message with the delegated browser key
* Sign an unsigned message with the delegated browser key
*/
signMessage(message: UnsignedMessage): OpchanMessage | null {
signMessageWithDelegatedKey(message: UnsignedMessage): OpchanMessage | null {
if (!this.isDelegationValid()) {
console.error('No valid key delegation found. Cannot sign message.');
return null;
}
const delegation = this.retrieveDelegation();
const delegation = DelegationStorage.retrieve();
if (!delegation) return null;
// Create the message content to sign (without signature fields)
@ -261,7 +209,7 @@ export class CryptoService implements CryptoServiceInterface {
}
/**
* Verifies an OpchanMessage signature
* Verify an OpchanMessage signature
*/
verifyMessage(message: OpchanMessage): boolean {
// Check for required signature fields
@ -294,7 +242,7 @@ export class CryptoService implements CryptoServiceInterface {
}
/**
* Verifies a signature made with the browser key
* Verify a signature made with the browser key
*/
private verifyRawSignature(
message: string,
@ -313,3 +261,8 @@ export class CryptoService implements CryptoServiceInterface {
}
}
}
// Export singleton instance
export const delegationManager = new DelegationManager();
export * from './types';
export { DelegationStorage };

View File

@ -0,0 +1,38 @@
import { LOCAL_STORAGE_KEYS } from '@/lib/waku/constants';
import { DelegationInfo } from './types';
export class DelegationStorage {
private static readonly STORAGE_KEY = LOCAL_STORAGE_KEYS.KEY_DELEGATION;
/**
* Store delegation information in localStorage
*/
static store(delegation: DelegationInfo): void {
localStorage.setItem(
DelegationStorage.STORAGE_KEY,
JSON.stringify(delegation)
);
}
/**
* Retrieve delegation information from localStorage
*/
static retrieve(): DelegationInfo | null {
const delegationJson = localStorage.getItem(DelegationStorage.STORAGE_KEY);
if (!delegationJson) return null;
try {
return JSON.parse(delegationJson);
} catch (e) {
console.error('Failed to parse delegation information', e);
return null;
}
}
/**
* Clear stored delegation information
*/
static clear(): void {
localStorage.removeItem(DelegationStorage.STORAGE_KEY);
}
}

View File

@ -0,0 +1,19 @@
export type DelegationDuration = '7days' | '30days';
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;
}
export interface DelegationStatus {
hasDelegation: boolean;
isValid: boolean;
timeRemaining?: number;
}

View File

@ -13,7 +13,8 @@ import {
import { Cell, Comment, Post } from '@/types/forum';
import { EVerificationStatus, User } from '@/types/identity';
import { transformCell, transformComment, transformPost } from './transformers';
import { MessageService, CryptoService } from '@/lib/services';
import { MessageService } from '@/lib/services';
import { DelegationManager } from '@/lib/delegation';
type ActionResult<T> = {
success: boolean;
@ -22,12 +23,12 @@ type ActionResult<T> = {
};
export class ForumActions {
private cryptoService: CryptoService;
private delegationManager: DelegationManager;
private messageService: MessageService;
constructor(cryptoService?: CryptoService) {
this.cryptoService = cryptoService || new CryptoService();
this.messageService = new MessageService(this.cryptoService);
constructor(delegationManager?: DelegationManager) {
this.delegationManager = delegationManager || new DelegationManager();
this.messageService = new MessageService(this.delegationManager);
}
/* ------------------------------------------------------------------

View File

@ -1,358 +0,0 @@
import walletService from './WalletService';
import { UseAppKitAccountReturn } from '@reown/appkit/react';
import { AppKit } from '@reown/appkit';
import { CryptoService, DelegationDuration } from './CryptoService';
import { EVerificationStatus, User, DisplayPreference } from '@/types/identity';
import { WalletInfo } from './WalletService';
export interface AuthResult {
success: boolean;
user?: User;
error?: string;
}
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<WalletInfo | null>;
}
export class AuthService implements AuthServiceInterface {
private walletService: typeof walletService;
private cryptoService: CryptoService;
constructor(cryptoService: CryptoService) {
this.walletService = walletService;
this.cryptoService = cryptoService;
}
/**
* Set AppKit accounts for wallet service
*/
setAccounts(
bitcoinAccount: UseAppKitAccountReturn,
ethereumAccount: UseAppKitAccountReturn
) {
this.walletService.setAccounts(bitcoinAccount, ethereumAccount);
}
/**
* Set AppKit instance for wallet service
*/
setAppKit(appKit: AppKit) {
this.walletService.setAppKit(appKit);
}
/**
* Get the active wallet address
*/
private getActiveAddress(): string | null {
const isBitcoinConnected = this.walletService.isWalletAvailable('bitcoin');
const isEthereumConnected =
this.walletService.isWalletAvailable('ethereum');
if (isBitcoinConnected) {
return this.walletService.getActiveAddress('bitcoin') || null;
} else if (isEthereumConnected) {
return this.walletService.getActiveAddress('ethereum') || null;
}
return null;
}
/**
* Connect to wallet and create user
*/
async connectWallet(): Promise<AuthResult> {
try {
// Check which wallet is connected
const isBitcoinConnected =
this.walletService.isWalletAvailable('bitcoin');
const isEthereumConnected =
this.walletService.isWalletAvailable('ethereum');
if (!isBitcoinConnected && !isEthereumConnected) {
return {
success: false,
error: 'No wallet connected',
};
}
// Determine which wallet is active
const walletType = isBitcoinConnected ? 'bitcoin' : 'ethereum';
const address = this.getActiveAddress();
if (!address) {
return {
success: false,
error: 'No wallet address available',
};
}
const user: User = {
address: address,
walletType: walletType,
verificationStatus: EVerificationStatus.UNVERIFIED,
displayPreference: DisplayPreference.WALLET_ADDRESS,
lastChecked: Date.now(),
};
// Add ENS info for Ethereum wallets (if available)
if (walletType === 'ethereum') {
try {
const walletInfo = await this.walletService.getWalletInfo();
if (walletInfo?.ensName) {
user.ensDetails = {
ensName: walletInfo.ensName,
};
}
} catch (error) {
console.warn(
'Failed to resolve ENS during wallet connection:',
error
);
user.ensDetails = undefined;
}
}
return {
success: true,
user,
};
} catch (error) {
return {
success: false,
error:
error instanceof Error ? error.message : 'Failed to connect wallet',
};
}
}
/**
* Disconnect wallet and clear stored data
*/
async disconnectWallet(): Promise<void> {
// Clear any existing delegations when disconnecting
this.cryptoService.clearDelegation();
// Clear stored user data
this.clearStoredUser();
}
/**
* Verify ordinal ownership for Bitcoin users or ENS ownership for Ethereum users
*/
async verifyOwnership(user: User): Promise<AuthResult> {
try {
if (user.walletType === 'bitcoin') {
return await this.verifyBitcoinOrdinal(user);
} else if (user.walletType === 'ethereum') {
return await this.verifyEthereumENS(user);
} else {
return {
success: false,
error: 'Unknown wallet type',
};
}
} catch (error) {
return {
success: false,
error:
error instanceof Error ? error.message : 'Failed to verify ownership',
};
}
}
/**
* Verify Bitcoin Ordinal ownership
*/
private async verifyBitcoinOrdinal(user: User): Promise<AuthResult> {
// TODO: revert when the API is ready
// const response = await this.ordinalApi.getOperatorDetails(user.address);
// const hasOperators = response.has_operators;
const hasOperators = true;
const updatedUser = {
...user,
ordinalOwnership: hasOperators,
verificationStatus: hasOperators
? EVerificationStatus.VERIFIED_OWNER
: EVerificationStatus.VERIFIED_BASIC,
lastChecked: Date.now(),
};
return {
success: true,
user: updatedUser,
};
}
/**
* Verify Ethereum ENS ownership
*/
private async verifyEthereumENS(user: User): Promise<AuthResult> {
try {
// Get wallet info with ENS resolution
const walletInfo = await this.walletService.getWalletInfo();
const hasENS = !!walletInfo?.ensName;
const ensName = walletInfo?.ensName;
const updatedUser = {
...user,
ensOwnership: hasENS,
ensName: ensName,
verificationStatus: hasENS
? EVerificationStatus.VERIFIED_OWNER
: EVerificationStatus.VERIFIED_BASIC,
lastChecked: Date.now(),
};
return {
success: true,
user: updatedUser,
};
} catch (error) {
console.error('Error verifying ENS ownership:', error);
// Fall back to basic verification on error
const updatedUser = {
...user,
ensOwnership: false,
ensName: undefined,
verificationStatus: EVerificationStatus.VERIFIED_BASIC,
lastChecked: Date.now(),
};
return {
success: true,
user: updatedUser,
};
}
}
/**
* Set up key delegation for the user
*/
async delegateKey(
user: User,
duration: DelegationDuration = '7days'
): Promise<AuthResult> {
try {
const walletType = user.walletType;
const isAvailable = this.walletService.isWalletAvailable(walletType);
if (!isAvailable) {
return {
success: false,
error: `${walletType} wallet is not available or connected. Please ensure it is connected.`,
};
}
const success = await this.walletService.createKeyDelegation(
walletType,
duration
);
if (!success) {
return {
success: false,
error: 'Failed to create key delegation',
};
}
// Get delegation status to update user
const delegationStatus =
this.walletService.getDelegationStatus(walletType);
// Get the actual browser public key from the delegation
const browserPublicKey = this.cryptoService.getBrowserPublicKey();
const updatedUser = {
...user,
browserPubKey: browserPublicKey || undefined,
delegationSignature: delegationStatus.isValid ? 'valid' : undefined,
delegationExpiry: delegationStatus.timeRemaining
? Date.now() + delegationStatus.timeRemaining
: undefined,
};
return {
success: true,
user: updatedUser,
};
} catch (error) {
return {
success: false,
error:
error instanceof Error ? error.message : 'Failed to delegate key',
};
}
}
/**
* Get current wallet info
*/
async getWalletInfo(): Promise<WalletInfo | null> {
// Use the wallet service to get detailed wallet info including ENS
return await this.walletService.getWalletInfo();
}
/**
* Load user from localStorage
*/
loadStoredUser(): User | null {
const storedUser = localStorage.getItem('opchan-user');
if (!storedUser) return null;
try {
const user = JSON.parse(storedUser);
const lastChecked = user.lastChecked || 0;
const expiryTime = 24 * 60 * 60 * 1000;
if (Date.now() - lastChecked < expiryTime) {
return user;
} else {
localStorage.removeItem('opchan-user');
return null;
}
} catch (e) {
console.error('Failed to parse stored user data', e);
localStorage.removeItem('opchan-user');
return null;
}
}
/**
* Save user to localStorage
*/
saveUser(user: User): void {
localStorage.setItem('opchan-user', JSON.stringify(user));
}
/**
* Clear stored user data
*/
clearStoredUser(): void {
localStorage.removeItem('opchan-user');
}
}

View File

@ -1,6 +1,6 @@
import { OpchanMessage } from '@/types/forum';
import { UnsignedMessage } from '@/types/waku';
import { CryptoService } from './CryptoService';
import { DelegationManager } from '@/lib/delegation';
import messageManager from '@/lib/waku';
export interface MessageResult {
@ -15,10 +15,10 @@ export interface MessageServiceInterface {
}
export class MessageService implements MessageServiceInterface {
private cryptoService: CryptoService;
private delegationManager: DelegationManager;
constructor(cryptoService: CryptoService) {
this.cryptoService = cryptoService;
constructor(delegationManager: DelegationManager) {
this.delegationManager = delegationManager;
}
/**
@ -26,12 +26,13 @@ export class MessageService implements MessageServiceInterface {
*/
async sendMessage(message: UnsignedMessage): Promise<MessageResult> {
try {
const signedMessage = this.cryptoService.signMessage(message);
const signedMessage =
this.delegationManager.signMessageWithDelegatedKey(message);
if (!signedMessage) {
// Check if delegation exists but is expired
const isDelegationExpired =
this.cryptoService.isDelegationValid() === false;
this.delegationManager.isDelegationValid() === false;
return {
success: false,
@ -81,6 +82,6 @@ export class MessageService implements MessageServiceInterface {
* Verify a message signature
*/
verifyMessage(message: OpchanMessage): boolean {
return this.cryptoService.verifyMessage(message);
return this.delegationManager.verifyMessage(message);
}
}

View File

@ -1,252 +0,0 @@
import { UseAppKitAccountReturn } from '@reown/appkit/react';
import {
CryptoService,
DelegationDuration,
} from '../CryptoService';
import { AppKit } from '@reown/appkit';
import { getEnsName } from '@wagmi/core';
import { ChainNamespace } from '@reown/appkit-common';
import { config } from './config';
import { Provider } from '@reown/appkit-controllers';
export interface WalletInfo {
address: string;
walletType: 'bitcoin' | 'ethereum';
ensName?: string;
isConnected: boolean;
}
class WalletService {
private cryptoService: CryptoService;
private bitcoinAccount?: UseAppKitAccountReturn;
private ethereumAccount?: UseAppKitAccountReturn;
private appKit?: AppKit;
constructor() {
this.cryptoService = new CryptoService();
}
/**
* Set account references from AppKit hooks
*/
setAccounts(
bitcoinAccount: UseAppKitAccountReturn,
ethereumAccount: UseAppKitAccountReturn
) {
this.bitcoinAccount = bitcoinAccount;
this.ethereumAccount = ethereumAccount;
}
/**
* Set the AppKit instance for accessing adapters
*/
setAppKit(appKit: AppKit) {
this.appKit = appKit;
}
/**
* Check if a wallet type is available and connected
*/
isWalletAvailable(walletType: 'bitcoin' | 'ethereum'): boolean {
if (walletType === 'bitcoin') {
return this.bitcoinAccount?.isConnected ?? false;
} else {
return this.ethereumAccount?.isConnected ?? false;
}
}
/**
* Get the active account based on wallet type
*/
private getActiveAccount(
walletType: 'bitcoin' | 'ethereum'
): UseAppKitAccountReturn | undefined {
return walletType === 'bitcoin'
? this.bitcoinAccount
: this.ethereumAccount;
}
/**
* Get the active address for a given wallet type
*/
getActiveAddress(walletType: 'bitcoin' | 'ethereum'): string | undefined {
const account = this.getActiveAccount(walletType);
return account?.address;
}
/**
* Get the appropriate namespace for the wallet type
*/
private getNamespace(walletType: 'bitcoin' | 'ethereum'): ChainNamespace {
return walletType === 'bitcoin' ? 'bip122' : 'eip155';
}
/**
* Sign a message using the appropriate adapter
*/
async signMessage(
messageBytes: Uint8Array,
walletType: 'bitcoin' | 'ethereum'
): Promise<string> {
if (!this.appKit) {
throw new Error('AppKit instance not set. Call setAppKit() first.');
}
const account = this.getActiveAccount(walletType);
if (!account?.address) {
throw new Error(`No ${walletType} wallet connected`);
}
const namespace = this.getNamespace(walletType);
// Convert message bytes to string for signing
const messageString = new TextDecoder().decode(messageBytes);
try {
// Access the adapter through the appKit instance
// The adapter is available through the appKit's chainAdapters property
const adapter = this.appKit.chainAdapters?.[namespace];
if (!adapter) {
throw new Error(`No adapter found for namespace: ${namespace}`);
}
// Get the provider for the current connection
const provider = this.appKit.getProvider(namespace);
if (!provider) {
throw new Error(`No provider found for namespace: ${namespace}`);
}
// Call the adapter's signMessage method
const result = await adapter.signMessage({
message: messageString,
address: account.address,
provider: provider as Provider,
});
return result.signature;
} catch (error) {
console.error(`Error signing message with ${walletType} wallet:`, error);
throw new Error(
`Failed to sign message with ${walletType} wallet: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
/**
* Create a key delegation for the connected wallet
*/
async createKeyDelegation(
walletType: 'bitcoin' | 'ethereum',
duration: DelegationDuration = '7days'
): Promise<boolean> {
try {
const account = this.getActiveAccount(walletType);
if (!account?.address) {
throw new Error(`No ${walletType} wallet connected`);
}
// Generate a new browser keypair
const keypair = this.cryptoService.generateKeypair();
// Create delegation message with expiry
const expiryHours = CryptoService.getDurationHours(duration);
const expiryTimestamp = Date.now() + expiryHours * 60 * 60 * 1000;
const delegationMessage = this.cryptoService.createDelegationMessage(
keypair.publicKey,
account.address,
expiryTimestamp
);
const messageBytes = new TextEncoder().encode(delegationMessage);
// Sign the delegation message
const signature = await this.signMessage(messageBytes, walletType);
// Create and store the delegation
this.cryptoService.createDelegation(
account.address,
signature,
keypair.publicKey,
keypair.privateKey,
duration,
walletType
);
return true;
} catch (error) {
console.error(`Error creating key delegation for ${walletType}:`, error);
return false;
}
}
/**
* Get delegation status for the connected wallet
*/
getDelegationStatus(walletType: 'bitcoin' | 'ethereum'): {
hasDelegation: boolean;
isValid: boolean;
timeRemaining?: number;
} {
const account = this.getActiveAccount(walletType);
const currentAddress = account?.address;
const hasDelegation = this.cryptoService.getBrowserPublicKey() !== null;
const isValid = this.cryptoService.isDelegationValid(
currentAddress,
walletType
);
const timeRemaining = this.cryptoService.getDelegationTimeRemaining();
return {
hasDelegation,
isValid,
timeRemaining: timeRemaining > 0 ? timeRemaining : undefined,
};
}
/**
* Clear delegation for the connected wallet
*/
clearDelegation(): void {
this.cryptoService.clearDelegation();
}
/**
* Get wallet connection info with ENS resolution for Ethereum
*/
async getWalletInfo(): Promise<WalletInfo | null> {
if (this.bitcoinAccount?.isConnected) {
return {
address: this.bitcoinAccount.address as string,
walletType: 'bitcoin',
isConnected: true,
};
} else if (this.ethereumAccount?.isConnected) {
// Use Wagmi to resolve ENS name
let ensName: string | undefined;
try {
const resolvedName = await getEnsName(config, {
address: this.ethereumAccount.address as `0x${string}`,
});
ensName = resolvedName || undefined;
} catch (error) {
console.warn('Failed to resolve ENS name:', error);
// Continue without ENS name
}
return {
address: this.ethereumAccount.address as string,
walletType: 'ethereum',
ensName,
isConnected: true,
};
}
return null;
}
}
const walletService = new WalletService();
export default walletService;

View File

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

View File

@ -1,5 +1,5 @@
import { OpchanMessage } from '@/types/forum';
import { CryptoService } from '@/lib/services/CryptoService';
import { DelegationManager } from '@/lib/delegation';
/**
* Type for potential message objects with partial structure
@ -18,10 +18,10 @@ interface PartialMessage {
* Ensures all messages have valid signatures and browserPubKey
*/
export class MessageValidator {
private cryptoService: CryptoService;
private delegationManager: DelegationManager;
constructor(cryptoService?: CryptoService) {
this.cryptoService = cryptoService || new CryptoService();
constructor(delegationManager?: DelegationManager) {
this.delegationManager = delegationManager || new DelegationManager();
}
/**
@ -34,7 +34,7 @@ export class MessageValidator {
}
// Verify signature - we know it's safe to cast here since hasRequiredFields passed
return this.cryptoService.verifyMessage(message as OpchanMessage);
return this.delegationManager.verifyMessage(message as OpchanMessage);
}
/**
@ -123,7 +123,7 @@ export class MessageValidator {
continue;
}
if (!this.cryptoService.verifyMessage(message as OpchanMessage)) {
if (!this.delegationManager.verifyMessage(message as OpchanMessage)) {
invalidCount.invalidSignature++;
continue;
}
@ -166,7 +166,7 @@ export class MessageValidator {
);
}
if (!this.cryptoService.verifyMessage(message as OpchanMessage)) {
if (!this.delegationManager.verifyMessage(message as OpchanMessage)) {
const partialMsg = message as PartialMessage;
throw new Error(
`Message validation failed: Invalid signature (messageId: ${partialMsg?.id})`
@ -242,7 +242,7 @@ export class MessageValidator {
}
if (hasRequiredFields) {
hasValidSignature = this.cryptoService.verifyMessage(
hasValidSignature = this.delegationManager.verifyMessage(
message as OpchanMessage
);
if (!hasValidSignature) {

196
src/lib/wallet/index.ts Normal file
View File

@ -0,0 +1,196 @@
import { UseAppKitAccountReturn } from '@reown/appkit/react';
import { AppKit } from '@reown/appkit';
import { getEnsName } from '@wagmi/core';
import { ChainNamespace } from '@reown/appkit-common';
import { config } from './config';
import { Provider } from '@reown/appkit-controllers';
import { WalletInfo, ActiveWallet } from './types';
export class WalletManager {
private appKit?: AppKit;
private bitcoinAccount?: UseAppKitAccountReturn;
private ethereumAccount?: UseAppKitAccountReturn;
/**
* Set the AppKit instance for accessing adapters
*/
setAppKit(appKit: AppKit): void {
this.appKit = appKit;
}
/**
* Set account references from AppKit hooks
*/
setAccounts(
bitcoinAccount: UseAppKitAccountReturn,
ethereumAccount: UseAppKitAccountReturn
): void {
this.bitcoinAccount = bitcoinAccount;
this.ethereumAccount = ethereumAccount;
}
/**
* Get the currently active wallet (Bitcoin takes priority)
*/
getActiveWallet(): ActiveWallet | null {
if (this.bitcoinAccount?.isConnected && this.bitcoinAccount.address) {
return {
type: 'bitcoin',
address: this.bitcoinAccount.address,
isConnected: true,
};
}
if (this.ethereumAccount?.isConnected && this.ethereumAccount.address) {
return {
type: 'ethereum',
address: this.ethereumAccount.address,
isConnected: true,
};
}
return null;
}
/**
* Check if any wallet is connected
*/
isConnected(): boolean {
return (
(this.bitcoinAccount?.isConnected ?? false) ||
(this.ethereumAccount?.isConnected ?? false)
);
}
/**
* Check if a specific wallet type is connected
*/
isWalletConnected(walletType: 'bitcoin' | 'ethereum'): boolean {
const account =
walletType === 'bitcoin' ? this.bitcoinAccount : this.ethereumAccount;
return account?.isConnected ?? false;
}
/**
* Get address for a specific wallet type
*/
getAddress(walletType: 'bitcoin' | 'ethereum'): string | undefined {
const account =
walletType === 'bitcoin' ? this.bitcoinAccount : this.ethereumAccount;
return account?.address;
}
/**
* Get the appropriate namespace for the wallet type
*/
private getNamespace(walletType: 'bitcoin' | 'ethereum'): ChainNamespace {
return walletType === 'bitcoin' ? 'bip122' : 'eip155';
}
/**
* Sign a message using the appropriate wallet adapter
*/
async signMessage(
message: string,
walletType: 'bitcoin' | 'ethereum'
): Promise<string> {
if (!this.appKit) {
throw new Error('AppKit instance not set. Call setAppKit() first.');
}
const account =
walletType === 'bitcoin' ? this.bitcoinAccount : this.ethereumAccount;
if (!account?.address) {
throw new Error(`No ${walletType} wallet connected`);
}
const namespace = this.getNamespace(walletType);
try {
// Access the adapter through the appKit instance
const adapter = this.appKit.chainAdapters?.[namespace];
if (!adapter) {
throw new Error(`No adapter found for namespace: ${namespace}`);
}
// Get the provider for the current connection
const provider = this.appKit.getProvider(namespace);
if (!provider) {
throw new Error(`No provider found for namespace: ${namespace}`);
}
// Call the adapter's signMessage method
const result = await adapter.signMessage({
message,
address: account.address,
provider: provider as Provider,
});
return result.signature;
} catch (error) {
console.error(`Error signing message with ${walletType} wallet:`, error);
throw new Error(
`Failed to sign message with ${walletType} wallet: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
/**
* Get comprehensive wallet info including ENS resolution for Ethereum
*/
async getWalletInfo(): Promise<WalletInfo | null> {
if (this.bitcoinAccount?.isConnected) {
return {
address: this.bitcoinAccount.address as string,
walletType: 'bitcoin',
isConnected: true,
};
}
if (this.ethereumAccount?.isConnected) {
const address = this.ethereumAccount.address as string;
// Try to resolve ENS name
let ensName: string | undefined;
try {
const resolvedName = await getEnsName(config, {
address: address as `0x${string}`,
});
ensName = resolvedName || undefined;
} catch (error) {
console.warn('Failed to resolve ENS name:', error);
}
return {
address,
walletType: 'ethereum',
ensName,
isConnected: true,
};
}
return null;
}
/**
* Resolve ENS name for an Ethereum address
*/
async resolveENS(address: string): Promise<string | null> {
try {
const ensName = await getEnsName(config, {
address: address as `0x${string}`,
});
return ensName || null;
} catch (error) {
console.warn('Failed to resolve ENS name:', error);
return null;
}
}
}
// Export singleton instance
export const walletManager = new WalletManager();
export * from './types';
export * from './config';

12
src/lib/wallet/types.ts Normal file
View File

@ -0,0 +1,12 @@
export interface WalletInfo {
address: string;
walletType: 'bitcoin' | 'ethereum';
ensName?: string;
isConnected: boolean;
}
export interface ActiveWallet {
type: 'bitcoin' | 'ethereum';
address: string;
isConnected: boolean;
}