Compare commits

..

30 Commits

Author SHA1 Message Date
6f09ac86b1 Use self-hosted dependecies & set package in Tornado scope 2023-09-13 22:56:32 -07:00
Serg
57aa950a2f
Typing (#4)
* 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>
2022-07-14 15:39:17 +10:00
Danil Kovtonyuk
98c41bfdc8
fix: lint 2022-06-28 17:55:59 +10:00
Ayanami
f43f8088e5 Fixed TypeError bug 2022-06-28 17:55:33 +10:00
Ayanami
32a712e1db Customizable ethers provider 2022-06-28 17:45:07 +10:00
Danil Kovtonyuk
142ce883b4
bump version 2022-05-25 16:09:25 +10:00
Danil Kovtonyuk
cdce2334e4
revert estimate priority fee 2022-05-25 16:07:43 +10:00
Danil Kovtonyuk
09c24e12c9 bump gas price oracle 2022-05-23 20:28:36 +10:00
Danil Kovtonyuk
c1c6079745 fix: use eth_maxPriorityFeePerGas 2022-05-23 20:28:36 +10:00
Danil Kovtonyuk
eaaa54b7f9 fix: configure EIP-1559 feature 2022-05-23 20:28:36 +10:00
Danil Kovtonyuk
6adf89472e fix: estimate priority fee
- fix: predefined errors
2022-05-23 20:28:36 +10:00
Danil Kovtonyuk
b6ed5d2bc0
bump gas price oracle 2022-04-13 23:09:49 +10:00
Danil Kovtonyuk
b4ee1e5117
bump gas price oracle 2021-11-16 03:50:18 +10:00
Danil Kovtonyuk
56aa312829
bump gas price oracle 2021-11-16 01:56:29 +10:00
Danil Kovtonyuk
07dcb5d258
bump gas price oracle 2021-11-15 16:43:36 +10:00
Danil Kovtonyuk
4a17833462
bump gas price oracle 2021-10-19 00:39:35 +10:00
poma
c6652baa7c
inline _estimateFees() 2021-09-03 16:09:26 +03:00
Danil Kovtonyuk
a8e504054e fix: review 2021-09-03 22:58:13 +10:00
Danil Kovtonyuk
23e2e01172 feat: add EIP-1559 support 2021-09-03 22:58:13 +10:00
Danil Kovtonyuk
07752e0714
update gas price oracle 2021-08-25 22:35:05 +10:00
Danil Kovtonyuk
37f6faa42d
bump gas price oracle 2021-08-17 17:23:54 +10:00
Danil Kovtonyuk
9599788224 bump gas price oracle 2021-06-15 13:31:41 +03:00
Danil Kovtonyuk
72a665a19a add gasPriceOracleConfig 2021-06-03 16:16:31 +03:00
f6a4e93a23
fix mutex 2021-02-16 22:08:12 -08:00
af7c597af9
Merge branch 'master' of github.com:tornadocash/tx-manager 2021-02-16 21:48:51 -08:00
221dce3d73
add MAX_GAS_PRICE 2021-02-16 21:48:44 -08:00
poma
8d4bab7fc2
handle bump gas price error 2021-02-17 08:40:14 +03:00
poma
af65d78be9
fix await 2021-02-17 08:39:50 +03:00
bc0b369095
fix gas price bump 2021-02-16 20:01:30 -08:00
Alexey Pertsev
e1620e15c1
Block gas limit (#1)
update gas-price-oracle dep, add BLOCK_GAS_LIMIT const
2020-12-24 08:39:07 +03:00
11 changed files with 3307 additions and 665 deletions

View File

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

View File

@ -11,7 +11,7 @@
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
"ecmaVersion": 2020
},
"rules": {
"indent": [
@ -22,7 +22,13 @@
}
],
"linebreak-style": ["error", "unix"],
"quotes": ["error", "single", { "avoidEscape": true }],
"quotes": [
"error",
"single",
{
"avoidEscape": true
}
],
"semi": ["error", "never"],
"object-curly-spacing": ["error", "always"],
"require-await": "error",

1
.npmrc Normal file
View File

@ -0,0 +1 @@
@tornado:registry=https://git.tornado.ws/api/packages/tornado-packages/npm/

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 '@tornado/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",
"version": "0.2.9",
"name": "@tornado/tx-manager",
"version": "0.4.9",
"description": "",
"main": "index.js",
"types": "index.d.ts",
"engines": {
"node": ">=14.0.0"
},
"scripts": {
"eslint": "eslint --ext .js --ignore-path .gitignore .",
"prettier:check": "prettier --check . --config .prettierrc",
@ -13,17 +17,15 @@
"keywords": [],
"author": "Roman Semenov <semenov.roma@gmail.com>",
"license": "ISC",
"repository": {
"type": "git",
"url": "git://github.com/tornadocash/tx-manager.git"
},
"repository": "https://git.tornado.ws/tornado-packages/tx-manager.git",
"files": [
"src/*"
"src/*",
"index.d.ts"
],
"dependencies": {
"@tornado/gas-price-oracle": "^0.5.3",
"async-mutex": "^0.2.4",
"ethers": "^5.0.17",
"gas-price-oracle": "^0.2.2",
"ethers": "^5.4.6",
"web3-core-promievent": "^1.3.0"
},
"devDependencies": {
@ -34,6 +36,7 @@
"eslint-plugin-prettier": "^3.1.4",
"mocha": "^8.1.3",
"prettier": "^2.1.2",
"web3": "^1.7.4",
"why-is-node-running": "^2.2.0"
}
}

View File

@ -6,31 +6,37 @@ const { sleep, min, max } = require('./utils')
const nonceErrors = [
'Transaction nonce is too low. Try incrementing the nonce.',
'nonce too low',
/nonce too low/i,
'nonce has already been used',
/OldNonce/,
'invalid transaction nonce',
]
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.',
'replacement transaction underpriced',
'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./,
/FeeTooLow/,
/max fee per gas less than block base fee/,
]
// prettier-ignore
const sameTxErrors = [
'Transaction with the same hash was already imported.',
'already known',
'AlreadyKnown',
'Known transaction',
]
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.replaced = false
this.retries = 0
this.currentTxHash = null
// store all submitted hashes to catch cases when an old tx is mined
@ -57,22 +63,47 @@ class Transaction {
*/
async replace(tx) {
// 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')
if (!this.executed) {
// Tx was not executed yet, just replace it
this.tx = { ...tx }
return
}
if (!tx.gasLimit) {
tx.gasLimit = await this._wallet.estimateGas(tx)
tx.gasLimit = Math.floor(tx.gasLimit * this.config.GAS_LIMIT_MULTIPLIER)
tx.gasLimit = Math.min(tx.gasLimit, this.config.BLOCK_GAS_LIMIT)
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
}
tx.chainId = this.tx.chainId
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
// start no less than current tx gas params
if (this.tx.gasPrice) {
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)
}
this.tx = { ...tx }
this._increaseGasPrice()
await this._prepare()
if (tx.gasPrice || tx.maxFeePerGas) {
this._increaseGasPrice()
}
this.replaced = true
await this._send()
}
@ -82,10 +113,9 @@ class Transaction {
cancel() {
console.log('Canceling the transaction')
return this.replace({
from: this.address,
to: this.address,
from: this.manager.address,
to: this.manager.address,
value: 0,
gasLimit: 21000,
})
}
@ -96,16 +126,16 @@ class Transaction {
* @private
*/
async _execute() {
await this.manager._mutex.acquire()
const mutexRelease = await this.manager._mutex.acquire()
try {
await this._prepare()
await this._send()
const receipt = this._waitForConfirmations()
const receipt = await this._waitForConfirmations()
// we could have bumped nonce during execution, so get the latest one + 1
this.manager._nonce = this.tx.nonce + 1
return receipt
} finally {
this.manager._mutex.release()
mutexRelease()
}
}
@ -116,30 +146,40 @@ class Transaction {
* @private
*/
async _prepare() {
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)
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)
}
if (!this.tx.gasLimit || this.config.ESTIMATE_GAS) {
const gas = await this._wallet.estimateGas(this.tx)
if (!this.manager._chainId) {
const net = await this.manager._provider.getNetwork()
this.manager._chainId = net.chainId
}
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)
if (!this.tx.gasLimit) {
const gasLimit = Math.floor(gas * this.config.GAS_LIMIT_MULTIPLIER)
this.tx.gasLimit = Math.min(gasLimit, this.config.BLOCK_GAS_LIMIT)
const gasLimit = Math.floor(gas * this.manager.config.GAS_LIMIT_MULTIPLIER)
this.tx.gasLimit = Math.min(gasLimit, this.manager.config.BLOCK_GAS_LIMIT)
}
}
if (!this.tx.gasPrice) {
this.tx.gasPrice = await this._getGasPrice('fast')
}
if (!this.manager._nonce) {
this.manager._nonce = await this._getLastNonce()
}
this.tx.nonce = this.manager._nonce
if (!this.manager._chainId) {
const net = await this._provider.getNetwork()
this.manager._chainId = net.chainId
if (!this.tx.nonce) {
this.tx.nonce = this.manager._nonce
}
if (!this.tx.gasPrice && !this.tx.maxFeePerGas) {
const gasParams = await this._getGasParams()
this.tx = Object.assign(this.tx, gasParams)
}
this.tx.chainId = this.manager._chainId
}
/**
@ -150,7 +190,7 @@ class Transaction {
*/
async _send() {
// 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()
const txHash = ethers.utils.keccak256(signedTx)
this.hashes.push(txHash)
@ -158,7 +198,7 @@ class Transaction {
try {
await this._broadcast(signedTx)
} catch (e) {
return this._handleSendError(e)
return this._handleRpcError(e, '_send')
}
this._emitter.emit('transactionHash', txHash)
@ -176,7 +216,7 @@ class Transaction {
while (true) {
// We are already waiting on certain tx hash
if (this.currentTxHash) {
const receipt = await this._provider.getTransactionReceipt(this.currentTxHash)
const receipt = await this.manager._provider.getTransactionReceipt(this.currentTxHash)
if (!receipt) {
// We were waiting for some tx but it disappeared
@ -185,20 +225,20 @@ class Transaction {
continue
}
const currentBlock = await this._provider.getBlockNumber()
const currentBlock = await this.manager._provider.getBlockNumber()
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) {
if (confirmations >= this.manager.config.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.')
}
return receipt
}
// 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
}
@ -207,15 +247,15 @@ class Transaction {
// 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 (Date.now() - this.submitTimestamp >= this.manager.config.GAS_BUMP_INTERVAL) {
if (this._increaseGasPrice()) {
console.log('Resubmitting with higher gas price')
console.log('Resubmitting with higher gas params')
await this._send()
continue
}
}
// Tx is still pending, keep waiting
await sleep(this.config.POLL_INTERVAL)
await sleep(this.manager.config.POLL_INTERVAL)
continue
}
@ -253,7 +293,7 @@ class Transaction {
async _getReceipts() {
for (const hash of this.hashes.reverse()) {
const receipt = await this._provider.getTransactionReceipt(hash)
const receipt = await this.manager._provider.getTransactionReceipt(hash)
if (receipt) {
return receipt
}
@ -264,11 +304,11 @@ class Transaction {
/**
* Broadcasts tx to multiple nodes, waits for tx hash only on main node
*/
_broadcast(rawTx) {
const main = this._provider.sendTransaction(rawTx)
for (const node of this._broadcastNodes) {
async _broadcast(rawTx) {
const main = await this.manager._provider.sendTransaction(rawTx)
for (const node of this.manager._broadcastNodes) {
try {
new ethers.providers.JsonRpcProvider(node).sendTransaction(rawTx)
await new ethers.providers.JsonRpcProvider(node).sendTransaction(rawTx)
} catch (e) {
console.log(`Failed to send transaction to node ${node}: ${e}`)
}
@ -276,38 +316,47 @@ class Transaction {
return main
}
_handleSendError(e) {
if (e.error.error) {
_handleRpcError(e, method) {
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
// web3 provider not wrapping message
const message = e.error?.message || e.message
// 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()
}
}
// 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, 'gwei')} gwei is too low, increasing and retrying`,
)
this._increaseGasPrice()
return this._send()
}
if (this._hasError(message, sameTxErrors)) {
console.log('Same transaction is already in mempool, skipping submit')
// 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
}
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}`)
@ -326,34 +375,48 @@ class Transaction {
}
_increaseGasPrice() {
const minGweiBump = parseUnits(this.config.MIN_GWEI_BUMP.toString(), 'gwei')
const oldGasPrice = BigNumber.from(this.tx.gasPrice)
const newGasPrice = max(
oldGasPrice.mul(100 + this.config.GAS_BUMP_PERCENTAGE).div(100),
oldGasPrice.add(minGweiBump),
)
const maxGasPrice = parseUnits(this.config.MAX_GAS_PRICE.toString(), 'gwei')
if (oldGasPrice.eq(maxGasPrice)) {
console.log('Already at max gas price, not bumping')
return false
}
this.tx.gasPrice = min(newGasPrice, maxGasPrice).toHexString()
console.log(`Increasing gas price to ${formatUnits(this.tx.gasPrice, 'gwei')} gwei`)
return true
}
const maxGasPrice = parseUnits(this.manager.config.MAX_GAS_PRICE.toString(), 'gwei')
const minGweiBump = parseUnits(this.manager.config.MIN_GWEI_BUMP.toString(), 'gwei')
/**
* 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()
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.manager.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.maxPriorityFeePerGas)
if (oldMaxFeePerGas.gte(maxGasPrice)) {
console.log('Already at max fee per gas, not bumping')
return false
}
const newMaxFeePerGas = max(
oldMaxFeePerGas.mul(100 + this.manager.config.GAS_BUMP_PERCENTAGE).div(100),
oldMaxFeePerGas.add(minGweiBump),
)
const newMaxPriorityFeePerGas = max(
oldMaxPriorityFeePerGas.mul(100 + this.manager.config.GAS_BUMP_PERCENTAGE).div(100),
oldMaxPriorityFeePerGas.add(minGweiBump),
)
const maxFeePerGas = min(newMaxFeePerGas, maxGasPrice)
this.tx.maxFeePerGas = maxFeePerGas.toHexString()
this.tx.maxPriorityFeePerGas = min(newMaxPriorityFeePerGas, maxFeePerGas).toHexString()
console.log(`Increasing maxFeePerGas to ${formatUnits(this.tx.maxFeePerGas, 'gwei')} gwei`)
}
return true
}
/**
@ -363,7 +426,36 @@ class Transaction {
* @private
*/
_getLastNonce() {
return this._wallet.getTransactionCount('latest')
return this.manager._wallet.getTransactionCount('latest')
}
/**
* Choose network gas params
*
* @returns {Promise<object>}
* @private
*/
async _getGasParams() {
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)
} else {
gasParams.maxFeePerGas = min(gasParams?.maxFeePerGas, maxGasPrice)
gasParams.maxPriorityFeePerGas = min(gasParams?.maxPriorityFeePerGas, maxGasPrice)
}
gasParams.type = gasParams?.maxFeePerGas ? 2 : 0
return gasParams
}
async _estimateGas(tx) {
try {
return await this.manager._wallet.estimateGas(tx)
} catch (e) {
return this._handleRpcError(e, '_estimateGas')
}
}
}

View File

@ -1,6 +1,6 @@
const ethers = require('ethers')
const { Mutex } = require('async-mutex')
const { GasPriceOracle } = require('gas-price-oracle')
const { GasPriceOracle } = require('@tornado/gas-price-oracle')
const Transaction = require('./Transaction')
const defaultConfig = {
@ -15,17 +15,20 @@ const defaultConfig = {
ESTIMATE_GAS: true,
THROW_ON_REVERT: true,
BLOCK_GAS_LIMIT: null,
ENABLE_EIP1559: true,
DEFAULT_PRIORITY_FEE: 3,
BASE_FEE_RESERVE_PERCENTAGE: 50,
}
class TxManager {
constructor({ privateKey, rpcUrl, broadcastNodes = [], config = {} }) {
constructor({ privateKey, rpcUrl, broadcastNodes = [], config = {}, gasPriceOracleConfig = {}, provider }) {
this.config = Object.assign({ ...defaultConfig }, config)
this._privateKey = privateKey.startsWith('0x') ? privateKey : '0x' + privateKey
this._provider = new ethers.providers.JsonRpcProvider(rpcUrl)
this._provider = provider || new ethers.providers.JsonRpcProvider(rpcUrl)
this._wallet = new ethers.Wallet(this._privateKey, this._provider)
this.address = this._wallet.address
this._broadcastNodes = broadcastNodes
this._gasPriceOracle = new GasPriceOracle({ defaultRpc: rpcUrl })
this._gasPriceOracle = new GasPriceOracle({ defaultRpc: rpcUrl, ...gasPriceOracleConfig })
this._mutex = new Mutex()
this._nonce = null
}

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
*/
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 = {
sleep,

View File

@ -1,105 +1,190 @@
require('dotenv').config()
require('chai').should()
const { providers } = require('ethers')
const { parseUnits } = require('ethers').utils
const TxManager = require('../src/TxManager')
// const Transaction = require('../src/Transaction')
const Web3 = require('web3')
const { RPC_URL, PRIVATE_KEY } = process.env
describe('TxManager', () => {
const manager = new TxManager({
privateKey: PRIVATE_KEY,
rpcUrl: RPC_URL,
config: {
CONFIRMATIONS: 1,
GAS_BUMP_INTERVAL: 1000 * 15,
},
const tx1 = {
value: 1,
gasPrice: parseUnits('2', 'gwei').toHexString(),
to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0',
}
const tx2 = {
value: 1,
gasPrice: parseUnits('0.5', 'gwei').toHexString(),
to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0',
}
const tx3 = {
value: 2,
to: '0x0039F22efB07A647557C7C5d17854CFD6D489eF3',
}
const tx4 = {
value: 1,
to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0',
}
const tx5 = {
value: 1,
to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0',
maxFeePerGas: parseUnits('7', 'gwei').toHexString(),
maxPriorityFeePerGas: parseUnits('1', 'gwei').toHexString(),
type: 2,
}
const defaultOptions = {
privateKey: PRIVATE_KEY,
rpcUrl: RPC_URL,
config: {
CONFIRMATIONS: 1,
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)
})
const tx1 = {
value: 1,
gasPrice: parseUnits('1', 'gwei').toHexString(),
to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0',
}
const tx2 = {
value: 1,
gasPrice: parseUnits('0.5', 'gwei').toHexString(),
to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0',
}
const tx3 = {
value: 2,
to: '0x0039F22efB07A647557C7C5d17854CFD6D489eF3',
}
const tx4 = {
value: 1,
to: '0xA43Ce8Cc89Eff3AA5593c742fC56A30Ef2427CB0',
}
describe('#transaction', () => {
it('should work', 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 fetch gas price', 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 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 () => {
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 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 () => {
const {
network: { name, chainId },
options,
} = await getOptions()
options.chainId = chainId
console.log('default\n\n', 'network', { name, chainId }, '\n\n')
this.manager = new TxManager(options)
})
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.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)
})

3227
yarn.lock

File diff suppressed because it is too large Load Diff