Commit 07d7010d authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

packages: remove unmaintained javascript (#10613)

* packages: remove unmaintained javascript

This commit removes the following packages from the monorepo:
- `common-ts`
- `contracts-ts`
- `core-utils`
- `fee-estimation`
- `web3js-plugin`

These packages are not maintained. `chain-mon` and the `sdk` still
exist but are pending deprecation.

* readme: update

* mergify: leftover references
parent f2e5a7a5
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
"changelog": ["@changesets/changelog-github", { "repo": "ethereum-optimism/optimism" }], "changelog": ["@changesets/changelog-github", { "repo": "ethereum-optimism/optimism" }],
"commit": false, "commit": false,
"fixed": [], "fixed": [],
"linked": [["@eth-optimism/contracts-bedrock", "@eth-optimism/contracts-ts"]], "linked": [],
"access": "public", "access": "public",
"baseBranch": "develop", "baseBranch": "develop",
"updateInternalDependencies": "patch", "updateInternalDependencies": "patch",
......
...@@ -725,28 +725,6 @@ jobs: ...@@ -725,28 +725,6 @@ jobs:
name: Upload coverage name: Upload coverage
command: codecov --verbose --clean --flags <<parameters.coverage_flag>> command: codecov --verbose --clean --flags <<parameters.coverage_flag>>
contracts-ts-tests:
docker:
- image: <<pipeline.parameters.ci_builder_image>>
resource_class: medium
steps:
- checkout
- attach_workspace: { at: "." }
- restore_cache:
name: Restore pnpm Package Cache
keys:
- pnpm-packages-v2-{{ checksum "pnpm.lock.yaml" }}
- check-changed:
patterns: sdk,contracts-bedrock,contracts
# populate node modules from the cache
- run:
name: Install dependencies
command: pnpm install:ci
- run:
name: Check generated and build
command: pnpm generate:check
working_directory: packages/contracts-ts
sdk-next-tests: sdk-next-tests:
docker: docker:
- image: <<pipeline.parameters.ci_builder_image>> - image: <<pipeline.parameters.ci_builder_image>>
...@@ -863,14 +841,6 @@ jobs: ...@@ -863,14 +841,6 @@ jobs:
- pnpm-packages-v2-{{ checksum "pnpm-lock.yaml" }} - pnpm-packages-v2-{{ checksum "pnpm-lock.yaml" }}
- check-changed: - check-changed:
patterns: packages patterns: packages
- run:
name: Check common-ts
command: npx depcheck
working_directory: packages/common-ts
- run:
name: Check core-utils
command: npx depcheck
working_directory: packages/core-utils
- run: - run:
name: Check sdk name: Check sdk
command: npx depcheck command: npx depcheck
...@@ -1675,18 +1645,6 @@ workflows: ...@@ -1675,18 +1645,6 @@ workflows:
jobs: jobs:
- pnpm-monorepo: - pnpm-monorepo:
name: pnpm-monorepo name: pnpm-monorepo
- js-lint-test:
name: common-ts-tests
coverage_flag: common-ts-tests
package_name: common-ts
requires:
- pnpm-monorepo
- js-lint-test:
name: core-utils-tests
coverage_flag: core-utils-tests
package_name: core-utils
requires:
- pnpm-monorepo
- contracts-bedrock-tests: - contracts-bedrock-tests:
requires: requires:
- pnpm-monorepo - pnpm-monorepo
...@@ -1701,21 +1659,14 @@ workflows: ...@@ -1701,21 +1659,14 @@ workflows:
name: chain-mon-tests name: chain-mon-tests
coverage_flag: chain-mon-tests coverage_flag: chain-mon-tests
package_name: chain-mon package_name: chain-mon
dependencies: "(common-ts|contracts-bedrock|core-utils|sdk)" dependencies: "(contracts-bedrock|sdk)"
requires:
- pnpm-monorepo
- js-lint-test:
name: contracts-ts-tests
coverage_flag: contracts-ts-tests
package_name: contracts-ts
dependencies: '(contracts-bedrock|contracts-ts)'
requires: requires:
- pnpm-monorepo - pnpm-monorepo
- js-lint-test: - js-lint-test:
name: sdk-tests name: sdk-tests
coverage_flag: sdk-tests coverage_flag: sdk-tests
package_name: sdk package_name: sdk
dependencies: "(contracts-bedrock|core-utils)" dependencies: "contracts-bedrock"
requires: requires:
- pnpm-monorepo - pnpm-monorepo
- depcheck: - depcheck:
......
# Packages # Packages
/packages/chain-mon @ethereum-optimism/security-reviewers /packages/chain-mon @ethereum-optimism/security-reviewers
/packages/chain-mon/internal/balance-mon @ethereum-optimism/infra-reviewers /packages/chain-mon/internal/balance-mon @ethereum-optimism/infra-reviewers
/packages/common-ts @ethereum-optimism/typescript-reviewers
/packages/contracts-bedrock @ethereum-optimism/contract-reviewers /packages/contracts-bedrock @ethereum-optimism/contract-reviewers
/packages/core-utils @ethereum-optimism/legacy-reviewers
/packages/sdk @ethereum-optimism/devxpod /packages/sdk @ethereum-optimism/devxpod
# Bedrock codebases # Bedrock codebases
......
...@@ -221,17 +221,6 @@ pull_request_rules: ...@@ -221,17 +221,6 @@ pull_request_rules:
label: label:
add: add:
- A-pkg-chain-mon - A-pkg-chain-mon
- name: Add A-pkg-common-ts label and ecopod reviewers
conditions:
- 'files~=^packages/common-ts/'
- '#label<5'
actions:
label:
add:
- A-pkg-common-ts
request_reviews:
users:
- roninjin10
- name: Add A-pkg-contracts-bedrock label - name: Add A-pkg-contracts-bedrock label
conditions: conditions:
- 'files~=^packages/contracts-bedrock/' - 'files~=^packages/contracts-bedrock/'
...@@ -239,30 +228,6 @@ pull_request_rules: ...@@ -239,30 +228,6 @@ pull_request_rules:
label: label:
add: add:
- A-pkg-contracts-bedrock - A-pkg-contracts-bedrock
- name: Add A-pkg-contracts-ts label
conditions:
- 'files~=^packages/contracts-ts/'
- '#label<5'
actions:
label:
add:
- A-pkg-contracts-ts
- name: Add A-pkg-core-utils label
conditions:
- 'files~=^packages/core-utils/'
- '#label<5'
actions:
label:
add:
- A-pkg-core-utils
- name: Add A-pkg-fee-estimation label
conditions:
- 'files~=^packages/fee-estimation/'
- '#label<5'
actions:
label:
add:
- A-pkg-fee-estimation
- name: Add A-pkg-sdk label and ecopod reviewers - name: Add A-pkg-sdk label and ecopod reviewers
conditions: conditions:
- 'files~=^packages/sdk/' - 'files~=^packages/sdk/'
...@@ -274,14 +239,6 @@ pull_request_rules: ...@@ -274,14 +239,6 @@ pull_request_rules:
request_reviews: request_reviews:
users: users:
- roninjin10 - roninjin10
- name: Add A-pkg-web3js-plugin label
conditions:
- 'files~=^packages/web3js-plugin/'
- '#label<5'
actions:
label:
add:
- A-pkg-web3js-plugin
- name: Add A-proxyd label - name: Add A-proxyd label
conditions: conditions:
- 'files~=^proxyd/' - 'files~=^proxyd/'
......
...@@ -4,18 +4,6 @@ ...@@ -4,18 +4,6 @@
"editor.formatOnSave": true "editor.formatOnSave": true
}, },
"eslint.workingDirectories": [ "eslint.workingDirectories": [
{
"directory": "packages/core-utils",
"changeProcessCWD": true
},
{
"directory": "packages/common-ts",
"changeProcessCWD": true
},
{
"directory": "packages/contracts",
"changeProcessCWD": true
},
{ {
"directory": "packages/chain-mon", "directory": "packages/chain-mon",
"changeProcessCWD": true "changeProcessCWD": true
...@@ -25,4 +13,4 @@ ...@@ -25,4 +13,4 @@
"eslint.format.enable": true, "eslint.format.enable": true,
"editorconfig.generateAuto": false, "editorconfig.generateAuto": false,
"files.trimTrailingWhitespace": true "files.trimTrailingWhitespace": true
} }
\ No newline at end of file
...@@ -81,13 +81,8 @@ The Optimism Immunefi program offers up to $2,000,042 for in-scope critical vuln ...@@ -81,13 +81,8 @@ The Optimism Immunefi program offers up to $2,000,042 for in-scope critical vuln
├── <a href="./ops-bedrock">ops-bedrock</a>: Bedrock devnet work ├── <a href="./ops-bedrock">ops-bedrock</a>: Bedrock devnet work
├── <a href="./packages">packages</a> ├── <a href="./packages">packages</a>
│ ├── <a href="./packages/chain-mon">chain-mon</a>: Chain monitoring services │ ├── <a href="./packages/chain-mon">chain-mon</a>: Chain monitoring services
│ ├── <a href="./packages/common-ts">common-ts</a>: Common tools for building apps in TypeScript
│ ├── <a href="./packages/contracts-bedrock">contracts-bedrock</a>: Bedrock smart contracts │ ├── <a href="./packages/contracts-bedrock">contracts-bedrock</a>: Bedrock smart contracts
│ ├── <a href="./packages/contracts-ts">contracts-ts</a>: ABI and Address constants
│ ├── <a href="./packages/core-utils">core-utils</a>: Low-level utilities that make building Optimism easier
│ ├── <a href="./packages/fee-estimation">fee-estimation</a>: Tools for estimating gas on OP chains
│ ├── <a href="./packages/sdk">sdk</a>: provides a set of tools for interacting with Optimism │ ├── <a href="./packages/sdk">sdk</a>: provides a set of tools for interacting with Optimism
│ └── <a href="./packages/web3js-plugin">web3js-plugin</a>: Adds functions to estimate L1 and L2 gas
├── <a href="./proxyd">proxyd</a>: Configurable RPC request router and proxy ├── <a href="./proxyd">proxyd</a>: Configurable RPC request router and proxy
├── <a href="./specs">specs</a>: Specs of the rollup starting at the Bedrock upgrade ├── <a href="./specs">specs</a>: Specs of the rollup starting at the Bedrock upgrade
└── <a href="./ufm-test-services">ufm-test-services</a>: Runs a set of tasks to generate metrics └── <a href="./ufm-test-services">ufm-test-services</a>: Runs a set of tasks to generate metrics
......
...@@ -38,8 +38,6 @@ flag_management: ...@@ -38,8 +38,6 @@ flag_management:
- type: patch - type: patch
target: 100% target: 100%
- name: bedrock-go-tests - name: bedrock-go-tests
- name: common-ts-tests
- name: contracts-tests - name: contracts-tests
- name: core-utils-tests
- name: chain-mon-tests - name: chain-mon-tests
- name: sdk-tests - name: sdk-tests
ignores: [
"@babel/eslint-parser",
"@typescript-eslint/parser",
"eslint-plugin-import",
"eslint-plugin-unicorn",
"eslint-plugin-jsdoc",
"eslint-plugin-prefer-arrow",
"eslint-plugin-react",
"@typescript-eslint/eslint-plugin",
"eslint-config-prettier",
"eslint-plugin-prettier"
]
module.exports = {
extends: '../../.eslintrc.js',
}
node_modules/
build/
\ No newline at end of file
module.exports = {
...require('../../.prettierrc.js'),
};
\ No newline at end of file
# @eth-optimism/common-ts
## 0.8.9
### Patch Changes
- [#9964](https://github.com/ethereum-optimism/optimism/pull/9964) [`8241220898128e1f61064f22dcb6fdd0a5f043c3`](https://github.com/ethereum-optimism/optimism/commit/8241220898128e1f61064f22dcb6fdd0a5f043c3) Thanks [@roninjin10](https://github.com/roninjin10)! - Removed only-allow command from package.json
- Updated dependencies [[`8241220898128e1f61064f22dcb6fdd0a5f043c3`](https://github.com/ethereum-optimism/optimism/commit/8241220898128e1f61064f22dcb6fdd0a5f043c3)]:
- @eth-optimism/core-utils@0.13.2
## 0.8.8
### Patch Changes
- [#9334](https://github.com/ethereum-optimism/optimism/pull/9334) [`1ed50c44a5c4fb7244ede3b4c45ea7bbf144c1e5`](https://github.com/ethereum-optimism/optimism/commit/1ed50c44a5c4fb7244ede3b4c45ea7bbf144c1e5) Thanks [@smartcontracts](https://github.com/smartcontracts)! - Adds a new validator for address types.
## 0.8.7
### Patch Changes
- [#7450](https://github.com/ethereum-optimism/optimism/pull/7450) [`ac90e16a7`](https://github.com/ethereum-optimism/optimism/commit/ac90e16a7f85c4f73661ae6023135c3d00421c1e) Thanks [@roninjin10](https://github.com/roninjin10)! - Updated dev dependencies related to testing that is causing audit tooling to report failures
- Updated dependencies [[`ac90e16a7`](https://github.com/ethereum-optimism/optimism/commit/ac90e16a7f85c4f73661ae6023135c3d00421c1e)]:
- @eth-optimism/core-utils@0.13.1
## 0.8.6
### Patch Changes
- Updated dependencies [[`210b2c81d`](https://github.com/ethereum-optimism/optimism/commit/210b2c81dd383bad93480aa876b283d9a0c991c2)]:
- @eth-optimism/core-utils@0.13.0
## 0.8.5
### Patch Changes
- [#6887](https://github.com/ethereum-optimism/optimism/pull/6887) [`33eb63b10`](https://github.com/ethereum-optimism/optimism/commit/33eb63b10559a2267c814eda8129447c72940839) Thanks [@roninjin10](https://github.com/roninjin10)! - Updated npm dependencies of common-ts
## 0.8.4
### Patch Changes
- Updated dependencies [[`dfa309e34`](https://github.com/ethereum-optimism/optimism/commit/dfa309e3430ebc8790b932554dde120aafc4161e)]:
- @eth-optimism/core-utils@0.12.3
## 0.8.3
### Patch Changes
- Updated dependencies [[`c11039060`](https://github.com/ethereum-optimism/optimism/commit/c11039060bc037a88916c2cba602687b6d69ad1a), [`77da6edc6`](https://github.com/ethereum-optimism/optimism/commit/77da6edc643e0b5e39f7b6bb41c3c7ead418a876)]:
- @eth-optimism/core-utils@0.12.2
## 0.8.2
### Patch Changes
- Updated dependencies [8d7dcc70c]
- Updated dependencies [d6388be4a]
- @eth-optimism/core-utils@0.12.1
## 0.8.1
### Patch Changes
- fecd42d67: Fix BaseServiceV2 configuration for caseCase options
## 0.8.0
### Minor Changes
- 4ae94b412: Add option to configure body parser
### Patch Changes
- 0e179781b: Fixes a minor bug where the provider name was incorrectly logged when using waitForProvider
## 0.7.1
### Patch Changes
- f04e5db2d: Fix unknown option error in base service v2
## 0.7.0
### Minor Changes
- ab8ec365c: Updates BaseServiceV2 so that options are secret by default. Services will have to explicitly mark options as "public" for those options to be logged and included in the metadata metric.
- 9b2891852: Refactors BaseServiceV2 slightly, merges standard options with regular options
### Patch Changes
- e23f60f63: Fixes a bug in BaseServiceV2 where options were not being parsed correctly when passed into the constructor rather than via environment variables or command line arguments
- c6c9c7dbf: Adds a function for waiting for ethers providers
- ffcee1013: Make logLevel a default option of BaseServiceV2
- eceb0de1d: Adds new standard options to disable parsing variables from environment and command line.
## 0.6.8
### Patch Changes
- Updated dependencies [c975c9620]
- Updated dependencies [136ea1785]
- @eth-optimism/core-utils@0.12.0
## 0.6.7
### Patch Changes
- Updated dependencies [1e76cdb86]
- @eth-optimism/core-utils@0.11.0
## 0.6.6
### Patch Changes
- ce7da914: Minor update to BaseServiceV2 to keep the raw body around when requests are made.
## 0.6.5
### Patch Changes
- 7215f4ce: Bump ethers to 5.7.0 globally
- d7679ca4: Add source maps
- Updated dependencies [7215f4ce]
- Updated dependencies [206f6033]
- @eth-optimism/core-utils@0.10.1
## 0.6.4
### Patch Changes
- Updated dependencies [dbfea116]
- @eth-optimism/core-utils@0.10.0
## 0.6.3
### Patch Changes
- Updated dependencies [0df744f6]
- Updated dependencies [8ae39154]
- Updated dependencies [dac4a9f0]
- @eth-optimism/core-utils@0.9.3
## 0.6.2
### Patch Changes
- Updated dependencies [0bf3b9b4]
- Updated dependencies [8d26459b]
- Updated dependencies [4477fe9f]
- @eth-optimism/core-utils@0.9.2
## 0.6.1
### Patch Changes
- Updated dependencies [f9fee446]
- @eth-optimism/core-utils@0.9.1
## 0.6.0
### Minor Changes
- 3d1cb720: Add version to healthz for convenience
### Patch Changes
- Updated dependencies [700dcbb0]
- @eth-optimism/core-utils@0.9.0
## 0.5.0
### Minor Changes
- cb71fcde: Make typescript type more permissive for MetricsV2
### Patch Changes
- 10e41522: Fix potential metrics DoS vector in recent commit to BSV2
## 0.4.0
### Minor Changes
- 52b26878: More gracefully shut down base service
### Patch Changes
- c201f3f1: Collect default node metrics
- 29ff7462: Revert es target back to 2017
- Updated dependencies [29ff7462]
- @eth-optimism/core-utils@0.8.7
## 0.3.1
### Patch Changes
- 9ba869a7: Log server messages to logger instead of stdout
- 050859fd: Include default options in metadata metric
## 0.3.0
### Minor Changes
- d9e39931: Minor upgrade to BaseServiceV2 to expose a full customizable server, instead of just metrics.
- 84a8934c: BaseServiceV2 exposes service name and version as standard synthetic metric
## 0.2.10
### Patch Changes
- 9ecbf3e5: Expose service internal options as environment or cli options
## 0.2.9
### Patch Changes
- Updated dependencies [17962ca9]
- @eth-optimism/core-utils@0.8.6
## 0.2.8
### Patch Changes
- f16383f2: Have legacy BaseService metrics bind to 0.0.0.0 by default
- d18ae135: Updates all ethers versions in response to BN.js bug
- Updated dependencies [d18ae135]
- @eth-optimism/core-utils@0.8.5
## 0.2.7
### Patch Changes
- Updated dependencies [5cb3a5f7]
- Updated dependencies [6b9fc055]
- @eth-optimism/core-utils@0.8.4
## 0.2.6
### Patch Changes
- b57014d1: Update to typescript@4.6.2
- Updated dependencies [b57014d1]
- @eth-optimism/core-utils@0.8.3
## 0.2.5
### Patch Changes
- e36b085c: Adds hard stop to BaseServiceV2 when multiple exit signals are received
- c1957126: Update Dockerfile to use Alpine
- 51673b90: Have BaseServiceV2 throw when options are undefined
- 7a179003: Adds the jsonRpcProvider validator as an input validator
- Updated dependencies [c1957126]
- @eth-optimism/core-utils@0.8.2
## 0.2.4
### Patch Changes
- f981b8da: Properly exposes metrics as part of a metrics server at port 7300
## 0.2.3
### Patch Changes
- f7761058: Update log lines for service shutdown
- 5ae15042: Update metric names to include proper snake_case for strings that include "L1" or "L2"
- 5cd1e996: Have BaseServiceV2 add spaces to environment variable names
## 0.2.2
### Patch Changes
- b3f9bdef: Have BaseServiceV2 gracefully catch exit signals
- e53b5783: Introduces the new BaseServiceV2 class.
## 0.2.1
### Patch Changes
- 243f33e5: Standardize package json file format
## 0.2.0
### Minor Changes
- 81ccd6e4: `regenesis/0.5.0` release
## 0.1.6
### Patch Changes
- 6d3e1d7f: Update dependencies
## 0.1.5
### Patch Changes
- c73c3939: Update the typescript version to `4.3.5`
## 0.1.4
### Patch Changes
- 5c89c45f: Move the metric prefix string to a label #1047
## 0.1.3
### Patch Changes
- baa3b761: Improve Sentry support, initializing as needed and ensuring ERROR logs route to Sentry
## 0.1.2
### Patch Changes
- 0c16805: add metrics server to common-ts and batch submitter
## 0.1.1
### Patch Changes
- 1d40586: Removed various unused dependencies
- 575bcf6: add environment and network to dtl, move metric init to app from base-service
## 0.1.0
### Minor Changes
- 28dc442: move metrics, logger, and base-service to new common-ts package
(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.
# @eth-optimism/common-ts
[![codecov](https://codecov.io/gh/ethereum-optimism/optimism/branch/develop/graph/badge.svg?token=0VTG7PG7YR&flag=common-ts-tests)](https://codecov.io/gh/ethereum-optimism/optimism)
## What is this?
`@eth-optimism/common-ts` contains useful tools for logging, metrics, and other Node stuff.
{
"name": "@eth-optimism/common-ts",
"version": "0.8.9",
"description": "[Optimism] Advanced typescript tooling used by various services",
"main": "dist/index",
"types": "dist/index",
"files": [
"dist/*",
"src/*"
],
"scripts": {
"all": "pnpm clean && pnpm build && pnpm test && pnpm lint:fix && pnpm lint",
"build": "tsc -p tsconfig.json",
"clean": "rimraf dist/ ./tsconfig.tsbuildinfo",
"lint:check": "eslint . --max-warnings=0",
"lint:fix": "pnpm lint:check --fix",
"lint": "pnpm lint:fix && pnpm lint:check",
"pre-commit": "lint-staged",
"test": "ts-mocha test/*.spec.ts",
"test:coverage": "nyc ts-mocha test/*.spec.ts && nyc merge .nyc_output coverage.json"
},
"keywords": [
"optimism",
"ethereum",
"common",
"typescript"
],
"homepage": "https://github.com/ethereum-optimism/optimism/tree/develop/packages/common-ts#readme",
"license": "MIT",
"author": "Optimism PBC",
"repository": {
"type": "git",
"url": "https://github.com/ethereum-optimism/optimism.git"
},
"dependencies": {
"@eth-optimism/core-utils": "^0.13.2",
"@sentry/node": "^7.99.0",
"bcfg": "^0.2.1",
"body-parser": "^1.20.2",
"commander": "^11.1.0",
"dotenv": "^16.4.5",
"envalid": "^8.0.0",
"ethers": "^5.7.2",
"express": "^4.19.2",
"express-prom-bundle": "^7.0.0",
"lodash": "^4.17.21",
"morgan": "^1.10.0",
"pino": "^8.19.0",
"pino-multi-stream": "^6.0.0",
"pino-sentry": "^0.14.0",
"prom-client": "^14.2.0"
},
"devDependencies": {
"@ethersproject/abstract-provider": "^5.7.0",
"@ethersproject/abstract-signer": "^5.7.0",
"@types/express": "^4.17.21",
"@types/morgan": "^1.9.9",
"@types/pino": "^7.0.5",
"@types/pino-multi-stream": "^5.1.6",
"chai": "^4.3.10",
"supertest": "^6.3.4"
}
}
import { Server } from 'net'
import Config from 'bcfg'
import * as dotenv from 'dotenv'
import { Command, Option } from 'commander'
import { cleanEnv } from 'envalid'
import snakeCase from 'lodash/snakeCase'
import express from 'express'
import prometheus, { Registry } from 'prom-client'
import promBundle from 'express-prom-bundle'
import bodyParser from 'body-parser'
import morgan from 'morgan'
import { ExpressRouter } from './router'
import { Logger } from '../common/logger'
import {
Metrics,
MetricsSpec,
StandardMetrics,
makeStdMetricsSpec,
} from './metrics'
import {
Options,
OptionsSpec,
StandardOptions,
stdOptionsSpec,
getPublicOptions,
} from './options'
/**
* BaseServiceV2 is an advanced but simple base class for long-running TypeScript services.
*/
export abstract class BaseServiceV2<
TOptions extends Options,
TMetrics extends Metrics,
TServiceState
> {
/**
* The timeout that controls the polling interval
* If clearTimeout(this.pollingTimeout) is called the timeout will stop
*/
private pollingTimeout: NodeJS.Timeout
/**
* The promise representing this.main
*/
private mainPromise: ReturnType<typeof this.main>
/**
* Whether or not the service will loop.
*/
protected loop: boolean
/**
* Waiting period in ms between loops, if the service will loop.
*/
protected loopIntervalMs: number
/**
* Whether or not the service is currently running.
*/
protected running: boolean
/**
* Whether or not the service is currently healthy.
*/
protected healthy: boolean
/**
* Logger class for this service.
*/
protected logger: Logger
/**
* Service state, persisted between loops.
*/
protected state: TServiceState
/**
* Service options.
*/
protected readonly options: TOptions & StandardOptions
/**
* Metrics.
*/
protected readonly metrics: TMetrics & StandardMetrics
/**
* Registry for prometheus metrics.
*/
protected readonly metricsRegistry: Registry
/**
* App server.
*/
protected server: Server
/**
* Port for the app server.
*/
protected readonly port: number
/**
* Hostname for the app server.
*/
protected readonly hostname: string
/**
* @param params Options for the construction of the service.
* @param params.name Name for the service.
* @param params.optionsSpec Settings for input options.
* @param params.metricsSpec Settings that define which metrics are collected.
* @param params.options Options to pass to the service.
* @param params.loops Whether or not the service should loop. Defaults to true.
* @param params.useEnv Whether or not to load options from the environment. Defaults to true.
* @param params.useArgv Whether or not to load options from the command line. Defaults to true.
*/
constructor(
private readonly params: {
name: string
version: string
optionsSpec: OptionsSpec<TOptions>
metricsSpec: MetricsSpec<TMetrics>
options?: Partial<TOptions & StandardOptions>
loop?: boolean
bodyParserParams?: bodyParser.OptionsJson
}
) {
this.loop = params.loop !== undefined ? params.loop : true
this.state = {} as TServiceState
// Add standard options spec to user options spec.
;(params.optionsSpec as any) = {
...params.optionsSpec,
...stdOptionsSpec,
}
// Add default metrics to metrics spec.
;(params.metricsSpec as any) = {
...params.metricsSpec,
...makeStdMetricsSpec(params.optionsSpec),
}
/**
* Special snake_case function which accounts for the common strings "L1" and "L2" which would
* normally be split into "L_1" and "L_2" by the snake_case function.
*
* @param str String to convert to snake_case.
* @returns snake_case string.
*/
const opSnakeCase = (str: string) => {
const reg = /l_1|l_2/g
const repl = str.includes('l1') ? 'l1' : 'l2'
return snakeCase(str).replace(reg, repl)
}
// Use commander as a way to communicate info about the service. We don't actually *use*
// commander for anything besides the ability to run `tsx ./service.ts --help`.
const program = new Command().allowUnknownOption(true)
for (const [optionName, optionSpec] of Object.entries(params.optionsSpec)) {
// Skip options that are not meant to be used by the user.
if (['useEnv', 'useArgv'].includes(optionName)) {
continue
}
program.addOption(
new Option(`--${optionName.toLowerCase()}`, `${optionSpec.desc}`).env(
`${opSnakeCase(
params.name.replace(/-/g, '_')
).toUpperCase()}__${opSnakeCase(optionName).toUpperCase()}`
)
)
}
const longestMetricNameLength = Object.keys(params.metricsSpec).reduce(
(acc, key) => {
const nameLength = snakeCase(key).length
if (nameLength > acc) {
return nameLength
} else {
return acc
}
},
0
)
program.addHelpText(
'after',
`\nMetrics:\n${Object.entries(params.metricsSpec)
.map(([metricName, metricSpec]) => {
const parsedName = opSnakeCase(metricName)
return ` ${parsedName}${' '.repeat(
longestMetricNameLength - parsedName.length + 2
)}${metricSpec.desc} (type: ${metricSpec.type.name})`
})
.join('\n')}
`
)
// Load all configuration values from the environment and argv.
program.parse()
dotenv.config()
const config = new Config(params.name)
config.load({
env: params.options?.useEnv ?? true,
argv: params.options?.useEnv ?? true,
})
// Clean configuration values using the options spec.
// Since BCFG turns everything into lower case, we're required to turn all of the input option
// names into lower case for the validation step. We'll turn the names back into their original
// names when we're done.
const lowerCaseOptions = Object.entries(params.options).reduce(
(acc, [key, val]) => {
acc[key.toLowerCase()] = val
return acc
},
{}
)
const cleaned = cleanEnv<TOptions>(
{ ...config.env, ...config.args, ...(lowerCaseOptions || {}) },
Object.entries(params.optionsSpec || {}).reduce((acc, [key, val]) => {
acc[key.toLowerCase()] = val.validator({
desc: val.desc,
default: val.default,
})
return acc
}, {}) as any
)
// Turn the lowercased option names back into camelCase.
this.options = Object.keys(params.optionsSpec || {}).reduce((acc, key) => {
acc[key] = cleaned[key.toLowerCase()]
return acc
}, {}) as TOptions
// Make sure all options are defined.
for (const [optionName, optionSpec] of Object.entries(params.optionsSpec)) {
if (
optionSpec.default === undefined &&
this.options[optionName] === undefined
) {
throw new Error(`missing required option: ${optionName}`)
}
}
// Create the metrics objects.
this.metrics = Object.keys(params.metricsSpec || {}).reduce((acc, key) => {
const spec = params.metricsSpec[key]
acc[key] = new spec.type({
name: `${opSnakeCase(params.name)}_${opSnakeCase(key)}`,
help: spec.desc,
labelNames: spec.labels || [],
})
return acc
}, {}) as TMetrics & StandardMetrics
// Create the metrics server.
this.metricsRegistry = prometheus.register
this.port = this.options.port
this.hostname = this.options.hostname
// Set up everything else.
this.healthy = true
this.loopIntervalMs = this.options.loopIntervalMs
this.logger = new Logger({
name: params.name,
level: this.options.logLevel,
})
// Gracefully handle stop signals.
const maxSignalCount = 3
let currSignalCount = 0
const stop = async (signal: string) => {
// Allow exiting fast if more signals are received.
currSignalCount++
if (currSignalCount === 1) {
this.logger.info(`stopping service with signal`, { signal })
await this.stop()
process.exit(0)
} else if (currSignalCount >= maxSignalCount) {
this.logger.info(`performing hard stop`)
process.exit(0)
} else {
this.logger.info(
`send ${maxSignalCount - currSignalCount} more signal(s) to hard stop`
)
}
}
// Handle stop signals.
process.on('SIGTERM', stop)
process.on('SIGINT', stop)
// Set metadata synthetic metric.
this.metrics.metadata.set(
{
name: params.name,
version: params.version,
...getPublicOptions(params.optionsSpec).reduce((acc, key) => {
if (key in stdOptionsSpec) {
acc[key] = this.options[key].toString()
} else {
acc[key] = config.str(key)
}
return acc
}, {}),
},
1
)
// Collect default node metrics.
prometheus.collectDefaultMetrics({
register: this.metricsRegistry,
labels: { name: params.name, version: params.version },
})
}
/**
* Runs the main function. If this service is set up to loop, will repeatedly loop around the
* main function. Will also catch unhandled errors.
*/
public async run(): Promise<void> {
// Start the app server if not yet running.
if (!this.server) {
this.logger.info('starting app server')
// Start building the app.
const app = express()
// Body parsing.
app.use(bodyParser.urlencoded({ extended: true }))
// Keep the raw body around in case the application needs it.
app.use(
bodyParser.json({
verify: (req, res, buf, encoding) => {
;(req as any).rawBody =
buf?.toString((encoding as BufferEncoding) || 'utf8') || ''
},
...(this.params.bodyParserParams ?? {}),
})
)
// Logging.
app.use(
morgan('short', {
stream: {
write: (str: string) => {
this.logger.info(`server log`, {
log: str,
})
},
},
})
)
// Health status.
app.get('/healthz', async (req, res) => {
return res.json({
ok: this.healthy,
version: this.params.version,
})
})
// Register user routes.
const router = express.Router()
if (this.routes) {
this.routes(router)
}
// Metrics.
// Will expose a /metrics endpoint by default.
app.use(
promBundle({
promRegistry: this.metricsRegistry,
includeMethod: true,
includePath: true,
includeStatusCode: true,
normalizePath: (req) => {
for (const layer of router.stack) {
if (layer.route && req.path.match(layer.regexp)) {
return layer.route.path
}
}
return '/invalid_path_not_a_real_route'
},
})
)
app.use('/api', router)
// Wait for server to come up.
await new Promise((resolve) => {
this.server = app.listen(this.port, this.hostname, () => {
resolve(null)
})
})
this.logger.info(`app server started`, {
port: this.port,
hostname: this.hostname,
})
}
if (this.init) {
this.logger.info('initializing service')
await this.init()
this.logger.info('service initialized')
}
if (this.loop) {
this.logger.info('starting main loop')
this.running = true
const doLoop = async () => {
try {
this.mainPromise = this.main()
await this.mainPromise
} catch (err) {
this.metrics.unhandledErrors.inc()
this.logger.error('caught an unhandled exception', {
message: err.message,
stack: err.stack,
code: err.code,
})
}
// Sleep between loops if we're still running (service not stopped).
if (this.running) {
this.pollingTimeout = setTimeout(doLoop, this.loopIntervalMs)
}
}
doLoop()
} else {
this.logger.info('running main function')
await this.main()
}
}
/**
* Tries to gracefully stop the service. Service will continue running until the current loop
* iteration is finished and will then stop looping.
*/
public async stop(): Promise<void> {
this.logger.info('stopping main loop...')
this.running = false
clearTimeout(this.pollingTimeout)
this.logger.info('waiting for main to complete')
// if main is in the middle of running wait for it to complete
await this.mainPromise
this.logger.info('main loop stopped.')
// Shut down the metrics server if it's running.
if (this.server) {
this.logger.info('stopping metrics server')
await new Promise((resolve) => {
this.server.close(() => {
resolve(null)
})
})
this.logger.info('metrics server stopped')
this.server = undefined
}
}
/**
* Initialization function. Runs once before the main function.
*/
protected init?(): Promise<void>
/**
* Initialization function for router.
*
* @param router Express router.
*/
protected routes?(router: ExpressRouter): Promise<void>
/**
* Main function. Runs repeatedly when run() is called.
*/
protected abstract main(): Promise<void>
}
/* Imports: Internal */
import { Logger } from '../common/logger'
import { LegacyMetrics } from '../common/metrics'
type OptionSettings<TOptions> = {
[P in keyof TOptions]?: {
default?: TOptions[P]
validate?: (val: any) => boolean
}
}
type BaseServiceOptions<T> = T & {
logger?: Logger
metrics?: LegacyMetrics
}
/**
* Base for other "Service" objects. Handles your standard initialization process, can dynamically
* start and stop.
*/
export class BaseService<T> {
protected name: string
protected options: T
protected logger: Logger
protected metrics: LegacyMetrics
protected initialized = false
protected running = false
constructor(
name: string,
options: BaseServiceOptions<T>,
optionSettings: OptionSettings<T>
) {
validateOptions(options, optionSettings)
this.name = name
this.options = mergeDefaultOptions(options, optionSettings)
this.logger = options.logger || new Logger({ name })
if (options.metrics) {
this.metrics = options.metrics
}
}
/**
* Initializes the service.
*/
public async init(): Promise<void> {
if (this.initialized) {
return
}
this.logger.info('Service is initializing...')
await this._init()
this.logger.info('Service has initialized.')
this.initialized = true
}
/**
* Starts the service (initializes it if needed).
*/
public async start(): Promise<void> {
if (this.running) {
return
}
this.logger.info('Service is starting...')
await this.init()
// set the service to running
this.running = true
await this._start()
this.logger.info('Service has started')
}
/**
* Stops the service.
*/
public async stop(): Promise<void> {
if (!this.running) {
return
}
this.logger.info('Service is stopping...')
await this._stop()
this.logger.info('Service has stopped')
this.running = false
}
/**
* Internal init function. Parent should implement.
*/
protected async _init(): Promise<void> {
return
}
/**
* Internal start function. Parent should implement.
*/
protected async _start(): Promise<void> {
return
}
/**
* Internal stop function. Parent should implement.
*/
protected async _stop(): Promise<void> {
return
}
}
/**
* Combines user provided and default options.
*/
const mergeDefaultOptions = <T>(
options: T,
optionSettings: OptionSettings<T>
): T => {
for (const optionName of Object.keys(optionSettings)) {
const optionDefault = optionSettings[optionName].default
if (optionDefault === undefined) {
continue
}
if (options[optionName] !== undefined && options[optionName] !== null) {
continue
}
options[optionName] = optionDefault
}
return options
}
/**
* Performs option validation against the option settings
*/
const validateOptions = <T>(options: T, optionSettings: OptionSettings<T>) => {
for (const optionName of Object.keys(optionSettings)) {
const optionValidationFunction = optionSettings[optionName].validate
if (optionValidationFunction === undefined) {
continue
}
const optionValue = options[optionName]
if (optionValidationFunction(optionValue) === false) {
throw new Error(
`Provided input for option "${optionName}" is invalid: ${optionValue}`
)
}
}
}
export * from './base-service'
export * from './base-service-v2'
export * from './validators'
export * from './metrics'
export * from './options'
export * from './router'
import {
Gauge as PGauge,
Counter as PCounter,
Histogram as PHistogram,
Summary as PSummary,
} from 'prom-client'
import { OptionsSpec, getPublicOptions } from './options'
// Prometheus metrics re-exported.
export class Gauge extends PGauge<string> {}
export class Counter extends PCounter<string> {}
export class Histogram extends PHistogram<string> {}
export class Summary extends PSummary<string> {}
export type Metric = Gauge | Counter | Histogram | Summary
/**
* Metrics that are available for a given service.
*/
export type Metrics = Record<any, Metric>
/**
* Specification for metrics.
*/
export type MetricsSpec<TMetrics extends Metrics> = {
[P in keyof Required<TMetrics>]: {
type: new (configuration: any) => TMetrics[P]
desc: string
labels?: string[]
}
}
/**
* Standard metrics that are always available.
*/
export type StandardMetrics = {
metadata: Gauge
unhandledErrors: Counter
}
/**
* Generates a standard metrics specification. Needs to be a function because the labels for
* service metadata are dynamic dependent on the list of given options.
*
* @param options Options to include in the service metadata.
* @returns Metrics specification.
*/
export const makeStdMetricsSpec = (
optionsSpec: OptionsSpec<any>
): MetricsSpec<StandardMetrics> => {
return {
// Users cannot set these options.
metadata: {
type: Gauge,
desc: 'Service metadata',
labels: ['name', 'version'].concat(getPublicOptions(optionsSpec)),
},
unhandledErrors: {
type: Counter,
desc: 'Unhandled errors',
},
}
}
import { ValidatorSpec, Spec } from 'envalid'
import { LogLevel } from '../common/logger'
import { validators } from './validators'
/**
* Options for a service.
*/
export type Options = {
[key: string]: any
}
/**
* Specification for options.
*/
export type OptionsSpec<TOptions extends Options> = {
[P in keyof Required<TOptions>]: {
validator: (spec?: Spec<TOptions[P]>) => ValidatorSpec<TOptions[P]>
desc: string
default?: TOptions[P]
public?: boolean
}
}
/**
* Standard options shared by all services.
*/
export type StandardOptions = {
loopIntervalMs?: number
port?: number
hostname?: string
logLevel?: LogLevel
useEnv?: boolean
useArgv?: boolean
}
/**
* Specification for standard options.
*/
export const stdOptionsSpec: OptionsSpec<StandardOptions> = {
loopIntervalMs: {
validator: validators.num,
desc: 'Loop interval in milliseconds, only applies if service is set to loop',
default: 0,
public: true,
},
port: {
validator: validators.num,
desc: 'Port for the app server',
default: 7300,
public: true,
},
hostname: {
validator: validators.str,
desc: 'Hostname for the app server',
default: '0.0.0.0',
public: true,
},
logLevel: {
validator: validators.logLevel,
desc: 'Log level',
default: 'debug',
public: true,
},
useEnv: {
validator: validators.bool,
desc: 'For programmatic use, whether to use environment variables',
default: true,
public: true,
},
useArgv: {
validator: validators.bool,
desc: 'For programmatic use, whether to use command line arguments',
default: true,
public: true,
},
}
/**
* Gets the list of public option names from an options specification.
*
* @param optionsSpec Options specification.
* @returns List of public option names.
*/
export const getPublicOptions = (
optionsSpec: OptionsSpec<Options>
): string[] => {
return Object.keys(optionsSpec).filter((key) => {
return optionsSpec[key].public
})
}
import { Router } from 'express'
/**
* Express router re-exported.
*/
export type ExpressRouter = Router
import {
str,
bool,
num,
email,
host,
port,
url,
json,
makeValidator,
} from 'envalid'
import { Provider } from '@ethersproject/abstract-provider'
import { Signer } from '@ethersproject/abstract-signer'
import { ethers } from 'ethers'
import { LogLevel, logLevels } from '../common'
const provider = makeValidator<Provider>((input) => {
const parsed = url()._parse(input)
return new ethers.providers.JsonRpcProvider(parsed)
})
const jsonRpcProvider = makeValidator<ethers.providers.JsonRpcProvider>(
(input) => {
const parsed = url()._parse(input)
return new ethers.providers.JsonRpcProvider(parsed)
}
)
const staticJsonRpcProvider =
makeValidator<ethers.providers.StaticJsonRpcProvider>((input) => {
const parsed = url()._parse(input)
return new ethers.providers.StaticJsonRpcProvider(parsed)
})
const wallet = makeValidator<Signer>((input) => {
if (!ethers.utils.isHexString(input)) {
throw new Error(`expected wallet to be a hex string`)
} else {
return new ethers.Wallet(input)
}
})
const logLevel = makeValidator<LogLevel>((input) => {
if (!logLevels.includes(input as LogLevel)) {
throw new Error(`expected log level to be one of ${logLevels.join(', ')}`)
} else {
return input as LogLevel
}
})
const address = makeValidator<string>((input) => {
if (!ethers.utils.isHexString(input, 20)) {
throw new Error(`expected input to be an address: ${input}`)
} else {
return input as `0x${string}`
}
})
export const validators = {
str,
bool,
num,
email,
host,
port,
url,
json,
wallet,
provider,
jsonRpcProvider,
staticJsonRpcProvider,
logLevel,
address,
}
export * from './logger'
export * from './metrics'
export * from './provider'
import pino, { LoggerOptions as PinoLoggerOptions } from 'pino'
import pinoms, { Streams } from 'pino-multi-stream'
import { createWriteStream } from 'pino-sentry'
import { NodeOptions } from '@sentry/node'
export const logLevels = [
'trace',
'debug',
'info',
'warn',
'error',
'fatal',
] as const
export type LogLevel = (typeof logLevels)[number]
export interface LoggerOptions {
name: string
level?: LogLevel
sentryOptions?: NodeOptions
streams?: Streams
}
/**
* Temporary wrapper class to maintain earlier module interface.
*/
export class Logger {
options: LoggerOptions
inner: pino.Logger
constructor(options: LoggerOptions) {
this.options = options
const loggerOptions: PinoLoggerOptions = {
name: options.name,
level: options.level || 'debug',
// Remove pid and hostname considering production runs inside docker
base: null,
}
let loggerStreams: Streams = [{ stream: process.stdout }]
if (options.sentryOptions) {
loggerStreams.push({
level: 'error',
stream: createWriteStream({
...options.sentryOptions,
stackAttributeKey: 'err',
}),
})
}
if (options.streams) {
loggerStreams = loggerStreams.concat(options.streams)
}
this.inner = pino(loggerOptions, pinoms.multistream(loggerStreams))
}
child(bindings: pino.Bindings): Logger {
const inner = this.inner.child(bindings)
const logger = new Logger(this.options)
logger.inner = inner
return logger
}
trace(msg: string, o?: object, ...args: any[]): void {
if (o) {
this.inner.trace(o, msg, ...args)
} else {
this.inner.trace(msg, ...args)
}
}
debug(msg: string, o?: object, ...args: any[]): void {
if (o) {
this.inner.debug(o, msg, ...args)
} else {
this.inner.debug(msg, ...args)
}
}
info(msg: string, o?: object, ...args: any[]): void {
if (o) {
this.inner.info(o, msg, ...args)
} else {
this.inner.info(msg, ...args)
}
}
warn(msg: string, o?: object, ...args: any[]): void {
if (o) {
this.inner.warn(o, msg, ...args)
} else {
this.inner.warn(msg, ...args)
}
}
warning(msg: string, o?: object, ...args: any[]): void {
if (o) {
this.inner.warn(o, msg, ...args)
} else {
this.inner.warn(msg, ...args)
}
}
error(msg: string, o?: object, ...args: any[]): void {
if (o) {
// Formatting error log for Sentry
const context = {
extra: { ...o },
}
this.inner.error(context, msg, ...args)
} else {
this.inner.error(msg, ...args)
}
}
fatal(msg: string, o?: object, ...args: any[]): void {
if (o) {
const context = {
extra: { ...o },
}
this.inner.fatal(context, msg, ...args)
} else {
this.inner.fatal(msg, ...args)
}
}
}
import { Server } from 'net'
import prometheus, {
collectDefaultMetrics,
DefaultMetricsCollectorConfiguration,
Registry,
} from 'prom-client'
import express from 'express'
import { Logger } from './logger'
export interface MetricsOptions {
prefix?: string
labels?: Object
}
export class LegacyMetrics {
options: MetricsOptions
client: typeof prometheus
registry: Registry
constructor(options: MetricsOptions) {
this.options = options
const metricsOptions: DefaultMetricsCollectorConfiguration = {
prefix: options.prefix,
labels: options.labels,
}
this.client = prometheus
this.registry = prometheus.register
// Collect default metrics (event loop lag, memory, file descriptors etc.)
collectDefaultMetrics(metricsOptions)
}
}
export interface MetricsServerOptions {
logger: Logger
registry: Registry
port?: number
route?: string
hostname?: string
}
export const createMetricsServer = async (
options: MetricsServerOptions
): Promise<Server> => {
const logger = options.logger.child({ component: 'MetricsServer' })
const app = express()
const route = options.route || '/metrics'
app.get(route, async (_, res) => {
res.status(200).send(await options.registry.metrics())
})
const port = options.port || 7300
const hostname = options.hostname || '0.0.0.0'
const server = app.listen(port, hostname, () => {
logger.info('Metrics server started', {
port,
hostname,
route,
})
})
return server
}
import { Provider } from '@ethersproject/abstract-provider'
import { sleep } from '@eth-optimism/core-utils'
import { Logger } from './logger'
/**
* Waits for an Ethers provider to be connected.
*
* @param provider Ethers provider to check.
* @param opts Options for the function.
* @param opts.logger Logger to use.
* @param opts.intervalMs Interval to wait between checks.
* @param opts.name Name of the provider for logs.
*/
export const waitForProvider = async (
provider: Provider,
opts?: {
logger?: Logger
intervalMs?: number
name?: string
}
) => {
const name = opts?.name || 'target'
opts?.logger?.info(`waiting for ${name} provider...`)
let connected = false
while (!connected) {
try {
await provider.getBlockNumber()
connected = true
} catch (e) {
opts?.logger?.info(`${name} provider not connected, retrying...`)
await sleep(opts?.intervalMs || 15000)
}
}
opts?.logger?.info(`${name} provider connected`)
}
export * from './common'
export * from './base-service'
import request from 'supertest'
// Setup
import chai = require('chai')
const expect = chai.expect
import { Logger, LegacyMetrics, createMetricsServer } from '../src'
describe('Metrics', () => {
it('should serve metrics', async () => {
const metrics = new LegacyMetrics({
prefix: 'test_metrics',
})
const registry = metrics.registry
const logger = new Logger({ name: 'test_logger' })
const server = await createMetricsServer({
logger,
registry,
port: 42069,
})
try {
// Create two metrics for testing
const counter = new metrics.client.Counter({
name: 'counter',
help: 'counter help',
registers: [registry],
})
const gauge = new metrics.client.Gauge({
name: 'gauge',
help: 'gauge help',
registers: [registry],
})
counter.inc()
counter.inc()
gauge.set(100)
// Verify that the registered metrics are served at `/`
const response = await request(server).get('/metrics').send()
expect(response.status).eq(200)
expect(response.text).match(/counter 2/)
expect(response.text).match(/gauge 100/)
} finally {
server.close()
registry.clear()
}
})
})
import { validators } from '../dist'
import { BaseServiceV2 } from '../src'
type ServiceOptions = {
camelCase: string
}
class Service extends BaseServiceV2<ServiceOptions, {}, {}> {
constructor(options?: Partial<ServiceOptions>) {
super({
name: 'test-service',
version: '0.0',
options,
optionsSpec: {
camelCase: { validator: validators.str, desc: 'test' },
},
metricsSpec: {},
})
}
protected async main() {
/* eslint-disable @typescript-eslint/no-empty-function */
}
}
describe('BaseServiceV2', () => {
it('base service ctor does not throw on camel case options', async () => {
new Service({ camelCase: 'test' })
})
})
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"skipLibCheck": true
},
"include": [
"src/**/*"
]
}
artifacts
cache
typechain
.deps
.envrc
.env
/dist/
coverage
artifacts
cache
typechain
.deps
.envrc
.env
/dist/
module.exports = {
...require('../../.prettierrc.js'),
}
# @eth-optimism/contracts-ts
## 0.17.2
### Patch Changes
- [#9964](https://github.com/ethereum-optimism/optimism/pull/9964) [`8241220898128e1f61064f22dcb6fdd0a5f043c3`](https://github.com/ethereum-optimism/optimism/commit/8241220898128e1f61064f22dcb6fdd0a5f043c3) Thanks [@roninjin10](https://github.com/roninjin10)! - Removed only-allow command from package.json
## 0.17.0
### Minor Changes
- [#7644](https://github.com/ethereum-optimism/optimism/pull/7644) [`86bdaa075`](https://github.com/ethereum-optimism/optimism/commit/86bdaa075f424974531ce98ac9e21037e43db1bb) Thanks [@roninjin10](https://github.com/roninjin10)! - Removed unused hooks and actions
## 0.16.2
### Patch Changes
- [#7450](https://github.com/ethereum-optimism/optimism/pull/7450) [`ac90e16a7`](https://github.com/ethereum-optimism/optimism/commit/ac90e16a7f85c4f73661ae6023135c3d00421c1e) Thanks [@roninjin10](https://github.com/roninjin10)! - Updated dev dependencies related to testing that is causing audit tooling to report failures
# Code gen
Summary -
- This package is generated from [contracts-bedrock](../contracts-bedrock/)
- Its version is kept in sync with contracts bedrock via the [changeset config](../../.changeset/config.json) e.g. if contracts-bedrock is `4.2.0` this package will have the same version.
## Code gen instructions
To run the code gen run the `generate` script from [package.json](./package.json). Make sure node modules is installed.
```bash
pnpm i && pnpm generate
```
MIT License
Copyright (c) 2022 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.
## Contracts TS
[![codecov](https://codecov.io/gh/ethereum-optimism/optimism/branch/develop/graph/badge.svg?token=0VTG7PG7YR&flag=contracts-bedrock-tests)](https://codecov.io/gh/ethereum-optimism/optimism)
ABI and Address constants + generated code from [@eth-optimism/contracts-bedrock/](../contracts-bedrock/) for use in TypeScript.
Much of this package is generated. See [CODE_GEN.md](./CODE_GEN.md) for instructions on how to generate.
#### @eth-optimism/contracts-ts
The main entrypoint exports constants related to contracts bedrock as const. As const allows it to be [used in TypeScript with stronger typing than importing JSON](https://github.com/microsoft/TypeScript/issues/32063).
- Exports contract abis.
- Exports contract addresses
```typescript
import {
l2OutputOracleProxyABI,
l2OutputOracleAddresses,
} from '@eth-optimism/contracts-ts'
console.log(l2OutputOracleAddresses[10], abi)
```
Addresses are also exported as an object for convenience.
```typescript
import { addresses } from '@eth-optimism/contracts-ts'
console.log(addresses.l2OutputOracle[10])
```
#### @eth-optimism/contracts-ts/react
- All [React hooks](https://wagmi.sh/cli/plugins/react) `@eth-optimism/contracts-ts/react`
```typescript
import { useAddressManagerAddress } from '@eth-optimism/contracts-ts/react'
const component = () => {
const { data, error, loading } = useAddressManagerAddress()
if (loading) {
return <div>Loading</div>
}
if (err) {
return <div>Error</div>
}
return <div>{data}</div>
}
```
#### @eth-optimism/contracts-ts/actions
- All [wagmi actions](https://wagmi.sh/react/actions) for use in Vanilla JS or non react code
```typescript
import { readSystemConfig } from '@eth-optimism/contracts-ts/actions'
console.log(await readSystemConfig())
```
#### See Also
- [Contracts bedrock specs](../../specs/)
- [Wagmi](https://wagmi.sh)
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"AddressManager": {
"1": "0xdE1FCfB0851916CA5101820A69b13a4E276bd81F",
"5": "0xa6f73589243a6A7a9023b1Fa0651b1d89c177111"
},
"AssetReceiver": {
"1": "0x15DdA60616Ffca20371ED1659dBB78E888f65556",
"10": "0x15DdA60616Ffca20371ED1659dBB78E888f65556"
},
"AttestationStation": {
"10": "0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77",
"420": "0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77"
},
"BaseFeeVault": {
"420": "0x4200000000000000000000000000000000000019"
},
"CheckBalanceHigh": {
"1": "0x7eC64a8a591bFf829ff6C8be76074D540ACb813F",
"5": "0x7eC64a8a591bFf829ff6C8be76074D540ACb813F",
"420": "0x5d7103853f12109A7d27F118e54BbC654ad847E9"
},
"CheckBalanceLow": {
"1": "0x381a4eFC2A2C914eA1889722bB4B44Fa6BD5b640",
"5": "0x381a4eFC2A2C914eA1889722bB4B44Fa6BD5b640",
"420": "0x7Ce13D154FAEE5C8B3E6b19d4Add16f21d884474"
},
"CheckGelatoLow": {
"1": "0x4f7CFc43f6D262a085F3b946cAC69E7a8E39BBAa",
"5": "0x4f7CFc43f6D262a085F3b946cAC69E7a8E39BBAa",
"420": "0xF9c8a4Cb4021f57F9f6d69799cA9BefF64524862"
},
"CheckTrue": {
"1": "0x5c741a38cb11424711231777D71689C458eE835D",
"5": "0x5c741a38cb11424711231777D71689C458eE835D",
"420": "0x47443D0C184e022F19BD1578F5bca6B8a9F58E32"
},
"Drippie": {
"1": "0x44b3A2a040057eBafC601A78647e805fd58B1f50"
},
"Drippie_goerli": {
"5": "0x44b3A2a040057eBafC601A78647e805fd58B1f50"
},
"Drippie_optimism-goerli": {
"420": "0x8D8d533C16D23847EB04EEB0925be8900Dd3af86"
},
"EAS": {
"10": "0x4E0275Ea5a89e7a3c1B58411379D1a0eDdc5b088",
"420": "0x5A633F1cc84B03F7588486CF2F386c102061E6e1"
},
"GasPriceOracle": {
"420": "0x420000000000000000000000000000000000000F"
},
"L1Block": {
"420": "0x4200000000000000000000000000000000000015"
},
"L1CrossDomainMessenger": {
"1": "0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1",
"5": "0x5086d1eEF304eb5284A0f6720f79403b4e9bE294"
},
"L1ERC721Bridge": {
"1": "0x5a7749f83b81B301cAb5f48EB8516B986DAef23D",
"5": "0x8DD330DdE8D9898d43b4dc840Da27A07dF91b3c9"
},
"L1FeeVault": {
"420": "0x420000000000000000000000000000000000001a"
},
"L1StandardBridge": {
"1": "0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1",
"5": "0x636Af16bf2f682dD3109e60102b8E1A089FedAa8"
},
"L2CrossDomainMessenger": {
"420": "0x4200000000000000000000000000000000000007"
},
"L2ERC721Bridge": {
"10": "0x4200000000000000000000000000000000000014"
},
"L2ERC721Bridge_optimism-goerli": {
"420": "0x4200000000000000000000000000000000000014"
},
"L2OutputOracle": {
"1": "0xdfe97868233d1aa22e815a266982f2cf17685a27",
"5": "0xE6Dfba0953616Bacab0c9A8ecb3a9BBa77FC15c0"
},
"L2StandardBridge": {
"420": "0x4200000000000000000000000000000000000010"
},
"L2ToL1MessagePasser": {
"420": "0x4200000000000000000000000000000000000016"
},
"MintManager": {
"10": "0x5C4e7Ba1E219E47948e6e3F55019A647bA501005",
"420": "0x038a8825A3C3B0c08d52Cc76E5E361953Cf6Dc76"
},
"OptimismMintableERC20Factory": {
"1": "0x4200000000000000000000000000000000000012"
},
"OptimismMintableERC20Factory_goerli": {
"5": "0x4200000000000000000000000000000000000012"
},
"OptimismMintableERC20Factory_optimism-goerli": {
"420": "0x4200000000000000000000000000000000000012"
},
"OptimismMintableERC721Factory": {
"10": "0x4200000000000000000000000000000000000017"
},
"OptimismMintableERC721Factory_optimism-goerli": {
"420": "0x4200000000000000000000000000000000000017"
},
"OptimismPortal": {
"1": "0xbEb5Fc579115071764c7423A4f12eDde41f106Ed",
"5": "0x5b47E1A08Ea6d985D6649300584e6722Ec4B1383"
},
"Optimist": {
"10": "0x2335022c740d17c2837f9C884Bfe4fFdbf0A95D5",
"420": "0x2335022c740d17c2837f9C884Bfe4fFdbf0A95D5"
},
"OptimistAllowlist": {
"10": "0x482b1945D58f2E9Db0CEbe13c7fcFc6876b41180",
"420": "0x482b1945D58f2E9Db0CEbe13c7fcFc6876b41180"
},
"OptimistInviter": {
"10": "0x073031A1E1b8F5458Ed41Ce56331F5fd7e1de929",
"420": "0x073031A1E1b8F5458Ed41Ce56331F5fd7e1de929"
},
"PortalSender": {
"1": "0x0A893d9576b9cFD9EF78595963dc973238E78210",
"5": "0xe7FACd39531ee3C313330E93B4d7a8B8A3c84Aa4"
},
"ProtocolVersions": {
"5": "0x0C24F5098774aA366827D667494e9F889f7cFc08"
},
"ProxyAdmin": {
"1": "0x4200000000000000000000000000000000000018",
"5": "0x4200000000000000000000000000000000000018"
},
"SchemaRegistry": {
"10": "0x6232208d66bAc2305b46b4Cb6BCB3857B298DF13",
"420": "0x2545fa928d5d278cA75Fd47306e4a89096ff6403"
},
"SequencerFeeVault": {
"420": "0x4200000000000000000000000000000000000011"
},
"SystemConfig": {
"1": "0x229047fed2591dbec1eF1118d64F7aF3dB9EB290",
"5": "0xAe851f927Ee40dE99aaBb7461C00f9622ab91d60"
},
"SystemDictator": {
"1": "0xB4453CEb33d2e67FA244A24acf2E50CEF31F53cB"
},
"SystemDictator_goerli": {
"5": "0x1f0613A44c9a8ECE7B3A2e0CdBdF0F5B47A50971"
},
"TeleportrWithdrawer": {
"1": "0x78A25524D90E3D0596558fb43789bD800a5c3007"
}
}
VITE_RPC_URL_L2_GOERLI=
VITE_RPC_URL_L2_MAINNET=
VITE_RPC_URL_L1_GOERLI=
VITE_RPC_URL_L1_MAINNET=
{
"name": "@eth-optimism/contracts-ts",
"version": "0.17.2",
"description": "TypeScript interface for Contracts Bedrock",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/ethereum-optimism/optimism.git",
"directory": "packages/contracts-ts"
},
"homepage": "https://optimism.io",
"type": "module",
"main": "dist/constants.cjs",
"module": "dist/constants.js",
"types": "src/constants.ts",
"exports": {
".": {
"types": "./src/constants.ts",
"import": "./dist/constants.js",
"require": "./dist/constants.cjs"
},
"./actions": {
"types": "./src/actions.ts",
"import": "./dist/actions.js",
"require": "./dist/actions.cjs"
},
"./react": {
"types": "./src/react.ts",
"import": "./dist/react.js",
"require": "./dist/react.cjs"
}
},
"files": [
"dist/",
"src/"
],
"scripts": {
"build": "tsup",
"clean": "rm -rf ./dist",
"generate": "wagmi generate && pnpm build && pnpm lint:fix",
"generate:check": "pnpm generate && git diff --exit-code ./addresses.json && git diff --exit-code ./abis.json",
"lint": "prettier --check .",
"lint:fix": "prettier --write .",
"test": "vitest",
"test:coverage": "vitest run --coverage",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@eth-optimism/contracts-bedrock": "workspace:*",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react-hooks": "^8.0.1",
"@types/glob": "^8.1.0",
"@vitest/coverage-istanbul": "^1.2.2",
"@wagmi/cli": "^2.1.4",
"@wagmi/core": "^2.6.3",
"abitype": "^1.0.2",
"glob": "^10.3.10",
"isomorphic-fetch": "^3.0.0",
"jest-dom": "link:@types/@testing-library/jest-dom",
"jsdom": "^24.0.0",
"tsup": "^8.0.1",
"typescript": "^5.4.5",
"vite": "^5.1.7",
"wagmi": "^2.5.19",
"vitest": "^1.2.2"
},
"peerDependencies": {
"@wagmi/core": "^2.6.3",
"wagmi": "^2.5.19"
},
"peerDependenciesMeta": {
"wagmi": {
"optional": true
},
"@wagmi/core": {
"optional": true
}
},
"dependencies": {
"@testing-library/react": "^14.2.1",
"@types/change-case": "^2.3.1",
"change-case": "4.1.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"viem": "^2.8.13"
}
}
import fetch from 'isomorphic-fetch'
// viem needs this
global.fetch = fetch
This source diff could not be displayed because it is too large. You can view the blob instead.
import { test, expect } from 'vitest'
import { addresses } from './constants'
import { readFileSync } from 'fs'
import { join } from 'path'
const jsonAddresses = JSON.parse(
readFileSync(join(__dirname, '../addresses.json'), 'utf8')
)
test('should have generated addresses', () => {
expect(addresses).toEqual(jsonAddresses)
})
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
/// <reference types="vite/client" />
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"baseUrl": "./src",
"strict": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "node",
"jsx": "react",
"target": "ESNext",
"noEmit": true
},
"include": ["./src"]
}
import { defineConfig } from 'tsup'
import packageJson from './package.json'
export default defineConfig({
name: packageJson.name,
entry: ['src/constants.ts', 'src/actions.ts', 'src/react.ts'],
outDir: 'dist',
format: ['esm', 'cjs'],
splitting: false,
sourcemap: true,
clean: false,
})
import { defineConfig } from 'vitest/config'
// @see https://vitest.dev/config/
export default defineConfig({
test: {
setupFiles: './setupVitest.ts',
environment: 'jsdom',
coverage: {
provider: 'istanbul',
},
},
})
import { ContractConfig, defineConfig, Plugin } from '@wagmi/cli'
import { actions, react } from '@wagmi/cli/plugins'
import * as glob from 'glob'
import { readFileSync, writeFileSync } from 'fs'
import type { Abi, Address } from 'abitype'
import { isDeepStrictEqual } from 'util'
import { camelCase, constantCase } from 'change-case'
/**
* Predeployed contract addresses
* In future it would be nice to have a json file in contracts bedrock be generated as source of truth
* Keep this in sync with op-service/predeploys/addresses.go in meantime
*/
const predeployContracts = {
LegacyMessagePasser: {
address: '0x4200000000000000000000000000000000000000',
introduced: 'Legacy',
deprecated: true,
proxied: true,
},
DeployerWhitelist: {
address: '0x4200000000000000000000000000000000000002',
introduced: 'Legacy',
deprecated: true,
proxied: true,
},
LegacyERC20ETH: {
address: '0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000',
introduced: 'Legacy',
deprecated: true,
proxied: false,
},
WETH9: {
address: '0x4200000000000000000000000000000000000006',
introduced: 'Legacy',
deprecated: false,
proxied: false,
},
L2CrossDomainMessenger: {
address: '0x4200000000000000000000000000000000000007',
introduced: 'Legacy',
deprecated: false,
proxied: true,
},
L2StandardBridge: {
address: '0x4200000000000000000000000000000000000010',
introduced: 'Legacy',
deprecated: false,
proxied: true,
},
SequencerFeeVault: {
address: '0x4200000000000000000000000000000000000011',
introduced: 'Legacy',
deprecated: false,
proxied: true,
},
OptimismMintableERC20Factory: {
address: '0x4200000000000000000000000000000000000012',
introduced: 'Legacy',
deprecated: false,
proxied: true,
},
L1BlockNumber: {
address: '0x4200000000000000000000000000000000000013',
introduced: 'Legacy',
deprecated: true,
proxied: true,
},
GasPriceOracle: {
address: '0x420000000000000000000000000000000000000F',
introduced: 'Legacy',
deprecated: false,
proxied: true,
},
GovernanceToken: {
address: '0x4200000000000000000000000000000000000042',
introduced: 'Legacy',
deprecated: false,
proxied: false,
},
L1Block: {
address: '0x4200000000000000000000000000000000000015',
introduced: 'Bedrock',
deprecated: false,
proxied: true,
},
L2ToL1MessagePasser: {
address: '0x4200000000000000000000000000000000000016',
introduced: 'Bedrock',
deprecated: false,
proxied: true,
},
L2ERC721Bridge: {
address: '0x4200000000000000000000000000000000000014',
introduced: 'Legacy',
deprecated: false,
proxied: true,
},
OptimismMintableERC721Factory: {
address: '0x4200000000000000000000000000000000000017',
introduced: 'Bedrock',
deprecated: false,
proxied: true,
},
ProxyAdmin: {
address: '0x4200000000000000000000000000000000000018',
introduced: 'Bedrock',
deprecated: false,
proxied: true,
},
BaseFeeVault: {
address: '0x4200000000000000000000000000000000000019',
introduced: 'Bedrock',
deprecated: false,
proxied: true,
},
L1FeeVault: {
address: '0x420000000000000000000000000000000000001a',
introduced: 'Bedrock',
deprecated: false,
proxied: true,
},
} as const
type DeploymentJson = {
abi: Abi
address: `0x${string}`
}
const chains = {
1: 'mainnet',
10: 'optimism-mainnet',
5: 'goerli',
420: 'optimism-goerli',
} as const
if (!glob.sync('node_modules/*').length) {
throw new Error(
'No node_modules found. Please run `pnpm install` before running this script'
)
}
const deployments = {
[1]: glob.sync(
`node_modules/@eth-optimism/contracts-bedrock/deployments/${chains[1]}/*.json`
),
[10]: glob.sync(
`node_modules/@eth-optimism/contracts-bedrock/deployments/${chains[10]}/*.json`
),
[5]: glob.sync(
`node_modules/@eth-optimism/contracts-bedrock/deployments/${chains[5]}/*.json`
),
[420]: glob.sync(
`node_modules/@eth-optimism/contracts-bedrock/deployments/${chains[420]}/*.json`
),
}
Object.entries(deployments).forEach(([chain, deploymentFiles]) => {
if (deploymentFiles.length === 0) {
throw new Error(`No bedrock deployments found for ${chains[chain]}`)
}
})
const getWagmiContracts = (
deploymentFiles: string[],
filterDuplicates = false
) =>
deploymentFiles.map((artifactPath) => {
const deployment = JSON.parse(
readFileSync(artifactPath, 'utf8')
) as DeploymentJson
// There is a known bug in the wagmi/cli repo where some contracts have FOO_CASE and fooCase in same contract causing issues
// This is a common pattern at OP
// @see https://github.com/wagmi-dev/wagmi/issues/2724
const abi = filterDuplicates
? deployment.abi.filter((item) => {
if (item.type !== 'function') {
return true
}
if (item.name !== constantCase(item.name)) {
return true
}
// if constante case make sure it is not a duplicate
// e.g. make sure fooBar doesn't exist with FOO_BAR
return !deployment.abi.some(
(otherItem) =>
otherItem.type === 'function' &&
otherItem.name !== item.name &&
otherItem.name === camelCase(item.name)
)
})
: deployment.abi
const contractConfig = {
abi,
name: artifactPath.split('/').reverse()[0]?.replace('.json', ''),
address: deployment.address,
} satisfies ContractConfig
if (!contractConfig.name) {
throw new Error(
'Unable to identify the name of the contract at ' + artifactPath
)
}
return contractConfig
})
/**
* Returns the contracts for the wagmi cli config
*/
const getContractConfigs = (filterDuplicates = false) => {
const contracts = {
1: getWagmiContracts(deployments[1], filterDuplicates),
10: getWagmiContracts(deployments[10], filterDuplicates),
5: getWagmiContracts(deployments[5], filterDuplicates),
420: getWagmiContracts(deployments[420], filterDuplicates),
}
const allContracts = Object.values(contracts).flat()
const config: ContractConfig[] = []
// this for loop is not terribly efficient but seems fast enough for the scale here
for (const contract of allContracts) {
// we will only process the implementation ABI but will use teh proxy addresses for deployments
const isProxy = contract.name.endsWith('Proxy')
// once we see the first deployment of a contract we will process all networks all at once
const alreadyProcessedContract = config.find(
(c) => c.name === contract.name
)
if (isProxy || alreadyProcessedContract) {
continue
}
const implementations = {
// @warning Later code assumes mainnet is first!!!
[1]: contracts[1].find((c) => c.name === contract.name),
// @warning Later code assumes mainnet is first!!!
[10]: contracts[10].find((c) => c.name === contract.name),
[5]: contracts[5].find((c) => c.name === contract.name),
[420]: contracts[420].find((c) => c.name === contract.name),
}
const maybeProxyName = contract.name + 'Proxy'
const proxies = {
// @warning Later code assumes mainnet is first!!!
[1]: contracts[1].find((c) => c.name === maybeProxyName),
// @warning Later code assumes mainnet is first!!!
[10]: contracts[10].find((c) => c.name === maybeProxyName),
[5]: contracts[5].find((c) => c.name === maybeProxyName),
[420]: contracts[420].find((c) => c.name === maybeProxyName),
}
const predeploy = predeployContracts[
contract.name as keyof typeof predeployContracts
] as { address: Address } | undefined
// If the contract has different abis on different networks we don't want to group them as a single abi
const isContractUnique = !Object.values(implementations).some(
(implementation) =>
implementation && !isDeepStrictEqual(implementation.abi, contract.abi)
)
if (!isContractUnique) {
Object.entries(implementations)
.filter(([_, implementation]) => implementation)
.forEach(([chain, implementation], i) => {
if (implementation) {
// make the first one canonical. This will be mainnet or op mainnet if they exist
const name =
i === 0 ? contract.name : `${contract.name}_${chains[chain]}`
const nextConfig = {
abi: implementation.abi,
name,
address: {
[Number.parseInt(chain)]:
predeploy?.address ??
proxies[chain]?.address ??
implementation?.address,
}, // predeploy?.address ?? proxies[chain]?.address ?? implementation?.address
} satisfies ContractConfig
config.push(nextConfig)
}
})
continue
}
const wagmiConfig = {
abi: contract.abi,
name: contract.name,
address: {},
} satisfies ContractConfig
Object.entries(implementations).forEach(([chain, proxy]) => {
if (proxy) {
wagmiConfig.address[chain] =
predeploy?.address ?? proxy.address ?? contract.address
}
})
// if proxies exist overwrite the address with the proxy address
Object.entries(proxies).forEach(([chain, proxy]) => {
if (proxy) {
wagmiConfig.address[chain] = predeploy?.address ?? proxy.address
}
})
config.push(wagmiConfig)
}
return config
}
/**
* This plugin will create a addresses mapping from contract name to address
*/
const addressesByContractByNetworkPlugin: Plugin = {
name: 'addressesByContractByNetwork',
run: async ({ contracts }) => {
const addresses = Object.fromEntries(
contracts.map((contract) => [contract.name, contract.address ?? {}])
)
// write to json file so it's easy to audit in prs relative to the generated file diff
writeFileSync('./addresses.json', JSON.stringify(addresses, null, 2))
return {
content: [
`export const addresses = ${JSON.stringify(addresses)} as const`,
`export const predeploys = ${JSON.stringify(predeployContracts)}`,
].join('\n'),
}
},
}
/**
* This plugin will create an abi mapping from contract name to abi
*/
const abiPlugin: Plugin = {
name: 'abisByContractByNetwork',
run: async ({ contracts }) => {
const abis = Object.fromEntries(
contracts.map((contract) => [contract.name, contract.abi])
)
// write to json file so it's easy to audit in prs relative to the generated file diff
writeFileSync('./abis.json', JSON.stringify(abis, null, 2))
return {
content: `export const abis = ${JSON.stringify(abis)} as const`,
}
},
}
/**
* This plugin adds an eslint ignore to the generated code
*/
const eslintIgnorePlugin: Plugin = {
name: 'eslintIgnore',
run: async () => {
return {
prepend: `/* eslint-disable */`,
content: ``,
}
},
}
const contracts = getContractConfigs()
// there is a known wagmi bug with contracts who have both FOO_BAR and fooBar method
const contractsWithFilteredDuplicates = getContractConfigs(true)
// @see https://wagmi.sh/cli
export default defineConfig([
{
out: 'src/constants.ts',
contracts,
plugins: [
eslintIgnorePlugin,
addressesByContractByNetworkPlugin,
abiPlugin,
],
},
{
out: 'src/actions.ts',
contracts: contractsWithFilteredDuplicates,
plugins: [
eslintIgnorePlugin,
actions({
getContract: true,
// don't include actions because they can be more simply done via getContract
prepareWriteContract: false,
readContract: false,
watchContractEvent: false,
writeContract: false,
}),
],
},
{
out: 'src/react.ts',
contracts: contractsWithFilteredDuplicates,
plugins: [
eslintIgnorePlugin,
react({
useContractRead: true,
useContractWrite: true,
useContractEvent: true,
// don't include more niche actions to keep api more simple
useContractFunctionRead: false,
useContractFunctionWrite: false,
useContractItemEvent: false,
usePrepareContractFunctionWrite: false,
usePrepareContractWrite: false,
}),
],
},
])
ignores: [
"@babel/eslint-parser",
"@typescript-eslint/parser",
"eslint-plugin-import",
"eslint-plugin-unicorn",
"eslint-plugin-jsdoc",
"eslint-plugin-prefer-arrow",
"eslint-plugin-react",
"@typescript-eslint/eslint-plugin",
"eslint-config-prettier",
"eslint-plugin-prettier",
"chai"
]
module.exports = {
extends: '../../.eslintrc.js',
}
module.exports = {
...require('../../.prettierrc.js'),
};
\ No newline at end of file
# @eth-optimism/core-utils
## 0.13.2
### Patch Changes
- [#9964](https://github.com/ethereum-optimism/optimism/pull/9964) [`8241220898128e1f61064f22dcb6fdd0a5f043c3`](https://github.com/ethereum-optimism/optimism/commit/8241220898128e1f61064f22dcb6fdd0a5f043c3) Thanks [@roninjin10](https://github.com/roninjin10)! - Removed only-allow command from package.json
## 0.13.1
### Patch Changes
- [#7450](https://github.com/ethereum-optimism/optimism/pull/7450) [`ac90e16a7`](https://github.com/ethereum-optimism/optimism/commit/ac90e16a7f85c4f73661ae6023135c3d00421c1e) Thanks [@roninjin10](https://github.com/roninjin10)! - Updated dev dependencies related to testing that is causing audit tooling to report failures
## 0.13.0
### Minor Changes
- [#7336](https://github.com/ethereum-optimism/optimism/pull/7336) [`210b2c81d`](https://github.com/ethereum-optimism/optimism/commit/210b2c81dd383bad93480aa876b283d9a0c991c2) Thanks [@tynes](https://github.com/tynes)! - Delete unmaintained geth types
## 0.12.3
### Patch Changes
- [#6797](https://github.com/ethereum-optimism/optimism/pull/6797) [`dfa309e34`](https://github.com/ethereum-optimism/optimism/commit/dfa309e3430ebc8790b932554dde120aafc4161e) Thanks [@roninjin10](https://github.com/roninjin10)! - Upgraded npm dependencies to latest
## 0.12.2
### Patch Changes
- [#6164](https://github.com/ethereum-optimism/optimism/pull/6164) [`c11039060`](https://github.com/ethereum-optimism/optimism/commit/c11039060bc037a88916c2cba602687b6d69ad1a) Thanks [@pengin7384](https://github.com/pengin7384)! - fix typo
- [#6198](https://github.com/ethereum-optimism/optimism/pull/6198) [`77da6edc6`](https://github.com/ethereum-optimism/optimism/commit/77da6edc643e0b5e39f7b6bb41c3c7ead418a876) Thanks [@tremarkley](https://github.com/tremarkley)! - Delete dead typescript https://github.com/ethereum-optimism/optimism/pull/6148.
## 0.12.1
### Patch Changes
- 8d7dcc70c: Delete legacy core-utils
- d6388be4a: Added a new service wallet-mon to identify unexpected transfers from key accounts
## 0.12.0
### Minor Changes
- c975c9620: Add suppory for finalizing legacy withdrawals after the Bedrock migration
### Patch Changes
- 136ea1785: Refactors the L2OutputOracle to key the l2Outputs mapping by index instead of by L2 block number.
## 0.11.0
### Minor Changes
- 1e76cdb86: Changes the type for Bedrock withdrawal proofs
## 0.10.1
### Patch Changes
- 7215f4ce: Bump ethers to 5.7.0 globally
- 206f6033: Fix outdated references to 'withdrawal contract'
## 0.10.0
### Minor Changes
- dbfea116: Removes ethers as a dependency in favor of individual ethers sub-packages
## 0.9.3
### Patch Changes
- 0df744f6: Implement basic OpNodeProvider
- 8ae39154: Update deposit transaction type
- dac4a9f0: Updates the SDK to be compatible with Bedrock (via the "bedrock: true" constructor param). Updates the build pipeline for contracts-bedrock to export a properly formatted dist folder that matches our other packages.
## 0.9.2
### Patch Changes
- 0bf3b9b4: Add encoding and hashing functions for bedrock
- 8d26459b: Remove subversion byte from deposit tx
- 4477fe9f: Update deposit transaction serialization
## 0.9.1
### Patch Changes
- f9fee446: Move the `DepositTx` type to `core-utils`. This way it can be more easily used across projects
## 0.9.0
### Minor Changes
- 700dcbb0: Update geth's Genesis type to work with modern geth
## 0.8.7
### Patch Changes
- 29ff7462: Revert es target back to 2017
## 0.8.6
### Patch Changes
- 17962ca9: Update geth genesis type
## 0.8.5
### Patch Changes
- d18ae135: Updates all ethers versions in response to BN.js bug
## 0.8.4
### Patch Changes
- 5cb3a5f7: Add a `calldataCost` function that computes the cost of calldata
- 6b9fc055: Adds a one-liner for getting chain ID from provider
## 0.8.3
### Patch Changes
- b57014d1: Update to typescript@4.6.2
## 0.8.2
### Patch Changes
- c1957126: Update Dockerfile to use Alpine
## 0.8.1
### Patch Changes
- 5a6f539c: Add toJSON methods to the batch primitives
- 27d8942e: Update batch serialization with typed batches and zlib compression
## 0.8.0
### Minor Changes
- 0b4453f7: Deletes the Watcher and injectL2Context functions. Use the SDK instead.
## 0.7.7
### Patch Changes
- b4165299: Added tests and docstrings to misc functions
- 3c2acd91: Refactor folder structure of @eth-optimism/core-utils.
## 0.7.6
### Patch Changes
- ba14c59d: Updates various ethers dependencies to their latest versions
## 0.7.5
### Patch Changes
- ad94b9d1: test/docs: Improve docstrings and tests for utils inside of hex-strings.ts
## 0.7.4
### Patch Changes
- ba96a455: Improved docstrings for BCFG typings
- c3e85fef: Cleans up the internal file and folder structure for the typings exported by core-utils
## 0.7.3
### Patch Changes
- 584cbc25: Clean up the L1 => L2 address aliasing utilities
## 0.7.2
### Patch Changes
- 8e634b49: Fix package JSON issues
## 0.7.1
### Patch Changes
- 243f33e5: Standardize package json file format
## 0.7.0
### Minor Changes
- 896168e2: Parse optimistic ethereum specific fields on transaction receipts
- 83a449c4: Change the expectApprox interface to allow setting an absoluteexpected deviation range
- 81ccd6e4: `regenesis/0.5.0` release
### Patch Changes
- 3ce62c81: Export bnToAddress
- cee2a464: Add awaitCondition to core utils
- 222a3eef: Add 'User-Agent' to the http headers for ethers providers
- 7c352b1e: Add bytes32ify
- b70ee70c: upgraded to solidity 0.8.9
- 20c8969b: Correctly move chai into deps instead of dev deps
- 6d32d701: Expose lower level API for tx fees
## 0.6.1
### Patch Changes
- 6d3e1d7f: Update dependencies
- 2e929aa9: Parse the L1 timestamp in `injectContext`
## 0.6.0
### Minor Changes
- 8da04505: Allow a configurable L1 and L2 blocks to fetch in the watcher
### Patch Changes
- e0be02e1: Add fallback provider support to DTL using helper function in core-utils
## 0.5.5
### Patch Changes
- eb0854e7: increased coverage of core-utils
- 21b17edd: Added coverage for packages
- dfe3598f: Lower per tx fee overhead to more accurately represent L1 costs
## 0.5.4
### Patch Changes
- 085b35ba: Watcher: Even lower num blocks to fetch
## 0.5.3
### Patch Changes
- 2aa4416e: Watcher: Make blocks to fetch a config option
- 0b8180b0: Lower NUM_BLOCKS_TO_FETCH in Watcher
## 0.5.2
### Patch Changes
- 918c08ca: Bump ethers dependency to 5.4.x to support eip1559
## 0.5.1
### Patch Changes
- c73c3939: Update the typescript version to `4.3.5`
## 0.5.0
### Minor Changes
- 049200f4: removed unused functions from core-utils
## 0.4.7
### Patch Changes
- 224b04c0: Adds a pollInterval delay to watcher.ts
## 0.4.6
### Patch Changes
- d9644c34: Minor fix on watchers to pick up finalization of transactions on L1
- df5ff890: improved watcher ability to find transactions during periods of high load
## 0.4.5
### Patch Changes
- a64f8161: Implement the next fee spec in both geth and in core-utils
- 750a5021: Delete dead transaction coders. These are no longer used now that RLP encoded transactions are used
- c2b6e14b: Implement the latest fee spec such that the L2 gas limit is scaled and the tx.gasPrice/tx.gasLimit show correctly in metamask
## 0.4.4
### Patch Changes
- f091e86: Have watcher correctly handle failed L1 => L2 messages
- f880479: End to end fee integration with recoverable L2 gas limit
## 0.4.3
### Patch Changes
- 96a586e: Migrate bcfg interface to core-utils
## 0.4.2
### Patch Changes
- b799caa: Update toRpcHexString to accept ethers.BigNumber and add tests
## 0.4.1
### Patch Changes
- 1d40586: Removed various unused dependencies
- ce7fa52: Add an additional enum for EthSign transactions as they now are batch submitted with 2 different enum values
## 0.4.0
### Minor Changes
- 28dc442: move metrics, logger, and base-service to new common-ts package
### Patch Changes
- a0a0052: Update toRpcHexString to accept ethers.BigNumber and add tests
## 0.3.2
### Patch Changes
- 6daa408: update hardhat versions so that solc is resolved correctly
- dee74ef: migrate batch submitter types to core-utils
- d64b66d: reformat error context for Sentry
## 0.3.1
### Patch Changes
- 5077441: - Use raw transaction in batch submitter -- incompatible with L2Geth v0.1.2.1
- Pass through raw transaction in l2context
## 0.3.0
### Minor Changes
- 91460d9: add Metrics and use in base-service, rename DTL services to avoid spaces
- a0a7956: initialize Sentry and streams in Logger, remove Sentry from Batch Submitter
### Patch Changes
- 0497d7d: Re-organize event typings to core-utils
## 0.2.3
### Patch Changes
- 35b99b0: add Sentry to TypeScript services for error tracking
## 0.2.2
### Patch Changes
- 01eaf2c: added extra logs to base-service / dtl to improve observability
## 0.2.1
### Patch Changes
- 5362d38: adds build files which were not published before to npm
## 0.2.0
### Minor Changes
- 6cbc54d: allow injecting L2 transaction and block context via core-utils (this removes the need to import the now deprecated @eth-optimism/provider package)
(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.
# @eth-optimism/core-utils
[![codecov](https://codecov.io/gh/ethereum-optimism/optimism/branch/develop/graph/badge.svg?token=0VTG7PG7YR&flag=core-utils-tests)](https://codecov.io/gh/ethereum-optimism/optimism)
## What is this?
`@eth-optimism/core-utils` contains the Optimistic Virtual Machine core utilities.
## Getting started
### Building and usage
After cloning and switching to the repository, install dependencies:
```bash
$ pnpm i
```
Use the following commands to build, use, test, and lint:
```bash
$ pnpm build
$ pnpm start
$ pnpm test
$ pnpm lint
```
{
"name": "@eth-optimism/core-utils",
"version": "0.13.2",
"description": "[Optimism] Core typescript utilities",
"main": "dist/index",
"types": "dist/index",
"files": [
"dist/*"
],
"scripts": {
"all": "pnpm clean && pnpm build && pnpm test && pnpm lint:fix && pnpm lint",
"build": "tsc -p tsconfig.json",
"clean": "rimraf dist/ ./tsconfig.tsbuildinfo",
"lint": "pnpm lint:fix && pnpm lint:check",
"lint:check": "eslint . --max-warnings=0",
"lint:fix": "pnpm lint:check --fix",
"pre-commit": "lint-staged",
"test": "ts-mocha test/**/*.spec.ts",
"test:coverage": "nyc ts-mocha test/**/*.spec.ts && nyc merge .nyc_output coverage.json"
},
"keywords": [
"optimism",
"ethereum",
"core",
"utils"
],
"homepage": "https://github.com/ethereum-optimism/optimism/tree/develop/packages/core-utils#readme",
"license": "MIT",
"author": "Optimism PBC",
"repository": {
"type": "git",
"url": "https://github.com/ethereum-optimism/optimism.git"
},
"dependencies": {
"@ethersproject/abi": "^5.7.0",
"@ethersproject/abstract-provider": "^5.7.0",
"@ethersproject/address": "^5.7.0",
"@ethersproject/bignumber": "^5.7.0",
"@ethersproject/bytes": "^5.7.0",
"@ethersproject/constants": "^5.7.0",
"@ethersproject/contracts": "^5.7.0",
"@ethersproject/keccak256": "^5.7.0",
"@ethersproject/properties": "^5.7.0",
"@ethersproject/rlp": "^5.7.0",
"@ethersproject/web": "^5.7.1",
"chai": "^4.3.10",
"ethers": "^5.7.2",
"node-fetch": "^2.6.7"
},
"devDependencies": {
"@types/node": "^20.11.17",
"mocha": "^10.2.0"
}
}
// Use this file for simple types that aren't necessarily associated with a specific project or
// package. Often used for alias types like Address = string.
export interface Signature {
r: string
s: string
v: number
}
export type Bytes32 = string
export type Uint16 = number
export type Uint8 = number
export type Uint24 = number
export type Address = string
import { BigNumber } from '@ethersproject/bignumber'
import { getAddress } from '@ethersproject/address'
import { remove0x, add0x } from './hex-strings'
/**
* Converts an ethers BigNumber into an equivalent Ethereum address representation.
*
* @param bn BigNumber to convert to an address.
* @return BigNumber converted to an address, represented as a hex string.
*/
export const bnToAddress = (bn: BigNumber | number): string => {
// Coerce numbers into a BigNumber.
bn = BigNumber.from(bn)
// Negative numbers are converted to addresses by adding MAX_ADDRESS + 1.
// TODO: Explain this in more detail, it's basically just matching the behavior of doing
// addr(uint256(addr) - some_number) in Solidity where some_number > uint256(addr).
if (bn.isNegative()) {
bn = BigNumber.from('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF')
.add(bn)
.add(1)
}
// Convert to a hex string
let addr = bn.toHexString()
// Remove leading 0x so we can mutate the address a bit
addr = remove0x(addr)
// Make sure it's 40 characters (= 20 bytes)
addr = addr.padStart(40, '0')
// Only take the last 40 characters (= 20 bytes)
addr = addr.slice(addr.length - 40, addr.length)
// Add 0x again
addr = add0x(addr)
// Convert into a checksummed address
addr = getAddress(addr)
return addr
}
/* Imports: External */
import { BigNumber } from '@ethersproject/bignumber'
import { isHexString, hexZeroPad } from '@ethersproject/bytes'
/**
* Removes "0x" from start of a string if it exists.
*
* @param str String to modify.
* @returns the string without "0x".
*/
export const remove0x = (str: string): string => {
if (str === undefined) {
return str
}
return str.startsWith('0x') ? str.slice(2) : str
}
/**
* Adds "0x" to the start of a string if necessary.
*
* @param str String to modify.
* @returns the string with "0x".
*/
export const add0x = (str: string): string => {
if (str === undefined) {
return str
}
return str.startsWith('0x') ? str : '0x' + str
}
/**
* Casts a hex string to a buffer.
*
* @param inp Input to cast to a buffer.
* @return Input cast as a buffer.
*/
export const fromHexString = (inp: Buffer | string): Buffer => {
if (typeof inp === 'string' && inp.startsWith('0x')) {
return Buffer.from(inp.slice(2), 'hex')
}
return Buffer.from(inp)
}
/**
* Casts an input to a hex string.
*
* @param inp Input to cast to a hex string.
* @return Input cast as a hex string.
*/
export const toHexString = (inp: Buffer | string | number | null): string => {
if (typeof inp === 'number') {
return BigNumber.from(inp).toHexString()
} else {
return '0x' + fromHexString(inp).toString('hex')
}
}
/**
* Casts a number to a hex string without zero padding.
*
* @param n Number to cast to a hex string.
* @return Number cast as a hex string.
*/
export const toRpcHexString = (n: number | BigNumber): string => {
let num
if (typeof n === 'number') {
num = '0x' + n.toString(16)
} else {
num = n.toHexString()
}
if (num === '0x0') {
return num
} else {
// BigNumber pads a single 0 to keep hex length even
return num.replace(/^0x0/, '0x')
}
}
/**
* Zero pads a hex string if str.length !== 2 + length * 2. Pads to length * 2.
*
* @param str Hex string to pad
* @param length Half the length of the desired padded hex string
* @return Hex string with length of 2 + length * 2
*/
export const padHexString = (str: string, length: number): string => {
if (str.length === 2 + length * 2) {
return str
} else {
return '0x' + str.slice(2).padStart(length * 2, '0')
}
}
/**
* Casts an input to hex string without '0x' prefix with conditional padding.
* Hex string will always start with a 0.
*
* @param val Input to cast to a hex string.
* @param len Desired length to pad hex string. Ignored if less than hex string length.
* @return Hex string with '0' prefix
*/
export const encodeHex = (val: any, len: number): string =>
remove0x(BigNumber.from(val).toHexString()).padStart(len, '0')
/**
* Case insensitive hex string equality check
*
* @param stringA Hex string A
* @param stringB Hex string B
* @throws {Error} Inputs must be valid hex strings
* @return True if equal
*/
export const hexStringEquals = (stringA: string, stringB: string): boolean => {
if (!isHexString(stringA)) {
throw new Error(`input is not a hex string: ${stringA}`)
}
if (!isHexString(stringB)) {
throw new Error(`input is not a hex string: ${stringB}`)
}
return stringA.toLowerCase() === stringB.toLowerCase()
}
/**
* Casts a number to a 32-byte, zero padded hex string.
*
* @param value Number to cast to a hex string.
* @return Number cast as a hex string.
*/
export const bytes32ify = (value: number | BigNumber): string => {
return hexZeroPad(BigNumber.from(value).toHexString(), 32)
}
/**
* Common JavaScript/TypeScript utilities
*/
export * from './basic-types'
export * from './bn'
export * from './hex-strings'
export * from './misc'
export * from './test-utils'
/**
* Basic timeout-based async sleep function.
*
* @param ms Number of milliseconds to sleep.
*/
export const sleep = async (ms: number): Promise<void> => {
return new Promise<void>((resolve) => {
setTimeout(() => {
resolve(null)
}, ms)
})
}
/**
* Returns a clone of the object.
*
* @param obj Object to clone.
* @returns Clone of the object.
*/
export const clone = (obj: any): any => {
if (typeof obj === 'undefined') {
throw new Error(`Trying to clone undefined object`)
}
return { ...obj }
}
/**
* Loads a variable from the environment and throws if the variable is not defined.
*
* @param name Name of the variable to load.
* @returns Value of the variable as a string.
*/
export const reqenv = (name: string): string => {
const value = process.env[name]
if (value === undefined) {
throw new Error(`missing env var ${name}`)
}
return value
}
/**
* Loads a variable from the environment and returns a fallback if not found.
*
* @param name Name of the variable to load.
* @param [fallback] Optional value to be returned as fallback.
* @returns Value of the variable as a string, fallback or undefined.
*/
export const getenv = (name: string, fallback?: string): string | undefined => {
return process.env[name] || fallback
}
/**
* Returns true if the given string is a valid address.
*
* @param a First address to check.
* @param b Second address to check.
* @returns True if the given addresses match.
*/
export const compareAddrs = (a: string, b: string): boolean => {
return a.toLowerCase() === b.toLowerCase()
}
import { expect } from 'chai'
import { BigNumber } from '@ethersproject/bignumber'
import { sleep } from './misc'
interface deviationRanges {
percentUpperDeviation?: number
percentLowerDeviation?: number
absoluteUpperDeviation?: number
absoluteLowerDeviation?: number
}
export const awaitCondition = async (
cond: () => Promise<boolean>,
rate = 1000,
attempts = 10
) => {
for (let i = 0; i < attempts; i++) {
const ok = await cond()
if (ok) {
return
}
await sleep(rate)
}
throw new Error('Timed out.')
}
/**
* Assert that a number lies within a custom defined range of the target.
*/
export const expectApprox = (
actual: BigNumber | number,
target: BigNumber | number,
{
percentUpperDeviation,
percentLowerDeviation,
absoluteUpperDeviation,
absoluteLowerDeviation,
}: deviationRanges
): void => {
actual = BigNumber.from(actual)
target = BigNumber.from(target)
// Ensure at least one deviation parameter is defined
const nonNullDeviations =
percentUpperDeviation ||
percentLowerDeviation ||
absoluteUpperDeviation ||
absoluteLowerDeviation
if (!nonNullDeviations) {
throw new Error(
'Must define at least one parameter to limit the deviation of the actual value.'
)
}
// Upper bound calculation.
let upper: BigNumber
// Set the two possible upper bounds if and only if they are defined.
const upperPcnt: BigNumber = !percentUpperDeviation
? null
: target.mul(100 + percentUpperDeviation).div(100)
const upperAbs: BigNumber = !absoluteUpperDeviation
? null
: target.add(absoluteUpperDeviation)
if (upperPcnt && upperAbs) {
// If both are set, take the lesser of the two upper bounds.
upper = upperPcnt.lte(upperAbs) ? upperPcnt : upperAbs
} else {
// Else take whichever is not undefined or set to null.
upper = upperPcnt || upperAbs
}
// Lower bound calculation.
let lower: BigNumber
// Set the two possible lower bounds if and only if they are defined.
const lowerPcnt: BigNumber = !percentLowerDeviation
? null
: target.mul(100 - percentLowerDeviation).div(100)
const lowerAbs: BigNumber = !absoluteLowerDeviation
? null
: target.sub(absoluteLowerDeviation)
if (lowerPcnt && lowerAbs) {
// If both are set, take the greater of the two lower bounds.
lower = lowerPcnt.gte(lowerAbs) ? lowerPcnt : lowerAbs
} else {
// Else take whichever is not undefined or set to null.
lower = lowerPcnt || lowerAbs
}
// Apply the assertions if they are non-null.
if (upper) {
expect(
actual.lte(upper),
`Actual value (${actual}) is greater than the calculated upper bound of (${upper})`
).to.be.true
}
if (lower) {
expect(
actual.gte(lower),
`Actual value (${actual}) is less than the calculated lower bound of (${lower})`
).to.be.true
}
}
import fetch from 'node-fetch'
interface NetworkData {
chainId: number
names: string[]
etherscanApiUrl: string
}
const networks: {
[id: number]: NetworkData
} = {
1: {
chainId: 1,
names: ['mainnet', 'main', 'eth', 'ethereum'],
etherscanApiUrl: 'https://api.etherscan.io',
},
3: {
chainId: 3,
names: ['ropsten'],
etherscanApiUrl: 'https://api-ropsten.etherscan.io',
},
4: {
chainId: 4,
names: ['rinkeby'],
etherscanApiUrl: 'https://api-rinkeby.etherscan.io',
},
5: {
chainId: 5,
names: ['goerli'],
etherscanApiUrl: 'https://api-goerli.etherscan.io',
},
10: {
chainId: 10,
names: ['optimism'],
etherscanApiUrl: 'https://api-optimistic.etherscan.io',
},
42: {
chainId: 42,
names: ['kovan'],
etherscanApiUrl: 'https://api-kovan.etherscan.io',
},
69: {
chainId: 69,
names: ['opkovan', 'kovan-optimism', 'optimistic-kovan'],
etherscanApiUrl: 'https://api-kovan-optimistic.etherscan.io',
},
}
export class Etherscan {
net: NetworkData
constructor(
private readonly apiKey: string,
private readonly network: string | number
) {
if (typeof network === 'string') {
this.net = Object.values(networks).find((net) => {
return net.names.includes(network)
})
} else {
this.net = networks[this.network]
}
}
public async getContractSource(address: string): Promise<any> {
const url = new URL(`${this.net.etherscanApiUrl}/api`)
url.searchParams.append('module', 'contract')
url.searchParams.append('action', 'getsourcecode')
url.searchParams.append('address', address)
url.searchParams.append('apikey', this.apiKey)
const response = await fetch(url)
const result = await response.json()
return (result as { result: number[] }).result[0]
}
public async getContractABI(address: string): Promise<any> {
const source = await this.getContractSource(address)
if (source.Proxy === '1') {
const impl = await this.getContractSource(source.Implementation)
return impl.ABI
} else {
return source.ABI
}
}
}
/**
* TypeScript typings for bcoin's BCFG config parser (https://github.com/bcoin-org/bcfg)
* This is NOT a complete set of typings, just what we use at Optimism at the moment.
* We could consider expanding this into a full set of typings in the future.
*/
export interface Bcfg {
/**
* Loads configuration values from the environment. Must be called before environment variables
* can be accessed with other methods like str(...) or uint(...).
*
* @param options Options to use when loading arguments.
* @param options.env Boolean, whether or not to load from process.env.
* @param options.argv Boolean, whether or not to load from process.argv.
*/
load: (options: { env?: boolean; argv?: boolean }) => void
/**
* Returns the variable with the given name and casts it as a string. Queries from the
* environment or from argv depending on which were loaded when load() was called.
*
* @param name Name of the variable to query.
* @param defaultValue Optional default value if the variable does not exist.
* @returns Variable cast to a string.
*/
str: (name: string, defaultValue?: string) => string
/**
* Returns the variable with the given name and casts it as a uint. Will throw an error if the
* variable cannot be cast into a uint. Queries from the environment or from argv depending on
* which were loaded when load() was called.
*
* @param name Name of the variable to query.
* @param defaultValue Optional default value if the variable does not exist.
* @returns Variable cast to a uint.
*/
uint: (name: string, defaultValue?: number) => number
/**
* Returns the variable with the given name and casts it as a bool. Will throw an error if the
* variable cannot be cast into a bool. Queries from the environment or from argv depending on
* which were loaded when load() was called.
*
* @param name Name of the variable to query.
* @param defaultValue Optional default value if the variable does not exist.
* @returns Variable cast to a bool.
*/
bool: (name: string, defaultValue?: boolean) => boolean
/**
* Returns the variable with the given name and casts it as a ufloat. Will throw an error if the
* variable cannot be cast into a ufloat. Queries from the environment or from argv depending on
* which were loaded when load() was called.
*
* @param name Name of the variable to query.
* @param defaultValue Optional default value if the variable does not exist.
* @returns Variable cast to a ufloat.
*/
ufloat: (name: string, defaultValue?: number) => number
/**
* Checks if the given variable exists.
*
* @param name Name of the variable to query.
* @returns True if the variable exists, false otherwise.
*/
has: (name: string) => boolean
}
/**
* Utilities related to BCFG
*/
export * from './bcfg-types'
/**
* Utilities that extend or enhance the ethers.js library
*/
export * from './network'
import { Provider } from '@ethersproject/abstract-provider'
export const getChainId = async (provider: Provider): Promise<number> => {
const network = await provider.getNetwork()
return network.chainId
}
/**
* Utilities related to specific external projects
*/
export * from './bcfg'
export * from './ethers'
import { ethers } from 'ethers'
// Slightly modified from:
// https://github.com/safe-global/safe-react-apps/blob/development/apps/tx-builder/src/lib/checksum.ts
const stringifyReplacer = (_: string, value: any) =>
value === undefined ? null : value
const serializeJSONObject = (json: any): string => {
if (Array.isArray(json)) {
return `[${json.map((el) => serializeJSONObject(el)).join(',')}]`
}
if (typeof json === 'object' && json !== null) {
let acc = ''
const keys = Object.keys(json).sort()
acc += `{${JSON.stringify(keys, stringifyReplacer)}`
for (const key of keys) {
acc += `${serializeJSONObject(json[key])},`
}
return `${acc}}`
}
return `${JSON.stringify(json, stringifyReplacer)}`
}
const calculateChecksum = (batchFile: any): string | undefined => {
const serialized = serializeJSONObject({
...batchFile,
meta: { ...batchFile.meta, name: null },
})
const sha = ethers.utils.solidityKeccak256(['string'], [serialized])
return sha || undefined
}
export const addChecksum = (batchFile: any): any => {
return {
...batchFile,
meta: {
...batchFile.meta,
checksum: calculateChecksum(batchFile),
},
}
}
import assert from 'assert'
import { ethers, utils } from 'ethers'
const { getAddress } = utils
type ProxyConfig = {
targetImplAddress: string
targetProxyOwnerAddress: string
postUpgradeCallCalldata?: string
}
// Sets up the newly deployed proxy contract such that:
// 1. The proxy's implementation is set to the target implementation
// 2. The proxy's admin is set to the target proxy owner
//
// If the values are set correctly already, it makes no transactions.
const setupProxyContract = async (
proxyContract: ethers.Contract,
signer: ethers.Signer,
{
targetImplAddress,
targetProxyOwnerAddress,
postUpgradeCallCalldata,
}: ProxyConfig
) => {
const currentAdmin = await proxyContract
.connect(ethers.constants.AddressZero)
.callStatic.admin()
const signerAddress = await signer.getAddress()
// Gets the current implementation address the proxy is pointing to.
// callStatic is used since the `Proxy.implementation()` is not a view function and ethers will
// try to make a transaction if we don't use callStatic. Using the zero address as `from` lets us
// call functions on the proxy and not trigger the delegatecall. See Proxy.sol proxyCallIfNotAdmin
// modifier for more details.
const currentImplementation = await proxyContract
.connect(ethers.constants.AddressZero)
.callStatic.implementation()
console.log(`implementation currently set to ${currentImplementation}`)
if (getAddress(currentImplementation) !== getAddress(targetImplAddress)) {
// If the proxy isn't pointing to the correct implementation, we need to set it to the correct
// one, then call initialize() in the proxy's context.
console.log('implementation not set to correct contract')
console.log(`Setting implementation to ${targetImplAddress}`)
// The signer needs to be the current admin, otherwise we don't have permission
// to update the implementation or admin
assert(
signerAddress === currentAdmin,
'the passed signer is not the admin, cannot update implementation'
)
let tx: ethers.providers.TransactionResponse
if (!postUpgradeCallCalldata) {
console.log(
'postUpgradeCallCalldata is not provided. Using Proxy.upgrade()'
)
// Point the proxy to the target implementation
tx = await proxyContract.connect(signer).upgradeTo(targetImplAddress)
} else {
console.log(
'postUpgradeCallCalldata is provided. Using Proxy.upgradeAndCall()'
)
// Point the proxy to the target implementation,
// and call function in the proxy's context
tx = await proxyContract
.connect(signer)
.upgradeToAndCall(targetImplAddress, postUpgradeCallCalldata)
}
const receipt = await tx.wait()
console.log(`implementation set in ${receipt.transactionHash}`)
} else {
console.log(`implementation already set correctly to ${targetImplAddress}`)
}
console.log(`admin set to ${currentAdmin}`)
if (getAddress(currentAdmin) !== getAddress(targetProxyOwnerAddress)) {
// If the proxy admin isn't the l2ProxyOwnerAddress, we need to update it
// We're assuming that the proxy admin is the ddd right now.
console.log('detected admin is not set correctly')
console.log(`Setting admin to ${targetProxyOwnerAddress}`)
// The signer needs to be the current admin, otherwise we don't have permission
// to update the implementation or admin
assert(
signerAddress === currentAdmin,
'proxyOwnerSigner is not the admin, cannot update admin'
)
// change admin to the l2ProxyOwnerAddress
const tx = await proxyContract
.connect(signer)
.changeAdmin(targetProxyOwnerAddress)
const receipt = await tx.wait()
console.log(`admin set in ${receipt.transactionHash}`)
} else {
console.log(`admin already set correctly to ${targetProxyOwnerAddress}`)
}
const updatedImplementation = await proxyContract
.connect(ethers.constants.AddressZero)
.callStatic.implementation()
const updatedAdmin = await proxyContract
.connect(ethers.constants.AddressZero)
.callStatic.admin()
assert(
getAddress(updatedAdmin) === getAddress(targetProxyOwnerAddress),
'Something went wrong - admin not set correctly after transaction'
)
assert(
getAddress(updatedImplementation) === getAddress(targetImplAddress),
'Something went wrong - implementation not set correctly after transaction'
)
console.log(
`Proxy at ${proxyContract.address} is set up with implementation: ${updatedImplementation} and admin: ${updatedAdmin}`
)
}
export { setupProxyContract }
export * from './common'
export * from './external'
export * from './optimism'
export * from './gnosis-safe-checksum'
export * from './etherscan'
export * from './helpers/setupProxyContract'
import { isAddress } from '@ethersproject/address'
import { BigNumber } from '@ethersproject/bignumber'
import { bnToAddress } from '../common'
// Constant representing the alias to apply to the msg.sender when a contract sends an L1 => L2
// message. We need this aliasing scheme because a contract can be deployed to the same address
// on both L1 and L2 but with different bytecode (address is not dependent on bytecode when using
// the standard CREATE opcode). We want to treat L1 contracts as having a different address while
// still making it possible for L2 contracts to easily reverse the aliasing scheme and figure out
// the real address of the contract that sent the L1 => L2 message.
export const L1_TO_L2_ALIAS_OFFSET =
'0x1111000000000000000000000000000000001111'
/**
* Applies the L1 => L2 aliasing scheme to an address.
*
* @param address Address to apply the scheme to.
* @returns Address with the scheme applied.
*/
export const applyL1ToL2Alias = (address: string): string => {
if (!isAddress(address)) {
throw new Error(`not a valid address: ${address}`)
}
return bnToAddress(BigNumber.from(address).add(L1_TO_L2_ALIAS_OFFSET))
}
/**
* Reverses the L1 => L2 aliasing scheme from an address.
*
* @param address Address to reverse the scheme from.
* @returns Alias with the scheme reversed.
*/
export const undoL1ToL2Alias = (address: string): string => {
if (!isAddress(address)) {
throw new Error(`not a valid address: ${address}`)
}
return bnToAddress(BigNumber.from(address).sub(L1_TO_L2_ALIAS_OFFSET))
}
import { ethers } from 'ethers'
/**
* Predeploys are Solidity contracts that are injected into the initial L2 state and provide
* various useful functions.
* Notes:
* 0x42...04 was the address of the OVM_ProxySequencerEntrypoint. This contract is no longer in
* use and has therefore been removed. We may place a new predeployed contract at this address
* in the future. See https://github.com/ethereum-optimism/optimism/pull/549 for more info.
*/
export const predeploys = {
L2ToL1MessagePasser: '0x4200000000000000000000000000000000000016',
DeployerWhitelist: '0x4200000000000000000000000000000000000002',
L2CrossDomainMessenger: '0x4200000000000000000000000000000000000007',
GasPriceOracle: '0x420000000000000000000000000000000000000F',
L2StandardBridge: '0x4200000000000000000000000000000000000010',
SequencerFeeVault: '0x4200000000000000000000000000000000000011',
OptimismMintableERC20Factory: '0x4200000000000000000000000000000000000012',
L1BlockNumber: '0x4200000000000000000000000000000000000013',
L1Block: '0x4200000000000000000000000000000000000015',
LegacyERC20ETH: '0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000',
WETH9: '0x4200000000000000000000000000000000000006',
GovernanceToken: '0x4200000000000000000000000000000000000042',
LegacyMessagePasser: '0x4200000000000000000000000000000000000000',
L2ERC721Bridge: '0x4200000000000000000000000000000000000014',
OptimismMintableERC721Factory: '0x4200000000000000000000000000000000000017',
ProxyAdmin: '0x4200000000000000000000000000000000000018',
BaseFeeVault: '0x4200000000000000000000000000000000000019',
L1FeeVault: '0x420000000000000000000000000000000000001a',
}
const uint128Max = ethers.BigNumber.from('0xffffffffffffffffffffffffffffffff')
export const defaultResourceConfig = {
maxResourceLimit: 20_000_000,
elasticityMultiplier: 10,
baseFeeMaxChangeDenominator: 8,
minimumBaseFee: ethers.utils.parseUnits('1', 'gwei'),
systemTxMaxGas: 1_000_000,
maximumBaseFee: uint128Max,
}
import { getAddress } from '@ethersproject/address'
import { ContractReceipt, Event } from '@ethersproject/contracts'
import { BigNumber, BigNumberish } from '@ethersproject/bignumber'
import { keccak256 } from '@ethersproject/keccak256'
import { Zero } from '@ethersproject/constants'
import * as RLP from '@ethersproject/rlp'
import {
arrayify,
BytesLike,
hexDataSlice,
stripZeros,
hexConcat,
zeroPad,
} from '@ethersproject/bytes'
const formatBoolean = (value: boolean): Uint8Array => {
return value ? new Uint8Array([1]) : new Uint8Array([])
}
const formatNumber = (value: BigNumberish, name: string): Uint8Array => {
const result = stripZeros(BigNumber.from(value).toHexString())
if (result.length > 32) {
throw new Error(`invalid length for ${name}`)
}
return result
}
const handleBoolean = (value: string): boolean => {
if (value === '0x') {
return false
}
if (value === '0x01') {
return true
}
throw new Error(`invalid boolean RLP hex value ${value}`)
}
const handleNumber = (value: string): BigNumber => {
if (value === '0x') {
return Zero
}
return BigNumber.from(value)
}
const handleAddress = (value: string): string => {
if (value === '0x') {
// @ts-ignore
return null
}
return getAddress(value)
}
export enum SourceHashDomain {
UserDeposit = 0,
L1InfoDeposit = 1,
}
interface DepositTxOpts {
sourceHash?: string
from: string
to: string | null
mint: BigNumberish
value: BigNumberish
gas: BigNumberish
isSystemTransaction: boolean
data: string
domain?: SourceHashDomain
l1BlockHash?: string
logIndex?: BigNumberish
sequenceNumber?: BigNumberish
}
interface DepositTxExtraOpts {
domain?: SourceHashDomain
l1BlockHash?: string
logIndex?: BigNumberish
sequenceNumber?: BigNumberish
}
export class DepositTx {
public type = 0x7e
public version = 0x00
private _sourceHash?: string
public from: string
public to: string | null
public mint: BigNumberish
public value: BigNumberish
public gas: BigNumberish
public isSystemTransaction: boolean
public data: BigNumberish
public domain?: SourceHashDomain
public l1BlockHash?: string
public logIndex?: BigNumberish
public sequenceNumber?: BigNumberish
constructor(opts: Partial<DepositTxOpts> = {}) {
this._sourceHash = opts.sourceHash
this.from = opts.from!
this.to = opts.to!
this.mint = opts.mint!
this.value = opts.value!
this.gas = opts.gas!
this.isSystemTransaction = opts.isSystemTransaction || false
this.data = opts.data!
this.domain = opts.domain
this.l1BlockHash = opts.l1BlockHash
this.logIndex = opts.logIndex
this.sequenceNumber = opts.sequenceNumber
}
hash() {
const encoded = this.encode()
return keccak256(encoded)
}
sourceHash() {
if (!this._sourceHash) {
let marker: string
switch (this.domain) {
case SourceHashDomain.UserDeposit:
marker = BigNumber.from(this.logIndex).toHexString()
break
case SourceHashDomain.L1InfoDeposit:
marker = BigNumber.from(this.sequenceNumber).toHexString()
break
default:
throw new Error(`Unknown domain: ${this.domain}`)
}
if (!this.l1BlockHash) {
throw new Error('Need l1BlockHash to compute sourceHash')
}
const l1BlockHash = this.l1BlockHash
const input = hexConcat([l1BlockHash, zeroPad(marker, 32)])
const depositIDHash = keccak256(input)
const domain = BigNumber.from(this.domain).toHexString()
const domainInput = hexConcat([zeroPad(domain, 32), depositIDHash])
this._sourceHash = keccak256(domainInput)
}
return this._sourceHash
}
encode() {
const fields: any = [
this.sourceHash() || '0x',
getAddress(this.from) || '0x',
this.to != null ? getAddress(this.to) : '0x',
formatNumber(this.mint || 0, 'mint'),
formatNumber(this.value || 0, 'value'),
formatNumber(this.gas || 0, 'gas'),
formatBoolean(this.isSystemTransaction),
this.data || '0x',
]
return hexConcat([
BigNumber.from(this.type).toHexString(),
RLP.encode(fields),
])
}
decode(raw: BytesLike, extra: DepositTxExtraOpts = {}) {
const payload = arrayify(raw)
if (payload[0] !== this.type) {
throw new Error(`Invalid type ${payload[0]}`)
}
this.version = payload[1]
const transaction = RLP.decode(payload.slice(1))
this._sourceHash = transaction[0]
this.from = handleAddress(transaction[1])
this.to = handleAddress(transaction[2])
this.mint = handleNumber(transaction[3])
this.value = handleNumber(transaction[4])
this.gas = handleNumber(transaction[5])
this.isSystemTransaction = handleBoolean(transaction[6])
this.data = transaction[7]
if ('l1BlockHash' in extra) {
this.l1BlockHash = extra.l1BlockHash
}
if ('domain' in extra) {
this.domain = extra.domain
}
if ('logIndex' in extra) {
this.logIndex = extra.logIndex
}
if ('sequenceNumber' in extra) {
this.sequenceNumber = extra.sequenceNumber
}
return this
}
static decode(raw: BytesLike, extra?: DepositTxExtraOpts): DepositTx {
return new this().decode(raw, extra)
}
fromL1Receipt(receipt: ContractReceipt, index: number): DepositTx {
if (!receipt.events) {
throw new Error('cannot parse receipt')
}
const event = receipt.events[index]
if (!event) {
throw new Error(`event index ${index} does not exist`)
}
return this.fromL1Event(event)
}
static fromL1Receipt(receipt: ContractReceipt, index: number): DepositTx {
return new this({}).fromL1Receipt(receipt, index)
}
fromL1Event(event: Event): DepositTx {
if (event.event !== 'TransactionDeposited') {
throw new Error(`incorrect event type: ${event.event}`)
}
if (typeof event.args === 'undefined') {
throw new Error('no event args')
}
if (typeof event.args.from === 'undefined') {
throw new Error('"from" undefined')
}
this.from = event.args.from
if (typeof event.args.to === 'undefined') {
throw new Error('"to" undefined')
}
if (typeof event.args.version === 'undefined') {
throw new Error(`"verison" undefined`)
}
if (!event.args.version.eq(0)) {
throw new Error(`Unsupported version ${event.args.version.toString()}`)
}
if (typeof event.args.opaqueData === 'undefined') {
throw new Error(`"opaqueData" undefined`)
}
const opaqueData = event.args.opaqueData
if (opaqueData.length < 32 + 32 + 8 + 1) {
throw new Error(`invalid opaqueData size: ${opaqueData.length}`)
}
let offset = 0
this.mint = BigNumber.from(hexDataSlice(opaqueData, offset, offset + 32))
offset += 32
this.value = BigNumber.from(hexDataSlice(opaqueData, offset, offset + 32))
offset += 32
this.gas = BigNumber.from(hexDataSlice(opaqueData, offset, offset + 8))
offset += 8
const isCreation = BigNumber.from(opaqueData[offset]).eq(1)
offset += 1
this.to = isCreation === true ? null : event.args.to
const length = opaqueData.length - offset
this.isSystemTransaction = false
this.data = hexDataSlice(opaqueData, offset, offset + length)
this.domain = SourceHashDomain.UserDeposit
this.l1BlockHash = event.blockHash
this.logIndex = event.logIndex
return this
}
static fromL1Event(event: Event): DepositTx {
return new this({}).fromL1Event(event)
}
}
import { BigNumberish, BigNumber } from '@ethersproject/bignumber'
import { Interface } from '@ethersproject/abi'
const iface = new Interface([
'function relayMessage(address,address,bytes,uint256)',
'function relayMessage(uint256,address,address,uint256,uint256,bytes)',
])
const nonceMask = BigNumber.from(
'0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
)
/**
* Encodes the version into the nonce.
*
* @param nonce
* @param version
*/
export const encodeVersionedNonce = (
nonce: BigNumber,
version: BigNumber
): BigNumber => {
return version.or(nonce.shl(240))
}
/**
* Decodes the version from the nonce and returns the unversioned nonce as well
* as the version. The version is encoded in the first byte of
* the nonce. Note that this nonce is the nonce held in the
* CrossDomainMessenger.
*
* @param nonce
*/
export const decodeVersionedNonce = (
nonce: BigNumber
): {
version: BigNumber
nonce: BigNumber
} => {
return {
version: nonce.shr(240),
nonce: nonce.and(nonceMask),
}
}
/**
* Encodes a V1 cross domain message. This message format was used before
* bedrock and does not support value transfer because ETH was represented as an
* ERC20 natively.
*
* @param target The target of the cross domain message
* @param sender The sender of the cross domain message
* @param data The data passed along with the cross domain message
* @param nonce The cross domain message nonce
*/
export const encodeCrossDomainMessageV0 = (
target: string,
sender: string,
data: string,
nonce: BigNumber
) => {
return iface.encodeFunctionData(
'relayMessage(address,address,bytes,uint256)',
[target, sender, data, nonce]
)
}
/**
* Encodes a V1 cross domain message. This message format shipped with bedrock
* and supports value transfer with native ETH.
*
* @param nonce The cross domain message nonce
* @param sender The sender of the cross domain message
* @param target The target of the cross domain message
* @param value The value being sent with the cross domain message
* @param gasLimit The gas limit of the cross domain execution
* @param data The data passed along with the cross domain message
*/
export const encodeCrossDomainMessageV1 = (
nonce: BigNumber,
sender: string,
target: string,
value: BigNumberish,
gasLimit: BigNumberish,
data: string
) => {
return iface.encodeFunctionData(
'relayMessage(uint256,address,address,uint256,uint256,bytes)',
[nonce, sender, target, value, gasLimit, data]
)
}
/**
* Encodes a cross domain message. The version byte in the nonce determines
* the serialization format that is used.
*
* @param nonce The cross domain message nonce
* @param sender The sender of the cross domain message
* @param target The target of the cross domain message
* @param value The value being sent with the cross domain message
* @param gasLimit The gas limit of the cross domain execution
* @param data The data passed along with the cross domain message
*/
export const encodeCrossDomainMessage = (
nonce: BigNumber,
sender: string,
target: string,
value: BigNumber,
gasLimit: BigNumber,
data: string
) => {
const { version } = decodeVersionedNonce(nonce)
if (version.eq(0)) {
return encodeCrossDomainMessageV0(target, sender, data, nonce)
} else if (version.eq(1)) {
return encodeCrossDomainMessageV1(
nonce,
sender,
target,
value,
gasLimit,
data
)
}
throw new Error(`unknown version ${version.toString()}`)
}
/**
* Fee related serialization and deserialization
*/
import { BigNumber } from '@ethersproject/bignumber'
import { remove0x } from '../common'
export const txDataZeroGas = 4
export const txDataNonZeroGasEIP2028 = 16
const big10 = BigNumber.from(10)
export const scaleDecimals = (
value: number | BigNumber,
decimals: number | BigNumber
): BigNumber => {
value = BigNumber.from(value)
decimals = BigNumber.from(decimals)
// 10**decimals
const divisor = big10.pow(decimals)
return value.div(divisor)
}
// data is the RLP encoded unsigned transaction
export const calculateL1GasUsed = (
data: string | Buffer,
overhead: number | BigNumber
): BigNumber => {
const [zeroes, ones] = zeroesAndOnes(data)
const zeroesCost = zeroes * txDataZeroGas
// Add a buffer to account for the signature
const onesCost = (ones + 68) * txDataNonZeroGasEIP2028
return BigNumber.from(onesCost).add(zeroesCost).add(overhead)
}
export const calculateL1Fee = (
data: string | Buffer,
overhead: number | BigNumber,
l1GasPrice: number | BigNumber,
scalar: number | BigNumber,
decimals: number | BigNumber
): BigNumber => {
const l1GasUsed = calculateL1GasUsed(data, overhead)
const l1Fee = l1GasUsed.mul(l1GasPrice)
const scaled = l1Fee.mul(scalar)
const result = scaleDecimals(scaled, decimals)
return result
}
// Count the number of zero bytes and non zero bytes in a buffer
export const zeroesAndOnes = (data: Buffer | string): Array<number> => {
if (typeof data === 'string') {
data = Buffer.from(remove0x(data), 'hex')
}
let zeros = 0
let ones = 0
for (const byte of data) {
if (byte === 0) {
zeros++
} else {
ones++
}
}
return [zeros, ones]
}
/**
* Computes the L1 calldata cost of bytes based
* on the London hardfork.
*
* @param data {Buffer|string} Bytes
* @returns {BigNumber} Gas consumed by the bytes
*/
export const calldataCost = (data: Buffer | string): BigNumber => {
const [zeros, ones] = zeroesAndOnes(data)
const zeroCost = BigNumber.from(zeros).mul(txDataZeroGas)
const nonZeroCost = BigNumber.from(ones).mul(txDataNonZeroGasEIP2028)
return zeroCost.add(nonZeroCost)
}
import { BigNumberish, BigNumber } from '@ethersproject/bignumber'
import { keccak256 } from '@ethersproject/keccak256'
import { defaultAbiCoder } from '@ethersproject/abi'
import {
decodeVersionedNonce,
encodeCrossDomainMessageV0,
encodeCrossDomainMessageV1,
} from './encoding'
/**
* Bedrock output oracle data.
*/
export interface BedrockOutputData {
outputRoot: string
l1Timestamp: number
l2BlockNumber: number
l2OutputIndex: number
}
/**
* Bedrock state commitment
*/
export interface OutputRootProof {
version: string
stateRoot: string
messagePasserStorageRoot: string
latestBlockhash: string
}
/**
* Bedrock proof data required to finalize an L2 to L1 message.
*/
export interface BedrockCrossChainMessageProof {
l2OutputIndex: number
outputRootProof: OutputRootProof
withdrawalProof: string[]
}
/**
* Parameters that govern the L2OutputOracle.
*/
export type L2OutputOracleParameters = {
submissionInterval: number
startingBlockNumber: number
l2BlockTime: number
}
/**
* Hahses a cross domain message.
*
* @param nonce The cross domain message nonce
* @param sender The sender of the cross domain message
* @param target The target of the cross domain message
* @param value The value being sent with the cross domain message
* @param gasLimit The gas limit of the cross domain execution
* @param data The data passed along with the cross domain message
*/
export const hashCrossDomainMessage = (
nonce: BigNumber,
sender: string,
target: string,
value: BigNumber,
gasLimit: BigNumber,
message: string
) => {
const { version } = decodeVersionedNonce(nonce)
if (version.eq(0)) {
return hashCrossDomainMessagev0(target, sender, message, nonce)
} else if (version.eq(1)) {
return hashCrossDomainMessagev1(
nonce,
sender,
target,
value,
gasLimit,
message
)
}
throw new Error(`unknown version ${version.toString()}`)
}
/**
* Hahses a V0 cross domain message
*
* @param target The target of the cross domain message
* @param sender The sender of the cross domain message
* @param message The message passed along with the cross domain message
* @param nonce The cross domain message nonce
*/
export const hashCrossDomainMessagev0 = (
target: string,
sender: string,
message: string,
nonce: BigNumber
) => {
return keccak256(encodeCrossDomainMessageV0(target, sender, message, nonce))
}
/**
* Hahses a V1 cross domain message
*
* @param nonce The cross domain message nonce
* @param sender The sender of the cross domain message
* @param target The target of the cross domain message
* @param value The value being sent with the cross domain message
* @param gasLimit The gas limit of the cross domain execution
* @param message The message passed along with the cross domain message
*/
export const hashCrossDomainMessagev1 = (
nonce: BigNumber,
sender: string,
target: string,
value: BigNumberish,
gasLimit: BigNumberish,
message: string
) => {
return keccak256(
encodeCrossDomainMessageV1(nonce, sender, target, value, gasLimit, message)
)
}
/**
* Hashes a withdrawal
*
* @param nonce The cross domain message nonce
* @param sender The sender of the cross domain message
* @param target The target of the cross domain message
* @param value The value being sent with the cross domain message
* @param gasLimit The gas limit of the cross domain execution
* @param message The message passed along with the cross domain message
*/
export const hashWithdrawal = (
nonce: BigNumber,
sender: string,
target: string,
value: BigNumber,
gasLimit: BigNumber,
message: string
): string => {
const types = ['uint256', 'address', 'address', 'uint256', 'uint256', 'bytes']
const encoded = defaultAbiCoder.encode(types, [
nonce,
sender,
target,
value,
gasLimit,
message,
])
return keccak256(encoded)
}
/**
* Hahses an output root proof
*
* @param proof OutputRootProof
*/
export const hashOutputRootProof = (proof: OutputRootProof): string => {
return keccak256(
defaultAbiCoder.encode(
['bytes32', 'bytes32', 'bytes32', 'bytes32'],
[
proof.version,
proof.stateRoot,
proof.messagePasserStorageRoot,
proof.latestBlockhash,
]
)
)
}
/**
* Utils specifically related to Optimism.
*/
export * from './alias'
export * from './fees'
export * from './op-node'
export * from './deposit-transaction'
export * from './encoding'
export * from './hashing'
export * from './op-provider'
export * from './constants'
export interface OpNodeConfig {
genesis: {
l1: {
hash: string
number: number
}
l2: {
hash: string
number: number
}
l2_time: number
}
block_time: number
max_sequencer_drift: number
seq_window_size: number
channel_timeout: number
l1_chain_id: number
l2_chain_id: number
p2p_sequencer_address: string
batch_inbox_address: string
batch_sender_address: string
deposit_contract_address: string
}
import EventEmitter from 'events'
import { BigNumber } from '@ethersproject/bignumber'
import { deepCopy } from '@ethersproject/properties'
import { ConnectionInfo, fetchJson } from '@ethersproject/web'
const getResult = (payload: {
error?: { code?: number; data?: any; message?: string }
result?: any
}): any => {
if (payload.error) {
const error: any = new Error(payload.error.message)
error.code = payload.error.code
error.data = payload.error.data
throw error
}
return payload.result
}
export interface BlockDescriptor {
hash: string
number: BigNumber
parentHash: string
timestamp: BigNumber
}
export interface L2BlockDescriptor extends BlockDescriptor {
l1Origin: {
hash: string
number: BigNumber
}
sequencerNumber: BigNumber
}
export interface SyncStatusResponse {
currentL1: BlockDescriptor
headL1: BlockDescriptor
unsafeL2: L2BlockDescriptor
safeL2: L2BlockDescriptor
finalizedL2: L2BlockDescriptor
}
export class OpNodeProvider extends EventEmitter {
readonly connection: ConnectionInfo
private _nextId: number = 0
constructor(url?: ConnectionInfo | string) {
super()
if (typeof url === 'string') {
this.connection = { url }
} else {
this.connection = url
}
}
async syncStatus(): Promise<SyncStatusResponse> {
const result = await this.send('optimism_syncStatus', [])
return {
currentL1: {
hash: result.current_l1.hash,
number: BigNumber.from(result.current_l1.number),
parentHash: result.current_l1.parentHash,
timestamp: BigNumber.from(result.current_l1.timestamp),
},
headL1: {
hash: result.head_l1.hash,
number: BigNumber.from(result.head_l1.number),
parentHash: result.head_l1.parentHash,
timestamp: BigNumber.from(result.head_l1.timestamp),
},
unsafeL2: {
hash: result.unsafe_l2.hash,
number: BigNumber.from(result.unsafe_l2.number),
parentHash: result.unsafe_l2.parentHash,
timestamp: BigNumber.from(result.unsafe_l2.timestamp),
l1Origin: {
hash: result.unsafe_l2.l1origin.hash,
number: BigNumber.from(result.unsafe_l2.l1origin.number),
},
sequencerNumber: BigNumber.from(result.unsafe_l2.sequenceNumber),
},
safeL2: {
hash: result.safe_l2.hash,
number: BigNumber.from(result.safe_l2.number),
parentHash: result.safe_l2.parentHash,
timestamp: BigNumber.from(result.safe_l2.timestamp),
l1Origin: {
hash: result.safe_l2.l1origin.hash,
number: BigNumber.from(result.safe_l2.l1origin.number),
},
sequencerNumber: BigNumber.from(result.safe_l2.sequenceNumber),
},
finalizedL2: {
hash: result.finalized_l2.hash,
number: BigNumber.from(result.finalized_l2.number),
parentHash: result.finalized_l2.parentHash,
timestamp: BigNumber.from(result.finalized_l2.timestamp),
l1Origin: {
hash: result.finalized_l2.l1origin.hash,
number: BigNumber.from(result.finalized_l2.l1origin.number),
},
sequencerNumber: BigNumber.from(result.finalized_l2.sequenceNumber),
},
}
}
// TODO(tynes): turn the response into a stronger type
async rollupConfig() {
const result = await this.send('optimism_rollupConfig', [])
return result
}
send(method: string, params: Array<any>): Promise<any> {
const request = {
method,
params,
id: this._nextId++,
jsonrpc: '2.0',
}
this.emit('debug', {
action: 'request',
request: deepCopy(request),
provider: this,
})
const result = fetchJson(
this.connection,
JSON.stringify(request),
getResult
).then(
(res) => {
this.emit('debug', {
action: 'response',
request,
response: res,
provider: this,
})
return res
},
(error) => {
this.emit('debug', {
action: 'response',
error,
request,
provider: this,
})
throw error
}
)
return result
}
}
import { BigNumber } from '@ethersproject/bignumber'
/* Imports: Internal */
import { expect } from '../setup'
import {
toRpcHexString,
remove0x,
add0x,
fromHexString,
toHexString,
padHexString,
encodeHex,
hexStringEquals,
bytes32ify,
} from '../../src'
describe('remove0x', () => {
it('should return undefined', () => {
expect(remove0x(undefined)).to.deep.equal(undefined)
})
it('should return without a 0x', () => {
const cases = [
{ input: '0x', output: '' },
{
input: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
output: '1f9840a85d5af5bf1d1762f925bdaddc4201f984',
},
{ input: 'a', output: 'a' },
]
for (const test of cases) {
expect(remove0x(test.input)).to.deep.equal(test.output)
}
})
})
describe('add0x', () => {
it('should return undefined', () => {
expect(add0x(undefined)).to.deep.equal(undefined)
})
it('should return with a 0x', () => {
const cases = [
{ input: '0x', output: '0x' },
{
input: '1f9840a85d5af5bf1d1762f925bdaddc4201f984',
output: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
},
{ input: '', output: '0x' },
]
for (const test of cases) {
expect(add0x(test.input)).to.deep.equal(test.output)
}
})
})
describe('toHexString', () => {
it('should throw an error when input is null', () => {
expect(() => {
toHexString(null)
}).to.throw(
'The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received null'
)
})
it('should return with a hex string', () => {
const cases = [
{ input: 0, output: '0x00' },
{ input: 48, output: '0x30' },
{
input: '0',
output: '0x30',
},
{ input: '', output: '0x' },
]
for (const test of cases) {
expect(toHexString(test.input)).to.deep.equal(test.output)
}
})
})
describe('fromHexString', () => {
it('should return a buffer from a hex string', () => {
const cases = [
{ input: '0x', output: Buffer.from('', 'hex') },
{
input: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
output: Buffer.from('1f9840a85d5af5bf1d1762f925bdaddc4201f984', 'hex'),
},
{ input: '', output: Buffer.from('', 'hex') },
{
input: Buffer.from('1f9840a85d5af5bf1d1762f925bdaddc4201f984'),
output: Buffer.from('1f9840a85d5af5bf1d1762f925bdaddc4201f984'),
},
]
for (const test of cases) {
expect(fromHexString(test.input)).to.deep.equal(test.output)
}
})
})
describe('padHexString', () => {
it('should return return input string if length is 2 + length * 2', () => {
expect(padHexString('abcd', 1)).to.deep.equal('abcd')
expect(padHexString('abcdefgh', 3).length).to.deep.equal(8)
})
it('should return a string padded with 0x and zeros', () => {
expect(padHexString('0xabcd', 3)).to.deep.equal('0x00abcd')
})
})
describe('toRpcHexString', () => {
it('should parse 0', () => {
expect(toRpcHexString(0)).to.deep.equal('0x0')
expect(toRpcHexString(BigNumber.from(0))).to.deep.equal('0x0')
})
it('should parse non 0', () => {
const cases = [
{ input: 2, output: '0x2' },
{ input: BigNumber.from(2), output: '0x2' },
{ input: 100, output: '0x64' },
{ input: BigNumber.from(100), output: '0x64' },
{ input: 300, output: '0x12c' },
{ input: BigNumber.from(300), output: '0x12c' },
]
for (const test of cases) {
expect(toRpcHexString(test.input)).to.deep.equal(test.output)
}
})
})
describe('encodeHex', () => {
it('should throw an error when val is invalid', () => {
expect(() => {
encodeHex(null, 0)
}).to.throw('invalid BigNumber value')
expect(() => {
encodeHex(10.5, 0)
}).to.throw('fault="underflow", operation="BigNumber.from", value=10.5')
expect(() => {
encodeHex('10.5', 0)
}).to.throw('invalid BigNumber string')
})
it('should return a hex string of val with length len', () => {
const cases = [
{
input: {
val: 0,
len: 0,
},
output: '00',
},
{
input: {
val: 0,
len: 4,
},
output: '0000',
},
{
input: {
val: 1,
len: 0,
},
output: '01',
},
{
input: {
val: 1,
len: 10,
},
output: '0000000001',
},
{
input: {
val: 100,
len: 4,
},
output: '0064',
},
{
input: {
val: '100',
len: 0,
},
output: '64',
},
]
for (const test of cases) {
expect(encodeHex(test.input.val, test.input.len)).to.deep.equal(
test.output
)
}
})
})
describe('hexStringEquals', () => {
it('should throw an error when input is not a hex string', () => {
expect(() => {
hexStringEquals('', '')
}).to.throw('input is not a hex string: ')
expect(() => {
hexStringEquals('0xx', '0x1')
}).to.throw('input is not a hex string: 0xx')
expect(() => {
hexStringEquals('0x1', '2')
}).to.throw('input is not a hex string: 2')
expect(() => {
hexStringEquals('-0x1', '0x1')
}).to.throw('input is not a hex string: -0x1')
})
it('should return the hex strings equality', () => {
const cases = [
{
input: {
stringA: '0x',
stringB: '0x',
},
output: true,
},
{
input: {
stringA: '0x1',
stringB: '0x1',
},
output: true,
},
{
input: {
stringA: '0x064',
stringB: '0x064',
},
output: true,
},
{
input: {
stringA: '0x',
stringB: '0x0',
},
output: false,
},
{
input: {
stringA: '0x0',
stringB: '0x1',
},
output: false,
},
{
input: {
stringA: '0x64',
stringB: '0x064',
},
output: false,
},
]
for (const test of cases) {
expect(
hexStringEquals(test.input.stringA, test.input.stringB)
).to.deep.equal(test.output)
}
})
})
describe('bytes32ify', () => {
it('should throw an error when input is invalid', () => {
expect(() => {
bytes32ify(-1)
}).to.throw('invalid hex string')
})
it('should return a zero padded, 32 bytes hex string', () => {
const cases = [
{
input: 0,
output:
'0x0000000000000000000000000000000000000000000000000000000000000000',
},
{
input: BigNumber.from(0),
output:
'0x0000000000000000000000000000000000000000000000000000000000000000',
},
{
input: 2,
output:
'0x0000000000000000000000000000000000000000000000000000000000000002',
},
{
input: BigNumber.from(2),
output:
'0x0000000000000000000000000000000000000000000000000000000000000002',
},
{
input: 100,
output:
'0x0000000000000000000000000000000000000000000000000000000000000064',
},
]
for (const test of cases) {
expect(bytes32ify(test.input)).to.deep.equal(test.output)
}
})
})
/* Imports: Internal */
import { expect } from '../setup'
import { sleep, clone, reqenv, getenv } from '../../src'
describe('sleep', async () => {
it('should return wait input amount of ms', async () => {
const startTime = Date.now()
await sleep(1000)
const endTime = Date.now()
expect(startTime + 1000 <= endTime).to.deep.equal(true)
})
})
describe('clone', async () => {
it('should return a cloned object', async () => {
const exampleObject = { example: 'Example' }
const clonedObject = clone(exampleObject)
expect(clonedObject).to.not.equal(exampleObject)
expect(JSON.stringify(clonedObject)).to.equal(JSON.stringify(exampleObject))
})
})
describe('reqenv', async () => {
let cachedEnvironment: NodeJS.ProcessEnv
const temporaryEnvironmentKey = 'testVariable'
const temporaryEnvironment = {
[temporaryEnvironmentKey]: 'This is an environment variable',
}
before(() => {
cachedEnvironment = process.env
process.env = temporaryEnvironment
})
it('should return an existent environment variable', async () => {
const requiredEnvironmentValue = reqenv(temporaryEnvironmentKey)
expect(requiredEnvironmentValue).to.equal(
temporaryEnvironment[temporaryEnvironmentKey]
)
})
it('should throw an error trying to return a variable that does not exist', async () => {
const undeclaredVariableName = 'undeclaredVariable'
const failedReqenv = () => reqenv(undeclaredVariableName)
expect(failedReqenv).to.throw()
})
after(() => {
process.env = cachedEnvironment
})
})
describe('getenv', async () => {
let cachedEnvironment: NodeJS.ProcessEnv
const temporaryEnvironmentKey = 'testVariable'
const temporaryEnvironment = {
[temporaryEnvironmentKey]: 'This is an environment variable',
}
const fallback = 'fallback'
before(() => {
cachedEnvironment = process.env
process.env = temporaryEnvironment
})
it('should return an existent environment variable', async () => {
const environmentVariable = getenv(temporaryEnvironmentKey)
expect(environmentVariable).to.equal(
temporaryEnvironment[temporaryEnvironmentKey]
)
})
it('should return an existent environment variable even if fallback is passed', async () => {
const environmentVariable = getenv(temporaryEnvironmentKey, fallback)
expect(environmentVariable).to.equal(
temporaryEnvironment[temporaryEnvironmentKey]
)
})
it('should return fallback if variable is not defined', async () => {
const undeclaredVariableName = 'undeclaredVariable'
expect(getenv(undeclaredVariableName, fallback)).to.equal(fallback)
})
it('should return undefined if no fallback is passed and variable is not defined', async () => {
expect(getenv('undeclaredVariable')).to.be.undefined
})
after(() => {
process.env = cachedEnvironment
})
})
import { assert } from 'chai'
/* Imports: Internal */
import { expect } from '../setup'
import { expectApprox, awaitCondition } from '../../src'
describe('awaitCondition', () => {
it('should try the condition fn until it returns true', async () => {
let i = 0
const condFn = async () => {
i++
return Promise.resolve(i === 2)
}
await awaitCondition(condFn, 50, 3)
expect(i).to.equal(2)
})
it('should only try the configured number of attempts', async () => {
let i = 0
const condFn = async () => {
i++
return Promise.resolve(i === 2)
}
try {
await awaitCondition(condFn, 50, 1)
} catch (e) {
return
}
assert.fail('Condition never failed, but it should have.')
})
})
describe('expectApprox', () => {
it('should pass when the actual number is higher, but within the expected range of the target', async () => {
expectApprox(119, 100, {
percentUpperDeviation: 20,
percentLowerDeviation: 20,
absoluteUpperDeviation: 20,
absoluteLowerDeviation: 20,
})
})
it('should pass when the actual number is lower, but within the expected range of the target', async () => {
expectApprox(81, 100, {
percentUpperDeviation: 20,
percentLowerDeviation: 20,
absoluteUpperDeviation: 20,
absoluteLowerDeviation: 20,
})
})
it('should throw an error when no deviation values are given', async () => {
try {
expectApprox(101, 100, {})
assert.fail('expectApprox did not throw an error')
} catch (error) {
expect(error.message).to.equal(
'Must define at least one parameter to limit the deviation of the actual value.'
)
}
})
describe('should throw an error if the actual value is higher than expected', () => {
describe('... when only one upper bound value is defined', () => {
it('... and percentUpperDeviation sets the upper bound', async () => {
try {
expectApprox(121, 100, {
percentUpperDeviation: 20,
})
assert.fail('expectApprox did not throw an error')
} catch (error) {
expect(error.message).to.equal(
'Actual value (121) is greater than the calculated upper bound of (120): expected false to be true'
)
}
})
it('... and absoluteUpperDeviation sets the upper bound', async () => {
try {
expectApprox(121, 100, {
absoluteUpperDeviation: 20,
})
assert.fail('expectApprox did not throw an error')
} catch (error) {
expect(error.message).to.equal(
'Actual value (121) is greater than the calculated upper bound of (120): expected false to be true'
)
}
})
})
describe('... when both values are defined', () => {
it('... and percentUpperDeviation sets the upper bound', async () => {
try {
expectApprox(121, 100, {
percentUpperDeviation: 20,
absoluteUpperDeviation: 30,
})
assert.fail('expectApprox did not throw an error')
} catch (error) {
expect(error.message).to.equal(
'Actual value (121) is greater than the calculated upper bound of (120): expected false to be true'
)
}
})
it('... and absoluteUpperDeviation sets the upper bound', async () => {
try {
expectApprox(121, 100, {
percentUpperDeviation: 30,
absoluteUpperDeviation: 20,
})
assert.fail('expectApprox did not throw an error')
} catch (error) {
expect(error.message).to.equal(
'Actual value (121) is greater than the calculated upper bound of (120): expected false to be true'
)
}
})
})
})
describe('should throw an error if the actual value is lower than expected', () => {
describe('... when only one lower bound value is defined', () => {
it('... and percentLowerDeviation sets the lower bound', async () => {
try {
expectApprox(79, 100, {
percentLowerDeviation: 20,
})
assert.fail('expectApprox did not throw an error')
} catch (error) {
expect(error.message).to.equal(
'Actual value (79) is less than the calculated lower bound of (80): expected false to be true'
)
}
})
it('... and absoluteLowerDeviation sets the lower bound', async () => {
try {
expectApprox(79, 100, {
absoluteLowerDeviation: 20,
})
assert.fail('expectApprox did not throw an error')
} catch (error) {
expect(error.message).to.equal(
'Actual value (79) is less than the calculated lower bound of (80): expected false to be true'
)
}
})
})
describe('... when both values are defined', () => {
it('... and percentLowerDeviation sets the lower bound', async () => {
try {
expectApprox(79, 100, {
percentLowerDeviation: 20,
absoluteLowerDeviation: 30,
})
assert.fail('expectApprox did not throw an error')
} catch (error) {
expect(error.message).to.equal(
'Actual value (79) is less than the calculated lower bound of (80): expected false to be true'
)
}
})
it('... and absoluteLowerDeviation sets the lower bound', async () => {
try {
expectApprox(79, 100, {
percentLowerDeviation: 30,
absoluteLowerDeviation: 20,
})
assert.fail('expectApprox did not throw an error')
} catch (error) {
expect(error.message).to.equal(
'Actual value (79) is less than the calculated lower bound of (80): expected false to be true'
)
}
})
})
})
})
import { expect } from '../setup'
import { applyL1ToL2Alias, undoL1ToL2Alias } from '../../src'
describe('address aliasing utils', () => {
describe('applyL1ToL2Alias', () => {
it('should be able to apply the alias to a valid address', () => {
expect(
applyL1ToL2Alias('0x0000000000000000000000000000000000000000')
).to.equal('0x1111000000000000000000000000000000001111')
})
it('should be able to apply the alias even if the operation overflows', () => {
expect(
applyL1ToL2Alias('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF')
).to.equal('0x1111000000000000000000000000000000001110')
})
it('should throw if the input is not a valid address', () => {
expect(() => {
applyL1ToL2Alias('0x1234')
}).to.throw('not a valid address: 0x1234')
})
})
describe('undoL1ToL2Alias', () => {
it('should be able to undo the alias from a valid address', () => {
expect(
undoL1ToL2Alias('0x1111000000000000000000000000000000001111')
).to.equal('0x0000000000000000000000000000000000000000')
})
it('should be able to undo the alias even if the operation underflows', () => {
expect(
undoL1ToL2Alias('0x1111000000000000000000000000000000001110')
).to.equal('0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF')
})
it('should throw if the input is not a valid address', () => {
expect(() => {
undoL1ToL2Alias('0x1234')
}).to.throw('not a valid address: 0x1234')
})
})
})
import '../setup'
import { BigNumber } from '@ethersproject/bignumber'
import { zeroesAndOnes, calldataCost } from '../../src'
describe('Fees', () => {
it('should count zeros and ones', () => {
const cases = [
{ input: Buffer.from('0001', 'hex'), zeros: 1, ones: 1 },
{ input: '0x0001', zeros: 1, ones: 1 },
{ input: '0x', zeros: 0, ones: 0 },
{ input: '0x1111', zeros: 0, ones: 2 },
]
for (const test of cases) {
const [zeros, ones] = zeroesAndOnes(test.input)
zeros.should.eq(test.zeros)
ones.should.eq(test.ones)
}
})
it('should compute calldata costs', () => {
const cases = [
{ input: '0x', output: BigNumber.from(0) },
{ input: '0x00', output: BigNumber.from(4) },
{ input: '0xff', output: BigNumber.from(16) },
{ input: Buffer.alloc(32), output: BigNumber.from(4 * 32) },
{ input: Buffer.alloc(32, 0xff), output: BigNumber.from(16 * 32) },
]
for (const test of cases) {
const cost = calldataCost(test.input)
cost.should.deep.eq(test.output)
}
})
})
/* External Imports */
import chai = require('chai')
import Mocha from 'mocha'
const should = chai.should()
const expect = chai.expect
export { should, expect, Mocha }
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"compilerOptions": {
"typeRoots": ["node_modules/@types"]
},
"include": [
"src/**/*"
]
}
artifacts
cache
typechain
.deps
.envrc
.env
/dist/
coverage
artifacts
cache
typechain
.deps
.envrc
.env
/dist/
module.exports = {
...require('../../.prettierrc.js'),
}
# @eth-optimism/fee-estimation
## 0.15.4
### Patch Changes
- [#9964](https://github.com/ethereum-optimism/optimism/pull/9964) [`8241220898128e1f61064f22dcb6fdd0a5f043c3`](https://github.com/ethereum-optimism/optimism/commit/8241220898128e1f61064f22dcb6fdd0a5f043c3) Thanks [@roninjin10](https://github.com/roninjin10)! - Removed only-allow command from package.json
## 0.15.3
### Patch Changes
- [#7450](https://github.com/ethereum-optimism/optimism/pull/7450) [`ac90e16a7`](https://github.com/ethereum-optimism/optimism/commit/ac90e16a7f85c4f73661ae6023135c3d00421c1e) Thanks [@roninjin10](https://github.com/roninjin10)! - Updated dev dependencies related to testing that is causing audit tooling to report failures
## 0.15.2
### Patch Changes
- [#6609](https://github.com/ethereum-optimism/optimism/pull/6609) [`0e83c4452`](https://github.com/ethereum-optimism/optimism/commit/0e83c44522e1a13e4d5c1395fd4dc9dbae8be08d) Thanks [@roninjin10](https://github.com/roninjin10)! - Fixed bug with 'estimateFees' not taking into account the l2 gas price
## 0.15.1
### Patch Changes
- [#6418](https://github.com/ethereum-optimism/optimism/pull/6418) [`d046711a3`](https://github.com/ethereum-optimism/optimism/commit/d046711a37ac050df1742b7de19ac548ffc12c7b) Thanks [@roninjin10](https://github.com/roninjin10)! - Updated docs
MIT License
Copyright (c) 2022 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.
# @eth-optimism/fee-estimation
Tools for estimating gas on OP chains
- **Tip** the [specs file](./src/estimateFees.spec.ts) has usage examples of every method in this library.
## Overview
This package is designed to provide an easy way to estimate gas on OP chains.
Fee estimation on OP-chains has both an l2 and l1 component. By default tools such as Viem, Wagmi, Ethers, and Web3.js do not support the l1 component. They will support this soon but in meantime, this library can help estimate fees for transactions, or act as a reference.
As these tools add support for gas estimation natively this README will be updated with framework specific instructions.
For more detailed information about gas fees on Optimism's Layer 2, you can visit their [official documentation](https://community.optimism.io/docs/developers/build/transaction-fees/#the-l2-execution-fee).
## GasPriceOracle contract
- The l2 contract that can estimate l1Fees is called [GasPriceOracle](../contracts-bedrock/contracts/l2/GasPriceOracle.sol) contract. This library provides utils for interacting with it at a high level.
- The GasPriceOracle is [deployed to Optimism](https://optimistic.etherscan.io/address/0x420000000000000000000000000000000000000F) and other OP chains at a predeployed address of `0x420000000000000000000000000000000000000F`
This library provides a higher level abstraction over the gasPriceOracle
## Installation
```bash
pnpm install @eth-optimism/fee-estimation
```
```bash
npm install @eth-optimism/fee-estimation
```
```bash
yarn add @eth-optimism/fee-estimation
```
### Basic Usage
```ts
import { estimateFees } from '@eth-optimism/fee-estimation'
import { optimistABI } from '@eth-optimism/contracts-ts'
import { viemClient } from './viem-client'
const optimistOwnerAddress =
'0x77194aa25a06f932c10c0f25090f3046af2c85a6' as const
const tokenId = BigInt(optimistOwnerAddress)
const fees = await estimateFees({
client: viemClient,
// If not using in viem can pass in this instead
/*
client: {
chainId: 10,
rpcUrl: 'https://mainnet.optimism.io',
},
*/
functionName: 'burn',
abi: optimistABI,
args: [tokenId],
account: optimistOwnerAddress,
to: '0x2335022c740d17c2837f9C884Bfe4fFdbf0A95D5',
})
```
## API
### `estimateFees` function
```ts
estimateFees(options: OracleTransactionParameters<TAbi, TFunctionName> & GasPriceOracleOptions & Omit<EstimateGasParameters, 'data'>): Promise<bigint>
```
#### Parameters
`options`: An object with the following fields:
- `abi`: A JSON object ABI of contract.
- `account`: A hex address of the account making the transaction.
- `args`: Array of arguments to contract function. The types of this will be inferred from the ABI
- `blockNumber`(optional): A BigInt representing the block number at which you want to estimate the fees.
- `chainId`: An integer chain id.
- `client`: An object with rpcUrl field, or an instance of a Viem PublicClient.
- `functionName`: A string representing the function name for the transaction call data.
- `maxFeePerGas`(optional): A BigInt representing the maximum fee per gas that the user is willing to pay.
- `maxPriorityFeePerGas`(optional): A BigInt representing the maximum priority fee per gas that the user is willing to pay.
- `to`: A hex address of the recipient of the transaction.
- `value`(optional): A BigInt representing the value in wei sent along with the transaction.
#### Returns
A Promise that resolves to a BigInt representing the estimated fee in wei.
## Other methods
This package also provides lower level methods for estimating gas
### getL2Client()
This method returns a Layer 2 (L2) client that communicates with an L2 network.
```ts
getL2Client(options: ClientOptions): PublicClient;
```
#### Parameters
- `options: ClientOptions` - The options required to initialize the L2 client.
#### Returns
- `PublicClient` - Returns a public client that can interact with the L2 network.
#### Example
```ts
const clientParams = {
chainId: 10,
rpcUrl: process.env.VITE_L2_RPC_URL ?? 'https://mainnet.optimism.io',
} as const
const client = getL2Client(clientParams)
```
---
### baseFee()
Returns the base fee.
```ts
baseFee({ client, ...params }: GasPriceOracleOptions): Promise<bigint>;
```
#### Parameters
- `{ client, ...params }: GasPriceOracleOptions` - The options required to fetch the base fee.
#### Returns
- `Promise<bigint>` - Returns a promise that resolves to the base fee.
#### Example
```ts
const blockNumber = BigInt(106889079)
const paramsWithClient = {
client: clientParams,
blockNumber,
}
const baseFeeValue = await baseFee(paramsWithClient)
```
---
### decimals()
Returns the decimals used in the scalar.
```ts
decimals({ client, ...params }: GasPriceOracleOptions): Promise<bigint>;
```
#### Parameters
- `{ client, ...params }: GasPriceOracleOptions` - The options required to fetch the decimals.
#### Returns
- `Promise<bigint>` - Returns a promise that resolves to the decimals used in the scalar.
#### Example
```ts
const decimalsValue = await decimals(paramsWithClient)
```
---
### gasPrice()
Returns the gas price.
```ts
gasPrice({ client, ...params }: GasPriceOracleOptions): Promise<bigint>;
```
#### Parameters
- `{ client, ...params }: GasPriceOracleOptions` - The options required to fetch the gas price.
#### Returns
- `Promise<bigint>` - Returns a promise that resolves to the gas price.
#### Example
```ts
const gasPriceValue = await gasPrice(paramsWithClient)
```
---
### getL1Fee()
Computes the L1 portion of the fee based on the size of the rlp encoded input transaction, the current L1 base fee, and the various dynamic parameters.
```ts
getL1Fee(data: Bytes, { client, ...params }: GasPriceOracleOptions): Promise<bigint>;
```
#### Parameters
- `data: Bytes` - The transaction call data as a 0x-prefixed hex string.
- `{ client, ...params }: GasPriceOracleOptions` - Optional lock options and provider options.
#### Returns
- `Promise<bigint>` - Returns a promise that resolves to the L1 portion of the fee.
#### Example
```ts
const data =
'0x5c19a95c00000000000000000000000046abfe1c972fca43766d6ad70e1c1df72f4bb4d1'
const l1FeeValue = await getL1Fee(data, paramsWithClient)
```
### getL1GasUsed()
This method returns the amount of gas used on the L1 network for a given transaction.
```ts
getL1GasUsed(data: Bytes, { client, ...params }: GasPriceOracleOptions): Promise<bigint>;
```
#### Parameters
- `data: Bytes` - The transaction call data as a 0x-prefixed hex string.
- `{ client, ...params }: GasPriceOracleOptions` - Optional lock options and provider options.
#### Returns
- `Promise<bigint>` - Returns a promise that resolves to the amount of gas used on the L1 network for the given transaction.
#### Example
```ts
const data =
'0x5c19a95c00000000000000000000000046abfe1c972fca43766d6ad70e1c1df72f4bb4d1'
const l1GasUsed = await getL1GasUsed(data, paramsWithClient)
```
---
### l1BaseFee()
Returns the base fee on the L1 network.
```ts
l1BaseFee({ client, ...params }: GasPriceOracleOptions): Promise<bigint>;
```
#### Parameters
- `{ client, ...params }: GasPriceOracleOptions` - Optional lock options and provider options.
#### Returns
- `Promise<bigint>` - Returns a promise that resolves to the base fee on the L1 network.
#### Example
```ts
const l1BaseFeeValue = await l1BaseFee(paramsWithClient)
```
---
### overhead()
Returns the overhead for the given transaction.
```ts
overhead({ client, ...params }: GasPriceOracleOptions): Promise<bigint>;
```
#### Parameters
- `{ client, ...params }: GasPriceOracleOptions` - Optional lock options and provider options.
#### Returns
- `Promise<bigint>` - Returns a promise that resolves to the overhead for the given transaction.
#### Example
```ts
const overheadValue = await overhead(paramsWithClient)
```
---
### scalar()
Returns the scalar value for the gas estimation.
```ts
scalar({ client, ...params }: GasPriceOracleOptions): Promise<bigint>;
```
#### Parameters
- `{ client, ...params }: GasPriceOracleOptions` - Optional lock options and provider options.
#### Returns
- `Promise<bigint>` - Returns a promise that resolves to the scalar value for the gas estimation.
#### Example
```ts
const scalarValue = await scalar(paramsWithClient)
```
---
### version()
Returns the version of the fee estimation library.
```ts
version({ client, ...params }: GasPriceOracleOptions): Promise<string>;
```
#### Parameters
- `{ client, ...params }: GasPriceOracleOptions` - Optional lock options and provider options.
#### Returns
- `Promise<string>` - Returns a promise that resolves to the version of the fee estimation library.
#### Example
```ts
const libraryVersion = await version(paramsWithClient)
```
---
VITE_RPC_URL_L2_GOERLI=
VITE_RPC_URL_L2_MAINNET=
VITE_RPC_URL_L1_GOERLI=
VITE_RPC_URL_L1_MAINNET=
{
"name": "@eth-optimism/fee-estimation",
"version": "0.15.4",
"description": "Lightweight library for doing OP-Chain gas estimation",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/ethereum-optimism/optimism.git",
"directory": "packages/fee-estimation"
},
"homepage": "https://optimism.io",
"type": "module",
"main": "dist/estimateFees.cjs",
"module": "dist/estimateFees.js",
"exports": {
".": {
"import": "./dist/estimateFees.js",
"require": "./dist/estimateFees.cjs",
"default": "./dist/estimateFees.js",
"types": "./src/estimateFees.ts"
}
},
"types": "src/estimateFees.ts",
"files": [
"dist/",
"src/"
],
"scripts": {
"build": "tsup",
"lint": "prettier --check .",
"lint:fix": "prettier --write .",
"test": "vitest",
"test:coverage": "vitest run --coverage",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@eth-optimism/contracts-ts": "workspace:^",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react-hooks": "^8.0.1",
"@vitest/coverage-istanbul": "^1.2.2",
"abitype": "^1.0.2",
"isomorphic-fetch": "^3.0.0",
"jest-dom": "link:@types/@testing-library/jest-dom",
"jsdom": "^24.0.0",
"tsup": "^8.0.1",
"typescript": "^5.4.5",
"viem": "^2.8.13",
"vite": "^5.1.7",
"vitest": "^1.2.2"
},
"peerDependencies": {
"viem": "^0.3.30"
}
}
import fetch from 'isomorphic-fetch'
// viem needs this
global.fetch = fetch
/**
* The first 2 test cases are good documentation of how to use this library
*/
import { vi, test, expect, beforeEach } from 'vitest'
import { formatEther } from 'viem/utils'
import {
baseFee,
decimals,
estimateFees,
gasPrice,
getL1Fee,
getL1GasUsed,
getL2Client,
l1BaseFee,
overhead,
scalar,
version,
} from './estimateFees'
import {
optimistABI,
optimistAddress,
l2StandardBridgeABI,
l2StandardBridgeAddress,
} from '@eth-optimism/contracts-ts'
import { parseEther, parseGwei } from 'viem'
vi.mock('viem', async () => {
const _viem = (await vi.importActual('viem')) as any
return {
..._viem,
// no way to get historical gas price
createPublicClient: (...args: [any]) => {
const client = _viem.createPublicClient(...args)
client.getGasPrice = async () => parseGwei('0.00000042')
return client
},
}
})
// using this optimist https://optimistic.etherscan.io/tx/0xaa291efba7ea40b0742e5ff84a1e7831a2eb6c2fc35001fa03ba80fd3b609dc9
const blockNumber = BigInt(107028270)
const optimistOwnerAddress =
'0x77194aa25a06f932c10c0f25090f3046af2c85a6' as const
const functionDataBurn = {
functionName: 'burn',
// this is an erc721 abi
abi: optimistABI,
args: [BigInt(optimistOwnerAddress)],
account: optimistOwnerAddress,
to: optimistAddress[10],
chainId: 10,
} as const
const functionDataBurnWithPriorityFees = {
...functionDataBurn,
maxFeePerGas: parseGwei('2'),
maxPriorityFeePerGas: parseGwei('2'),
} as const
// This tx
// https://optimistic.etherscan.io/tx/0xe6f3719be7327a991b9cb562ebf8d979cbca72bbdb2775f55a18274f4d0c9bbf
const functionDataWithdraw = {
abi: l2StandardBridgeABI,
functionName: 'withdraw',
value: BigInt(parseEther('0.00000001')),
account: '0x6387a88a199120aD52Dd9742C7430847d3cB2CD4',
// currently a bug is making chain id 10 not exist
to: l2StandardBridgeAddress[420],
chainId: 10,
args: [
// l2 token address
'0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000',
// amount
BigInt(parseEther('0.00000001')),
// l1 gas
0,
// extra data
'0x0',
],
maxFeePerGas: parseGwei('.2'),
maxPriorityFeePerGas: parseGwei('.1'),
} as const
const clientParams = {
chainId: functionDataBurn.chainId,
rpcUrl: process.env.VITE_L2_RPC_URL ?? 'https://mainnet.optimism.io',
} as const
const viemClient = getL2Client(clientParams)
const paramsWithRpcUrl = {
client: clientParams,
blockNumber,
} as const
const paramsWithViemClient = {
client: viemClient,
viemClient,
blockNumber,
} as const
const blockNumberWithdraw = BigInt(107046472)
const paramsWithRpcUrlWithdraw = {
client: clientParams,
blockNumber: blockNumberWithdraw,
} as const
beforeEach(() => {
vi.resetAllMocks()
})
test('estimateFees should return correct fees', async () => {
// burn
const res = await estimateFees({ ...paramsWithRpcUrl, ...functionDataBurn })
expect(res).toMatchInlineSnapshot('20573203833264n')
expect(formatEther(res)).toMatchInlineSnapshot('"0.000020573203833264"')
expect(
await estimateFees({ ...paramsWithRpcUrl, ...functionDataBurn })
).toMatchInlineSnapshot('20573203833264n')
expect(
await estimateFees({ ...paramsWithViemClient, ...functionDataBurn })
).toMatchInlineSnapshot('20573203833264n')
expect(
await estimateFees({
...paramsWithRpcUrl,
...functionDataBurnWithPriorityFees,
})
).toMatchInlineSnapshot('21536992690265n')
// what is the l2 and l1 part of the fees for reference?
const l1Fee = await getL1Fee({ ...paramsWithRpcUrl, ...functionDataBurn })
const l2Fee = res - l1Fee
expect(l1Fee).toMatchInlineSnapshot('20573185216764n')
expect(formatEther(l1Fee)).toMatchInlineSnapshot('"0.000020573185216764"')
expect(l2Fee).toMatchInlineSnapshot('18616500n')
expect(formatEther(l2Fee)).toMatchInlineSnapshot('"0.0000000000186165"')
// withdraw
const res2 = await estimateFees({
...paramsWithRpcUrlWithdraw,
...functionDataWithdraw,
})
expect(res2).toMatchInlineSnapshot('62857090247510n')
expect(
await estimateFees({ ...paramsWithRpcUrlWithdraw, ...functionDataWithdraw })
).toMatchInlineSnapshot('62857090247510n')
expect(
await estimateFees({ ...paramsWithRpcUrlWithdraw, ...functionDataWithdraw })
).toMatchInlineSnapshot('62857090247510n')
expect(
await estimateFees({ ...paramsWithRpcUrlWithdraw, ...functionDataWithdraw })
).toMatchInlineSnapshot('62857090247510n')
expect(formatEther(res2)).toMatchInlineSnapshot('"0.00006285709024751"')
// what is the l2 and l1 part of the fees for reference?
const l1Fee2 = await getL1Fee({
...paramsWithRpcUrlWithdraw,
...functionDataWithdraw,
})
const l2Fee2 = res2 - l1Fee
expect(l1Fee2).toMatchInlineSnapshot('62857038894110n')
expect(formatEther(l1Fee2)).toMatchInlineSnapshot('"0.00006285703889411"')
expect(l2Fee2).toMatchInlineSnapshot('42283905030746n')
expect(formatEther(l2Fee2)).toMatchInlineSnapshot('"0.000042283905030746"')
})
test('baseFee should return the correct result', async () => {
expect(await baseFee(paramsWithRpcUrl)).toMatchInlineSnapshot('64n')
expect(await baseFee(paramsWithViemClient)).toMatchInlineSnapshot('64n')
})
test('decimals should return the correct result', async () => {
expect(await decimals(paramsWithRpcUrl)).toMatchInlineSnapshot('6n')
expect(await decimals(paramsWithViemClient)).toMatchInlineSnapshot('6n')
})
test('gasPrice should return the correct result', async () => {
expect(await gasPrice(paramsWithRpcUrl)).toMatchInlineSnapshot('64n')
expect(await gasPrice(paramsWithViemClient)).toMatchInlineSnapshot('64n')
})
test('getL1Fee should return the correct result', async () => {
// burn
expect(
await getL1Fee({ ...paramsWithRpcUrl, ...functionDataBurn })
).toMatchInlineSnapshot('20573185216764n')
expect(
await getL1Fee({ ...paramsWithViemClient, ...functionDataBurn })
).toMatchInlineSnapshot('20573185216764n')
expect(
await getL1Fee({
...paramsWithViemClient,
...functionDataBurnWithPriorityFees,
})
).toMatchInlineSnapshot('21536974073765n')
expect(
formatEther(
await getL1Fee({ ...paramsWithViemClient, ...functionDataBurn })
)
).toMatchInlineSnapshot('"0.000020573185216764"')
// withdraw
expect(
await getL1Fee({ ...paramsWithRpcUrlWithdraw, ...functionDataWithdraw })
).toMatchInlineSnapshot('62857038894110n')
expect(
formatEther(
await getL1Fee({ ...paramsWithRpcUrlWithdraw, ...functionDataWithdraw })
)
).toMatchInlineSnapshot('"0.00006285703889411"')
})
test('getL1GasUsed should return the correct result', async () => {
// burn
expect(
await getL1GasUsed({ ...paramsWithRpcUrl, ...functionDataBurn })
).toMatchInlineSnapshot('2220n')
expect(
await getL1GasUsed({ ...paramsWithViemClient, ...functionDataBurn })
).toMatchInlineSnapshot('2220n')
expect(
await getL1GasUsed({
...paramsWithViemClient,
...functionDataBurnWithPriorityFees,
})
).toMatchInlineSnapshot('2324n')
// withdraw
expect(
await getL1GasUsed({ ...paramsWithRpcUrlWithdraw, ...functionDataWithdraw })
).toMatchInlineSnapshot('2868n')
})
test('l1BaseFee should return the correct result', async () => {
expect(await l1BaseFee(paramsWithRpcUrl)).toMatchInlineSnapshot(
'13548538813n'
)
expect(await l1BaseFee(paramsWithViemClient)).toMatchInlineSnapshot(
'13548538813n'
)
})
test('overhead should return the correct result', async () => {
expect(await overhead(paramsWithRpcUrl)).toMatchInlineSnapshot('188n')
expect(await overhead(paramsWithViemClient)).toMatchInlineSnapshot('188n')
})
test('scalar should return the correct result', async () => {
expect(await scalar(paramsWithRpcUrl)).toMatchInlineSnapshot('684000n')
expect(await scalar(paramsWithViemClient)).toMatchInlineSnapshot('684000n')
})
test('version should return the correct result', async () => {
expect(await version(paramsWithRpcUrl)).toMatchInlineSnapshot('"1.0.0"')
expect(await version(paramsWithViemClient)).toMatchInlineSnapshot('"1.0.0"')
})
import {
gasPriceOracleABI,
gasPriceOracleAddress,
} from '@eth-optimism/contracts-ts'
import {
getContract,
createPublicClient,
http,
BlockTag,
Address,
EstimateGasParameters,
serializeTransaction,
encodeFunctionData,
EncodeFunctionDataParameters,
TransactionSerializableEIP1559,
TransactionSerializedEIP1559,
PublicClient,
} from 'viem'
import * as chains from 'viem/chains'
import { Abi } from 'abitype'
/**
* Bytes type representing a hex string with a 0x prefix
* @typedef {`0x${string}`} Bytes
*/
export type Bytes = `0x${string}`
/**
* Options to query a specific block
*/
type BlockOptions = {
/**
* Block number to query from
*/
blockNumber?: bigint
/**
* Block tag to query from
*/
blockTag?: BlockTag
}
const knownChains = [
chains.optimism.id,
chains.goerli.id,
chains.base,
chains.baseGoerli.id,
chains.zora,
chains.zoraTestnet,
]
/**
* ClientOptions type
* @typedef {Object} ClientOptions
* @property {keyof typeof gasPriceOracleAddress | number} chainId - Chain ID
* @property {string} [rpcUrl] - RPC URL. If not provided the provider will attempt to use public RPC URLs for the chain
* @property {chains.Chain['nativeCurrency']} [nativeCurrency] - Native currency. Defaults to ETH
*/
type ClientOptions =
// for known chains like base don't require an rpcUrl
| {
chainId: (typeof knownChains)[number]
rpcUrl?: string
nativeCurrency?: chains.Chain['nativeCurrency']
}
| {
chainId: number
rpcUrl: string
nativeCurrency?: chains.Chain['nativeCurrency']
}
| PublicClient
/**
* Options for all GasPriceOracle methods
*/
export type GasPriceOracleOptions = BlockOptions & { client: ClientOptions }
/**
* Options for specifying the transaction being estimated
*/
export type OracleTransactionParameters<
TAbi extends Abi | readonly unknown[],
TFunctionName extends string | undefined = undefined
> = EncodeFunctionDataParameters<TAbi, TFunctionName> &
Omit<TransactionSerializableEIP1559, 'data' | 'type'>
/**
* Options for specifying the transaction being estimated
*/
export type GasPriceOracleEstimator = <
TAbi extends Abi | readonly unknown[],
TFunctionName extends string | undefined = undefined
>(
options: OracleTransactionParameters<TAbi, TFunctionName> &
GasPriceOracleOptions
) => Promise<bigint>
/**
* Throws an error if fetch is not defined
* Viem requires fetch
*/
const validateFetch = (): void => {
if (typeof fetch === 'undefined') {
throw new Error(
'No fetch implementation found. Please provide a fetch polyfill. This can be done in NODE by passing in NODE_OPTIONS=--experimental-fetch or by using the isomorphic-fetch npm package'
)
}
}
/**
* Internal helper to serialize a transaction
*/
const transactionSerializer = <
TAbi extends Abi | readonly unknown[],
TFunctionName extends string | undefined = undefined
>(
options: EncodeFunctionDataParameters<TAbi, TFunctionName> &
Omit<TransactionSerializableEIP1559, 'data'>
): TransactionSerializedEIP1559 => {
const encodedFunctionData = encodeFunctionData(options)
const serializedTransaction = serializeTransaction({
...options,
data: encodedFunctionData,
type: 'eip1559',
})
return serializedTransaction as TransactionSerializedEIP1559
}
/**
* Gets L2 client
* @example
* const client = getL2Client({ chainId: 1, rpcUrl: "http://localhost:8545" });
*/
export const getL2Client = (options: ClientOptions): PublicClient => {
validateFetch()
if ('chainId' in options && options.chainId) {
const viemChain = Object.values(chains)?.find(
(chain) => chain.id === options.chainId
)
const rpcUrls = options.rpcUrl
? { default: { http: [options.rpcUrl] } }
: viemChain?.rpcUrls
if (!rpcUrls) {
throw new Error(
`No rpcUrls found for chainId ${options.chainId}. Please explicitly provide one`
)
}
return createPublicClient({
chain: {
id: options.chainId,
name: viemChain?.name ?? 'op-chain',
nativeCurrency:
options.nativeCurrency ??
viemChain?.nativeCurrency ??
chains.optimism.nativeCurrency,
network: viemChain?.network ?? 'Unknown OP Chain',
rpcUrls,
explorers:
(viemChain as typeof chains.optimism)?.blockExplorers ??
chains.optimism.blockExplorers,
},
transport: http(
options.rpcUrl ?? chains[options.chainId].rpcUrls.public.http[0]
),
})
}
return options as PublicClient
}
/**
* Get gas price Oracle contract
*/
export const getGasPriceOracleContract = (params: ClientOptions) => {
return getContract({
address: gasPriceOracleAddress['420'],
abi: gasPriceOracleABI,
publicClient: getL2Client(params),
})
}
/**
* Returns the base fee
* @returns {Promise<bigint>} - The base fee
* @example
* const baseFeeValue = await baseFee(params);
*/
export const baseFee = async ({
client,
blockNumber,
blockTag,
}: GasPriceOracleOptions): Promise<bigint> => {
const contract = getGasPriceOracleContract(client)
return contract.read.baseFee({ blockNumber, blockTag })
}
/**
* Returns the decimals used in the scalar
* @example
* const decimalsValue = await decimals(params);
*/
export const decimals = async ({
client,
blockNumber,
blockTag,
}: GasPriceOracleOptions): Promise<bigint> => {
const contract = getGasPriceOracleContract(client)
return contract.read.decimals({ blockNumber, blockTag })
}
/**
* Returns the gas price
* @example
* const gasPriceValue = await gasPrice(params);
*/
export const gasPrice = async ({
client,
blockNumber,
blockTag,
}: GasPriceOracleOptions): Promise<bigint> => {
const contract = getGasPriceOracleContract(client)
return contract.read.gasPrice({ blockNumber, blockTag })
}
/**
* Computes the L1 portion of the fee based on the size of the rlp encoded input
* transaction, the current L1 base fee, and the various dynamic parameters.
* @example
* const L1FeeValue = await getL1Fee(data, params);
*/
export const getL1Fee: GasPriceOracleEstimator = async (options) => {
const data = transactionSerializer(options)
const contract = getGasPriceOracleContract(options.client)
return contract.read.getL1Fee([data], {
blockNumber: options.blockNumber,
blockTag: options.blockTag,
})
}
/**
* Returns the L1 gas used
* @example
*/
export const getL1GasUsed: GasPriceOracleEstimator = async (options) => {
const data = transactionSerializer(options)
const contract = getGasPriceOracleContract(options.client)
return contract.read.getL1GasUsed([data], {
blockNumber: options.blockNumber,
blockTag: options.blockTag,
})
}
/**
* Returns the L1 base fee
* @example
* const L1BaseFeeValue = await l1BaseFee(params);
*/
export const l1BaseFee = async ({
client,
blockNumber,
blockTag,
}: GasPriceOracleOptions): Promise<bigint> => {
const contract = getGasPriceOracleContract(client)
return contract.read.l1BaseFee({ blockNumber, blockTag })
}
/**
* Returns the overhead
* @example
* const overheadValue = await overhead(params);
*/
export const overhead = async ({
client,
blockNumber,
blockTag,
}: GasPriceOracleOptions): Promise<bigint> => {
const contract = getGasPriceOracleContract(client)
return contract.read.overhead({ blockNumber, blockTag })
}
/**
* Returns the current fee scalar
* @example
* const scalarValue = await scalar(params);
*/
export const scalar = async ({
client,
...params
}: GasPriceOracleOptions): Promise<bigint> => {
const contract = getGasPriceOracleContract(client)
return contract.read.scalar(params)
}
/**
* Returns the version
* @example
* const versionValue = await version(params);
*/
export const version = async ({
client,
...params
}: GasPriceOracleOptions): Promise<string> => {
const contract = getGasPriceOracleContract(client)
return contract.read.version(params)
}
export type EstimateFeeParams = {
/**
* The transaction call data as a 0x-prefixed hex string
*/
data: Bytes
/**
* The address of the account that will be sending the transaction
*/
account: Address
} & GasPriceOracleOptions &
Omit<EstimateGasParameters, 'data' | 'account'>
export type EstimateFees = <
TAbi extends Abi | readonly unknown[],
TFunctionName extends string | undefined = undefined
>(
options: OracleTransactionParameters<TAbi, TFunctionName> &
GasPriceOracleOptions &
Omit<EstimateGasParameters, 'data'>
) => Promise<bigint>
/**
* Estimates gas for an L2 transaction including the l1 fee
*/
export const estimateFees: EstimateFees = async (options) => {
const client = getL2Client(options.client)
const encodedFunctionData = encodeFunctionData({
abi: options.abi,
args: options.args,
functionName: options.functionName,
} as EncodeFunctionDataParameters)
const [l1Fee, l2Gas, l2GasPrice] = await Promise.all([
getL1Fee({
...options,
// account must be undefined or else viem will return undefined
account: undefined as any,
}),
client.estimateGas({
to: options.to,
account: options.account,
accessList: options.accessList,
blockNumber: options.blockNumber,
blockTag: options.blockTag,
data: encodedFunctionData,
value: options.value,
} as EstimateGasParameters<typeof chains.optimism>),
client.getGasPrice(),
])
return l1Fee + l2Gas * l2GasPrice
}
/// <reference types="vite/client" />
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"baseUrl": "./src",
"strict": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "node",
"jsx": "react",
"target": "ESNext",
"noEmit": true
},
"include": ["./src"]
}
import { defineConfig } from 'tsup'
import packageJson from './package.json'
// @see https://tsup.egoist.dev/
export default defineConfig({
name: packageJson.name,
entry: ['src/estimateFees.ts'],
outDir: 'dist',
format: ['esm', 'cjs'],
splitting: false,
sourcemap: true,
clean: false,
})
import { defineConfig } from 'vitest/config'
// @see https://vitest.dev/config/
export default defineConfig({
test: {
setupFiles: './setupVitest.ts',
environment: 'jsdom',
coverage: {
provider: 'istanbul',
},
},
})
artifacts
cache
typechain
.deps
.envrc
.env
/dist/
module.exports = {
...require('../../.prettierrc.js'),
}
# @eth-optimism/web3.js-plugin
## 0.1.4
### Patch Changes
- [#9964](https://github.com/ethereum-optimism/optimism/pull/9964) [`8241220898128e1f61064f22dcb6fdd0a5f043c3`](https://github.com/ethereum-optimism/optimism/commit/8241220898128e1f61064f22dcb6fdd0a5f043c3) Thanks [@roninjin10](https://github.com/roninjin10)! - Removed only-allow command from package.json
## 0.1.3
### Patch Changes
- [#7450](https://github.com/ethereum-optimism/optimism/pull/7450) [`ac90e16a7`](https://github.com/ethereum-optimism/optimism/commit/ac90e16a7f85c4f73661ae6023135c3d00421c1e) Thanks [@roninjin10](https://github.com/roninjin10)! - Updated dev dependencies related to testing that is causing audit tooling to report failures
## 0.1.2
### Patch Changes
- [#6873](https://github.com/ethereum-optimism/optimism/pull/6873) [`fdab6caa7`](https://github.com/ethereum-optimism/optimism/commit/fdab6caa7e6684b08882d2a766ccd727068c2b2f) Thanks [@spacesailor24](https://github.com/spacesailor24)! - Update code exmaples in README
## 0.1.1
### Patch Changes
- [#6848](https://github.com/ethereum-optimism/optimism/pull/6848) [`b08fccd9e`](https://github.com/ethereum-optimism/optimism/commit/b08fccd9e21c499f9fefd4d58fb8a36bfa0d800a) Thanks [@spacesailor24](https://github.com/spacesailor24)! - Correct use of web3js-plugin to web3.js-plugin in README. Rename OptimismFeeEstimationPlugin export to OptimismPlugin
MIT License
Copyright (c) 2023 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.
# @eth-optimism/web3.js-plugin
This web3.js plugin adds utility functions for estimating L1 and L2 gas for OP chains by wrapping the [GasPriceOracle](../contracts-bedrock/contracts/l2/GasPriceOracle.sol) contract
The GasPriceOracle is [deployed to Optimism](https://optimistic.etherscan.io/address/0x420000000000000000000000000000000000000F) and other OP chains at a predeployed address of `0x420000000000000000000000000000000000000F`
For more detailed information about gas fees on Optimism's Layer 2, you can visit the [official documentation](https://community.optimism.io/docs/developers/build/transaction-fees/#the-l2-execution-fee)
## Installation
This plugin is intended to be [registered](https://docs.web3js.org/guides/web3_plugin_guide/plugin_users#registering-the-plugin) onto an instance of `Web3`. It has a [peerDependency](https://nodejs.org/es/blog/npm/peer-dependencies) of `web3` version `4.x`, so make sure you have that latest version of `web3` installed for your project before installing the plugin
### Installing the Plugin
```bash
pnpm install @eth-optimism/web3.js-plugin
```
```bash
npm install @eth-optimism/web3.js-plugin
```
```bash
yarn add @eth-optimism/web3.js-plugin
```
### Registering the Plugin
```typescript
import Web3 from 'web3'
import { OptimismPlugin } from '@eth-optimism/web3.js-plugin'
const web3 = new Web3('http://yourProvider.com')
web3.registerPlugin(new OptimismPlugin())
```
You will now have access to the following functions under the `op` namespace, i.e. `web3.op.someMethod`
## API
| Function Name | Returns |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [estimateFees](#estimatefees) | The combined estimated L1 and L2 fees for a transaction |
| [getL1Fee](#getl1fee) | The L1 portion of the fee based on the size of the [RLP](https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/) encoded transaction, the current L1 base fee, and other various dynamic parameters |
| [getL2Fee](#getl2fee) | The L2 portion of the fee based on the simulated execution of the provided transaction and current `gasPrice` |
| [getBaseFee](#getbasefee) | The current L2 base fee |
| [getDecimals](#getdecimals) | The decimals used in the scalar |
| [getGasPrice](#getgasprice) | The current L2 gas price |
| [getL1GasUsed](#getl1gasused) | The amount of L1 gas estimated to be used to execute a transaction |
| [getL1BaseFee](#getdegetl1basefeecimals) | The L1 base fee |
| [getOverhead](#getoverhead) | The current overhead |
| [getScalar](#getscalar) | The current fee scalar |
| [getVersion](#getversion) | The current version of `GasPriceOracle` |
---
### `estimateFees`
Computes the total (L1 + L2) fee estimate to execute a transaction
```typescript
async estimateFees(transaction: Transaction, returnFormat?: ReturnFormat)
```
#### Parameters
- `transaction: Transaction` - An unsigned web3.js [transaction](https://docs.web3js.org/api/web3-types/interface/Transaction) object
- `returnFormat?: ReturnFormat` - A web3.js [DataFormat][1] object that specifies how to format number and bytes values
- If `returnFormat` is not provided, [DEFAULT_RETURN_FORMAT][2] is used which will format numbers to `BigInt`s
#### Returns
- `Promise<Numbers>` - The estimated total fee as a `BigInt` by default, but `returnFormat` determines type
#### Example
```typescript
import Web3 from 'web3'
import { OptimismPlugin } from '@eth-optimism/web3.js-plugin'
import {
l2StandardBridgeABI,
l2StandardBridgeAddress,
} from '@eth-optimism/contracts-ts'
const web3 = new Web3('https://mainnet.optimism.io')
web3.registerPlugin(new OptimismPlugin())
const l2BridgeContract = new web3.eth.Contract(
l2StandardBridgeABI,
optimistAddress[420]
)
const encodedWithdrawMethod = l2BridgeContract.methods
.withdraw(
// l2 token address
'0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000',
// amount
Web3.utils.toWei('0.00000001', 'ether'),
// l1 gas
0,
// extra data
'0x00'
)
.encodeABI()
const totalFee = await web3.op.estimateFees({
chainId: 10,
data: encodedWithdrawMethod,
value: Web3.utils.toWei('0.00000001', 'ether'),
type: 2,
to: '0x420000000000000000000000000000000000000F',
from: '0x6387a88a199120aD52Dd9742C7430847d3cB2CD4',
maxFeePerGas: Web3.utils.toWei('0.2', 'gwei'),
maxPriorityFeePerGas: Web3.utils.toWei('0.1', 'gwei'),
})
console.log(totalFee) // 26608988767659n
```
##### Formatting Response as a Hex String
```typescript
import Web3 from 'web3'
import { OptimismPlugin } from '@eth-optimism/web3.js-plugin'
import {
l2StandardBridgeABI,
l2StandardBridgeAddress,
} from '@eth-optimism/contracts-ts'
const web3 = new Web3('https://mainnet.optimism.io')
web3.registerPlugin(new OptimismPlugin())
const l2BridgeContract = new web3.eth.Contract(
l2StandardBridgeABI,
optimistAddress[420]
)
const encodedWithdrawMethod = l2BridgeContract.methods
.withdraw(
// l2 token address
'0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000',
// amount
Web3.utils.toWei('0.00000001', 'ether'),
// l1 gas
0,
// extra data
'0x00'
)
.encodeABI()
const totalFee = await web3.op.estimateFees(
{
chainId: 10,
data: encodedWithdrawMethod,
value: Web3.utils.toWei('0.00000001', 'ether'),
type: 2,
to: '0x420000000000000000000000000000000000000F',
from: '0x6387a88a199120aD52Dd9742C7430847d3cB2CD4',
maxFeePerGas: Web3.utils.toWei('0.2', 'gwei'),
maxPriorityFeePerGas: Web3.utils.toWei('0.1', 'gwei'),
},
{ number: FMT_NUMBER.HEX, bytes: FMT_BYTES.HEX }
)
console.log(totalFee) // 0x18336352c5ab
```
### `getL1Fee`
Computes the L1 portion of the fee based on the size of the rlp encoded input transaction, the current L1 base fee, and the various dynamic parameters
```typescript
async getL1Fee(transaction: Transaction, returnFormat?: ReturnFormat)
```
#### Parameters
- `transaction: Transaction` - An unsigned web3.js [transaction](https://docs.web3js.org/api/web3-types/interface/Transaction) object
- `returnFormat?: ReturnFormat` - A web3.js [DataFormat][1] object that specifies how to format number and bytes values
- If `returnFormat` is not provided, [DEFAULT_RETURN_FORMAT][2] is used which will format numbers to `BigInt`s
#### Returns
- `Promise<Numbers>` - The estimated L1 fee as a `BigInt` by default, but `returnFormat` determines type
#### Example
```typescript
import { Contract } from 'web3'
import { optimistABI, optimistAddress } from '@eth-optimism/contracts-ts'
const optimistContract = new Contract(optimistABI, optimistAddress[420])
const encodedBurnMethod = optimistContract.methods
.burn('0x77194aa25a06f932c10c0f25090f3046af2c85a6')
.encodeABI()
const l1Fee = await web3.op.getL1Fee({
chainId: 10,
data: encodedBurnMethod,
type: 2,
})
console.log(l1Fee) // 18589035222172n
```
##### Formatting Response as a Hex String
```typescript
import { Contract } from 'web3'
import { optimistABI, optimistAddress } from '@eth-optimism/contracts-ts'
const optimistContract = new Contract(optimistABI, optimistAddress[420])
const encodedBurnMethod = optimistContract.methods
.burn('0x77194aa25a06f932c10c0f25090f3046af2c85a6')
.encodeABI()
const l1Fee = await web3.op.getL1Fee(
{
chainId: 10,
data: encodedBurnMethod,
type: 2,
},
{ number: FMT_NUMBER.HEX, bytes: FMT_BYTES.HEX }
)
console.log(l1Fee) // 0x10e818d7549c
```
### `getL2Fee`
Retrieves the amount of L2 gas estimated to execute `transaction`
```typescript
async getL2Fee(transaction: Transaction, returnFormat?: ReturnFormat)
```
#### Parameters
- `transaction: Transaction` - An unsigned web3.js [transaction](https://docs.web3js.org/api/web3-types/interface/Transaction) object
- `options?: { blockNumber?: BlockNumberOrTag, returnFormat?: ReturnFormat }` - An optional object with properties:
- `blockNumber?: BlockNumberOrTag` - Specifies what block to use for gas estimation. Can be either:
- **Note** Specifying a block to estimate L2 gas for is currently not working
- A web3.js [Numbers](https://docs.web3js.org/api/web3-types#Numbers)
- A web3.js [BlockTags](https://docs.web3js.org/api/web3-types/enum/BlockTags)
- If not provided, `BlockTags.LATEST` is used
- `returnFormat?: ReturnFormat` - A web3.js [DataFormat][1] object that specifies how to format number and bytes values
- If `returnFormat` is not provided, [DEFAULT_RETURN_FORMAT][2] is used which will format numbers to `BigInt`s
#### Returns
- `Promise<Numbers>` - The estimated total fee as a `BigInt` by default, but `returnFormat` determines type
#### Example
```typescript
import { Contract } from 'web3'
import { optimistABI, optimistAddress } from '@eth-optimism/contracts-ts'
const optimistContract = new Contract(optimistABI, optimistAddress[420])
const encodedBurnMethod = optimistContract.methods
.burn('0x77194aa25a06f932c10c0f25090f3046af2c85a6')
.encodeABI()
const l2Fee = await web3.op.getL2Fee({
chainId: '0xa',
data: encodedBurnMethod,
type: '0x2',
to: optimistAddress[420],
from: '0x77194aa25a06f932c10c0f25090f3046af2c85a6',
})
console.log(l2Fee) // 2659500n
```
##### Formatting Response as a Hex String
```typescript
import { Contract } from 'web3'
import { optimistABI, optimistAddress } from '@eth-optimism/contracts-ts'
const optimistContract = new Contract(optimistABI, optimistAddress[420])
const encodedBurnMethod = optimistContract.methods
.burn('0x77194aa25a06f932c10c0f25090f3046af2c85a6')
.encodeABI()
const l2Fee = await web3.op.getL2Fee(
{
chainId: '0xa',
data: encodedBurnMethod,
type: '0x2',
to: optimistAddress[420],
from: '0x77194aa25a06f932c10c0f25090f3046af2c85a6',
},
{
returnFormat: { number: FMT_NUMBER.HEX, bytes: FMT_BYTES.HEX },
}
)
console.log(l2Fee) // 0x2894ac
```
### `getBaseFee`
Retrieves the current L2 base fee
```typescript
async getBaseFee(returnFormat?: ReturnFormat)
```
#### Parameters
- `returnFormat?: ReturnFormat` - A web3.js [DataFormat][1] object that specifies how to format number and bytes values
- If `returnFormat` is not provided, [DEFAULT_RETURN_FORMAT][2] is used which will format numbers to `BigInt`s
#### Returns
- `Promise<Numbers>` - The L2 base fee as a `BigInt` by default, but `returnFormat` determines type
#### Example
```typescript
const baseFee = await web3.op.getBaseFee()
console.log(baseFee) // 68n
```
##### Formatting Response as a Hex String
```typescript
const baseFee = await web3.op.getBaseFee({
number: FMT_NUMBER.HEX,
bytes: FMT_BYTES.HEX,
})
console.log(baseFee) // 0x44
```
### `getDecimals`
Retrieves the decimals used in the scalar
```typescript
async getDecimals(returnFormat?: ReturnFormat)
```
#### Parameters
- `returnFormat?: ReturnFormat` - A web3.js [DataFormat][3] object that specifies how to format number and bytes values
- If `returnFormat` is not provided, [DEFAULT_RETURN_FORMAT][2] is used which will format numbers to `BigInt`s
#### Returns
- `Promise<Numbers>` - The number of decimals as a `BigInt` by default, but `returnFormat` determines type
#### Example
```typescript
const decimals = await web3.op.getDecimals()
console.log(decimals) // 6n
```
##### Formatting Response as a Hex String
```typescript
const decimals = await web3.op.getDecimals({
number: FMT_NUMBER.HEX,
bytes: FMT_BYTES.HEX,
})
console.log(decimals) // 0x6
```
### `getGasPrice`
Retrieves the current L2 gas price (base fee)
```typescript
async getGasPrice(returnFormat?: ReturnFormat)
```
#### Parameters
- `returnFormat?: ReturnFormat` - A web3.js [DataFormat][3] object that specifies how to format number and bytes values
- If `returnFormat` is not provided, [DEFAULT_RETURN_FORMAT][2] is used which will format numbers to `BigInt`s
#### Returns
- `Promise<Numbers>` - The current L2 gas price as a `BigInt` by default, but `returnFormat` determines type
#### Example
```typescript
const gasPrice = await web3.op.getGasPrice()
console.log(gasPrice) // 77n
```
##### Formatting Response as a Hex String
```typescript
const gasPrice = await web3.op.getGasPrice({
number: FMT_NUMBER.HEX,
bytes: FMT_BYTES.HEX,
})
console.log(gasPrice) // 0x4d
```
### `getL1GasUsed`
Computes the amount of L1 gas used for {transaction}. Adds the overhead which represents the per-transaction gas overhead of posting the {transaction} and state roots to L1. Adds 68 bytes of padding to account for the fact that the input does not have a signature.
```typescript
async getL1GasUsed(transaction: Transaction, returnFormat?: ReturnFormat)
```
#### Parameters
- `transaction: Transaction` - An unsigned web3.js [transaction](https://docs.web3js.org/api/web3-types/interface/Transaction) object
- `returnFormat?: ReturnFormat` - A web3.js [DataFormat][3] object that specifies how to format number and bytes values
- If `returnFormat` is not provided, [DEFAULT_RETURN_FORMAT][2] is used which will format numbers to `BigInt`s
#### Returns
- `Promise<Numbers>` - The amount of gas as a `BigInt` by default, but `returnFormat` determines type
#### Example
```typescript
import { Contract } from 'web3'
import { optimistABI, optimistAddress } from '@eth-optimism/contracts-ts'
const optimistContract = new Contract(optimistABI, optimistAddress[420])
const encodedBurnMethod = optimistContract.methods
.burn('0x77194aa25a06f932c10c0f25090f3046af2c85a6')
.encodeABI()
const l1GasUsed = await web3.op.getL1GasUsed({
chainId: 10,
data: encodedBurnMethod,
type: 2,
})
console.log(l1GasUsed) // 1884n
```
##### Formatting Response as a Hex String
```typescript
import { Contract } from 'web3'
import { optimistABI, optimistAddress } from '@eth-optimism/contracts-ts'
const optimistContract = new Contract(optimistABI, optimistAddress[420])
const encodedBurnMethod = optimistContract.methods
.burn('0x77194aa25a06f932c10c0f25090f3046af2c85a6')
.encodeABI()
const l1GasUsed = await web3.op.getL1GasUsed(
{
chainId: 10,
data: encodedBurnMethod,
type: 2,
},
{ number: FMT_NUMBER.HEX, bytes: FMT_BYTES.HEX }
)
console.log(l1GasUsed) // 0x75c
```
### `getL1BaseFee`
Retrieves the latest known L1 base fee
```typescript
async getL1BaseFee(returnFormat?: ReturnFormat)
```
#### Parameters
- `returnFormat?: ReturnFormat` - A web3.js [DataFormat][3] object that specifies how to format number and bytes values
- If `returnFormat` is not provided, [DEFAULT_RETURN_FORMAT][2] is used which will format numbers to `BigInt`s
#### Returns
- `Promise<Numbers>` - The L1 base fee as a `BigInt` by default, but `returnFormat` determines type
#### Example
```typescript
const baseFee = await web3.op.getL1BaseFee()
console.log(baseFee) // 13752544112n
```
##### Formatting Response as a Hex String
```typescript
const baseFee = await web3.op.getL1BaseFee({
number: FMT_NUMBER.HEX,
bytes: FMT_BYTES.HEX,
})
console.log(baseFee) // 0x333b72b70
```
### `getOverhead`
Retrieves the current fee overhead
```typescript
async getOverhead(returnFormat?: ReturnFormat)
```
#### Parameters
- `returnFormat?: ReturnFormat` - A web3.js [DataFormat][3] object that specifies how to format number and bytes values
- If `returnFormat` is not provided, [DEFAULT_RETURN_FORMAT][2] is used which will format numbers to `BigInt`s
#### Returns
- `Promise<Numbers>` - The current overhead as a `BigInt` by default, but `returnFormat` determines type
#### Example
```typescript
const overhead = await web3.op.getOverhead()
console.log(overhead) // 188n
```
##### Formatting Response as a Hex String
```typescript
const overhead = await web3.op.getOverhead({
number: FMT_NUMBER.HEX,
bytes: FMT_BYTES.HEX,
})
console.log(overhead) // 0xbc
```
### `getScalar`
Retrieves the current fee scalar
```typescript
async getScalar(returnFormat?: ReturnFormat)
```
#### Parameters
- `returnFormat?: ReturnFormat` - A web3.js [DataFormat][1] object that specifies how to format number and bytes values
- If `returnFormat` is not provided, [DEFAULT_RETURN_FORMAT][2] is used which will format numbers to `BigInt`s
#### Returns
- `Promise<Numbers>` - The current scalar fee as a `BigInt` by default, but `returnFormat` determines type
#### Example
```typescript
const scalarFee = await web3.op.getScalar()
console.log(scalarFee) // 684000n
```
##### Formatting Response as a Hex String
```typescript
const scalarFee = await web3.op.getScalar({
number: FMT_NUMBER.HEX,
bytes: FMT_BYTES.HEX,
})
console.log(scalarFee) // 0xa6fe0
```
### `getVersion`
Retrieves the full semver version of GasPriceOracle
```typescript
async getVersion()
```
#### Returns
- `Promise<string>` - The semver version
#### Example
```typescript
const version = await web3.op.getVersion()
console.log(version) // 1.0.0
```
## Known Issues
- As of version `4.0.3` of web3.js, both `input` and `data` parameters are automatically added to a transaction objects causing the gas estimations to be inflated. This was corrected in [this](https://github.com/web3/web3.js/pull/6294) PR, but has yet to be released
- For the plugin function `getL2Fee`, you should be able to get the fee estimates using the state of the blockchain at a specified block, however, this doesn't seem to be working with web3.js and requires further investigation
[1]: https://docs.web3js.org/api/web3-types#DataFormat
[2]: https://docs.web3js.org/api/web3-types#DEFAULT_RETURN_FORMAT
[3]: https://docs.web3js.org/api/web3-types#DataFormat
{
"name": "@eth-optimism/web3.js-plugin",
"version": "0.1.4",
"description": "A Web3.js plugin for doing OP-Chain gas estimation",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/ethereum-optimism/optimism.git",
"directory": "packages/web3js-plugin"
},
"homepage": "https://optimism.io",
"type": "module",
"exports": {
".": {
"import": "./dist/plugin.js",
"require": "./dist/plugin.cjs",
"default": "./dist/plugin.js",
"types": "./src/plugin.d.ts"
}
},
"types": "dist/plugin.d.ts",
"files": [
"dist/",
"src/"
],
"scripts": {
"build": "tsup",
"lint": "prettier --check .",
"lint:fix": "prettier --write .",
"test": "vitest --coverage",
"test:coverage": "vitest run --coverage",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@eth-optimism/contracts-ts": "workspace:^",
"@swc/core": "^1.4.13",
"@vitest/coverage-istanbul": "^1.2.2",
"tsup": "^8.0.1",
"typescript": "^5.4.5",
"viem": "^2.8.13",
"vite": "^5.1.7",
"vitest": "^1.2.2",
"zod": "^3.22.4"
},
"dependencies": {
"@ethereumjs/rlp": "^5.0.2",
"web3-eth": "^4.0.3",
"web3-eth-accounts": "^4.0.3"
},
"peerDependencies": {
"web3": ">= 4.0.3 < 5.x"
}
}
import { beforeAll, describe, expect, test } from 'vitest'
import { z } from 'zod'
import Web3, { Contract, FMT_BYTES, FMT_NUMBER } from 'web3'
import {
l2StandardBridgeABI,
l2StandardBridgeAddress,
optimistABI,
optimistAddress,
} from '@eth-optimism/contracts-ts'
import { OptimismPlugin } from './plugin'
const defaultProvider = 'https://mainnet.optimism.io'
const provider = z
.string()
.url()
.default(defaultProvider)
.parse(process.env['VITE_L2_RPC_URL'])
if (provider === defaultProvider)
console.warn(
'Warning: Using default public provider, this could cause tests to fail due to rate limits. Set the VITE_L2_RPC_URL env to override default provider'
)
describe('OptimismPlugin', () => {
let web3: Web3
beforeAll(() => {
web3 = new Web3(provider)
web3.registerPlugin(new OptimismPlugin())
})
test('should be registered under .op namespace', () =>
expect(web3.op).toMatchInlineSnapshot(`
OptimismPlugin {
"_accountProvider": {
"create": [Function],
"decrypt": [Function],
"encrypt": [Function],
"hashMessage": [Function],
"privateKeyToAccount": [Function],
"recover": [Function],
"recoverTransaction": [Function],
"sign": [Function],
"signTransaction": [Function],
"wallet": Wallet [],
},
"_emitter": EventEmitter {
"_events": {},
"_eventsCount": 0,
"_maxListeners": undefined,
Symbol(kCapture): false,
},
"_gasPriceOracleContract": undefined,
"_requestManager": Web3RequestManager {
"_emitter": EventEmitter {
"_events": {
"BEFORE_PROVIDER_CHANGE": [Function],
"PROVIDER_CHANGED": [Function],
},
"_eventsCount": 2,
"_maxListeners": undefined,
Symbol(kCapture): false,
},
"_provider": HttpProvider {
"clientUrl": "https://opt-mainnet.g.alchemy.com/v2/OVlbpe9COlhG-ijOXGvL_phb5ns6p9-w",
"httpProviderOptions": undefined,
},
"useRpcCallSpecification": undefined,
},
"_subscriptionManager": Web3SubscriptionManager {
"_subscriptions": Map {},
"registeredSubscriptions": {
"logs": [Function],
"newBlockHeaders": [Function],
"newHeads": [Function],
"newPendingTransactions": [Function],
"pendingTransactions": [Function],
"syncing": [Function],
},
"requestManager": Web3RequestManager {
"_emitter": EventEmitter {
"_events": {
"BEFORE_PROVIDER_CHANGE": [Function],
"PROVIDER_CHANGED": [Function],
},
"_eventsCount": 2,
"_maxListeners": undefined,
Symbol(kCapture): false,
},
"_provider": HttpProvider {
"clientUrl": "https://opt-mainnet.g.alchemy.com/v2/OVlbpe9COlhG-ijOXGvL_phb5ns6p9-w",
"httpProviderOptions": undefined,
},
"useRpcCallSpecification": undefined,
},
"tolerateUnlinkedSubscription": false,
},
"_wallet": Wallet [],
"config": {
"blockHeaderTimeout": 10,
"defaultAccount": undefined,
"defaultBlock": "latest",
"defaultChain": "mainnet",
"defaultCommon": undefined,
"defaultHardfork": "london",
"defaultMaxPriorityFeePerGas": "0x9502f900",
"defaultNetworkId": undefined,
"defaultTransactionType": "0x0",
"enableExperimentalFeatures": {
"useRpcCallSpecification": false,
"useSubscriptionWhenCheckingBlockTimeout": false,
},
"handleRevert": false,
"maxListenersWarningThreshold": 100,
"transactionBlockTimeout": 50,
"transactionBuilder": undefined,
"transactionConfirmationBlocks": 24,
"transactionConfirmationPollingInterval": undefined,
"transactionPollingInterval": 1000,
"transactionPollingTimeout": 750000,
"transactionReceiptPollingInterval": undefined,
"transactionSendTimeout": 750000,
"transactionTypeParser": undefined,
},
"pluginNamespace": "op",
"providers": {
"HttpProvider": [Function],
"WebsocketProvider": [Function],
},
}
`))
describe('should return a bigint by default', () => {
test('getBaseFee', async () =>
expect(await web3.op.getBaseFee()).toBeTypeOf('bigint'))
test('getDecimals should return 6n', async () =>
expect(await web3.op.getDecimals()).toBe(BigInt(6)))
test('getGasPrice', async () =>
expect(await web3.op.getGasPrice()).toBeTypeOf('bigint'))
test('getL1BaseFee', async () =>
expect(await web3.op.getL1BaseFee()).toBeTypeOf('bigint'))
test('getOverhead should return 188n', async () =>
expect(await web3.op.getOverhead()).toBe(BigInt(188)))
test('getScalar should return 684000n', async () =>
expect(await web3.op.getScalar()).toBe(BigInt(684000)))
})
describe('should return a number', () => {
const numberFormat = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }
test('getBaseFee', async () =>
expect(await web3.op.getBaseFee(numberFormat)).toBeTypeOf('number'))
test('getDecimals should return 6', async () =>
expect(await web3.op.getDecimals(numberFormat)).toBe(6))
test('getGasPrice', async () =>
expect(await web3.op.getGasPrice(numberFormat)).toBeTypeOf('number'))
test('getL1BaseFee', async () =>
expect(await web3.op.getL1BaseFee(numberFormat)).toBeTypeOf('number'))
test('getOverhead should return 188', async () =>
expect(await web3.op.getOverhead(numberFormat)).toBe(188))
test('getScalar should return 684000', async () =>
expect(await web3.op.getScalar(numberFormat)).toBe(684000))
})
test('getVersion should return the string 1.0.0', async () =>
expect(await web3.op.getVersion()).toBe('1.0.0'))
describe('Contract transaction gas estimates - optimistABI.burn', () => {
let optimistContract: Contract<typeof optimistABI>
let encodedBurnMethod: string
beforeAll(() => {
optimistContract = new web3.eth.Contract(optimistABI)
encodedBurnMethod = optimistContract.methods
.burn('0x77194aa25a06f932c10c0f25090f3046af2c85a6')
.encodeABI()
})
describe('should return a bigint by default', () => {
test('getL1Fee', async () => {
expect(
await web3.op.getL1Fee({
chainId: '0xa',
data: encodedBurnMethod,
type: '0x2',
})
).toBeTypeOf('bigint')
})
test('getL1GasUsed should return 1884n', async () =>
expect(
await web3.op.getL1GasUsed({
chainId: '0xa',
data: encodedBurnMethod,
type: '0x2',
})
).toBe(BigInt(1884)))
test('estimateFees', async () =>
expect(
await web3.op.estimateFees({
chainId: 10,
data: encodedBurnMethod,
type: 2,
to: optimistAddress[10],
from: '0x77194aa25a06f932c10c0f25090f3046af2c85a6',
})
).toBeTypeOf('bigint'))
test('getL2Fee', async () => {
expect(
await web3.op.getL2Fee({
chainId: '0xa',
data: encodedBurnMethod,
type: '0x2',
to: optimistAddress[10],
from: '0x77194aa25a06f932c10c0f25090f3046af2c85a6',
})
).toBeTypeOf('bigint')
})
test('estimateFees', async () =>
expect(
await web3.op.estimateFees({
chainId: 10,
data: encodedBurnMethod,
type: 2,
to: optimistAddress[10],
from: '0x77194aa25a06f932c10c0f25090f3046af2c85a6',
})
).toBeTypeOf('bigint'))
})
describe('should return a hexString', () => {
const hexStringFormat = { number: FMT_NUMBER.HEX, bytes: FMT_BYTES.HEX }
test('getL1Fee', async () => {
expect(
await web3.op.getL1Fee(
{
chainId: '0xa',
data: encodedBurnMethod,
type: '0x2',
},
hexStringFormat
)
).toBeTypeOf('string')
})
test('getL1GasUsed should return 0x75c', async () =>
expect(
await web3.op.getL1GasUsed(
{
chainId: '0xa',
data: encodedBurnMethod,
type: '0x2',
},
hexStringFormat
)
).toBe('0x75c'))
test('estimateFees', async () =>
expect(
await web3.op.estimateFees(
{
chainId: 10,
data: encodedBurnMethod,
type: 2,
to: optimistAddress[10],
from: '0x77194aa25a06f932c10c0f25090f3046af2c85a6',
},
hexStringFormat
)
).toBeTypeOf('string'))
})
})
describe('Contract transaction gas estimates - l2StandardBridgeABI.withdraw', () => {
let l2BridgeContract: Contract<typeof l2StandardBridgeABI>
let encodedWithdrawMethod: string
beforeAll(() => {
l2BridgeContract = new Contract(
l2StandardBridgeABI,
l2StandardBridgeAddress[420]
)
encodedWithdrawMethod = l2BridgeContract.methods
.withdraw(
// l2 token address
'0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000',
// amount
Web3.utils.toWei('0.00000001', 'ether'),
// l1 gas
0,
// extra data
'0x00'
)
.encodeABI()
})
describe('should return a bigint by default', () => {
test('getL1Fee', async () => {
expect(
await web3.op.getL1Fee({
chainId: '0xa',
data: encodedWithdrawMethod,
type: '0x2',
})
).toBeTypeOf('bigint')
})
test('getL1GasUsed should return 2592n', async () =>
expect(
await web3.op.getL1GasUsed({
chainId: '0xa',
data: encodedWithdrawMethod,
type: '0x2',
})
).toBe(BigInt(2592)))
test('estimateFees', async () =>
expect(
await web3.op.estimateFees({
chainId: 10,
data: encodedWithdrawMethod,
value: Web3.utils.toWei('0.00000001', 'ether'),
type: 2,
to: l2StandardBridgeAddress[420],
from: '0x6387a88a199120aD52Dd9742C7430847d3cB2CD4',
maxFeePerGas: Web3.utils.toWei('0.2', 'gwei'),
maxPriorityFeePerGas: Web3.utils.toWei('0.1', 'gwei'),
})
).toBeTypeOf('bigint'))
})
})
})
import Web3, {
type BlockNumberOrTag,
BlockTags,
Contract,
type DataFormat,
DEFAULT_RETURN_FORMAT,
FMT_BYTES,
FMT_NUMBER,
type Transaction,
Web3PluginBase,
} from 'web3'
import { TransactionFactory, type TxData } from 'web3-eth-accounts'
import { estimateGas, formatTransaction } from 'web3-eth'
import {
gasPriceOracleABI,
gasPriceOracleAddress,
} from '@eth-optimism/contracts-ts'
import { RLP } from '@ethereumjs/rlp'
export class OptimismPlugin extends Web3PluginBase {
public pluginNamespace = 'op'
private _gasPriceOracleContract:
| Contract<typeof gasPriceOracleABI>
| undefined
/**
* Retrieves the current L2 base fee
* @param {DataFormat} [returnFormat=DEFAULT_RETURN_FORMAT] - The web3.js format object that specifies how to format number and bytes values
* @returns {Promise<bigint>} - The L2 base fee as a BigInt by default, but {returnFormat} determines type
* @example
* const baseFeeValue: bigint = await web3.op.getBaseFee();
* @example
* const numberFormat = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }
* const baseFeeValue: number = await web3.op.getBaseFee(numberFormat);
*/
public async getBaseFee<
ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT
>(returnFormat?: ReturnFormat) {
return Web3.utils.format(
{ format: 'uint' },
await this._getPriceOracleContractInstance().methods.baseFee().call(),
returnFormat ?? DEFAULT_RETURN_FORMAT
)
}
/**
* Retrieves the decimals used in the scalar
* @param {DataFormat} [returnFormat=DEFAULT_RETURN_FORMAT] - The web3.js format object that specifies how to format number and bytes values
* @returns {Promise<Numbers>} - The number of decimals as a BigInt by default, but {returnFormat} determines type
* @example
* const decimalsValue: bigint = await web3.op.getDecimals();
* @example
* const numberFormat = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }
* const decimalsValue: number = await web3.op.getDecimals(numberFormat);
*/
public async getDecimals<
ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT
>(returnFormat?: ReturnFormat) {
return Web3.utils.format(
{ format: 'uint' },
await this._getPriceOracleContractInstance().methods.decimals().call(),
returnFormat ?? DEFAULT_RETURN_FORMAT
)
}
/**
* Retrieves the current L2 gas price (base fee)
* @param {DataFormat} [returnFormat=DEFAULT_RETURN_FORMAT] - The web3.js format object that specifies how to format number and bytes values
* @returns {Promise<Numbers>} - The current L2 gas price as a BigInt by default, but {returnFormat} determines type
* @example
* const gasPriceValue: bigint = await web3.op.getGasPrice();
* @example
* const numberFormat = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }
* const gasPriceValue: number = await web3.op.getGasPrice(numberFormat);
*/
public async getGasPrice<
ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT
>(returnFormat?: ReturnFormat) {
return Web3.utils.format(
{ format: 'uint' },
await this._getPriceOracleContractInstance().methods.gasPrice().call(),
returnFormat ?? DEFAULT_RETURN_FORMAT
)
}
/**
* Computes the L1 portion of the fee based on the size of the rlp encoded input
* transaction, the current L1 base fee, and the various dynamic parameters
* @param transaction - An unsigned web3.js {Transaction} object
* @param {DataFormat} [returnFormat=DEFAULT_RETURN_FORMAT] - The web3.js format object that specifies how to format number and bytes values
* @returns {Promise<Numbers>} - The fee as a BigInt by default, but {returnFormat} determines type
* @example
* const l1FeeValue: bigint = await getL1Fee(transaction);
* @example
* const numberFormat = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }
* const l1FeeValue: number = await getL1Fee(transaction, numberFormat);
*/
public async getL1Fee<
ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT
>(transaction: Transaction, returnFormat?: ReturnFormat) {
return Web3.utils.format(
{ format: 'uint' },
await this._getPriceOracleContractInstance()
.methods.getL1Fee(this._serializeTransaction(transaction))
.call(),
returnFormat ?? DEFAULT_RETURN_FORMAT
)
}
/**
* Computes the amount of L1 gas used for {transaction}. Adds the overhead which
* represents the per-transaction gas overhead of posting the {transaction} and state
* roots to L1. Adds 68 bytes of padding to account for the fact that the input does
* not have a signature.
* @param transaction - An unsigned web3.js {Transaction} object
* @param {DataFormat} [returnFormat=DEFAULT_RETURN_FORMAT] - The web3.js format object that specifies how to format number and bytes values
* @returns {Promise<Numbers>} - The amount gas as a BigInt by default, but {returnFormat} determines type
* @example
* const gasUsedValue: bigint = await getL1GasUsed(transaction);
* @example
* const numberFormat = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }
* const gasUsedValue: number = await getL1GasUsed(transaction, numberFormat);
*/
public async getL1GasUsed<
ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT
>(transaction: Transaction, returnFormat?: ReturnFormat) {
return Web3.utils.format(
{ format: 'uint' },
await this._getPriceOracleContractInstance()
.methods.getL1GasUsed(this._serializeTransaction(transaction))
.call(),
returnFormat ?? DEFAULT_RETURN_FORMAT
)
}
/**
* Retrieves the latest known L1 base fee
* @param {DataFormat} [returnFormat=DEFAULT_RETURN_FORMAT] - The web3.js format object that specifies how to format number and bytes values
* @returns {Promise<Numbers>} - The L1 base fee as a BigInt by default, but {returnFormat} determines type
* @example
* const baseFeeValue: bigint = await web3.op.getL1BaseFee();
* @example
* const numberFormat = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }
* const baseFeeValue: number = await web3.op.getL1BaseFee(numberFormat);
*/
public async getL1BaseFee<
ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT
>(returnFormat?: ReturnFormat) {
return Web3.utils.format(
{ format: 'uint' },
await this._getPriceOracleContractInstance().methods.l1BaseFee().call(),
returnFormat ?? DEFAULT_RETURN_FORMAT
)
}
/**
* Retrieves the current fee overhead
* @param {DataFormat} [returnFormat=DEFAULT_RETURN_FORMAT] - The web3.js format object that specifies how to format number and bytes values
* @returns {Promise<Numbers>} - The current overhead fee as a BigInt by default, but {returnFormat} determines type
* @example
* const overheadValue: bigint = await web3.op.getOverhead();
* @example
* const numberFormat = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }
* const overheadValue: number = await web3.op.getOverhead(numberFormat);
*/
public async getOverhead<
ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT
>(returnFormat?: ReturnFormat) {
return Web3.utils.format(
{ format: 'uint' },
await this._getPriceOracleContractInstance().methods.overhead().call(),
returnFormat ?? DEFAULT_RETURN_FORMAT
)
}
/**
* Retrieves the current fee scalar
* @param {DataFormat} [returnFormat=DEFAULT_RETURN_FORMAT] - The web3.js format object that specifies how to format number and bytes values
* @returns {Promise<Numbers>} - The current scalar fee as a BigInt by default, but {returnFormat} determines type
* @example
* const scalarValue: bigint = await web3.op.getScalar();
* @example
* const numberFormat = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }
* const scalarValue: number = await web3.op.getScalar(numberFormat);
*/
public async getScalar<
ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT
>(returnFormat?: ReturnFormat) {
return Web3.utils.format(
{ format: 'uint' },
await this._getPriceOracleContractInstance().methods.scalar().call(),
returnFormat ?? DEFAULT_RETURN_FORMAT
)
}
/**
* Retrieves the full semver version of GasPriceOracle
* @returns {Promise<string>} - The semver version
* @example
* const version = await web3.op.getVersion();
*/
public async getVersion() {
return this._getPriceOracleContractInstance().methods.version().call()
}
/**
* Retrieves the amount of L2 gas estimated to execute {transaction}
* @param transaction - An unsigned web3.js {Transaction} object
* @param {{ blockNumber: BlockNumberOrTag, returnFormat: DataFormat }} [options={blockNumber: BlockTags.LATEST, returnFormat: DEFAULT_RETURN_FORMAT}] -
* An options object specifying what block to use for gas estimates and the web3.js format object that specifies how to format number and bytes values
* @returns {Promise<Numbers>} - The gas estimate as a BigInt by default, but {returnFormat} determines type
* @example
* const l2Fee: bigint = await getL2Fee(transaction);
* @example
* const numberFormat = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }
* const l2Fee: number = await getL2Fee(transaction, numberFormat);
*/
public async getL2Fee<
ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT
>(
transaction: Transaction,
options?: {
blockNumber?: BlockNumberOrTag | undefined
returnFormat?: ReturnFormat
}
) {
const [gasCost, gasPrice] = await Promise.all([
estimateGas(
this,
transaction,
options?.blockNumber ?? BlockTags.LATEST,
DEFAULT_RETURN_FORMAT
),
this.getGasPrice(),
])
return Web3.utils.format(
{ format: 'uint' },
gasCost * gasPrice,
options?.returnFormat ?? DEFAULT_RETURN_FORMAT
)
}
/**
* Computes the total (L1 + L2) fee estimate to execute {transaction}
* @param transaction - An unsigned web3.js {Transaction} object
* @param {DataFormat} [returnFormat=DEFAULT_RETURN_FORMAT] - The web3.js format object that specifies how to format number and bytes values
* @returns {Promise<Numbers>} - The estimated total fee as a BigInt by default, but {returnFormat} determines type
* @example
* const estimatedFees: bigint = await estimateFees(transaction);
* @example
* const numberFormat = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }
* const estimatedFees: number = await estimateFees(transaction, numberFormat);
*/
public async estimateFees<
ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT
>(transaction: Transaction, returnFormat?: ReturnFormat) {
const [l1Fee, l2Fee] = await Promise.all([
this.getL1Fee(transaction),
this.getL2Fee(transaction),
])
return Web3.utils.format(
{ format: 'uint' },
l1Fee + l2Fee,
returnFormat ?? DEFAULT_RETURN_FORMAT
)
}
/**
* Used to get the web3.js contract instance for gas price oracle contract
* @returns {Contract<typeof gasPriceOracleABI>} - A web.js contract instance with an RPC provider inherited from root {web3} instance
*/
private _getPriceOracleContractInstance() {
if (this._gasPriceOracleContract === undefined) {
this._gasPriceOracleContract = new Contract(
gasPriceOracleABI,
gasPriceOracleAddress[420]
)
// This plugin's Web3Context is overridden with main Web3 instance's context
// when the plugin is registered. This overwrites the Contract instance's context
this._gasPriceOracleContract.link(this)
}
return this._gasPriceOracleContract
}
/**
* Returns the RLP encoded hex string for {transaction}
* @param transaction - A web3.js {Transaction} object
* @returns {string} - The RLP encoded hex string
*/
private _serializeTransaction(transaction: Transaction) {
const ethereumjsTransaction = TransactionFactory.fromTxData(
formatTransaction(transaction, {
number: FMT_NUMBER.HEX,
bytes: FMT_BYTES.HEX,
}) as TxData
)
return Web3.utils.bytesToHex(
Web3.utils.uint8ArrayConcat(
Web3.utils.hexToBytes(
ethereumjsTransaction.type.toString(16).padStart(2, '0')
),
// If <transaction> doesn't include a signature,
// <ethereumjsTransaction.raw()> will autofill v, r, and s
// with empty uint8Array. Because L1 fee calculation
// is dependent on the number of bytes, we are removing
// the zero values bytes
RLP.encode(ethereumjsTransaction.raw().slice(0, -3))
)
)
}
}
// Module Augmentation to add op namespace to root {web3} instance
declare module 'web3' {
interface Web3Context {
op: OptimismPlugin
}
}
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"baseUrl": "./src",
"noEmit": true,
"target": "ESNext",
"lib": ["esnext"],
"module": "esnext",
"moduleResolution": "Node",
"isolatedModules": true,
"allowUnreachableCode": false,
"skipLibCheck": false,
"allowUnusedLabels": false,
"alwaysStrict": true,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitOverride": true,
"noImplicitThis": true,
"forceConsistentCasingInFileNames": true,
"verbatimModuleSyntax": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"strict": true
},
"include": ["./src"]
}
import { defineConfig } from 'tsup'
import packageJson from './package.json'
// @see https://tsup.egoist.dev/
export default defineConfig({
name: packageJson.name,
entry: ['src/plugin.ts'],
outDir: 'dist',
format: ['esm', 'cjs'],
splitting: false,
sourcemap: true,
clean: false,
dts: true,
})
import { defineConfig } from 'vitest/config'
// @see https://vitest.dev/config/
export default defineConfig({
test: {
environment: 'jsdom',
coverage: {
provider: 'istanbul',
},
},
})
This source diff could not be displayed because it is too large. You can view the blob instead.
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