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 address = activeAccount.address;
// Create manager instances that persist between renders
const walletManager = useMemo(() => new WalletManager(), []);
// Create manager instances
const delegationManager = useMemo(() => new DelegationManager(), []);
// Set AppKit instance and accounts in WalletManager
// Create wallet manager when we have all dependencies
useEffect(() => {
if (modal) {
walletManager.setAppKit(modal);
if (modal && (bitcoinAccount.isConnected || ethereumAccount.isConnected)) {
try {
WalletManager.create(modal, bitcoinAccount, ethereumAccount);
} catch (error) {
console.warn('Failed to create WalletManager:', error);
WalletManager.clear();
}
} else {
WalletManager.clear();
}
}, [walletManager]);
useEffect(() => {
walletManager.setAccounts(bitcoinAccount, ethereumAccount);
}, [bitcoinAccount, ethereumAccount, walletManager]);
}, [bitcoinAccount, ethereumAccount]);
// Helper functions for user persistence
const loadStoredUser = (): User | null => {
@ -115,7 +117,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
};
} else if (user.walletType === 'ethereum') {
try {
const walletInfo = await walletManager.getWalletInfo();
const walletInfo = WalletManager.hasInstance()
? await WalletManager.getInstance().getWalletInfo()
: null;
const hasENS = !!walletInfo?.ensName;
const ensName = walletInfo?.ensName;
@ -147,12 +151,18 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
duration: DelegationDuration = '7days'
): Promise<boolean> => {
try {
const walletType = user.walletType;
const isAvailable = walletManager.isWalletConnected(walletType);
if (!isAvailable) {
if (!WalletManager.hasInstance()) {
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
const signature = await walletManager.signMessage(
delegationMessage,
walletType
);
const signature = await walletManager.signMessage(delegationMessage);
// Create and store the delegation
delegationManager.createDelegation(
@ -181,7 +188,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
keypair.publicKey,
keypair.privateKey,
duration,
walletType
user.walletType
);
return true;
@ -216,30 +223,38 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
// For Ethereum wallets, try to check ENS ownership immediately
if (isEthereumConnected) {
walletManager
.getWalletInfo()
.then(walletInfo => {
if (walletInfo?.ensName) {
const updatedUser = {
...newUser,
ensDetails: { ensName: walletInfo.ensName },
verificationStatus: EVerificationStatus.VERIFIED_OWNER,
};
setCurrentUser(updatedUser);
setVerificationStatus('verified-owner');
saveUser(updatedUser);
} else {
try {
const walletManager = WalletManager.getInstance();
walletManager
.getWalletInfo()
.then(walletInfo => {
if (walletInfo?.ensName) {
const updatedUser = {
...newUser,
ensDetails: { ensName: walletInfo.ensName },
verificationStatus: EVerificationStatus.VERIFIED_OWNER,
};
setCurrentUser(updatedUser);
setVerificationStatus('verified-owner');
saveUser(updatedUser);
} else {
setCurrentUser(newUser);
setVerificationStatus('verified-basic');
saveUser(newUser);
}
})
.catch(() => {
// Fallback to basic verification if ENS check fails
setCurrentUser(newUser);
setVerificationStatus('verified-basic');
saveUser(newUser);
}
})
.catch(() => {
// Fallback to basic verification if ENS check fails
setCurrentUser(newUser);
setVerificationStatus('verified-basic');
saveUser(newUser);
});
});
} catch {
// WalletManager not ready, fallback to basic verification
setCurrentUser(newUser);
setVerificationStatus('verified-basic');
saveUser(newUser);
}
} else {
setCurrentUser(newUser);
setVerificationStatus('verified-basic');
@ -267,14 +282,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setCurrentUser(null);
setVerificationStatus('unverified');
}
}, [
isConnected,
address,
isBitcoinConnected,
isEthereumConnected,
toast,
walletManager,
]);
}, [isConnected, address, isBitcoinConnected, isEthereumConnected, toast]);
const { disconnect } = useDisconnect();

View File

@ -7,177 +7,81 @@ import { Provider } from '@reown/appkit-controllers';
import { WalletInfo, ActiveWallet } from './types';
export class WalletManager {
private appKit?: AppKit;
private bitcoinAccount?: UseAppKitAccountReturn;
private ethereumAccount?: UseAppKitAccountReturn;
private static instance: WalletManager | null = null;
/**
* Set the AppKit instance for accessing adapters
*/
setAppKit(appKit: AppKit): void {
this.appKit = appKit;
}
private appKit: AppKit;
private activeAccount: UseAppKitAccountReturn;
private activeWalletType: 'bitcoin' | 'ethereum';
private namespace: ChainNamespace;
/**
* Set account references from AppKit hooks
*/
setAccounts(
private constructor(
appKit: AppKit,
bitcoinAccount: UseAppKitAccountReturn,
ethereumAccount: UseAppKitAccountReturn
): void {
this.bitcoinAccount = bitcoinAccount;
this.ethereumAccount = ethereumAccount;
) {
this.appKit = appKit;
// 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 {
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)
static create(
appKit: AppKit,
bitcoinAccount: UseAppKitAccountReturn,
ethereumAccount: UseAppKitAccountReturn
): WalletManager {
// Always create a new instance to reflect current wallet state
WalletManager.instance = new WalletManager(
appKit,
bitcoinAccount,
ethereumAccount
);
return WalletManager.instance;
}
/**
* Check if a specific wallet type is connected
* Get the current instance (throws if not created)
*/
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);
static getInstance(): WalletManager {
if (!WalletManager.instance) {
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> {
if (this.bitcoinAccount?.isConnected) {
return {
address: this.bitcoinAccount.address as string,
walletType: 'bitcoin',
isConnected: true,
};
}
static hasInstance(): boolean {
return WalletManager.instance !== null;
}
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;
/**
* Clear the singleton instance
*/
static clear(): void {
WalletManager.instance = null;
}
/**
* Resolve ENS name for an Ethereum address
*/
async resolveENS(address: string): Promise<string | null> {
static async resolveENS(address: string): Promise<string | null> {
try {
const ensName = await getEnsName(config, {
address: address as `0x${string}`,
@ -188,9 +92,123 @@ export class WalletManager {
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
export const walletManager = new WalletManager();
// Convenience exports for singleton access
export const walletManager = {
create: WalletManager.create,
getInstance: WalletManager.getInstance,
hasInstance: WalletManager.hasInstance,
clear: WalletManager.clear,
resolveENS: WalletManager.resolveENS,
};
export * from './types';
export * from './config';