10 Commits

Author SHA1 Message Date
Danil Kovtonyuk
142ce883b4 bump version 2022-05-25 16:09:25 +10:00
Danil Kovtonyuk
cdce2334e4 revert estimate priority fee 2022-05-25 16:07:43 +10:00
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
Danil Kovtonyuk
b6ed5d2bc0 bump gas price oracle 2022-04-13 23:09:49 +10:00
Danil Kovtonyuk
b4ee1e5117 bump gas price oracle 2021-11-16 03:50:18 +10:00
Danil Kovtonyuk
56aa312829 bump gas price oracle 2021-11-16 01:56:29 +10:00
Danil Kovtonyuk
07dcb5d258 bump gas price oracle 2021-11-15 16:43:36 +10:00
5 changed files with 115 additions and 61 deletions

View File

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

View File

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

View File

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

View File

@@ -1,19 +1,13 @@
require('dotenv').config() require('dotenv').config()
require('chai').should() require('chai').should()
const { providers } = require('ethers')
const { parseUnits } = require('ethers').utils 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: 1,
GAS_BUMP_INTERVAL: 1000 * 20,
},
})
const tx1 = { const tx1 = {
value: 1, value: 1,
@@ -45,6 +39,26 @@ describe('TxManager', () => {
type: 2, 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 legacy tx', async () => { it('should work legacy tx', async () => {
const tx = manager.createTx(tx1) const tx = manager.createTx(tx1)
@@ -122,6 +136,36 @@ 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 () => { it('should send multiple txs', async () => {
const genTx = value => ({ const genTx = value => ({
value, value,
@@ -139,6 +183,6 @@ describe('TxManager', () => {
manager.createTx(genTx(9)).send(), manager.createTx(genTx(9)).send(),
manager.createTx(genTx(10)).send(), manager.createTx(genTx(10)).send(),
]) ])
}) }).timeout(600000)
}) })
}) })

View File

@@ -485,12 +485,12 @@ async-mutex@^0.2.4:
dependencies: dependencies:
tslib "^2.0.0" tslib "^2.0.0"
axios@^0.19.2: axios@^0.21.2:
version "0.19.2" version "0.21.4"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==
dependencies: dependencies:
follow-redirects "1.5.10" follow-redirects "^1.14.0"
balanced-match@^1.0.0: balanced-match@^1.0.0:
version "1.0.0" version "1.0.0"
@@ -655,13 +655,6 @@ debug@4.1.1:
dependencies: dependencies:
ms "^2.1.1" ms "^2.1.1"
debug@=3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
dependencies:
ms "2.0.0"
debug@^4.0.1, debug@^4.1.1: debug@^4.0.1, debug@^4.1.1:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1"
@@ -1038,12 +1031,10 @@ flatted@^2.0.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
follow-redirects@1.5.10: follow-redirects@^1.14.0:
version "1.5.10" version "1.14.5"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.5.tgz#f09a5848981d3c772b5392309778523f8d85c381"
integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== integrity sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==
dependencies:
debug "=3.1.0"
fs.realpath@^1.0.0: fs.realpath@^1.0.0:
version "1.0.0" version "1.0.0"
@@ -1065,12 +1056,12 @@ 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" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
gas-price-oracle@^0.4.0: gas-price-oracle@^0.4.7:
version "0.4.0" version "0.4.7"
resolved "https://registry.yarnpkg.com/gas-price-oracle/-/gas-price-oracle-0.4.0.tgz#1b8426bce92ebcff6cc98a0c5638769cb22417b1" resolved "https://registry.yarnpkg.com/gas-price-oracle/-/gas-price-oracle-0.4.7.tgz#47406048083074bcab677efb9de08663e742153d"
integrity sha512-5ct4VwTqTigD1V3EvUn513e41OIA+/Ubw0FITVfarw7AjVpg4LQSt9anRt+LcDU4+u+eMUEA3VP0VnvMN8nybA== integrity sha512-Ti8nhpATm83YebWU/Pz5xclZoTkzOblIhT504ZViZJUcd8jOxgj9pWtCasg8RYw+d0f19m0dJUPvdj04RC4o3A==
dependencies: dependencies:
axios "^0.19.2" axios "^0.21.2"
bignumber.js "^9.0.0" bignumber.js "^9.0.0"
get-caller-file@^2.0.1: get-caller-file@^2.0.1:
@@ -1428,11 +1419,6 @@ mocha@^8.1.3:
yargs-parser "13.1.2" yargs-parser "13.1.2"
yargs-unparser "1.6.1" yargs-unparser "1.6.1"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
ms@2.1.2, ms@^2.1.1: ms@2.1.2, ms@^2.1.1:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"