/** * * @param phrase this is the pass phrase for this vm * this is a class that will be responsible for creating several evm wallets code */ import { EntropyToMnemonic, EVMDeriveChildPrivateKey } from "../walletBip32"; import { ChainAddress, ChainWallet } from "../IChainWallet"; import { Balance, ChainWalletConfig, NFTInfo, UserTokenBalance, TokenInfo, TransactionResult, NFT, DiscoveredWallet, WalletDiscoveryOptions, WalletDiscoveryResult, PocketDiscoveryOptions } from "../types"; import { VM } from "../vm"; import { VMValidation, sanitizeError, logSafeError } from "../vm-validation"; import { ethers, formatUnits, Interface, JsonRpcProvider } from "ethers"; import BN from "bn.js"; import { getNativeBalance, getTokenBalance, sendERC20Token, sendNativeToken, checkAndApprove, signSendAndConfirm, TransactionParams, approveToken, getTokenInfo, DESERIALIZED_SUPPORTED_CHAINS, discoverTokens, discoverNFTs, fromChainToViemChain } from "./utils"; import { EVMTransactionHistoryItem, getEVMTransactionHistory } from "./transactionParsing"; import { createPublicClient, Hex, http, parseEther, parseUnits, Call, walletActions, WalletClient, createWalletClient, Chain, ClientConfig, EIP1193RequestFn, TransportConfig, PublicClient, ChainConfig, toHex } from "viem"; import { EVMSmartWallet } from "./smartWallet"; import { SmartWalletOptions } from "./aa-service"; import { SmartSavingsManager } from "../savings/smart-savings"; import { SavingsAccount, SmartSavingsAccount, AddressVerificationResult, SavingsAuditResult, TransferToSavingsOptions, WithdrawFromSavingsOptions } from "../savings/types"; import { fetchPrices } from "../price"; import { PriceResponse } from "../price.types"; import { Account, generatePrivateKey, privateKeyToAccount } from "viem/accounts"; // import { extendWalletClientWithSavings } from "../savings"; // import { SavingsManager } from "../savings/saving-actions"; /** * Create a viem public client for the given EVM chain configuration. * * @param config - Chain configuration with RPC endpoint and chain metadata. * @returns Configured viem `PublicClient`. */ export const createEvmPublicClient = (config: ChainWalletConfig): PublicClient => createPublicClient({ chain: fromChainToViemChain(config), transport: http(config.rpcUrl), }); /** * Create a viem wallet client bound to a specific account. * * @param config - Chain configuration with RPC endpoint and chain metadata. * @param account - Account used for signing transactions and messages. * @returns Configured viem `WalletClient`. */ export const createEvmWalletClient = (config: ChainWalletConfig, account: Account): WalletClient => createWalletClient({ account, chain: fromChainToViemChain(config), transport: http(config.rpcUrl), }); /** * Fetch transaction history and return an empty array on failure. * * @param connection - viem public client used to query chain data. * @param address - Wallet address to inspect. * @returns Parsed EVM transaction history items. */ export const getEvmTransactionHistorySafe = async ( connection: PublicClient, address: string ): Promise => { try { return await getEVMTransactionHistory(connection, address as Hex); } catch { return []; } }; /** * Fetch token prices for EVM tokens on the configured chain. * * @param config - Chain configuration used for chain ID resolution. * @param tokenAddresses - Token contract addresses to query. * @returns Price data keyed by token address. * @throws Error when the pricing service returns an error payload. */ export const getSvmPricesForTokens = async ( config: ChainWalletConfig, tokenAddresses: string[] ): Promise => { const result = await fetchPrices({ vm: 'EVM', chainId: config.chainId, tokenAddresses, }); if (result.error) { throw new Error(result.error.message); } return result.data as PriceResponse; }; /** * Get native token balance for an address. * * @param address - Wallet address to query. * @param connection - viem public client. * @returns Native balance details. */ export const getEvmNativeBalance = async ( address: string, connection: PublicClient ): Promise => { return await EVMVM.getNativeBalance(address, connection); }; /** * Get ERC-20 token balance for an address. * * @param address - Wallet address to query. * @param tokenAddress - ERC-20 contract address. * @param connection - viem public client. * @returns Token balance details. */ export const getEvmTokenBalance = async ( address: string, tokenAddress: string, connection: PublicClient ): Promise => { return await EVMVM.getTokenBalance(address, tokenAddress, connection); }; /** * Discover token balances held by an address. * * @param address - Wallet address to scan. * @param config - Chain configuration. * @returns Discovered token balances. */ export const discoverEvmTokens = async ( address: string, config: ChainWalletConfig ): Promise[]> => { return await discoverTokens(address, config); }; /** * Discover NFTs held by an address. * * @param address - Wallet address to scan. * @param config - Chain configuration. * @returns Discovered NFT items. */ export const discoverEvmNFTs = async ( address: string, config: ChainWalletConfig ): Promise => { return await discoverNFTs(address, config); }; interface DebonkQuoteResponse { tokenA: string; tokenB: string; amountIn: string; amountOut: string; tokenPrice: string; routePlan: Array<{ tokenA: string; tokenB: string; dexId: string; poolAddress: string; aToB: boolean; fee: number; }>; dexId: string; dexFactory: string; } interface DebonkSwapResponse { transactions: Array<{ from: string; to: string; data: string; value: string; gasLimit?: string; gasPrice?: string; }>; } interface DebonkSwapResult { success: boolean; hash: string; error?: string; } export class EVMVM extends VM { derivationPath = "m/44'/60'/0'/0/"; // Default EVM derivation path constructor(seed: string) { super(seed, "EVM"); } getTokenInfo = getTokenInfo static getTokenInfo = getTokenInfo /** * Derive an EVM private key for a wallet index. * * @param index - Wallet index in the derivation path. * @param seed - Optional explicit seed (takes priority over mnemonic). * @param mnemonic - Optional mnemonic used when seed is not provided. * @param derivationPath - Base derivation path prefix. * @returns Derived private key and index. */ generatePrivateKey(index: number, seed?: string, mnemonic?: string, derivationPath = this.derivationPath) { // Validate inputs VMValidation.validateIndex(index, 'Wallet index'); // VMValidation.validateDerivationPath(derivationPath + index + "'", 'EVM'); let _seed: string; if (seed) { VMValidation.validateSeed(seed); _seed = seed; } else if (mnemonic) { VMValidation.validateMnemonic(mnemonic); _seed = VM.mnemonicToSeed(mnemonic); } else { // Check if VM has been disposed this.checkNotDisposed(); _seed = this.seed; } const privateKey = EVMDeriveChildPrivateKey(_seed, index, derivationPath).privateKey; return { privateKey, index }; } /** * Convert raw private key entropy into a mnemonic phrase. * * @param privateKey - Private key entropy input. * @returns Generated mnemonic phrase. */ static generateMnemonicFromPrivateKey(privateKey: string): string { return EntropyToMnemonic(privateKey) } /** * Create an `EVMVM` instance from a mnemonic phrase. * * @param mnemonic - BIP-39 mnemonic phrase. * @returns Initialized EVM VM instance. */ static fromMnemonic(mnemonic: string): VM { const seed = VM.mnemonicToSeed(mnemonic) return new EVMVM(seed) } /** * Validate Ethereum address format * * @param address - Address to validate * @param requireChecksum - Require valid EIP-55 checksum (default: false) * @returns true if valid * * @remarks * - Checks basic format (0x + 40 hex chars) * - If requireChecksum=true and address is mixed case, validates checksum * - All lowercase or all uppercase addresses pass checksum validation * * @example * ```typescript * EVMVM.validateAddress('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb'); // true (valid checksum) * EVMVM.validateAddress('0x742d35cc6634c0532925a3b844bc9e7595f0beb'); // true (all lowercase) * EVMVM.validateAddress('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', true); // true * EVMVM.validateAddress('0x742d35cc6634c0532925a3b844bc9e7595f0beB', true); // false (invalid checksum) * ``` */ static validateAddress(address: string, requireChecksum: boolean = false): boolean { // Check basic format if (!ethers.isAddress(address)) { return false; } // If checksum required and address is mixed case, validate checksum if (requireChecksum && address !== address.toLowerCase() && address !== address.toUpperCase()) { try { const checksummed = ethers.getAddress(address); return checksummed === address; } catch { return false; } } return true; } /** * Normalize address to checksummed format (EIP-55) * * @param address - Address to normalize * @returns Checksummed address * @throws Error if address is invalid * * @example * ```typescript * const normalized = EVMVM.normalizeAddress('0x742d35cc6634c0532925a3b844bc9e7595f0beb'); * // Returns: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb' * ``` */ static normalizeAddress(address: string): string { if (!this.validateAddress(address)) { throw new Error(`Invalid Ethereum address: ${address}`); } // Return checksummed version return ethers.getAddress(address); } /** * Read the native token balance for an address. * * @param address - Wallet address to query. * @param connection - viem public client. * @returns Native balance details. */ static async getNativeBalance(address: string, connection: PublicClient): Promise { // Implement native balance retrieval logic here return await getNativeBalance(address as Hex, connection) } /** * Read an ERC-20 token balance for an address. * * @param address - Wallet address that owns the token. * @param tokenAddress - ERC-20 contract address. * @param connection - viem public client. * @returns Token balance details. */ static async getTokenBalance(address: string, tokenAddress: string, connection: PublicClient): Promise { // Implement token balance retrieval logic here return await getTokenBalance(tokenAddress as Hex, address as Hex, connection) } /** * Convert arbitrary entropy text into a deterministic private-key-like hash. * * @param entropy - Input entropy string. * @returns `keccak256` hash of the input. */ static convertFromEntropyToPrivateKey = (entropy: string): string => { const p = ethers.id(entropy) return p } /** * Discover wallets with native token balances * * Scans BIP-44 derived wallet indices to find wallets with non-zero balances. * Follows BIP-44 standard with gap limit (default: 20 consecutive empty wallets). * * @param connection - ethers JsonRpcProvider or viem PublicClient * @param options - Discovery options * @returns Discovery result with found wallets * * @example * ```typescript * const vm = new EVMVM(seed); * const provider = new JsonRpcProvider('https://eth.llamarpc.com'); * * const result = await vm.discoverWallets(provider, { * maxIndex: 50, * gapLimit: 20, * minBalance: parseEther("0.001"), * onProgress: (current, total, found) => { * console.log(`Scanned ${current}/${total} - Found ${found} wallets`); * } * }); * * console.log(`Found ${result.discovered.length} wallets`); * result.discovered.forEach(w => { * console.log(`Index ${w.index}: ${formatEther(w.nativeBalance.amount)} ETH`); * }); * ``` */ async discoverWallets( connection: JsonRpcProvider | PublicClient, options?: WalletDiscoveryOptions ): Promise { const startTime = Date.now(); // Default options - parallel checking for speed const checkInParallel = options?.checkInParallel ?? true; // Default to parallel const opts: Required = { startIndex: options?.startIndex ?? 0, maxIndex: options?.maxIndex ?? 100, gapLimit: options?.gapLimit ?? 20, minBalance: options?.minBalance ?? 0n, includeZeroBalance: options?.includeZeroBalance ?? false, includePrivateKeys: options?.includePrivateKeys ?? false, checkInParallel, batchSize: options?.batchSize ?? 10, // Larger batch for better performance checkDelay: options?.checkDelay ?? (checkInParallel ? 200 : 50), onProgress: options?.onProgress ?? (() => { }), onDiscovered: options?.onDiscovered ?? (() => { }), }; const discovered: DiscoveredWallet[] = []; let gapCounter = 0; let highestIndex = opts.startIndex; let stoppedByGapLimit = false; // Determine if we're using viem or ethers const isViem = 'readContract' in connection; if (opts.checkInParallel) { // Parallel checking with batches for (let i = opts.startIndex; i <= opts.maxIndex; i += opts.batchSize) { if (gapCounter >= opts.gapLimit) { stoppedByGapLimit = true; break; } const batchIndices = Array.from( { length: Math.min(opts.batchSize, opts.maxIndex - i + 1) }, (_, idx) => i + idx ); const batchResults = await Promise.all( batchIndices.map(index => this.checkWalletBalance(index, connection, isViem)) ); for (let j = 0; j < batchResults.length; j++) { const result = batchResults[j]; const index = batchIndices[j]; highestIndex = index; if (result) { const meetsMinBalance = result.nativeBalance.amount >= opts.minBalance; if (meetsMinBalance || opts.includeZeroBalance) { if (!opts.includePrivateKeys) { delete result.privateKey; } discovered.push(result); opts.onDiscovered(result); if (meetsMinBalance) { gapCounter = 0; // Reset gap counter } else { gapCounter++; } } else { gapCounter++; } } else { gapCounter++; } opts.onProgress(index + 1 - opts.startIndex, opts.maxIndex - opts.startIndex + 1, discovered.length); if (gapCounter >= opts.gapLimit) { stoppedByGapLimit = true; break; } } // Delay between batches if (i + opts.batchSize <= opts.maxIndex && !stoppedByGapLimit) { await this.sleep(opts.checkDelay); } } } else { // Sequential checking for (let index = opts.startIndex; index <= opts.maxIndex; index++) { if (gapCounter >= opts.gapLimit) { stoppedByGapLimit = true; break; } const result = await this.checkWalletBalance(index, connection, isViem); highestIndex = index; if (result) { const meetsMinBalance = result.nativeBalance.amount >= opts.minBalance; if (meetsMinBalance || opts.includeZeroBalance) { if (!opts.includePrivateKeys) { delete result.privateKey; } discovered.push(result); opts.onDiscovered(result); if (meetsMinBalance) { gapCounter = 0; // Reset gap counter } else { gapCounter++; } } else { gapCounter++; } } else { gapCounter++; } opts.onProgress(index + 1 - opts.startIndex, opts.maxIndex - opts.startIndex + 1, discovered.length); // Delay between checks if (index < opts.maxIndex && !stoppedByGapLimit) { await this.sleep(opts.checkDelay); } } } // Calculate total balance const totalBalance = discovered.reduce((sum, wallet) => sum + wallet.nativeBalance.amount, 0n); const duration = Date.now() - startTime; return { discovered, scannedIndices: highestIndex - opts.startIndex + 1, highestIndex, totalBalance, stoppedByGapLimit, duration, }; } /** * Check balance for a single wallet index * @private */ private async checkWalletBalance( index: number, connection: JsonRpcProvider | PublicClient, isViem: boolean, maxRetries: number = 3 ): Promise { for (let attempt = 0; attempt < maxRetries; attempt++) { try { // Derive wallet const { privateKey } = this.generatePrivateKey(index); const wallet = new ethers.Wallet(privateKey); const address = wallet.address; // Get balance let balanceWei: bigint; if (isViem) { // Using viem PublicClient const viemConnection = connection as PublicClient; balanceWei = await viemConnection.getBalance({ address: address as Hex }); } else { // Using ethers JsonRpcProvider const ethersConnection = connection as JsonRpcProvider; balanceWei = await ethersConnection.getBalance(address); } // Format balance const formatted = Number(ethers.formatEther(balanceWei)); return { index, address, derivationPath: `${this.derivationPath}${index}'`, nativeBalance: { amount: balanceWei, formatted, symbol: 'ETH', // Generic - could be MATIC, BNB, etc. }, privateKey, }; } catch (error) { if (attempt === maxRetries - 1) { console.warn(`Failed to check wallet at index ${index} after ${maxRetries} attempts:`, error); return null; } // Exponential backoff: 1s, 2s, 4s const delay = Math.pow(2, attempt) * 1000; await this.sleep(delay); } } return null; } /** * Sleep utility * @private */ private sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Discover savings pockets with native token balances using BIP-44 derivation * * Scans pocket account indices to find pockets containing native tokens. * Pockets use derivation path: m/44'/60'/{accountIndex+1}'/0/{walletIndex} * * @param connection - JsonRpcProvider or Viem PublicClient instance * @param options - Discovery options (gap limit, parallel checking, callbacks, etc.) * @returns Discovery result with found pockets and statistics * * @example * ```typescript * const vm = new EVMVM(seed); * const provider = new JsonRpcProvider('https://eth.llamarpc.com'); * * // Discover pockets for wallet index 0 * const result = await vm.discoverPockets(provider, { * walletIndex: 0, * gapLimit: 20, * onDiscovered: (pocket) => console.log(`Found pocket at account ${pocket.index}`) * }); * * console.log(`Found ${result.discovered.length} pockets with funds`); * ``` */ async discoverPockets( connection: JsonRpcProvider | PublicClient, options?: PocketDiscoveryOptions ): Promise { const startTime = Date.now(); // Default options - parallel checking for speed const walletIndex = options?.walletIndex ?? 0; const checkInParallel = options?.checkInParallel ?? true; const opts: Required = { startIndex: options?.startIndex ?? 0, maxIndex: options?.maxIndex ?? 100, gapLimit: options?.gapLimit ?? 20, minBalance: options?.minBalance ?? 0n, includeZeroBalance: options?.includeZeroBalance ?? false, includePrivateKeys: options?.includePrivateKeys ?? false, checkInParallel, batchSize: options?.batchSize ?? 10, checkDelay: options?.checkDelay ?? (checkInParallel ? 200 : 50), onProgress: options?.onProgress ?? (() => { }), onDiscovered: options?.onDiscovered ?? (() => { }), walletIndex, }; const discovered: DiscoveredWallet[] = []; let gapCounter = 0; let highestIndex = opts.startIndex; let stoppedByGapLimit = false; // Determine if we're using viem or ethers const isViem = 'readContract' in connection; if (opts.checkInParallel) { // Parallel checking with batches for (let i = opts.startIndex; i <= opts.maxIndex; i += opts.batchSize) { if (gapCounter >= opts.gapLimit) { stoppedByGapLimit = true; break; } const batchIndices = Array.from( { length: Math.min(opts.batchSize, opts.maxIndex - i + 1) }, (_, idx) => i + idx ); const batchResults = await Promise.all( batchIndices.map(index => this.checkPocketBalance(index, walletIndex, connection, isViem)) ); for (let j = 0; j < batchResults.length; j++) { const result = batchResults[j]; const index = batchIndices[j]; highestIndex = index; if (result) { const meetsMinBalance = result.nativeBalance.amount >= opts.minBalance; if (meetsMinBalance || opts.includeZeroBalance) { if (!opts.includePrivateKeys) { delete result.privateKey; } discovered.push(result); opts.onDiscovered(result); if (meetsMinBalance) { gapCounter = 0; // Reset gap counter } else { gapCounter++; } } else { gapCounter++; } } else { gapCounter++; } opts.onProgress(index + 1 - opts.startIndex, opts.maxIndex - opts.startIndex + 1, discovered.length); if (gapCounter >= opts.gapLimit) { stoppedByGapLimit = true; break; } } // Delay between batches if (i + opts.batchSize <= opts.maxIndex && !stoppedByGapLimit) { await this.sleep(opts.checkDelay); } } } else { // Sequential checking for (let index = opts.startIndex; index <= opts.maxIndex; index++) { if (gapCounter >= opts.gapLimit) { stoppedByGapLimit = true; break; } const result = await this.checkPocketBalance(index, walletIndex, connection, isViem); highestIndex = index; if (result) { const meetsMinBalance = result.nativeBalance.amount >= opts.minBalance; if (meetsMinBalance || opts.includeZeroBalance) { if (!opts.includePrivateKeys) { delete result.privateKey; } discovered.push(result); opts.onDiscovered(result); if (meetsMinBalance) { gapCounter = 0; // Reset gap counter } else { gapCounter++; } } else { gapCounter++; } } else { gapCounter++; } opts.onProgress(index + 1 - opts.startIndex, opts.maxIndex - opts.startIndex + 1, discovered.length); // Delay between checks if (index < opts.maxIndex && !stoppedByGapLimit) { await this.sleep(opts.checkDelay); } } } // Calculate total balance const totalBalance = discovered.reduce((sum, wallet) => sum + wallet.nativeBalance.amount, 0n); const duration = Date.now() - startTime; return { discovered, scannedIndices: highestIndex - opts.startIndex + 1, highestIndex, totalBalance, stoppedByGapLimit, duration, }; } /** * Check balance for a single pocket at account index * @private */ private async checkPocketBalance( accountIndex: number, walletIndex: number, connection: JsonRpcProvider | PublicClient, isViem: boolean, maxRetries: number = 3 ): Promise { for (let attempt = 0; attempt < maxRetries; attempt++) { try { // Pocket derivation: m/44'/60'/{accountIndex+1}'/0/{walletIndex} const pocketIndex = accountIndex + 1; const derivationPath = `m/44'/60'/${pocketIndex}'/0/${walletIndex}`; // Derive pocket const { privateKey } = EVMDeriveChildPrivateKey(this.seed, walletIndex, `m/44'/60'/${pocketIndex}'/0/`); const wallet = new ethers.Wallet(privateKey); const address = wallet.address; // Get balance let balanceWei: bigint; if (isViem) { // Using viem PublicClient const viemConnection = connection as PublicClient; balanceWei = await viemConnection.getBalance({ address: address as Hex }); } else { // Using ethers JsonRpcProvider const ethersConnection = connection as JsonRpcProvider; balanceWei = await ethersConnection.getBalance(address); } // Format balance const formatted = Number(ethers.formatEther(balanceWei)); return { index: accountIndex, address, derivationPath, nativeBalance: { amount: balanceWei, formatted, symbol: 'ETH', // Generic - could be MATIC, BNB, etc. }, privateKey, }; } catch (error) { if (attempt === maxRetries - 1) { console.warn(`Failed to check pocket at account ${accountIndex} after ${maxRetries} attempts:`, error); return null; } // Exponential backoff: 1s, 2s, 4s const delay = Math.pow(2, attempt) * 1000; await this.sleep(delay); } } return null; } } export class EVMChainAddress extends ChainAddress { /** * Create an EVM chain address wrapper with an attached public client. * * @param config - Chain configuration. * @param address - Wallet address. * @param index - Optional derivation index. */ constructor(config: ChainWalletConfig, address: string, index?: number) { const connection = createEvmPublicClient(config); super(config, address, index); this.connection = connection; } /** * Get native balance for this address. * * @returns Native balance details. */ async getNativeBalance(): Promise { return await getEvmNativeBalance(this.address, this.connection!); } /** * Get ERC-20 token balance for this address. * * @param tokenAddress - ERC-20 contract address. * @returns Token balance details. */ async getTokenBalance(tokenAddress: string): Promise { return await getEvmTokenBalance(this.address, tokenAddress, this.connection!); } /** * Discover fungible tokens held by this address. * * @returns Discovered token balances. */ async discoverToken(): Promise[]> { return await discoverEvmTokens(this.address, this.config); } /** * Discover NFTs held by this address. * * @returns Discovered NFTs. */ async discoverNFT(): Promise { return await discoverEvmNFTs(this.address, this.config); } /** * Retrieve transaction history for this address. * * @returns Parsed transaction history entries. */ async getTransactionHistory(): Promise { return await getEvmTransactionHistorySafe(this.connection!, this.address); } //add gas estimation method here? async estimateGas(transaction: Call): Promise { try { const gasEstimate = await this.connection!.estimateGas(transaction); return gasEstimate; } catch (error) { console.error('Gas estimation failed:', error); throw new Error('Failed to estimate gas for the transaction.'); } } /** * Fetch price data for the provided token addresses. * * @param tokenAddresses - Token contract addresses. * @returns Price response. */ async getPrices(tokenAddresses: string[]): Promise { return await getSvmPricesForTokens(this.config, tokenAddresses); } } export class EVMChainWallet extends ChainWallet { wallet: WalletClient private smartWallet?: EVMSmartWallet // private savingsManager?: SavingsManager // private smartSavingsManager?: SmartSavingsManager /** * Create an EVM wallet bound to a chain configuration and private key. * * @param config - Chain configuration. * @param privateKey - Hex private key (with or without `0x` prefix). * @param index - Wallet index. */ constructor(config: ChainWalletConfig, privateKey: string, index: number) { privateKey = privateKey.startsWith('0x') ? privateKey : `0x${privateKey}` const connection = createEvmPublicClient(config); const account = privateKeyToAccount(privateKey as Hex) const wallet = createEvmWalletClient(config, account); const address = account.address super(config, address, privateKey, index); this.privateKey = privateKey; this.wallet = wallet; this.connection = connection; this.address = address } // ============================================ // Smart Wallet Extension Methods // ============================================ /** * Check if Account Abstraction is supported on this chain * @returns true if aaSupport is enabled in config */ private isAASupportedByChain(): boolean { return this.config.aaSupport?.enabled === true; } /** * Convert entropy text into a hex string. * * @param entropy - Entropy string. * @returns Hex-encoded string. */ convertFromEntropyToPrivateKey = (entropy: string): string => { return toHex(entropy) } /** * Check if smart wallet is initialized and ready for AA operations * @returns true if extend() has been called and smart wallet exists */ private isSmartWalletInitialized(): boolean { return !!this.smartWallet; } // createSavingsManager(mnemonic: string, index: number = 0, chain: ChainWalletConfig) { // return new SavingsManager(mnemonic, index, (chain)) // } /** * Validate that AA is available for sponsored transactions * @throws Error with helpful message if AA is not available */ private validateAAAvailability(): void { if (!this.isSmartWalletInitialized()) { if (this.isAASupportedByChain()) { throw new Error( 'Smart wallet not initialized. Call await wallet.extend() before using sponsored transactions.\n' + `Example: await wallet.extend()` ); } else { throw new Error( 'This chain does not support Account Abstraction (AA) for sponsored transactions.\n' + 'To use sponsored transactions, you need:\n' + '1. A chain that supports EIP-4337 and EIP-7702\n' + '2. Configure aaSupport in your chain config, OR\n' + '3. Manually call extend() with bundlerUrl and paymasterUrl' ); } } } /** * Extend wallet with smart wallet capabilities * Enables Account Abstraction (EIP-4337) and EIP-7702 features * * @param options - Smart wallet configuration (optional if bundlerUrl is in config) * @returns EVMSmartWallet instance * * @example * // Using bundlerUrl from chainConfig * const smartWallet = await evmWallet.extend(); * * @example * // Or provide bundlerUrl directly * const smartWallet = await evmWallet.extend({ * bundlerUrl: 'https://api.pimlico.io/v2/...' * }); */ async extend(options: SmartWalletOptions = {}): Promise { if (!this.smartWallet) { // Merge options with aaSupport config (options take priority) const mergedOptions: SmartWalletOptions = { ...options, aaConfig: this.config.aaSupport, bundlerUrl: options.bundlerUrl || this.config.aaSupport?.bundlerUrl, paymasterUrl: options.paymasterUrl || this.config.aaSupport?.paymasterUrl, entryPointVersion: options.entryPointVersion || this.config.aaSupport?.entryPoints?.[0]?.version }; // Validate bundlerUrl is available if (!mergedOptions.bundlerUrl) { throw new Error( 'bundlerUrl is required to enable smart wallet features.\n' + 'Provide it via:\n' + '1. extend({ bundlerUrl: "..." }), OR\n' + '2. Configure aaSupport in your chain config' ); } // Create viem chain object from config const chain = { id: this.config.chainId, name: this.config.name, nativeCurrency: { name: this.config.nativeToken.name, symbol: this.config.nativeToken.symbol, decimals: this.config.nativeToken.decimals }, rpcUrls: { default: { http: [this.config.rpcUrl] } }, blockExplorers: { default: { name: this.config.name + " Explorer", url: this.config.explorerUrl } }, testnet: this.config.testnet || false }; this.smartWallet = new EVMSmartWallet( this.privateKey, chain, mergedOptions ); // Auto-initialize if option is set (default: true) if (options.autoInitialize !== false) { await this.smartWallet.initialize(); } } return this.smartWallet; } /** * Check if smart wallet is enabled * * @returns true if extend() has been called */ hasSmartWallet(): boolean { return !!this.smartWallet; } /** * Get smart wallet instance * * @returns EVMSmartWallet instance or undefined */ getSmartWallet(): EVMSmartWallet | undefined { return this.smartWallet; } /** * Get smart wallet address (if initialized) * * @returns Smart wallet address or undefined */ getSmartWalletAddress(): string | undefined { if (!this.smartWallet) { return undefined; } try { return this.smartWallet.getAddress(); } catch { return undefined; } } // ============================================ // Existing Wallet Methods // ============================================ /** * Get the underlying viem wallet client. * * @returns Wallet client instance. */ getWallet(): WalletClient { return this.wallet } /** * Get this wallet's address. * * @returns Wallet address. */ generateAddress(): string { return this.address; } /** * Get native token balance for this wallet. * * @returns Native balance details. */ async getNativeBalance(): Promise { // Implement native balance retrieval logic here return await getEvmNativeBalance(this.address, this.connection!); } /** * Get ERC-20 token balance for this wallet. * * @param tokenAddress - ERC-20 contract address. * @returns Token balance details. */ async getTokenBalance(tokenAddress: string): Promise { // Implement token balance retrieval logic here return await getEvmTokenBalance(this.address, tokenAddress, this.connection!); } /** * Get ERC-20 token metadata. * * @param tokenAddress - ERC-20 contract address. * @returns Token metadata returned by `getTokenInfo`. */ async getTokenInfo(tokenAddress: string) { return await EVMVM.getTokenInfo(tokenAddress as Hex, this.connection!) } /** * Discover fungible tokens held by this wallet. * * @returns Discovered token balances. */ async discoverToken(): Promise[]> { // Implement token discovery logic here return await discoverEvmTokens(this.address, this.config); } /** * Discover NFTs held by this wallet. * * @returns Discovered NFTs. */ async discoverNFT(): Promise { // Implement NFT discovery logic here return await discoverEvmNFTs(this.address, this.config); } /** * Send native tokens to another address. * * @param to - Recipient address. * @param amount - Amount in native token units. * @returns Transaction result. */ async transferNative(to: string, amount: number): Promise { const wallet = this.getWallet(); return await sendNativeToken(wallet, this.connection!, to as Hex, amount.toString(), this.config.confirmationNo || 5); } /** * Send ERC-20 tokens to another address. * * @param tokenAddress - Token info containing contract address. * @param to - Recipient address. * @param amount - Token amount to transfer. * @returns Transaction result. */ async transferToken(tokenAddress: TokenInfo, to: string, amount: number): Promise { const wallet = this.getWallet(); return await sendERC20Token(wallet, this.connection!, tokenAddress.address as Hex, to as Hex, BigInt(amount.toString()), this.config.confirmationNo || 5); } /** * Get transaction history for this wallet address. * * @returns Parsed transaction history entries. */ async getTransactionHistory(): Promise { const wallet = this.getWallet(); let res: EVMTransactionHistoryItem return await getEvmTransactionHistorySafe(this.connection!, this.address); } /** * Fetch prices for token contracts on this wallet's chain. * * @param tokenAddresses - Token contract addresses. * @returns Price response. */ async getPrices(tokenAddresses: string[]): Promise { return await getSvmPricesForTokens(this.config, tokenAddresses); } // Updated swap method signature to match base class so created another method to use it inside swap /** * Swap tokens using the chain's swap integration. * * @param tokenAddress - Source token info. * @param to - Destination token or recipient address, depending on integration. * @param amount - Amount to swap. * @param slippage - Slippage tolerance in basis points. * @returns Transaction result. * @throws Error Always throws because this method is not implemented. */ async swap( tokenAddress: TokenInfo, to: string, amount: number, slippage: number = 50 ): Promise { throw new Error("Not Implemented") } // Helper method for EVMChainWallet class /** * Build a standardized failed swap result. * * @param message - Failure reason. * @returns Failed Debonk swap result. */ private fail(message: string): DebonkSwapResult { return { success: false, hash: "", error: message }; } /** * Approve ERC-20 token allowance for a spender. * * @param params - Approval parameters. * @param params.tokenAddress - ERC-20 contract address. * @param params.spender - Spender address to approve. * @param params.amountRaw - Raw allowance amount. * @param params.confirmations - Confirmation blocks to wait for. * @param params.gasLimit - Optional gas limit (currently unused in this wrapper). * @returns Transaction result. */ async approveToken(params: { tokenAddress: string spender: string amountRaw: string | bigint confirmations?: number gasLimit?: string | bigint }): Promise { const signer = this.getWallet() const r = await approveToken( signer, this.connection!, params.tokenAddress as Hex, params.spender as Hex, BigInt(params.amountRaw), params.confirmations ?? this.config.confirmationNo ?? 1, ) return { hash: r.hash, success: r.success, } } /** * Sign an arbitrary message with this wallet account. * * @param message - Message to sign. * @returns Hex signature string. * @throws Error if wallet account is unavailable. */ async signMessage(message: string): Promise { const signer = this.wallet if (!signer.account) { throw new Error("Account is required for signing, signer.account from this.wallet is undefined") } return signer.signMessage({ message, account: signer.account?.address }) } }