* type definitions

* remove Object.assign(), use BigNumber

* path custom ethers provider to constructor. test

* providers test

* using gas-price-oracle v0.5.0

* manager params provider type

* provider type fix

* update deps

* maxPriorityFee param, tests fix

* fix: cancel/replace

* fix: gasParams check

* fix: handleRpcError with web3 provider

Co-authored-by: Danil Kovtonyuk <danx.kov@gmail.com>
This commit is contained in:
Serg 2022-07-14 15:39:17 +10:00 committed by GitHub
parent 98c41bfdc8
commit 57aa950a2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 3191 additions and 631 deletions

View File

@ -1,3 +1,6 @@
# Needed for tests only # Needed for tests only
RPC_URL=https://kovan.infura.io/v3/... RPC_URL=https://kovan.infura.io/v3/...
PRIVATE_KEY=... PRIVATE_KEY=...
ETHERSCAN_API_KEY=...
ALCHEMY_API_KEY=...
INFURA_API_KEY=...

View File

@ -22,7 +22,13 @@
} }
], ],
"linebreak-style": ["error", "unix"], "linebreak-style": ["error", "unix"],
"quotes": ["error", "single", { "avoidEscape": true }], "quotes": [
"error",
"single",
{
"avoidEscape": true
}
],
"semi": ["error", "never"], "semi": ["error", "never"],
"object-curly-spacing": ["error", "always"], "object-curly-spacing": ["error", "always"],
"require-await": "error", "require-await": "error",

124
index.d.ts vendored Normal file
View File

@ -0,0 +1,124 @@
import { BigNumberish, providers, Wallet } from 'ethers'
import { EventEmitter } from 'eventemitter3'
import { TransactionReceipt } from '@ethersproject/abstract-provider'
import PromiEvent from 'web3-core-promievent'
import { GasOracleOptions, GasPriceOracle } from 'gas-price-oracle'
import { Mutex } from 'async-mutex'
import { Provider } from '@ethersproject/providers'
export interface TransactionData {
to: string
from?: string
nonce?: number
gasLimit?: BigNumberish
gasPrice?: BigNumberish
data?: string
value: BigNumberish
chainId?: number
type?: number
maxFeePerGas?: BigNumberish
maxPriorityFeePerGas?: BigNumberish
}
export interface TxManagerConfig {
MAX_RETRIES?: number
GAS_BUMP_PERCENTAGE?: number
MIN_GWEI_BUMP?: number
GAS_BUMP_INTERVAL?: number
MAX_GAS_PRICE?: number
GAS_LIMIT_MULTIPLIER?: number
POLL_INTERVAL?: number
CONFIRMATIONS?: number
ESTIMATE_GAS?: boolean
THROW_ON_REVERT?: boolean
BLOCK_GAS_LIMIT?: number
BASE_FEE_RESERVE_PERCENTAGE?: number
ENABLE_EIP1559?: boolean
DEFAULT_PRIORITY_FEE?: number
}
export interface TxManagerParams {
privateKey: string
rpcUrl: string
broadcastNodes?: string[]
config?: TxManagerConfig
gasPriceOracleConfig?: GasOracleOptions
provider?: Provider
}
export class TxManager {
private _privateKey: string
config: TxManagerConfig
address: string
_provider: providers.JsonRpcProvider
_wallet: Wallet
_broadcastNodes: string[]
_gasPriceOracle: GasPriceOracle
_mutex: Mutex
_nonce: number
constructor(params?: TxManagerParams)
createTx(tx: TransactionData): Transaction
}
export type GasParams = {
maxFeePerGas?: string
maxPriorityFeePerGas?: string
gasPrice?: string
type: number
}
export type TxManagerEvents = keyof MessageEvents
export type MessageEvents = {
error: (error: Error) => void
transactionHash: (transactionHash: string) => void
mined: (receipt: TransactionReceipt) => void
confirmations: (confirmations: number) => void
}
type TEventEmitter = typeof EventEmitter
declare interface TxManagerEventEmitter extends TEventEmitter {
on<U extends TxManagerEvents>(event: U, listener: MessageEvents[U]): this
on(event: 'confirmations', listener: MessageEvents['confirmations']): Promise<TransactionReceipt>
emit<U extends TxManagerEvents>(event: U, ...args: Parameters<MessageEvents[U]>): boolean
}
export class Transaction {
manager: TxManager
tx: TransactionData
private _promise: typeof PromiEvent
private _emitter: TxManagerEventEmitter
executed: boolean
retries: number
currentTxHash: string
hashes: string[]
constructor(tx: TransactionData, manager: TxManager)
send(): TxManagerEventEmitter
replace(tx: TransactionData): Promise<void>
cancel(): this
private _prepare(): Promise<void>
private _send(): Promise<void>
private _execute(): Promise<TransactionReceipt>
private _waitForConfirmations(): Promise<TransactionReceipt>
private _getReceipts(): Promise<TransactionReceipt>
private _increaseGasPrice(): boolean
private _hasError(message: string, errors: (string | RegExp)[]): boolean
private _getLastNonce(): Promise<number>
private _getGasParams(): Promise<GasParams>
}

