new architecture (createTx); replace, cancel methods

This commit is contained in:
Alexey 2020-09-30 13:48:27 +03:00
parent 6c514dc904
commit d888fdbd44
2 changed files with 90 additions and 31 deletions

@ -1,7 +1,7 @@
const Web3 = require('web3') const Web3 = require('web3')
const { Mutex } = require('async-mutex') const { Mutex } = require('async-mutex')
const { GasPriceOracle } = require('gas-price-oracle') const { GasPriceOracle } = require('gas-price-oracle')
const { toWei, toHex, toBN, BN } = 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, when } = require('./utils')
@ -18,6 +18,7 @@ const gasPriceErrors = [
const defaultConfig = { const defaultConfig = {
MAX_RETRIES: 10, MAX_RETRIES: 10,
GAS_BUMP_PERCENTAGE: 5, GAS_BUMP_PERCENTAGE: 5,
MIN_GWEI_BUMP: 1,
GAS_BUMP_INTERVAL: 1000 * 60 * 5, GAS_BUMP_INTERVAL: 1000 * 60 * 5,
MAX_GAS_PRICE: 1000, MAX_GAS_PRICE: 1000,
POLL_INTERVAL: 5000, POLL_INTERVAL: 5000,
@ -39,47 +40,79 @@ class TxManager {
} }
/** /**
* Submits transaction to Ethereum network. Resolves when tx gets enough confirmations. * Creates Transaction class instance.
* Emits progress events.
* *
* @param tx Transaction to send * @param tx Transaction to send
*/ */
submit(tx) { async createTx(tx) {
const promiEvent = PromiEvent()
this._submit(tx, promiEvent.eventEmitter).then(promiEvent.resolve).catch(promiEvent.reject)
return promiEvent.eventEmitter
}
async _submit(tx, emitter) {
const release = await this._mutex.acquire()
try { try {
await this._mutex.acquire()
if (!this.nonce) { if (!this.nonce) {
this.nonce = await this._web3.eth.getTransactionCount(this.address, 'latest') this.nonce = await this._web3.eth.getTransactionCount(this.address, 'latest')
} }
return new Transaction(tx, emitter, this).submit() return new Transaction(tx, this)
} finally { } catch (e) {
release() console.log('e', e)
this._mutex.release()
} }
} }
} }
class Transaction { class Transaction {
constructor(tx, emitter, manager) { constructor(tx, manager) {
Object.assign(this, manager) Object.assign(this, manager)
this.manager = manager this.manager = manager
this.tx = tx this.tx = tx
this.emitter = emitter this.promiReceipt = PromiEvent()
this.emitter = this.promiReceipt.eventEmitter
this.retries = 0 this.retries = 0
this.hash = null
// store all submitted hashes to catch cases when an old tx is mined // store all submitted hashes to catch cases when an old tx is mined
// todo: what to do if old tx with the same nonce was submitted // todo: what to do if old tx with the same nonce was submitted
// by other client and we don't have its hash? // by other client and we don't have its hash?
this.hashes = [] this.hashes = []
} }
async submit() { /**
await this._prepare() * Submits transaction to Ethereum network. Resolves when tx gets enough confirmations.
return this._send() * Emits progress events.
*/
send() {
this._prepare()
.then(() => {
this._send()
.then((result) => this.promiReceipt.resolve(result))
.catch((e) => this.promiReceipt.reject(e))
})
.catch((e) => this.promiReceipt.reject(e))
.finally(this.manager._mutex.release())
return this.emitter
}
/**
* Replaces pending tx.
*
* @param tx Transaction to send
*/
replace(tx) {
// todo check if it's not mined yet
console.log('Replacing...')
this.tx = tx
return this.send()
}
/**
* Cancels pending tx.
*/
cancel() {
// todo check if it's not mined yet
console.log('Canceling...')
this.tx = {
to: this.address,
gasPrice: this.tx.gasPrice,
}
this._increaseGasPrice()
return this.send()
} }
async _prepare() { async _prepare() {
@ -87,7 +120,7 @@ class Transaction {
if (!this.tx.gasPrice) { if (!this.tx.gasPrice) {
this.tx.gasPrice = await this._getGasPrice('fast') this.tx.gasPrice = await this._getGasPrice('fast')
} }
this.tx.nonce = this.nonce this.tx.nonce = this.manager.nonce
} }
async _send() { async _send() {
@ -101,8 +134,8 @@ class Transaction {
await this._broadcast(signedTx.rawTransaction) await this._broadcast(signedTx.rawTransaction)
console.log('Broadcasted. Start waiting for mining...') console.log('Broadcasted. Start waiting for mining...')
// The most reliable way to see if one of our tx was mined is to track current nonce // The most reliable way to see if one of our tx was mined is to track current nonce
let latestNonce = await this._getLastNonce()
while (this.tx.nonce > latestNonce) { while (this.tx.nonce >= (await this._getLastNonce())) {
if (Date.now() - this.tx.date >= this.config.GAS_BUMP_INTERVAL) { if (Date.now() - this.tx.date >= this.config.GAS_BUMP_INTERVAL) {
if (this._increaseGasPrice()) { if (this._increaseGasPrice()) {
console.log('Resubmit with higher gas price') console.log('Resubmit with higher gas price')
@ -116,7 +149,6 @@ class Transaction {
let receipt = await this._getReceipts() let receipt = await this._getReceipts()
let retryAttempt = 5 let retryAttempt = 5
while (retryAttempt >= 0 && !receipt) { while (retryAttempt >= 0 && !receipt) {
console.log('retryAttempt', retryAttempt)
await sleep(1000) await sleep(1000)
receipt = await this._getReceipts() receipt = await this._getReceipts()
retryAttempt-- retryAttempt--
@ -199,14 +231,19 @@ class Transaction {
} }
_increaseGasPrice() { _increaseGasPrice() {
const newGasPrice = toBN(this.tx.gasPrice).mul(toBN(this.config.GAS_BUMP_PERCENTAGE)).div(toBN(100)) const minGweiBump = toBN(toWei(this.config.MIN_GWEI_BUMP.toString(), 'Gwei'))
const oldGasPrice = toBN(this.tx.gasPrice)
const newGasPrice = BN.max(
oldGasPrice.mul(toBN(100 + this.config.GAS_BUMP_PERCENTAGE)).div(toBN(100)),
oldGasPrice.add(minGweiBump),
)
const maxGasPrice = toBN(toWei(this.config.MAX_GAS_PRICE.toString(), 'gwei')) const maxGasPrice = toBN(toWei(this.config.MAX_GAS_PRICE.toString(), 'gwei'))
if (toBN(this.tx.gasPrice).eq(maxGasPrice)) { if (toBN(this.tx.gasPrice).eq(maxGasPrice)) {
console.log('Already at max gas price, not bumping') console.log('Already at max gas price, not bumping')
return false return false
} }
this.tx.gasPrice = toHex(BN.min(newGasPrice, maxGasPrice)) this.tx.gasPrice = toHex(BN.min(newGasPrice, maxGasPrice))
console.log(`Increasing gas price to ${this.tx.gasPrice}`) console.log(`Increasing gas price to ${fromWei(this.tx.gasPrice, 'Gwei')}`)
return true return true
} }

@ -6,20 +6,27 @@ const TxM = new TxManager({
privateKey, privateKey,
rpcUrl, rpcUrl,
config: { config: {
CONFIRMATIONS: 2, CONFIRMATIONS: 1,
GAS_BUMP_INTERVAL: 1000 * 15, GAS_BUMP_INTERVAL: 1000 * 15,
}, },
}) })
const tx = { const tx = {
from: '0x03Ebd0748Aa4D1457cF479cce56309641e0a98F5',
value: 0, value: 0,
gasPrice: toHex(toWei('0.1', 'gwei')),
to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0',
}
const tx2 = {
value: 1,
// gasPrice: toHex(toWei('0.1', 'gwei')), // gasPrice: toHex(toWei('0.1', 'gwei')),
to: '0x03Ebd0748Aa4D1457cF479cce56309641e0a98F5', to: '0x0039F22efB07A647557C7C5d17854CFD6D489eF3',
} }
async function main() { async function main() {
const receipt = await TxM.submit(tx) const Tx = await TxM.createTx(tx)
const receipt1 = await Tx.send()
.on('transactionHash', (hash) => { .on('transactionHash', (hash) => {
console.log('hash', hash) console.log('hash', hash)
}) })
@ -29,7 +36,22 @@ async function main() {
.on('confirmations', (confirmations) => { .on('confirmations', (confirmations) => {
console.log('confirmations', confirmations) console.log('confirmations', confirmations)
}) })
console.log('receipt', receipt)
// setTimeout(async () => await Tx.cancel(), 800)
// const receipt2 = await Tx.replace(tx2)
// .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('receipt2', receipt2)
console.log('receipt1', await receipt1)
} }
main() main()