2020-10-16 20:44:09 +02:00
const ethers = require ( 'ethers' )
2022-05-23 10:47:40 +02:00
const { parseUnits , formatUnits } = ethers . utils
2020-10-17 04:22:55 +02:00
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.' ,
2022-04-26 15:49:03 +02:00
/nonce too low/i ,
2020-11-25 21:33:34 +01:00
'nonce has already been used' ,
2022-04-26 15:49:03 +02:00
/OldNonce/ ,
'invalid transaction nonce' ,
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.' ,
2022-04-26 15:49:03 +02:00
/transaction underpriced/ ,
2022-07-14 07:39:17 +02:00
/fee too low/i ,
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./ ,
2022-04-26 15:49:03 +02:00
/FeeTooLow/ ,
/max fee per gas less than block base fee/ ,
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' ,
2022-04-26 15:49:03 +02:00
'AlreadyKnown' ,
2022-07-14 07:39:17 +02:00
'Known transaction' ,
2020-10-02 12:04:09 +02:00
]
2020-10-01 06:56:47 +02:00
class Transaction {
constructor ( tx , manager ) {
this . manager = manager
this . tx = { ... tx }
this . _promise = PromiEvent ( )
this . _emitter = this . _promise . eventEmitter
this . executed = false
2022-07-14 07:39:17 +02:00
this . replaced = false
2020-10-01 06:56:47 +02:00
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
2022-07-14 07:39:17 +02:00
// if (this.currentTxHash) {
// throw new Error('Previous transaction was mined')
// }
2020-10-01 06:56:47 +02:00
console . log ( 'Replacing current transaction' )
if ( ! this . executed ) {
// Tx was not executed yet, just replace it
this . tx = { ... tx }
return
}
2022-04-26 15:49:03 +02:00
2020-10-16 20:44:09 +02:00
if ( ! tx . gasLimit ) {
2022-07-14 07:39:17 +02:00
const estimatedGasLimit = await this . _estimateGas ( tx )
const gasLimit = estimatedGasLimit
. mul ( Math . floor ( this . manager . config . GAS _LIMIT _MULTIPLIER * 100 ) )
. div ( 100 )
tx . gasLimit = this . manager . config . BLOCK _GAS _LIMIT
? min ( gasLimit , this . manager . config . BLOCK _GAS _LIMIT )
: gasLimit
2020-10-01 06:56:47 +02:00
}
2022-04-26 15:49:03 +02:00
tx . chainId = this . tx . chainId
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 ) {
2022-07-14 07:39:17 +02:00
tx . gasPrice = max ( this . tx . gasPrice , tx . gasPrice || 0 )
} else if ( this . tx . maxFeePerGas ) {
tx . maxFeePerGas = max ( this . tx . maxFeePerGas , tx . maxFeePerGas || 0 )
tx . maxPriorityFeePerGas = max ( this . tx . maxPriorityFeePerGas , tx . maxPriorityFeePerGas || 0 )
2021-09-02 08:25:23 +02:00
}
2020-10-01 06:56:47 +02:00
this . tx = { ... tx }
2022-07-14 07:39:17 +02:00
await this . _prepare ( )
if ( tx . gasPrice || tx . maxFeePerGas ) {
this . _increaseGasPrice ( )
}
this . replaced = true
2020-10-01 06:56:47 +02:00
await this . _send ( )
}
/ * *
* Cancels a pending tx .
* /
cancel ( ) {
console . log ( 'Canceling the transaction' )
return this . replace ( {
2022-07-14 07:39:17 +02:00
from : this . manager . address ,
to : this . manager . address ,
2020-10-01 06:56:47 +02:00
value : 0 ,
} )
}
/ * *
* 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 ( ) {
2022-07-14 07:39:17 +02:00
if ( ! this . manager . config . BLOCK _GAS _LIMIT ) {
const lastBlock = await this . manager . _provider . getBlock ( 'latest' )
this . manager . config . BLOCK _GAS _LIMIT = Math . floor ( lastBlock . gasLimit . toNumber ( ) * 0.95 )
2020-12-24 06:39:07 +01:00
}
2022-04-26 15:49:03 +02:00
if ( ! this . manager . _chainId ) {
2022-07-14 07:39:17 +02:00
const net = await this . manager . _provider . getNetwork ( )
2022-04-26 15:49:03 +02:00
this . manager . _chainId = net . chainId
}
2022-07-14 07:39:17 +02:00
if ( ! this . tx . chainId ) {
this . tx . chainId = this . manager . _chainId
}
if ( ! this . tx . gasLimit || this . manager . config . ESTIMATE _GAS ) {
2022-04-26 15:49:03 +02:00
const gas = await this . _estimateGas ( this . tx )
2020-10-16 20:44:09 +02:00
if ( ! this . tx . gasLimit ) {
2022-07-14 07:39:17 +02:00
const gasLimit = Math . floor ( gas * this . manager . config . GAS _LIMIT _MULTIPLIER )
this . tx . gasLimit = Math . min ( gasLimit , this . manager . 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
}
2021-09-02 08:25:23 +02:00
2022-07-14 07:39:17 +02:00
if ( ! this . tx . nonce ) {
this . tx . nonce = this . manager . _nonce
2021-09-02 08:25:23 +02:00
}
2022-07-14 07:39:17 +02:00
if ( ! this . tx . gasPrice && ! this . tx . maxFeePerGas ) {
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
2022-07-14 07:39:17 +02:00
const signedTx = await this . manager . _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 ) {
2022-04-26 15:49:03 +02:00
return this . _handleRpcError ( e , '_send' )
2020-10-01 06:56:47 +02:00
}
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 ) {
2022-07-14 07:39:17 +02:00
const receipt = await this . manager . _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
}
2022-07-14 07:39:17 +02:00
const currentBlock = await this . manager . _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 )
2022-07-14 07:39:17 +02:00
if ( confirmations >= this . manager . config . CONFIRMATIONS ) {
2020-10-01 06:56:47 +02:00
// Tx is mined and has enough confirmations
2022-07-14 07:39:17 +02:00
if ( this . manager . config . THROW _ON _REVERT && Number ( receipt . status ) === 0 ) {
2020-11-19 18:33:58 +01:00
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
2022-07-14 07:39:17 +02:00
await sleep ( this . manager . config . POLL _INTERVAL )
2020-10-01 06:56:47 +02:00
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
2022-07-14 07:39:17 +02:00
if ( Date . now ( ) - this . submitTimestamp >= this . manager . config . GAS _BUMP _INTERVAL ) {
2020-10-01 06:56:47 +02:00
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
2022-07-14 07:39:17 +02:00
await sleep ( this . manager . config . POLL _INTERVAL )
2020-10-01 06:56:47 +02:00
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 ( ) ) {
2022-07-14 07:39:17 +02:00
const receipt = await this . manager . _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
* /
2022-07-14 07:39:17 +02:00
async _broadcast ( rawTx ) {
const main = await this . manager . _provider . sendTransaction ( rawTx )
for ( const node of this . manager . _broadcastNodes ) {
2020-10-01 06:56:47 +02:00
try {
2022-07-14 07:39:17 +02:00
await 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
}
2022-04-26 15:49:03 +02:00
_handleRpcError ( e , method ) {
2022-06-08 19:27:56 +02:00
if ( e . error ? . error ) {
2020-11-26 08:34:40 +01:00
// Sometimes ethers wraps known errors, unwrap it in this case
e = e . error
}
2022-07-14 07:39:17 +02:00
// web3 provider not wrapping message
const message = e . error ? . message || e . message
2020-10-16 20:44:09 +02:00
2022-07-14 07:39:17 +02:00
// nonce is too low, trying to increase and resubmit
if ( this . _hasError ( message , nonceErrors ) ) {
if ( this . replaced ) {
console . log ( 'Transaction with the same nonce was mined' )
return // do nothing
2020-10-01 06:56:47 +02:00
}
2022-07-14 07:39:17 +02:00
console . log ( ` Nonce ${ this . tx . nonce } is too low, increasing and retrying ` )
if ( this . retries <= this . manager . config . MAX _RETRIES ) {
this . tx . nonce ++
this . retries ++
return this [ method ] ( )
2020-10-16 20:44:09 +02:00
}
2022-07-14 07:39:17 +02:00
}
2020-10-01 06:56:47 +02:00
2022-07-14 07:39:17 +02:00
// there is already a pending tx with higher gas price, trying to bump and resubmit
if ( this . _hasError ( message , gasPriceErrors ) ) {
console . log (
` Gas price ${ formatUnits (
this . tx . gasPrice || this . tx . maxFeePerGas ,
'gwei' ,
) } gwei is too low , increasing and retrying ` ,
)
if ( this . _increaseGasPrice ( ) ) {
return this [ method ] ( )
} 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
}
2022-07-14 07:39:17 +02:00
if ( this . _hasError ( message , sameTxErrors ) ) {
console . log ( 'Same transaction is already in mempool, skipping submit' )
return // do nothing
}
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 ( ) {
2022-07-14 07:39:17 +02:00
const maxGasPrice = parseUnits ( this . manager . config . MAX _GAS _PRICE . toString ( ) , 'gwei' )
const minGweiBump = parseUnits ( this . manager . 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 (
2022-07-14 07:39:17 +02:00
oldGasPrice . mul ( 100 + this . manager . config . GAS _BUMP _PERCENTAGE ) . div ( 100 ) ,
2021-09-02 08:25:23 +02:00
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 )
2021-09-03 12:44:17 +02:00
const oldMaxPriorityFeePerGas = BigNumber . from ( this . tx . maxPriorityFeePerGas )
2021-09-02 08:25:23 +02:00
if ( oldMaxFeePerGas . gte ( maxGasPrice ) ) {
console . log ( 'Already at max fee per gas, not bumping' )
return false
}
2021-09-03 12:44:17 +02:00
const newMaxFeePerGas = max (
2022-07-14 07:39:17 +02:00
oldMaxFeePerGas . mul ( 100 + this . manager . config . GAS _BUMP _PERCENTAGE ) . div ( 100 ) ,
2021-09-03 12:44:17 +02:00
oldMaxFeePerGas . add ( minGweiBump ) ,
)
const newMaxPriorityFeePerGas = max (
2022-07-14 07:39:17 +02:00
oldMaxPriorityFeePerGas . mul ( 100 + this . manager . config . GAS _BUMP _PERCENTAGE ) . div ( 100 ) ,
2021-09-03 12:44:17 +02:00
oldMaxPriorityFeePerGas . add ( minGweiBump ) ,
)
2022-04-26 15:49:03 +02:00
const maxFeePerGas = min ( newMaxFeePerGas , maxGasPrice )
this . tx . maxFeePerGas = maxFeePerGas . toHexString ( )
this . tx . maxPriorityFeePerGas = min ( newMaxPriorityFeePerGas , maxFeePerGas ) . toHexString ( )
2021-09-02 08:25:23 +02:00
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
}
/ * *
* Gets current nonce for the current account , ignoring any pending transactions
*
* @ returns { Promise < number > }
* @ private
* /
_getLastNonce ( ) {
2022-07-14 07:39:17 +02:00
return this . manager . _wallet . getTransactionCount ( 'latest' )
2020-10-01 06:56:47 +02:00
}
2021-09-02 08:25:23 +02:00
/ * *
2021-09-03 15:09:26 +02:00
* Choose network gas params
2021-09-02 08:25:23 +02:00
*
* @ returns { Promise < object > }
* @ private
* /
2021-09-03 15:09:26 +02:00
async _getGasParams ( ) {
2022-07-14 07:39:17 +02:00
const maxGasPrice = parseUnits ( this . manager . config . MAX _GAS _PRICE . toString ( ) , 'gwei' )
const gasParams = await this . manager . _gasPriceOracle . getTxGasParams ( {
isLegacy : ! this . manager . config . ENABLE _EIP1559 ,
} )
if ( gasParams . gasPrice ) {
gasParams . gasPrice = min ( gasParams . gasPrice , maxGasPrice )
2021-09-02 08:25:23 +02:00
} else {
2022-07-14 07:39:17 +02:00
gasParams . maxFeePerGas = min ( gasParams ? . maxFeePerGas , maxGasPrice )
gasParams . maxPriorityFeePerGas = min ( gasParams ? . maxPriorityFeePerGas , maxGasPrice )
2021-09-02 08:25:23 +02:00
}
2022-07-14 07:39:17 +02:00
gasParams . type = gasParams ? . maxFeePerGas ? 2 : 0
return gasParams
2021-09-02 08:25:23 +02:00
}
2022-04-26 15:49:03 +02:00
async _estimateGas ( tx ) {
try {
2022-07-14 07:39:17 +02:00
return await this . manager . _wallet . estimateGas ( tx )
2022-04-26 15:49:03 +02:00
} catch ( e ) {
return this . _handleRpcError ( e , '_estimateGas' )
}
}
2020-10-01 06:56:47 +02:00
}
module . exports = Transaction