View File

@ -1,8 +1,12 @@
{ {
"name": "tx-manager", "name": "tx-manager",
"version": "0.4.8", "version": "0.4.9",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"types": "index.d.ts",
"engines": {
"node": ">=14.0.0"
},
"scripts": { "scripts": {
"eslint": "eslint --ext .js --ignore-path .gitignore .", "eslint": "eslint --ext .js --ignore-path .gitignore .",
"prettier:check": "prettier --check . --config .prettierrc", "prettier:check": "prettier --check . --config .prettierrc",
@ -18,12 +22,13 @@
"url": "git://github.com/tornadocash/tx-manager.git" "url": "git://github.com/tornadocash/tx-manager.git"
}, },
"files": [ "files": [
"src/*" "src/*",
"index.d.ts"
], ],
"dependencies": { "dependencies": {
"async-mutex": "^0.2.4", "async-mutex": "^0.2.4",
"ethers": "^5.4.6", "ethers": "^5.4.6",
"gas-price-oracle": "^0.4.7", "gas-price-oracle": "^0.5.0",
"web3-core-promievent": "^1.3.0" "web3-core-promievent": "^1.3.0"
}, },
"devDependencies": { "devDependencies": {
@ -34,6 +39,7 @@
"eslint-plugin-prettier": "^3.1.4", "eslint-plugin-prettier": "^3.1.4",
"mocha": "^8.1.3", "mocha": "^8.1.3",
"prettier": "^2.1.2", "prettier": "^2.1.2",
"web3": "^1.7.4",
"why-is-node-running": "^2.2.0" "why-is-node-running": "^2.2.0"
} }
} }

View File

