4 Commits

Author SHA1 Message Date
Danil Kovtonyuk
09c24e12c9 bump gas price oracle 2022-05-23 20:28:36 +10:00
Danil Kovtonyuk
c1c6079745 fix: use eth_maxPriorityFeePerGas 2022-05-23 20:28:36 +10:00
Danil Kovtonyuk
eaaa54b7f9 fix: configure EIP-1559 feature 2022-05-23 20:28:36 +10:00
Danil Kovtonyuk
6adf89472e fix: estimate priority fee
- fix: predefined errors
2022-05-23 20:28:36 +10:00
5 changed files with 134 additions and 37 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "tx-manager",
"version": "0.4.6",
"version": "0.4.7",
"description": "",
"main": "index.js",
"scripts": {
@@ -23,7 +23,7 @@
"dependencies": {
"async-mutex": "^0.2.4",
"ethers": "^5.4.6",
"gas-price-oracle": "^0.4.6",
"gas-price-oracle": "^0.4.7",
"web3-core-promievent": "^1.3.0"
},
"devDependencies": {

View File

@@ -6,21 +6,27 @@ const { sleep, min, max } = require('./utils')
const nonceErrors = [
'Transaction nonce is too low. Try incrementing the nonce.',
'nonce too low',
/nonce too low/i,
'nonce has already been used',
/OldNonce/,
'invalid transaction nonce',
]
const gasPriceErrors = [
'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.',
'replacement transaction underpriced',
'transaction underpriced',
/replacement transaction underpriced/i,
/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
const sameTxErrors = [
'Transaction with the same hash was already imported.',
'already known',
'AlreadyKnown',
'Known transaction'
]
class Transaction {
@@ -63,11 +69,14 @@ class Transaction {
this.tx = { ...tx }
return
}
if (!tx.gasLimit) {
tx.gasLimit = await this._wallet.estimateGas(tx)
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`
// start no less than current tx gas params
@@ -92,7 +101,6 @@ class Transaction {
from: this.address,
to: this.address,
value: 0,
gasLimit: 21000,
})
}
@@ -128,8 +136,14 @@ class Transaction {
this.config.BLOCK_GAS_LIMIT = Math.floor(lastBlock.gasLimit.toNumber() * 0.95)
}
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._wallet.estimateGas(this.tx)
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)
@@ -141,12 +155,6 @@ class Transaction {
}
this.tx.nonce = this.manager._nonce
if (!this.manager._chainId) {
const net = await this._provider.getNetwork()
this.manager._chainId = net.chainId
}
this.tx.chainId = this.manager._chainId
if (this.tx.gasPrice || (this.tx.maxFeePerGas && this.tx.maxPriorityFeePerGas)) {
return
}
@@ -172,7 +180,7 @@ class Transaction {
try {
await this._broadcast(signedTx)
} catch (e) {
return this._handleSendError(e)
return this._handleRpcError(e, '_send')
}
this._emitter.emit('transactionHash', txHash)
@@ -290,7 +298,7 @@ class Transaction {
return main
}
_handleSendError(e) {
_handleRpcError(e, method) {
if (e.error.error) {
// Sometimes ethers wraps known errors, unwrap it in this case
e = e.error
@@ -305,17 +313,20 @@ class Transaction {
if (this.retries <= this.config.MAX_RETRIES) {
this.tx.nonce++
this.retries++
return this._send()
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, 'gwei')} gwei is too low, increasing and retrying`,
`Gas price ${formatUnits(
this.tx.gasPrice || this.tx.maxFeePerGas,
'gwei',
)} gwei is too low, increasing and retrying`,
)
if (this._increaseGasPrice()) {
return this._send()
return this[method]()
} else {
throw new Error('Already at max gas price, but still not enough to submit the transaction')
}
@@ -376,8 +387,10 @@ class Transaction {
oldMaxPriorityFeePerGas.add(minGweiBump),
)
this.tx.maxFeePerGas = min(newMaxFeePerGas, maxGasPrice).toHexString()
this.tx.maxPriorityFeePerGas = min(newMaxPriorityFeePerGas, this.tx.maxFeePerGas).toHexString()
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`)
}
@@ -409,6 +422,34 @@ class Transaction {
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
*
@@ -420,12 +461,14 @@ class Transaction {
const block = await this._provider.getBlock('latest')
// Check network support for EIP-1559
if (block && block.baseFeePerGas) {
const maxPriorityFeePerGas = parseUnits(this.config.PRIORITY_FEE_GWEI.toString(), 'gwei')
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(),
@@ -439,6 +482,14 @@ class Transaction {
}
}
}
async _estimateGas(tx) {
try {
return await this._wallet.estimateGas(tx)
} catch (e) {
return this._handleRpcError(e, '_estimateGas')
}
}
}
module.exports = Transaction

View File

@@ -15,8 +15,10 @@ const defaultConfig = {
ESTIMATE_GAS: true,
THROW_ON_REVERT: true,
BLOCK_GAS_LIMIT: null,
PRIORITY_FEE_GWEI: 3,
ENABLE_EIP1559: true,
DEFAULT_PRIORITY_FEE: 3,
BASE_FEE_RESERVE_PERCENTAGE: 50,
PRIORITY_FEE_RESERVE_PERCENTAGE: 10,
}
class TxManager {

View File

@@ -1,19 +1,13 @@
require('dotenv').config()
require('chai').should()
const { providers } = require('ethers')
const { parseUnits } = require('ethers').utils
const TxManager = require('../src/TxManager')
// const Transaction = require('../src/Transaction')
const { RPC_URL, PRIVATE_KEY } = process.env
describe('TxManager', () => {
const manager = new TxManager({
privateKey: PRIVATE_KEY,
rpcUrl: RPC_URL,
config: {
CONFIRMATIONS: 1,
GAS_BUMP_INTERVAL: 1000 * 20,
},
})
let manager
const tx1 = {
value: 1,
@@ -45,6 +39,26 @@ describe('TxManager', () => {
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', () => {
it('should work legacy tx', async () => {
const tx = manager.createTx(tx1)
@@ -122,6 +136,36 @@ describe('TxManager', () => {
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,
@@ -139,6 +183,6 @@ describe('TxManager', () => {
manager.createTx(genTx(9)).send(),
manager.createTx(genTx(10)).send(),
])
})
}).timeout(600000)
})
})

View File

@@ -1056,10 +1056,10 @@ functional-red-black-tree@^1.0.1:
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
gas-price-oracle@^0.4.6:
version "0.4.6"
resolved "https://registry.yarnpkg.com/gas-price-oracle/-/gas-price-oracle-0.4.6.tgz#3e496092122896f1c80ea7eeeecea979d106b3aa"
integrity sha512-/z0wtzKa6FDTWmgikPnELWN8KiPHhCy3Z+waeKVMgvs5FBxibgwOUL1VlMsC4mVkXBoDadnBtFNOpUMgbt5pvg==
gas-price-oracle@^0.4.7:
version "0.4.7"
resolved "https://registry.yarnpkg.com/gas-price-oracle/-/gas-price-oracle-0.4.7.tgz#47406048083074bcab677efb9de08663e742153d"
integrity sha512-Ti8nhpATm83YebWU/Pz5xclZoTkzOblIhT504ZViZJUcd8jOxgj9pWtCasg8RYw+d0f19m0dJUPvdj04RC4o3A==
dependencies:
axios "^0.21.2"
bignumber.js "^9.0.0"