UNPKG

p-sdk-wallet

Version:

A comprehensive wallet SDK for React Native (pwc), supporting multi-chain and multi-account features.

1,058 lines 51.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Vault = void 0; const HDKeyring_1 = require("./keyrings/HDKeyring"); const SimpleKeyring_1 = require("./keyrings/SimpleKeyring"); const SolanaKeyring_1 = require("./keyrings/SolanaKeyring"); const ChainService_1 = require("./chain/ChainService"); const SolanaChainService_1 = require("./chain/SolanaChainService"); const EncryptionService_1 = require("./crypto/EncryptionService"); const chains_1 = require("./config/chains"); const constants_1 = require("./config/constants"); const ethers_1 = require("ethers"); const MultiTransferService_1 = require("./services/MultiTransferService"); const NFTService_1 = require("./services/nft/NFTService"); const QRCodeService_1 = require("./services/QRCodeService"); class Vault { /** * Creates a new Vault instance with the specified keyrings. * @param keyrings - Array of keyrings to initialize the vault with. Defaults to empty array. */ constructor(keyrings = [], config) { this.keyrings = []; this.chainId = '1'; // Default to Ethereum mainnet this.keyrings = keyrings; this.config = config; if (config?.rpcUrls) { require('./config/chains').setGlobalRPCConfig(config.rpcUrls); } if (config?.explorerUrls) { require('./config/chains').setGlobalExplorerConfig(config.explorerUrls); } if (config?.gasConfig) { require('./config/gas').setGlobalGasConfig(config.gasConfig); } if (config?.networkGasConfig) { require('./config/gas').setGlobalNetworkGasConfig(config.networkGasConfig); } if (config?.defaultChainId) { this.chainId = config.defaultChainId; } } // --- Vault Creation / Loading --- /** * Creates a new Vault with a fresh mnemonic. * @param password - The password used to encrypt the vault data * @param chainType - The type of blockchain to support ('evm' for Ethereum-compatible chains or 'solana' for Solana). Defaults to 'evm' * @returns Promise resolving to an object containing the created vault instance and its encrypted data * @throws Error if mnemonic generation or keyring initialization fails */ static async createNew(password, chainType = 'evm', config) { const mnemonic = EncryptionService_1.EncryptionService.generateMnemonic(); if (chainType === 'solana') { const solanaKeyring = new SolanaKeyring_1.SolanaKeyring(mnemonic); const vault = new Vault([solanaKeyring], config); vault.chainId = 'solana'; // Set Solana chain ID const encryptedVault = await vault.encrypt(password); return { vault, encryptedVault }; } else { const hdKeyring = new HDKeyring_1.HDKeyring(mnemonic); await hdKeyring.initialize(); const vault = new Vault([hdKeyring], config); vault.chainId = '1'; // Set Ethereum mainnet as default const encryptedVault = await vault.encrypt(password); return { vault, encryptedVault }; } } /** * Creates a new Vault from an existing mnemonic phrase. * @param mnemonic - The existing mnemonic phrase (12, 15, 18, 21, or 24 words) * @param password - The password used to encrypt the vault data * @param chainType - The type of blockchain to support ('evm' for Ethereum-compatible chains or 'solana' for Solana). Defaults to 'evm' * @returns Promise resolving to an object containing the created vault instance and its encrypted data * @throws Error if mnemonic is invalid or keyring initialization fails */ static async createFromMnemonic(mnemonic, password, chainType = 'evm', config) { if (chainType === 'solana') { const solanaKeyring = new SolanaKeyring_1.SolanaKeyring(mnemonic); const vault = new Vault([solanaKeyring], config); vault.chainId = 'solana'; // Set Solana chain ID const encryptedVault = await vault.encrypt(password); return { vault, encryptedVault }; } else { const hdKeyring = new HDKeyring_1.HDKeyring(mnemonic); await hdKeyring.initialize(); const vault = new Vault([hdKeyring], config); vault.chainId = '1'; // Set Ethereum mainnet as default const encryptedVault = await vault.encrypt(password); return { vault, encryptedVault }; } } /** * Loads a Vault from its encrypted state using the provided password. * @param password - The password used to decrypt the vault data * @param encryptedVault - The encrypted vault data to decrypt and load * @returns Promise resolving to the loaded Vault instance * @throws Error if password is incorrect or decryption fails */ static async load(password, encryptedVault, config) { const decryptedString = await EncryptionService_1.EncryptionService.decrypt(encryptedVault, password); const serializedKeyrings = JSON.parse(decryptedString); const keyrings = []; for (const sKeyring of serializedKeyrings) { if (sKeyring.type === 'HD') { keyrings.push(await HDKeyring_1.HDKeyring.deserialize(sKeyring)); } else if (sKeyring.type === 'Simple') { keyrings.push(SimpleKeyring_1.SimpleKeyring.deserialize(sKeyring)); } else if (sKeyring.type === 'Solana') { keyrings.push(await SolanaKeyring_1.SolanaKeyring.deserialize(sKeyring)); } } const vault = new Vault(keyrings, config); // Set chain ID based on keyring type if (keyrings.some(k => k instanceof SolanaKeyring_1.SolanaKeyring)) { vault.chainId = 'solana'; } else { vault.chainId = '1'; // Default to Ethereum mainnet } return vault; } // --- Account Management --- /** * Adds a new account derived from the HD keyring using BIP-44 derivation. * @returns Promise resolving to the newly created account information * @throws Error if no HD keyring is available in the vault */ async addNewHDAccount() { const hdKeyring = this.keyrings.find(k => k instanceof HDKeyring_1.HDKeyring); if (!hdKeyring) { throw new Error('No HD keyring available to create new accounts.'); } const newAddress = await hdKeyring.addNewAccount(); return { address: newAddress, type: 'HD', name: `Account ${hdKeyring.accounts.length}` }; } /** * Adds a new Solana account derived from the Solana keyring. * @returns Promise resolving to the newly created Solana account information * @throws Error if no Solana keyring is available in the vault */ async addNewSolanaAccount() { const solanaKeyring = this.keyrings.find(k => k instanceof SolanaKeyring_1.SolanaKeyring); if (!solanaKeyring) { throw new Error('No Solana keyring available to create new accounts.'); } const newAddress = await solanaKeyring.addNewAccount(); return { address: newAddress, type: 'Solana', name: `Solana ${solanaKeyring.accounts.length}` }; } /** * Imports an account from a private key and adds it to the vault. * @param privateKey - The private key to import (hex string without '0x' prefix for EVM, base58 for Solana) * @returns Promise resolving to the imported account information * @throws Error if the private key is already managed by an HD keyring or if import fails */ async importAccount(privateKey) { // Prevent importing a private key that's already managed by the HD keyring for (const keyring of this.keyrings) { if (keyring instanceof HDKeyring_1.HDKeyring) { const pk = await keyring.getPrivateKeyForAddress(ChainService_1.ChainService.getAddress(privateKey)).catch(() => null); if (pk) { throw new Error("This private key is already part of your mnemonic-derived accounts."); } } } const newKeyring = new SimpleKeyring_1.SimpleKeyring(privateKey); this.keyrings.push(newKeyring); return { address: newKeyring.address, type: newKeyring.type, name: `Imported ${this.keyrings.filter(k => k.type === 'Simple').length}` }; } /** * Returns a list of all accounts managed by the vault across all keyrings. * @returns Array of Account objects representing all accounts in the vault */ getAccounts() { const accounts = []; let hdAccountCount = 1; let importedAccountCount = 1; let solanaAccountCount = 1; for (const keyring of this.keyrings) { if (keyring instanceof HDKeyring_1.HDKeyring) { for (const address of keyring.accounts) { accounts.push({ address, type: 'HD', name: `Account ${hdAccountCount++}`, }); } } else if (keyring instanceof SimpleKeyring_1.SimpleKeyring) { accounts.push({ address: keyring.address, type: 'Simple', name: `Imported ${importedAccountCount++}`, }); } else if (keyring instanceof SolanaKeyring_1.SolanaKeyring) { for (const address of keyring.accounts) { accounts.push({ address, type: 'Solana', name: `Solana ${solanaAccountCount++}`, }); } } } return accounts; } /** * Exports the mnemonic phrase from the vault for backup purposes. * Requires password verification and includes rate limiting for security. * @param password - The vault password to verify before exporting * @returns Promise resolving to the mnemonic phrase as a string * @throws Error if password is incorrect, no mnemonic exists, or rate limit exceeded */ async exportMnemonic(password) { // Rate limiting check const vaultId = this.getVaultId(); const now = Date.now(); const attempts = Vault.exportAttempts.get(vaultId); if (attempts) { if (attempts.count >= Vault.MAX_EXPORT_ATTEMPTS) { const timeRemaining = Vault.EXPORT_COOLDOWN_MS - (now - attempts.lastAttempt); if (timeRemaining > 0) { throw new Error(`Too many export attempts. Please wait ${Math.ceil(timeRemaining / 1000)} seconds before trying again.`); } else { // Reset attempts after cooldown Vault.exportAttempts.delete(vaultId); } } } const hdKeyring = this.keyrings.find(k => k instanceof HDKeyring_1.HDKeyring); if (!hdKeyring) { throw new Error('This vault does not have a mnemonic.'); } // Update attempt counter const currentAttempts = Vault.exportAttempts.get(vaultId) || { count: 0, lastAttempt: 0 }; currentAttempts.count++; currentAttempts.lastAttempt = now; Vault.exportAttempts.set(vaultId, currentAttempts); // Audit logging (without sensitive data) // console.log(`Mnemonic export attempted for vault ${vaultId}, attempt ${currentAttempts.count}`); try { // Verify password by attempting to decrypt the vault const encryptedVault = await this.encrypt(password); // If encryption succeeds, password is correct const mnemonic = hdKeyring.getMnemonic(); // Reset attempts on successful export Vault.exportAttempts.delete(vaultId); // Audit logging for successful export // console.log(`Mnemonic export successful for vault ${vaultId}`); // Use temporary data protection return EncryptionService_1.EncryptionService.withTemporaryData(mnemonic, (tempMnemonic) => { // Return a copy to avoid direct reference to the original return String(tempMnemonic); }); } catch (error) { // Use constant-time error message to prevent timing attacks throw new Error('Incorrect password. Cannot export mnemonic.'); } } /** * Gets a unique identifier for this vault used for rate limiting purposes. * @returns String identifier based on the first account address */ getVaultId() { // Use the first account address as vault identifier const accounts = this.getAccounts(); return accounts.length > 0 ? accounts[0].address : 'unknown'; } // --- Internal & Utility --- /** * Encrypts the entire vault's state using the provided password. * @param password - The password to use for encryption * @returns Promise resolving to the encrypted vault data * @throws Error if encryption fails */ async encrypt(password) { const serializedKeyrings = this.keyrings.map(k => k.serialize()); const jsonString = JSON.stringify(serializedKeyrings); return EncryptionService_1.EncryptionService.encrypt(jsonString, password); } /** * Retrieves the private key for a specific address from the appropriate keyring. * @param address - The account address to get the private key for * @returns Promise resolving to the private key as a string * @throws Error if the address is not found in any keyring */ async getPrivateKeyFor(address) { for (const keyring of this.keyrings) { if (keyring instanceof HDKeyring_1.HDKeyring && keyring.accounts.includes(address)) { return keyring.getPrivateKeyForAddress(address); } if (keyring instanceof SimpleKeyring_1.SimpleKeyring && keyring.address === address) { return keyring.getPrivateKey(); } if (keyring instanceof SolanaKeyring_1.SolanaKeyring && keyring.accounts.includes(address)) { return keyring.getPrivateKeyForAddress(address); } } throw new Error('Private key not found for the given address.'); } // --- Blockchain Interaction Methods --- /** * Gets the token balance for a specific account and token contract. * @param accountAddress - The account address to check balance for * @param tokenAddress - The contract address of the token * @param chainId - The ID of the blockchain where the token is deployed * @returns Promise resolving to the token balance as a bigint * @throws Error if account or token not found, or chain service fails */ async getTokenBalance(accountAddress, tokenAddress, chainId) { const chainInfo = (0, chains_1.getChainConfig)(chainId); const privateKey = await this.getPrivateKeyFor(accountAddress); if (chainInfo.type === 'solana') { const chainService = new SolanaChainService_1.SolanaChainService(privateKey, chainInfo); return chainService.getTokenBalance(tokenAddress); } else { const chainService = new ChainService_1.ChainService(privateKey, chainInfo); return chainService.getTokenBalance(tokenAddress); } } /** * Sends tokens from a specific account to another address. * @param fromAddress - The sender's account address * @param to - The recipient's address * @param amount - The amount of tokens to send as a string * @param tokenAddress - The contract address of the token to send * @param chainId - The ID of the blockchain for the transaction * @returns Promise resolving to the transaction response with hash and details * @throws Error if insufficient balance, invalid addresses, or transaction fails */ async sendToken(fromAddress, to, amount, tokenAddress, chainId) { const chainInfo = (0, chains_1.getChainConfig)(chainId); const privateKey = await this.getPrivateKeyFor(fromAddress); if (chainInfo.type === 'solana') { const chainService = new SolanaChainService_1.SolanaChainService(privateKey, chainInfo); const result = await chainService.sendToken(tokenAddress, to, amount); return { hash: result.hash, from: result.from, to: result.to, blockNumber: result.blockNumber }; } else { const chainService = new ChainService_1.ChainService(privateKey, chainInfo); const result = await chainService.sendToken(tokenAddress, to, amount); return { hash: result.hash, from: result.from, to: result.to || '', blockNumber: result.blockNumber || undefined }; } } /** * Generates a vanity HD wallet with a specific address prefix. * Uses default configuration from VANITY_WALLET_CONFIG for optimal settings. * The prefix defaults to 'aaa' as configured in VANITY_WALLET_CONFIG.DEFAULT_PREFIX. * * @param password - The password to encrypt the generated vault * @param onProgress - Optional callback function for progress updates (attempts count and current address) * @returns Promise resolving to an object containing the generated vault, encrypted vault, number of attempts, and the found address * @throws Error if generation fails or max attempts exceeded * * @example * ```typescript * // Simple usage - will use default prefix 'aaa' * const result = await Vault.generateVanityHDWallet("mypassword"); * * // With progress callback * const result = await Vault.generateVanityHDWallet( * "mypassword", * (attempts, address) => console.log(`Attempt ${attempts}: ${address}`) * ); * ``` */ static async generateVanityHDWallet(password, onProgress) { // Use default values from config const targetPrefix = constants_1.VANITY_WALLET_CONFIG.DEFAULT_PREFIX; const maxAttempts = constants_1.VANITY_WALLET_CONFIG.DEFAULT_MAX_ATTEMPTS; const caseSensitive = constants_1.VANITY_WALLET_CONFIG.DEFAULT_CASE_SENSITIVE; const chainType = 'evm'; // Default to EVM for simplicity // Validate prefix if (targetPrefix.length > constants_1.VANITY_WALLET_CONFIG.MAX_PREFIX_LENGTH) { throw new Error(`Prefix too long. Maximum length is ${constants_1.VANITY_WALLET_CONFIG.MAX_PREFIX_LENGTH} characters.`); } const normalizedPrefix = caseSensitive ? targetPrefix : targetPrefix.toLowerCase(); let attempts = 0; let foundAddress = ''; let foundMnemonic = ''; while (attempts < maxAttempts) { attempts++; // Generate new mnemonic const mnemonic = EncryptionService_1.EncryptionService.generateMnemonic(); // Create HD keyring and get first address const hdKeyring = new HDKeyring_1.HDKeyring(mnemonic); await hdKeyring.initialize(); const firstAddress = hdKeyring.accounts[0]; // Check if address matches prefix (remove '0x' for EVM) const addressWithoutPrefix = firstAddress.slice(2); const addressToCheck = caseSensitive ? addressWithoutPrefix : addressWithoutPrefix.toLowerCase(); if (addressToCheck.startsWith(normalizedPrefix)) { foundAddress = firstAddress; foundMnemonic = mnemonic; break; } // Progress callback if (onProgress && attempts % constants_1.VANITY_WALLET_CONFIG.PROGRESS_UPDATE_INTERVAL === 0) { onProgress(attempts, firstAddress); } // Non-blocking interval to allow event loop to breathe if (attempts % constants_1.VANITY_WALLET_CONFIG.NON_BLOCKING_INTERVAL === 0) { await new Promise(resolve => setTimeout(resolve, 0)); } } if (!foundAddress) { throw new Error(`Could not generate vanity address with prefix "${targetPrefix}" after ${attempts} attempts. Try using a shorter prefix.`); } // Create vault with the found mnemonic const { vault, encryptedVault } = await Vault.createFromMnemonic(foundMnemonic, password, chainType); return { vault, encryptedVault, attempts, foundAddress }; } /** * Generates a vanity HD wallet optimized for mobile devices. * Uses mobile-optimized settings for faster completion and better UX. * * @param password - The password to encrypt the generated vault * @param onProgress - Optional callback function for progress updates (attempts count and current address) * @param options - Optional configuration options for mobile optimization * @returns Promise resolving to an object containing the generated vault, encrypted vault, number of attempts, and the found address * @throws Error if generation fails, max attempts exceeded, or timeout reached * * @example * ```typescript * // Simple mobile-optimized usage * const result = await Vault.generateVanityHDWalletMobile("mypassword"); * * // With custom options * const result = await Vault.generateVanityHDWalletMobile( * "mypassword", * (attempts, address) => console.log(`Attempt ${attempts}: ${address}`), * { prefix: 'ab', maxAttempts: 10000 } * ); * ``` */ static async generateVanityHDWalletMobile(password, onProgress, options = {}) { // Use mobile-optimized defaults const targetPrefix = options.prefix || constants_1.VANITY_WALLET_CONFIG.MOBILE_DEFAULT_PREFIX; const maxAttempts = options.maxAttempts || constants_1.VANITY_WALLET_CONFIG.MOBILE_DEFAULT_MAX_ATTEMPTS; const caseSensitive = options.caseSensitive ?? constants_1.VANITY_WALLET_CONFIG.DEFAULT_CASE_SENSITIVE; const timeout = options.timeout || constants_1.VANITY_WALLET_CONFIG.MOBILE_TIMEOUT_MS; const chainType = 'evm'; // Default to EVM for simplicity // Validate prefix if (targetPrefix.length > constants_1.VANITY_WALLET_CONFIG.MAX_PREFIX_LENGTH) { throw new Error(`Prefix too long. Maximum length is ${constants_1.VANITY_WALLET_CONFIG.MAX_PREFIX_LENGTH} characters.`); } const normalizedPrefix = caseSensitive ? targetPrefix : targetPrefix.toLowerCase(); let attempts = 0; let foundAddress = ''; let foundMnemonic = ''; const startTime = Date.now(); // Batch processing for better performance const processBatch = async (batchSize) => { const batchPromises = []; for (let i = 0; i < batchSize; i++) { batchPromises.push((async () => { const mnemonic = EncryptionService_1.EncryptionService.generateMnemonic(); const hdKeyring = new HDKeyring_1.HDKeyring(mnemonic); await hdKeyring.initialize(); const address = hdKeyring.accounts[0]; return { address, mnemonic }; })()); } const results = await Promise.all(batchPromises); for (const { address, mnemonic } of results) { attempts++; // Check if address matches prefix (remove '0x' for EVM) const addressWithoutPrefix = address.slice(2); const addressToCheck = caseSensitive ? addressWithoutPrefix : addressWithoutPrefix.toLowerCase(); if (addressToCheck.startsWith(normalizedPrefix)) { return { found: true, address, mnemonic }; } // Progress callback if (onProgress && attempts % constants_1.VANITY_WALLET_CONFIG.MOBILE_PROGRESS_UPDATE_INTERVAL === 0) { onProgress(attempts, address); } // Check timeout if (Date.now() - startTime > timeout) { throw new Error(`Vanity wallet generation timed out after ${timeout}ms. Try using a shorter prefix.`); } } return { found: false }; }; // Process in batches until found or max attempts reached while (attempts < maxAttempts) { const batchSize = Math.min(constants_1.VANITY_WALLET_CONFIG.MOBILE_BATCH_SIZE, maxAttempts - attempts); const result = await processBatch(batchSize); if (result.found) { foundAddress = result.address; foundMnemonic = result.mnemonic; break; } // Non-blocking interval to allow event loop to breathe (more frequent for mobile) if (attempts % constants_1.VANITY_WALLET_CONFIG.MOBILE_NON_BLOCKING_INTERVAL === 0) { await new Promise(resolve => setTimeout(resolve, 0)); } } if (!foundAddress) { throw new Error(`Could not generate vanity address with prefix "${targetPrefix}" after ${attempts} attempts. Try using a shorter prefix.`); } // Create vault with the found mnemonic const { vault, encryptedVault } = await Vault.createFromMnemonic(foundMnemonic, password, chainType); return { vault, encryptedVault, attempts, foundAddress }; } /** * Ultra-optimized vanity wallet generation for maximum performance. * Bypasses heavy HDKeyring initialization and uses direct cryptographic operations. * Similar to web-based vanity generators for maximum speed. * * @param password - The password to encrypt the generated vault * @param onProgress - Optional callback function for progress updates * @param options - Configuration options for ultra-optimized generation * @returns Promise resolving to generated vault data */ static async generateVanityHDWalletUltra(password, onProgress, options = {}) { const targetPrefix = options.prefix || 'a'; const maxAttempts = options.maxAttempts || 100000; const caseSensitive = options.caseSensitive ?? false; const timeout = options.timeout || 15000; // 15 seconds default const batchSize = options.batchSize || 1000; // Larger batches for better performance const chainType = 'evm'; // Validate prefix if (targetPrefix.length > constants_1.VANITY_WALLET_CONFIG.MAX_PREFIX_LENGTH) { throw new Error(`Prefix too long. Maximum length is ${constants_1.VANITY_WALLET_CONFIG.MAX_PREFIX_LENGTH} characters.`); } const normalizedPrefix = caseSensitive ? targetPrefix : targetPrefix.toLowerCase(); let attempts = 0; let foundAddress = ''; let foundMnemonic = ''; const startTime = Date.now(); // Ultra-optimized batch processing with direct crypto operations const processUltraBatch = async (batchSize) => { const batchPromises = []; for (let i = 0; i < batchSize; i++) { batchPromises.push((async () => { // Generate mnemonic directly using existing service const mnemonic = EncryptionService_1.EncryptionService.generateMnemonic(); // Use existing HDKeyring but with minimal overhead const hdKeyring = new HDKeyring_1.HDKeyring(mnemonic); await hdKeyring.initialize(); const address = hdKeyring.accounts[0]; return { address, mnemonic }; })()); } const results = await Promise.all(batchPromises); for (const { address, mnemonic } of results) { attempts++; // Check if address matches prefix (remove '0x' for EVM) const addressWithoutPrefix = address.slice(2); const addressToCheck = caseSensitive ? addressWithoutPrefix : addressWithoutPrefix.toLowerCase(); if (addressToCheck.startsWith(normalizedPrefix)) { return { found: true, address, mnemonic }; } // Progress callback (less frequent for performance) if (onProgress && attempts % 5000 === 0) { onProgress(attempts, address); } // Check timeout if (Date.now() - startTime > timeout) { throw new Error(`Vanity wallet generation timed out after ${timeout}ms. Try using a shorter prefix.`); } } return { found: false }; }; // Process in ultra-batches until found or max attempts reached while (attempts < maxAttempts) { const currentBatchSize = Math.min(batchSize, maxAttempts - attempts); const result = await processUltraBatch(currentBatchSize); if (result.found) { foundAddress = result.address; foundMnemonic = result.mnemonic; break; } // Minimal non-blocking interval for ultra-performance if (attempts % 10000 === 0) { await new Promise(resolve => setTimeout(resolve, 0)); } } if (!foundAddress) { throw new Error(`Could not generate vanity address with prefix "${targetPrefix}" after ${attempts} attempts. Try using a shorter prefix.`); } // Create vault with the found mnemonic const { vault, encryptedVault } = await Vault.createFromMnemonic(foundMnemonic, password, chainType); return { vault, encryptedVault, attempts, foundAddress }; } /** * Transfers native tokens to multiple recipients in a single operation. * Uses batch processing for optimal performance and provides progress tracking. * * @param fromAddress - The sender's account address * @param recipients - Array of recipients with addresses and amounts * @param chainId - The ID of the blockchain for the transaction * @param onProgress - Optional callback for progress updates (completed, total, txHash) * @returns Promise resolving to the multi-transfer result with success/failure details * @throws Error if validation fails, insufficient balance, or transfer fails * * @example * ```typescript * const recipients = [ * { address: '0x123...', amount: '0.1' }, * { address: '0x456...', amount: '0.2' }, * { address: '0x789...', amount: '0.05' } * ]; * * const result = await vault.multiTransferNativeTokens( * '0xmyAddress', * recipients, * '1', // Ethereum * (completed, total, txHash) => console.log(`Completed ${completed}/${total}: ${txHash}`) * ); * * console.log(`Success: ${result.successfulCount}, Failed: ${result.failedCount}`); * ``` */ async multiTransferNativeTokens(fromAddress, recipients, chainId, onProgress) { const chainInfo = (0, chains_1.getChainConfig)(chainId); const privateKey = await this.getPrivateKeyFor(fromAddress); let chainService; if (chainInfo.type === 'solana') { chainService = new SolanaChainService_1.SolanaChainService(privateKey, chainInfo); } else { chainService = new ChainService_1.ChainService(privateKey, chainInfo); } const multiTransferService = new MultiTransferService_1.MultiTransferService(this, chainService, chainId); return multiTransferService.transferNativeTokens(fromAddress, recipients, chainId, { onProgress }); } /** * Transfers ERC-20/SPL tokens to multiple recipients in a single operation. * Uses batch processing for optimal performance and provides progress tracking. * * @param fromAddress - The sender's account address * @param tokenAddress - The contract address of the token to send * @param recipients - Array of recipients with addresses and amounts * @param chainId - The ID of the blockchain for the transaction * @param onProgress - Optional callback for progress updates (completed, total, txHash) * @returns Promise resolving to the multi-transfer result with success/failure details * @throws Error if validation fails, insufficient balance, or transfer fails * * @example * ```typescript * const recipients = [ * { address: '0x1111...', amount: '100' }, * { address: '0x456...', amount: '200' }, * { address: '0x789...', amount: '50' } * ]; * * const result = await vault.multiTransferTokens( * '0xmyAddress', * '0xtokenContract', * recipients, * '1', // Ethereum * (completed, total, txHash) => console.log(`Completed ${completed}/${total}: ${txHash}`) * ); * * console.log(`Success: ${result.successfulCount}, Failed: ${result.failedCount}`); * ``` */ async multiTransferTokens(fromAddress, tokenAddress, recipients, chainId, onProgress) { const chainInfo = (0, chains_1.getChainConfig)(chainId); const privateKey = await this.getPrivateKeyFor(fromAddress); let chainService; if (chainInfo.type === 'solana') { chainService = new SolanaChainService_1.SolanaChainService(privateKey, chainInfo); } else { chainService = new ChainService_1.ChainService(privateKey, chainInfo); } const multiTransferService = new MultiTransferService_1.MultiTransferService(this, chainService, chainId); return multiTransferService.transferTokens(fromAddress, tokenAddress, recipients, chainId, { onProgress }); } /** * Estimates gas cost for a native token transfer. * Useful for displaying gas costs to users before sending transactions. * * @param fromAddress - The sender's account address * @param to - The recipient's address * @param amount - The amount to send as a string (e.g., "0.1") * @param chainId - The ID of the blockchain for the transaction * @returns Promise resolving to the estimated gas cost as a bigint * @throws Error if estimation fails or addresses are invalid * * @example * ```typescript * const gasEstimate = await vault.estimateNativeTransferGas( * '0x1234...', * '0x5678...', * '0.01', // 0.01 ETH * '1' // Ethereum * ); * * console.log('Estimated gas:', gasEstimate.toString()); * console.log('Estimated cost (in ETH):', ethers.formatEther(gasEstimate * gasPrice)); * ``` */ async estimateNativeTransferGas(fromAddress, to, amount, chainId) { const chainInfo = (0, chains_1.getChainConfig)(chainId); const privateKey = await this.getPrivateKeyFor(fromAddress); if (chainInfo.type === 'solana') { const chainService = new SolanaChainService_1.SolanaChainService(privateKey, chainInfo); return chainService.estimateTransactionFee(); } else { const chainService = new ChainService_1.ChainService(privateKey, chainInfo); const tx = { to: to, value: ethers_1.ethers.parseEther(amount) }; return chainService.estimateGas(tx); } } /** * Estimates gas cost for an ERC-20/SPL token transfer. * Useful for displaying gas costs to users before sending token transactions. * * @param fromAddress - The sender's account address * @param tokenAddress - The contract address of the token * @param to - The recipient's address * @param amount - The amount of tokens to send as a string * @param chainId - The ID of the blockchain for the transaction * @returns Promise resolving to the estimated gas cost as a bigint * @throws Error if estimation fails, token contract is invalid, or addresses are invalid * * @example * ```typescript * const gasEstimate = await vault.estimateTokenTransferGas( * '0x1234...', * '0xA0b86a33E6441b8c4C8C8C8C8C8C8C8C8C8C8C8C', // USDC * '0x5678...', * '100', // 100 USDC * '1' // Ethereum * ); * * console.log('Estimated gas for token transfer:', gasEstimate.toString()); * ``` */ async estimateTokenTransferGas(fromAddress, tokenAddress, to, amount, chainId) { const chainInfo = (0, chains_1.getChainConfig)(chainId); const privateKey = await this.getPrivateKeyFor(fromAddress); if (chainInfo.type === 'solana') { const chainService = new SolanaChainService_1.SolanaChainService(privateKey, chainInfo); return chainService.estimateTransactionFee(); } else { const chainService = new ChainService_1.ChainService(privateKey, chainInfo); // Get token info to parse amount correctly const tokenInfo = await chainService.getTokenInfo(tokenAddress); const parsedAmount = ethers_1.ethers.parseUnits(amount, tokenInfo.decimals); // Create contract instance for gas estimation const contract = new ethers_1.ethers.Contract(tokenAddress, [ "function transfer(address to, uint amount) returns (bool)" ], chainService['wallet'].provider); // Estimate gas for token transfer return contract.transfer.estimateGas(to, parsedAmount); } } /** * Estimates gas cost for multi-transfer operations. * Useful for displaying total gas costs before executing batch transfers. * * @param fromAddress - The sender's account address * @param recipients - Array of recipients with addresses and amounts * @param chainId - The ID of the blockchain for the transaction * @param isNative - Whether estimating for native tokens (true) or ERC-20/SPL tokens (false) * @param tokenAddress - Token contract address (required if isNative is false) * @returns Promise resolving to the estimated total gas cost as a bigint * @throws Error if estimation fails or parameters are invalid * * @example * ```typescript * const recipients = [ * { address: '0x1111...', amount: '0.01' }, * { address: '0x2222...', amount: '0.02' }, * { address: '0x3333...', amount: '0.005' } * ]; * * // Estimate for native token multi-transfer * const nativeGasEstimate = await vault.estimateMultiTransferGas( * '0x1234...', * recipients, * '1', // Ethereum * true // Native tokens * ); * * // Estimate for token multi-transfer * const tokenGasEstimate = await vault.estimateMultiTransferGas( * '0x1234...', * recipients, * '1', // Ethereum * false, // ERC-20 tokens * '0xA0b86a33E6441b8c4C8C8C8C8C8C8C8C8C8C8C8C' // USDC * ); * * console.log('Native multi-transfer gas:', nativeGasEstimate.toString()); * console.log('Token multi-transfer gas:', tokenGasEstimate.toString()); * ``` */ async estimateMultiTransferGas(fromAddress, recipients, chainId, isNative = true, tokenAddress) { const chainInfo = (0, chains_1.getChainConfig)(chainId); const privateKey = await this.getPrivateKeyFor(fromAddress); let chainService; if (chainInfo.type === 'solana') { chainService = new SolanaChainService_1.SolanaChainService(privateKey, chainInfo); } else { chainService = new ChainService_1.ChainService(privateKey, chainInfo); } const multiTransferService = new MultiTransferService_1.MultiTransferService(this, chainService, chainId); return multiTransferService.estimateGasCost(recipients, chainId, isNative, tokenAddress); } // --- NFT Methods --- /** * Gets all NFTs owned by an address across multiple collections. * Pure blockchain implementation - reads from smart contracts. * * @param address - Wallet address to check * @param contractAddresses - Array of NFT contract addresses to check * @param options - NFT query options * @returns Promise resolving to array of NFT details * @throws Error if any contract query fails * * @example * ```typescript * const collections = [ * '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D', // BAYC * '0x60E4d786628Fea6478F785A6d7e704777c86a7c6' // MAYC * ]; * * const nfts = await vault.getOwnedNFTs( * '0x1234...', // Wallet address * collections, * { includeMetadata: true } * ); * * console.log(`Found ${nfts.length} owned NFTs`); * nfts.forEach(nft => { * console.log(`${nft.name} from ${nft.contractAddress}`); * }); * ``` */ async getOwnedNFTs(address, contractAddresses, options = {}) { const nftService = new NFTService_1.NFTService(this.chainId, this, address); const nfts = []; for (const contractAddress of contractAddresses) { try { const balance = await nftService.getNFTBalance(address, contractAddress); for (const tokenId of balance.tokenIds) { try { const nftDetail = await nftService.getNFTDetail(contractAddress, tokenId, options); nfts.push(nftDetail); } catch (error) { console.warn(`Failed to get NFT detail for ${contractAddress}:${tokenId}:`, error); } } } catch (error) { console.warn(`Failed to get balance for contract ${contractAddress}:`, error); } } return nfts; } /** * Transfers an NFT from one address to another. * Pure blockchain implementation using smart contract calls. * * @param fromAddress - Sender's address * @param toAddress - Recipient's address * @param contractAddress - NFT contract address * @param tokenId - Token ID to transfer * @returns Promise resolving to transaction response * @throws Error if transfer fails, insufficient permissions, or NFT not owned * * @example * ```typescript * const result = await vault.transferNFT( * '0x1234...', // From address * '0x5678...', // To address * '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D', // BAYC contract * '1234' // Token ID * ); * * console.log('Transfer successful:', result.hash); * ``` */ async transferNFT(fromAddress, toAddress, contractAddress, tokenId) { const chainInfo = (0, chains_1.getChainConfig)(this.chainId); const privateKey = await this.getPrivateKeyFor(fromAddress); if (chainInfo.type === 'solana') { const chainService = new SolanaChainService_1.SolanaChainService(privateKey, chainInfo); // Solana NFT transfer implementation would go here throw new Error('Solana NFT transfer not yet implemented'); } else { const chainService = new ChainService_1.ChainService(privateKey, chainInfo); // Create ERC-721 contract instance const contract = new ethers_1.ethers.Contract(contractAddress, [ "function transferFrom(address from, address to, uint256 tokenId)", "function ownerOf(uint256 tokenId) view returns (address)" ], chainService['wallet']); // Verify ownership const owner = await contract.ownerOf(tokenId); if (owner.toLowerCase() !== fromAddress.toLowerCase()) { throw new Error('NFT is not owned by the specified address'); } // Transfer NFT const tx = await contract.transferFrom(fromAddress, toAddress, tokenId); const receipt = await tx.wait(); return { hash: receipt.hash, from: fromAddress, to: toAddress, blockNumber: receipt.blockNumber }; } } // --- QR Code Methods --- /** * Imports wallet from QR code data. * @param qrString - QR code string to parse * @param password - Password to decrypt the mnemonic * @returns Promise resolving to imported account information * @throws Error if QR data is invalid or import fails * * @example * ```typescript * // Import wallet from scanned QR code * const accounts = await vault.importFromQR(qrString, 'my-password'); * console.log('Imported accounts:', accounts); * ``` */ async importFromQR(qrString, password) { try { const importData = QRCodeService_1.QRCodeService.extractWalletImportData(qrString); // Decrypt the mnemonic const mnemonic = await EncryptionService_1.EncryptionService.decrypt(importData.encryptedMnemonic, password); // Create new keyring from mnemonic let newKeyring; if (importData.chainType === 'solana') { newKeyring = new SolanaKeyring_1.SolanaKeyring(mnemonic); } else { newKeyring = new HDKeyring_1.HDKeyring(mnemonic); await newKeyring.initialize(); } // Add the keyring to vault this.keyrings.push(newKeyring); // Update chain ID if needed if (importData.chainType === 'solana' && this.chainId !== 'solana') { this.chainId = 'solana'; } // Return imported accounts const accounts = []; for (let i = 0; i < Math.min(importData.accountCount, newKeyring.accounts.length); i++) { accounts.push({ address: newKeyring.accounts[i], type: newKeyring.type, name: `${newKeyring.type} ${i + 1}` }); } return accounts; } catch (error) { throw new Error(`Failed to import wallet from QR: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Processes transaction from QR code data. * @param qrString - QR code string to parse * @param fromAddress - Sender's address * @returns Promise resolving to transaction response * @throws Error if QR data is invalid or transaction fails * * @example * ```typescript * // Process transaction from scanned QR code * const result = await vault.processTransactionFromQR(qrString, '0x1234...'); * console.log('Transaction hash:', result.hash); * ``` */ async processTransactionFromQR(qrString, fromAddress) { try { const txData = QRCodeService_1.QRCodeService.extractTransactionData(qrString); // Validate sender address const accounts = this.getAccounts(); const senderAccount = accounts.find(acc => acc.address.toLowerCase() === fromAddress.toLowerCase()); if (!senderAccount) { throw new Error('Sender address not found in wallet accounts'); } // Execute transaction based on type switch (txData.txType) { case 'native': return await this.sendNativeToken(fromAddress, txData.to, txData.amount, txData.chainId); case 'token': if (!txData.tokenAddress) { throw new Error('Token address required for token transfer'); } return await this.sendToken(fromAddress, txData.to, txData.amount, txData.tokenAddress, txData.chainId); case 'nft': if (!txData.tokenAddress || !txData.tokenId) { throw new Error('Token address and token ID required for NFT transfer'); } return await this.transferNFT(fromAddress, txData.to, txData.tokenAddress, txData.tokenId); default: throw new Error(`Unsupported transaction type: ${txData.txType}`); } } catch (error) { throw new Error(`Failed to process transaction from QR: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Sends native tokens (ETH, BNB, MATIC, etc.) from a specific account to another address. * @param fromAddress - The sender's account address * @param to - The recipient's address * @param amount - The amount to send as a string (e.g., "0.1") * @param chainId - The ID of the blockchain for the transaction * @returns Promise resolving to the transaction response * @throws Error if insufficient balance, invalid addresses, or transaction fails * * @example * ```typescript * // Send 0.1 ETH on Ethereum mainnet * const tx = await vault.sendNativeToken(from, to, '0.1', '1'); * console.log('Transaction hash:', tx.hash); * ``` */ async sendNativeToken(fromAddress, to, amount, chainId) { const chainInfo = (0, chains_1.getChainConfig)(chainId); co