dop-stick
Version:
Source control tooling for versionable-upgradeable smart contracts
217 lines • 8.57 kB
JavaScript
;
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