UNPKG

dop-stick

Version:

Source control tooling for versionable-upgradeable smart contracts

217 lines 8.57 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TransactionManager = void 0; const ethers_1 = require("ethers"); const logger_1 = require("../../logsAndMetrics/core/logger"); class TransactionManager { constructor(provider, defaultConfig) { this.pendingTransactions = new Map(); this.provider = provider; this.defaultConfig = { maxRetries: 3, minGasPrice: '1', maxGasPrice: '200', gasPriceMultiplier: 1.1, timeout: 300000, confirmations: 1, speedUp: true, ...defaultConfig }; } /** * Send a transaction with automatic management */ async sendTransaction(tx, signer, config) { const finalConfig = { ...this.defaultConfig, ...config }; let attempt = 0; let lastError = null; while (attempt < (finalConfig.maxRetries || 1)) { try { // Prepare transaction const preparedTx = await this.prepareTransaction(tx, signer, finalConfig); // Send transaction const response = await signer.sendTransaction(preparedTx); // Monitor transaction const state = await this.monitorTransaction(response, finalConfig); // Handle transaction state if (state.status === 'confirmed') { return state; } else if (state.status === 'speedup') { // Continue with higher gas price tx.gasPrice = state.gasPrice.mul(finalConfig.gasPriceMultiplier || 1.1); } else { throw new Error(`Transaction failed with status: ${state.status}`); } } catch (error) { lastError = error; attempt++; if (attempt >= (finalConfig.maxRetries || 1)) { throw new Error(`Transaction failed after ${attempt} attempts: ${lastError.message}`); } // Wait before retry await new Promise(resolve => setTimeout(resolve, 2000)); } } throw lastError || new Error('Transaction failed'); } /** * Speed up a pending transaction */ async speedUpTransaction(hash, signer, multiplier = 1.5) { var _a; const tx = await this.provider.getTransaction(hash); if (!tx) { throw new Error('Transaction not found'); } const state = this.pendingTransactions.get(hash); if (!state || state.status !== 'pending') { throw new Error('Transaction is not pending'); } // Create replacement transaction const newTx = { to: tx.to, from: tx.from, nonce: tx.nonce, data: tx.data, value: tx.value, gasLimit: tx.gasLimit, gasPrice: (_a = tx.gasPrice) === null || _a === void 0 ? void 0 : _a.mul(Math.floor(multiplier * 100)).div(100), chainId: tx.chainId }; // Send replacement transaction const response = await signer.sendTransaction(newTx); // Update states state.status = 'replaced'; state.replacedBy = response.hash; // Monitor new transaction return await this.monitorTransaction(response, { confirmations: 1 }); } /** * Check if a transaction can be sped up */ async canSpeedUp(hash) { const state = this.pendingTransactions.get(hash); if (!state || state.status !== 'pending') { return false; } const currentGasPrice = await this.provider.getGasPrice(); return currentGasPrice.gt(state.gasPrice); } async prepareTransaction(tx, signer, config) { // Estimate gas if not provided if (!tx.gasLimit) { tx.gasLimit = await signer.estimateGas(tx); } // Set gas price if not provided if (!tx.gasPrice && !tx.maxFeePerGas) { const currentGasPrice = await this.provider.getGasPrice(); const minGasPrice = ethers_1.ethers.utils.parseUnits(config.minGasPrice || '1', 'gwei'); const maxGasPrice = ethers_1.ethers.utils.parseUnits(config.maxGasPrice || '200', 'gwei'); tx.gasPrice = ethers_1.BigNumber.from(currentGasPrice) .mul(Math.floor((config.gasPriceMultiplier || 1.1) * 100)) .div(100); if (tx.gasPrice.lt(minGasPrice)) tx.gasPrice = minGasPrice; if (tx.gasPrice.gt(maxGasPrice)) tx.gasPrice = maxGasPrice; } // Set nonce if provided if (config.nonce !== undefined) { tx.nonce = config.nonce; } else if (tx.nonce === undefined) { tx.nonce = await signer.getTransactionCount('pending'); } return tx; } async monitorTransaction(tx, config) { const state = { status: 'pending', hash: tx.hash, nonce: tx.nonce, gasPrice: tx.gasPrice || ethers_1.BigNumber.from(0), gasLimit: tx.gasLimit, confirmations: 0 }; this.pendingTransactions.set(tx.hash, state); return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Transaction monitoring timed out')); }, config.timeout || 300000); const handleReceipt = async (receipt) => { if (receipt.confirmations >= (config.confirmations || 1)) { clearTimeout(timeout); state.status = receipt.status === 1 ? 'confirmed' : 'failed'; state.blockNumber = receipt.blockNumber; state.confirmations = receipt.confirmations; state.receipt = receipt; resolve(state); } }; tx.wait(config.confirmations || 1) .then(handleReceipt) .catch(error => { clearTimeout(timeout); state.status = 'failed'; state.error = error.message; reject(error); }); // Monitor for replacement this.provider.on('block', async () => { try { const current = await this.provider.getTransaction(tx.hash); if (!current && state.status === 'pending') { const receipt = await this.provider.getTransactionReceipt(tx.hash); if (receipt) { await handleReceipt(receipt); } else { // Transaction might have been replaced const newTx = await this.findReplacementTransaction(tx.nonce, tx.from || ''); if (newTx) { state.status = 'replaced'; state.replacedBy = newTx.hash; resolve(state); } } } } catch (error) { logger_1.Logger.error('Error monitoring transaction:', error); } }); }); } async findReplacementTransaction(nonce, from) { const block = await this.provider.getBlock('latest'); const blockNumber = block.number; // Search last 50 blocks for replacement transaction for (let i = blockNumber; i > blockNumber - 50 && i > 0; i--) { // Get block with transactions const block = await this.provider.getBlockWithTransactions(i); // Find matching transaction const tx = block.transactions.find((tx) => tx.nonce === nonce && tx.from.toLowerCase() === from.toLowerCase()); if (tx) return tx; } return null; } /** * Get current state of a transaction */ getTransactionState(hash) { return this.pendingTransactions.get(hash); } /** * Clear transaction history */ clearHistory() { this.pendingTransactions.clear(); } } exports.TransactionManager = TransactionManager; //# sourceMappingURL=transaction-manager.js.map