switch from web3 to ethers

This commit is contained in:
poma 2020-10-16 21:44:09 +03:00
parent b0e25e800f
commit adf328f81c
No known key found for this signature in database
GPG Key ID: BA20CB01FE165657
6 changed files with 480 additions and 1995 deletions

@ -18,8 +18,8 @@
], ],
"dependencies": { "dependencies": {
"async-mutex": "^0.2.4", "async-mutex": "^0.2.4",
"ethers": "^5.0.17",
"gas-price-oracle": "^0.1.5", "gas-price-oracle": "^0.1.5",
"web3": "^1.3.0",
"web3-core-promievent": "^1.3.0", "web3-core-promievent": "^1.3.0",
"web3-utils": "^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 { toWei, toHex, toBN, BN, fromWei } = require('web3-utils')
const PromiEvent = require('web3-core-promievent') const PromiEvent = require('web3-core-promievent')
const { sleep, when } = require('./utils') const { sleep } = require('./utils')
// prettier-ignore
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'
] ]
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',
/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 \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 // prettier-ignore
const sameTxErrors = [ const sameTxErrors = [
'Returned error: Transaction with the same hash was already imported.', 'Transaction with the same hash was already imported.',
] ]
class Transaction { class Transaction {
@ -59,9 +60,9 @@ class Transaction {
this.tx = { ...tx } this.tx = { ...tx }
return return
} }
if (!tx.gas) { if (!tx.gasLimit) {
tx.gas = await this._web3.eth.estimateGas(tx) tx.gasLimit = await this._wallet.estimateGas(tx)
tx.gas = Math.floor(tx.gas * 1.1) tx.gasLimit = Math.floor(tx.gasLimit * this.config.GAS_LIMIT_MULTIPLIER)
} }
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 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, from: this.address,
to: this.address, to: this.address,
value: 0, value: 0,
gas: 21000, gasLimit: 21000,
}) })
} }
@ -111,19 +112,24 @@ class Transaction {
* @private * @private
*/ */
async _prepare() { async _prepare() {
if (!this.tx.gas || this.config.ESTIMATE_GAS) { if (!this.tx.gasLimit || this.config.ESTIMATE_GAS) {
const gas = await this._web3.eth.estimateGas(this.tx) const gas = await this._wallet.estimateGas(this.tx)
if (!this.tx.gas) { if (!this.tx.gasLimit) {
this.tx.gas = Math.floor(gas * 1.1) this.tx.gasLimit = Math.floor(gas * this.config.GAS_LIMIT_MULTIPLIER)
} }
} }
if (!this.tx.gasPrice) { if (!this.tx.gasPrice) {
this.tx.gasPrice = await this._getGasPrice('fast') 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.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() { 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._handleSendError(e)
} }
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) console.log(this.tx)
} }
@ -161,7 +167,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,7 +176,7 @@ 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)
@ -236,7 +242,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 +254,48 @@ 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) { _handleSendError(e) {
console.log('Got error', e) console.log('Got error', e)
// nonce is too low, trying to increase and resubmit if (e.code === 'SERVER_ERROR' && e.error) {
if (this._hasError(e.message, nonceErrors)) { const message = e.error.message
console.log(`Nonce ${this.tx.nonce} is too low, increasing and retrying`) console.log('Error', e.error.code, e.error.message)
if (this.retries <= this.config.MAX_RETRIES) {
this.tx.nonce++ // nonce is too low, trying to increase and resubmit
this.retries++ 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() 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 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}`)
} }
/** /**
@ -337,7 +348,7 @@ class Transaction {
* @private * @private
*/ */
_getLastNonce() { _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 { 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,6 +9,7 @@ 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,
@ -17,12 +18,11 @@ const defaultConfig = {
class TxManager { class TxManager {
constructor({ privateKey, rpcUrl, broadcastNodes = [], config = {} }) { constructor({ privateKey, rpcUrl, broadcastNodes = [], config = {} }) {
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._web3.eth.accounts.wallet.add(this._privateKey)
this._web3.eth.defaultAccount = this.address
this._gasPriceOracle = new GasPriceOracle({ defaultRpc: rpcUrl }) this._gasPriceOracle = new GasPriceOracle({ defaultRpc: rpcUrl })
this._mutex = new Mutex() this._mutex = new Mutex()
this._nonce = null this._nonce = null

@ -3,13 +3,6 @@
*/ */
const sleep = ms => new Promise(res => setTimeout(res, ms)) 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 = { module.exports = {
sleep, sleep,
when,
} }

@ -17,11 +17,17 @@ describe('TxManager', () => {
const tx1 = { const tx1 = {
value: 1, value: 1,
gasPrice: toHex(toWei('0.5', 'gwei')), gasPrice: toHex(toWei('1', 'gwei')),
to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0', to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0',
} }
const tx2 = { const tx2 = {
value: 1,
gasPrice: toHex(toWei('0.5', 'gwei')),
to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0',
}
const tx3 = {
value: 2, value: 2,
to: '0x0039F22efB07A647557C7C5d17854CFD6D489eF3', to: '0x0039F22efB07A647557C7C5d17854CFD6D489eF3',
} }
@ -39,8 +45,20 @@ describe('TxManager', () => {
console.log('receipt', receipt) 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 () => { it('should cancel', async () => {
const tx = manager.createTx(tx1) const tx = manager.createTx(tx2)
setTimeout(() => tx.cancel(), 1000) setTimeout(() => tx.cancel(), 1000)
@ -54,9 +72,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()

2319
yarn.lock

File diff suppressed because it is too large Load Diff