Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09c24e12c9 | ||
|
|
c1c6079745 | ||
|
|
eaaa54b7f9 | ||
|
|
6adf89472e | ||
|
|
b6ed5d2bc0 | ||
|
|
b4ee1e5117 | ||
|
|
56aa312829 | ||
|
|
07dcb5d258 | ||
|
|
4a17833462 | ||
|
|
c6652baa7c | ||
|
|
a8e504054e | ||
|
|
23e2e01172 | ||
|
|
07752e0714 | ||
|
|
37f6faa42d | ||
|
|
9599788224 | ||
|
|
72a665a19a | ||
|
|
f6a4e93a23 | ||
|
|
af7c597af9 | ||
|
|
221dce3d73 | ||
|
|
8d4bab7fc2 | ||
|
|
af65d78be9 | ||
|
|
bc0b369095 | ||
|
|
e1620e15c1 | ||
|
|
414fb28a5e | ||
|
|
780df01b43 | ||
|
|
c5e4d76dc5 | ||
|
|
8cb2bb0fbe | ||
|
|
6bb265d3b9 | ||
|
|
b940fad5e0 | ||
|
|
1634e5fb16 | ||
|
|
62bcd2aa95 | ||
|
|
1eec6aa329 | ||
|
|
715ad59273 | ||
|
|
c6344b40d5 | ||
|
|
3b7d5ebd24 | ||
|
|
fcdaa2d52c | ||
|
|
adf328f81c | ||
|
|
b0e25e800f |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
.env
|
||||||
|
|||||||
17
package.json
17
package.json
@@ -1,27 +1,30 @@
|
|||||||
{
|
{
|
||||||
"name": "tx-manager",
|
"name": "tx-manager",
|
||||||
"version": "0.1.1",
|
"version": "0.4.7",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"eslint": "eslint --ext .js --ignore-path .gitignore .",
|
"eslint": "eslint --ext .js --ignore-path .gitignore .",
|
||||||
"prettier:check": "npx prettier --check . --config .prettierrc",
|
"prettier:check": "prettier --check . --config .prettierrc",
|
||||||
"prettier:fix": "npx prettier --write . --config .prettierrc",
|
"prettier:fix": "prettier --write . --config .prettierrc",
|
||||||
"lint": "yarn eslint && yarn prettier:check",
|
"lint": "yarn eslint && yarn prettier:check",
|
||||||
"test": "mocha --timeout 300000"
|
"test": "mocha --timeout 300000"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Roman Semenov <semenov.roma@gmail.com>",
|
"author": "Roman Semenov <semenov.roma@gmail.com>",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git://github.com/tornadocash/tx-manager.git"
|
||||||
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"src/*"
|
"src/*"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async-mutex": "^0.2.4",
|
"async-mutex": "^0.2.4",
|
||||||
"gas-price-oracle": "^0.1.5",
|
"ethers": "^5.4.6",
|
||||||
"web3": "^1.3.0",
|
"gas-price-oracle": "^0.4.7",
|
||||||
"web3-core-promievent": "^1.3.0",
|
"web3-core-promievent": "^1.3.0"
|
||||||
"web3-utils": "^1.3.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
|
|||||||
@@ -1,22 +1,32 @@
|
|||||||
const Web3 = require('web3')
|
const ethers = require('ethers')
|
||||||
const { toWei, toHex, toBN, BN, fromWei } = require('web3-utils')
|
const { parseUnits, formatUnits } = ethers.utils
|
||||||
|
const BigNumber = ethers.BigNumber
|
||||||
const PromiEvent = require('web3-core-promievent')
|
const PromiEvent = require('web3-core-promievent')
|
||||||
const { sleep, when } = require('./utils')
|
const { sleep, min, max } = require('./utils')
|
||||||
|
|
||||||
const nonceErrors = [
|
const nonceErrors = [
|
||||||
'Returned error: Transaction nonce is too low. Try incrementing the nonce.',
|
'Transaction nonce is too low. Try incrementing the nonce.',
|
||||||
'Returned error: nonce too low',
|
/nonce too low/i,
|
||||||
|
'nonce has already been used',
|
||||||
|
/OldNonce/,
|
||||||
|
'invalid transaction nonce',
|
||||||
]
|
]
|
||||||
|
|
||||||
const gasPriceErrors = [
|
const gasPriceErrors = [
|
||||||
'Returned error: Transaction gas price supplied is too low. There is another transaction with same nonce in the queue. Try increasing the gas price or incrementing the nonce.',
|
'Transaction gas price supplied is too low. There is another transaction with same nonce in the queue. Try increasing the gas price or incrementing the nonce.',
|
||||||
'Returned error: replacement transaction underpriced',
|
/replacement transaction underpriced/i,
|
||||||
/Returned error: Transaction gas price \d+wei is too low. There is another transaction with same nonce in the queue with gas price: \d+wei. Try increasing the gas price or incrementing the nonce./,
|
/transaction underpriced/,
|
||||||
|
/Transaction gas price \d+wei is too low. There is another transaction with same nonce in the queue with gas price: \d+wei. Try increasing the gas price or incrementing the nonce./,
|
||||||
|
/FeeTooLow/,
|
||||||
|
/max fee per gas less than block base fee/,
|
||||||
]
|
]
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
const sameTxErrors = [
|
const sameTxErrors = [
|
||||||
'Returned error: Transaction with the same hash was already imported.',
|
'Transaction with the same hash was already imported.',
|
||||||
|
'already known',
|
||||||
|
'AlreadyKnown',
|
||||||
|
'Known transaction'
|
||||||
]
|
]
|
||||||
|
|
||||||
class Transaction {
|
class Transaction {
|
||||||
@@ -59,12 +69,23 @@ class Transaction {
|
|||||||
this.tx = { ...tx }
|
this.tx = { ...tx }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!tx.gas) {
|
|
||||||
tx.gas = await this._web3.eth.estimateGas(tx)
|
if (!tx.gasLimit) {
|
||||||
tx.gas = Math.floor(tx.gas * 1.1)
|
tx.gasLimit = await this._estimateGas(tx)
|
||||||
|
tx.gasLimit = Math.floor(tx.gasLimit * this.config.GAS_LIMIT_MULTIPLIER)
|
||||||
|
tx.gasLimit = Math.min(tx.gasLimit, this.config.BLOCK_GAS_LIMIT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tx.chainId = this.tx.chainId
|
||||||
tx.nonce = this.tx.nonce // can be different from `this.manager._nonce`
|
tx.nonce = this.tx.nonce // can be different from `this.manager._nonce`
|
||||||
tx.gasPrice = Math.max(this.tx.gasPrice, tx.gasPrice || 0) // start no less than current tx gas price
|
|
||||||
|
// start no less than current tx gas params
|
||||||
|
if (this.tx.gasPrice) {
|
||||||
|
tx.gasPrice = Math.max(this.tx.gasPrice, tx.gasPrice || 0)
|
||||||
|
} else {
|
||||||
|
tx.maxFeePerGas = Math.max(this.tx.maxFeePerGas, tx.maxFeePerGas || 0)
|
||||||
|
tx.maxPriorityFeePerGas = Math.max(this.tx.maxPriorityFeePerGas, tx.maxPriorityFeePerGas || 0)
|
||||||
|
}
|
||||||
|
|
||||||
this.tx = { ...tx }
|
this.tx = { ...tx }
|
||||||
this._increaseGasPrice()
|
this._increaseGasPrice()
|
||||||
@@ -80,7 +101,6 @@ class Transaction {
|
|||||||
from: this.address,
|
from: this.address,
|
||||||
to: this.address,
|
to: this.address,
|
||||||
value: 0,
|
value: 0,
|
||||||
gas: 21000,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,16 +111,16 @@ class Transaction {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
async _execute() {
|
async _execute() {
|
||||||
await this.manager._mutex.acquire()
|
const mutexRelease = await this.manager._mutex.acquire()
|
||||||
try {
|
try {
|
||||||
await this._prepare()
|
await this._prepare()
|
||||||
await this._send()
|
await this._send()
|
||||||
const receipt = this._waitForConfirmations()
|
const receipt = await this._waitForConfirmations()
|
||||||
// we could have bumped nonce during execution, so get the latest one + 1
|
// we could have bumped nonce during execution, so get the latest one + 1
|
||||||
this.manager._nonce = this.tx.nonce + 1
|
this.manager._nonce = this.tx.nonce + 1
|
||||||
return receipt
|
return receipt
|
||||||
} finally {
|
} finally {
|
||||||
this.manager._mutex.release()
|
mutexRelease()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,19 +131,37 @@ class Transaction {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
async _prepare() {
|
async _prepare() {
|
||||||
if (!this.tx.gas || this.config.ESTIMATE_GAS) {
|
if (!this.config.BLOCK_GAS_LIMIT) {
|
||||||
const gas = await this._web3.eth.estimateGas(this.tx)
|
const lastBlock = await this._provider.getBlock('latest')
|
||||||
if (!this.tx.gas) {
|
this.config.BLOCK_GAS_LIMIT = Math.floor(lastBlock.gasLimit.toNumber() * 0.95)
|
||||||
this.tx.gas = Math.floor(gas * 1.1)
|
}
|
||||||
|
|
||||||
|
if (!this.manager._chainId) {
|
||||||
|
const net = await this._provider.getNetwork()
|
||||||
|
this.manager._chainId = net.chainId
|
||||||
|
}
|
||||||
|
this.tx.chainId = this.manager._chainId
|
||||||
|
|
||||||
|
if (!this.tx.gasLimit || this.config.ESTIMATE_GAS) {
|
||||||
|
const gas = await this._estimateGas(this.tx)
|
||||||
|
if (!this.tx.gasLimit) {
|
||||||
|
const gasLimit = Math.floor(gas * this.config.GAS_LIMIT_MULTIPLIER)
|
||||||
|
this.tx.gasLimit = Math.min(gasLimit, this.config.BLOCK_GAS_LIMIT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this.tx.gasPrice) {
|
|
||||||
this.tx.gasPrice = await this._getGasPrice('fast')
|
|
||||||
}
|
|
||||||
if (!this.manager._nonce) {
|
if (!this.manager._nonce) {
|
||||||
this.manager._nonce = await this._web3.eth.getTransactionCount(this.address, 'latest')
|
this.manager._nonce = await this._getLastNonce()
|
||||||
}
|
}
|
||||||
this.tx.nonce = this.manager._nonce
|
this.tx.nonce = this.manager._nonce
|
||||||
|
|
||||||
|
if (this.tx.gasPrice || (this.tx.maxFeePerGas && this.tx.maxPriorityFeePerGas)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const gasParams = await this._getGasParams()
|
||||||
|
|
||||||
|
this.tx = Object.assign(this.tx, gasParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -134,20 +172,19 @@ class Transaction {
|
|||||||
*/
|
*/
|
||||||
async _send() {
|
async _send() {
|
||||||
// todo throw is we attempt to send a tx that attempts to replace already mined tx
|
// todo throw is we attempt to send a tx that attempts to replace already mined tx
|
||||||
const signedTx = await this._web3.eth.accounts.signTransaction(this.tx, this._privateKey)
|
const signedTx = await this._wallet.signTransaction(this.tx)
|
||||||
this.submitTimestamp = Date.now()
|
this.submitTimestamp = Date.now()
|
||||||
this.tx.hash = signedTx.transactionHash
|
const txHash = ethers.utils.keccak256(signedTx)
|
||||||
this.hashes.push(signedTx.transactionHash)
|
this.hashes.push(txHash)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this._broadcast(signedTx.rawTransaction)
|
await this._broadcast(signedTx)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return this._handleSendError(e)
|
return this._handleRpcError(e, '_send')
|
||||||
}
|
}
|
||||||
|
|
||||||
this._emitter.emit('transactionHash', signedTx.transactionHash)
|
this._emitter.emit('transactionHash', txHash)
|
||||||
console.log(`Broadcasted transaction ${signedTx.transactionHash}`)
|
console.log(`Broadcasted transaction ${txHash}`)
|
||||||
console.log(this.tx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -161,7 +198,7 @@ class Transaction {
|
|||||||
while (true) {
|
while (true) {
|
||||||
// We are already waiting on certain tx hash
|
// We are already waiting on certain tx hash
|
||||||
if (this.currentTxHash) {
|
if (this.currentTxHash) {
|
||||||
const receipt = await this._web3.eth.getTransactionReceipt(this.currentTxHash)
|
const receipt = await this._provider.getTransactionReceipt(this.currentTxHash)
|
||||||
|
|
||||||
if (!receipt) {
|
if (!receipt) {
|
||||||
// We were waiting for some tx but it disappeared
|
// We were waiting for some tx but it disappeared
|
||||||
@@ -170,12 +207,15 @@ class Transaction {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentBlock = await this._web3.eth.getBlockNumber()
|
const currentBlock = await this._provider.getBlockNumber()
|
||||||
const confirmations = Math.max(0, currentBlock - receipt.blockNumber)
|
const confirmations = Math.max(0, currentBlock - receipt.blockNumber)
|
||||||
// todo don't emit repeating confirmation count
|
// todo don't emit repeating confirmation count
|
||||||
this._emitter.emit('confirmations', confirmations)
|
this._emitter.emit('confirmations', confirmations)
|
||||||
if (confirmations >= this.config.CONFIRMATIONS) {
|
if (confirmations >= this.config.CONFIRMATIONS) {
|
||||||
// Tx is mined and has enough confirmations
|
// Tx is mined and has enough confirmations
|
||||||
|
if (this.config.THROW_ON_REVERT && Number(receipt.status) === 0) {
|
||||||
|
throw new Error('EVM execution failed, so the transaction was reverted.')
|
||||||
|
}
|
||||||
return receipt
|
return receipt
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +231,7 @@ class Transaction {
|
|||||||
// We were waiting too long, increase gas price and resubmit
|
// We were waiting too long, increase gas price and resubmit
|
||||||
if (Date.now() - this.submitTimestamp >= this.config.GAS_BUMP_INTERVAL) {
|
if (Date.now() - this.submitTimestamp >= this.config.GAS_BUMP_INTERVAL) {
|
||||||
if (this._increaseGasPrice()) {
|
if (this._increaseGasPrice()) {
|
||||||
console.log('Resubmitting with higher gas price')
|
console.log('Resubmitting with higher gas params')
|
||||||
await this._send()
|
await this._send()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -228,7 +268,6 @@ class Transaction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Mined. Start waiting for confirmations...')
|
|
||||||
this._emitter.emit('mined', receipt)
|
this._emitter.emit('mined', receipt)
|
||||||
this.currentTxHash = receipt.transactionHash
|
this.currentTxHash = receipt.transactionHash
|
||||||
}
|
}
|
||||||
@@ -236,7 +275,7 @@ class Transaction {
|
|||||||
|
|
||||||
async _getReceipts() {
|
async _getReceipts() {
|
||||||
for (const hash of this.hashes.reverse()) {
|
for (const hash of this.hashes.reverse()) {
|
||||||
const receipt = await this._web3.eth.getTransactionReceipt(hash)
|
const receipt = await this._provider.getTransactionReceipt(hash)
|
||||||
if (receipt) {
|
if (receipt) {
|
||||||
return receipt
|
return receipt
|
||||||
}
|
}
|
||||||
@@ -248,43 +287,58 @@ class Transaction {
|
|||||||
* Broadcasts tx to multiple nodes, waits for tx hash only on main node
|
* Broadcasts tx to multiple nodes, waits for tx hash only on main node
|
||||||
*/
|
*/
|
||||||
_broadcast(rawTx) {
|
_broadcast(rawTx) {
|
||||||
const main = this._web3.eth.sendSignedTransaction(rawTx)
|
const main = this._provider.sendTransaction(rawTx)
|
||||||
for (const node of this._broadcastNodes) {
|
for (const node of this._broadcastNodes) {
|
||||||
try {
|
try {
|
||||||
new Web3(node).eth.sendSignedTransaction(rawTx)
|
new ethers.providers.JsonRpcProvider(node).sendTransaction(rawTx)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`Failed to send transaction to node ${node}: ${e}`)
|
console.log(`Failed to send transaction to node ${node}: ${e}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return when(main, 'transactionHash')
|
return main
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleSendError(e) {
|
_handleRpcError(e, method) {
|
||||||
console.log('Got error', e)
|
if (e.error.error) {
|
||||||
|
// Sometimes ethers wraps known errors, unwrap it in this case
|
||||||
|
e = e.error
|
||||||
|
}
|
||||||
|
|
||||||
// nonce is too low, trying to increase and resubmit
|
if (e.error && e.code === 'SERVER_ERROR') {
|
||||||
if (this._hasError(e.message, nonceErrors)) {
|
const message = e.error.message
|
||||||
console.log(`Nonce ${this.tx.nonce} is too low, increasing and retrying`)
|
|
||||||
if (this.retries <= this.config.MAX_RETRIES) {
|
// nonce is too low, trying to increase and resubmit
|
||||||
this.tx.nonce++
|
if (this._hasError(message, nonceErrors)) {
|
||||||
this.retries++
|
console.log(`Nonce ${this.tx.nonce} is too low, increasing and retrying`)
|
||||||
return this._send()
|
if (this.retries <= this.config.MAX_RETRIES) {
|
||||||
|
this.tx.nonce++
|
||||||
|
this.retries++
|
||||||
|
return this[method]()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// there is already a pending tx with higher gas price, trying to bump and resubmit
|
||||||
|
if (this._hasError(message, gasPriceErrors)) {
|
||||||
|
console.log(
|
||||||
|
`Gas price ${formatUnits(
|
||||||
|
this.tx.gasPrice || this.tx.maxFeePerGas,
|
||||||
|
'gwei',
|
||||||
|
)} gwei is too low, increasing and retrying`,
|
||||||
|
)
|
||||||
|
if (this._increaseGasPrice()) {
|
||||||
|
return this[method]()
|
||||||
|
} else {
|
||||||
|
throw new Error('Already at max gas price, but still not enough to submit the transaction')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._hasError(message, sameTxErrors)) {
|
||||||
|
console.log('Same transaction is already in mempool, skipping submit')
|
||||||
|
return // do nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// there is already a pending tx with higher gas price, trying to bump and resubmit
|
throw new Error(`Send error: ${e}`)
|
||||||
if (this._hasError(e.message, gasPriceErrors)) {
|
|
||||||
console.log(`Gas price ${fromWei(this.tx.gasPrice, 'gwei')} gwei is too low, increasing and retrying`)
|
|
||||||
this._increaseGasPrice()
|
|
||||||
return this._send()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._hasError(e.message, sameTxErrors)) {
|
|
||||||
console.log('Same transaction is already in mempool, skipping submit')
|
|
||||||
return // do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Send error: ${e.message}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -300,19 +354,47 @@ class Transaction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_increaseGasPrice() {
|
_increaseGasPrice() {
|
||||||
const minGweiBump = toBN(toWei(this.config.MIN_GWEI_BUMP.toString(), 'Gwei'))
|
const maxGasPrice = parseUnits(this.config.MAX_GAS_PRICE.toString(), 'gwei')
|
||||||
const oldGasPrice = toBN(this.tx.gasPrice)
|
const minGweiBump = parseUnits(this.config.MIN_GWEI_BUMP.toString(), 'gwei')
|
||||||
const newGasPrice = BN.max(
|
|
||||||
oldGasPrice.mul(toBN(100 + this.config.GAS_BUMP_PERCENTAGE)).div(toBN(100)),
|
if (this.tx.gasPrice) {
|
||||||
oldGasPrice.add(minGweiBump),
|
const oldGasPrice = BigNumber.from(this.tx.gasPrice)
|
||||||
)
|
if (oldGasPrice.gte(maxGasPrice)) {
|
||||||
const maxGasPrice = toBN(toWei(this.config.MAX_GAS_PRICE.toString(), 'gwei'))
|
console.log('Already at max gas price, not bumping')
|
||||||
if (toBN(this.tx.gasPrice).eq(maxGasPrice)) {
|
return false
|
||||||
console.log('Already at max gas price, not bumping')
|
}
|
||||||
return false
|
|
||||||
|
const newGasPrice = max(
|
||||||
|
oldGasPrice.mul(100 + this.config.GAS_BUMP_PERCENTAGE).div(100),
|
||||||
|
oldGasPrice.add(minGweiBump),
|
||||||
|
)
|
||||||
|
this.tx.gasPrice = min(newGasPrice, maxGasPrice).toHexString()
|
||||||
|
console.log(`Increasing gas price to ${formatUnits(this.tx.gasPrice, 'gwei')} gwei`)
|
||||||
|
} else {
|
||||||
|
const oldMaxFeePerGas = BigNumber.from(this.tx.maxFeePerGas)
|
||||||
|
const oldMaxPriorityFeePerGas = BigNumber.from(this.tx.maxPriorityFeePerGas)
|
||||||
|
if (oldMaxFeePerGas.gte(maxGasPrice)) {
|
||||||
|
console.log('Already at max fee per gas, not bumping')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const newMaxFeePerGas = max(
|
||||||
|
oldMaxFeePerGas.mul(100 + this.config.GAS_BUMP_PERCENTAGE).div(100),
|
||||||
|
oldMaxFeePerGas.add(minGweiBump),
|
||||||
|
)
|
||||||
|
const newMaxPriorityFeePerGas = max(
|
||||||
|
oldMaxPriorityFeePerGas.mul(100 + this.config.GAS_BUMP_PERCENTAGE).div(100),
|
||||||
|
oldMaxPriorityFeePerGas.add(minGweiBump),
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxFeePerGas = min(newMaxFeePerGas, maxGasPrice)
|
||||||
|
|
||||||
|
this.tx.maxFeePerGas = maxFeePerGas.toHexString()
|
||||||
|
this.tx.maxPriorityFeePerGas = min(newMaxPriorityFeePerGas, maxFeePerGas).toHexString()
|
||||||
|
|
||||||
|
console.log(`Increasing maxFeePerGas to ${formatUnits(this.tx.maxFeePerGas, 'gwei')} gwei`)
|
||||||
}
|
}
|
||||||
this.tx.gasPrice = toHex(BN.min(newGasPrice, maxGasPrice))
|
|
||||||
console.log(`Increasing gas price to ${fromWei(this.tx.gasPrice, 'gwei')} gwei`)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,7 +409,7 @@ class Transaction {
|
|||||||
const gasPrices = await this._gasPriceOracle.gasPrices()
|
const gasPrices = await this._gasPriceOracle.gasPrices()
|
||||||
const result = gasPrices[type].toString()
|
const result = gasPrices[type].toString()
|
||||||
console.log(`${type} gas price is now ${result} gwei`)
|
console.log(`${type} gas price is now ${result} gwei`)
|
||||||
return toHex(toWei(gasPrices[type].toString(), 'gwei'))
|
return parseUnits(result, 'gwei').toHexString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -337,7 +419,76 @@ class Transaction {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_getLastNonce() {
|
_getLastNonce() {
|
||||||
return this._web3.eth.getTransactionCount(this.address, 'latest')
|
return this._wallet.getTransactionCount('latest')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches priority fee from the chain
|
||||||
|
*
|
||||||
|
* @param blockNumber The newest number block
|
||||||
|
* @returns {Promise<BigNumber>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async _estimatePriorityFee() {
|
||||||
|
const defaultPriorityFee = parseUnits(this.config.DEFAULT_PRIORITY_FEE.toString(), 'gwei')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const estimatedPriorityFee = await this._provider.send('eth_maxPriorityFeePerGas', [])
|
||||||
|
|
||||||
|
if (!estimatedPriorityFee || isNaN(estimatedPriorityFee)) {
|
||||||
|
return defaultPriorityFee
|
||||||
|
}
|
||||||
|
|
||||||
|
const bumpedPriorityFee = BigNumber.from(estimatedPriorityFee)
|
||||||
|
.mul(100 + this.config.PRIORITY_FEE_RESERVE_PERCENTAGE)
|
||||||
|
.div(100)
|
||||||
|
|
||||||
|
return max(bumpedPriorityFee, defaultPriorityFee)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('_estimatePriorityFee has error:', err.message)
|
||||||
|
return defaultPriorityFee
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Choose network gas params
|
||||||
|
*
|
||||||
|
* @returns {Promise<object>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async _getGasParams() {
|
||||||
|
const maxGasPrice = parseUnits(this.config.MAX_GAS_PRICE.toString(), 'gwei')
|
||||||
|
const block = await this._provider.getBlock('latest')
|
||||||
|
|
||||||
|
// Check network support for EIP-1559
|
||||||
|
if (this.config.ENABLE_EIP1559 && block && block.baseFeePerGas) {
|
||||||
|
const maxPriorityFeePerGas = await this._estimatePriorityFee()
|
||||||
|
|
||||||
|
const maxFeePerGas = block.baseFeePerGas
|
||||||
|
.mul(100 + this.config.BASE_FEE_RESERVE_PERCENTAGE)
|
||||||
|
.div(100)
|
||||||
|
.add(maxPriorityFeePerGas)
|
||||||
|
|
||||||
|
return {
|
||||||
|
maxFeePerGas: min(maxFeePerGas, maxGasPrice).toHexString(),
|
||||||
|
maxPriorityFeePerGas: min(maxPriorityFeePerGas, maxGasPrice).toHexString(),
|
||||||
|
type: 2,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const fastGasPrice = BigNumber.from(await this._getGasPrice('fast'))
|
||||||
|
return {
|
||||||
|
gasPrice: min(fastGasPrice, maxGasPrice).toHexString(),
|
||||||
|
type: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _estimateGas(tx) {
|
||||||
|
try {
|
||||||
|
return await this._wallet.estimateGas(tx)
|
||||||
|
} catch (e) {
|
||||||
|
return this._handleRpcError(e, '_estimateGas')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const Web3 = require('web3')
|
const ethers = require('ethers')
|
||||||
const { Mutex } = require('async-mutex')
|
const { Mutex } = require('async-mutex')
|
||||||
const { GasPriceOracle } = require('gas-price-oracle')
|
const { GasPriceOracle } = require('gas-price-oracle')
|
||||||
const Transaction = require('./Transaction')
|
const Transaction = require('./Transaction')
|
||||||
@@ -9,21 +9,27 @@ const defaultConfig = {
|
|||||||
MIN_GWEI_BUMP: 1,
|
MIN_GWEI_BUMP: 1,
|
||||||
GAS_BUMP_INTERVAL: 1000 * 60 * 5,
|
GAS_BUMP_INTERVAL: 1000 * 60 * 5,
|
||||||
MAX_GAS_PRICE: 1000,
|
MAX_GAS_PRICE: 1000,
|
||||||
|
GAS_LIMIT_MULTIPLIER: 1.1,
|
||||||
POLL_INTERVAL: 5000,
|
POLL_INTERVAL: 5000,
|
||||||
CONFIRMATIONS: 8,
|
CONFIRMATIONS: 8,
|
||||||
ESTIMATE_GAS: true,
|
ESTIMATE_GAS: true,
|
||||||
|
THROW_ON_REVERT: true,
|
||||||
|
BLOCK_GAS_LIMIT: null,
|
||||||
|
ENABLE_EIP1559: true,
|
||||||
|
DEFAULT_PRIORITY_FEE: 3,
|
||||||
|
BASE_FEE_RESERVE_PERCENTAGE: 50,
|
||||||
|
PRIORITY_FEE_RESERVE_PERCENTAGE: 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
class TxManager {
|
class TxManager {
|
||||||
constructor({ privateKey, rpcUrl, broadcastNodes = [], config = {} }) {
|
constructor({ privateKey, rpcUrl, broadcastNodes = [], config = {}, gasPriceOracleConfig = {} }) {
|
||||||
this.config = Object.assign({ ...defaultConfig }, config)
|
this.config = Object.assign({ ...defaultConfig }, config)
|
||||||
this._privateKey = '0x' + privateKey
|
this._privateKey = privateKey.startsWith('0x') ? privateKey : '0x' + privateKey
|
||||||
this._web3 = new Web3(rpcUrl)
|
this._provider = new ethers.providers.JsonRpcProvider(rpcUrl)
|
||||||
|
this._wallet = new ethers.Wallet(this._privateKey, this._provider)
|
||||||
|
this.address = this._wallet.address
|
||||||
this._broadcastNodes = broadcastNodes
|
this._broadcastNodes = broadcastNodes
|
||||||
this.address = this._web3.eth.accounts.privateKeyToAccount(this._privateKey).address
|
this._gasPriceOracle = new GasPriceOracle({ defaultRpc: rpcUrl, ...gasPriceOracleConfig })
|
||||||
this._web3.eth.accounts.wallet.add(this._privateKey)
|
|
||||||
this._web3.eth.defaultAccount = this.address
|
|
||||||
this._gasPriceOracle = new GasPriceOracle({ defaultRpc: rpcUrl })
|
|
||||||
this._mutex = new Mutex()
|
this._mutex = new Mutex()
|
||||||
this._nonce = null
|
this._nonce = null
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/utils.js
11
src/utils.js
@@ -3,13 +3,12 @@
|
|||||||
*/
|
*/
|
||||||
const sleep = ms => new Promise(res => setTimeout(res, ms))
|
const sleep = ms => new Promise(res => setTimeout(res, ms))
|
||||||
|
|
||||||
/**
|
const max = (a, b) => (a.gt(b) ? a : b)
|
||||||
* A promise that resolves when the source emits specified event
|
|
||||||
*/
|
const min = (a, b) => (a.lt(b) ? a : b)
|
||||||
const when = (source, event) =>
|
|
||||||
new Promise((resolve, reject) => source.once(event, resolve).on('error', reject))
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
sleep,
|
sleep,
|
||||||
when,
|
max,
|
||||||
|
min,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,66 @@
|
|||||||
require('dotenv').config()
|
require('dotenv').config()
|
||||||
require('chai').should()
|
require('chai').should()
|
||||||
const { toHex, toWei } = require('web3-utils')
|
const { providers } = require('ethers')
|
||||||
|
const { parseUnits } = require('ethers').utils
|
||||||
const TxManager = require('../src/TxManager')
|
const TxManager = require('../src/TxManager')
|
||||||
// const Transaction = require('../src/Transaction')
|
// const Transaction = require('../src/Transaction')
|
||||||
const { RPC_URL, PRIVATE_KEY } = process.env
|
const { RPC_URL, PRIVATE_KEY } = process.env
|
||||||
|
|
||||||
describe('TxManager', () => {
|
describe('TxManager', () => {
|
||||||
const manager = new TxManager({
|
let manager
|
||||||
privateKey: PRIVATE_KEY,
|
|
||||||
rpcUrl: RPC_URL,
|
|
||||||
config: {
|
|
||||||
CONFIRMATIONS: 3,
|
|
||||||
GAS_BUMP_INTERVAL: 1000 * 15,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const tx1 = {
|
const tx1 = {
|
||||||
value: 1,
|
value: 1,
|
||||||
gasPrice: toHex(toWei('0.5', 'gwei')),
|
gasPrice: parseUnits('2', 'gwei').toHexString(),
|
||||||
to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0',
|
to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0',
|
||||||
}
|
}
|
||||||
|
|
||||||
const tx2 = {
|
const tx2 = {
|
||||||
|
value: 1,
|
||||||
|
gasPrice: parseUnits('0.5', 'gwei').toHexString(),
|
||||||
|
to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0',
|
||||||
|
}
|
||||||
|
|
||||||
|
const tx3 = {
|
||||||
value: 2,
|
value: 2,
|
||||||
to: '0x0039F22efB07A647557C7C5d17854CFD6D489eF3',
|
to: '0x0039F22efB07A647557C7C5d17854CFD6D489eF3',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tx4 = {
|
||||||
|
value: 1,
|
||||||
|
to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0',
|
||||||
|
}
|
||||||
|
|
||||||
|
const tx5 = {
|
||||||
|
value: 1,
|
||||||
|
to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0',
|
||||||
|
maxFeePerGas: parseUnits('7', 'gwei').toHexString(),
|
||||||
|
maxPriorityFeePerGas: parseUnits('1', 'gwei').toHexString(),
|
||||||
|
type: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
const provider = new providers.JsonRpcProvider(RPC_URL)
|
||||||
|
|
||||||
|
const { name, chainId } = await provider.getNetwork()
|
||||||
|
console.log('\n\n', 'network', { name, chainId }, '\n\n')
|
||||||
|
|
||||||
|
manager = new TxManager({
|
||||||
|
privateKey: PRIVATE_KEY,
|
||||||
|
rpcUrl: RPC_URL,
|
||||||
|
config: {
|
||||||
|
CONFIRMATIONS: 1,
|
||||||
|
GAS_BUMP_INTERVAL: 1000 * 20,
|
||||||
|
},
|
||||||
|
gasPriceOracleConfig: {
|
||||||
|
chainId: chainId,
|
||||||
|
defaultRpc: RPC_URL,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('#transaction', () => {
|
describe('#transaction', () => {
|
||||||
it('should work', async () => {
|
it('should work legacy tx', async () => {
|
||||||
const tx = manager.createTx(tx1)
|
const tx = manager.createTx(tx1)
|
||||||
|
|
||||||
const receipt = await tx
|
const receipt = await tx
|
||||||
@@ -39,8 +72,44 @@ describe('TxManager', () => {
|
|||||||
console.log('receipt', receipt)
|
console.log('receipt', receipt)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should work eip-1559 tx', async () => {
|
||||||
|
const tx = manager.createTx(tx5)
|
||||||
|
|
||||||
|
const receipt = await tx
|
||||||
|
.send()
|
||||||
|
.on('transactionHash', hash => console.log('hash', hash))
|
||||||
|
.on('mined', receipt => console.log('Mined in block', receipt.blockNumber))
|
||||||
|
.on('confirmations', confirmations => console.log('confirmations', confirmations))
|
||||||
|
|
||||||
|
console.log('receipt', receipt)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fetch gas params', async () => {
|
||||||
|
const tx = manager.createTx(tx4)
|
||||||
|
|
||||||
|
const receipt = await tx
|
||||||
|
.send()
|
||||||
|
.on('transactionHash', hash => console.log('hash', hash))
|
||||||
|
.on('mined', receipt => console.log('Mined in block', receipt.blockNumber))
|
||||||
|
.on('confirmations', confirmations => console.log('confirmations', confirmations))
|
||||||
|
|
||||||
|
console.log('receipt', receipt)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should bump gas params', async () => {
|
||||||
|
const tx = manager.createTx(tx2)
|
||||||
|
|
||||||
|
const receipt = await tx
|
||||||
|
.send()
|
||||||
|
.on('transactionHash', hash => console.log('hash', hash))
|
||||||
|
.on('mined', receipt => console.log('Mined in block', receipt.blockNumber))
|
||||||
|
.on('confirmations', confirmations => console.log('confirmations', confirmations))
|
||||||
|
|
||||||
|
console.log('receipt', receipt)
|
||||||
|
})
|
||||||
|
|
||||||
it('should cancel', async () => {
|
it('should cancel', async () => {
|
||||||
const tx = manager.createTx(tx1)
|
const tx = manager.createTx(tx2)
|
||||||
|
|
||||||
setTimeout(() => tx.cancel(), 1000)
|
setTimeout(() => tx.cancel(), 1000)
|
||||||
|
|
||||||
@@ -54,9 +123,9 @@ describe('TxManager', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should replace', async () => {
|
it('should replace', async () => {
|
||||||
const tx = manager.createTx(tx1)
|
const tx = manager.createTx(tx2)
|
||||||
|
|
||||||
setTimeout(() => tx.replace(tx2), 1000)
|
setTimeout(() => tx.replace(tx3), 1000)
|
||||||
|
|
||||||
const receipt = await tx
|
const receipt = await tx
|
||||||
.send()
|
.send()
|
||||||
@@ -66,5 +135,54 @@ describe('TxManager', () => {
|
|||||||
|
|
||||||
console.log('receipt', receipt)
|
console.log('receipt', receipt)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should increase nonce', async () => {
|
||||||
|
const currentNonce = await manager._wallet.getTransactionCount('latest')
|
||||||
|
|
||||||
|
manager._nonce = currentNonce - 1
|
||||||
|
|
||||||
|
const tx = manager.createTx(tx4)
|
||||||
|
|
||||||
|
const receipt = await tx
|
||||||
|
.send()
|
||||||
|
.on('transactionHash', hash => console.log('hash', hash))
|
||||||
|
.on('mined', receipt => console.log('Mined in block', receipt.blockNumber))
|
||||||
|
.on('confirmations', confirmations => console.log('confirmations', confirmations))
|
||||||
|
|
||||||
|
console.log('receipt', receipt)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should disable eip-1559 transactions', async () => {
|
||||||
|
manager.config.ENABLE_EIP1559 = false
|
||||||
|
|
||||||
|
const tx = manager.createTx(tx3)
|
||||||
|
const receipt = await tx
|
||||||
|
.send()
|
||||||
|
.on('transactionHash', hash => console.log('hash', hash))
|
||||||
|
.on('mined', receipt => console.log('Mined in block', receipt.blockNumber))
|
||||||
|
.on('confirmations', confirmations => console.log('confirmations', confirmations))
|
||||||
|
console.log('receipt', receipt)
|
||||||
|
|
||||||
|
manager.config.ENABLE_EIP1559 = true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should send multiple txs', async () => {
|
||||||
|
const genTx = value => ({
|
||||||
|
value,
|
||||||
|
to: '0x0039F22efB07A647557C7C5d17854CFD6D489eF3',
|
||||||
|
})
|
||||||
|
await Promise.all([
|
||||||
|
manager.createTx(genTx(1)).send(),
|
||||||
|
manager.createTx(genTx(2)).send(),
|
||||||
|
manager.createTx(genTx(3)).send(),
|
||||||
|
manager.createTx(genTx(4)).send(),
|
||||||
|
manager.createTx(genTx(5)).send(),
|
||||||
|
manager.createTx(genTx(6)).send(),
|
||||||
|
manager.createTx(genTx(7)).send(),
|
||||||
|
manager.createTx(genTx(8)).send(),
|
||||||
|
manager.createTx(genTx(9)).send(),
|
||||||
|
manager.createTx(genTx(10)).send(),
|
||||||
|
])
|
||||||
|
}).timeout(600000)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user