Merge the develop branch to the master branch, preparation to v3.4.0
This merge contains the following set of changes: * [Oracle, Improvement] Refetch old logs ranges to see if there are missed events (#627) * [Oracle, Improvement] Add support for EIP1559 gas price oracle (#631) * [Oracle, Improvement] CollectedSignatures AMB watcher for MEV bundling (#634) * [Oracle, Fix] Fix eip1559 transaction sending problems (#632)
This commit is contained in:
commit
5bc562e810
@ -8,11 +8,11 @@ COMMON_HOME_RPC_URL | The HTTPS URL(s) used to communicate to the RPC nodes in t
|
||||
COMMON_FOREIGN_RPC_URL | The HTTPS URL(s) used to communicate to the RPC nodes in the Foreign network. Several URLs can be specified, delimited by spaces. If the connection to one of these nodes is lost the next URL is used for connection. | URL(s)
|
||||
COMMON_HOME_BRIDGE_ADDRESS | The address of the bridge contract address in the Home network. It is used to listen to events from and send validators' transactions to the Home network. | hexidecimal beginning with "0x"
|
||||
COMMON_FOREIGN_BRIDGE_ADDRESS | The address of the bridge contract address in the Foreign network. It is used to listen to events from and send validators' transactions to the Foreign network. | hexidecimal beginning with "0x"
|
||||
COMMON_HOME_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from the gas price prediction oracle for the Home network. The gas price provided by the oracle is used to send the validator's transactions to the RPC node. Since it is assumed that the Home network has a predefined gas price (e.g. the gas price in the Core of POA.Network is `1 GWei`), the gas price oracle parameter can be omitted for such networks. | URL
|
||||
COMMON_HOME_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from the gas price prediction oracle for the Home network. The gas price provided by the oracle is used to send the validator's transactions to the RPC node. Since it is assumed that the Home network has a predefined gas price (e.g. the gas price in the Core of POA.Network is `1 GWei`), the gas price oracle parameter can be omitted for such networks. Set to `eip1559-gas-estimation` if you want to use EIP1559 RPC-based gas estimation. | URL
|
||||
COMMON_HOME_GAS_PRICE_SPEED_TYPE | Assuming the gas price oracle responds with the following JSON structure: `{"fast": 20.0, "block_time": 12.834, "health": true, "standard": 6.0, "block_number": 6470469, "instant": 71.0, "slow": 1.889}`, this parameter specifies the desirable transaction speed. The speed type can be omitted when `COMMON_HOME_GAS_PRICE_SUPPLIER_URL` is not used. | `instant` / `fast` / `standard` / `slow`
|
||||
COMMON_HOME_GAS_PRICE_FALLBACK | The gas price (in Wei) that is used if both the oracle and the fall back gas price specified in the Home Bridge contract are not available. | integer
|
||||
COMMON_HOME_GAS_PRICE_FACTOR | A value that will multiply the gas price of the oracle to convert it to gwei. If the oracle API returns gas prices in gwei then this can be set to `1`. Also, it could be used to intentionally pay more gas than suggested by the oracle to guarantee the transaction verification. E.g. `1.25` or `1.5`. | integer
|
||||
COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from the gas price prediction oracle for the Foreign network. The provided gas price is used to send the validator's transactions to the RPC node. If the Foreign network is Ethereum Foundation mainnet, the oracle URL can be: https://gasprice.poa.network. Otherwise this parameter can be omitted. Set to `gas-price-oracle` if you want to use npm `gas-price-oracle` package for retrieving gas price from multiple sources. | URL
|
||||
COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from the gas price prediction oracle for the Foreign network. The provided gas price is used to send the validator's transactions to the RPC node. If the Foreign network is Ethereum Foundation mainnet, the oracle URL can be: https://gasprice.poa.network. Otherwise this parameter can be omitted. Set to `gas-price-oracle` if you want to use npm `gas-price-oracle` package for retrieving gas price from multiple sources. Set to `eip1559-gas-estimation` if you want to use EIP1559 RPC-based gas estimation. | URL
|
||||
COMMON_FOREIGN_GAS_PRICE_SPEED_TYPE | Assuming the gas price oracle responds with the following JSON structure: `{"fast": 20.0, "block_time": 12.834, "health": true, "standard": 6.0, "block_number": 6470469, "instant": 71.0, "slow": 1.889}`, this parameter specifies the desirable transaction speed. The speed type can be omitted when `COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL`is not used. | `instant` / `fast` / `standard` / `slow`
|
||||
COMMON_FOREIGN_GAS_PRICE_FALLBACK | The gas price (in Wei) used if both the oracle and fall back gas price specified in the Foreign Bridge contract are not available. | integer
|
||||
COMMON_FOREIGN_GAS_PRICE_FACTOR | A value that will multiply the gas price of the oracle to convert it to gwei. If the oracle API returns gas prices in gwei then this can be set to `1`. Also, it could be used to intentionally pay more gas than suggested by the oracle to guarantee the transaction verification. E.g. `1.25` or `1.5`. | integer
|
||||
@ -53,6 +53,12 @@ ORACLE_SHUTDOWN_CONTRACT_METHOD | Method signature to be used in the side chain
|
||||
ORACLE_FOREIGN_RPC_BLOCK_POLLING_LIMIT | Max length for the block range used in `eth_getLogs` requests for polling contract events for the Foreign chain. Infinite, if not provided. | `integer`
|
||||
ORACLE_HOME_RPC_BLOCK_POLLING_LIMIT | Max length for the block range used in `eth_getLogs` requests for polling contract events for the Home chain. Infinite, if not provided. | `integer`
|
||||
ORACLE_JSONRPC_ERROR_CODES | Override default JSON rpc error codes that can trigger RPC fallback to the next URL from the list (or a retry in case of a single RPC URL). Default is `-32603,-32002,-32005`. Should be a comma-separated list of negative integers. | `string`
|
||||
ORACLE_HOME_EVENTS_REPROCESSING | If set to `true`, home events happened in the past will be refetched and processed once again, to ensure that nothing was missed on the first pass. | `bool`
|
||||
ORACLE_HOME_EVENTS_REPROCESSING_BATCH_SIZE | Batch size for one `eth_getLogs` request when reprocessing old logs in the home chain. Defaults to `1000` | `integer`
|
||||
ORACLE_HOME_EVENTS_REPROCESSING_BLOCK_DELAY | Block confirmations number, after which old logs are being reprocessed in the home chain. Defaults to `500` | `integer`
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING | If set to `true`, foreign events happened in the past will be refetched and processed once again, to ensure that nothing was missed on the first pass. | `bool`
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING_BATCH_SIZE | Batch size for one `eth_getLogs` request when reprocessing old logs in the foreign chain. Defaults to `500` | `integer`
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING_BLOCK_DELAY | Block confirmations number, after which old logs are being reprocessed in the foreign chain. Defaults to `250` | `integer`
|
||||
|
||||
|
||||
## Monitor configuration
|
||||
|
@ -8,6 +8,7 @@
|
||||
"test": "NODE_ENV=test mocha"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mycrypto/gas-estimation": "^1.1.0",
|
||||
"gas-price-oracle": "^0.1.5",
|
||||
"web3-utils": "^1.3.0",
|
||||
"node-fetch": "^2.1.2"
|
||||
|
@ -1,5 +1,6 @@
|
||||
const { toWei, toBN, BN } = require('web3-utils')
|
||||
const { GasPriceOracle } = require('gas-price-oracle')
|
||||
const { estimateFees } = require('@mycrypto/gas-estimation')
|
||||
const fetch = require('node-fetch')
|
||||
const { BRIDGE_MODES } = require('./constants')
|
||||
const { REWARDABLE_VALIDATORS_ABI } = require('./abis')
|
||||
@ -176,12 +177,20 @@ const gasPriceWithinLimits = (gasPrice, limits) => {
|
||||
const normalizeGasPrice = (oracleGasPrice, factor, limits = null) => {
|
||||
let gasPrice = oracleGasPrice * factor
|
||||
gasPrice = gasPriceWithinLimits(gasPrice, limits)
|
||||
return toBN(toWei(gasPrice.toFixed(2).toString(), 'gwei'))
|
||||
return toWei(gasPrice.toFixed(2).toString(), 'gwei')
|
||||
}
|
||||
|
||||
const gasPriceFromSupplier = async (url, options = {}) => {
|
||||
const gasPriceFromSupplier = async (web3, url, options = {}) => {
|
||||
try {
|
||||
let json
|
||||
if (url === 'eip1559-gas-estimation') {
|
||||
const { maxFeePerGas, maxPriorityFeePerGas } = await estimateFees(web3)
|
||||
const res = { maxFeePerGas: maxFeePerGas.toString(10), maxPriorityFeePerGas: maxPriorityFeePerGas.toString(10) }
|
||||
options.logger &&
|
||||
options.logger.debug &&
|
||||
options.logger.debug(res, 'Gas price updated using eip1559-gas-estimation')
|
||||
return res
|
||||
}
|
||||
if (url === 'gas-price-oracle') {
|
||||
json = await gasPriceOracle.fetchGasPricesOffChain()
|
||||
} else if (url) {
|
||||
@ -205,7 +214,7 @@ const gasPriceFromSupplier = async (url, options = {}) => {
|
||||
options.logger.debug &&
|
||||
options.logger.debug({ oracleGasPrice, normalizedGasPrice }, 'Gas price updated using the API')
|
||||
|
||||
return normalizedGasPrice
|
||||
return { gasPrice: normalizedGasPrice }
|
||||
} catch (e) {
|
||||
options.logger && options.logger.error && options.logger.error(`Gas Price API is not available. ${e.message}`)
|
||||
}
|
||||
@ -214,11 +223,11 @@ const gasPriceFromSupplier = async (url, options = {}) => {
|
||||
|
||||
const gasPriceFromContract = async (bridgeContract, options = {}) => {
|
||||
try {
|
||||
const gasPrice = await bridgeContract.methods.gasPrice().call()
|
||||
const gasPrice = (await bridgeContract.methods.gasPrice().call()).toString()
|
||||
options.logger &&
|
||||
options.logger.debug &&
|
||||
options.logger.debug({ gasPrice }, 'Gas price updated using the contracts')
|
||||
return gasPrice
|
||||
return { gasPrice }
|
||||
} catch (e) {
|
||||
options.logger &&
|
||||
options.logger.error &&
|
||||
|
@ -23,3 +23,9 @@ ORACLE_HOME_START_BLOCK=1
|
||||
ORACLE_FOREIGN_START_BLOCK=1
|
||||
ORACLE_HOME_TO_FOREIGN_BLOCK_LIST=/mono/oracle/access-lists/block_list.txt
|
||||
ORACLE_FOREIGN_ARCHIVE_RPC_URL=http://parity2:8545
|
||||
ORACLE_HOME_EVENTS_REPROCESSING=false
|
||||
ORACLE_HOME_EVENTS_REPROCESSING_BATCH_SIZE=10
|
||||
ORACLE_HOME_EVENTS_REPROCESSING_BLOCK_DELAY=10
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING=true
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING_BATCH_SIZE=10
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING_BLOCK_DELAY=10
|
||||
|
@ -22,3 +22,9 @@ ORACLE_ALLOW_HTTP_FOR_RPC=yes
|
||||
ORACLE_HOME_START_BLOCK=1
|
||||
ORACLE_FOREIGN_START_BLOCK=1
|
||||
ORACLE_HOME_TO_FOREIGN_BLOCK_LIST=/mono/oracle/access-lists/block_list.txt
|
||||
ORACLE_HOME_EVENTS_REPROCESSING=true
|
||||
ORACLE_HOME_EVENTS_REPROCESSING_BATCH_SIZE=10
|
||||
ORACLE_HOME_EVENTS_REPROCESSING_BLOCK_DELAY=10
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING=true
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING_BATCH_SIZE=10
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING_BLOCK_DELAY=10
|
||||
|
@ -23,7 +23,13 @@ const {
|
||||
ORACLE_HOME_START_BLOCK,
|
||||
ORACLE_FOREIGN_START_BLOCK,
|
||||
ORACLE_HOME_RPC_BLOCK_POLLING_LIMIT,
|
||||
ORACLE_FOREIGN_RPC_BLOCK_POLLING_LIMIT
|
||||
ORACLE_FOREIGN_RPC_BLOCK_POLLING_LIMIT,
|
||||
ORACLE_HOME_EVENTS_REPROCESSING,
|
||||
ORACLE_HOME_EVENTS_REPROCESSING_BATCH_SIZE,
|
||||
ORACLE_HOME_EVENTS_REPROCESSING_BLOCK_DELAY,
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING,
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING_BATCH_SIZE,
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING_BLOCK_DELAY
|
||||
} = process.env
|
||||
|
||||
let homeAbi
|
||||
@ -61,7 +67,12 @@ const homeConfig = {
|
||||
blockPollingLimit: parseInt(ORACLE_HOME_RPC_BLOCK_POLLING_LIMIT, 10),
|
||||
web3: web3Home,
|
||||
bridgeContract: homeContract,
|
||||
eventContract: homeContract
|
||||
eventContract: homeContract,
|
||||
reprocessingOptions: {
|
||||
enabled: ORACLE_HOME_EVENTS_REPROCESSING === 'true',
|
||||
batchSize: parseInt(ORACLE_HOME_EVENTS_REPROCESSING_BATCH_SIZE, 10) || 1000,
|
||||
blockDelay: parseInt(ORACLE_HOME_EVENTS_REPROCESSING_BLOCK_DELAY, 10) || 500
|
||||
}
|
||||
}
|
||||
|
||||
const foreignContract = new web3Foreign.eth.Contract(foreignAbi, COMMON_FOREIGN_BRIDGE_ADDRESS)
|
||||
@ -74,7 +85,12 @@ const foreignConfig = {
|
||||
blockPollingLimit: parseInt(ORACLE_FOREIGN_RPC_BLOCK_POLLING_LIMIT, 10),
|
||||
web3: web3Foreign,
|
||||
bridgeContract: foreignContract,
|
||||
eventContract: foreignContract
|
||||
eventContract: foreignContract,
|
||||
reprocessingOptions: {
|
||||
enabled: ORACLE_FOREIGN_EVENTS_REPROCESSING === 'true',
|
||||
batchSize: parseInt(ORACLE_FOREIGN_EVENTS_REPROCESSING_BATCH_SIZE, 10) || 500,
|
||||
blockDelay: parseInt(ORACLE_FOREIGN_EVENTS_REPROCESSING_BLOCK_DELAY, 10) || 250
|
||||
}
|
||||
}
|
||||
|
||||
const maxProcessingTime =
|
||||
|
37
oracle/config/foreign-mev-sender.config.js
Normal file
37
oracle/config/foreign-mev-sender.config.js
Normal file
@ -0,0 +1,37 @@
|
||||
const baseConfig = require('./base.config')
|
||||
|
||||
const { DEFAULT_TRANSACTION_RESEND_INTERVAL } = require('../src/utils/constants')
|
||||
const { MEV_HELPER_ABI } = require('../src/utils/mev')
|
||||
const { web3Foreign, getFlashbotsProvider } = require('../src/services/web3')
|
||||
|
||||
const {
|
||||
ORACLE_FOREIGN_TX_RESEND_INTERVAL,
|
||||
ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS,
|
||||
ORACLE_MEV_FOREIGN_MIN_GAS_PRICE,
|
||||
ORACLE_MEV_FOREIGN_FLAT_MINER_FEE,
|
||||
ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS,
|
||||
ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS,
|
||||
ORACLE_MEV_FOREIGN_BUNDLES_BLOCK_RANGE
|
||||
} = process.env
|
||||
|
||||
const contract = new baseConfig.foreign.web3.eth.Contract(MEV_HELPER_ABI, ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS)
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
pollingInterval: baseConfig.foreign.pollingInterval,
|
||||
mevForeign: {
|
||||
contractAddress: ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS,
|
||||
contract,
|
||||
minGasPrice: ORACLE_MEV_FOREIGN_MIN_GAS_PRICE,
|
||||
flatMinerFee: ORACLE_MEV_FOREIGN_FLAT_MINER_FEE,
|
||||
maxPriorityFeePerGas: ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS,
|
||||
maxFeePerGas: ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS,
|
||||
bundlesPerIteration: Math.max(parseInt(ORACLE_MEV_FOREIGN_BUNDLES_BLOCK_RANGE, 10) || 5, 1),
|
||||
getFlashbotsProvider
|
||||
},
|
||||
mevJobsRedisKey: `${baseConfig.id}-collected-signatures-mev:mevJobs`,
|
||||
id: 'mev-sender-foreign',
|
||||
name: 'mev-sender-foreign',
|
||||
web3: web3Foreign,
|
||||
resendInterval: parseInt(ORACLE_FOREIGN_TX_RESEND_INTERVAL, 10) || DEFAULT_TRANSACTION_RESEND_INTERVAL
|
||||
}
|
@ -8,7 +8,6 @@ const { ORACLE_FOREIGN_TX_RESEND_INTERVAL } = process.env
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
queue: 'foreign-prioritized',
|
||||
oldQueue: 'foreign',
|
||||
id: 'foreign',
|
||||
name: 'sender-foreign',
|
||||
web3: web3Foreign,
|
||||
|
@ -8,7 +8,6 @@ const { ORACLE_HOME_TX_RESEND_INTERVAL } = process.env
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
queue: 'home-prioritized',
|
||||
oldQueue: 'home',
|
||||
id: 'home',
|
||||
name: 'sender-home',
|
||||
web3: web3Home,
|
||||
|
30
oracle/config/mev-collected-signatures-watcher.config.js
Normal file
30
oracle/config/mev-collected-signatures-watcher.config.js
Normal file
@ -0,0 +1,30 @@
|
||||
const baseConfig = require('./base.config')
|
||||
const { MEV_HELPER_ABI } = require('../src/utils/mev')
|
||||
|
||||
const {
|
||||
ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS,
|
||||
ORACLE_MEV_FOREIGN_MIN_GAS_PRICE,
|
||||
ORACLE_MEV_FOREIGN_FLAT_MINER_FEE,
|
||||
ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS,
|
||||
ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS
|
||||
} = process.env
|
||||
|
||||
const id = `${baseConfig.id}-collected-signatures-mev`
|
||||
|
||||
const contract = new baseConfig.foreign.web3.eth.Contract(MEV_HELPER_ABI, ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS)
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
mevForeign: {
|
||||
contractAddress: ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS,
|
||||
contract,
|
||||
minGasPrice: ORACLE_MEV_FOREIGN_MIN_GAS_PRICE,
|
||||
flatMinerFee: ORACLE_MEV_FOREIGN_FLAT_MINER_FEE,
|
||||
maxPriorityFeePerGas: ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS,
|
||||
maxFeePerGas: ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS
|
||||
},
|
||||
main: baseConfig.home,
|
||||
event: 'CollectedSignatures',
|
||||
name: `watcher-${id}`,
|
||||
id
|
||||
}
|
@ -1,6 +1,14 @@
|
||||
---
|
||||
version: '2.4'
|
||||
services:
|
||||
redis:
|
||||
cpus: 0.1
|
||||
mem_limit: 500m
|
||||
command: [ redis-server, --appendonly, 'yes' ]
|
||||
hostname: redis
|
||||
image: redis:4
|
||||
restart: unless-stopped
|
||||
volumes: [ '~/bridge_data/helpers/redis:/data' ]
|
||||
interestFetcher:
|
||||
cpus: 0.1
|
||||
mem_limit: 500m
|
||||
@ -13,3 +21,41 @@ services:
|
||||
INTERVAL: 300000
|
||||
restart: unless-stopped
|
||||
entrypoint: yarn helper:interestFether
|
||||
mevWatcher:
|
||||
cpus: 0.1
|
||||
mem_limit: 500m
|
||||
image: poanetwork/tokenbridge-oracle:latest
|
||||
env_file: ./.env
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
ORACLE_VALIDATOR_ADDRESS: ${ORACLE_VALIDATOR_ADDRESS}
|
||||
ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS: 'TBD'
|
||||
ORACLE_MEV_FOREIGN_MIN_GAS_PRICE: '50000000000' # 50 gwei
|
||||
ORACLE_MEV_FOREIGN_FLAT_MINER_FEE: '1500000000000000' # 0.0015 eth = 300k gas * 5 gwei
|
||||
ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS: '0' # 0 gwei
|
||||
ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS: '1000000000000' # 1000 gwei
|
||||
ORACLE_FOREIGN_RPC_POLLING_INTERVAL: '15000' # CollectedSignatures event polling interval
|
||||
ORACLE_HOME_START_BLOCK: 'TBD'
|
||||
ORACLE_HOME_SKIP_MANUAL_LANE: 'true'
|
||||
restart: unless-stopped
|
||||
entrypoint: yarn mev:watcher:collected-signatures
|
||||
mevSender:
|
||||
cpus: 0.1
|
||||
mem_limit: 500m
|
||||
image: poanetwork/tokenbridge-oracle:latest
|
||||
env_file: ./.env
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
ORACLE_VALIDATOR_ADDRESS: ${ORACLE_VALIDATOR_ADDRESS}
|
||||
ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY: ${ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY}
|
||||
ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS: 'TBD'
|
||||
ORACLE_MEV_FOREIGN_MIN_GAS_PRICE: '50000000000' # 50 gwei
|
||||
ORACLE_MEV_FOREIGN_FLAT_MINER_FEE: '1500000000000000' # 0.0015 eth = 300k gas * 5 gwei
|
||||
ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS: '0' # 0 gwei
|
||||
ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS: '1000000000000' # 1000 gwei
|
||||
ORACLE_MEV_FOREIGN_FLASHBOTS_RPC_URL: 'https://relay-goerli.flashbots.net'
|
||||
ORACLE_MEV_FOREIGN_FLASHBOTS_AUTH_SIGNING_KEY: 82db7175932f4e6c8e45283b78b54fd5f195149378ec90d95b8fd0ec8bdadf1d
|
||||
ORACLE_MEV_FOREIGN_BUNDLES_BLOCK_RANGE: '5'
|
||||
ORACLE_FOREIGN_RPC_POLLING_INTERVAL: '70000' # time between sending different batches of MEV bundles (~= 5 blocks * 14 seconds)
|
||||
restart: unless-stopped
|
||||
entrypoint: yarn mev:sender:foreign
|
||||
|
@ -19,6 +19,8 @@
|
||||
"confirm:information-request": "./scripts/start-worker.sh confirmRelay information-request-watcher",
|
||||
"manager:shutdown": "./scripts/start-worker.sh shutdownManager shutdown-manager",
|
||||
"helper:interestFether": "node ./scripts/interestFetcher.js",
|
||||
"mev:watcher:collected-signatures": "./scripts/start-worker.sh mevWatcher mev-collected-signatures-watcher",
|
||||
"mev:sender:foreign": "./scripts/start-worker.sh mevSender foreign-mev-sender",
|
||||
"dev": "concurrently -n 'watcher:signature-request,watcher:collected-signatures,watcher:affirmation-request,watcher:transfer, sender:home,sender:foreign' -c 'red,green,yellow,blue,magenta,cyan' 'yarn watcher:signature-request' 'yarn watcher:collected-signatures' 'yarn watcher:affirmation-request' 'yarn watcher:transfer' 'yarn sender:home' 'yarn sender:foreign'",
|
||||
"test": "NODE_ENV=test mocha",
|
||||
"test:watch": "NODE_ENV=test mocha --watch --reporter=min",
|
||||
@ -28,17 +30,19 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@flashbots/ethers-provider-bundle": "^0.4.3",
|
||||
"amqp-connection-manager": "^2.0.0",
|
||||
"amqplib": "^0.5.2",
|
||||
"bignumber.js": "^7.2.1",
|
||||
"dotenv": "^5.0.1",
|
||||
"ethers": "^5.5.3",
|
||||
"ioredis": "^3.2.2",
|
||||
"node-fetch": "^2.1.2",
|
||||
"pino": "^4.17.3",
|
||||
"pino-pretty": "^2.0.1",
|
||||
"promise-limit": "^2.7.0",
|
||||
"promise-retry": "^1.1.1",
|
||||
"web3": "^1.3.0"
|
||||
"web3": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bn-chai": "^1.0.1",
|
||||
|
@ -36,7 +36,7 @@ async function main() {
|
||||
data,
|
||||
nonce,
|
||||
gasPrice: FOREIGN_TEST_TX_GAS_PRICE,
|
||||
amount: '0',
|
||||
value: '0',
|
||||
gasLimit,
|
||||
to: bridgeableTokenAddress,
|
||||
web3: web3Foreign,
|
||||
|
@ -29,7 +29,7 @@ async function main() {
|
||||
data: '0x',
|
||||
nonce,
|
||||
gasPrice: HOME_TEST_TX_GAS_PRICE,
|
||||
amount: HOME_MIN_AMOUNT_PER_TX,
|
||||
value: web3Home.utils.toWei(HOME_MIN_AMOUNT_PER_TX),
|
||||
gasLimit: 100000,
|
||||
to: COMMON_HOME_BRIDGE_ADDRESS,
|
||||
web3: web3Home,
|
||||
|
@ -54,7 +54,7 @@ async function main() {
|
||||
nonce,
|
||||
gasPrice,
|
||||
gasLimit: Math.round(gasLimit * 1.5),
|
||||
amount: '0',
|
||||
value: '0',
|
||||
chainId,
|
||||
web3: web3Home
|
||||
})
|
||||
|
@ -153,11 +153,11 @@ async function main({ sendJob, txHashes }) {
|
||||
}
|
||||
|
||||
async function sendJobTx(jobs) {
|
||||
await GasPrice.start(chain, true)
|
||||
const gasPrice = GasPrice.getPrice().toString(10)
|
||||
|
||||
const { web3 } = config.sender === 'foreign' ? config.foreign : config.home
|
||||
|
||||
await GasPrice.start(chain, web3, true)
|
||||
const gasPriceOptions = GasPrice.gasPriceOptions()
|
||||
|
||||
const chainId = await getChainId(web3)
|
||||
let nonce = await getNonce(web3, config.validatorAddress)
|
||||
|
||||
@ -174,13 +174,13 @@ async function sendJobTx(jobs) {
|
||||
const txHash = await sendTx({
|
||||
data: job.data,
|
||||
nonce,
|
||||
gasPrice,
|
||||
amount: '0',
|
||||
value: '0',
|
||||
gasLimit,
|
||||
privateKey: config.validatorPrivateKey,
|
||||
to: job.to,
|
||||
chainId,
|
||||
web3
|
||||
web3,
|
||||
gasPriceOptions
|
||||
})
|
||||
|
||||
nonce++
|
||||
@ -197,7 +197,7 @@ async function sendJobTx(jobs) {
|
||||
|
||||
if (e.message.toLowerCase().includes('insufficient funds')) {
|
||||
const currentBalance = await web3.eth.getBalance(config.validatorAddress)
|
||||
const minimumBalance = gasLimit.multipliedBy(gasPrice)
|
||||
const minimumBalance = gasLimit.multipliedBy(gasPriceOptions.gasPrice || gasPriceOptions.maxFeePerGas)
|
||||
logger.error(
|
||||
`Insufficient funds: ${currentBalance}. Stop processing messages until the balance is at least ${minimumBalance}.`
|
||||
)
|
||||
|
@ -4,7 +4,6 @@ const { AlreadyProcessedError, IncompatibleContractError, InvalidValidatorError
|
||||
const logger = require('../../services/logger').child({
|
||||
module: 'processCollectedSignatures:estimateGas'
|
||||
})
|
||||
const { parseAMBHeader } = require('../../utils/message')
|
||||
|
||||
const web3 = new Web3()
|
||||
const { toBN } = Web3.utils
|
||||
@ -22,15 +21,9 @@ async function estimateGas({
|
||||
address
|
||||
}) {
|
||||
try {
|
||||
const gasEstimate = await foreignBridge.methods.executeSignatures(message, signatures).estimateGas({
|
||||
return await foreignBridge.methods.executeSignatures(message, signatures).estimateGas({
|
||||
from: address
|
||||
})
|
||||
const msgGasLimit = parseAMBHeader(message).gasLimit
|
||||
|
||||
// + estimateExtraGas(len)
|
||||
// is not needed here, since estimateGas will already take into account gas
|
||||
// needed for memory expansion, message processing, etc.
|
||||
return gasEstimate + msgGasLimit
|
||||
} catch (e) {
|
||||
if (e instanceof HttpListProviderError) {
|
||||
throw e
|
||||
|
183
oracle/src/events/processAMBCollectedSignaturesMEV/index.js
Normal file
183
oracle/src/events/processAMBCollectedSignaturesMEV/index.js
Normal file
@ -0,0 +1,183 @@
|
||||
require('dotenv').config()
|
||||
const promiseLimit = require('promise-limit')
|
||||
const { HttpListProviderError } = require('../../services/HttpListProvider')
|
||||
const { getValidatorContract } = require('../../tx/web3')
|
||||
const rootLogger = require('../../services/logger')
|
||||
const { signatureToVRS, packSignatures } = require('../../utils/message')
|
||||
const { readAccessListFile, isRevertError } = require('../../utils/utils')
|
||||
const { parseAMBMessage } = require('../../../../commons')
|
||||
const estimateGas = require('../processAMBCollectedSignatures/estimateGas')
|
||||
const { AlreadyProcessedError, IncompatibleContractError, InvalidValidatorError } = require('../../utils/errors')
|
||||
const { MAX_CONCURRENT_EVENTS, EXTRA_GAS_ABSOLUTE } = require('../../utils/constants')
|
||||
|
||||
const limit = promiseLimit(MAX_CONCURRENT_EVENTS)
|
||||
|
||||
const { ORACLE_HOME_TO_FOREIGN_ALLOWANCE_LIST, ORACLE_HOME_TO_FOREIGN_BLOCK_LIST } = process.env
|
||||
const ORACLE_HOME_SKIP_MANUAL_LANE = process.env.ORACLE_HOME_SKIP_MANUAL_LANE === 'true'
|
||||
|
||||
function processCollectedSignaturesBuilder(config) {
|
||||
const { home, foreign, mevForeign } = config
|
||||
|
||||
let validatorContract = null
|
||||
|
||||
return async function processCollectedSignatures(signatures) {
|
||||
const txToSend = []
|
||||
|
||||
if (validatorContract === null) {
|
||||
validatorContract = await getValidatorContract(foreign.bridgeContract, foreign.web3)
|
||||
}
|
||||
|
||||
rootLogger.debug(`Processing ${signatures.length} CollectedSignatures events`)
|
||||
const callbacks = signatures
|
||||
.map(colSignature => async () => {
|
||||
const { messageHash, NumberOfCollectedSignatures } = colSignature.returnValues
|
||||
|
||||
const logger = rootLogger.child({
|
||||
eventTransactionHash: colSignature.transactionHash
|
||||
})
|
||||
|
||||
logger.info(`Processing CollectedSignatures ${colSignature.transactionHash}`)
|
||||
const message = await home.bridgeContract.methods.message(messageHash).call()
|
||||
const parsedMessage = parseAMBMessage(message)
|
||||
|
||||
if (ORACLE_HOME_TO_FOREIGN_ALLOWANCE_LIST || ORACLE_HOME_TO_FOREIGN_BLOCK_LIST) {
|
||||
const sender = parsedMessage.sender.toLowerCase()
|
||||
const executor = parsedMessage.executor.toLowerCase()
|
||||
|
||||
if (ORACLE_HOME_TO_FOREIGN_ALLOWANCE_LIST) {
|
||||
const allowanceList = await readAccessListFile(ORACLE_HOME_TO_FOREIGN_ALLOWANCE_LIST, logger)
|
||||
if (!allowanceList.includes(executor) && !allowanceList.includes(sender)) {
|
||||
logger.info(
|
||||
{ sender, executor },
|
||||
'Validator skips a message. Neither sender nor executor addresses are in the allowance list.'
|
||||
)
|
||||
return
|
||||
}
|
||||
} else if (ORACLE_HOME_TO_FOREIGN_BLOCK_LIST) {
|
||||
const blockList = await readAccessListFile(ORACLE_HOME_TO_FOREIGN_BLOCK_LIST, logger)
|
||||
if (blockList.includes(executor)) {
|
||||
logger.info({ executor }, 'Validator skips a message. Executor address is in the block list.')
|
||||
return
|
||||
}
|
||||
if (blockList.includes(sender)) {
|
||||
logger.info({ sender }, 'Validator skips a message. Sender address is in the block list.')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ORACLE_HOME_SKIP_MANUAL_LANE && parsedMessage.decodedDataType.manualLane) {
|
||||
logger.info(
|
||||
{ dataType: parsedMessage.dataType },
|
||||
'Validator skips a message. Message was forwarded to the manual lane by the extension'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug({ NumberOfCollectedSignatures }, 'Number of signatures to get')
|
||||
|
||||
const requiredSignatures = []
|
||||
requiredSignatures.length = NumberOfCollectedSignatures
|
||||
requiredSignatures.fill(0)
|
||||
|
||||
const signaturesArray = []
|
||||
const [v, r, s] = [[], [], []]
|
||||
logger.debug('Getting message signatures')
|
||||
const signaturePromises = requiredSignatures.map(async (el, index) => {
|
||||
logger.debug({ index }, 'Getting message signature')
|
||||
const signature = await home.bridgeContract.methods.signature(messageHash, index).call()
|
||||
const vrs = signatureToVRS(signature)
|
||||
v.push(vrs.v)
|
||||
r.push(vrs.r)
|
||||
s.push(vrs.s)
|
||||
signaturesArray.push(vrs)
|
||||
})
|
||||
|
||||
await Promise.all(signaturePromises)
|
||||
const signatures = packSignatures(signaturesArray)
|
||||
logger.info(`Processing messageId: ${parsedMessage.messageId}`)
|
||||
|
||||
let gasEstimate
|
||||
try {
|
||||
logger.debug('Estimate gas')
|
||||
gasEstimate = await estimateGas({
|
||||
foreignBridge: foreign.bridgeContract,
|
||||
validatorContract,
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
signatures,
|
||||
message,
|
||||
numberOfCollectedSignatures: NumberOfCollectedSignatures,
|
||||
messageId: parsedMessage.messageId,
|
||||
address: config.validatorAddress
|
||||
})
|
||||
logger.debug({ gasEstimate }, 'Gas estimated')
|
||||
} catch (e) {
|
||||
if (e instanceof HttpListProviderError) {
|
||||
throw new Error('RPC Connection Error: submitSignature Gas Estimate cannot be obtained.')
|
||||
} else if (e instanceof AlreadyProcessedError) {
|
||||
logger.info(`Already processed CollectedSignatures ${colSignature.transactionHash}`)
|
||||
return
|
||||
} else if (e instanceof IncompatibleContractError || e instanceof InvalidValidatorError) {
|
||||
logger.error(`The message couldn't be processed; skipping: ${e.message}`)
|
||||
return
|
||||
} else {
|
||||
logger.error(e, 'Unknown error while processing transaction')
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
const executeData = foreign.bridgeContract.methods.executeSignatures(message, signatures).encodeABI()
|
||||
const profit = await estimateProfit(
|
||||
mevForeign.contract,
|
||||
mevForeign.minGasPrice,
|
||||
executeData,
|
||||
mevForeign.flatMinerFee
|
||||
)
|
||||
if (profit === '0') {
|
||||
logger.error('No MEV opportunity found when testing with min gas price, skipping job')
|
||||
return
|
||||
}
|
||||
logger.info(`Estimated profit of ${profit} when simulating with ${mevForeign.minGasPrice} gas price`)
|
||||
|
||||
txToSend.push({
|
||||
profit,
|
||||
executeData,
|
||||
data: mevForeign.contract.methods.execute(executeData).encodeABI(),
|
||||
gasEstimate,
|
||||
extraGas: EXTRA_GAS_ABSOLUTE,
|
||||
maxFeePerGas: mevForeign.maxFeePerGas,
|
||||
maxPriorityFeePerGas: mevForeign.maxPriorityFeePerGas,
|
||||
transactionReference: colSignature.transactionHash,
|
||||
to: mevForeign.contractAddress,
|
||||
value: mevForeign.flatMinerFee
|
||||
})
|
||||
})
|
||||
.map(promise => limit(promise))
|
||||
|
||||
await Promise.all(callbacks)
|
||||
|
||||
return txToSend
|
||||
}
|
||||
}
|
||||
|
||||
async function estimateProfit(contract, gasPrice, data, minerFee) {
|
||||
return contract.methods
|
||||
.estimateProfit(gasPrice, data)
|
||||
.call({ value: minerFee })
|
||||
.then(
|
||||
res => res.toString(),
|
||||
e => {
|
||||
if (isRevertError(e)) {
|
||||
return '0'
|
||||
}
|
||||
throw e
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
processCollectedSignaturesBuilder,
|
||||
estimateProfit
|
||||
}
|
@ -6,11 +6,9 @@ const logger = require('../../services/logger').child({
|
||||
|
||||
async function estimateGas({ web3, homeBridge, validatorContract, recipient, value, txHash, address }) {
|
||||
try {
|
||||
const gasEstimate = await homeBridge.methods.executeAffirmation(recipient, value, txHash).estimateGas({
|
||||
return await homeBridge.methods.executeAffirmation(recipient, value, txHash).estimateGas({
|
||||
from: address
|
||||
})
|
||||
|
||||
return gasEstimate
|
||||
} catch (e) {
|
||||
if (e instanceof HttpListProviderError) {
|
||||
throw e
|
||||
|
@ -20,8 +20,7 @@ async function estimateGas({
|
||||
signatures
|
||||
}) {
|
||||
try {
|
||||
const gasEstimate = await foreignBridge.methods.executeSignatures(message, signatures).estimateGas()
|
||||
return gasEstimate
|
||||
return await foreignBridge.methods.executeSignatures(message, signatures).estimateGas()
|
||||
} catch (e) {
|
||||
if (e instanceof HttpListProviderError) {
|
||||
throw e
|
||||
|
@ -6,10 +6,9 @@ const logger = require('../../services/logger').child({
|
||||
|
||||
async function estimateGas({ web3, homeBridge, validatorContract, signature, message, address }) {
|
||||
try {
|
||||
const gasEstimate = await homeBridge.methods.submitSignature(signature, message).estimateGas({
|
||||
return await homeBridge.methods.submitSignature(signature, message).estimateGas({
|
||||
from: address
|
||||
})
|
||||
return gasEstimate
|
||||
} catch (e) {
|
||||
if (e instanceof HttpListProviderError) {
|
||||
throw e
|
||||
|
159
oracle/src/mevSender.js
Normal file
159
oracle/src/mevSender.js
Normal file
@ -0,0 +1,159 @@
|
||||
require('../env')
|
||||
const path = require('path')
|
||||
const BigNumber = require('bignumber.js')
|
||||
const { redis } = require('./services/redisClient')
|
||||
const logger = require('./services/logger')
|
||||
const { sendTx } = require('./tx/sendTx')
|
||||
const { getNonce, getChainId, getBlock } = require('./tx/web3')
|
||||
const { addExtraGas, checkHTTPS, watchdog } = require('./utils/utils')
|
||||
const { EXIT_CODES, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT } = require('./utils/constants')
|
||||
const { estimateProfit } = require('./events/processAMBCollectedSignaturesMEV')
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
logger.error('Please check the number of arguments, config file was not provided')
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR)
|
||||
}
|
||||
|
||||
const config = require(path.join('../config/', process.argv[2]))
|
||||
|
||||
const { web3, mevForeign, validatorAddress } = config
|
||||
|
||||
let chainId = 0
|
||||
let flashbotsProvider
|
||||
|
||||
async function initialize() {
|
||||
try {
|
||||
const checkHttps = checkHTTPS(process.env.ORACLE_ALLOW_HTTP_FOR_RPC, logger)
|
||||
|
||||
web3.currentProvider.urls.forEach(checkHttps(config.id))
|
||||
|
||||
chainId = await getChainId(web3)
|
||||
flashbotsProvider = await mevForeign.getFlashbotsProvider(chainId)
|
||||
return runMain()
|
||||
} catch (e) {
|
||||
logger.error(e.message)
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
async function runMain() {
|
||||
try {
|
||||
if (redis.status === 'ready') {
|
||||
if (config.maxProcessingTime) {
|
||||
await watchdog(main, config.maxProcessingTime, () => {
|
||||
logger.fatal('Max processing time reached')
|
||||
process.exit(EXIT_CODES.MAX_TIME_REACHED)
|
||||
})
|
||||
} else {
|
||||
await main()
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
|
||||
setTimeout(runMain, config.pollingInterval)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const jobs = Object.values(await redis.hgetall(config.mevJobsRedisKey)).map(JSON.parse)
|
||||
const totalJobs = jobs.length
|
||||
|
||||
if (totalJobs === 0) {
|
||||
logger.debug('Nothing to process')
|
||||
return
|
||||
}
|
||||
|
||||
const { baseFeePerGas: pendingBaseFee, number: pendingBlockNumber } = await getBlock(web3, 'pending')
|
||||
const bestJob = pickBestJob(jobs, pendingBaseFee)
|
||||
|
||||
if (!bestJob) {
|
||||
logger.info({ totalJobs, pendingBaseFee }, 'No suitable job was found, waiting for a lower gas price')
|
||||
return
|
||||
}
|
||||
|
||||
const jobLogger = logger.child({ eventTransactionHash: bestJob.transactionReference })
|
||||
|
||||
const maxProfit = await estimateProfit(
|
||||
mevForeign.contract,
|
||||
mevForeign.minGasPrice,
|
||||
bestJob.executeData,
|
||||
bestJob.value
|
||||
)
|
||||
|
||||
if (maxProfit === '0') {
|
||||
jobLogger.info(`No MEV opportunity found when testing with min gas price ${mevForeign.minGasPrice}, removing job`)
|
||||
await redis.hdel(config.mevJobsRedisKey, bestJob.transactionReference)
|
||||
return
|
||||
}
|
||||
jobLogger.info(`Estimated profit of ${maxProfit} when simulating with ${mevForeign.minGasPrice} gas price`)
|
||||
bestJob.profit = maxProfit
|
||||
|
||||
if (new BigNumber(pendingBaseFee).gt(mevForeign.minGasPrice)) {
|
||||
const profit = await estimateProfit(mevForeign.contract, pendingBaseFee, bestJob.executeData, bestJob.value)
|
||||
if (profit === '0') {
|
||||
jobLogger.info(
|
||||
`No MEV opportunity found when testing with current gas price ${pendingBaseFee}, waiting for lower gas price`
|
||||
)
|
||||
bestJob.maxFeePerGas = pendingBaseFee
|
||||
await redis.hset(config.mevJobsRedisKey, bestJob.transactionReference, JSON.stringify(bestJob))
|
||||
return
|
||||
}
|
||||
jobLogger.info(`Estimated profit of ${profit} when simulating with ${pendingBaseFee} gas price`)
|
||||
}
|
||||
|
||||
let gasLimit
|
||||
if (typeof bestJob.extraGas === 'number') {
|
||||
gasLimit = addExtraGas(bestJob.gasEstimate + bestJob.extraGas, 0, MAX_GAS_LIMIT)
|
||||
} else {
|
||||
gasLimit = addExtraGas(bestJob.gasEstimate, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT)
|
||||
}
|
||||
|
||||
const nonce = await getNonce(web3, validatorAddress)
|
||||
jobLogger.info(
|
||||
{ nonce, fromBlock: pendingBlockNumber, toBlock: pendingBlockNumber + mevForeign.bundlesPerIteration - 1 },
|
||||
'Sending MEV bundles'
|
||||
)
|
||||
const txHash = await sendTx({
|
||||
data: bestJob.data,
|
||||
nonce,
|
||||
value: bestJob.value,
|
||||
gasLimit,
|
||||
privateKey: config.validatorPrivateKey,
|
||||
to: bestJob.to,
|
||||
chainId,
|
||||
web3,
|
||||
gasPriceOptions: {
|
||||
maxFeePerGas: bestJob.maxFeePerGas,
|
||||
maxPriorityFeePerGas: bestJob.maxPriorityFeePerGas
|
||||
},
|
||||
mevOptions: {
|
||||
provider: flashbotsProvider,
|
||||
fromBlock: pendingBlockNumber,
|
||||
toBlock: pendingBlockNumber + mevForeign.bundlesPerIteration - 1,
|
||||
logger
|
||||
}
|
||||
})
|
||||
|
||||
jobLogger.info({ txHash }, `Tx generated ${txHash} for event Tx ${bestJob.transactionReference}`)
|
||||
|
||||
await redis.hset(config.mevJobsRedisKey, bestJob.transactionReference, JSON.stringify(bestJob))
|
||||
jobLogger.debug(`Finished processing msg`)
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
function pickBestJob(jobs, feePerGas) {
|
||||
const feePerGasBN = new BigNumber(feePerGas)
|
||||
let best = null
|
||||
jobs.forEach(job => {
|
||||
if (feePerGasBN.lt(job.maxFeePerGas) && (!best || new BigNumber(best.profit).lt(job.profit))) {
|
||||
best = job
|
||||
}
|
||||
})
|
||||
return best
|
||||
}
|
||||
|
||||
initialize()
|
251
oracle/src/mevWatcher.js
Normal file
251
oracle/src/mevWatcher.js
Normal file
@ -0,0 +1,251 @@
|
||||
require('../env')
|
||||
const path = require('path')
|
||||
const { redis } = require('./services/redisClient')
|
||||
const logger = require('./services/logger')
|
||||
const { getBlockNumber, getRequiredBlockConfirmations, getEvents } = require('./tx/web3')
|
||||
const { checkHTTPS, watchdog, syncForEach } = require('./utils/utils')
|
||||
const { processCollectedSignaturesBuilder } = require('./events/processAMBCollectedSignaturesMEV')
|
||||
const {
|
||||
EXIT_CODES,
|
||||
BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT,
|
||||
MAX_HISTORY_BLOCK_TO_REPROCESS
|
||||
} = require('./utils/constants')
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
logger.error('Please check the number of arguments, config file was not provided')
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR)
|
||||
}
|
||||
|
||||
const config = require(path.join('../config/', process.argv[2]))
|
||||
|
||||
const processAMBCollectedSignaturesMEV = processCollectedSignaturesBuilder(config)
|
||||
|
||||
const {
|
||||
web3,
|
||||
bridgeContract,
|
||||
eventContract,
|
||||
startBlock,
|
||||
pollingInterval,
|
||||
chain,
|
||||
reprocessingOptions,
|
||||
blockPollingLimit
|
||||
} = config.main
|
||||
const lastBlockRedisKey = `${config.id}:lastProcessedBlock`
|
||||
const lastReprocessedBlockRedisKey = `${config.id}:lastReprocessedBlock`
|
||||
const seenEventsRedisKey = `${config.id}:seenEvents`
|
||||
const mevJobsRedisKey = `${config.id}:mevJobs`
|
||||
let lastProcessedBlock = Math.max(startBlock - 1, 0)
|
||||
let lastReprocessedBlock
|
||||
let lastSeenBlockNumber = 0
|
||||
let sameBlockNumberCounter = 0
|
||||
|
||||
async function initialize() {
|
||||
try {
|
||||
const checkHttps = checkHTTPS(process.env.ORACLE_ALLOW_HTTP_FOR_RPC, logger)
|
||||
|
||||
web3.currentProvider.urls.forEach(checkHttps(chain))
|
||||
|
||||
await getLastProcessedBlock()
|
||||
await getLastReprocessedBlock()
|
||||
runMain({ sendToQueue: saveJobsToRedis })
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
async function runMain({ sendToQueue }) {
|
||||
try {
|
||||
if (redis.status === 'ready') {
|
||||
if (config.maxProcessingTime) {
|
||||
await watchdog(() => main({ sendToQueue }), config.maxProcessingTime, () => {
|
||||
logger.fatal('Max processing time reached')
|
||||
process.exit(EXIT_CODES.MAX_TIME_REACHED)
|
||||
})
|
||||
} else {
|
||||
await main({ sendToQueue })
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
runMain({ sendToQueue })
|
||||
}, pollingInterval)
|
||||
}
|
||||
|
||||
async function saveJobsToRedis(jobs) {
|
||||
return syncForEach(jobs, job => redis.hset(mevJobsRedisKey, job.transactionReference, JSON.stringify(job)))
|
||||
}
|
||||
|
||||
async function getLastProcessedBlock() {
|
||||
const result = await redis.get(lastBlockRedisKey)
|
||||
logger.debug({ fromRedis: result, fromConfig: lastProcessedBlock }, 'Last Processed block obtained')
|
||||
lastProcessedBlock = result ? parseInt(result, 10) : lastProcessedBlock
|
||||
}
|
||||
|
||||
async function getLastReprocessedBlock() {
|
||||
if (reprocessingOptions.enabled) {
|
||||
const result = await redis.get(lastReprocessedBlockRedisKey)
|
||||
if (result) {
|
||||
lastReprocessedBlock = Math.max(parseInt(result, 10), lastProcessedBlock - MAX_HISTORY_BLOCK_TO_REPROCESS)
|
||||
} else {
|
||||
lastReprocessedBlock = lastProcessedBlock
|
||||
}
|
||||
logger.debug({ block: lastReprocessedBlock }, 'Last reprocessed block obtained')
|
||||
} else {
|
||||
// when reprocessing is being enabled not for the first time,
|
||||
// we do not want to process blocks for which we didn't recorded seen events,
|
||||
// instead, we want to start from the current block.
|
||||
// Thus we should delete this reprocessing pointer once it is disabled.
|
||||
await redis.del(lastReprocessedBlockRedisKey)
|
||||
}
|
||||
}
|
||||
|
||||
function updateLastProcessedBlock(lastBlockNumber) {
|
||||
lastProcessedBlock = lastBlockNumber
|
||||
return redis.set(lastBlockRedisKey, lastProcessedBlock)
|
||||
}
|
||||
|
||||
function updateLastReprocessedBlock(lastBlockNumber) {
|
||||
lastReprocessedBlock = lastBlockNumber
|
||||
return redis.set(lastReprocessedBlockRedisKey, lastReprocessedBlock)
|
||||
}
|
||||
|
||||
function processEvents(events) {
|
||||
switch (config.id) {
|
||||
case 'amb-collected-signatures-mev':
|
||||
return processAMBCollectedSignaturesMEV(events)
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const eventKey = e => `${e.transactionHash}-${e.logIndex}`
|
||||
|
||||
async function reprocessOldLogs(sendToQueue) {
|
||||
const fromBlock = lastReprocessedBlock + 1
|
||||
const toBlock = lastReprocessedBlock + reprocessingOptions.batchSize
|
||||
const events = await getEvents({
|
||||
contract: eventContract,
|
||||
event: config.event,
|
||||
fromBlock,
|
||||
toBlock,
|
||||
filter: config.eventFilter
|
||||
})
|
||||
const alreadySeenEvents = await getSeenEvents(fromBlock, toBlock)
|
||||
const missingEvents = events.filter(e => !alreadySeenEvents[eventKey(e)])
|
||||
if (missingEvents.length === 0) {
|
||||
logger.debug('No missed events were found')
|
||||
} else {
|
||||
logger.info(`Found ${missingEvents.length} ${config.event} missed events`)
|
||||
const job = await processEvents(missingEvents)
|
||||
logger.info('Missed events transactions to send:', job.length)
|
||||
if (job.length) {
|
||||
await sendToQueue(job)
|
||||
}
|
||||
}
|
||||
|
||||
await updateLastReprocessedBlock(toBlock)
|
||||
await deleteSeenEvents(0, toBlock)
|
||||
}
|
||||
|
||||
async function getSeenEvents(fromBlock, toBlock) {
|
||||
const keys = await redis.zrangebyscore(seenEventsRedisKey, fromBlock, toBlock)
|
||||
const res = {}
|
||||
keys.forEach(k => {
|
||||
res[k] = true
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
function deleteSeenEvents(fromBlock, toBlock) {
|
||||
return redis.zremrangebyscore(seenEventsRedisKey, fromBlock, toBlock)
|
||||
}
|
||||
|
||||
function addSeenEvents(events) {
|
||||
return redis.zadd(seenEventsRedisKey, ...events.flatMap(e => [e.blockNumber, eventKey(e)]))
|
||||
}
|
||||
|
||||
async function getLastBlockToProcess(web3, bridgeContract) {
|
||||
const [lastBlockNumber, requiredBlockConfirmations] = await Promise.all([
|
||||
getBlockNumber(web3),
|
||||
getRequiredBlockConfirmations(bridgeContract)
|
||||
])
|
||||
|
||||
if (lastBlockNumber < lastSeenBlockNumber) {
|
||||
sameBlockNumberCounter = 0
|
||||
logger.warn({ lastBlockNumber, lastSeenBlockNumber }, 'Received block number less than already seen block')
|
||||
web3.currentProvider.switchToFallbackRPC()
|
||||
} else if (lastBlockNumber === lastSeenBlockNumber) {
|
||||
sameBlockNumberCounter++
|
||||
if (sameBlockNumberCounter > 1) {
|
||||
logger.info({ lastBlockNumber, sameBlockNumberCounter }, 'Received the same block number more than twice')
|
||||
if (sameBlockNumberCounter >= BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT) {
|
||||
sameBlockNumberCounter = 0
|
||||
logger.warn(
|
||||
{ lastBlockNumber, n: BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT },
|
||||
'Received the same block number for too many times. Probably node is not synced anymore'
|
||||
)
|
||||
web3.currentProvider.switchToFallbackRPC()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sameBlockNumberCounter = 0
|
||||
lastSeenBlockNumber = lastBlockNumber
|
||||
}
|
||||
return lastBlockNumber - requiredBlockConfirmations
|
||||
}
|
||||
|
||||
async function main({ sendToQueue }) {
|
||||
try {
|
||||
const lastBlockToProcess = await getLastBlockToProcess(web3, bridgeContract)
|
||||
|
||||
if (reprocessingOptions.enabled) {
|
||||
if (lastReprocessedBlock + reprocessingOptions.batchSize + reprocessingOptions.blockDelay < lastBlockToProcess) {
|
||||
await reprocessOldLogs(sendToQueue)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (lastBlockToProcess <= lastProcessedBlock) {
|
||||
logger.debug('All blocks already processed')
|
||||
return
|
||||
}
|
||||
|
||||
const fromBlock = lastProcessedBlock + 1
|
||||
const rangeEndBlock = blockPollingLimit ? fromBlock + blockPollingLimit : lastBlockToProcess
|
||||
const toBlock = Math.min(lastBlockToProcess, rangeEndBlock)
|
||||
|
||||
const events = await getEvents({
|
||||
contract: eventContract,
|
||||
event: config.event,
|
||||
fromBlock,
|
||||
toBlock,
|
||||
filter: config.eventFilter
|
||||
})
|
||||
logger.info(`Found ${events.length} ${config.event} events`)
|
||||
|
||||
if (events.length) {
|
||||
const job = await processEvents(events)
|
||||
logger.info('Transactions to send:', job.length)
|
||||
|
||||
if (job.length) {
|
||||
await sendToQueue(job)
|
||||
}
|
||||
if (reprocessingOptions.enabled) {
|
||||
await addSeenEvents(events)
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug({ lastProcessedBlock: toBlock.toString() }, 'Updating last processed block')
|
||||
await updateLastProcessedBlock(toBlock)
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
|
||||
logger.debug('Finished')
|
||||
}
|
||||
|
||||
initialize()
|
@ -9,6 +9,8 @@ const { sendTx } = require('./tx/sendTx')
|
||||
const { getNonce, getChainId } = require('./tx/web3')
|
||||
const {
|
||||
addExtraGas,
|
||||
applyMinGasFeeBump,
|
||||
chooseGasPriceOptions,
|
||||
checkHTTPS,
|
||||
syncForEach,
|
||||
waitForFunds,
|
||||
@ -19,7 +21,7 @@ const {
|
||||
isInsufficientBalanceError,
|
||||
isNonceError
|
||||
} = require('./utils/utils')
|
||||
const { EXIT_CODES, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT } = require('./utils/constants')
|
||||
const { EXIT_CODES, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT, MIN_GAS_PRICE_BUMP_FACTOR } = require('./utils/constants')
|
||||
|
||||
const { ORACLE_TX_REDUNDANCY } = process.env
|
||||
|
||||
@ -42,7 +44,7 @@ async function initialize() {
|
||||
|
||||
web3.currentProvider.urls.forEach(checkHttps(config.id))
|
||||
|
||||
GasPrice.start(config.id)
|
||||
GasPrice.start(config.id, web3)
|
||||
|
||||
chainId = await getChainId(web3)
|
||||
connectQueue()
|
||||
@ -55,7 +57,6 @@ async function initialize() {
|
||||
function connectQueue() {
|
||||
connectSenderToQueue({
|
||||
queueName: config.queue,
|
||||
oldQueueName: config.oldQueue,
|
||||
resendInterval: config.resendInterval,
|
||||
cb: options => {
|
||||
if (config.maxProcessingTime) {
|
||||
@ -121,7 +122,7 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT
|
||||
|
||||
const txArray = JSON.parse(msg.content)
|
||||
logger.debug(`Msg received with ${txArray.length} Tx to send`)
|
||||
const gasPrice = GasPrice.getPrice().toString(10)
|
||||
const gasPriceOptions = GasPrice.gasPriceOptions()
|
||||
|
||||
let nonce
|
||||
let insufficientFunds = false
|
||||
@ -147,6 +148,7 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT
|
||||
}
|
||||
|
||||
try {
|
||||
const newGasPriceOptions = chooseGasPriceOptions(gasPriceOptions, job.gasPriceOptions)
|
||||
if (isResend) {
|
||||
const tx = await web3Fallback.eth.getTransaction(job.txHash)
|
||||
|
||||
@ -159,24 +161,26 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT
|
||||
nonce = await readNonce(true)
|
||||
}
|
||||
|
||||
logger.info(`Transaction ${job.txHash} was not mined, updating gasPrice: ${job.gasPrice} -> ${gasPrice}`)
|
||||
const oldGasPrice = JSON.stringify(job.gasPriceOptions)
|
||||
const newGasPrice = JSON.stringify(newGasPriceOptions)
|
||||
logger.info(`Transaction ${job.txHash} was not mined, updating gasPrice: ${oldGasPrice} -> ${newGasPrice}`)
|
||||
}
|
||||
logger.info(`Sending transaction with nonce ${nonce}`)
|
||||
const txHash = await sendTx({
|
||||
data: job.data,
|
||||
nonce,
|
||||
gasPrice,
|
||||
amount: '0',
|
||||
value: '0',
|
||||
gasLimit,
|
||||
privateKey: config.validatorPrivateKey,
|
||||
to: job.to,
|
||||
chainId,
|
||||
web3: web3Redundant
|
||||
web3: web3Redundant,
|
||||
gasPriceOptions: newGasPriceOptions
|
||||
})
|
||||
const resendJob = {
|
||||
...job,
|
||||
txHash,
|
||||
gasPrice
|
||||
gasPriceOptions: newGasPriceOptions
|
||||
}
|
||||
resendJobs.push(resendJob)
|
||||
|
||||
@ -194,8 +198,8 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT
|
||||
|
||||
if (isGasPriceError(e)) {
|
||||
logger.info('Replacement transaction underpriced, forcing gas price update')
|
||||
GasPrice.start(config.id)
|
||||
failedTx.push(job)
|
||||
GasPrice.start(config.id, web3)
|
||||
failedTx.push(applyMinGasFeeBump(job, MIN_GAS_PRICE_BUMP_FACTOR))
|
||||
} else if (isResend || isSameTransactionError(e)) {
|
||||
resendJobs.push(job)
|
||||
} else {
|
||||
@ -208,7 +212,7 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT
|
||||
if (isInsufficientBalanceError(e)) {
|
||||
insufficientFunds = true
|
||||
const currentBalance = await web3.eth.getBalance(config.validatorAddress)
|
||||
minimumBalance = gasLimit.multipliedBy(gasPrice)
|
||||
minimumBalance = gasLimit.multipliedBy(gasPriceOptions.gasPrice || gasPriceOptions.maxFeePerGas)
|
||||
logger.error(
|
||||
`Insufficient funds: ${currentBalance}. Stop processing messages until the balance is at least ${minimumBalance}.`
|
||||
)
|
||||
|
@ -40,23 +40,9 @@ function connectWatcherToQueue({ queueName, cb }) {
|
||||
cb({ sendToQueue, channel: channelWrapper })
|
||||
}
|
||||
|
||||
function connectSenderToQueue({ queueName, oldQueueName, cb, resendInterval }) {
|
||||
function connectSenderToQueue({ queueName, cb, resendInterval }) {
|
||||
const deadLetterExchange = `${queueName}-retry`
|
||||
|
||||
async function resendMessagesToNewQueue(channel) {
|
||||
logger.info(`Trying to check messages in the old non-priority queue ${queueName}`)
|
||||
while (true) {
|
||||
const msg = await channel.get(oldQueueName)
|
||||
if (msg === false) {
|
||||
logger.info(`No messages in the old queue ${oldQueueName} left`)
|
||||
break
|
||||
}
|
||||
logger.debug(`Message in the old queue ${oldQueueName} was found, redirecting it to the new queue ${queueName}`)
|
||||
await channel.sendToQueue(queueName, msg.content, { persistent: true, priority: SENDER_QUEUE_SEND_PRIORITY })
|
||||
await channel.ack(msg)
|
||||
}
|
||||
}
|
||||
|
||||
const channelWrapper = connection.createChannel({
|
||||
json: true
|
||||
})
|
||||
@ -64,7 +50,6 @@ function connectSenderToQueue({ queueName, oldQueueName, cb, resendInterval }) {
|
||||
channelWrapper.addSetup(async channel => {
|
||||
await channel.assertExchange(deadLetterExchange, 'fanout', { durable: true })
|
||||
await channel.assertQueue(queueName, { durable: true, maxPriority: SENDER_QUEUE_MAX_PRIORITY })
|
||||
await channel.assertQueue(oldQueueName, { durable: true }).then(() => resendMessagesToNewQueue(channel))
|
||||
await channel.bindQueue(queueName, deadLetterExchange)
|
||||
await channel.prefetch(1)
|
||||
await channel.consume(queueName, msg =>
|
||||
|
@ -20,21 +20,21 @@ const {
|
||||
COMMON_HOME_GAS_PRICE_FACTOR
|
||||
} = process.env
|
||||
|
||||
let cachedGasPrice = null
|
||||
let cachedGasPriceOptions = null
|
||||
|
||||
let fetchGasPriceInterval = null
|
||||
|
||||
const fetchGasPrice = async (speedType, factor, bridgeContract, gasPriceSupplierUrl) => {
|
||||
const fetchGasPrice = async (speedType, factor, web3, bridgeContract, gasPriceSupplierUrl) => {
|
||||
const contractOptions = { logger }
|
||||
const supplierOptions = { speedType, factor, limits: GAS_PRICE_BOUNDARIES, logger }
|
||||
cachedGasPrice =
|
||||
(await gasPriceFromSupplier(gasPriceSupplierUrl, supplierOptions)) ||
|
||||
cachedGasPriceOptions =
|
||||
(await gasPriceFromSupplier(web3, gasPriceSupplierUrl, supplierOptions)) ||
|
||||
(await gasPriceFromContract(bridgeContract, contractOptions)) ||
|
||||
cachedGasPrice
|
||||
return cachedGasPrice
|
||||
cachedGasPriceOptions
|
||||
return cachedGasPriceOptions
|
||||
}
|
||||
|
||||
async function start(chainId, fetchOnce) {
|
||||
async function start(chainId, web3, fetchOnce) {
|
||||
clearInterval(fetchGasPriceInterval)
|
||||
|
||||
let contract = null
|
||||
@ -49,7 +49,7 @@ async function start(chainId, fetchOnce) {
|
||||
updateInterval = ORACLE_HOME_GAS_PRICE_UPDATE_INTERVAL || DEFAULT_UPDATE_INTERVAL
|
||||
factor = Number(COMMON_HOME_GAS_PRICE_FACTOR) || DEFAULT_GAS_PRICE_FACTOR
|
||||
|
||||
cachedGasPrice = COMMON_HOME_GAS_PRICE_FALLBACK
|
||||
cachedGasPriceOptions = { gasPrice: COMMON_HOME_GAS_PRICE_FALLBACK }
|
||||
} else if (chainId === 'foreign') {
|
||||
contract = foreign.bridgeContract
|
||||
gasPriceSupplierUrl = COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL
|
||||
@ -57,7 +57,7 @@ async function start(chainId, fetchOnce) {
|
||||
updateInterval = ORACLE_FOREIGN_GAS_PRICE_UPDATE_INTERVAL || DEFAULT_UPDATE_INTERVAL
|
||||
factor = Number(COMMON_FOREIGN_GAS_PRICE_FACTOR) || DEFAULT_GAS_PRICE_FACTOR
|
||||
|
||||
cachedGasPrice = COMMON_FOREIGN_GAS_PRICE_FALLBACK
|
||||
cachedGasPriceOptions = { gasPrice: COMMON_FOREIGN_GAS_PRICE_FALLBACK }
|
||||
} else {
|
||||
throw new Error(`Unrecognized chainId '${chainId}'`)
|
||||
}
|
||||
@ -67,21 +67,21 @@ async function start(chainId, fetchOnce) {
|
||||
}
|
||||
|
||||
if (fetchOnce) {
|
||||
await fetchGasPrice(speedType, factor, contract, gasPriceSupplierUrl)
|
||||
await fetchGasPrice(speedType, factor, web3, contract, gasPriceSupplierUrl)
|
||||
} else {
|
||||
fetchGasPriceInterval = await setIntervalAndRun(
|
||||
() => fetchGasPrice(speedType, factor, contract, gasPriceSupplierUrl),
|
||||
() => fetchGasPrice(speedType, factor, web3, contract, gasPriceSupplierUrl),
|
||||
updateInterval
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function getPrice() {
|
||||
return cachedGasPrice
|
||||
function gasPriceOptions() {
|
||||
return cachedGasPriceOptions
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
getPrice,
|
||||
gasPriceOptions,
|
||||
fetchGasPrice
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
const Web3 = require('web3')
|
||||
const ethers = require('ethers')
|
||||
const flashbots = require('@flashbots/ethers-provider-bundle')
|
||||
const { HttpListProvider } = require('./HttpListProvider')
|
||||
const { SafeEthLogsProvider } = require('./SafeEthLogsProvider')
|
||||
const { RedundantHttpListProvider } = require('./RedundantHttpListProvider')
|
||||
@ -9,6 +11,8 @@ const {
|
||||
COMMON_FOREIGN_RPC_URL,
|
||||
ORACLE_SIDE_RPC_URL,
|
||||
ORACLE_FOREIGN_ARCHIVE_RPC_URL,
|
||||
ORACLE_MEV_FOREIGN_FLASHBOTS_RPC_URL,
|
||||
ORACLE_MEV_FOREIGN_FLASHBOTS_AUTH_SIGNING_KEY,
|
||||
ORACLE_RPC_REQUEST_TIMEOUT,
|
||||
ORACLE_HOME_RPC_POLLING_INTERVAL,
|
||||
ORACLE_FOREIGN_RPC_POLLING_INTERVAL
|
||||
@ -94,6 +98,15 @@ if (foreignUrls.length > 1) {
|
||||
web3ForeignRedundant = new Web3(redundantProvider)
|
||||
}
|
||||
|
||||
let getFlashbotsProvider
|
||||
if (ORACLE_MEV_FOREIGN_FLASHBOTS_RPC_URL) {
|
||||
const provider = new ethers.providers.JsonRpcProvider(foreignUrls[0])
|
||||
const authSigner = new ethers.Wallet(ORACLE_MEV_FOREIGN_FLASHBOTS_AUTH_SIGNING_KEY, provider)
|
||||
|
||||
getFlashbotsProvider = chainId =>
|
||||
flashbots.FlashbotsBundleProvider.create(provider, authSigner, ORACLE_MEV_FOREIGN_FLASHBOTS_RPC_URL, chainId)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
web3Home,
|
||||
web3Foreign,
|
||||
@ -102,5 +115,6 @@ module.exports = {
|
||||
web3HomeRedundant,
|
||||
web3ForeignRedundant,
|
||||
web3HomeFallback,
|
||||
web3ForeignFallback
|
||||
web3ForeignFallback,
|
||||
getFlashbotsProvider
|
||||
}
|
||||
|
@ -1,25 +1,38 @@
|
||||
const { toWei } = require('web3').utils
|
||||
|
||||
async function sendTx({ privateKey, data, nonce, gasPrice, amount, gasLimit, to, chainId, web3 }) {
|
||||
async function sendTx(opts) {
|
||||
const { privateKey, data, nonce, gasPrice, gasPriceOptions, value, gasLimit, to, chainId, web3, mevOptions } = opts
|
||||
const gasOpts = gasPriceOptions || { gasPrice }
|
||||
const serializedTx = await web3.eth.accounts.signTransaction(
|
||||
{
|
||||
nonce: Number(nonce),
|
||||
chainId,
|
||||
to,
|
||||
data,
|
||||
value: toWei(amount),
|
||||
gasPrice,
|
||||
gas: gasLimit
|
||||
value,
|
||||
gas: gasLimit,
|
||||
...gasOpts
|
||||
},
|
||||
privateKey
|
||||
)
|
||||
|
||||
return new Promise((res, rej) =>
|
||||
web3.eth
|
||||
.sendSignedTransaction(serializedTx.rawTransaction)
|
||||
.once('transactionHash', res)
|
||||
.once('error', rej)
|
||||
if (!mevOptions) {
|
||||
return new Promise((res, rej) =>
|
||||
web3.eth
|
||||
.sendSignedTransaction(serializedTx.rawTransaction)
|
||||
.once('transactionHash', res)
|
||||
.once('error', rej)
|
||||
)
|
||||
}
|
||||
|
||||
mevOptions.logger.debug(
|
||||
{ rawTx: serializedTx.rawTransaction, txHash: serializedTx.transactionHash },
|
||||
'Signed MEV helper transaction'
|
||||
)
|
||||
|
||||
for (let blockNumber = mevOptions.fromBlock; blockNumber <= mevOptions.toBlock; blockNumber++) {
|
||||
mevOptions.logger.debug({ txHash: serializedTx.transactionHash, blockNumber }, 'Sending MEV bundle transaction')
|
||||
await mevOptions.provider.sendRawBundle([serializedTx.rawTransaction], blockNumber)
|
||||
}
|
||||
return Promise.resolve(serializedTx.transactionHash)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
@ -87,7 +87,7 @@ async function getEvents({ contract, event, fromBlock, toBlock, filter }) {
|
||||
)
|
||||
const pastEvents = await contract.getPastEvents(event, { fromBlock, toBlock, filter })
|
||||
logger.debug({ contractAddress, event, count: pastEvents.length }, 'Past events obtained')
|
||||
return pastEvents
|
||||
return pastEvents.sort((a, b) => a.blockNumber - b.blockNumber || a.transactionIndex - b.transactionIndex)
|
||||
} catch (e) {
|
||||
logger.error(e.message)
|
||||
throw new Error(`${event} events cannot be obtained`)
|
||||
|
@ -5,6 +5,7 @@ module.exports = {
|
||||
MIN_AMB_HEADER_LENGTH: 32 + 20 + 20 + 4 + 2 + 1 + 2,
|
||||
MAX_GAS_LIMIT: 10000000,
|
||||
MAX_CONCURRENT_EVENTS: 50,
|
||||
MAX_HISTORY_BLOCK_TO_REPROCESS: 10000,
|
||||
RETRY_CONFIG: {
|
||||
retries: 20,
|
||||
factor: 1.4,
|
||||
@ -23,6 +24,7 @@ module.exports = {
|
||||
MIN: 1,
|
||||
MAX: 1000
|
||||
},
|
||||
MIN_GAS_PRICE_BUMP_FACTOR: 0.1,
|
||||
DEFAULT_TRANSACTION_RESEND_INTERVAL: 20 * 60 * 1000,
|
||||
FALLBACK_RPC_URL_SWITCH_TIMEOUT: 60 * 60 * 1000,
|
||||
BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT: 10,
|
||||
|
43
oracle/src/utils/mev.js
Normal file
43
oracle/src/utils/mev.js
Normal file
@ -0,0 +1,43 @@
|
||||
const MEV_HELPER_ABI = [
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_data',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
name: 'execute',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_gasPrice',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_data',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
name: 'estimateProfit',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: true,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
module.exports = {
|
||||
MEV_HELPER_ABI
|
||||
}
|
@ -2,6 +2,9 @@ const fs = require('fs')
|
||||
const BigNumber = require('bignumber.js')
|
||||
const promiseRetry = require('promise-retry')
|
||||
const Web3 = require('web3')
|
||||
const { GAS_PRICE_BOUNDARIES } = require('./constants')
|
||||
|
||||
const { toBN, toWei } = Web3.utils
|
||||
|
||||
const retrySequence = [1, 2, 3, 5, 8, 13, 21, 34, 55, 60]
|
||||
|
||||
@ -34,8 +37,8 @@ const promiseRetryForever = f => promiseRetry(f, { forever: true, factor: 1 })
|
||||
async function waitForFunds(web3, address, minimumBalance, cb, logger) {
|
||||
promiseRetryForever(async retry => {
|
||||
logger.debug('Getting balance of validator account')
|
||||
const newBalance = web3.utils.toBN(await web3.eth.getBalance(address))
|
||||
if (newBalance.gte(web3.utils.toBN(minimumBalance.toString(10)))) {
|
||||
const newBalance = toBN(await web3.eth.getBalance(address))
|
||||
if (newBalance.gte(toBN(minimumBalance.toString(10)))) {
|
||||
logger.debug({ balance: newBalance, minimumBalance }, 'Validator has minimum necessary balance')
|
||||
cb(newBalance)
|
||||
} else {
|
||||
@ -64,6 +67,48 @@ function addExtraGas(gas, extraPercentage, maxGasLimit = Infinity) {
|
||||
return BigNumber.min(maxGasLimit, gasWithExtra)
|
||||
}
|
||||
|
||||
function applyMinGasFeeBump(job, bumpFactor = 0.1) {
|
||||
if (!job.gasPriceOptions) {
|
||||
return job
|
||||
}
|
||||
const { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = job.gasPriceOptions
|
||||
const maxGasPrice = toWei(GAS_PRICE_BOUNDARIES.MAX.toString(), 'gwei')
|
||||
if (gasPrice) {
|
||||
return {
|
||||
...job,
|
||||
gasPriceOptions: {
|
||||
gasPrice: addExtraGas(gasPrice, bumpFactor, maxGasPrice).toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (maxFeePerGas && maxPriorityFeePerGas) {
|
||||
return {
|
||||
...job,
|
||||
gasPriceOptions: {
|
||||
maxFeePerGas: addExtraGas(maxFeePerGas, bumpFactor, maxGasPrice).toString(),
|
||||
maxPriorityFeePerGas: addExtraGas(maxPriorityFeePerGas, bumpFactor, maxGasPrice).toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
return job
|
||||
}
|
||||
|
||||
function chooseGasPriceOptions(a, b) {
|
||||
if (!a) {
|
||||
return b
|
||||
}
|
||||
if (a && b && a.gasPrice && b.gasPrice) {
|
||||
return { gasPrice: BigNumber.max(a.gasPrice, b.gasPrice).toString() }
|
||||
}
|
||||
if (a && b && a.maxFeePerGas && b.maxFeePerGas && a.maxPriorityFeePerGas && b.maxPriorityFeePerGas) {
|
||||
return {
|
||||
maxFeePerGas: BigNumber.max(a.maxFeePerGas, b.maxFeePerGas).toString(),
|
||||
maxPriorityFeePerGas: BigNumber.max(a.maxPriorityFeePerGas, b.maxPriorityFeePerGas).toString()
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
async function setIntervalAndRun(f, interval) {
|
||||
const handler = setInterval(f, interval)
|
||||
await f()
|
||||
@ -183,6 +228,8 @@ module.exports = {
|
||||
waitForFunds,
|
||||
waitForUnsuspend,
|
||||
addExtraGas,
|
||||
chooseGasPriceOptions,
|
||||
applyMinGasFeeBump,
|
||||
setIntervalAndRun,
|
||||
watchdog,
|
||||
add0xPrefix,
|
||||
|
@ -6,7 +6,11 @@ const logger = require('./services/logger')
|
||||
const { getShutdownFlag } = require('./services/shutdownState')
|
||||
const { getBlockNumber, getRequiredBlockConfirmations, getEvents } = require('./tx/web3')
|
||||
const { checkHTTPS, watchdog } = require('./utils/utils')
|
||||
const { EXIT_CODES, BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT } = require('./utils/constants')
|
||||
const {
|
||||
EXIT_CODES,
|
||||
BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT,
|
||||
MAX_HISTORY_BLOCK_TO_REPROCESS
|
||||
} = require('./utils/constants')
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
logger.error('Please check the number of arguments, config file was not provided')
|
||||
@ -26,9 +30,21 @@ const processAMBInformationRequests = require('./events/processAMBInformationReq
|
||||
|
||||
const { getTokensState } = require('./utils/tokenState')
|
||||
|
||||
const { web3, bridgeContract, eventContract, startBlock, pollingInterval, chain } = config.main
|
||||
const {
|
||||
web3,
|
||||
bridgeContract,
|
||||
eventContract,
|
||||
startBlock,
|
||||
pollingInterval,
|
||||
chain,
|
||||
reprocessingOptions,
|
||||
blockPollingLimit
|
||||
} = config.main
|
||||
const lastBlockRedisKey = `${config.id}:lastProcessedBlock`
|
||||
const lastReprocessedBlockRedisKey = `${config.id}:lastReprocessedBlock`
|
||||
const seenEventsRedisKey = `${config.id}:seenEvents`
|
||||
let lastProcessedBlock = Math.max(startBlock - 1, 0)
|
||||
let lastReprocessedBlock
|
||||
let lastSeenBlockNumber = 0
|
||||
let sameBlockNumberCounter = 0
|
||||
|
||||
@ -39,6 +55,8 @@ async function initialize() {
|
||||
web3.currentProvider.urls.forEach(checkHttps(chain))
|
||||
|
||||
await getLastProcessedBlock()
|
||||
await getLastReprocessedBlock()
|
||||
await checkConditions()
|
||||
connectWatcherToQueue({
|
||||
queueName: config.queue,
|
||||
cb: runMain
|
||||
@ -76,11 +94,34 @@ async function getLastProcessedBlock() {
|
||||
lastProcessedBlock = result ? parseInt(result, 10) : lastProcessedBlock
|
||||
}
|
||||
|
||||
async function getLastReprocessedBlock() {
|
||||
if (reprocessingOptions.enabled) {
|
||||
const result = await redis.get(lastReprocessedBlockRedisKey)
|
||||
if (result) {
|
||||
lastReprocessedBlock = Math.max(parseInt(result, 10), lastProcessedBlock - MAX_HISTORY_BLOCK_TO_REPROCESS)
|
||||
} else {
|
||||
lastReprocessedBlock = lastProcessedBlock
|
||||
}
|
||||
logger.debug({ block: lastReprocessedBlock }, 'Last reprocessed block obtained')
|
||||
} else {
|
||||
// when reprocessing is being enabled not for the first time,
|
||||
// we do not want to process blocks for which we didn't recorded seen events,
|
||||
// instead, we want to start from the current block.
|
||||
// Thus we should delete this reprocessing pointer once it is disabled.
|
||||
await redis.del(lastReprocessedBlockRedisKey)
|
||||
}
|
||||
}
|
||||
|
||||
function updateLastProcessedBlock(lastBlockNumber) {
|
||||
lastProcessedBlock = lastBlockNumber
|
||||
return redis.set(lastBlockRedisKey, lastProcessedBlock)
|
||||
}
|
||||
|
||||
function updateLastReprocessedBlock(lastBlockNumber) {
|
||||
lastReprocessedBlock = lastBlockNumber
|
||||
return redis.set(lastReprocessedBlockRedisKey, lastReprocessedBlock)
|
||||
}
|
||||
|
||||
function processEvents(events) {
|
||||
switch (config.id) {
|
||||
case 'erc-native-signature-request':
|
||||
@ -114,6 +155,71 @@ async function checkConditions() {
|
||||
}
|
||||
}
|
||||
|
||||
const eventKey = e => `${e.transactionHash}-${e.logIndex}`
|
||||
|
||||
async function reprocessOldLogs(sendToQueue) {
|
||||
const fromBlock = lastReprocessedBlock + 1
|
||||
let toBlock = lastReprocessedBlock + reprocessingOptions.batchSize
|
||||
const events = await getEvents({
|
||||
contract: eventContract,
|
||||
event: config.event,
|
||||
fromBlock,
|
||||
toBlock,
|
||||
filter: config.eventFilter
|
||||
})
|
||||
const alreadySeenEvents = await getSeenEvents(fromBlock, toBlock)
|
||||
const missingEvents = events.filter(e => !alreadySeenEvents[eventKey(e)])
|
||||
if (missingEvents.length === 0) {
|
||||
logger.debug('No missed events were found')
|
||||
} else {
|
||||
logger.info(`Found ${missingEvents.length} ${config.event} missed events`)
|
||||
let job
|
||||
if (config.id === 'amb-information-request') {
|
||||
// obtain block number and events from the earliest block
|
||||
const batchBlockNumber = missingEvents[0].blockNumber
|
||||
const batchEvents = missingEvents.filter(event => event.blockNumber === batchBlockNumber)
|
||||
|
||||
// if there are some other events in the later blocks,
|
||||
// adjust lastReprocessedBlock so that these events will be processed again on the next iteration
|
||||
if (batchEvents.length < missingEvents.length) {
|
||||
// pick event outside from the batch
|
||||
toBlock = missingEvents[batchEvents.length].blockNumber - 1
|
||||
}
|
||||
|
||||
job = await processAMBInformationRequests(batchEvents)
|
||||
if (job === null) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
job = await processEvents(missingEvents)
|
||||
}
|
||||
logger.info('Missed events transactions to send:', job.length)
|
||||
if (job.length) {
|
||||
await sendToQueue(job)
|
||||
}
|
||||
}
|
||||
|
||||
await updateLastReprocessedBlock(toBlock)
|
||||
await deleteSeenEvents(0, toBlock)
|
||||
}
|
||||
|
||||
async function getSeenEvents(fromBlock, toBlock) {
|
||||
const keys = await redis.zrangebyscore(seenEventsRedisKey, fromBlock, toBlock)
|
||||
const res = {}
|
||||
keys.forEach(k => {
|
||||
res[k] = true
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
function deleteSeenEvents(fromBlock, toBlock) {
|
||||
return redis.zremrangebyscore(seenEventsRedisKey, fromBlock, toBlock)
|
||||
}
|
||||
|
||||
function addSeenEvents(events) {
|
||||
return redis.zadd(seenEventsRedisKey, ...events.flatMap(e => [e.blockNumber, eventKey(e)]))
|
||||
}
|
||||
|
||||
async function getLastBlockToProcess(web3, bridgeContract) {
|
||||
const [lastBlockNumber, requiredBlockConfirmations] = await Promise.all([
|
||||
getBlockNumber(web3),
|
||||
@ -158,24 +264,29 @@ async function main({ sendToQueue }) {
|
||||
|
||||
const lastBlockToProcess = await getLastBlockToProcess(web3, bridgeContract)
|
||||
|
||||
if (reprocessingOptions.enabled) {
|
||||
if (lastReprocessedBlock + reprocessingOptions.batchSize + reprocessingOptions.blockDelay < lastBlockToProcess) {
|
||||
await reprocessOldLogs(sendToQueue)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (lastBlockToProcess <= lastProcessedBlock) {
|
||||
logger.debug('All blocks already processed')
|
||||
return
|
||||
}
|
||||
|
||||
await checkConditions()
|
||||
|
||||
const fromBlock = lastProcessedBlock + 1
|
||||
const rangeEndBlock = config.blockPollingLimit ? fromBlock + config.blockPollingLimit : lastBlockToProcess
|
||||
const rangeEndBlock = blockPollingLimit ? fromBlock + blockPollingLimit : lastBlockToProcess
|
||||
let toBlock = Math.min(lastBlockToProcess, rangeEndBlock)
|
||||
|
||||
const events = (await getEvents({
|
||||
let events = await getEvents({
|
||||
contract: eventContract,
|
||||
event: config.event,
|
||||
fromBlock,
|
||||
toBlock,
|
||||
filter: config.eventFilter
|
||||
})).sort((a, b) => a.blockNumber - b.blockNumber)
|
||||
})
|
||||
logger.info(`Found ${events.length} ${config.event} events`)
|
||||
|
||||
if (events.length) {
|
||||
@ -192,9 +303,10 @@ async function main({ sendToQueue }) {
|
||||
if (batchEvents.length < events.length) {
|
||||
// pick event outside from the batch
|
||||
toBlock = events[batchEvents.length].blockNumber - 1
|
||||
events = batchEvents
|
||||
}
|
||||
|
||||
job = await processAMBInformationRequests(batchEvents)
|
||||
job = await processAMBInformationRequests(events)
|
||||
if (job === null) {
|
||||
return
|
||||
}
|
||||
@ -206,6 +318,9 @@ async function main({ sendToQueue }) {
|
||||
if (job.length) {
|
||||
await sendToQueue(job)
|
||||
}
|
||||
if (reprocessingOptions.enabled) {
|
||||
await addSeenEvents(events)
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug({ lastProcessedBlock: toBlock.toString() }, 'Updating last processed block')
|
||||
|
@ -71,10 +71,10 @@ describe('gasPrice', () => {
|
||||
await gasPrice.start('home')
|
||||
|
||||
// when
|
||||
await gasPrice.fetchGasPrice('standard', 1, null, null)
|
||||
await gasPrice.fetchGasPrice('standard', 1, null, null, null)
|
||||
|
||||
// then
|
||||
expect(gasPrice.getPrice()).to.equal('101000000000')
|
||||
expect(gasPrice.gasPriceOptions()).to.eql({ gasPrice: '101000000000' })
|
||||
})
|
||||
|
||||
it('should fetch gas from supplier', async () => {
|
||||
@ -82,10 +82,10 @@ describe('gasPrice', () => {
|
||||
await gasPrice.start('home')
|
||||
|
||||
// when
|
||||
await gasPrice.fetchGasPrice('standard', 1, null, 'url')
|
||||
await gasPrice.fetchGasPrice('standard', 1, null, null, 'url')
|
||||
|
||||
// then
|
||||
expect(gasPrice.getPrice().toString()).to.equal('103000000000')
|
||||
expect(gasPrice.gasPriceOptions()).to.eql({ gasPrice: '103000000000' })
|
||||
})
|
||||
|
||||
it('should fetch gas from contract', async () => {
|
||||
@ -101,10 +101,10 @@ describe('gasPrice', () => {
|
||||
}
|
||||
|
||||
// when
|
||||
await gasPrice.fetchGasPrice('standard', 1, bridgeContractMock, null)
|
||||
await gasPrice.fetchGasPrice('standard', 1, null, bridgeContractMock, null)
|
||||
|
||||
// then
|
||||
expect(gasPrice.getPrice().toString()).to.equal('102000000000')
|
||||
expect(gasPrice.gasPriceOptions()).to.eql({ gasPrice: '102000000000' })
|
||||
})
|
||||
|
||||
it('should fetch the gas price from the oracle first', async () => {
|
||||
@ -120,10 +120,10 @@ describe('gasPrice', () => {
|
||||
}
|
||||
|
||||
// when
|
||||
await gasPrice.fetchGasPrice('standard', 1, bridgeContractMock, 'url')
|
||||
await gasPrice.fetchGasPrice('standard', 1, null, bridgeContractMock, 'url')
|
||||
|
||||
// then
|
||||
expect(gasPrice.getPrice().toString()).to.equal('103000000000')
|
||||
expect(gasPrice.gasPriceOptions()).to.eql({ gasPrice: '103000000000' })
|
||||
})
|
||||
|
||||
it('log error using the logger', async () => {
|
||||
@ -131,7 +131,7 @@ describe('gasPrice', () => {
|
||||
await gasPrice.start('home')
|
||||
|
||||
// when
|
||||
await gasPrice.fetchGasPrice('standard', 1, null, null)
|
||||
await gasPrice.fetchGasPrice('standard', 1, null, null, null)
|
||||
|
||||
// then
|
||||
expect(fakeLogger.warn.calledOnce).to.equal(true) // one warning
|
||||
|
@ -3,7 +3,13 @@ const chai = require('chai')
|
||||
const chaiAsPromised = require('chai-as-promised')
|
||||
const BigNumber = require('bignumber.js')
|
||||
const proxyquire = require('proxyquire')
|
||||
const { addExtraGas, syncForEach, promiseAny } = require('../src/utils/utils')
|
||||
const {
|
||||
addExtraGas,
|
||||
applyMinGasFeeBump,
|
||||
chooseGasPriceOptions,
|
||||
syncForEach,
|
||||
promiseAny
|
||||
} = require('../src/utils/utils')
|
||||
|
||||
chai.use(chaiAsPromised)
|
||||
chai.should()
|
||||
@ -173,4 +179,43 @@ describe('utils', () => {
|
||||
await promiseAny(array.map(f)).should.be.rejected
|
||||
})
|
||||
})
|
||||
|
||||
describe('applyMinGasFeeBump', () => {
|
||||
it('should bump pre-eip1559 fee', () => {
|
||||
const job = { gasPriceOptions: { gasPrice: '100000000000' } }
|
||||
const newJob = applyMinGasFeeBump(job)
|
||||
expect(newJob.gasPriceOptions.gasPrice).to.be.equal('110000000000')
|
||||
})
|
||||
|
||||
it('should bump eip1559 fee', () => {
|
||||
const job = { gasPriceOptions: { maxFeePerGas: '100000000000', maxPriorityFeePerGas: '20000000000' } }
|
||||
const newJob = applyMinGasFeeBump(job)
|
||||
expect(newJob.gasPriceOptions.maxFeePerGas).to.be.equal('110000000000')
|
||||
expect(newJob.gasPriceOptions.maxPriorityFeePerGas).to.be.equal('22000000000')
|
||||
})
|
||||
})
|
||||
|
||||
describe('chooseGasPriceOptions', () => {
|
||||
it('should choose max pre-eip1559 fee', () => {
|
||||
const opts1 = { gasPrice: '100000000000' }
|
||||
const opts2 = { gasPrice: '101000000000' }
|
||||
expect(chooseGasPriceOptions(opts1, opts2).gasPrice).to.be.equal('101000000000')
|
||||
expect(chooseGasPriceOptions(opts2, opts1).gasPrice).to.be.equal('101000000000')
|
||||
expect(chooseGasPriceOptions(opts2, undefined).gasPrice).to.be.equal('101000000000')
|
||||
expect(chooseGasPriceOptions(undefined, opts2).gasPrice).to.be.equal('101000000000')
|
||||
})
|
||||
|
||||
it('should choose max eip1559 fee', () => {
|
||||
const opts1 = { maxFeePerGas: '100000000000', maxPriorityFeePerGas: '21000000000' }
|
||||
const opts2 = { maxFeePerGas: '101000000000', maxPriorityFeePerGas: '20000000000' }
|
||||
expect(chooseGasPriceOptions(opts1, opts2).maxFeePerGas).to.be.equal('101000000000')
|
||||
expect(chooseGasPriceOptions(opts1, opts2).maxPriorityFeePerGas).to.be.equal('21000000000')
|
||||
expect(chooseGasPriceOptions(opts2, opts1).maxFeePerGas).to.be.equal('101000000000')
|
||||
expect(chooseGasPriceOptions(opts2, opts1).maxPriorityFeePerGas).to.be.equal('21000000000')
|
||||
expect(chooseGasPriceOptions(opts2, undefined).maxFeePerGas).to.be.equal('101000000000')
|
||||
expect(chooseGasPriceOptions(opts2, undefined).maxPriorityFeePerGas).to.be.equal('20000000000')
|
||||
expect(chooseGasPriceOptions(undefined, opts2).maxFeePerGas).to.be.equal('101000000000')
|
||||
expect(chooseGasPriceOptions(undefined, opts2).maxPriorityFeePerGas).to.be.equal('20000000000')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -45,5 +45,8 @@
|
||||
"compile:contracts": "yarn workspace tokenbridge-contracts run compile",
|
||||
"install:deploy": "cd contracts/deploy && npm install --unsafe-perm --silent",
|
||||
"postinstall": "test -n \"$NOYARNPOSTINSTALL\" || ln -sf $(pwd)/node_modules/openzeppelin-solidity/ contracts/node_modules/openzeppelin-solidity"
|
||||
},
|
||||
"resolutions": {
|
||||
"**/@mycrypto/eth-scan": "3.5.3"
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user