switch from web3 to ethers
This commit is contained in:
parent
b0e25e800f
commit
adf328f81c
@ -18,8 +18,8 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"async-mutex": "^0.2.4",
|
||||
"ethers": "^5.0.17",
|
||||
"gas-price-oracle": "^0.1.5",
|
||||
"web3": "^1.3.0",
|
||||
"web3-core-promievent": "^1.3.0",
|
||||
"web3-utils": "^1.3.0"
|
||||
},
|
||||
|
@ -1,22 +1,23 @@
|
||||
const Web3 = require('web3')
|
||||
const ethers = require('ethers')
|
||||
const { toWei, toHex, toBN, BN, fromWei } = require('web3-utils')
|
||||
const PromiEvent = require('web3-core-promievent')
|
||||
const { sleep, when } = require('./utils')
|
||||
const { sleep } = require('./utils')
|
||||
|
||||
// prettier-ignore
|
||||
const nonceErrors = [
|
||||
'Returned error: Transaction nonce is too low. Try incrementing the nonce.',
|
||||
'Returned error: nonce too low',
|
||||
'Transaction nonce is too low. Try incrementing the nonce.',
|
||||
'nonce too low'
|
||||
]
|
||||
|
||||
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.',
|
||||
'Returned error: replacement transaction underpriced',
|
||||
/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 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 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./,
|
||||
]
|
||||
|
||||
// prettier-ignore
|
||||
const sameTxErrors = [
|
||||
'Returned error: Transaction with the same hash was already imported.',
|
||||
'Transaction with the same hash was already imported.',
|
||||
]
|
||||
|
||||
class Transaction {
|
||||
@ -59,9 +60,9 @@ class Transaction {
|
||||
this.tx = { ...tx }
|
||||
return
|
||||
}
|
||||
if (!tx.gas) {
|
||||
tx.gas = await this._web3.eth.estimateGas(tx)
|
||||
tx.gas = Math.floor(tx.gas * 1.1)
|
||||
if (!tx.gasLimit) {
|
||||
tx.gasLimit = await this._wallet.estimateGas(tx)
|
||||
tx.gasLimit = Math.floor(tx.gasLimit * this.config.GAS_LIMIT_MULTIPLIER)
|
||||
}
|
||||
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
|
||||
@ -80,7 +81,7 @@ class Transaction {
|
||||
from: this.address,
|
||||
to: this.address,
|
||||
value: 0,
|
||||
gas: 21000,
|
||||
gasLimit: 21000,
|
||||
})
|
||||
}
|
||||
|
||||
@ -111,19 +112,24 @@ class Transaction {
|
||||
* @private
|
||||
*/
|
||||
async _prepare() {
|
||||
if (!this.tx.gas || this.config.ESTIMATE_GAS) {
|
||||
const gas = await this._web3.eth.estimateGas(this.tx)
|
||||
if (!this.tx.gas) {
|
||||
this.tx.gas = Math.floor(gas * 1.1)
|
||||
if (!this.tx.gasLimit || this.config.ESTIMATE_GAS) {
|
||||
const gas = await this._wallet.estimateGas(this.tx)
|
||||
if (!this.tx.gasLimit) {
|
||||
this.tx.gasLimit = Math.floor(gas * this.config.GAS_LIMIT_MULTIPLIER)
|
||||
}
|
||||
}
|
||||
if (!this.tx.gasPrice) {
|
||||
this.tx.gasPrice = await this._getGasPrice('fast')
|
||||
}
|
||||
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
|
||||
if (!this.manager._chainId) {
|
||||
const net = await this._provider.getNetwork()
|
||||
this.manager._chainId = net.chainId
|
||||
}
|
||||
this.tx.chainId = this.manager._chainId
|
||||
}
|
||||
|
||||
/**
|
||||
@ -134,19 +140,19 @@ class Transaction {
|
||||
*/
|
||||
async _send() {
|
||||
// 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.tx.hash = signedTx.transactionHash
|
||||
this.hashes.push(signedTx.transactionHash)
|
||||
const txHash = ethers.utils.keccak256(signedTx)
|
||||
this.hashes.push(txHash)
|
||||
|
||||
try {
|
||||
await this._broadcast(signedTx.rawTransaction)
|
||||
await this._broadcast(signedTx)
|
||||
} catch (e) {
|
||||
return this._handleSendError(e)
|
||||
}
|
||||
|
||||
this._emitter.emit('transactionHash', signedTx.transactionHash)
|
||||
console.log(`Broadcasted transaction ${signedTx.transactionHash}`)
|
||||
this._emitter.emit('transactionHash', txHash)
|
||||
console.log(`Broadcasted transaction ${txHash}`)
|
||||
console.log(this.tx)
|
||||
}
|
||||
|
||||
@ -161,7 +167,7 @@ class Transaction {
|
||||
while (true) {
|
||||
// We are already waiting on certain tx hash
|
||||
if (this.currentTxHash) {
|
||||
const receipt = await this._web3.eth.getTransactionReceipt(this.currentTxHash)
|
||||
const receipt = await this._provider.getTransactionReceipt(this.currentTxHash)
|
||||
|
||||
if (!receipt) {
|
||||
// We were waiting for some tx but it disappeared
|
||||
@ -170,7 +176,7 @@ class Transaction {
|
||||
continue
|
||||
}
|
||||
|
||||
const currentBlock = await this._web3.eth.getBlockNumber()
|
||||
const currentBlock = await this._provider.getBlockNumber()
|
||||
const confirmations = Math.max(0, currentBlock - receipt.blockNumber)
|
||||
// todo don't emit repeating confirmation count
|
||||
this._emitter.emit('confirmations', confirmations)
|
||||
@ -236,7 +242,7 @@ class Transaction {
|
||||
|
||||
async _getReceipts() {
|
||||
for (const hash of this.hashes.reverse()) {
|
||||
const receipt = await this._web3.eth.getTransactionReceipt(hash)
|
||||
const receipt = await this._provider.getTransactionReceipt(hash)
|
||||
if (receipt) {
|
||||
return receipt
|
||||
}
|
||||
@ -248,43 +254,48 @@ class Transaction {
|
||||
* Broadcasts tx to multiple nodes, waits for tx hash only on main node
|
||||
*/
|
||||
_broadcast(rawTx) {
|
||||
const main = this._web3.eth.sendSignedTransaction(rawTx)
|
||||
const main = this._provider.sendTransaction(rawTx)
|
||||
for (const node of this._broadcastNodes) {
|
||||
try {
|
||||
new Web3(node).eth.sendSignedTransaction(rawTx)
|
||||
new ethers.providers.JsonRpcProvider(node).sendTransaction(rawTx)
|
||||
} catch (e) {
|
||||
console.log(`Failed to send transaction to node ${node}: ${e}`)
|
||||
}
|
||||
}
|
||||
return when(main, 'transactionHash')
|
||||
return main
|
||||
}
|
||||
|
||||
_handleSendError(e) {
|
||||
console.log('Got error', e)
|
||||
|
||||
// nonce is too low, trying to increase and resubmit
|
||||
if (this._hasError(e.message, nonceErrors)) {
|
||||
console.log(`Nonce ${this.tx.nonce} is too low, increasing and retrying`)
|
||||
if (this.retries <= this.config.MAX_RETRIES) {
|
||||
this.tx.nonce++
|
||||
this.retries++
|
||||
if (e.code === 'SERVER_ERROR' && e.error) {
|
||||
const message = e.error.message
|
||||
console.log('Error', e.error.code, e.error.message)
|
||||
|
||||
// nonce is too low, trying to increase and resubmit
|
||||
if (this._hasError(message, nonceErrors)) {
|
||||
console.log(`Nonce ${this.tx.nonce} is too low, increasing and retrying`)
|
||||
if (this.retries <= this.config.MAX_RETRIES) {
|
||||
this.tx.nonce++
|
||||
this.retries++
|
||||
return this._send()
|
||||
}
|
||||
}
|
||||
|
||||
// there is already a pending tx with higher gas price, trying to bump and resubmit
|
||||
if (this._hasError(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(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
|
||||
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}`)
|
||||
throw new Error(`Send error: ${e}`)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -337,7 +348,7 @@ class Transaction {
|
||||
* @private
|
||||
*/
|
||||
_getLastNonce() {
|
||||
return this._web3.eth.getTransactionCount(this.address, 'latest')
|
||||
return this._wallet.getTransactionCount('latest')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
const Web3 = require('web3')
|
||||
const ethers = require('ethers')
|
||||
const { Mutex } = require('async-mutex')
|
||||
const { GasPriceOracle } = require('gas-price-oracle')
|
||||
const Transaction = require('./Transaction')
|
||||
@ -9,6 +9,7 @@ const defaultConfig = {
|
||||
MIN_GWEI_BUMP: 1,
|
||||
GAS_BUMP_INTERVAL: 1000 * 60 * 5,
|
||||
MAX_GAS_PRICE: 1000,
|
||||
GAS_LIMIT_MULTIPLIER: 1.1,
|
||||
POLL_INTERVAL: 5000,
|
||||
CONFIRMATIONS: 8,
|
||||
ESTIMATE_GAS: true,
|
||||
@ -17,12 +18,11 @@ const defaultConfig = {
|
||||
class TxManager {
|
||||
constructor({ privateKey, rpcUrl, broadcastNodes = [], config = {} }) {
|
||||
this.config = Object.assign({ ...defaultConfig }, config)
|
||||
this._privateKey = '0x' + privateKey
|
||||
this._web3 = new Web3(rpcUrl)
|
||||
this._privateKey = privateKey.startsWith('0x') ? privateKey : '0x' + privateKey
|
||||
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.address = this._web3.eth.accounts.privateKeyToAccount(this._privateKey).address
|
||||
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._nonce = null
|
||||
|
@ -3,13 +3,6 @@
|
||||
*/
|
||||
const sleep = ms => new Promise(res => setTimeout(res, ms))
|
||||
|
||||
/**
|
||||
* A promise that resolves when the source emits specified event
|
||||
*/
|
||||
const when = (source, event) =>
|
||||
new Promise((resolve, reject) => source.once(event, resolve).on('error', reject))
|
||||
|
||||
module.exports = {
|
||||
sleep,
|
||||
when,
|
||||
}
|
||||
|
@ -17,11 +17,17 @@ describe('TxManager', () => {
|
||||
|
||||
const tx1 = {
|
||||
value: 1,
|
||||
gasPrice: toHex(toWei('0.5', 'gwei')),
|
||||
gasPrice: toHex(toWei('1', 'gwei')),
|
||||
to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0',
|
||||
}
|
||||
|
||||
const tx2 = {
|
||||
value: 1,
|
||||
gasPrice: toHex(toWei('0.5', 'gwei')),
|
||||
to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0',
|
||||
}
|
||||
|
||||
const tx3 = {
|
||||
value: 2,
|
||||
to: '0x0039F22efB07A647557C7C5d17854CFD6D489eF3',
|
||||
}
|
||||
@ -39,8 +45,20 @@ describe('TxManager', () => {
|
||||
console.log('receipt', receipt)
|
||||
})
|
||||
|
||||
it('should bump gas price', 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 () => {
|
||||
const tx = manager.createTx(tx1)
|
||||
const tx = manager.createTx(tx2)
|
||||
|
||||
setTimeout(() => tx.cancel(), 1000)
|
||||
|
||||
@ -54,9 +72,9 @@ describe('TxManager', () => {
|
||||
})
|
||||
|
||||
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
|
||||
.send()
|
||||
|
Loading…
Reference in New Issue
Block a user