2020-10-16 20:44:09 +02:00
const ethers = require ( 'ethers' )
2020-10-17 04:22:55 +02:00
const { parseUnits , formatUnits } = ethers . utils
const BigNumber = ethers . BigNumber
2020-10-01 06:56:47 +02:00
const PromiEvent = require ( 'web3-core-promievent' )
2020-10-17 04:22:55 +02:00
const { sleep , min , max } = require ( './utils' )
2020-10-01 06:56:47 +02:00
const nonceErrors = [
2020-10-16 20:44:09 +02:00
'Transaction nonce is too low. Try incrementing the nonce.' ,
2020-11-25 21:33:34 +01:00
'nonce too low' ,
'nonce has already been used' ,
2020-10-01 06:56:47 +02:00
]
const gasPriceErrors = [
2020-10-16 20:44:09 +02:00
'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' ,
2020-11-26 08:34:40 +01:00
'transaction underpriced' ,
2020-10-16 20:44:09 +02:00
/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./ ,
2020-10-01 06:56:47 +02:00
]
2020-10-02 11:59:26 +02:00
// prettier-ignore
2020-10-02 12:04:09 +02:00
const sameTxErrors = [
2020-10-16 20:44:09 +02:00
'Transaction with the same hash was already imported.' ,
2020-11-26 08:34:40 +01:00
'already known' ,
2020-10-02 12:04:09 +02:00
]
2020-10-01 06:56:47 +02:00
class Transaction {
constructor ( tx , manager ) {
Object . assign ( this , manager )
this . manager = manager
this . tx = { ... tx }
this . _promise = PromiEvent ( )
this . _emitter = this . _promise . eventEmitter
this . executed = false
this . retries = 0
this . currentTxHash = null
// store all submitted hashes to catch cases when an old tx is mined
this . hashes = [ ]
}
/ * *
* Submits the transaction to Ethereum network . Resolves when tx gets enough confirmations .
* Emits progress events .
* /
send ( ) {
if ( this . executed ) {
throw new Error ( 'The transaction was already executed' )
}
this . executed = true
2020-10-02 11:14:40 +02:00
this . _execute ( ) . then ( this . _promise . resolve ) . catch ( this . _promise . reject )
2020-10-01 06:56:47 +02:00
return this . _emitter
}
/ * *
* Replaces a pending tx .
*
* @ param tx Transaction to send
* /
async replace ( tx ) {
// todo throw error if the current transaction is mined already
console . log ( 'Replacing current transaction' )
if ( ! this . executed ) {
// Tx was not executed yet, just replace it
this . tx = { ... tx }
return
}
2020-10-16 20:44:09 +02:00
if ( ! tx . gasLimit ) {
tx . gasLimit = await this . _wallet . estimateGas ( tx )
tx . gasLimit = Math . floor ( tx . gasLimit * this . config . GAS _LIMIT _MULTIPLIER )
2020-12-24 06:39:07 +01:00
tx . gasLimit = Math . min ( tx . gasLimit , this . config . BLOCK _GAS _LIMIT )
2020-10-01 06:56:47 +02:00
}
tx . nonce = this . tx . nonce // can be different from `this.manager._nonce`
2021-09-02 08:25:23 +02:00
// start no less than current tx gas params
if ( this . tx . gasPrice ) {
tx . gasPrice = Math . max ( this . tx . gasPrice , tx . gasPrice || 0 )
} else {
tx . maxFeePerGas = Math . max ( this . tx . maxFeePerGas , tx . maxFeePerGas || 0 )
tx . maxPriorityFeePerGas = Math . max ( this . tx . maxPriorityFeePerGas , tx . maxPriorityFeePerGas || 0 )
}
2020-10-01 06:56:47 +02:00
this . tx = { ... tx }
this . _increaseGasPrice ( )
await this . _send ( )
}
/ * *
* Cancels a pending tx .
* /
cancel ( ) {
console . log ( 'Canceling the transaction' )
return this . replace ( {
from : this . address ,
to : this . address ,
value : 0 ,
2020-10-16 20:44:09 +02:00
gasLimit : 21000 ,
2020-10-01 06:56:47 +02:00
} )
}
/ * *
* Executes the transaction . Acquires global mutex for transaction duration
*
* @ returns { Promise < TransactionReceipt > }
* @ private
* /
async _execute ( ) {
2021-02-17 07:08:12 +01:00
const mutexRelease = await this . manager . _mutex . acquire ( )
2020-10-01 06:56:47 +02:00
try {
await this . _prepare ( )
await this . _send ( )
2021-02-17 06:39:50 +01:00
const receipt = await this . _waitForConfirmations ( )
2020-10-01 06:56:47 +02:00
// we could have bumped nonce during execution, so get the latest one + 1
this . manager . _nonce = this . tx . nonce + 1
return receipt
} finally {
2021-02-17 07:08:12 +01:00
mutexRelease ( )
2020-10-01 06:56:47 +02:00
}
}
/ * *
* Prepare first transaction before submitting it . Inits ` gas ` , ` gasPrice ` , ` nonce `
*
* @ returns { Promise < void > }
* @ private
* /
async _prepare ( ) {
2020-12-24 06:39:07 +01:00
if ( ! this . config . BLOCK _GAS _LIMIT ) {
const lastBlock = await this . _provider . getBlock ( 'latest' )
this . config . BLOCK _GAS _LIMIT = Math . floor ( lastBlock . gasLimit . toNumber ( ) * 0.95 )
}
2020-10-16 20:44:09 +02:00
if ( ! this . tx . gasLimit || this . config . ESTIMATE _GAS ) {
const gas = await this . _wallet . estimateGas ( this . tx )
if ( ! this . tx . gasLimit ) {
2020-12-24 06:39:07 +01:00
const gasLimit = Math . floor ( gas * this . config . GAS _LIMIT _MULTIPLIER )
this . tx . gasLimit = Math . min ( gasLimit , this . config . BLOCK _GAS _LIMIT )
2020-10-15 01:29:59 +02:00
}
2020-10-01 06:56:47 +02:00
}
2021-09-02 08:25:23 +02:00
2020-10-01 06:56:47 +02:00
if ( ! this . manager . _nonce ) {
2020-10-16 20:44:09 +02:00
this . manager . _nonce = await this . _getLastNonce ( )
2020-10-01 06:56:47 +02:00
}
this . tx . nonce = this . manager . _nonce
2021-09-02 08:25:23 +02:00
2020-10-16 20:44:09 +02:00
if ( ! this . manager . _chainId ) {
const net = await this . _provider . getNetwork ( )
this . manager . _chainId = net . chainId
}
this . tx . chainId = this . manager . _chainId
2021-09-02 08:25:23 +02:00
if ( this . tx . gasPrice || ( this . tx . maxFeePerGas && this . tx . maxPriorityFeePerGas ) ) {
return
}
const gasParams = await this . _getGasParams ( )
this . tx = Object . assign ( this . tx , gasParams )
2020-10-01 06:56:47 +02:00
}
/ * *
* Send the current transaction
*
* @ returns { Promise }
* @ private
* /
async _send ( ) {
// todo throw is we attempt to send a tx that attempts to replace already mined tx
2020-10-16 20:44:09 +02:00
const signedTx = await this . _wallet . signTransaction ( this . tx )
2020-10-01 06:56:47 +02:00
this . submitTimestamp = Date . now ( )
2020-10-16 20:44:09 +02:00
const txHash = ethers . utils . keccak256 ( signedTx )
this . hashes . push ( txHash )
2020-10-01 06:56:47 +02:00
try {
2020-10-16 20:44:09 +02:00
await this . _broadcast ( signedTx )
2020-10-01 06:56:47 +02:00
} catch ( e ) {
return this . _handleSendError ( e )
}
2020-10-16 20:44:09 +02:00
this . _emitter . emit ( 'transactionHash' , txHash )
console . log ( ` Broadcasted transaction ${ txHash } ` )
2020-10-01 06:56:47 +02:00
}
/ * *
* A loop that waits until the current transaction is mined and gets enough confirmations
*
* @ returns { Promise < TransactionReceipt > } The transaction receipt
* @ private
* /
async _waitForConfirmations ( ) {
// eslint-disable-next-line no-constant-condition
while ( true ) {
// We are already waiting on certain tx hash
if ( this . currentTxHash ) {
2020-10-16 20:44:09 +02:00
const receipt = await this . _provider . getTransactionReceipt ( this . currentTxHash )
2020-10-01 06:56:47 +02:00
if ( ! receipt ) {
// We were waiting for some tx but it disappeared
// Erase the hash and start over
this . currentTxHash = null
continue
}
2020-10-16 20:44:09 +02:00
const currentBlock = await this . _provider . getBlockNumber ( )
2020-10-01 06:56:47 +02:00
const confirmations = Math . max ( 0 , currentBlock - receipt . blockNumber )
// todo don't emit repeating confirmation count
this . _emitter . emit ( 'confirmations' , confirmations )
if ( confirmations >= this . config . CONFIRMATIONS ) {
// Tx is mined and has enough confirmations
2020-11-19 18:33:58 +01:00
if ( this . config . THROW _ON _REVERT && Number ( receipt . status ) === 0 ) {
throw new Error ( 'EVM execution failed, so the transaction was reverted.' )
}
2020-10-01 06:56:47 +02:00
return receipt
}
// Tx is mined but doesn't have enough confirmations yet, keep waiting
await sleep ( this . config . POLL _INTERVAL )
continue
}
// Tx is still pending
2020-10-02 11:14:40 +02:00
if ( ( await this . _getLastNonce ( ) ) <= this . tx . nonce ) {
2020-10-01 06:56:47 +02:00
// todo optionally run estimateGas on each iteration and cancel the transaction if it fails
// We were waiting too long, increase gas price and resubmit
if ( Date . now ( ) - this . submitTimestamp >= this . config . GAS _BUMP _INTERVAL ) {
if ( this . _increaseGasPrice ( ) ) {
2021-09-02 08:25:23 +02:00
console . log ( 'Resubmitting with higher gas params' )
2020-10-01 06:56:47 +02:00
await this . _send ( )
continue
}
}
// Tx is still pending, keep waiting
await sleep ( this . config . POLL _INTERVAL )
continue
}
2020-10-02 11:10:21 +02:00
// There is a mined tx with our nonce, let's see if it has a known hash
2020-10-01 06:56:47 +02:00
let receipt = await this . _getReceipts ( )
// There is a mined tx with current nonce, but it's not one of ours
// Probably other tx submitted by other process/client
if ( ! receipt ) {
2020-10-02 11:14:40 +02:00
console . log ( "Can't find our transaction receipt, retrying a few times" )
2020-10-01 06:56:47 +02:00
// Give node a few more attempts to respond with our receipt
let retries = 5
while ( ! receipt && retries -- ) {
await sleep ( 1000 )
receipt = await this . _getReceipts ( )
}
// Receipt was not found after a few retries
// Resubmit our tx
if ( ! receipt ) {
2020-10-02 11:14:40 +02:00
console . log (
'There is a mined tx with our nonce but unknown tx hash, resubmitting with tx with increased nonce' ,
)
2020-10-01 06:56:47 +02:00
this . tx . nonce ++
// todo drop gas price to original value?
await this . _send ( )
continue
}
}
this . _emitter . emit ( 'mined' , receipt )
this . currentTxHash = receipt . transactionHash
}
}
async _getReceipts ( ) {
for ( const hash of this . hashes . reverse ( ) ) {
2020-10-16 20:44:09 +02:00
const receipt = await this . _provider . getTransactionReceipt ( hash )
2020-10-01 06:56:47 +02:00
if ( receipt ) {
return receipt
}
}
return null
}
/ * *
* Broadcasts tx to multiple nodes , waits for tx hash only on main node
* /
_broadcast ( rawTx ) {
2020-10-16 20:44:09 +02:00
const main = this . _provider . sendTransaction ( rawTx )
2020-10-01 06:56:47 +02:00
for ( const node of this . _broadcastNodes ) {
try {
2020-10-16 20:44:09 +02:00
new ethers . providers . JsonRpcProvider ( node ) . sendTransaction ( rawTx )
2020-10-01 06:56:47 +02:00
} catch ( e ) {
console . log ( ` Failed to send transaction to node ${ node } : ${ e } ` )
}
}
2020-10-16 20:44:09 +02:00
return main
2020-10-01 06:56:47 +02:00
}
_handleSendError ( e ) {
2020-11-26 08:34:40 +01:00
if ( e . error . error ) {
// Sometimes ethers wraps known errors, unwrap it in this case
e = e . error
}
if ( e . error && e . code === 'SERVER_ERROR' ) {
const message = e . error . message
2020-10-16 20:44:09 +02:00
// 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 ( )
}
2020-10-01 06:56:47 +02:00
}
2020-10-16 20:44:09 +02:00
// there is already a pending tx with higher gas price, trying to bump and resubmit
if ( this . _hasError ( message , gasPriceErrors ) ) {
2020-10-17 04:22:55 +02:00
console . log (
` Gas price ${ formatUnits ( this . tx . gasPrice , 'gwei' ) } gwei is too low, increasing and retrying ` ,
)
2021-02-17 06:40:14 +01:00
if ( this . _increaseGasPrice ( ) ) {
return this . _send ( )
} else {
throw new Error ( 'Already at max gas price, but still not enough to submit the transaction' )
}
2020-10-16 20:44:09 +02:00
}
2020-10-01 06:56:47 +02:00
2020-10-16 20:44:09 +02:00
if ( this . _hasError ( message , sameTxErrors ) ) {
console . log ( 'Same transaction is already in mempool, skipping submit' )
return // do nothing
}
2020-10-01 06:56:47 +02:00
}
2020-10-16 20:44:09 +02:00
throw new Error ( ` Send error: ${ e } ` )
2020-10-01 06:56:47 +02:00
}
/ * *
* Returns whether error message is contained in errors array
*
* @ param message The message to look up
* @ param { Array < string | RegExp > } errors Array with errors . Errors can be either string or regexp .
* @ returns { boolean } Returns true if error message is present in the ` errors ` array
* @ private
* /
_hasError ( message , errors ) {
2020-10-02 11:51:36 +02:00
return errors . find ( e => ( typeof e === 'string' ? e === message : message . match ( e ) ) ) !== undefined
2020-10-01 06:56:47 +02:00
}
_increaseGasPrice ( ) {
2021-02-17 05:01:30 +01:00
const maxGasPrice = parseUnits ( this . config . MAX _GAS _PRICE . toString ( ) , 'gwei' )
2020-10-17 04:22:55 +02:00
const minGweiBump = parseUnits ( this . config . MIN _GWEI _BUMP . toString ( ) , 'gwei' )
2021-09-02 08:25:23 +02:00
if ( this . tx . gasPrice ) {
const oldGasPrice = BigNumber . from ( this . tx . gasPrice )
if ( oldGasPrice . gte ( maxGasPrice ) ) {
console . log ( 'Already at max gas price, not bumping' )
return false
}
const newGasPrice = max (
oldGasPrice . mul ( 100 + this . config . GAS _BUMP _PERCENTAGE ) . div ( 100 ) ,
oldGasPrice . add ( minGweiBump ) ,
)
this . tx . gasPrice = min ( newGasPrice , maxGasPrice ) . toHexString ( )
console . log ( ` Increasing gas price to ${ formatUnits ( this . tx . gasPrice , 'gwei' ) } gwei ` )
} else {
const oldMaxFeePerGas = BigNumber . from ( this . tx . maxFeePerGas )
const oldMaxPriorityFeePerGas = BigNumber . from ( this . tx . maxFeePerGas )
if ( oldMaxFeePerGas . gte ( maxGasPrice ) ) {
console . log ( 'Already at max fee per gas, not bumping' )
return false
}
const newMaxFeePerGas = oldMaxFeePerGas . add ( minGweiBump )
this . tx . maxFeePerGas = min ( newMaxFeePerGas , maxGasPrice ) . toHexString ( )
this . tx . maxPriorityFeePerGas = oldMaxPriorityFeePerGas . add ( minGweiBump ) . toHexString ( )
console . log ( ` Increasing maxFeePerGas to ${ formatUnits ( this . tx . maxFeePerGas , 'gwei' ) } gwei ` )
2021-02-17 05:01:30 +01:00
}
2021-09-02 08:25:23 +02:00
2020-10-01 06:56:47 +02:00
return true
}
/ * *
* Fetches gas price from the oracle
*
* @ param { 'instant' | 'fast' | 'normal' | 'slow' } type
* @ returns { Promise < string > } A hex string representing gas price in wei
* @ private
* /
async _getGasPrice ( type ) {
const gasPrices = await this . _gasPriceOracle . gasPrices ( )
const result = gasPrices [ type ] . toString ( )
console . log ( ` ${ type } gas price is now ${ result } gwei ` )
2020-10-20 08:28:59 +02:00
return parseUnits ( result , 'gwei' ) . toHexString ( )
2020-10-01 06:56:47 +02:00
}
/ * *
* Gets current nonce for the current account , ignoring any pending transactions
*
* @ returns { Promise < number > }
* @ private
* /
_getLastNonce ( ) {
2020-10-16 20:44:09 +02:00
return this . _wallet . getTransactionCount ( 'latest' )
2020-10-01 06:56:47 +02:00
}
2021-09-02 08:25:23 +02:00
/ * *
* Fetches baseFee from chain and calculate fee params
*
* @ returns { Promise < object > }
* @ private
* /
async _estimateFees ( ) {
const block = await this . _provider . getBlock ( 'latest' )
let maxFeePerGas = null ,
maxPriorityFeePerGas = null
if ( block && block . baseFeePerGas ) {
maxPriorityFeePerGas = BigNumber . from ( '3000000000' )
maxFeePerGas = block . baseFeePerGas . mul ( 125 ) . div ( 100 ) . add ( maxPriorityFeePerGas )
}
return { maxFeePerGas , maxPriorityFeePerGas }
}
/ * *
* Choose network gas params
*
* @ returns { Promise < object > }
* @ private
* /
async _getGasParams ( ) {
const { maxFeePerGas , maxPriorityFeePerGas } = await this . _estimateFees ( )
const maxGasPrice = parseUnits ( this . config . MAX _GAS _PRICE . toString ( ) , 'gwei' )
// Check network support for EIP-1559
if ( maxFeePerGas && maxPriorityFeePerGas ) {
return {
maxFeePerGas : min ( maxFeePerGas , maxGasPrice ) . toHexString ( ) ,
maxPriorityFeePerGas : maxPriorityFeePerGas . toHexString ( ) ,
type : 2 ,
}
} else {
const fastGasPrice = BigNumber . from ( await this . _getGasPrice ( 'fast' ) )
return {
gasPrice : min ( fastGasPrice , maxGasPrice ) . toHexString ( ) ,
type : 0 ,
}
}
}
2020-10-01 06:56:47 +02:00
}
module . exports = Transaction