mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-02 12:53:10 +00:00
chore: move out of services into hooks + simplified classes
This commit is contained in:
parent
9ade3efb10
commit
dc4468078e
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
70
src/hooks/useDelegation.ts
Normal file
70
src/hooks/useDelegation.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
48
src/hooks/useMessageSigning.ts
Normal file
48
src/hooks/useMessageSigning.ts
Normal 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
54
src/hooks/useWallet.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@ -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 };
|
||||
38
src/lib/delegation/storage.ts
Normal file
38
src/lib/delegation/storage.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
19
src/lib/delegation/types.ts
Normal file
19
src/lib/delegation/types.ts
Normal 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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -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';
|
||||
|
||||
@ -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
196
src/lib/wallet/index.ts
Normal 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
12
src/lib/wallet/types.ts
Normal 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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user