diff --git a/src/server.ts b/src/server.ts index 44f2ba7..a0c6d89 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,5 +1,6 @@ import express, { Request, Response, Router } from "express"; import { WalletService } from "./services/wallet.service"; +import { ParallelDistributionService } from "./services/parallel-distribution.service"; import { DiscordService } from "./services/discord.service"; import { CheckBalanceService } from "./services/cronjobs/check-balance.service"; import { config } from "./config"; @@ -11,6 +12,7 @@ import { debugConfiguration, validateAddresses } from "./utils/config-validation const app = express(); const router = Router(); const walletService = new WalletService(); +const parallelDistributionService = new ParallelDistributionService(); const discordService = new DiscordService(); const checkBalanceService = new CheckBalanceService(); @@ -69,6 +71,14 @@ interface DistributeFundsRequest { causeOwnerAddress: string; // Address of the cause owner for fee distribution } +interface ParallelDistributeFundsRequest { + walletAddresses: string[]; + projects: Project[]; + causeId: number; + causeOwnerAddress: string; // Address of the cause owner for fee distribution + floorFactor?: number; +} + // Add JSON parsing middleware app.use(express.json()); @@ -186,6 +196,90 @@ router.post( } ); +// Distribute funds in parallel from multiple wallets +router.post( + "/distribute-funds-parallel", + async (req: Request<{}, {}, ParallelDistributeFundsRequest>, res: Response) => { + try { + console.log("Parallel distribute funds endpoint hit"); + const { walletAddresses, projects, causeId, causeOwnerAddress, floorFactor } = req.body; + + if (!walletAddresses || walletAddresses.length === 0) { + return res.status(400).json({ + success: false, + error: "No wallet addresses provided" + }); + } + + console.log(`Starting parallel distribution for ${walletAddresses.length} wallets`); + + const results = await parallelDistributionService.distributeFundsInParallel({ + walletAddresses, + projects, + causeId, + causeOwnerAddress, + floorFactor + }); + + // Calculate summary statistics + const successfulDistributions = results.filter(r => r.success); + const failedDistributions = results.filter(r => !r.success); + const totalSuccessCount = successfulDistributions.length; + const totalFailureCount = failedDistributions.length; + + console.log(`Parallel distribution completed: ${totalSuccessCount}/${results.length} successful`); + + if (totalSuccessCount === results.length) { + // All distributions successful + console.log(`✅ All parallel distributions completed successfully`); + res.status(200).json({ + success: true, + message: "All parallel distributions completed successfully", + data: { + totalWallets: results.length, + successfulDistributions: totalSuccessCount, + failedDistributions: totalFailureCount, + results + } + }); + } else if (totalSuccessCount > 0) { + // Some distributions successful, some failed + console.log(`⚠️ Parallel distribution completed with some failures: ${totalSuccessCount}/${results.length} successful`); + res.status(207).json({ + success: false, + message: "Parallel distribution completed with some failures", + data: { + totalWallets: results.length, + successfulDistributions: totalSuccessCount, + failedDistributions: totalFailureCount, + results + } + }); + } else { + // All distributions failed + console.log(`❌ All parallel distributions failed`); + res.status(500).json({ + success: false, + message: "All parallel distributions failed", + error: "No successful distributions", + data: { + totalWallets: results.length, + successfulDistributions: totalSuccessCount, + failedDistributions: totalFailureCount, + results + } + }); + } + } catch (error) { + console.error(`❌ Parallel distribution endpoint error:`, error); + res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : "Unknown error", + }); + } + } +); + // Check fee provider status router.get("/fee-status", async (req: Request, res: Response) => { try { diff --git a/src/services/distribution-relationship.test.ts b/src/services/distribution-relationship.test.ts deleted file mode 100644 index 2799e22..0000000 --- a/src/services/distribution-relationship.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { expect } from 'chai'; -import { DistributionRepository } from '../repositories/distribution.repository'; -import { Project } from './fund-allocation.service'; -import { AppDataSource } from '../data-source'; - -describe('Distribution Relationship', () => { - let distributionRepository: DistributionRepository; - - before(async () => { - await AppDataSource.initialize(); - distributionRepository = new DistributionRepository(AppDataSource); - }); - - after(async () => { - await AppDataSource.destroy(); - }); - - describe('One-to-Many Relationship', () => { - it('should save distribution with project shares and retrieve them via relationship', async () => { - // Mock distribution result - const mockProjects: Project[] = [ - { - slug: 'test-project-1', - projectId: 1, - name: 'Test Project 1', - walletAddress: '0x1234567890123456789012345678901234567890', - rank: 1, - score: 95.5, - usdValue: 1.2 - }, - { - slug: 'test-project-2', - projectId: 2, - name: 'Test Project 2', - walletAddress: '0x2345678901234567890123456789012345678901', - rank: 2, - score: 85.0, - usdValue: 1.0 - } - ]; - - const mockDistributionResult = { - walletAddress: '0x3456789012345678901234567890123456789012', - totalBalance: '1000.0', - distributedAmount: '50.0', - transactions: [ - { - to: '0x4567890123456789012345678901234567890123', - amount: '50.0', - transactionHash: '0x7890123456789012345678901234567890123456789012345678901234567890' - } - ], - summary: { - totalRecipients: 2, - totalTransactions: 1, - successCount: 1, - failureCount: 0 - }, - projectsDistributionDetails: [ - { - project: mockProjects[0], - amount: '30.0' - }, - { - project: mockProjects[1], - amount: '20.0' - } - ] - }; - - // Save distribution - const savedDistribution = await distributionRepository.saveDistribution(mockDistributionResult, 101); - - // Retrieve distribution with project shares using relationship - const distributionWithShares = await distributionRepository.findByIdWithProjectShares(savedDistribution.id); - - // Verify the relationship works - expect(distributionWithShares).to.not.be.null; - expect(distributionWithShares!.projectShares).to.have.length(2); - expect(distributionWithShares!.projectShares[0].projectName).to.equal('Test Project 1'); - expect(distributionWithShares!.projectShares[1].projectName).to.equal('Test Project 2'); - - // Verify the foreign key relationship - expect(distributionWithShares!.projectShares[0].distributionId).to.equal(savedDistribution.id); - expect(distributionWithShares!.projectShares[1].distributionId).to.equal(savedDistribution.id); - }); - - it('should retrieve distributions with project shares for a wallet', async () => { - const walletAddress = '0x3456789012345678901234567890123456789012'; - - const distributionsWithShares = await distributionRepository.findByWalletAddressWithProjectShares(walletAddress); - - expect(distributionsWithShares).to.be.an('array'); - - if (distributionsWithShares.length > 0) { - const distribution = distributionsWithShares[0]; - expect(distribution.projectShares).to.be.an('array'); - expect(distribution.walletAddress).to.equal(walletAddress); - } - }); - }); -}); \ No newline at end of file diff --git a/src/services/parallel-distribution.service.test.ts b/src/services/parallel-distribution.service.test.ts new file mode 100644 index 0000000..9097a71 --- /dev/null +++ b/src/services/parallel-distribution.service.test.ts @@ -0,0 +1,155 @@ +import { expect } from 'chai'; +import { ethers } from 'ethers'; +import { ParallelDistributionService, ParallelDistributionRequest } from './parallel-distribution.service'; +import { Project } from './fund-allocation.service'; + +describe('ParallelDistributionService', () => { + let service: ParallelDistributionService; + + beforeEach(() => { + service = new ParallelDistributionService(); + }); + + describe('constructor', () => { + it('should create a ParallelDistributionService instance', () => { + expect(service).to.be.instanceOf(ParallelDistributionService); + }); + }); + + describe('service initialization', () => { + it('should have required properties initialized', () => { + expect(service).to.have.property('walletService'); + expect(service).to.have.property('donationHandlerService'); + expect(service).to.have.property('transactionService'); + expect(service).to.have.property('feeRefillerService'); + expect(service).to.have.property('walletRepository'); + expect(service).to.have.property('provider'); + expect(service).to.have.property('seedPhrase'); + }); + + it('should have provider configured with correct network', () => { + expect(service['provider']).to.be.instanceOf(ethers.JsonRpcProvider); + }); + + it('should have seed phrase configured', () => { + expect(service['seedPhrase']).to.be.a('string'); + expect(service['seedPhrase'].length).to.be.greaterThan(0); + }); + }); + + describe('request validation', () => { + it('should validate ParallelDistributionRequest structure', () => { + const mockProjects: Project[] = [ + { + projectId: 1, + name: 'Test Project 1', + slug: 'test-project-1', + walletAddress: '0x1234567890123456789012345678901234567890', + score: 100 + } + ]; + + const request: ParallelDistributionRequest = { + walletAddresses: ['0x1234567890123456789012345678901234567890'], + projects: mockProjects, + causeId: 1, + causeOwnerAddress: '0x3456789012345678901234567890123456789012', + floorFactor: 0.25 + }; + + expect(request).to.have.property('walletAddresses'); + expect(request).to.have.property('projects'); + expect(request).to.have.property('causeId'); + expect(request).to.have.property('causeOwnerAddress'); + expect(request).to.have.property('floorFactor'); + expect(request.walletAddresses).to.be.an('array'); + expect(request.projects).to.be.an('array'); + expect(request.causeId).to.be.a('number'); + expect(request.causeOwnerAddress).to.be.a('string'); + expect(request.floorFactor).to.be.a('number'); + }); + + it('should validate Project structure', () => { + const project: Project = { + projectId: 1, + name: 'Test Project', + slug: 'test-project', + walletAddress: '0x1234567890123456789012345678901234567890', + score: 100 + }; + + expect(project).to.have.property('projectId'); + expect(project).to.have.property('name'); + expect(project).to.have.property('slug'); + expect(project).to.have.property('walletAddress'); + expect(project).to.have.property('score'); + expect(project.projectId).to.be.a('number'); + expect(project.name).to.be.a('string'); + expect(project.slug).to.be.a('string'); + expect(project.walletAddress).to.be.a('string'); + expect(project.score).to.be.a('number'); + }); + }); + + describe('gas estimation validation', () => { + it('should validate GasEstimationResult structure', () => { + const mockResult = { + totalGasNeeded: ethers.parseUnits('1000000', 'wei'), + gasPrice: ethers.parseUnits('30', 'gwei'), + estimatedFeeInPOL: '0.03', + gasLimit: ethers.parseUnits('1500000', 'wei') + }; + + expect(mockResult).to.have.property('totalGasNeeded'); + expect(mockResult).to.have.property('gasPrice'); + expect(mockResult).to.have.property('estimatedFeeInPOL'); + expect(mockResult).to.have.property('gasLimit'); + expect(mockResult.totalGasNeeded).to.be.a('bigint'); + expect(mockResult.gasPrice).to.be.a('bigint'); + expect(mockResult.estimatedFeeInPOL).to.be.a('string'); + expect(mockResult.gasLimit).to.be.a('bigint'); + }); + }); + + describe('distribution result validation', () => { + it('should validate ParallelDistributionResult structure', () => { + const mockResult = { + walletAddress: '0x1234567890123456789012345678901234567890', + success: true, + gasFilled: true, + gasFillTransactionHash: '0x7890123456789012345678901234567890123456789012345678901234567890' + }; + + expect(mockResult).to.have.property('walletAddress'); + expect(mockResult).to.have.property('success'); + expect(mockResult).to.have.property('gasFilled'); + expect(mockResult).to.have.property('gasFillTransactionHash'); + expect(mockResult.walletAddress).to.be.a('string'); + expect(mockResult.success).to.be.a('boolean'); + expect(mockResult.gasFilled).to.be.a('boolean'); + expect(mockResult.gasFillTransactionHash).to.be.a('string'); + }); + }); + + describe('ethers integration', () => { + it('should properly format ethers values', () => { + const amount = ethers.parseEther('1.5'); + const formatted = ethers.formatEther(amount); + + expect(amount).to.be.a('bigint'); + expect(formatted).to.equal('1.5'); + }); + + it('should handle gas price calculations', () => { + const gasPrice = ethers.parseUnits('30', 'gwei'); + const gasLimit = ethers.parseUnits('1500000', 'wei'); + const totalGas = gasPrice * gasLimit; + const feeInETH = ethers.formatEther(totalGas); + + expect(gasPrice).to.be.a('bigint'); + expect(gasLimit).to.be.a('bigint'); + expect(totalGas).to.be.a('bigint'); + expect(feeInETH).to.be.a('string'); + }); + }); +}); \ No newline at end of file diff --git a/src/services/parallel-distribution.service.ts b/src/services/parallel-distribution.service.ts new file mode 100644 index 0000000..c4cc893 --- /dev/null +++ b/src/services/parallel-distribution.service.ts @@ -0,0 +1,416 @@ +import { ethers } from 'ethers'; +import { config } from '../config'; +import { WalletService, DistributionResult } from './wallet.service'; +import { Project } from './fund-allocation.service'; +import { DonationHandlerService } from './donation-handler.service'; +import { TransactionService } from './transaction.service'; +import { FeeRefillerService } from './fee-refiller.service'; +import { WalletRepository } from '../repositories/wallet.repository'; +import { deriveWalletFromSeedPhrase } from '../utils/wallet.util'; +import { withTimeoutAndRetry } from '../utils/rpc.util'; + +export interface ParallelDistributionRequest { + walletAddresses: string[]; + projects: Project[]; + causeId: number; + causeOwnerAddress: string; + floorFactor?: number; +} + +export interface ParallelDistributionResult { + walletAddress: string; + success: boolean; + result?: DistributionResult; + error?: string; + gasFilled?: boolean; + gasFillTransactionHash?: string; +} + +export interface GasEstimationResult { + totalGasNeeded: bigint; + gasPrice: bigint; + estimatedFeeInPOL: string; + gasLimit: bigint; +} + +export class ParallelDistributionService { + private walletService: WalletService; + private donationHandlerService: DonationHandlerService; + private transactionService: TransactionService; + private feeRefillerService: FeeRefillerService; + private walletRepository: WalletRepository; + private provider: ethers.JsonRpcProvider; + private seedPhrase: string; + + constructor() { + this.walletService = new WalletService(); + this.donationHandlerService = new DonationHandlerService(); + this.transactionService = new TransactionService(); + this.feeRefillerService = new FeeRefillerService(); + this.walletRepository = new WalletRepository(); + this.provider = new ethers.JsonRpcProvider(config.blockchain.rpcUrl); + this.seedPhrase = config.blockchain.seedPhrase; + } + + /** + * Estimate gas requirements for all wallets with dynamic buffer and factor + * @param walletAddresses Array of wallet addresses + * @param projects Projects to distribute to + * @param causeId Cause ID + * @param causeOwnerAddress Cause owner address + * @returns Gas estimation result + */ + async estimateGasRequirements( + walletAddresses: string[], + projects: Project[], + causeId: number, + causeOwnerAddress: string + ): Promise { + try { + console.log(`Estimating gas requirements for ${walletAddresses.length} wallets`); + + // Get current gas price with retry logic + let gasPrice; + try { + gasPrice = await withTimeoutAndRetry( + () => this.provider.getFeeData(), + { timeoutMs: 30000, maxRetries: 2, baseDelayMs: 2000 } + ); + } catch (error) { + console.warn('Failed to get gas price, using fallback:', error); + gasPrice = { gasPrice: ethers.parseUnits('30', 'gwei') }; + } + + const currentGasPrice = gasPrice.gasPrice || ethers.parseUnits('30', 'gwei'); + + // Estimate gas for a single distribution transaction + const sampleWallet = walletAddresses[0]; + const sampleWalletInfo = await this.walletRepository.findByAddress(sampleWallet); + if (!sampleWalletInfo) { + throw new Error(`Sample wallet ${sampleWallet} not found`); + } + + // Create sample distribution data + const sampleDistribution = this.walletService['distributionFeeService'].createAllRecipients( + 1, // Sample amount + causeOwnerAddress, + projects.map(project => ({ project, amount: 0.1 })) + ); + + // Prepare sample transaction data + const donationHandlerContract = new ethers.Contract( + config.blockchain.donationHandlerAddress, + ['function donateManyERC20(address,uint256,address[],uint256[],bytes[])'], + this.provider + ); + + const sampleData = donationHandlerContract.interface.encodeFunctionData('donateManyERC20', [ + config.blockchain.tokenAddress, + ethers.parseEther('1'), // Sample total amount + sampleDistribution.map(r => r.address), + sampleDistribution.map(r => ethers.parseEther(r.amount)), + sampleDistribution.map(() => '0x') // Empty data + ]); + + // Estimate gas for the transaction with retry logic + let estimatedGas; + try { + estimatedGas = await withTimeoutAndRetry( + () => this.provider.estimateGas({ + from: sampleWallet, + to: config.blockchain.donationHandlerAddress, + data: sampleData + }), + { timeoutMs: 30000, maxRetries: 2, baseDelayMs: 2000 } + ); + } catch (error) { + console.warn('Gas estimation failed, using fallback estimate:', error); + // Use fallback gas estimate based on transaction complexity + estimatedGas = this.getFallbackGasEstimate(sampleData); + } + + // Dynamic buffer calculation based on transaction complexity and network conditions + const baseBuffer = 150n; // 50% base buffer + const complexityFactor = BigInt(Math.max(1, sampleDistribution.length / 5)); // More recipients = higher buffer + const networkFactor = currentGasPrice > ethers.parseUnits('50', 'gwei') ? 120n : 100n; // Higher gas price = higher buffer + const dynamicBuffer = (baseBuffer * complexityFactor * networkFactor) / 10000n; + + // Apply dynamic buffer (minimum 50%, can go higher based on factors) + const gasLimit = (estimatedGas * BigInt(Math.max(150, Number(dynamicBuffer)))) / 100n; + + // Calculate total gas needed for all wallets + const totalGasNeeded = gasLimit * BigInt(walletAddresses.length); + const estimatedFeeInPOL = ethers.formatEther(totalGasNeeded * currentGasPrice); + + // Check dynamic minimum balance + const dynamicMinimumBalance = this.calculateDynamicMinimumBalance(currentGasPrice, walletAddresses.length); + const hasSufficientBalance = await this.checkRefillerBalance(totalGasNeeded + dynamicMinimumBalance); + + console.log(`Gas estimation complete:`, { + estimatedGas: estimatedGas.toString(), + gasLimit: gasLimit.toString(), + gasPrice: ethers.formatUnits(currentGasPrice, 'gwei') + ' gwei', + totalGasNeeded: totalGasNeeded.toString(), + estimatedFeeInPOL, + walletCount: walletAddresses.length, + dynamicBuffer: Number(dynamicBuffer), + complexityFactor: Number(complexityFactor), + networkFactor: Number(networkFactor), + dynamicMinimumBalance: ethers.formatEther(dynamicMinimumBalance), + hasSufficientBalance + }); + + return { + totalGasNeeded, + gasPrice: currentGasPrice, + estimatedFeeInPOL, + gasLimit + }; + } catch (error) { + throw new Error(`Failed to estimate gas requirements: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } + + /** + * Get fallback gas estimate based on transaction data + * @param data Transaction data + * @returns Fallback gas limit + */ + private getFallbackGasEstimate(data: string): bigint { + // Base gas for ERC20 transfer + const baseGas = 65000n; + + // Additional gas for contract interaction + const contractGas = 21000n; + + // Gas for data (4 gas per byte) + const dataGas = BigInt(data.length / 2 - 1) * 4n; + + // Buffer for complex operations + const buffer = 50000n; + + return baseGas + contractGas + dataGas + buffer; + } + + /** + * Calculate dynamic minimum balance based on gas price and wallet count + * @param gasPrice Current gas price + * @param walletCount Number of wallets + * @returns Dynamic minimum balance + */ + private calculateDynamicMinimumBalance(gasPrice: bigint, walletCount: number): bigint { + // Base minimum balance + const baseMinimum = ethers.parseEther('0.01'); // 0.01 POL base + + // Factor based on gas price (higher gas price = higher minimum) + const gasPriceFactor = gasPrice > ethers.parseUnits('50', 'gwei') ? 2n : 1n; + + // Factor based on wallet count (more wallets = higher minimum) + const walletCountFactor = BigInt(Math.max(1, Math.ceil(walletCount / 10))); + + return baseMinimum * gasPriceFactor * walletCountFactor; + } + + /** + * Check if refiller wallet has sufficient balance + * @param requiredAmount Required amount + * @returns True if sufficient balance + */ + private async checkRefillerBalance(requiredAmount: bigint): Promise { + try { + const refillerWallet = new ethers.Wallet(config.feeRefiller.privateKey, this.provider); + const balance = await this.provider.getBalance(refillerWallet.address); + return balance >= requiredAmount; + } catch (error) { + console.error('Failed to check refiller balance:', error); + return false; + } + } + + /** + * Fill gas for all wallets using donateManyETH batch transaction + * @param walletAddresses Array of wallet addresses + * @param gasAmount Amount of gas to fill for each wallet + * @returns Array of gas fill results + */ + async fillGasForAllWallets( + walletAddresses: string[], + gasAmount: bigint + ): Promise> { + try { + console.log(`Filling gas for ${walletAddresses.length} wallets using batch transaction`); + + // Check current balances and filter wallets that need gas + const walletsNeedingGas: Array<{ address: string; amount: bigint }> = []; + + for (const walletAddress of walletAddresses) { + try { + const currentBalance = await this.provider.getBalance(walletAddress); + if (currentBalance < ethers.parseEther(config.feeRefiller.minimumBalance)) { + const gasToSend = gasAmount - currentBalance; + walletsNeedingGas.push({ address: walletAddress, amount: gasToSend }); + console.log(`Wallet ${walletAddress} needs ${ethers.formatEther(gasToSend)} POL`); + } else { + console.log(`Wallet ${walletAddress} already has sufficient gas balance`); + } + } catch (error) { + console.error(`Failed to check balance for ${walletAddress}:`, error); + } + } + + if (walletsNeedingGas.length === 0) { + console.log('All wallets already have sufficient gas balance'); + return walletAddresses.map(address => ({ + walletAddress: address, + success: true, + transactionHash: undefined + })); + } + + // Use the refiller wallet to send batch gas transaction + const refillerWallet = new ethers.Wallet(config.feeRefiller.privateKey, this.provider); + + // Check refiller wallet balance + const refillerBalance = await this.provider.getBalance(refillerWallet.address); + const totalGasNeeded = walletsNeedingGas.reduce((sum, wallet) => sum + wallet.amount, 0n); + + if (refillerBalance < totalGasNeeded) { + throw new Error(`Insufficient balance in refiller wallet. Required: ${ethers.formatEther(totalGasNeeded)} POL, Available: ${ethers.formatEther(refillerBalance)} POL`); + } + + // Prepare batch transaction data for donateManyETH + const donationHandlerContract = new ethers.Contract( + config.blockchain.donationHandlerAddress, + ['function donateManyETH(uint256,address[],uint256[],bytes[])'], + refillerWallet + ); + + const recipientAddresses = walletsNeedingGas.map(wallet => wallet.address); + const amounts = walletsNeedingGas.map(wallet => wallet.amount); + const data = walletsNeedingGas.map(() => '0x'); // Empty data for gas transfers + + console.log(`Sending batch gas transaction to ${walletsNeedingGas.length} wallets`); + + // Send batch transaction + const tx = await donationHandlerContract.donateManyETH( + totalGasNeeded, + recipientAddresses, + amounts, + data, + { + value: totalGasNeeded, + gasLimit: 500000n // High gas limit for batch transaction + } + ); + + console.log(`Batch gas transaction sent: ${tx.hash}`); + + // Wait for transaction confirmation + const receipt = await tx.wait(); + const transactionHash = receipt?.hash || tx.hash; + + console.log(`Batch gas transaction confirmed: ${transactionHash}`); + + // Return results for all wallets + return walletAddresses.map(address => { + const needsGas = walletsNeedingGas.find(wallet => wallet.address === address); + return { + walletAddress: address, + success: true, + transactionHash: needsGas ? transactionHash : undefined + }; + }); + + } catch (error) { + console.error('Failed to fill gas for wallets:', error); + + // Return failure results for all wallets + return walletAddresses.map(address => ({ + walletAddress: address, + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + })); + } + } + + /** + * Distribute funds in parallel for all wallets + * @param request Parallel distribution request + * @returns Array of distribution results + */ + async distributeFundsInParallel(request: ParallelDistributionRequest): Promise { + try { + console.log(`Starting parallel distribution for ${request.walletAddresses.length} wallets`); + + const { walletAddresses, projects, causeId, causeOwnerAddress, floorFactor = 0.25 } = request; + + // Step 1: Estimate gas requirements + const gasEstimation = await this.estimateGasRequirements( + walletAddresses, + projects, + causeId, + causeOwnerAddress + ); + + // Step 2: Fill gas for all wallets + const gasFillResults = await this.fillGasForAllWallets( + walletAddresses, + gasEstimation.gasLimit * gasEstimation.gasPrice * BigInt(config.feeRefiller.refillFactor) + ); + + // Step 3: Distribute funds in parallel + const distributionResults = await Promise.allSettled( + walletAddresses.map(async (walletAddress) => { + try { + console.log(`Starting distribution for wallet ${walletAddress}`); + + const result = await this.walletService.distributeFunds( + walletAddress, + projects, + causeId, + causeOwnerAddress, + floorFactor + ); + + const gasFillResult = gasFillResults.find(r => r.walletAddress === walletAddress); + + return { + walletAddress, + success: true, + result, + gasFilled: gasFillResult?.success || false, + gasFillTransactionHash: gasFillResult?.transactionHash + }; + } catch (error) { + console.error(`Distribution failed for ${walletAddress}:`, error); + return { + walletAddress, + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + }; + } + }) + ); + + // Convert results to expected format + const results: ParallelDistributionResult[] = distributionResults.map((result, index) => { + if (result.status === 'fulfilled') { + return result.value; + } else { + return { + walletAddress: walletAddresses[index], + success: false, + error: result.reason?.message || 'Unknown error' + }; + } + }); + + const successCount = results.filter(r => r.success).length; + console.log(`Parallel distribution completed: ${successCount}/${walletAddresses.length} successful`); + + return results; + } catch (error) { + throw new Error(`Failed to distribute funds in parallel: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } +} \ No newline at end of file