/** * Abstract Base Savings Manager * * Base class for managing multi-pocket savings accounts across different chains. * Similar to the VM pattern. * * @template AddressType - Address format (Hex for EVM, PublicKey for SVM) * @template ClientType - RPC client type (PublicClient for EVM, Connection for SVM) * @template WalletClientType - Wallet client type (WalletClient for EVM, Keypair for SVM) */ import { SavingsValidation } from "./validation"; export interface Pocket { privateKey: any; address: AddressType; derivationPath: string; index: number; } /** * Abstract base class for chain-specific savings managers * * Follows the same pattern as VM class: * - Abstract base with generic types * - Concrete implementations for each chain type (EVM, SVM) * - Shared memory management and disposal patterns */ export abstract class SavingsManager< AddressType, ClientType, WalletClientType > { protected mnemonic: string; protected walletIndex: number; protected disposed: boolean = false; // Abstract properties (must be implemented by subclasses) abstract coinType: number; abstract derivationPathBase: string; // Pocket cache: Map protected pockets: Map> = new Map(); /** * Create a new SavingsManager * * @param mnemonic - BIP-39 mnemonic phrase * @param walletIndex - Wallet index in derivation path (default: 0) */ constructor(mnemonic: string, walletIndex: number = 0) { SavingsValidation.validateMnemonic(mnemonic); SavingsValidation.validateWalletIndex(walletIndex); this.mnemonic = mnemonic; this.walletIndex = walletIndex; } // Abstract methods (must be implemented by subclasses) /** * Derive a savings pocket at the specified account index * * @param accountIndex - Account index for the pocket (0-based) * @returns Pocket object with privateKey, address, derivationPath, and index */ abstract derivePocket(accountIndex: number): Pocket; /** * Get the main wallet credentials * * @returns Main wallet object with privateKey, address, and derivationPath */ abstract getMainWallet(): { privateKey: any; address: AddressType; derivationPath: string; }; /** * Create an RPC client for this chain * * @param rpcUrl - RPC endpoint URL * @returns Chain-specific RPC client */ abstract createClient(rpcUrl: string): ClientType; // Shared methods (implemented in base class) /** * Get or create a savings pocket at the specified index * * @param accountIndex - The pocket index (0-based) * @returns Pocket object * @throws Error if validation fails or VM is disposed */ getPocket(accountIndex: number): Pocket { this.checkNotDisposed(); SavingsValidation.validateAccountIndex(accountIndex); if (!this.pockets.has(accountIndex)) { return this.derivePocket(accountIndex); } return this.pockets.get(accountIndex)!; } /** * Clear a specific pocket's cached private key from memory * * @param accountIndex - Index of the pocket to clear */ clearPocket(accountIndex: number): void { SavingsValidation.validateAccountIndex(accountIndex); if (this.pockets.has(accountIndex)) { const pocket = this.pockets.get(accountIndex)!; // Attempt to clear the private key // Note: JavaScript strings are immutable, so this only clears our reference (pocket as any).privateKey = ''; // Remove from cache this.pockets.delete(accountIndex); } } /** * Clear all cached pocket private keys from memory * * @remarks * Call this method when: * - User locks the wallet * - Application goes to background (mobile) * - Extension popup closes (browser extension) * - Session ends */ clearAllPockets(): void { for (const [_, pocket] of this.pockets.entries()) { // Attempt to clear the private key (pocket as any).privateKey = ''; } // Clear the map this.pockets.clear(); } /** * Clear all sensitive data from memory * * @remarks * IMPORTANT: After calling dispose(), the manager instance should not be used. * JavaScript strings are immutable, so this method can only clear references. * The actual memory will be cleared by garbage collection. */ dispose(): void { if (this.disposed) { return; // Already disposed } // Clear all cached pockets this.clearAllPockets(); // Attempt to clear mnemonic (this as any).mnemonic = ''; this.disposed = true; } /** * Check if manager has been disposed * * @returns true if dispose() has been called */ isDisposed(): boolean { return this.disposed || !this.mnemonic || this.mnemonic === ''; } /** * Throw error if manager has been disposed * * @throws Error if manager is disposed * @protected */ protected checkNotDisposed(): void { if (this.isDisposed()) { throw new Error('SavingsManager has been disposed. Create a new instance to perform operations.'); } } }