/** * Multi-Chain Savings Manager * * Orchestrates savings across multiple blockchain networks (EVM and Solana) * Provides unified interface for managing savings pockets across chains */ import { EVMSavingsManager } from "./evm-savings"; import { SVMSavingsManager } from "./svm-savings"; import { ChainWalletConfig, Balance } from "../types"; import { Hex } from "viem"; import { PublicKey } from "@solana/web3.js"; import { fetchPrices } from "../price"; /** * Chain type identifier */ export type ChainType = 'EVM' | 'SVM'; /** * Chain configuration for multi-chain manager */ export interface ChainConfig { /** Unique identifier for the chain (e.g., "ethereum", "polygon", "solana") */ id: string; /** Chain type (EVM or SVM) */ type: ChainType; /** Chain-specific configuration */ config: ChainWalletConfig | { rpcUrl: string }; } /** * Pocket balance information for a specific chain */ export interface PocketBalance { /** Chain identifier */ chainId: string; /** Chain type */ chainType: ChainType; /** Pocket index */ pocketIndex: number; /** Pocket address (string format for compatibility) */ address: string; /** Token balances */ balances: { /** Token address or 'native' */ token: string; /** Balance information */ balance: Balance; /** Token unit price in USD (if unavailable, defaults to 0) */ priceUsd: number; /** Token 24h price change percentage (if unavailable, defaults to 0) */ priceChange24h: number; /** Balance in USD (if unavailable, defaults to 0) */ balanceInUsd: number; }[]; } /** * Multi-Chain Savings Manager * * Manages savings pockets across multiple blockchain networks. * - EVM chains (Ethereum, Polygon, BSC, etc.) share the same addresses (coin type 60) * - Solana has different addresses (coin type 501) * * @example * ```typescript * const manager = new MultiChainSavingsManager( * mnemonic, * [ * { id: 'ethereum', type: 'EVM', config: ethConfig }, * { id: 'polygon', type: 'EVM', config: polyConfig }, * { id: 'solana', type: 'SVM', config: { rpcUrl: '...' } } * ], * 0 // wallet index * ); * * // Get pocket address on Ethereum * const ethAddress = manager.getPocketAddress('ethereum', 0); * * // Get pocket address on Polygon (same as Ethereum!) * const polyAddress = manager.getPocketAddress('polygon', 0); * console.log(ethAddress === polyAddress); // true * * // Get balances across all chains * const balances = await manager.getPocketBalanceAcrossChains(0, tokensByChain); * ``` */ export class MultiChainSavingsManager { private mnemonic: string; private walletIndex: number; // Separate managers by chain type private evmManagers: Map = new Map(); private svmManagers: Map = new Map(); // Track chain configs private chainConfigs: Map = new Map(); private getTokenKey(tokenAddress: string, chainType: ChainType): string { if (chainType === 'EVM') { return tokenAddress.toLowerCase(); } return tokenAddress; } private normalizePriceMap( prices: Array<{ tokenAddress: string; price: number; priceChange24h?: number }>, chainType: ChainType ): Map { const priceMap = new Map(); for (const item of prices) { if (!item?.tokenAddress || typeof item.price !== 'number') continue; priceMap.set(this.getTokenKey(item.tokenAddress, chainType), { priceUsd: item.price, priceChange24h: typeof item.priceChange24h === 'number' ? item.priceChange24h : 0 }); } return priceMap; } private async getPriceMap( chain: ChainConfig, tokenAddresses: string[] ): Promise> { const chainWithId = chain.config as Partial; if (typeof chainWithId.chainId !== 'number') { return new Map(); } const uniqueTokenAddresses = Array.from(new Set(tokenAddresses)); const priceResult = await fetchPrices({ vm: chain.type, chainId: chainWithId.chainId, tokenAddresses: uniqueTokenAddresses }); return this.normalizePriceMap(priceResult.data?.prices ?? [], chain.type); } /** * Create a new MultiChainSavingsManager * * @param mnemonic - BIP-39 mnemonic phrase * @param chains - Array of chain configurations * @param walletIndex - Wallet index in derivation path (default: 0) */ constructor( mnemonic: string, chains: ChainConfig[], walletIndex: number = 0 ) { if (!mnemonic || typeof mnemonic !== 'string') { throw new Error('Mnemonic must be a non-empty string'); } if (!Array.isArray(chains) || chains.length === 0) { throw new Error('Chains array must be non-empty'); } this.mnemonic = mnemonic; this.walletIndex = walletIndex; // Initialize managers for each chain for (const chain of chains) { this.addChain(chain); } } /** * Add a new chain to the manager * * @param chain - Chain configuration */ addChain(chain: ChainConfig): void { if (!chain.id || !chain.type) { throw new Error('Chain must have id and type'); } if (this.chainConfigs.has(chain.id)) { throw new Error(`Chain with id '${chain.id}' already exists`); } if (chain.type === 'EVM') { const manager = new EVMSavingsManager( this.mnemonic, chain.config as ChainWalletConfig, this.walletIndex ); this.chainConfigs.set(chain.id, chain); this.evmManagers.set(chain.id, manager); } else if (chain.type === 'SVM') { const config = chain.config as { rpcUrl: string }; if (!config.rpcUrl) { throw new Error(`SVM chain '${chain.id}' must have rpcUrl in config`); } const manager = new SVMSavingsManager( this.mnemonic, config.rpcUrl, this.walletIndex ); this.chainConfigs.set(chain.id, chain); this.svmManagers.set(chain.id, manager); } else { throw new Error(`Unknown chain type: ${chain.type}`); } } /** * Remove a chain from the manager * * @param chainId - Chain identifier to remove */ removeChain(chainId: string): void { if (this.evmManagers.has(chainId)) { this.evmManagers.get(chainId)?.dispose(); this.evmManagers.delete(chainId); } if (this.svmManagers.has(chainId)) { this.svmManagers.get(chainId)?.dispose(); this.svmManagers.delete(chainId); } this.chainConfigs.delete(chainId); } /** * Get list of all chain IDs * * @returns Array of chain identifiers */ getChains(): string[] { return Array.from(this.chainConfigs.keys()); } /** * Get chain configuration * * @param chainId - Chain identifier * @returns Chain configuration */ getChainConfig(chainId: string): ChainConfig { const config = this.chainConfigs.get(chainId); if (!config) { throw new Error(`Chain not found: ${chainId}`); } return config; } /** * Get pocket address for a specific chain * * @param chainId - Chain identifier * @param pocketIndex - Pocket index * @returns Pocket address as string */ getPocketAddress(chainId: string, pocketIndex: number): string { const chain = this.getChainConfig(chainId); if (chain.type === 'EVM') { const manager = this.evmManagers.get(chainId)!; return manager.getPocket(pocketIndex).address; } else { const manager = this.svmManagers.get(chainId)!; return manager.getPocket(pocketIndex).address.toBase58(); } } /** * Get main wallet address for a specific chain * * @param chainId - Chain identifier * @returns Main wallet address as string */ getMainWalletAddress(chainId: string): string { const chain = this.getChainConfig(chainId); if (chain.type === 'EVM') { const manager = this.evmManagers.get(chainId)!; return manager.getMainWalletAddress(); } else { const manager = this.svmManagers.get(chainId)!; return manager.getMainWalletAddress().toBase58(); } } /** * Get pocket balance for a specific chain * * @param chainId - Chain identifier * @param pocketIndex - Pocket index * @param tokens - Array of token addresses to query * @returns Pocket balance information */ async getPocketBalance( chainId: string, pocketIndex: number, tokens: string[] ): Promise { const chain = this.getChainConfig(chainId); if (chain.type === 'EVM') { const manager = this.evmManagers.get(chainId)!; const balances = await manager.getPocketBalance(pocketIndex, tokens); const pocket = manager.getPocket(pocketIndex); const balanceRows = balances.map(b => ({ token: b.address === 'native' ? 'native' : b.address, balance: b.balance })); const priceMap = await this.getPriceMap( chain, balanceRows.map(row => row.token) ); return { chainId, chainType: 'EVM', pocketIndex, address: pocket.address, balances: balanceRows.map(row => ({ token: row.token, balance: row.balance, priceUsd: priceMap.get(this.getTokenKey(row.token, 'EVM'))?.priceUsd ?? 0, priceChange24h: priceMap.get(this.getTokenKey(row.token, 'EVM'))?.priceChange24h ?? 0, balanceInUsd: row.balance.formatted * (priceMap.get(this.getTokenKey(row.token, 'EVM'))?.priceUsd ?? 0) })) }; } else { const manager = this.svmManagers.get(chainId)!; const balances = await manager.getPocketBalance(pocketIndex, tokens); const pocket = manager.getPocket(pocketIndex); const balanceRows = balances.map(b => ({ token: b.address === 'native' ? 'native' : b.address.toBase58(), balance: b.balance })); const priceMap = await this.getPriceMap( chain, balanceRows.map(row => row.token) ); return { chainId, chainType: 'SVM', pocketIndex, address: pocket.address.toBase58(), balances: balanceRows.map(row => ({ token: row.token, balance: row.balance, priceUsd: priceMap.get(this.getTokenKey(row.token, 'SVM'))?.priceUsd ?? 0, priceChange24h: priceMap.get(this.getTokenKey(row.token, 'SVM'))?.priceChange24h ?? 0, balanceInUsd: row.balance.formatted * (priceMap.get(this.getTokenKey(row.token, 'SVM'))?.priceUsd ?? 0) })) }; } } /** * Get pocket balance across multiple chains * * @param pocketIndex - Pocket index * @param tokensByChain - Map of chain IDs to token addresses * @returns Array of pocket balances per chain */ async getPocketBalanceAcrossChains( pocketIndex: number, tokensByChain: Map ): Promise { const promises: Promise[] = []; for (const [chainId, tokens] of tokensByChain) { promises.push(this.getPocketBalance(chainId, pocketIndex, tokens)); } return await Promise.all(promises); } /** * Get balances for multiple pockets across multiple chains * * @param pocketIndices - Array of pocket indices * @param tokensByChain - Map of chain IDs to token addresses * @returns Map of pocket indices to their balances across chains */ async getAllPocketsBalanceAcrossChains( pocketIndices: number[], tokensByChain: Map ): Promise> { const results = new Map(); // Fetch all pockets in parallel await Promise.all( pocketIndices.map(async (pocketIndex) => { const balances = await this.getPocketBalanceAcrossChains( pocketIndex, tokensByChain ); results.set(pocketIndex, balances); }) ); return results; } /** * Get EVM manager for a chain (for advanced operations) * * @param chainId - Chain identifier * @returns EVMSavingsManager instance * @throws Error if chain is not EVM or not found */ getEVMManager(chainId: string): EVMSavingsManager { const manager = this.evmManagers.get(chainId); if (!manager) { throw new Error(`EVM chain not found: ${chainId}`); } return manager; } /** * Get SVM manager for a chain (for advanced operations) * * @param chainId - Chain identifier * @returns SVMSavingsManager instance * @throws Error if chain is not SVM or not found */ getSVMManager(chainId: string): SVMSavingsManager { const manager = this.svmManagers.get(chainId); if (!manager) { throw new Error(`SVM chain not found: ${chainId}`); } return manager; } /** * Check if a chain is EVM-compatible * * @param chainId - Chain identifier * @returns true if chain is EVM */ isEVMChain(chainId: string): boolean { return this.evmManagers.has(chainId); } /** * Check if a chain is Solana (SVM) * * @param chainId - Chain identifier * @returns true if chain is SVM */ isSVMChain(chainId: string): boolean { return this.svmManagers.has(chainId); } /** * Get all EVM chain IDs * * @returns Array of EVM chain identifiers */ getEVMChains(): string[] { return Array.from(this.evmManagers.keys()); } /** * Get all SVM chain IDs * * @returns Array of SVM chain identifiers */ getSVMChains(): string[] { return Array.from(this.svmManagers.keys()); } /** * Clear a specific pocket across all chains * * @param pocketIndex - Pocket index to clear */ clearPocket(pocketIndex: number): void { for (const manager of this.evmManagers.values()) { manager.clearPocket(pocketIndex); } for (const manager of this.svmManagers.values()) { manager.clearPocket(pocketIndex); } } /** * Clear all pockets across all chains */ clearAllPockets(): void { for (const manager of this.evmManagers.values()) { manager.clearAllPockets(); } for (const manager of this.svmManagers.values()) { manager.clearAllPockets(); } } /** * Clear all RPC clients across all chains */ clearAllClients(): void { for (const manager of this.evmManagers.values()) { manager.clearClient(); } for (const manager of this.svmManagers.values()) { manager.clearClient(); } } /** * Dispose all managers and clear all sensitive data * * @remarks * After calling dispose(), this manager instance should not be used. */ dispose(): void { for (const manager of this.evmManagers.values()) { manager.dispose(); } for (const manager of this.svmManagers.values()) { manager.dispose(); } this.evmManagers.clear(); this.svmManagers.clear(); this.chainConfigs.clear(); (this as any).mnemonic = ''; } }