UNPKG

p-sdk-wallet

Version:

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

253 lines (252 loc) 11.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MultiTransferService = void 0; const ChainService_1 = require("../chain/ChainService"); const SolanaChainService_1 = require("../chain/SolanaChainService"); const BatchProcessor_1 = require("./BatchProcessor"); const ethers_1 = require("ethers"); const gas_1 = require("../config/gas"); const TokenUtils_1 = require("./TokenUtils"); const chains_1 = require("../config/chains"); /** * Service for handling multi-transfer operations across different blockchain networks. * Supports both native tokens and ERC-20/SPL tokens with batch processing and progress tracking. */ class MultiTransferService { /** * Creates a new MultiTransferService instance. * @param vault - The vault instance containing the accounts * @param chainService - The chain service for the target blockchain * @param chainId - The chain ID for all operations */ constructor(vault, chainService, chainId) { this.vault = vault; this.chainService = chainService; this.chainId = chainId; } /** * Transfers native tokens to multiple recipients. * @param fromAddress - The sender's address * @param recipients - Array of recipients with addresses and amounts * @param chainId - The chain ID for the transfer * @param options - Optional configuration for the transfer operation * @returns Promise resolving to the multi-transfer result * @throws Error if validation fails or insufficient balance */ async transferNativeTokens(fromAddress, recipients, chainId, options = {}) { // Validate inputs const validation = this.validateTransferInputs(fromAddress, recipients, options); if (!validation.isValid) { throw new Error(`Validation failed: ${validation.errors.join(', ')}`); } // Check balance await this.checkBalance(fromAddress, validation.totalAmount, true, undefined, chainId); // Process transfers const batchSize = options.batchSize || 10; const batches = BatchProcessor_1.BatchProcessor.splitIntoBatches(recipients, batchSize); const allSuccessful = []; const allFailed = []; let totalGasUsed = BigInt(0); for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) { const batch = batches[batchIndex]; const batchResult = await BatchProcessor_1.BatchProcessor.processNativeTokenBatch(this.chainService, batch, options.onProgress); allSuccessful.push(...batchResult.transactions); allFailed.push(...batchResult.failed); totalGasUsed += batchResult.gasUsed; // Small delay between batches if (batchIndex < batches.length - 1) { await new Promise(resolve => setTimeout(resolve, 500)); } } return { successful: allSuccessful, failed: allFailed, totalGasUsed, totalAmount: validation.totalAmount, totalRecipients: recipients.length, successfulCount: allSuccessful.length, failedCount: allFailed.length }; } /** * Transfers ERC-20/SPL tokens to multiple recipients. * @param fromAddress - The sender's address * @param tokenAddress - The token contract address * @param recipients - Array of recipients with addresses and amounts * @param chainId - The chain ID for the transfer * @param options - Optional configuration for the transfer operation * @returns Promise resolving to the multi-transfer result * @throws Error if validation fails or insufficient balance */ async transferTokens(fromAddress, tokenAddress, recipients, chainId, options = {}) { // Validate inputs const validation = this.validateTransferInputs(fromAddress, recipients, options); if (!validation.isValid) { throw new Error(`Validation failed: ${validation.errors.join(', ')}`); } // Check token balance await this.checkBalance(fromAddress, validation.totalAmount, false, tokenAddress, chainId); // Process transfers const batchSize = options.batchSize || 10; const batches = BatchProcessor_1.BatchProcessor.splitIntoBatches(recipients, batchSize); const allSuccessful = []; const allFailed = []; let totalGasUsed = BigInt(0); for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) { const batch = batches[batchIndex]; const batchResult = await BatchProcessor_1.BatchProcessor.processTokenBatch(this.chainService, tokenAddress, batch, options.onProgress); allSuccessful.push(...batchResult.transactions); allFailed.push(...batchResult.failed); totalGasUsed += batchResult.gasUsed; // Small delay between batches if (batchIndex < batches.length - 1) { await new Promise(resolve => setTimeout(resolve, 500)); } } return { successful: allSuccessful, failed: allFailed, totalGasUsed, totalAmount: validation.totalAmount, totalRecipients: recipients.length, successfulCount: allSuccessful.length, failedCount: allFailed.length }; } /** * Validates transfer inputs including recipients and amounts. * @param fromAddress - The sender's address * @param recipients - Array of recipients to validate * @param options - Transfer options * @returns Validation result with errors and total amount */ validateTransferInputs(fromAddress, recipients, options) { const errors = []; // Validate sender address if (!fromAddress || fromAddress.trim() === '') { errors.push('Invalid sender address'); } // Validate recipients const recipientValidation = BatchProcessor_1.BatchProcessor.validateRecipients(recipients); if (!recipientValidation.isValid) { errors.push(...recipientValidation.errors); } // Validate batch size if (options.batchSize && (options.batchSize < 1 || options.batchSize > 50)) { errors.push('Batch size must be between 1 and 50'); } // Calculate total amount let totalAmount = '0'; if (recipients.length > 0) { const total = recipients.reduce((sum, recipient) => { return sum + parseFloat(recipient.amount); }, 0); totalAmount = total.toString(); } return { isValid: errors.length === 0, errors, totalAmount }; } /** * Checks if the account has sufficient balance for the transfer. * @param address - The account address to check * @param amount - The amount to transfer * @param isNative - Whether checking native token balance * @param tokenAddress - Token address (for non-native tokens) * @param chainId - The chain ID for the check * @throws Error if insufficient balance */ async checkBalance(address, amount, isNative, tokenAddress, chainId) { let balance; let amountWei; const useChainId = chainId || this.chainId; if (isNative) { balance = await (0, TokenUtils_1.getNativeBalance)(address, useChainId); amountWei = ethers_1.ethers.parseEther(amount); } else if (tokenAddress) { balance = await (0, TokenUtils_1.getTokenBalance)(tokenAddress, address, useChainId); // For token transfers, we need to get token decimals to parse amount correctly // For now, we'll use a default of 18 decimals (most common) // In a real implementation, you'd get this from the token contract amountWei = ethers_1.ethers.parseUnits(amount, 18); } else { throw new Error('Token address required for token balance check'); } if (balance < amountWei) { const balanceEth = isNative ? ethers_1.ethers.formatEther(balance) : ethers_1.ethers.formatUnits(balance, 18); const amountEth = isNative ? ethers_1.ethers.formatEther(amountWei) : ethers_1.ethers.formatUnits(amountWei, 18); throw new Error(`Insufficient balance. Available: ${balanceEth}, Required: ${amountEth}`); } } /** * Estimates gas cost for a multi-transfer operation. * @param recipients - Array of recipients to estimate gas for * @param chainId - The chain ID for the estimation * @param isNative - Whether estimating for native tokens (default: true) * @param tokenAddress - Token contract address (required for non-native tokens) * @returns Promise resolving to estimated gas cost in wei */ async estimateGasCost(recipients, chainId, isNative = true, tokenAddress) { if (recipients.length === 0) { return BigInt(0); } // Use first recipient for estimation const sampleRecipient = recipients[0]; let estimatedGas; try { if (isNative) { // Estimate native token transfer const tx = { to: sampleRecipient.address, value: ethers_1.ethers.parseEther(sampleRecipient.amount) }; if (this.chainService instanceof ChainService_1.ChainService) { estimatedGas = await this.chainService.estimateGas(tx); } else { // For Solana, estimate based on transaction fee estimatedGas = await this.chainService.estimateTransactionFee(); } } else if (tokenAddress) { // Use configuration constant instead of hardcoded value estimatedGas = BigInt(gas_1.GAS_CONFIG.ERC20_TRANSFER); } else { throw new Error('Token address required for token transfer gas estimation'); } // Apply batch multiplier for multiple recipients const totalGas = estimatedGas * BigInt(recipients.length); const adjustedGas = totalGas * BigInt(Math.ceil(gas_1.GAS_CONFIG.BATCH_MULTIPLIER * 100)) / BigInt(100); return adjustedGas; } catch (error) { throw new Error(`Failed to estimate gas cost: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Factory method: Create MultiTransferService from vault, fromAddress, and chainId. * Handles private key and chainService creation internally for simplicity. * @param vault - The vault instance * @param fromAddress - The sender's address * @param chainId - The chain ID * @returns Promise resolving to a MultiTransferService instance */ static async fromVault(vault, fromAddress, chainId) { const chainInfo = (0, chains_1.getChainConfig)(chainId); const privateKey = await vault.getPrivateKeyFor(fromAddress); let chainService; if (chainInfo.type === 'solana') { chainService = new SolanaChainService_1.SolanaChainService(privateKey, chainInfo); } else { chainService = new ChainService_1.ChainService(privateKey, chainInfo); } return new MultiTransferService(vault, chainService, chainId); } } exports.MultiTransferService = MultiTransferService;