chore: convert WalletManager into a factory class

This commit is contained in:
Danish Arora 2025-09-02 10:56:59 +05:30
parent dc4468078e
commit 4232878d39
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
2 changed files with 223 additions and 197 deletions

View File

@ -53,20 +53,22 @@ 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 manager instances that persist between renders // Create manager instances
const walletManager = useMemo(() => new WalletManager(), []);
const delegationManager = useMemo(() => new DelegationManager(), []); const delegationManager = useMemo(() => new DelegationManager(), []);
// Set AppKit instance and accounts in WalletManager // Create wallet manager when we have all dependencies
useEffect(() => { useEffect(() => {
if (modal) { if (modal && (bitcoinAccount.isConnected || ethereumAccount.isConnected)) {
walletManager.setAppKit(modal); try {
WalletManager.create(modal, bitcoinAccount, ethereumAccount);
} catch (error) {
console.warn('Failed to create WalletManager:', error);
WalletManager.clear();
}
} else {
WalletManager.clear();
} }
}, [walletManager]); }, [bitcoinAccount, ethereumAccount]);
useEffect(() => {
walletManager.setAccounts(bitcoinAccount, ethereumAccount);
}, [bitcoinAccount, ethereumAccount, walletManager]);
// Helper functions for user persistence // Helper functions for user persistence
const loadStoredUser = (): User | null => { const loadStoredUser = (): User | null => {
@ -115,7 +117,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}; };
} else if (user.walletType === 'ethereum') { } else if (user.walletType === 'ethereum') {
try { try {
const walletInfo = await walletManager.getWalletInfo(); const walletInfo = WalletManager.hasInstance()
? await WalletManager.getInstance().getWalletInfo()
: null;
const hasENS = !!walletInfo?.ensName; const hasENS = !!walletInfo?.ensName;
const ensName = walletInfo?.ensName; const ensName = walletInfo?.ensName;
@ -147,12 +151,18 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
duration: DelegationDuration = '7days' duration: DelegationDuration = '7days'
): Promise<boolean> => { ): Promise<boolean> => {
try { try {
const walletType = user.walletType; if (!WalletManager.hasInstance()) {
const isAvailable = walletManager.isWalletConnected(walletType);
if (!isAvailable) {
throw new Error( throw new Error(
`${walletType} wallet is not available or connected. Please ensure it is connected.` 'Wallet not connected. Please ensure your wallet is connected.'
);
}
const walletManager = WalletManager.getInstance();
// Verify wallet type matches
if (walletManager.getWalletType() !== user.walletType) {
throw new Error(
`Expected ${user.walletType} wallet, but ${walletManager.getWalletType()} is connected.`
); );
} }
@ -169,10 +179,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
); );
// Sign the delegation message with wallet // Sign the delegation message with wallet
const signature = await walletManager.signMessage( const signature = await walletManager.signMessage(delegationMessage);
delegationMessage,
walletType
);
// Create and store the delegation // Create and store the delegation
delegationManager.createDelegation( delegationManager.createDelegation(
@ -181,7 +188,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
keypair.publicKey, keypair.publicKey,
keypair.privateKey, keypair.privateKey,
duration, duration,
walletType user.walletType
); );
return true; return true;
@ -216,30 +223,38 @@ 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) {
walletManager try {
.getWalletInfo() const walletManager = WalletManager.getInstance();
.then(walletInfo => { walletManager
if (walletInfo?.ensName) { .getWalletInfo()
const updatedUser = { .then(walletInfo => {
...newUser, if (walletInfo?.ensName) {
ensDetails: { ensName: walletInfo.ensName }, const updatedUser = {
verificationStatus: EVerificationStatus.VERIFIED_OWNER, ...newUser,
}; ensDetails: { ensName: walletInfo.ensName },
setCurrentUser(updatedUser); verificationStatus: EVerificationStatus.VERIFIED_OWNER,
setVerificationStatus('verified-owner'); };
saveUser(updatedUser); setCurrentUser(updatedUser);
} else { setVerificationStatus('verified-owner');
saveUser(updatedUser);
} else {
setCurrentUser(newUser);
setVerificationStatus('verified-basic');
saveUser(newUser);
}
})
.catch(() => {
// Fallback to basic verification if ENS check fails
setCurrentUser(newUser); setCurrentUser(newUser);
setVerificationStatus('verified-basic'); setVerificationStatus('verified-basic');
saveUser(newUser); saveUser(newUser);
} });
}) } catch {
.catch(() => { // WalletManager not ready, fallback to basic verification
// Fallback to basic verification if ENS check fails setCurrentUser(newUser);
setCurrentUser(newUser); setVerificationStatus('verified-basic');
setVerificationStatus('verified-basic'); saveUser(newUser);
saveUser(newUser); }
});
} else { } else {
setCurrentUser(newUser); setCurrentUser(newUser);
setVerificationStatus('verified-basic'); setVerificationStatus('verified-basic');
@ -267,14 +282,7 @@ 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();

View File

@ -7,177 +7,81 @@ import { Provider } from '@reown/appkit-controllers';
import { WalletInfo, ActiveWallet } from './types'; import { WalletInfo, ActiveWallet } from './types';
export class WalletManager { export class WalletManager {
private appKit?: AppKit; private static instance: WalletManager | null = null;
private bitcoinAccount?: UseAppKitAccountReturn;
private ethereumAccount?: UseAppKitAccountReturn;
/** private appKit: AppKit;
* Set the AppKit instance for accessing adapters private activeAccount: UseAppKitAccountReturn;
*/ private activeWalletType: 'bitcoin' | 'ethereum';
setAppKit(appKit: AppKit): void { private namespace: ChainNamespace;
this.appKit = appKit;
}
/** private constructor(
* Set account references from AppKit hooks appKit: AppKit,
*/
setAccounts(
bitcoinAccount: UseAppKitAccountReturn, bitcoinAccount: UseAppKitAccountReturn,
ethereumAccount: UseAppKitAccountReturn ethereumAccount: UseAppKitAccountReturn
): void { ) {
this.bitcoinAccount = bitcoinAccount; this.appKit = appKit;
this.ethereumAccount = ethereumAccount;
// Determine active wallet (Bitcoin takes priority)
if (bitcoinAccount.isConnected && bitcoinAccount.address) {
this.activeAccount = bitcoinAccount;
this.activeWalletType = 'bitcoin';
this.namespace = 'bip122';
} else if (ethereumAccount.isConnected && ethereumAccount.address) {
this.activeAccount = ethereumAccount;
this.activeWalletType = 'ethereum';
this.namespace = 'eip155';
} else {
throw new Error('No wallet is connected');
}
} }
/** /**
* Get the currently active wallet (Bitcoin takes priority) * Create or get the singleton instance
*/ */
getActiveWallet(): ActiveWallet | null { static create(
if (this.bitcoinAccount?.isConnected && this.bitcoinAccount.address) { appKit: AppKit,
return { bitcoinAccount: UseAppKitAccountReturn,
type: 'bitcoin', ethereumAccount: UseAppKitAccountReturn
address: this.bitcoinAccount.address, ): WalletManager {
isConnected: true, // Always create a new instance to reflect current wallet state
}; WalletManager.instance = new WalletManager(
} appKit,
bitcoinAccount,
if (this.ethereumAccount?.isConnected && this.ethereumAccount.address) { ethereumAccount
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)
); );
return WalletManager.instance;
} }
/** /**
* Check if a specific wallet type is connected * Get the current instance (throws if not created)
*/ */
isWalletConnected(walletType: 'bitcoin' | 'ethereum'): boolean { static getInstance(): WalletManager {
const account = if (!WalletManager.instance) {
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( throw new Error(
`Failed to sign message with ${walletType} wallet: ${error instanceof Error ? error.message : 'Unknown error'}` 'WalletManager not initialized. Call WalletManager.create() first.'
); );
} }
return WalletManager.instance;
} }
/** /**
* Get comprehensive wallet info including ENS resolution for Ethereum * Check if instance exists
*/ */
async getWalletInfo(): Promise<WalletInfo | null> { static hasInstance(): boolean {
if (this.bitcoinAccount?.isConnected) { return WalletManager.instance !== null;
return { }
address: this.bitcoinAccount.address as string,
walletType: 'bitcoin',
isConnected: true,
};
}
if (this.ethereumAccount?.isConnected) { /**
const address = this.ethereumAccount.address as string; * Clear the singleton instance
*/
// Try to resolve ENS name static clear(): void {
let ensName: string | undefined; WalletManager.instance = null;
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 * Resolve ENS name for an Ethereum address
*/ */
async resolveENS(address: string): Promise<string | null> { static async resolveENS(address: string): Promise<string | null> {
try { try {
const ensName = await getEnsName(config, { const ensName = await getEnsName(config, {
address: address as `0x${string}`, address: address as `0x${string}`,
@ -188,9 +92,123 @@ export class WalletManager {
return null; return null;
} }
} }
/**
* Get the currently active wallet
*/
getActiveWallet(): ActiveWallet {
return {
type: this.activeWalletType,
address: this.activeAccount.address!,
isConnected: true,
};
}
/**
* Check if wallet is connected
*/
isConnected(): boolean {
return this.activeAccount.isConnected;
}
/**
* Get the active wallet type
*/
getWalletType(): 'bitcoin' | 'ethereum' {
return this.activeWalletType;
}
/**
* Get address of the active wallet
*/
getAddress(): string {
return this.activeAccount.address!;
}
/**
* Sign a message using the active wallet
*/
async signMessage(message: string): Promise<string> {
try {
// Access the adapter through the appKit instance
const adapter = this.appKit.chainAdapters?.[this.namespace];
if (!adapter) {
throw new Error(`No adapter found for namespace: ${this.namespace}`);
}
// Get the provider for the current connection
const provider = this.appKit.getProvider(this.namespace);
if (!provider) {
throw new Error(`No provider found for namespace: ${this.namespace}`);
}
if (!this.activeAccount.address) {
throw new Error('No address found for active account');
}
// Call the adapter's signMessage method
const result = await adapter.signMessage({
message,
address: this.activeAccount.address,
provider: provider as Provider,
});
return result.signature;
} catch (error) {
console.error(
`Error signing message with ${this.activeWalletType} wallet:`,
error
);
throw new Error(
`Failed to sign message with ${this.activeWalletType} wallet: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
/**
* Get comprehensive wallet info including ENS resolution for Ethereum
*/
async getWalletInfo(): Promise<WalletInfo> {
const address = this.activeAccount.address!;
if (this.activeWalletType === 'bitcoin') {
return {
address,
walletType: 'bitcoin',
isConnected: true,
};
}
// For Ethereum, 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,
};
}
} }
// Export singleton instance // Convenience exports for singleton access
export const walletManager = new WalletManager(); export const walletManager = {
create: WalletManager.create,
getInstance: WalletManager.getInstance,
hasInstance: WalletManager.hasInstance,
clear: WalletManager.clear,
resolveENS: WalletManager.resolveENS,
};
export * from './types'; export * from './types';
export * from './config'; export * from './config';