Commit 1efebf14 authored by Matthew Slipper's avatar Matthew Slipper Committed by GitHub

Merge pull request #2254 from cfromknecht/remove-ts-batch-submitter

feat: remove packages/batch-submitter
parents 66df4cd4 6b0d57aa
......@@ -20,9 +20,6 @@ jobs:
- 5000:5000
strategy:
matrix:
batch-submitter:
- ts-batch-submitter
- go-batch-submitter
batch-type:
- zlib
- legacy
......@@ -53,7 +50,7 @@ jobs:
working-directory: ./ops
run: |
./scripts/stats.sh &
docker-compose -f docker-compose.yml -f docker-compose.${{ matrix.batch-submitter }}.yml up -d
docker-compose -f docker-compose.yml up -d
- name: Wait for the Sequencer node
working-directory: ./ops
......
......@@ -113,12 +113,6 @@ jobs:
fail_ci_if_error: false
verbose: true
flags: core-utils
- uses: codecov/codecov-action@v1
with:
files: ./packages/batch-submitter/coverage.json
fail_ci_if_error: false
verbose: true
flags: batch-submitter
- uses: codecov/codecov-action@v1
with:
files: ./packages/data-transport-layer/coverage.json
......@@ -170,10 +164,6 @@ jobs:
# if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install
- name: Check packages/batch-submitter
working-directory: ./packages/batch-submitter
run: npx depcheck
- name: Check packages/contracts
working-directory: ./packages/contracts
run: npx depcheck
......
......@@ -116,7 +116,7 @@ This will build the following containers:
* [`l2geth`](https://hub.docker.com/r/ethereumoptimism/l2geth): L2 geth node running in Sequencer mode
* [`verifier`](https://hub.docker.com/r/ethereumoptimism/go-ethereum): L2 geth node running in Verifier mode
* [`relayer`](https://hub.docker.com/r/ethereumoptimism/message-relayer): helper process that relays messages between L1 and L2
* [`batch_submitter`](https://hub.docker.com/r/ethereumoptimism/batch-submitter): service that submits batches of Sequencer transactions to the L1 chain
* [`batch_submitter`](https://hub.docker.com/r/ethereumoptimism/batch-submitter-service): service that submits batches of Sequencer transactions to the L1 chain
* [`integration_tests`](https://hub.docker.com/r/ethereumoptimism/integration-tests): integration tests in a box
If you want to make a change to a container, you'll need to take it down and rebuild it.
......
......@@ -35,7 +35,6 @@ root
│ ├── <a href="./packages/core-utils">core-utils</a>: Low-level utilities that make building Optimism easier
│ ├── <a href="./packages/common-ts">common-ts</a>: Common tools for building apps in TypeScript
│ ├── <a href="./packages/data-transport-layer">data-transport-layer</a>: Service for indexing Optimism-related L1 data
│ ├── <a href="./packages/batch-submitter">batch-submitter</a>: Service for submitting batches of transactions and results to L1
│ ├── <a href="./packages/message-relayer">message-relayer</a>: Tool for automatically relaying L1<>L2 messages in development
│ └── <a href="./packages/replica-healthcheck">replica-healthcheck</a>: Service for monitoring the health of a replica node
├── <a href="./go">go</a>
......
BATCH_SUBMITTER ?= docker-compose.ts-batch-submitter.yml
DOCKER_COMPOSE_CMD := docker-compose \
-f docker-compose.yml \
-f $(BATCH_SUBMITTER)
-f docker-compose.yml
build:
DOCKER_BUILDKIT=1 \
......
......@@ -26,7 +26,6 @@ The base stack can be started and stopped with a command like this:
```
docker-compose \
-f docker-compose.yml \
-f docker-compose.ts-batch-submitter.yml \
up --build --detach
```
......@@ -40,7 +39,6 @@ To start the stack with monitoring enabled, just add the metric composition file
```
docker-compose \
-f docker-compose.yml \
-f docker-compose.ts-batch-submitter.yml \
-f docker-compose-metrics.yml \
up --build --detach
```
......@@ -50,7 +48,6 @@ Optionally, run a verifier along the rest of the stack. Run a replica with the s
```
docker-compose
-f docker-compose.yml \
-f docker-compose.ts-batch-submitter.yml \
up --scale \
verifier=1 \
--build --detach
......@@ -71,24 +68,6 @@ Fees can be turned off at runtime by setting the environment variable
ROLLUP_ENFORCE_FEES=false docker-compose up
```
## Using the Go Batch Submitter
The existing Typescript batch submitter is in the process of being reimplemented
in Go. During this transition, the user is required to specify which batch
submitter to use with docker-compose.
The commands above all use the Typescript batch submitter, by specifying
`-f docker-compose.ts-batch-submitter.yml`. This can be swapped out for the go
batch submitter by supplying `-f docker-compose.go-batch-submitter.yml` instead.
Additionally, the `make` targets assume the use of the Typescript batch
submitter. This can be overridden by setting the `BATCH_SUBMITTER` environment
variable, e.g. `BATCH_SUBMITTER=docker-compose.go-batch-submitter.yml make up`.
Once the transition is complete, specifying the desired batch submitter will be
obsolete, and the Go batch submitter will be selected by default from the
`docker-compose.yml` file and `Makefile`.
## Cross domain communication
By default, the `message-relayer` service is turned off. This means that
......
......@@ -85,16 +85,17 @@ services:
- l1_chain
- deployer
- l2geth
image: ethereumoptimism/batch-submitter:${DOCKER_TAG:-prerelease-0.5.0-rc-7-ee217ce}
entrypoint: ./batches.sh
image: ethereumoptimism/batch-submitter-service:${DOCKER_TAG:-prerelease-0.5.0-rc-7-ee217ce}
entrypoint: ./batch-submitter.sh
env_file:
- ./envs/batches.env
- ./envs/batch-submitter.env
environment:
L1_NODE_WEB3_URL: http://l1_chain:8545
L2_NODE_WEB3_URL: http://l2geth:8545
URL: http://deployer:8081/addresses.json
SEQUENCER_PRIVATE_KEY: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
PROPOSER_PRIVATE_KEY: "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a"
L1_ETH_RPC: http://l1_chain:8545
L2_ETH_RPC: http://l2geth:8545
URL: http://deployer:8081/addresses.json
BATCH_SUBMITTER_SEQUENCER_PRIVATE_KEY: '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d'
BATCH_SUBMITTER_PROPOSER_PRIVATE_KEY: '0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a'
BATCH_SUBMITTER_SEQUENCER_BATCH_TYPE: ${BATCH_SUBMITTER_SEQUENCER_BATCH_TYPE:-zlib}
verifier:
depends_on:
......
services:
batch_submitter:
depends_on:
- l1_chain
- deployer
- l2geth
build:
context: ..
dockerfile: ./ops/docker/Dockerfile.batch-submitter-service
entrypoint: ./batch-submitter.sh
env_file:
- ./envs/batch-submitter.env
environment:
L1_ETH_RPC: http://l1_chain:8545
L2_ETH_RPC: http://l2geth:8545
URL: http://deployer:8081/addresses.json
BATCH_SUBMITTER_SEQUENCER_PRIVATE_KEY: '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d'
BATCH_SUBMITTER_PROPOSER_PRIVATE_KEY: '0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a'
BATCH_SUBMITTER_SEQUENCER_BATCH_TYPE: ${BATCH_SUBMITTER_SEQUENCER_BATCH_TYPE:-zlib}
services:
batch_submitter:
depends_on:
- l1_chain
- deployer
- l2geth
build:
context: ..
dockerfile: ./ops/docker/Dockerfile.packages
target: batch-submitter
entrypoint: ./batches.sh
env_file:
- ./envs/batches.env
environment:
L1_NODE_WEB3_URL: http://l1_chain:8545
L2_NODE_WEB3_URL: http://l2geth:8545
URL: http://deployer:8081/addresses.json
SEQUENCER_PRIVATE_KEY: '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d'
PROPOSER_PRIVATE_KEY: '0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a'
BATCH_SUBMITTER_SEQUENCER_BATCH_TYPE: ${BATCH_SUBMITTER_SEQUENCER_BATCH_TYPE:-zlib}
......@@ -212,3 +212,22 @@ services:
GAS_PRICE_ORACLE_ETHEREUM_HTTP_URL: http://l2geth:8545
# Default hardhat account 5
GAS_PRICE_ORACLE_PRIVATE_KEY: '0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba'
batch_submitter:
depends_on:
- l1_chain
- deployer
- l2geth
build:
context: ..
dockerfile: ./ops/docker/Dockerfile.batch-submitter-service
entrypoint: ./batch-submitter.sh
env_file:
- ./envs/batch-submitter.env
environment:
L1_ETH_RPC: http://l1_chain:8545
L2_ETH_RPC: http://l2geth:8545
URL: http://deployer:8081/addresses.json
BATCH_SUBMITTER_SEQUENCER_PRIVATE_KEY: '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d'
BATCH_SUBMITTER_PROPOSER_PRIVATE_KEY: '0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a'
BATCH_SUBMITTER_SEQUENCER_BATCH_TYPE: ${BATCH_SUBMITTER_SEQUENCER_BATCH_TYPE:-zlib}
ARG LOCAL_REGISTRY=docker.io
ARG BUILDER_TAG=latest
FROM ${LOCAL_REGISTRY}/ethereumoptimism/builder:${BUILDER_TAG} AS builder
FROM node:16-alpine
RUN apk add --no-cache curl bash jq
WORKDIR /opt/optimism
# copy top level files
COPY --from=builder /optimism/*.json /optimism/yarn.lock ./
COPY --from=builder /optimism/node_modules ./node_modules
# copy deps (would have been nice if docker followed the symlinks required)
COPY --from=builder /optimism/packages/core-utils/package.json ./packages/core-utils/package.json
COPY --from=builder /optimism/packages/core-utils/dist ./packages/core-utils/dist
COPY --from=builder /optimism/packages/common-ts/package.json ./packages/common-ts/package.json
COPY --from=builder /optimism/packages/common-ts/dist ./packages/common-ts/dist
COPY --from=builder /optimism/packages/contracts/package.json ./packages/contracts/package.json
COPY --from=builder /optimism/packages/contracts/deployments ./packages/contracts/deployments
COPY --from=builder /optimism/packages/contracts/dist ./packages/contracts/dist
COPY --from=builder /optimism/packages/contracts/artifacts ./packages/contracts/artifacts
# copy the service
WORKDIR /opt/optimism/packages/batch-submitter
COPY --from=builder /optimism/packages/batch-submitter/package.json ./
COPY --from=builder /optimism/packages/batch-submitter/dist ./dist
COPY --from=builder /optimism/packages/batch-submitter/exec ./exec
COPY --from=builder /optimism/packages/batch-submitter/node_modules ./node_modules
# copy this over in case you want to run alongside other services
COPY ./ops/scripts/batches.sh .
ENTRYPOINT ["npm", "run", "start"]
......@@ -29,7 +29,6 @@ COPY packages/core-utils/package.json ./packages/core-utils/package.json
COPY packages/common-ts/package.json ./packages/common-ts/package.json
COPY packages/contracts/package.json ./packages/contracts/package.json
COPY packages/data-transport-layer/package.json ./packages/data-transport-layer/package.json
COPY packages/batch-submitter/package.json ./packages/batch-submitter/package.json
COPY packages/message-relayer/package.json ./packages/message-relayer/package.json
COPY packages/replica-healthcheck/package.json ./packages/replica-healthcheck/package.json
COPY integration-tests/package.json ./integration-tests/package.json
......
......@@ -18,7 +18,6 @@ COPY packages/core-utils/package.json ./packages/core-utils/package.json
COPY packages/common-ts/package.json ./packages/common-ts/package.json
COPY packages/contracts/package.json ./packages/contracts/package.json
COPY packages/data-transport-layer/package.json ./packages/data-transport-layer/package.json
COPY packages/batch-submitter/package.json ./packages/batch-submitter/package.json
COPY packages/message-relayer/package.json ./packages/message-relayer/package.json
COPY packages/replica-healthcheck/package.json ./packages/replica-healthcheck/package.json
COPY integration-tests/package.json ./integration-tests/package.json
......@@ -38,12 +37,6 @@ COPY ./ops/scripts/deployer.sh .
CMD ["yarn", "run", "deploy"]
FROM base as batch-submitter
WORKDIR /opt/optimism/packages/batch-submitter
COPY ./ops/scripts/batches.sh .
CMD ["npm", "run", "start"]
FROM base as data-transport-layer
WORKDIR /opt/optimism/packages/data-transport-layer
COPY ./ops/scripts/dtl.sh .
......
ADDRESS_MANAGER_ADDRESS=
DEBUG=info*,error*,warn*,debug*
MAX_L1_TX_SIZE=90000
MIN_L1_TX_SIZE=32
MAX_TX_BATCH_COUNT=50
MAX_STATE_BATCH_COUNT=50
POLL_INTERVAL=500
NUM_CONFIRMATIONS=0
RESUBMISSION_TIMEOUT=1
FINALITY_CONFIRMATIONS=0
RUN_TX_BATCH_SUBMITTER=true
RUN_STATE_BATCH_SUBMITTER=true
MAX_BATCH_SUBMISSION_TIME=0
SAFE_MINIMUM_ETHER_BALANCE=0
CLEAR_PENDING_TXS=false
RETRIES=80
#!/bin/bash
set -e
RETRIES=${RETRIES:-40}
if [[ ! -z "$URL" ]]; then
# get the addrs from the URL provided
ADDRESSES=$(curl --fail --show-error --silent --retry-connrefused --retry $RETRIES --retry-delay 5 $URL)
# set the env
export ADDRESS_MANAGER_ADDRESS=$(echo $ADDRESSES | jq -r '.AddressManager')
fi
# waits for l2geth to be up
curl --fail \
--show-error \
--silent \
--retry-connrefused \
--retry $RETRIES \
--retry-delay 1 \
--output /dev/null \
$L2_NODE_WEB3_URL
# go
exec node ./exec/run-batch-submitter.js
......@@ -43,7 +43,6 @@ docker tag localhost:5000/ethereumoptimism/builder:latest ethereumoptimism/build
build deployer "ethereumoptimism/deployer:latest" "./ops/docker/Dockerfile.deployer" .
build dtl "ethereumoptimism/data-transport-layer:latest" "./ops/docker/Dockerfile.data-transport-layer" .
build batch_submitter "ethereumoptimism/batch-submitter:latest" "./ops/docker/Dockerfile.batch-submitter" .
build relayer "ethereumoptimism/message-relayer:latest" "./ops/docker/Dockerfile.message-relayer" .
build integration-tests "ethereumoptimism/integration-tests:latest" "./ops/docker/Dockerfile.integration-tests" .
......
# Environment
NODE_ENV=development
# Leave blank during local development
ETH_NETWORK_NAME=
# Logging & monitoring
DEBUG=info*,error*,warn*,debug*
RUN_METRICS_SERVER=
METRICS_PORT=
METRICS_HOSTNAME=
# Leave the SENTRY_DSN variable unset during local development
SENTRY_DSN=
SENTRY_TRACE_RATE=
USE_SENTRY=
L1_NODE_WEB3_URL=http://localhost:9545
L2_NODE_WEB3_URL=http://localhost:8545
MAX_L1_TX_SIZE=90000
MIN_L1_TX_SIZE=32
MAX_TX_BATCH_SIZE=50
MAX_STATE_BATCH_COUNT=2000
MAX_TX_BATCH_COUNT=250
MAX_BATCH_SUBMISSION_TIME=0
POLL_INTERVAL=15000
NUM_CONFIRMATIONS=0
RESUBMISSION_TIMEOUT=300 # in seconds
FINALITY_CONFIRMATIONS=0
RUN_TX_BATCH_SUBMITTER=true
RUN_STATE_BATCH_SUBMITTER=true
SAFE_MINIMUM_ETHER_BALANCE=0
CLEAR_PENDING_TXS=false
ADDRESS_MANAGER_ADDRESS=
USE_HARDHAT=
DEBUG_IMPERSONATE_SEQUENCER_ADDRESS=
DEBUG_IMPERSONATE_PROPOSER_ADDRESS=
# Optional gas settings
MAX_GAS_PRICE_IN_GWEI=200
GAS_RETRY_INCREMENT=5
GAS_THRESHOLD_IN_GWEI=100
SEQUENCER_PRIVATE_KEY=0xd2ab07f7c10ac88d5f86f1b4c1035d5195e81f27dbe62ad65e59cbf88205629b
module.exports = {
extends: '../../.eslintrc.js',
}
module.exports = {
...require('../../.prettierrc.js'),
};
\ No newline at end of file
This diff is collapsed.
(The MIT License)
Copyright 2020-2021 Optimism
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
[![codecov](https://codecov.io/gh/ethereum-optimism/optimism/branch/master/graph/badge.svg?token=0VTG7PG7YR&flag=batch-submitter)](https://codecov.io/gh/ethereum-optimism/optimism)
# Batch Submitter
Contains an executable batch submitter service which watches L1 and a local L2 node and submits batches to the
`CanonicalTransactionChain` & `StateCommitmentChain` based on its local information.
## Configuration
All configuration is done via environment variables. See all variables at [.env.example](.env.example); copy into a `.env` file before running.
## Building & Running
1. Make sure dependencies are installed just run `yarn` in the base directory
2. Build `yarn build`
3. Run `yarn start`
## Controlling log output verbosity
Before running, set the `DEBUG` environment variable to specify the verbosity level. It must be made up of comma-separated values of patterns to match in debug logs. Here's a few common options:
* `debug*` - Will match all debug statements -- very verbose
* `info*` - Will match all info statements -- less verbose, useful in most cases
* `warn*` - Will match all warnings -- recommended at a minimum
* `error*` - Will match all errors -- would not omit this
Examples:
* Everything but debug: `export DEBUG=info*,error*,warn*`
* Most verbose: `export DEBUG=info*,error*,warn*,debug*`
## Testing & linting
### Local
- Run unit tests with `yarn test`
- See lint errors with `yarn lint`; auto-fix with `yarn lint --fix`
### Submission
You may test a submission locally against a local Hardhat fork.
1. Follow the instructions [here](https://github.com/ethereum-optimism/hardhat) to run a Hardhat node.
2. Change the Batch Submitter `.env` field `L1_NODE_WEB3_URL` to the local Hardhat url. Depending on which network you are using, update `ADDRESS_MANAGER_ADDRESS` according to the [Regenesis repo](https://github.com/ethereum-optimism/regenesis).
3. Also check `L2_NODE_WEB3_URL` is correctly set and has transactions to submit.
3. Run `yarn build` to build your changes.
4. Start Batch Submitter with `yarn start`. It will automatically start submitting pending transactions from L2.
## Observability in production
When deploying Batch Submitter to production / a live ETH network, populate the environment variables `NODE_ENV` (`development`, `production`, or `test`) and `ETH_NETWORK_NAME` (`mainnet`, `kovan`, `goerli`). This enables Batch Submitter to capture more context in logs and metrics, and initializes [Sentry](https://docs.sentry.io/platforms/node/) to track errors.
#!/usr/bin/env node
const batchSubmitter = require('../dist/src/exec/run-batch-submitter')
batchSubmitter.run()
import '@nomiclabs/hardhat-waffle'
import { HardhatUserConfig } from 'hardhat/config'
import {
DEFAULT_ACCOUNTS_HARDHAT,
RUN_OVM_TEST_GAS,
} from './test/helpers/constants'
import '@nomiclabs/hardhat-ethers'
const config: HardhatUserConfig = {
networks: {
hardhat: {
accounts: DEFAULT_ACCOUNTS_HARDHAT,
blockGasLimit: RUN_OVM_TEST_GAS * 2,
},
},
mocha: {
timeout: 50000,
},
solidity: {
version: '0.7.0',
settings: {
optimizer: {
enabled: true,
runs: 200,
},
metadata: {
bytecodeHash: 'none',
},
outputSelection: {
'*': {
'*': ['metadata', 'storageLayout'],
},
},
},
},
}
export default config
const rootPath = __dirname
export { rootPath }
{
"private": true,
"name": "@eth-optimism/batch-submitter",
"version": "0.4.20",
"description": "[Optimism] Service for submitting transactions and transaction results",
"main": "dist/index",
"types": "dist/index",
"files": [
"dist/*"
],
"scripts": {
"start": "node ./exec/run-batch-submitter.js",
"build": "tsc -p ./tsconfig.build.json",
"clean": "rimraf cache/ dist/ ./tsconfig.build.tsbuildinfo",
"lint": "yarn lint:fix && yarn lint:check",
"pre-commit": "lint-staged",
"lint:fix": "yarn lint:check --fix",
"lint:check": "eslint . --max-warnings=0",
"test": "hardhat test --show-stack-traces",
"test:coverage": "nyc hardhat test && nyc merge .nyc_output coverage.json"
},
"keywords": [
"optimism",
"ethereum",
"sequencer",
"aggregator"
],
"homepage": "https://github.com/ethereum-optimism/optimism/tree/develop/packages/batch-submitter#readme",
"license": "MIT",
"author": "Optimism PBC",
"repository": {
"type": "git",
"url": "https://github.com/ethereum-optimism/optimism.git"
},
"dependencies": {
"@eth-optimism/common-ts": "0.2.1",
"@eth-optimism/contracts": "0.5.15",
"@eth-optimism/core-utils": "0.8.0",
"@eth-optimism/sdk": "^0.2.3",
"@eth-optimism/ynatm": "^0.2.2",
"@ethersproject/abstract-provider": "^5.5.1",
"@ethersproject/providers": "^5.5.3",
"@sentry/node": "^6.3.1",
"bcfg": "^0.1.6",
"bluebird": "^3.7.2",
"dotenv": "^10.0.0",
"ethers": "^5.5.4",
"old-contracts": "npm:@eth-optimism/contracts@^0.0.2-alpha.7",
"prom-client": "^13.1.0"
},
"devDependencies": {
"@eth-optimism/smock": "1.1.10",
"@nomiclabs/hardhat-ethers": "^2.0.2",
"@nomiclabs/hardhat-waffle": "^2.0.1",
"@types/bluebird": "^3.5.34",
"@types/chai": "^4.2.18",
"@types/mocha": "^8.2.2",
"@types/node": "^15.12.2",
"@types/sinon": "^9.0.10",
"@types/sinon-chai": "^3.2.5",
"@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0",
"babel-eslint": "^10.1.0",
"chai": "^4.3.4",
"eslint": "^7.27.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-jsdoc": "^35.1.2",
"eslint-plugin-prefer-arrow": "^1.2.3",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-unicorn": "^32.0.1",
"ethereum-waffle": "^3.3.0",
"hardhat": "^2.3.0",
"lint-staged": "11.0.0",
"mocha": "^8.4.0",
"prettier": "^2.3.1",
"rimraf": "^3.0.2",
"sinon": "^9.2.4",
"sinon-chai": "^3.5.0",
"typescript": "^4.3.5"
},
"resolutions": {
"ganache-core": "^2.13.2",
"**/@sentry/node": "^6.2.5"
},
"publishConfig": {
"access": "public"
}
}
/* External Imports */
import {
Contract,
Signer,
utils,
providers,
PopulatedTransaction,
} from 'ethers'
import {
TransactionReceipt,
TransactionResponse,
} from '@ethersproject/abstract-provider'
import { Gauge, Histogram, Counter } from 'prom-client'
import { RollupInfo, sleep } from '@eth-optimism/core-utils'
import { Logger, Metrics } from '@eth-optimism/common-ts'
import { getContractFactory } from 'old-contracts'
/* Internal Imports */
import { TxSubmissionHooks } from '..'
export interface BlockRange {
start: number
end: number
}
interface BatchSubmitterMetrics {
batchSubmitterETHBalance: Gauge<string>
batchSizeInBytes: Histogram<string>
numTxPerBatch: Histogram<string>
submissionTimestamp: Histogram<string>
submissionGasUsed: Histogram<string>
batchesSubmitted: Counter<string>
failedSubmissions: Counter<string>
malformedBatches: Counter<string>
batchTxBuildTime: Gauge<string>
}
export abstract class BatchSubmitter {
protected rollupInfo: RollupInfo
protected chainContract: Contract
protected l2ChainId: number
protected syncing: boolean
protected lastBatchSubmissionTimestamp: number = 0
protected metrics: BatchSubmitterMetrics
constructor(
readonly signer: Signer,
readonly l2Provider: providers.StaticJsonRpcProvider,
readonly minTxSize: number,
readonly maxTxSize: number,
readonly maxBatchSize: number,
readonly maxBatchSubmissionTime: number,
readonly numConfirmations: number,
readonly resubmissionTimeout: number,
readonly finalityConfirmations: number,
readonly addressManagerAddress: string,
readonly minBalanceEther: number,
readonly blockOffset: number,
readonly logger: Logger,
readonly defaultMetrics: Metrics
) {
this.metrics = this._registerMetrics(defaultMetrics)
}
public abstract _submitBatch(
startBlock: number,
endBlock: number
): Promise<TransactionReceipt>
public abstract _onSync(): Promise<TransactionReceipt>
public abstract _getBatchStartAndEnd(): Promise<BlockRange>
public abstract _updateChainInfo(): Promise<void>
public async submitNextBatch(): Promise<TransactionReceipt> {
if (typeof this.l2ChainId === 'undefined') {
this.l2ChainId = await this._getL2ChainId()
}
await this._updateChainInfo()
if (!(await this._hasEnoughETHToCoverGasCosts())) {
await sleep(this.resubmissionTimeout)
return
}
this.logger.info('Readying to submit next batch...', {
l2ChainId: this.l2ChainId,
batchSubmitterAddress: await this.signer.getAddress(),
})
if (this.syncing === true) {
this.logger.info(
'Syncing mode enabled! Skipping batch submission and clearing queue...'
)
return this._onSync()
}
const range = await this._getBatchStartAndEnd()
if (!range) {
return
}
return this._submitBatch(range.start, range.end)
}
protected async _hasEnoughETHToCoverGasCosts(): Promise<boolean> {
const address = await this.signer.getAddress()
const balance = await this.signer.getBalance()
const ether = utils.formatEther(balance)
const num = parseFloat(ether)
this.logger.info('Checked balance', {
address,
ether,
})
this.metrics.batchSubmitterETHBalance.set(num)
if (num < this.minBalanceEther) {
this.logger.fatal('Current balance lower than min safe balance', {
current: num,
safeBalance: this.minBalanceEther,
})
return false
}
return true
}
protected async _getRollupInfo(): Promise<RollupInfo> {
return this.l2Provider.send('rollup_getInfo', [])
}
protected async _getL2ChainId(): Promise<number> {
return this.l2Provider.send('eth_chainId', [])
}
protected async _getChainAddresses(): Promise<{
ctcAddress: string
sccAddress: string
}> {
const addressManager = (
await getContractFactory('Lib_AddressManager', this.signer)
).attach(this.addressManagerAddress)
const sccAddress = await addressManager.getAddress('StateCommitmentChain')
const ctcAddress = await addressManager.getAddress(
'CanonicalTransactionChain'
)
return {
ctcAddress,
sccAddress,
}
}
protected _shouldSubmitBatch(batchSizeInBytes: number): boolean {
const currentTimestamp = Date.now()
if (batchSizeInBytes < this.minTxSize) {
const timeSinceLastSubmission =
currentTimestamp - this.lastBatchSubmissionTimestamp
if (timeSinceLastSubmission < this.maxBatchSubmissionTime) {
this.logger.info(
'Skipping batch submission. Batch too small & max submission timeout not reached.',
{
batchSizeInBytes,
timeSinceLastSubmission,
maxBatchSubmissionTime: this.maxBatchSubmissionTime,
minTxSize: this.minTxSize,
lastBatchSubmissionTimestamp: this.lastBatchSubmissionTimestamp,
currentTimestamp,
}
)
return false
}
this.logger.info('Timeout reached, proceeding with batch submission.', {
batchSizeInBytes,
timeSinceLastSubmission,
maxBatchSubmissionTime: this.maxBatchSubmissionTime,
lastBatchSubmissionTimestamp: this.lastBatchSubmissionTimestamp,
currentTimestamp,
})
this.metrics.batchSizeInBytes.observe(batchSizeInBytes)
return true
}
this.logger.info(
'Sufficient batch size, proceeding with batch submission.',
{
batchSizeInBytes,
lastBatchSubmissionTimestamp: this.lastBatchSubmissionTimestamp,
currentTimestamp,
}
)
this.metrics.batchSizeInBytes.observe(batchSizeInBytes)
return true
}
protected _makeHooks(txName: string): TxSubmissionHooks {
return {
beforeSendTransaction: (tx: PopulatedTransaction) => {
this.logger.info(`Submitting ${txName} transaction`, {
gasPrice: tx.gasPrice,
maxFeePerGas: tx.maxFeePerGas,
maxPriorityFeePerGas: tx.maxPriorityFeePerGas,
gasLimit: tx.gasLimit,
nonce: tx.nonce,
contractAddr: this.chainContract.address,
})
},
onTransactionResponse: (txResponse: TransactionResponse) => {
this.logger.info(`Submitted ${txName} transaction`, {
txHash: txResponse.hash,
from: txResponse.from,
})
this.logger.debug(`${txName} transaction data`, {
data: txResponse.data,
})
},
}
}
protected async _submitAndLogTx(
submitTransaction: () => Promise<TransactionReceipt>,
successMessage: string
): Promise<TransactionReceipt> {
this.lastBatchSubmissionTimestamp = Date.now()
this.logger.debug('Submitting transaction & waiting for receipt...')
let receipt: TransactionReceipt
try {
receipt = await submitTransaction()
} catch (err) {
this.metrics.failedSubmissions.inc()
if (err.reason) {
this.logger.error(`Transaction invalid: ${err.reason}, aborting`, {
message: err.toString(),
stack: err.stack,
code: err.code,
})
return
}
this.logger.error('Encountered error at submission, aborting', {
message: err.toString(),
stack: err.stack,
code: err.code,
})
return
}
this.logger.info('Received transaction receipt', { receipt })
this.logger.info(successMessage)
this.metrics.batchesSubmitted.inc()
this.metrics.submissionGasUsed.observe(receipt.gasUsed.toNumber())
this.metrics.submissionTimestamp.observe(Date.now())
return receipt
}
private _registerMetrics(metrics: Metrics): BatchSubmitterMetrics {
metrics.registry.clear()
return {
batchSubmitterETHBalance: new metrics.client.Gauge({
name: 'batch_submitter_eth_balance',
help: 'ETH balance of the batch submitter',
registers: [metrics.registry],
}),
batchSizeInBytes: new metrics.client.Histogram({
name: 'batch_size_in_bytes',
help: 'Size of batches in bytes',
registers: [metrics.registry],
}),
numTxPerBatch: new metrics.client.Histogram({
name: 'num_txs_per_batch',
help: 'Number of transactions in each batch',
registers: [metrics.registry],
}),
submissionTimestamp: new metrics.client.Histogram({
name: 'submission_timestamp',
help: 'Timestamp of each batch submitter submission',
registers: [metrics.registry],
}),
submissionGasUsed: new metrics.client.Histogram({
name: 'submission_gash_used',
help: 'Gas used to submit each batch',
registers: [metrics.registry],
}),
batchesSubmitted: new metrics.client.Counter({
name: 'batches_submitted',
help: 'Count of batches submitted',
registers: [metrics.registry],
}),
failedSubmissions: new metrics.client.Counter({
name: 'failed_submissions',
help: 'Count of failed batch submissions',
registers: [metrics.registry],
}),
malformedBatches: new metrics.client.Counter({
name: 'malformed_batches',
help: 'Count of malformed batches',
registers: [metrics.registry],
}),
batchTxBuildTime: new metrics.client.Gauge({
name: 'batch_tx_build_time',
help: 'Time to construct batch transaction',
registers: [metrics.registry],
}),
}
}
}
export * from './batch-submitter'
export * from './tx-batch-submitter'
export * from './state-batch-submitter'
export const TX_BATCH_SUBMITTER_LOG_TAG = 'oe:batch_submitter:tx_chain'
export const STATE_BATCH_SUBMITTER_LOG_TAG = 'oe:batch_submitter:state_chain'
/* External Imports */
import { performance } from 'perf_hooks'
import { Promise as bPromise } from 'bluebird'
import { Contract, Signer, providers } from 'ethers'
import { TransactionReceipt } from '@ethersproject/abstract-provider'
import { getContractFactory } from 'old-contracts'
import {
L2Block,
RollupInfo,
Bytes32,
remove0x,
} from '@eth-optimism/core-utils'
import { Logger, Metrics } from '@eth-optimism/common-ts'
/* Internal Imports */
import { TransactionSubmitter } from '../utils'
import { BlockRange, BatchSubmitter } from '.'
export class StateBatchSubmitter extends BatchSubmitter {
// TODO: Change this so that we calculate start = scc.totalElements() and end = ctc.totalElements()!
// Not based on the length of the L2 chain -- that is only used in the batch submitter
// Note this means we've got to change the state / end calc logic
protected l2ChainId: number
protected syncing: boolean
protected ctcContract: Contract
private fraudSubmissionAddress: string
private transactionSubmitter: TransactionSubmitter
constructor(
signer: Signer,
l2Provider: providers.StaticJsonRpcProvider,
minTxSize: number,
maxTxSize: number,
maxBatchSize: number,
maxBatchSubmissionTime: number,
numConfirmations: number,
resubmissionTimeout: number,
finalityConfirmations: number,
addressManagerAddress: string,
minBalanceEther: number,
transactionSubmitter: TransactionSubmitter,
blockOffset: number,
logger: Logger,
metrics: Metrics,
fraudSubmissionAddress: string
) {
super(
signer,
l2Provider,
minTxSize,
maxTxSize,
maxBatchSize,
maxBatchSubmissionTime,
numConfirmations,
resubmissionTimeout,
finalityConfirmations,
addressManagerAddress,
minBalanceEther,
blockOffset,
logger,
metrics
)
this.fraudSubmissionAddress = fraudSubmissionAddress
this.transactionSubmitter = transactionSubmitter
}
/*****************************
* Batch Submitter Overrides *
****************************/
public async _updateChainInfo(): Promise<void> {
const info: RollupInfo = await this._getRollupInfo()
if (info.mode === 'verifier') {
this.logger.error(
'Verifier mode enabled! Batch submitter only compatible with sequencer mode'
)
process.exit(1)
}
this.syncing = info.syncing
const addrs = await this._getChainAddresses()
const sccAddress = addrs.sccAddress
const ctcAddress = addrs.ctcAddress
if (
typeof this.chainContract !== 'undefined' &&
sccAddress === this.chainContract.address &&
ctcAddress === this.ctcContract.address
) {
this.logger.debug('Chain contract already initialized', {
sccAddress,
ctcAddress,
})
return
}
this.chainContract = (
await getContractFactory('OVM_StateCommitmentChain', this.signer)
).attach(sccAddress)
this.ctcContract = (
await getContractFactory('OVM_CanonicalTransactionChain', this.signer)
).attach(ctcAddress)
this.logger.info('Connected Optimism contracts', {
stateCommitmentChain: this.chainContract.address,
canonicalTransactionChain: this.ctcContract.address,
})
return
}
public async _onSync(): Promise<TransactionReceipt> {
this.logger.info('Syncing mode enabled! Skipping state batch submission...')
return
}
public async _getBatchStartAndEnd(): Promise<BlockRange> {
this.logger.info('Getting batch start and end for state batch submitter...')
const startBlock: number =
(await this.chainContract.getTotalElements()).toNumber() +
this.blockOffset
this.logger.info('Retrieved start block number from SCC', {
startBlock,
})
// We will submit state roots for txs which have been in the tx chain for a while.
const totalElements: number =
(await this.ctcContract.getTotalElements()).toNumber() + this.blockOffset
this.logger.info('Retrieved total elements from CTC', {
totalElements,
})
const endBlock: number = Math.min(
startBlock + this.maxBatchSize,
totalElements
)
if (startBlock >= endBlock) {
if (startBlock > endBlock) {
this.logger.error(
'State commitment chain is larger than transaction chain. This should never happen!'
)
}
this.logger.info(
'No state commitments to submit. Skipping batch submission...'
)
return
}
return {
start: startBlock,
end: endBlock,
}
}
public async _submitBatch(
startBlock: number,
endBlock: number
): Promise<TransactionReceipt> {
const batchTxBuildStart = performance.now()
const batch = await this._generateStateCommitmentBatch(startBlock, endBlock)
const calldata = this.chainContract.interface.encodeFunctionData(
'appendStateBatch',
[batch, startBlock]
)
const batchSizeInBytes = remove0x(calldata).length / 2
this.logger.debug('State batch generated', {
batchSizeInBytes,
calldata,
})
if (!this._shouldSubmitBatch(batchSizeInBytes)) {
return
}
const batchTxBuildEnd = performance.now()
this.metrics.batchTxBuildTime.set(batchTxBuildEnd - batchTxBuildStart)
const offsetStartsAtIndex = startBlock - this.blockOffset
this.logger.debug('Submitting batch.', { calldata })
// Generate the transaction we will repeatedly submit
const nonce = await this.signer.getTransactionCount()
const tx = await this.chainContract.populateTransaction.appendStateBatch(
batch,
offsetStartsAtIndex,
{ nonce }
)
const submitTransaction = (): Promise<TransactionReceipt> => {
return this.transactionSubmitter.submitTransaction(
tx,
this._makeHooks('appendStateBatch')
)
}
return this._submitAndLogTx(
submitTransaction,
'Submitted state root batch!'
)
}
/*********************
* Private Functions *
********************/
private async _generateStateCommitmentBatch(
startBlock: number,
endBlock: number
): Promise<Bytes32[]> {
const blockRange = endBlock - startBlock
const batch: Bytes32[] = await bPromise.map(
[...Array(blockRange).keys()],
async (i: number) => {
this.logger.debug('Fetching L2BatchElement', {
blockNo: startBlock + i,
})
const block = (await this.l2Provider.getBlockWithTransactions(
startBlock + i
)) as L2Block
const blockTx = block.transactions[0]
if (blockTx.from === this.fraudSubmissionAddress) {
this.logger.warn('Found transaction from fraud submission address', {
txHash: blockTx.hash,
fraudSubmissionAddress: this.fraudSubmissionAddress,
})
this.fraudSubmissionAddress = 'no fraud'
return '0xbad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1'
}
return block.stateRoot
},
{ concurrency: 100 }
)
let tx = this.chainContract.interface.encodeFunctionData(
'appendStateBatch',
[batch, startBlock]
)
while (remove0x(tx).length / 2 > this.maxTxSize) {
batch.splice(Math.ceil((batch.length * 2) / 3)) // Delete 1/3rd of all of the batch elements
this.logger.debug('Splicing batch...', {
batchSizeInBytes: tx.length / 2,
})
tx = this.chainContract.interface.encodeFunctionData('appendStateBatch', [
batch,
startBlock,
])
}
this.logger.info('Generated state commitment batch', {
batch, // list of stateRoots
})
return batch
}
}
export * from './batch-submitter'
export * from './utils'
export * from './transaction-chain-contract'
/* External Imports */
import { Contract, ethers } from 'ethers'
import {
TransactionResponse,
TransactionRequest,
} from '@ethersproject/abstract-provider'
import {
AppendSequencerBatchParams,
BatchContext,
encodeAppendSequencerBatch,
sequencerBatch,
} from '@eth-optimism/core-utils'
export { encodeAppendSequencerBatch, BatchContext, AppendSequencerBatchParams }
/*
* OVM_CanonicalTransactionChainContract is a wrapper around a normal Ethers contract
* where the `appendSequencerBatch(...)` function uses a specialized encoding for improved efficiency.
*/
export class CanonicalTransactionChainContract extends Contract {
public customPopulateTransaction = {
appendSequencerBatch: async (
batch: AppendSequencerBatchParams
): Promise<ethers.PopulatedTransaction> => {
const nonce = await this.signer.getTransactionCount()
const to = this.address
const data = getEncodedCalldata(batch)
const gasLimit = await this.signer.provider.estimateGas({
to,
from: await this.signer.getAddress(),
data,
})
return {
nonce,
to,
data,
gasLimit,
}
},
}
public async appendSequencerBatch(
batch: AppendSequencerBatchParams,
options?: TransactionRequest
): Promise<TransactionResponse> {
return appendSequencerBatch(this, batch, options)
}
}
/**********************
* Internal Functions *
*********************/
const appendSequencerBatch = async (
OVM_CanonicalTransactionChain: Contract,
batch: AppendSequencerBatchParams,
options?: TransactionRequest
): Promise<TransactionResponse> => {
return OVM_CanonicalTransactionChain.signer.sendTransaction({
to: OVM_CanonicalTransactionChain.address,
data: getEncodedCalldata(batch),
...options,
})
}
const getEncodedCalldata = (params: AppendSequencerBatchParams): string => {
return sequencerBatch.encode(params)
}
import { Signer, ethers, PopulatedTransaction } from 'ethers'
import {
TransactionReceipt,
TransactionResponse,
} from '@ethersproject/abstract-provider'
import * as ynatm from '@eth-optimism/ynatm'
export interface ResubmissionConfig {
resubmissionTimeout: number
minGasPriceInGwei: number
maxGasPriceInGwei: number
gasRetryIncrement: number
}
export type SubmitTransactionFn = (
tx: PopulatedTransaction
) => Promise<TransactionReceipt>
export interface TxSubmissionHooks {
beforeSendTransaction: (tx: PopulatedTransaction) => void
onTransactionResponse: (txResponse: TransactionResponse) => void
}
const getGasPriceInGwei = async (signer: Signer): Promise<number> => {
return parseInt(
ethers.utils.formatUnits(await signer.getGasPrice(), 'gwei'),
10
)
}
export const ynatmRejectOn = (e) => {
// taken almost verbatim from the readme,
// see https://github.com/ethereum-optimism/ynatm.
// immediately rejects on reverts and nonce errors
const errMsg = e.toString().toLowerCase()
const conditions = ['revert', 'nonce']
for (const cond of conditions) {
if (errMsg.includes(cond)) {
return true
}
}
return false
}
export const submitTransactionWithYNATM = async (
tx: PopulatedTransaction,
signer: Signer,
config: ResubmissionConfig,
numConfirmations: number,
hooks: TxSubmissionHooks
): Promise<TransactionReceipt> => {
const sendTxAndWaitForReceipt = async (
gasPrice
): Promise<TransactionReceipt> => {
const fullTx = {
...tx,
gasPrice,
}
hooks.beforeSendTransaction(fullTx)
const txResponse = await signer.sendTransaction(fullTx)
hooks.onTransactionResponse(txResponse)
return signer.provider.waitForTransaction(txResponse.hash, numConfirmations)
}
const minGasPrice = await getGasPriceInGwei(signer)
const receipt = await ynatm.send({
sendTransactionFunction: sendTxAndWaitForReceipt,
minGasPrice: ynatm.toGwei(minGasPrice),
maxGasPrice: ynatm.toGwei(config.maxGasPriceInGwei),
gasPriceScalingFunction: ynatm.LINEAR(config.gasRetryIncrement),
delay: config.resubmissionTimeout,
rejectImmediatelyOnCondition: ynatmRejectOn,
})
return receipt
}
export interface TransactionSubmitter {
submitTransaction(
tx: PopulatedTransaction,
hooks?: TxSubmissionHooks
): Promise<TransactionReceipt>
}
export class YnatmTransactionSubmitter implements TransactionSubmitter {
constructor(
readonly signer: Signer,
readonly ynatmConfig: ResubmissionConfig,
readonly numConfirmations: number
) {}
public async submitTransaction(
tx: PopulatedTransaction,
hooks?: TxSubmissionHooks
): Promise<TransactionReceipt> {
if (!hooks) {
hooks = {
beforeSendTransaction: () => undefined,
onTransactionResponse: () => undefined,
}
}
return submitTransactionWithYNATM(
tx,
this.signer,
this.ynatmConfig,
this.numConfirmations,
hooks
)
}
}
/* External Imports */
import { defaultAccounts } from 'ethereum-waffle'
export const FORCE_INCLUSION_PERIOD_SECONDS = 600
export const DEFAULT_ACCOUNTS = defaultAccounts
export const DEFAULT_ACCOUNTS_HARDHAT = defaultAccounts.map((account) => {
return {
balance: account.balance,
privateKey: account.secretKey,
}
})
export const OVM_TX_GAS_LIMIT = 10_000_000
export const RUN_OVM_TEST_GAS = 20_000_000
/* External Imports */
import { keccak256 } from 'ethers/lib/utils'
export const DUMMY_BYTECODE = '0x123412341234'
export const DUMMY_BYTECODE_BYTELEN = 6
export const DUMMY_BYTECODE_HASH = keccak256(DUMMY_BYTECODE)
/* External Imports */
import { ethers } from 'ethers'
export const DUMMY_BYTES32: string[] = Array.from(
{
length: 10,
},
(_, i) => {
return ethers.utils.keccak256(`0x0${i}`)
}
)
export * from './bytes32'
export * from './bytecode'
export * from './dummy'
export * from './constants'
export * from './resolver'
/* External Imports */
import { ethers } from 'hardhat'
import { Contract } from 'ethers'
import { getContractFactory as ctFactory } from 'old-contracts'
export const getContractFactory = async (contract: string) =>
ctFactory(contract, (await ethers.getSigners())[0])
export const setProxyTarget = async (
AddressManager: Contract,
name: string,
target: Contract
): Promise<void> => {
const SimpleProxy: Contract = await (
await getContractFactory('Helper_SimpleProxy')
).deploy()
await SimpleProxy.setTarget(target.address)
await AddressManager.setAddress(name, SimpleProxy.address)
}
export const makeAddressManager = async (): Promise<Contract> => {
return (await getContractFactory('Lib_AddressManager')).deploy()
}
/* External Imports */
import chai = require('chai')
import sinonChai from 'sinon-chai'
import Mocha from 'mocha'
const should = chai.should()
const expect = chai.expect
chai.use(sinonChai)
export { should, expect, chai, Mocha }
import { ethers, BigNumber, Signer } from 'ethers'
import {
TransactionReceipt,
TransactionResponse,
} from '@ethersproject/abstract-provider'
import { expect } from '../setup'
import { submitTransactionWithYNATM } from '../../src/utils/tx-submission'
import { ResubmissionConfig } from '../../src'
const nullFunction = () => undefined
const nullHooks = {
beforeSendTransaction: nullFunction,
onTransactionResponse: nullFunction,
}
describe('submitTransactionWithYNATM', async () => {
it('calls sendTransaction, waitForTransaction, and hooks with correct inputs', async () => {
const called = {
sendTransaction: false,
waitForTransaction: false,
beforeSendTransaction: false,
onTransactionResponse: false,
}
const dummyHash = 'dummy hash'
const numConfirmations = 3
const tx = {
data: 'we here though',
} as ethers.PopulatedTransaction
const sendTransaction = async (
_tx: ethers.PopulatedTransaction
): Promise<TransactionResponse> => {
called.sendTransaction = true
expect(_tx.data).to.equal(tx.data)
return {
hash: dummyHash,
} as TransactionResponse
}
const waitForTransaction = async (
hash: string,
_numConfirmations: number
): Promise<TransactionReceipt> => {
called.waitForTransaction = true
expect(hash).to.equal(dummyHash)
expect(_numConfirmations).to.equal(numConfirmations)
return {
to: '',
from: '',
status: 1,
} as TransactionReceipt
}
const signer = {
getGasPrice: async () => ethers.BigNumber.from(0),
sendTransaction,
provider: {
waitForTransaction,
},
} as Signer
const hooks = {
beforeSendTransaction: (submittingTx: ethers.PopulatedTransaction) => {
called.beforeSendTransaction = true
expect(submittingTx.data).to.equal(tx.data)
},
onTransactionResponse: (txResponse: TransactionResponse) => {
called.onTransactionResponse = true
expect(txResponse.hash).to.equal(dummyHash)
},
}
const config: ResubmissionConfig = {
resubmissionTimeout: 1000,
minGasPriceInGwei: 0,
maxGasPriceInGwei: 0,
gasRetryIncrement: 1,
}
await submitTransactionWithYNATM(
tx,
signer,
config,
numConfirmations,
hooks
)
expect(called.sendTransaction).to.be.true
expect(called.waitForTransaction).to.be.true
expect(called.beforeSendTransaction).to.be.true
expect(called.onTransactionResponse).to.be.true
})
it('repeatedly increases the gas limit of the transaction when wait takes too long', async () => {
// Make transactions take longer to be included
// than our resubmission timeout
const resubmissionTimeout = 100
const txReceiptDelay = resubmissionTimeout * 3
let lastGasPrice = BigNumber.from(0)
// Create a transaction which has a gas price that we will watch increment
const tx = {
gasPrice: lastGasPrice.add(1),
data: 'hello world!',
} as ethers.PopulatedTransaction
const sendTransaction = async (
_tx: ethers.PopulatedTransaction
): Promise<TransactionResponse> => {
// Ensure the gas price is always increasing
expect(_tx.gasPrice > lastGasPrice).to.be.true
lastGasPrice = _tx.gasPrice
return {
hash: 'dummy hash',
} as TransactionResponse
}
const waitForTransaction = async (): Promise<TransactionReceipt> => {
await new Promise((r) => setTimeout(r, txReceiptDelay))
return {} as TransactionReceipt
}
const signer = {
getGasPrice: async () => ethers.BigNumber.from(0),
sendTransaction,
provider: {
waitForTransaction: waitForTransaction as any,
},
} as Signer
const config: ResubmissionConfig = {
resubmissionTimeout,
minGasPriceInGwei: 0,
maxGasPriceInGwei: 1000,
gasRetryIncrement: 1,
}
await submitTransactionWithYNATM(tx, signer, config, 0, nullHooks)
})
it('should immediately reject if a nonce error is encountered', async () => {
const tx = {
gasPrice: BigNumber.from(1),
data: 'hello world!',
} as ethers.PopulatedTransaction
let txCount = 0
const waitForTransaction = async (): Promise<TransactionReceipt> => {
return {} as TransactionReceipt
}
const sendTransaction = async () => {
txCount++
throw new Error('Transaction nonce is too low.')
}
const signer = {
getGasPrice: async () => BigNumber.from(1),
sendTransaction: sendTransaction as any,
provider: {
waitForTransaction: waitForTransaction as any,
},
} as Signer
const config: ResubmissionConfig = {
resubmissionTimeout: 100,
minGasPriceInGwei: 0,
maxGasPriceInGwei: 1000,
gasRetryIncrement: 1,
}
try {
await submitTransactionWithYNATM(tx, signer, config, 0, nullHooks)
} catch (e) {
expect(txCount).to.equal(1)
return
}
expect.fail('Expected an error.')
})
})
{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"outDir": "./dist"
},
"include": [
"src/**/*"
]
}
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"resolveJsonModule": true
}
}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment