Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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();

Expand Down Expand Up @@ -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());

Expand Down Expand Up @@ -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 {
Expand Down
102 changes: 0 additions & 102 deletions src/services/distribution-relationship.test.ts

This file was deleted.

155 changes: 155 additions & 0 deletions src/services/parallel-distribution.service.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
Loading