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
JavaScript
;
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;