mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-03 21:33:09 +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 NotFound from './pages/NotFound';
|
||||||
import Dashboard from './pages/Dashboard';
|
import Dashboard from './pages/Dashboard';
|
||||||
import Index from './pages/Index';
|
import Index from './pages/Index';
|
||||||
import { appkitConfig } from './lib/services/WalletService/config';
|
import { appkitConfig } from './lib/wallet/config';
|
||||||
import { WagmiProvider } from 'wagmi';
|
import { WagmiProvider } from 'wagmi';
|
||||||
import { config } from './lib/services/WalletService/config';
|
import { config } from './lib/wallet/config';
|
||||||
import { AppKitProvider } from '@reown/appkit/react';
|
import { AppKitProvider } from '@reown/appkit/react';
|
||||||
|
|
||||||
// Create a client
|
// Create a client
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { Badge } from '@/components/ui/badge';
|
|||||||
import { Shield, Crown } from 'lucide-react';
|
import { Shield, Crown } from 'lucide-react';
|
||||||
import { UserVerificationStatus } from '@/types/forum';
|
import { UserVerificationStatus } from '@/types/forum';
|
||||||
import { getEnsName } from '@wagmi/core';
|
import { getEnsName } from '@wagmi/core';
|
||||||
import { config } from '@/lib/services/WalletService/config';
|
import { config } from '@/lib/wallet/config';
|
||||||
import { OrdinalAPI } from '@/lib/services/Ordinal';
|
import { OrdinalAPI } from '@/lib/services/Ordinal';
|
||||||
|
|
||||||
interface AuthorDisplayProps {
|
interface AuthorDisplayProps {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { Button } from './button';
|
import { Button } from './button';
|
||||||
import { useAuth } from '@/contexts/useAuth';
|
import { useAuth } from '@/contexts/useAuth';
|
||||||
import { CheckCircle, AlertCircle, Trash2 } from 'lucide-react';
|
import { CheckCircle, AlertCircle, Trash2 } from 'lucide-react';
|
||||||
import { DelegationDuration } from '@/lib/services/CryptoService';
|
import { DelegationDuration } from '@/lib/delegation';
|
||||||
|
|
||||||
interface DelegationStepProps {
|
interface DelegationStepProps {
|
||||||
onComplete: () => void;
|
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 { useToast } from '@/components/ui/use-toast';
|
||||||
import { OpchanMessage } from '@/types/forum';
|
import { OpchanMessage } from '@/types/forum';
|
||||||
import { User, EVerificationStatus, DisplayPreference } from '@/types/identity';
|
import { User, EVerificationStatus, DisplayPreference } from '@/types/identity';
|
||||||
import { AuthService, CryptoService, DelegationDuration } from '@/lib/services';
|
import { WalletManager } from '@/lib/wallet';
|
||||||
import { AuthResult } from '@/lib/services/AuthService';
|
import { DelegationManager, DelegationDuration } from '@/lib/delegation';
|
||||||
import { useAppKitAccount, useDisconnect, modal } from '@reown/appkit/react';
|
import { useAppKitAccount, useDisconnect, modal } from '@reown/appkit/react';
|
||||||
|
|
||||||
export type VerificationStatus =
|
export type VerificationStatus =
|
||||||
@ -53,26 +53,152 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const activeAccount = isBitcoinConnected ? bitcoinAccount : ethereumAccount;
|
const activeAccount = isBitcoinConnected ? bitcoinAccount : ethereumAccount;
|
||||||
const address = activeAccount.address;
|
const address = activeAccount.address;
|
||||||
|
|
||||||
// Create service instances that persist between renders
|
// Create manager instances that persist between renders
|
||||||
const cryptoServiceRef = useRef(new CryptoService());
|
const walletManager = useMemo(() => new WalletManager(), []);
|
||||||
const authServiceRef = useRef(new AuthService(cryptoServiceRef.current));
|
const delegationManager = useMemo(() => new DelegationManager(), []);
|
||||||
|
|
||||||
// Set AppKit instance and accounts in AuthService
|
// Set AppKit instance and accounts in WalletManager
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (modal) {
|
if (modal) {
|
||||||
authServiceRef.current.setAppKit(modal);
|
walletManager.setAppKit(modal);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [walletManager]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
authServiceRef.current.setAccounts(bitcoinAccount, ethereumAccount);
|
walletManager.setAccounts(bitcoinAccount, ethereumAccount);
|
||||||
}, [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
|
// Sync with AppKit wallet state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isConnected && address) {
|
if (isConnected && address) {
|
||||||
// Check if we have a stored user for this address
|
// Check if we have a stored user for this address
|
||||||
const storedUser = authServiceRef.current.loadStoredUser();
|
const storedUser = loadStoredUser();
|
||||||
|
|
||||||
if (storedUser && storedUser.address === address) {
|
if (storedUser && storedUser.address === address) {
|
||||||
// Use stored user data
|
// Use stored user data
|
||||||
@ -90,35 +216,34 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
// For Ethereum wallets, try to check ENS ownership immediately
|
// For Ethereum wallets, try to check ENS ownership immediately
|
||||||
if (isEthereumConnected) {
|
if (isEthereumConnected) {
|
||||||
authServiceRef.current
|
walletManager
|
||||||
.getWalletInfo()
|
.getWalletInfo()
|
||||||
.then(walletInfo => {
|
.then(walletInfo => {
|
||||||
if (walletInfo?.ensName) {
|
if (walletInfo?.ensName) {
|
||||||
const updatedUser = {
|
const updatedUser = {
|
||||||
...newUser,
|
...newUser,
|
||||||
ensOwnership: true,
|
ensDetails: { ensName: walletInfo.ensName },
|
||||||
ensName: walletInfo.ensName,
|
|
||||||
verificationStatus: EVerificationStatus.VERIFIED_OWNER,
|
verificationStatus: EVerificationStatus.VERIFIED_OWNER,
|
||||||
};
|
};
|
||||||
setCurrentUser(updatedUser);
|
setCurrentUser(updatedUser);
|
||||||
setVerificationStatus('verified-owner');
|
setVerificationStatus('verified-owner');
|
||||||
authServiceRef.current.saveUser(updatedUser);
|
saveUser(updatedUser);
|
||||||
} else {
|
} else {
|
||||||
setCurrentUser(newUser);
|
setCurrentUser(newUser);
|
||||||
setVerificationStatus('verified-basic');
|
setVerificationStatus('verified-basic');
|
||||||
authServiceRef.current.saveUser(newUser);
|
saveUser(newUser);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// Fallback to basic verification if ENS check fails
|
// Fallback to basic verification if ENS check fails
|
||||||
setCurrentUser(newUser);
|
setCurrentUser(newUser);
|
||||||
setVerificationStatus('verified-basic');
|
setVerificationStatus('verified-basic');
|
||||||
authServiceRef.current.saveUser(newUser);
|
saveUser(newUser);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setCurrentUser(newUser);
|
setCurrentUser(newUser);
|
||||||
setVerificationStatus('verified-basic');
|
setVerificationStatus('verified-basic');
|
||||||
authServiceRef.current.saveUser(newUser);
|
saveUser(newUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
const chainName = isBitcoinConnected ? 'Bitcoin' : 'Ethereum';
|
const chainName = isBitcoinConnected ? 'Bitcoin' : 'Ethereum';
|
||||||
@ -142,7 +267,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setCurrentUser(null);
|
setCurrentUser(null);
|
||||||
setVerificationStatus('unverified');
|
setVerificationStatus('unverified');
|
||||||
}
|
}
|
||||||
}, [isConnected, address, isBitcoinConnected, isEthereumConnected, toast]);
|
}, [
|
||||||
|
isConnected,
|
||||||
|
address,
|
||||||
|
isBitcoinConnected,
|
||||||
|
isEthereumConnected,
|
||||||
|
toast,
|
||||||
|
walletManager,
|
||||||
|
]);
|
||||||
|
|
||||||
const { disconnect } = useDisconnect();
|
const { disconnect } = useDisconnect();
|
||||||
|
|
||||||
@ -165,13 +297,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
const getVerificationStatus = (user: User): VerificationStatus => {
|
const getVerificationStatus = (user: User): VerificationStatus => {
|
||||||
if (user.walletType === 'bitcoin') {
|
if (user.walletType === 'bitcoin') {
|
||||||
return user.ordinalDetails
|
return user.ordinalDetails ? 'verified-owner' : 'verified-basic';
|
||||||
? EVerificationStatus.VERIFIED_OWNER
|
|
||||||
: EVerificationStatus.VERIFIED_BASIC;
|
|
||||||
} else if (user.walletType === 'ethereum') {
|
} else if (user.walletType === 'ethereum') {
|
||||||
return user.ensDetails
|
return user.ensDetails ? 'verified-owner' : 'verified-basic';
|
||||||
? EVerificationStatus.VERIFIED_OWNER
|
|
||||||
: EVerificationStatus.VERIFIED_BASIC;
|
|
||||||
}
|
}
|
||||||
return 'unverified';
|
return 'unverified';
|
||||||
};
|
};
|
||||||
@ -197,16 +325,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
description: `Checking your wallet for ${verificationType} ownership...`,
|
description: `Checking your wallet for ${verificationType} ownership...`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result: AuthResult =
|
const updatedUser = await verifyUserOwnership(currentUser);
|
||||||
await authServiceRef.current.verifyOwnership(currentUser);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedUser = result.user!;
|
|
||||||
setCurrentUser(updatedUser);
|
setCurrentUser(updatedUser);
|
||||||
authServiceRef.current.saveUser(updatedUser);
|
saveUser(updatedUser);
|
||||||
|
|
||||||
// Update verification status
|
// Update verification status
|
||||||
setVerificationStatus(getVerificationStatus(updatedUser));
|
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}.`,
|
description: `This will let you post, comment, and vote without approving each action for ${durationText}.`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result: AuthResult = await authServiceRef.current.delegateKey(
|
const success = await createUserDelegation(currentUser, duration);
|
||||||
currentUser,
|
if (!success) {
|
||||||
duration
|
throw new Error('Failed to create key delegation');
|
||||||
);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
setCurrentUser(updatedUser);
|
||||||
authServiceRef.current.saveUser(updatedUser);
|
saveUser(updatedUser);
|
||||||
|
|
||||||
// Format date for user-friendly display
|
// Format date for user-friendly display
|
||||||
const expiryDate = new Date(updatedUser.delegationExpiry!);
|
const expiryDate = new Date(updatedUser.delegationExpiry!);
|
||||||
@ -328,15 +460,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isDelegationValid = (): boolean => {
|
const isDelegationValid = (): boolean => {
|
||||||
return cryptoServiceRef.current.isDelegationValid();
|
return delegationManager.isDelegationValid();
|
||||||
};
|
};
|
||||||
|
|
||||||
const delegationTimeRemaining = (): number => {
|
const delegationTimeRemaining = (): number => {
|
||||||
return cryptoServiceRef.current.getDelegationTimeRemaining();
|
return delegationManager.getDelegationTimeRemaining();
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearDelegation = (): void => {
|
const clearDelegation = (): void => {
|
||||||
cryptoServiceRef.current.clearDelegation();
|
delegationManager.clearDelegation();
|
||||||
|
|
||||||
// Update the current user to remove delegation info
|
// Update the current user to remove delegation info
|
||||||
if (currentUser) {
|
if (currentUser) {
|
||||||
@ -346,7 +478,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
browserPublicKey: undefined,
|
browserPublicKey: undefined,
|
||||||
};
|
};
|
||||||
setCurrentUser(updatedUser);
|
setCurrentUser(updatedUser);
|
||||||
authServiceRef.current.saveUser(updatedUser);
|
saveUser(updatedUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@ -360,10 +492,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
signMessage: async (
|
signMessage: async (
|
||||||
message: OpchanMessage
|
message: OpchanMessage
|
||||||
): Promise<OpchanMessage | null> => {
|
): Promise<OpchanMessage | null> => {
|
||||||
return cryptoServiceRef.current.signMessage(message);
|
return delegationManager.signMessageWithDelegatedKey(message);
|
||||||
},
|
},
|
||||||
verifyMessage: (message: OpchanMessage): boolean => {
|
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 { getDataFromCache } from '@/lib/forum/transformers';
|
||||||
import { RelevanceCalculator } from '@/lib/forum/RelevanceCalculator';
|
import { RelevanceCalculator } from '@/lib/forum/RelevanceCalculator';
|
||||||
import { UserVerificationStatus } from '@/types/forum';
|
import { UserVerificationStatus } from '@/types/forum';
|
||||||
import { CryptoService } from '@/lib/services';
|
import { DelegationManager } from '@/lib/delegation';
|
||||||
import { getEnsName } from '@wagmi/core';
|
import { getEnsName } from '@wagmi/core';
|
||||||
import { config } from '@/lib/services/WalletService/config';
|
import { config } from '@/lib/wallet/config';
|
||||||
|
|
||||||
interface ForumContextType {
|
interface ForumContextType {
|
||||||
cells: Cell[];
|
cells: Cell[];
|
||||||
@ -98,17 +98,17 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { currentUser, isAuthenticated } = useAuth();
|
const { currentUser, isAuthenticated } = useAuth();
|
||||||
|
|
||||||
const cryptoService = useMemo(() => new CryptoService(), []);
|
const delegationManager = useMemo(() => new DelegationManager(), []);
|
||||||
const forumActions = useMemo(
|
const forumActions = useMemo(
|
||||||
() => new ForumActions(cryptoService),
|
() => new ForumActions(delegationManager),
|
||||||
[cryptoService]
|
[delegationManager]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Transform message cache data to the expected types
|
// Transform message cache data to the expected types
|
||||||
const updateStateFromCache = useCallback(() => {
|
const updateStateFromCache = useCallback(() => {
|
||||||
// Use the verifyMessage function from cryptoService if available
|
// Use the verifyMessage function from delegationManager if available
|
||||||
const verifyFn = isAuthenticated
|
const verifyFn = isAuthenticated
|
||||||
? (message: OpchanMessage) => cryptoService.verifyMessage(message)
|
? (message: OpchanMessage) => delegationManager.verifyMessage(message)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// Build user verification status for relevance calculation
|
// Build user verification status for relevance calculation
|
||||||
@ -214,7 +214,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setComments(transformed.comments);
|
setComments(transformed.comments);
|
||||||
setUserVerificationStatus(enrichedStatus);
|
setUserVerificationStatus(enrichedStatus);
|
||||||
})();
|
})();
|
||||||
}, [cryptoService, isAuthenticated, currentUser]);
|
}, [delegationManager, isAuthenticated, currentUser]);
|
||||||
|
|
||||||
const handleRefreshData = async () => {
|
const handleRefreshData = async () => {
|
||||||
setIsRefreshing(true);
|
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 * as ed from '@noble/ed25519';
|
||||||
import { sha512 } from '@noble/hashes/sha512';
|
import { sha512 } from '@noble/hashes/sha512';
|
||||||
import { bytesToHex, hexToBytes } from '@/lib/utils';
|
import { bytesToHex, hexToBytes } from '@/lib/utils';
|
||||||
import { LOCAL_STORAGE_KEYS } from '@/lib/waku/constants';
|
|
||||||
import { OpchanMessage } from '@/types/forum';
|
import { OpchanMessage } from '@/types/forum';
|
||||||
import { UnsignedMessage } from '@/types/waku';
|
import { UnsignedMessage } from '@/types/waku';
|
||||||
|
import { DelegationDuration, DelegationInfo, DelegationStatus } from './types';
|
||||||
|
import { DelegationStorage } from './storage';
|
||||||
|
|
||||||
export interface DelegationSignature {
|
// Set up ed25519 with sha512
|
||||||
signature: string; // Signature from wallet
|
|
||||||
expiryTimestamp: number; // When this delegation expires
|
|
||||||
browserPublicKey: string; // Browser-generated public key that was delegated to
|
|
||||||
walletAddress: string; // Wallet address that signed the delegation
|
|
||||||
walletType: 'bitcoin' | 'ethereum'; // Type of wallet that created the delegation
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DelegationInfo extends DelegationSignature {
|
|
||||||
browserPrivateKey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
|
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
|
||||||
|
|
||||||
export type DelegationDuration = '7days' | '30days';
|
export class DelegationManager {
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Duration options in hours
|
// Duration options in hours
|
||||||
private static readonly DURATION_HOURS = {
|
private static readonly DURATION_HOURS = {
|
||||||
'7days': 24 * 7, // 168 hours
|
'7days': 24 * 7, // 168 hours
|
||||||
@ -72,7 +20,7 @@ export class CryptoService implements CryptoServiceInterface {
|
|||||||
* Get the number of hours for a given duration
|
* Get the number of hours for a given duration
|
||||||
*/
|
*/
|
||||||
static getDurationHours(duration: DelegationDuration): number {
|
static getDurationHours(duration: DelegationDuration): number {
|
||||||
return 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 } {
|
generateKeypair(): { publicKey: string; privateKey: string } {
|
||||||
const privateKey = ed.utils.randomPrivateKey();
|
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(
|
createDelegationMessage(
|
||||||
browserPublicKey: string,
|
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(
|
createDelegation(
|
||||||
walletAddress: string,
|
walletAddress: string,
|
||||||
@ -121,7 +69,7 @@ export class CryptoService implements CryptoServiceInterface {
|
|||||||
duration: DelegationDuration = '7days',
|
duration: DelegationDuration = '7days',
|
||||||
walletType: 'bitcoin' | 'ethereum'
|
walletType: 'bitcoin' | 'ethereum'
|
||||||
): void {
|
): void {
|
||||||
const expiryHours = CryptoService.getDurationHours(duration);
|
const expiryHours = DelegationManager.getDurationHours(duration);
|
||||||
const expiryTimestamp = Date.now() + expiryHours * 60 * 60 * 1000;
|
const expiryTimestamp = Date.now() + expiryHours * 60 * 60 * 1000;
|
||||||
|
|
||||||
const delegationInfo: DelegationInfo = {
|
const delegationInfo: DelegationInfo = {
|
||||||
@ -133,35 +81,17 @@ export class CryptoService implements CryptoServiceInterface {
|
|||||||
walletType,
|
walletType,
|
||||||
};
|
};
|
||||||
|
|
||||||
localStorage.setItem(
|
DelegationStorage.store(delegationInfo);
|
||||||
CryptoService.STORAGE_KEY,
|
|
||||||
JSON.stringify(delegationInfo)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves delegation information from local storage
|
* Check if a delegation is valid
|
||||||
*/
|
|
||||||
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
|
|
||||||
*/
|
*/
|
||||||
isDelegationValid(
|
isDelegationValid(
|
||||||
currentAddress?: string,
|
currentAddress?: string,
|
||||||
currentWalletType?: 'bitcoin' | 'ethereum'
|
currentWalletType?: 'bitcoin' | 'ethereum'
|
||||||
): boolean {
|
): boolean {
|
||||||
const delegation = this.retrieveDelegation();
|
const delegation = DelegationStorage.retrieve();
|
||||||
if (!delegation) return false;
|
if (!delegation) return false;
|
||||||
|
|
||||||
// Check if delegation has expired
|
// 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 {
|
getDelegationTimeRemaining(): number {
|
||||||
const delegation = this.retrieveDelegation();
|
const delegation = DelegationStorage.retrieve();
|
||||||
if (!delegation) return 0;
|
if (!delegation) return 0;
|
||||||
|
|
||||||
const now = Date.now();
|
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 {
|
getBrowserPublicKey(): string | null {
|
||||||
const delegation = this.retrieveDelegation();
|
const delegation = DelegationStorage.retrieve();
|
||||||
if (!delegation) return null;
|
if (!delegation) return null;
|
||||||
return delegation.browserPublicKey;
|
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 {
|
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 {
|
signRawMessage(message: string): string | null {
|
||||||
const delegation = this.retrieveDelegation();
|
const delegation = DelegationStorage.retrieve();
|
||||||
if (!delegation || !this.isDelegationValid()) return null;
|
if (!delegation || !this.isDelegationValid()) return null;
|
||||||
|
|
||||||
try {
|
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()) {
|
if (!this.isDelegationValid()) {
|
||||||
console.error('No valid key delegation found. Cannot sign message.');
|
console.error('No valid key delegation found. Cannot sign message.');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const delegation = this.retrieveDelegation();
|
const delegation = DelegationStorage.retrieve();
|
||||||
if (!delegation) return null;
|
if (!delegation) return null;
|
||||||
|
|
||||||
// Create the message content to sign (without signature fields)
|
// 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 {
|
verifyMessage(message: OpchanMessage): boolean {
|
||||||
// Check for required signature fields
|
// 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(
|
private verifyRawSignature(
|
||||||
message: string,
|
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 { Cell, Comment, Post } from '@/types/forum';
|
||||||
import { EVerificationStatus, User } from '@/types/identity';
|
import { EVerificationStatus, User } from '@/types/identity';
|
||||||
import { transformCell, transformComment, transformPost } from './transformers';
|
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> = {
|
type ActionResult<T> = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@ -22,12 +23,12 @@ type ActionResult<T> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class ForumActions {
|
export class ForumActions {
|
||||||
private cryptoService: CryptoService;
|
private delegationManager: DelegationManager;
|
||||||
private messageService: MessageService;
|
private messageService: MessageService;
|
||||||
|
|
||||||
constructor(cryptoService?: CryptoService) {
|
constructor(delegationManager?: DelegationManager) {
|
||||||
this.cryptoService = cryptoService || new CryptoService();
|
this.delegationManager = delegationManager || new DelegationManager();
|
||||||
this.messageService = new MessageService(this.cryptoService);
|
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 { OpchanMessage } from '@/types/forum';
|
||||||
import { UnsignedMessage } from '@/types/waku';
|
import { UnsignedMessage } from '@/types/waku';
|
||||||
import { CryptoService } from './CryptoService';
|
import { DelegationManager } from '@/lib/delegation';
|
||||||
import messageManager from '@/lib/waku';
|
import messageManager from '@/lib/waku';
|
||||||
|
|
||||||
export interface MessageResult {
|
export interface MessageResult {
|
||||||
@ -15,10 +15,10 @@ export interface MessageServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MessageService implements MessageServiceInterface {
|
export class MessageService implements MessageServiceInterface {
|
||||||
private cryptoService: CryptoService;
|
private delegationManager: DelegationManager;
|
||||||
|
|
||||||
constructor(cryptoService: CryptoService) {
|
constructor(delegationManager: DelegationManager) {
|
||||||
this.cryptoService = cryptoService;
|
this.delegationManager = delegationManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -26,12 +26,13 @@ export class MessageService implements MessageServiceInterface {
|
|||||||
*/
|
*/
|
||||||
async sendMessage(message: UnsignedMessage): Promise<MessageResult> {
|
async sendMessage(message: UnsignedMessage): Promise<MessageResult> {
|
||||||
try {
|
try {
|
||||||
const signedMessage = this.cryptoService.signMessage(message);
|
const signedMessage =
|
||||||
|
this.delegationManager.signMessageWithDelegatedKey(message);
|
||||||
|
|
||||||
if (!signedMessage) {
|
if (!signedMessage) {
|
||||||
// Check if delegation exists but is expired
|
// Check if delegation exists but is expired
|
||||||
const isDelegationExpired =
|
const isDelegationExpired =
|
||||||
this.cryptoService.isDelegationValid() === false;
|
this.delegationManager.isDelegationValid() === false;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -81,6 +82,6 @@ export class MessageService implements MessageServiceInterface {
|
|||||||
* Verify a message signature
|
* Verify a message signature
|
||||||
*/
|
*/
|
||||||
verifyMessage(message: OpchanMessage): boolean {
|
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 { MessageService, type MessageServiceInterface } from './MessageService';
|
||||||
export {
|
|
||||||
CryptoService,
|
|
||||||
type CryptoServiceInterface,
|
|
||||||
type DelegationDuration,
|
|
||||||
} from './CryptoService';
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { OpchanMessage } from '@/types/forum';
|
import { OpchanMessage } from '@/types/forum';
|
||||||
import { CryptoService } from '@/lib/services/CryptoService';
|
import { DelegationManager } from '@/lib/delegation';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type for potential message objects with partial structure
|
* Type for potential message objects with partial structure
|
||||||
@ -18,10 +18,10 @@ interface PartialMessage {
|
|||||||
* Ensures all messages have valid signatures and browserPubKey
|
* Ensures all messages have valid signatures and browserPubKey
|
||||||
*/
|
*/
|
||||||
export class MessageValidator {
|
export class MessageValidator {
|
||||||
private cryptoService: CryptoService;
|
private delegationManager: DelegationManager;
|
||||||
|
|
||||||
constructor(cryptoService?: CryptoService) {
|
constructor(delegationManager?: DelegationManager) {
|
||||||
this.cryptoService = cryptoService || new CryptoService();
|
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
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.cryptoService.verifyMessage(message as OpchanMessage)) {
|
if (!this.delegationManager.verifyMessage(message as OpchanMessage)) {
|
||||||
invalidCount.invalidSignature++;
|
invalidCount.invalidSignature++;
|
||||||
continue;
|
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;
|
const partialMsg = message as PartialMessage;
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Message validation failed: Invalid signature (messageId: ${partialMsg?.id})`
|
`Message validation failed: Invalid signature (messageId: ${partialMsg?.id})`
|
||||||
@ -242,7 +242,7 @@ export class MessageValidator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasRequiredFields) {
|
if (hasRequiredFields) {
|
||||||
hasValidSignature = this.cryptoService.verifyMessage(
|
hasValidSignature = this.delegationManager.verifyMessage(
|
||||||
message as OpchanMessage
|
message as OpchanMessage
|
||||||
);
|
);
|
||||||
if (!hasValidSignature) {
|
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