@ -14,8 +14,8 @@ const nonceErrors = [
const gasPriceErrors = [ const gasPriceErrors = [
'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.',
/replacement transaction underpriced/i,
/transaction underpriced/, /transaction underpriced/,
/fee too low/i,
/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./,
/FeeTooLow/, /FeeTooLow/,
/max fee per gas less than block base fee/, /max fee per gas less than block base fee/,
@ -26,17 +26,17 @@ const sameTxErrors = [
'Transaction with the same hash was already imported.', 'Transaction with the same hash was already imported.',
'already known', 'already known',
'AlreadyKnown', 'AlreadyKnown',
'Known transaction' 'Known transaction',
] ]
class Transaction { class Transaction {
constructor(tx, manager) { constructor(tx, manager) {
Object.assign(this, manager)
this.manager = manager this.manager = manager
this.tx = { ...tx } this.tx = { ...tx }
this._promise = PromiEvent() this._promise = PromiEvent()
this._emitter = this._promise.eventEmitter this._emitter = this._promise.eventEmitter
this.executed = false this.executed = false
this.replaced = false
this.retries = 0 this.retries = 0
this.currentTxHash = null this.currentTxHash = 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
@ -63,6 +63,10 @@ class Transaction {
*/ */
async replace(tx) { async replace(tx) {
// todo throw error if the current transaction is mined already // todo throw error if the current transaction is mined already
// if (this.currentTxHash) {
// throw new Error('Previous transaction was mined')
// }
console.log('Replacing current transaction') console.log('Replacing current transaction')
if (!this.executed) { if (!this.executed) {
// Tx was not executed yet, just replace it // Tx was not executed yet, just replace it
@ -71,9 +75,14 @@ class Transaction {
} }
if (!tx.gasLimit) { if (!tx.gasLimit) {
tx.gasLimit = await this._estimateGas(tx) const estimatedGasLimit = await this._estimateGas(tx)
tx.gasLimit = Math.floor(tx.gasLimit * this.config.GAS_LIMIT_MULTIPLIER) const gasLimit = estimatedGasLimit
tx.gasLimit = Math.min(tx.gasLimit, this.config.BLOCK_GAS_LIMIT) .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
} }
tx.chainId = this.tx.chainId tx.chainId = this.tx.chainId
@ -81,14 +90,20 @@ class Transaction {
// start no less than current tx gas params // start no less than current tx gas params
if (this.tx.gasPrice) { if (this.tx.gasPrice) {
tx.gasPrice = Math.max(this.tx.gasPrice, tx.gasPrice || 0) tx.gasPrice = max(this.tx.gasPrice, tx.gasPrice || 0)
} else { } else if (this.tx.maxFeePerGas) {
tx.maxFeePerGas = Math.max(this.tx.maxFeePerGas, tx.maxFeePerGas || 0) tx.maxFeePerGas = max(this.tx.maxFeePerGas, tx.maxFeePerGas || 0)
tx.maxPriorityFeePerGas = Math.max(this.tx.maxPriorityFeePerGas, tx.maxPriorityFeePerGas || 0) tx.maxPriorityFeePerGas = max(this.tx.maxPriorityFeePerGas, tx.maxPriorityFeePerGas || 0)
} }
this.tx = { ...tx } this.tx = { ...tx }
this._increaseGasPrice() await this._prepare()
if (tx.gasPrice || tx.maxFeePerGas) {
this._increaseGasPrice()
}
this.replaced = true
await this._send() await this._send()
} }
@ -98,8 +113,8 @@ class Transaction {
cancel() { cancel() {
console.log('Canceling the transaction') console.log('Canceling the transaction')
return this.replace({ return this.replace({
from: this.address, from: this.manager.address,
to: this.address, to: this.manager.address,
value: 0, value: 0,
}) })
} }
@ -131,37 +146,40 @@ class Transaction {
* @private * @private
*/ */
async _prepare() { async _prepare() {
if (!this.config.BLOCK_GAS_LIMIT) { if (!this.manager.config.BLOCK_GAS_LIMIT) {
const lastBlock = await this._provider.getBlock('latest') const lastBlock = await this.manager._provider.getBlock('latest')
this.config.BLOCK_GAS_LIMIT = Math.floor(lastBlock.gasLimit.toNumber() * 0.95) this.manager.config.BLOCK_GAS_LIMIT = Math.floor(lastBlock.gasLimit.toNumber() * 0.95)
} }
if (!this.manager._chainId) { if (!this.manager._chainId) {
const net = await this._provider.getNetwork() const net = await this.manager._provider.getNetwork()
this.manager._chainId = net.chainId this.manager._chainId = net.chainId
} }
this.tx.chainId = this.manager._chainId
if (!this.tx.gasLimit || this.config.ESTIMATE_GAS) { if (!this.tx.chainId) {
this.tx.chainId = this.manager._chainId
}
if (!this.tx.gasLimit || this.manager.config.ESTIMATE_GAS) {
const gas = await this._estimateGas(this.tx) const gas = await this._estimateGas(this.tx)
if (!this.tx.gasLimit) { if (!this.tx.gasLimit) {
const gasLimit = Math.floor(gas * this.config.GAS_LIMIT_MULTIPLIER) const gasLimit = Math.floor(gas * this.manager.config.GAS_LIMIT_MULTIPLIER)
this.tx.gasLimit = Math.min(gasLimit, this.config.BLOCK_GAS_LIMIT) this.tx.gasLimit = Math.min(gasLimit, this.manager.config.BLOCK_GAS_LIMIT)
} }
} }
if (!this.manager._nonce) { if (!this.manager._nonce) {
this.manager._nonce = await this._getLastNonce() this.manager._nonce = await this._getLastNonce()
} }
this.tx.nonce = this.manager._nonce
if (this.tx.gasPrice || (this.tx.maxFeePerGas && this.tx.maxPriorityFeePerGas)) { if (!this.tx.nonce) {
return this.tx.nonce = this.manager._nonce
} }
const gasParams = await this._getGasParams() if (!this.tx.gasPrice && !this.tx.maxFeePerGas) {
const gasParams = await this._getGasParams()
this.tx = Object.assign(this.tx, gasParams) this.tx = Object.assign(this.tx, gasParams)
}
} }
/** /**
@ -172,7 +190,7 @@ 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._wallet.signTransaction(this.tx) const signedTx = await this.manager._wallet.signTransaction(this.tx)
this.submitTimestamp = Date.now() this.submitTimestamp = Date.now()
const txHash = ethers.utils.keccak256(signedTx) const txHash = ethers.utils.keccak256(signedTx)
this.hashes.push(txHash) this.hashes.push(txHash)
@ -198,7 +216,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._provider.getTransactionReceipt(this.currentTxHash) const receipt = await this.manager._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
@ -207,20 +225,20 @@ class Transaction {
continue continue
} }
const currentBlock = await this._provider.getBlockNumber() const currentBlock = await this.manager._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)
if (confirmations >= this.config.CONFIRMATIONS) { if (confirmations >= this.manager.config.CONFIRMATIONS) {
// Tx is mined and has enough confirmations // Tx is mined and has enough confirmations
if (this.config.THROW_ON_REVERT && Number(receipt.status) === 0) { if (this.manager.config.THROW_ON_REVERT && Number(receipt.status) === 0) {
throw new Error('EVM execution failed, so the transaction was reverted.') throw new Error('EVM execution failed, so the transaction was reverted.')
} }
return receipt return receipt
} }
// Tx is mined but doesn't have enough confirmations yet, keep waiting // Tx is mined but doesn't have enough confirmations yet, keep waiting
await sleep(this.config.POLL_INTERVAL) await sleep(this.manager.config.POLL_INTERVAL)
continue continue
} }
@ -229,7 +247,7 @@ class Transaction {
// todo optionally run estimateGas on each iteration and cancel the transaction if it fails // todo optionally run estimateGas on each iteration and cancel the transaction if it fails
// We were waiting too long, increase gas price and resubmit // We were waiting too long, increase gas price and resubmit
if (Date.now() - this.submitTimestamp >= this.config.GAS_BUMP_INTERVAL) { if (Date.now() - this.submitTimestamp >= this.manager.config.GAS_BUMP_INTERVAL) {
if (this._increaseGasPrice()) { if (this._increaseGasPrice()) {
console.log('Resubmitting with higher gas params') console.log('Resubmitting with higher gas params')
await this._send() await this._send()
@ -237,7 +255,7 @@ class Transaction {
} }
} }
// Tx is still pending, keep waiting // Tx is still pending, keep waiting
await sleep(this.config.POLL_INTERVAL) await sleep(this.manager.config.POLL_INTERVAL)
continue continue
} }
@ -275,7 +293,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._provider.getTransactionReceipt(hash) const receipt = await this.manager._provider.getTransactionReceipt(hash)
if (receipt) { if (receipt) {
return receipt return receipt
} }
@ -286,11 +304,11 @@ 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) { async _broadcast(rawTx) {
const main = this._provider.sendTransaction(rawTx) const main = await this.manager._provider.sendTransaction(rawTx)
for (const node of this._broadcastNodes) { for (const node of this.manager._broadcastNodes) {
try { try {
new ethers.providers.JsonRpcProvider(node).sendTransaction(rawTx) await 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}`)
} }
@ -304,38 +322,41 @@ class Transaction {
e = e.error e = e.error
} }
if (e.error && e.code === 'SERVER_ERROR') { // web3 provider not wrapping message
const message = e.error.message const message = e.error?.message || e.message
// nonce is too low, trying to increase and resubmit // nonce is too low, trying to increase and resubmit
if (this._hasError(message, nonceErrors)) { if (this._hasError(message, nonceErrors)) {
console.log(`Nonce ${this.tx.nonce} is too low, increasing and retrying`) if (this.replaced) {
if (this.retries <= this.config.MAX_RETRIES) { console.log('Transaction with the same nonce was mined')
this.tx.nonce++
this.retries++
return this[method]()
}
}
// 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')
}
}
if (this._hasError(message, sameTxErrors)) {
console.log('Same transaction is already in mempool, skipping submit')
return // do nothing return // do nothing
} }
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]()
}
}
// 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')
}
}
if (this._hasError(message, sameTxErrors)) {
console.log('Same transaction is already in mempool, skipping submit')
return // do nothing
} }
throw new Error(`Send error: ${e}`) throw new Error(`Send error: ${e}`)
@ -354,8 +375,8 @@ class Transaction {
} }
_increaseGasPrice() { _increaseGasPrice() {
const maxGasPrice = parseUnits(this.config.MAX_GAS_PRICE.toString(), 'gwei') const maxGasPrice = parseUnits(this.manager.config.MAX_GAS_PRICE.toString(), 'gwei')
const minGweiBump = parseUnits(this.config.MIN_GWEI_BUMP.toString(), 'gwei') const minGweiBump = parseUnits(this.manager.config.MIN_GWEI_BUMP.toString(), 'gwei')
if (this.tx.gasPrice) { if (this.tx.gasPrice) {
const oldGasPrice = BigNumber.from(this.tx.gasPrice) const oldGasPrice = BigNumber.from(this.tx.gasPrice)
@ -365,7 +386,7 @@ class Transaction {
} }
const newGasPrice = max( const newGasPrice = max(
oldGasPrice.mul(100 + this.config.GAS_BUMP_PERCENTAGE).div(100), oldGasPrice.mul(100 + this.manager.config.GAS_BUMP_PERCENTAGE).div(100),
oldGasPrice.add(minGweiBump), oldGasPrice.add(minGweiBump),
) )
this.tx.gasPrice = min(newGasPrice, maxGasPrice).toHexString() this.tx.gasPrice = min(newGasPrice, maxGasPrice).toHexString()
@ -379,11 +400,11 @@ class Transaction {
} }
const newMaxFeePerGas = max( const newMaxFeePerGas = max(
oldMaxFeePerGas.mul(100 + this.config.GAS_BUMP_PERCENTAGE).div(100), oldMaxFeePerGas.mul(100 + this.manager.config.GAS_BUMP_PERCENTAGE).div(100),
oldMaxFeePerGas.add(minGweiBump), oldMaxFeePerGas.add(minGweiBump),
) )
const newMaxPriorityFeePerGas = max( const newMaxPriorityFeePerGas = max(
oldMaxPriorityFeePerGas.mul(100 + this.config.GAS_BUMP_PERCENTAGE).div(100), oldMaxPriorityFeePerGas.mul(100 + this.manager.config.GAS_BUMP_PERCENTAGE).div(100),
oldMaxPriorityFeePerGas.add(minGweiBump), oldMaxPriorityFeePerGas.add(minGweiBump),
) )
@ -398,20 +419,6 @@ class Transaction {
return true 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`)
return parseUnits(result, 'gwei').toHexString()
}
/** /**
* Gets current nonce for the current account, ignoring any pending transactions * Gets current nonce for the current account, ignoring any pending transactions
* *
@ -419,7 +426,7 @@ class Transaction {
* @private * @private
*/ */
_getLastNonce() { _getLastNonce() {
return this._wallet.getTransactionCount('latest') return this.manager._wallet.getTransactionCount('latest')
} }
/** /**
@ -429,35 +436,23 @@ class Transaction {
* @private * @private
*/ */
async _getGasParams() { async _getGasParams() {
const maxGasPrice = parseUnits(this.config.MAX_GAS_PRICE.toString(), 'gwei') const maxGasPrice = parseUnits(this.manager.config.MAX_GAS_PRICE.toString(), 'gwei')
const block = await this._provider.getBlock('latest') const gasParams = await this.manager._gasPriceOracle.getTxGasParams({
isLegacy: !this.manager.config.ENABLE_EIP1559,
// Check network support for EIP-1559 })
if (this.config.ENABLE_EIP1559 && block && block.baseFeePerGas) { if (gasParams.gasPrice) {
const maxPriorityFeePerGas = parseUnits(this.config.DEFAULT_PRIORITY_FEE.toString(), 'gwei') gasParams.gasPrice = min(gasParams.gasPrice, maxGasPrice)
const maxFeePerGas = block.baseFeePerGas
.mul(100 + this.config.BASE_FEE_RESERVE_PERCENTAGE)
.div(100)
.add(maxPriorityFeePerGas)
return {
maxFeePerGas: min(maxFeePerGas, maxGasPrice).toHexString(),
maxPriorityFeePerGas: min(maxPriorityFeePerGas, maxGasPrice).toHexString(),
type: 2,
}
} else { } else {
const fastGasPrice = BigNumber.from(await this._getGasPrice('fast')) gasParams.maxFeePerGas = min(gasParams?.maxFeePerGas, maxGasPrice)
return { gasParams.maxPriorityFeePerGas = min(gasParams?.maxPriorityFeePerGas, maxGasPrice)
gasPrice: min(fastGasPrice, maxGasPrice).toHexString(),
type: 0,
}
} }
gasParams.type = gasParams?.maxFeePerGas ? 2 : 0
return gasParams
} }
async _estimateGas(tx) { async _estimateGas(tx) {
try { try {
return await this._wallet.estimateGas(tx) return await this.manager._wallet.estimateGas(tx)
} catch (e) { } catch (e) {
return this._handleRpcError(e, '_estimateGas') return this._handleRpcError(e, '_estimateGas')
} }

7
src/utils.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
import { BigNumberish } from 'ethers'
export function sleep(ms: number): Promise<any>
export function max(a: BigNumberish, b: BigNumberish): BigNumberish
export function min(a: BigNumberish, b: BigNumberish): BigNumberish

View File

@ -1,11 +1,12 @@
const { BigNumber } = require('ethers')
/** /**
* A promise that resolves after `ms` milliseconds * A promise that resolves after `ms` milliseconds
*/ */
const sleep = ms => new Promise(res => setTimeout(res, ms)) const sleep = ms => new Promise(res => setTimeout(res, ms))
const max = (a, b) => (a.gt(b) ? a : b) const max = (a, b) => (BigNumber.from(a).gt(b) ? a : b)
const min = (a, b) => (a.lt(b) ? a : b) const min = (a, b) => (BigNumber.from(a).lt(b) ? a : b)
module.exports = { module.exports = {
sleep, sleep,

View File

@ -3,186 +3,202 @@ require('chai').should()
const { providers } = require('ethers') const { providers } = require('ethers')
const { parseUnits } = require('ethers').utils const { parseUnits } = require('ethers').utils
const TxManager = require('../src/TxManager') const TxManager = require('../src/TxManager')
// const Transaction = require('../src/Transaction') const Web3 = require('web3')
const { RPC_URL, PRIVATE_KEY } = process.env const { RPC_URL, PRIVATE_KEY } = process.env
describe('TxManager', () => { const tx1 = {
let manager value: 1,
gasPrice: parseUnits('2', 'gwei').toHexString(),
to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0',
}
const tx1 = { const tx2 = {
value: 1, value: 1,
gasPrice: parseUnits('2', 'gwei').toHexString(), gasPrice: parseUnits('0.5', 'gwei').toHexString(),
to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0', to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0',
} }
const tx2 = { const tx3 = {
value: 1, value: 2,
gasPrice: parseUnits('0.5', 'gwei').toHexString(), to: '0x0039F22efB07A647557C7C5d17854CFD6D489eF3',
to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0', }
}
const tx3 = { const tx4 = {
value: 2, value: 1,
to: '0x0039F22efB07A647557C7C5d17854CFD6D489eF3', to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0',
} }
const tx4 = { const tx5 = {
value: 1, value: 1,
to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0', to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0',
} maxFeePerGas: parseUnits('7', 'gwei').toHexString(),
maxPriorityFeePerGas: parseUnits('1', 'gwei').toHexString(),
type: 2,
}
const tx5 = { const defaultOptions = {
value: 1, privateKey: PRIVATE_KEY,
to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0', rpcUrl: RPC_URL,
maxFeePerGas: parseUnits('7', 'gwei').toHexString(), config: {
maxPriorityFeePerGas: parseUnits('1', 'gwei').toHexString(), CONFIRMATIONS: 1,
type: 2, GAS_BUMP_INTERVAL: 1000 * 20,
} },
gasPriceOracleConfig: {
chainId: 1,
defaultRpc: RPC_URL,
},
provider: undefined,
}
const getOptions = async () => {
const provider = new providers.JsonRpcProvider(RPC_URL)
const network = await provider.getNetwork()
const options = { ...defaultOptions }
return { network, provider, options }
}
const sendTx = async tx => {
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)
return receipt
}
const transactionTests = () => {
it('should work legacy tx', async () => {
const tx = this.manager.createTx(tx1)
const receipt = await sendTx(tx)
receipt.type.should.equal(0)
})
it('should work eip-1559 tx', async () => {
const tx = this.manager.createTx(tx5)
const receipt = await sendTx(tx)
receipt.type.should.equal(2)
})
it('should fetch gas params', async () => {
const tx = this.manager.createTx(tx4)
await sendTx(tx)
})
it('should bump gas params', async () => {
const tx = this.manager.createTx(tx2)
await sendTx(tx)
})
it('should cancel', async () => {
const currentNonce = await this.manager._wallet.getTransactionCount('latest')
const tx = this.manager.createTx(tx3)
setTimeout(() => tx.cancel(), 1000)
const receipt = await sendTx(tx)
const transaction = await this.manager._provider.getTransaction(receipt.transactionHash)
transaction.value.toNumber().should.equal(0)
transaction.nonce.should.equal(currentNonce)
})
it('should replace', async () => {
const currentNonce = await this.manager._wallet.getTransactionCount('latest')
const tx = this.manager.createTx(tx3)
setTimeout(() => tx.replace(tx4), 1000)
const receipt = await sendTx(tx)
const transaction = await this.manager._provider.getTransaction(receipt.transactionHash)
receipt.to.should.equal(tx4.to)
transaction.nonce.should.equal(currentNonce)
})
it('should increase nonce', async () => {
const currentNonce = await this.manager._wallet.getTransactionCount('latest')
this.manager._nonce = currentNonce - 1
const tx = this.manager.createTx(tx4)
await sendTx(tx)
})
it('should disable eip-1559 transactions', async () => {
this.manager.config.ENABLE_EIP1559 = false
const tx = this.manager.createTx(tx3)
const receipt = await sendTx(tx)
receipt.type.should.equal(0)
this.manager.config.ENABLE_EIP1559 = true
})
it('should send multiple txs', async () => {
const genTx = value => ({
value,
to: '0x0039F22efB07A647557C7C5d17854CFD6D489eF3',
})
await Promise.all(Array.from({ length: 10 }).map(n => this.manager.createTx(genTx(n + 1)).send()))
}).timeout(600000)
}
describe('TxManager.default', () => {
before(async () => { before(async () => {
const provider = new providers.JsonRpcProvider(RPC_URL) const {
network: { name, chainId },
const { name, chainId } = await provider.getNetwork() options,
console.log('\n\n', 'network', { name, chainId }, '\n\n') } = await getOptions()
options.chainId = chainId
manager = new TxManager({ console.log('default\n\n', 'network', { name, chainId }, '\n\n')
privateKey: PRIVATE_KEY, this.manager = new TxManager(options)
rpcUrl: RPC_URL,
config: {
CONFIRMATIONS: 1,
GAS_BUMP_INTERVAL: 1000 * 20,
},
gasPriceOracleConfig: {
chainId: chainId,
defaultRpc: RPC_URL,
},
})
})
describe('#transaction', () => {
it('should work legacy tx', async () => {
const tx = manager.createTx(tx1)
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 work eip-1559 tx', async () => {
const tx = manager.createTx(tx5)
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 fetch gas params', async () => {
const tx = manager.createTx(tx4)
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 bump gas params', 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(tx2)
setTimeout(() => tx.cancel(), 1000)
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 replace', async () => {
const tx = manager.createTx(tx2)
setTimeout(() => tx.replace(tx3), 1000)
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 increase nonce', async () => {
const currentNonce = await manager._wallet.getTransactionCount('latest')
manager._nonce = currentNonce - 1
const tx = manager.createTx(tx4)
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 disable eip-1559 transactions', async () => {
manager.config.ENABLE_EIP1559 = false
const tx = manager.createTx(tx3)
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)
manager.config.ENABLE_EIP1559 = true
})
it('should send multiple txs', async () => {
const genTx = value => ({
value,
to: '0x0039F22efB07A647557C7C5d17854CFD6D489eF3',
})
await Promise.all([
manager.createTx(genTx(1)).send(),
manager.createTx(genTx(2)).send(),
manager.createTx(genTx(3)).send(),
manager.createTx(genTx(4)).send(),
manager.createTx(genTx(5)).send(),
manager.createTx(genTx(6)).send(),
manager.createTx(genTx(7)).send(),
manager.createTx(genTx(8)).send(),
manager.createTx(genTx(9)).send(),
manager.createTx(genTx(10)).send(),
])
}).timeout(600000)
}) })
describe('#transaction', transactionTests)
})
describe('TxManager.EtherscanProvider', () => {
before(async () => {
const {
network: { name, chainId },
options,
} = await getOptions()
options.chainId = chainId
options.provider = new providers.EtherscanProvider(chainId, process.env.ETHERSCAN_API_KEY)
console.log('EtherscanProvider\n\n', 'network', { name, chainId }, '\n\n')
this.manager = new TxManager(options)
})
describe('#transaction', transactionTests)
})
describe('TxManager.AlchemyProvider', () => {
before(async () => {
const {
network: { name, chainId },
options,
} = await getOptions()
options.chainId = chainId
options.provider = new providers.AlchemyProvider(chainId, process.env.ALCHEMY_API_KEY)
console.log('AlchemyProvider\n\n', 'network', { name, chainId }, '\n\n')
this.manager = new TxManager(options)
})
describe('#transaction', transactionTests)
})
describe('TxManager.InfuraProvider', () => {
before(async () => {
const {
network: { name, chainId },
options,
} = await getOptions()
options.chainId = chainId
options.provider = new providers.InfuraProvider(chainId, process.env.INFURA_API_KEY)
console.log('InfuraProvider\n\n', 'network', { name, chainId }, '\n\n')
this.manager = new TxManager(options)
})
describe('#transaction', transactionTests)
})
describe('TxManager.Web3Provider', () => {
before(async () => {
const {
network: { name, chainId },
options,
} = await getOptions()
options.chainId = chainId
options.provider = new providers.Web3Provider(new Web3.providers.HttpProvider(RPC_URL))
console.log('Web3Provider\n\n', 'network', { name, chainId }, '\n\n')
this.manager = new TxManager(options)
})
describe('#transaction', transactionTests)
}) })

3084
yarn.lock

File diff suppressed because it is too large Load Diff