Commit 6c87ec92 authored by Will Cory's avatar Will Cory Committed by Mark Tyneway

sdk: bugfixes

parent a00e9eb4
...@@ -5,7 +5,7 @@ orbs: ...@@ -5,7 +5,7 @@ orbs:
gcp-cli: circleci/gcp-cli@3.0.1 gcp-cli: circleci/gcp-cli@3.0.1
commands: commands:
gcp-oidc-authenticate: gcp-oidc-authenticate:
description: "Authenticate with GCP using a CircleCI OIDC token." description: 'Authenticate with GCP using a CircleCI OIDC token.'
parameters: parameters:
project_id: project_id:
type: env_var_name type: env_var_name
...@@ -27,7 +27,7 @@ commands: ...@@ -27,7 +27,7 @@ commands:
default: /home/circleci/oidc_token.json default: /home/circleci/oidc_token.json
steps: steps:
- run: - run:
name: "Create OIDC credential configuration" name: 'Create OIDC credential configuration'
command: | command: |
# Store OIDC token in temp file # Store OIDC token in temp file
echo $CIRCLE_OIDC_TOKEN > << parameters.oidc_token_file_path >> echo $CIRCLE_OIDC_TOKEN > << parameters.oidc_token_file_path >>
...@@ -38,21 +38,21 @@ commands: ...@@ -38,21 +38,21 @@ commands:
--service-account="${<< parameters.service_account_email >>}" \ --service-account="${<< parameters.service_account_email >>}" \
--credential-source-file=<< parameters.oidc_token_file_path >> --credential-source-file=<< parameters.oidc_token_file_path >>
- run: - run:
name: "Authenticate with GCP using OIDC" name: 'Authenticate with GCP using OIDC'
command: | command: |
# Configure gcloud to leverage the generated credential configuration # Configure gcloud to leverage the generated credential configuration
gcloud auth login --brief --cred-file "<< parameters.gcp_cred_config_file_path >>" gcloud auth login --brief --cred-file "<< parameters.gcp_cred_config_file_path >>"
# Configure ADC # Configure ADC
echo "export GOOGLE_APPLICATION_CREDENTIALS='<< parameters.gcp_cred_config_file_path >>'" | tee -a "$BASH_ENV" echo "export GOOGLE_APPLICATION_CREDENTIALS='<< parameters.gcp_cred_config_file_path >>'" | tee -a "$BASH_ENV"
check-changed: check-changed:
description: "Conditionally halts a step if certain modules change" description: 'Conditionally halts a step if certain modules change'
parameters: parameters:
patterns: patterns:
type: string type: string
description: "Comma-separated list of dependencies" description: 'Comma-separated list of dependencies'
steps: steps:
- run: - run:
name: "Check for changes" name: 'Check for changes'
command: | command: |
cd ops/check-changed cd ops/check-changed
pip3 install -r requirements.txt pip3 install -r requirements.txt
...@@ -77,26 +77,26 @@ jobs: ...@@ -77,26 +77,26 @@ jobs:
name: Save Yarn Package Cache name: Save Yarn Package Cache
key: yarn-packages-v2-{{ checksum "yarn.lock" }} key: yarn-packages-v2-{{ checksum "yarn.lock" }}
paths: paths:
- "node_modules" - 'node_modules'
- "packages/actor-tests/node_modules" - 'packages/actor-tests/node_modules'
- "packages/atst/node_modules" - 'packages/atst/node_modules'
- "packages/balance-monitor/node_modules" - 'packages/balance-monitor/node_modules'
- "packages/chain-mon/node_modules" - 'packages/chain-mon/node_modules'
- "packages/common-ts/node_modules" - 'packages/common-ts/node_modules'
- "packages/contracts/node_modules" - 'packages/contracts/node_modules'
- "packages/contracts-bedrock/node_modules" - 'packages/contracts-bedrock/node_modules'
- "packages/contracts-governance/node_modules" - 'packages/contracts-governance/node_modules'
- "packages/contracts-periphery/node_modules" - 'packages/contracts-periphery/node_modules'
- "packages/core-utils/node_modules" - 'packages/core-utils/node_modules'
- "packages/data-transport-layer/node_modules" - 'packages/data-transport-layer/node_modules'
- "packages/drippie-mon/node_modules" - 'packages/drippie-mon/node_modules'
- "packages/fault-detector/node_modules" - 'packages/fault-detector/node_modules'
- "packages/hardhat-deploy-config/node_modules" - 'packages/hardhat-deploy-config/node_modules'
- "packages/integration-tests-bedrock/node_modules" - 'packages/integration-tests-bedrock/node_modules'
- "packages/message-relayer/node_modules" - 'packages/message-relayer/node_modules'
- "packages/migration-data/node_modules" - 'packages/migration-data/node_modules'
- "packages/replica-healthcheck/node_modules" - 'packages/replica-healthcheck/node_modules'
- "packages/sdk/node_modules" - 'packages/sdk/node_modules'
- run: - run:
name: print forge version name: print forge version
command: forge --version command: forge --version
...@@ -104,17 +104,17 @@ jobs: ...@@ -104,17 +104,17 @@ jobs:
name: Build monorepo name: Build monorepo
command: yarn build command: yarn build
- persist_to_workspace: - persist_to_workspace:
root: "." root: '.'
paths: paths:
- "packages/*/dist" - 'packages/*/dist'
- "packages/*/artifacts" - 'packages/*/artifacts'
- "packages/contracts/src/contract-artifacts.ts" - 'packages/contracts/src/contract-artifacts.ts'
- "packages/contracts/src/contract-deployed-artifacts.ts" - 'packages/contracts/src/contract-deployed-artifacts.ts'
- "packages/contracts/chugsplash" - 'packages/contracts/chugsplash'
- "packages/contracts/L1" - 'packages/contracts/L1'
- "packages/contracts/L2" - 'packages/contracts/L2'
- "packages/contracts/libraries" - 'packages/contracts/libraries'
- "packages/contracts/standards" - 'packages/contracts/standards'
docker-build: docker-build:
environment: environment:
...@@ -135,11 +135,11 @@ jobs: ...@@ -135,11 +135,11 @@ jobs:
registry: registry:
description: Docker registry description: Docker registry
type: string type: string
default: "us-docker.pkg.dev" default: 'us-docker.pkg.dev'
repo: repo:
description: Docker repo description: Docker repo
type: string type: string
default: "oplabs-tools-artifacts/images" default: 'oplabs-tools-artifacts/images'
machine: machine:
image: ubuntu-2204:2022.07.1 image: ubuntu-2204:2022.07.1
resource_class: medium resource_class: medium
...@@ -170,7 +170,7 @@ jobs: ...@@ -170,7 +170,7 @@ jobs:
- persist_to_workspace: - persist_to_workspace:
root: /tmp/docker_images root: /tmp/docker_images
paths: paths:
- "." - '.'
docker-publish: docker-publish:
environment: environment:
...@@ -188,23 +188,23 @@ jobs: ...@@ -188,23 +188,23 @@ jobs:
docker_context: docker_context:
description: Docker build context description: Docker build context
type: string type: string
default: "." default: '.'
docker_target: docker_target:
description: "target build stage" description: 'target build stage'
type: string type: string
default: "" default: ''
registry: registry:
description: Docker registry description: Docker registry
type: string type: string
default: "us-docker.pkg.dev" default: 'us-docker.pkg.dev'
repo: repo:
description: Docker repo description: Docker repo
type: string type: string
default: "oplabs-tools-artifacts/images" default: 'oplabs-tools-artifacts/images'
platforms: platforms:
description: Platforms to build for description: Platforms to build for
type: string type: string
default: "linux/amd64" default: 'linux/amd64'
machine: machine:
image: ubuntu-2204:2022.07.1 image: ubuntu-2204:2022.07.1
resource_class: medium resource_class: medium
...@@ -250,15 +250,15 @@ jobs: ...@@ -250,15 +250,15 @@ jobs:
registry: registry:
description: Docker registry description: Docker registry
type: string type: string
default: "us-docker.pkg.dev" default: 'us-docker.pkg.dev'
repo: repo:
description: Docker repo description: Docker repo
type: string type: string
default: "oplabs-tools-artifacts/images" default: 'oplabs-tools-artifacts/images'
platforms: platforms:
description: Platforms to build for description: Platforms to build for
type: string type: string
default: "linux/amd64" default: 'linux/amd64'
machine: machine:
image: ubuntu-2204:2022.07.1 image: ubuntu-2204:2022.07.1
resource_class: medium resource_class: medium
...@@ -321,7 +321,7 @@ jobs: ...@@ -321,7 +321,7 @@ jobs:
resource_class: large resource_class: large
steps: steps:
- checkout - checkout
- attach_workspace: { at: "." } - attach_workspace: { at: '.' }
- restore_cache: - restore_cache:
name: Restore Yarn Package Cache name: Restore Yarn Package Cache
keys: keys:
...@@ -344,7 +344,7 @@ jobs: ...@@ -344,7 +344,7 @@ jobs:
- image: us-docker.pkg.dev/oplabs-tools-artifacts/images/ci-builder:latest - image: us-docker.pkg.dev/oplabs-tools-artifacts/images/ci-builder:latest
steps: steps:
- checkout - checkout
- attach_workspace: { at: "." } - attach_workspace: { at: '.' }
- restore_cache: - restore_cache:
name: Restore Yarn Package Cache name: Restore Yarn Package Cache
keys: keys:
...@@ -405,7 +405,7 @@ jobs: ...@@ -405,7 +405,7 @@ jobs:
resource_class: large resource_class: large
steps: steps:
- checkout - checkout
- attach_workspace: { at: "." } - attach_workspace: { at: '.' }
- restore_cache: - restore_cache:
name: Restore Yarn Package Cache name: Restore Yarn Package Cache
keys: keys:
...@@ -424,7 +424,7 @@ jobs: ...@@ -424,7 +424,7 @@ jobs:
- image: us-docker.pkg.dev/oplabs-tools-artifacts/images/ci-builder:latest - image: us-docker.pkg.dev/oplabs-tools-artifacts/images/ci-builder:latest
steps: steps:
- checkout - checkout
- attach_workspace: { at: "." } - attach_workspace: { at: '.' }
- restore_cache: - restore_cache:
name: Restore Yarn Package Cache name: Restore Yarn Package Cache
keys: keys:
...@@ -441,7 +441,7 @@ jobs: ...@@ -441,7 +441,7 @@ jobs:
- image: us-docker.pkg.dev/oplabs-tools-artifacts/images/ci-builder:latest - image: us-docker.pkg.dev/oplabs-tools-artifacts/images/ci-builder:latest
steps: steps:
- checkout - checkout
- attach_workspace: { at: "." } - attach_workspace: { at: '.' }
- check-changed: - check-changed:
patterns: contracts-bedrock,contracts patterns: contracts-bedrock,contracts
- run: - run:
...@@ -451,7 +451,7 @@ jobs: ...@@ -451,7 +451,7 @@ jobs:
- persist_to_workspace: - persist_to_workspace:
root: . root: .
paths: paths:
- "node_modules" - 'node_modules'
- packages/contracts-bedrock - packages/contracts-bedrock
bedrock-echidna-run: bedrock-echidna-run:
...@@ -468,7 +468,7 @@ jobs: ...@@ -468,7 +468,7 @@ jobs:
resource_class: <<parameters.size>> resource_class: <<parameters.size>>
steps: steps:
- checkout - checkout
- attach_workspace: { at: "." } - attach_workspace: { at: '.' }
- restore_cache: - restore_cache:
name: Restore Yarn Package Cache name: Restore Yarn Package Cache
keys: keys:
...@@ -487,7 +487,7 @@ jobs: ...@@ -487,7 +487,7 @@ jobs:
resource_class: medium resource_class: medium
steps: steps:
- checkout - checkout
- attach_workspace: { at: "." } - attach_workspace: { at: '.' }
- restore_cache: - restore_cache:
name: Restore Yarn Package Cache name: Restore Yarn Package Cache
keys: keys:
...@@ -516,7 +516,7 @@ jobs: ...@@ -516,7 +516,7 @@ jobs:
resource_class: large resource_class: large
steps: steps:
- checkout - checkout
- attach_workspace: { at: "." } - attach_workspace: { at: '.' }
- restore_cache: - restore_cache:
name: Restore Yarn Package Cache name: Restore Yarn Package Cache
keys: keys:
...@@ -544,6 +544,10 @@ jobs: ...@@ -544,6 +544,10 @@ jobs:
- attach_workspace: { at: '.' } - attach_workspace: { at: '.' }
- check-changed: - check-changed:
patterns: sdk,contracts-bedrock,contracts patterns: sdk,contracts-bedrock,contracts
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-v2-{{ checksum "yarn.lock" }}
- run: - run:
name: anvil-l1 name: anvil-l1
background: true background: true
...@@ -552,6 +556,14 @@ jobs: ...@@ -552,6 +556,14 @@ jobs:
name: anvil-l2 name: anvil-l2
background: true background: true
command: anvil --fork-url $ANVIL_L2_FORK_URL --port 9545 command: anvil --fork-url $ANVIL_L2_FORK_URL --port 9545
- run:
name: build
command: yarn build
working_directory: packages/atst
- run:
name: lint
command: yarn lint:check
working_directory: packages/atst
- run: - run:
name: make sure anvil l1 is up name: make sure anvil l1 is up
command: cast block-number --rpc-url http://localhost:8545 command: cast block-number --rpc-url http://localhost:8545
...@@ -565,9 +577,9 @@ jobs: ...@@ -565,9 +577,9 @@ jobs:
working_directory: packages/sdk working_directory: packages/sdk
environment: environment:
# anvil[0] test private key # anvil[0] test private key
VITE_E2E_PRIVATE_KEY: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 VITE_E2E_PRIVATE_KEY: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
VITE_E2E_RPC_URLS_L1: http://localhost:8545 VITE_E2E_RPC_URL_L1: http://localhost:8545
VITE_E2E_RPC_URLS_L2: http://localhost:9545 VITE_E2E_RPC_URL_L2: http://localhost:9545
bedrock-markdown: bedrock-markdown:
machine: machine:
...@@ -577,7 +589,7 @@ jobs: ...@@ -577,7 +589,7 @@ jobs:
- check-changed: - check-changed:
patterns: specs/(.*)\.md$ patterns: specs/(.*)\.md$
- run: - run:
name: yarn dev deps # todo: what's the best way to pull in the dependencies for linting? yarn install above is using production env without dev dependencies name: yarn dev deps # todo: what's the best way to pull in the dependencies for linting? yarn install above is using production env without dev dependencies
command: yarn install --production=false command: yarn install --production=false
- run: - run:
name: specs toc name: specs toc
...@@ -607,7 +619,7 @@ jobs: ...@@ -607,7 +619,7 @@ jobs:
- image: us-docker.pkg.dev/oplabs-tools-artifacts/images/ci-builder:latest - image: us-docker.pkg.dev/oplabs-tools-artifacts/images/ci-builder:latest
steps: steps:
- checkout - checkout
- attach_workspace: { at: "." } - attach_workspace: { at: '.' }
- restore_cache: - restore_cache:
name: Restore Yarn Package Cache name: Restore Yarn Package Cache
keys: keys:
...@@ -753,7 +765,7 @@ jobs: ...@@ -753,7 +765,7 @@ jobs:
working_directory: <<parameters.working_directory>> working_directory: <<parameters.working_directory>>
- when: - when:
condition: condition:
equal: [ true, <<parameters.build>> ] equal: [true, <<parameters.build>>]
steps: steps:
- run: - run:
name: Build name: Build
...@@ -809,7 +821,7 @@ jobs: ...@@ -809,7 +821,7 @@ jobs:
- when: - when:
condition: condition:
and: and:
- equal: [ true, <<parameters.deploy>> ] - equal: [true, <<parameters.deploy>>]
steps: steps:
- run: - run:
name: Bring up the stack name: Bring up the stack
...@@ -859,7 +871,7 @@ jobs: ...@@ -859,7 +871,7 @@ jobs:
- when: - when:
condition: condition:
and: and:
- equal: [ false, <<parameters.deploy>> ] - equal: [false, <<parameters.deploy>>]
steps: steps:
- run: - run:
name: Bring up the stack name: Bring up the stack
...@@ -962,22 +974,22 @@ jobs: ...@@ -962,22 +974,22 @@ jobs:
- checkout - checkout
- unless: - unless:
condition: condition:
equal: [ "develop", << pipeline.git.branch >> ] equal: ['develop', << pipeline.git.branch >>]
steps: steps:
- run: - run:
# Scan changed files in PRs, block on new issues only (existing issues ignored) # Scan changed files in PRs, block on new issues only (existing issues ignored)
# Do a full scan when scanning develop, otherwise do an incremental scan. # Do a full scan when scanning develop, otherwise do an incremental scan.
name: "Conditionally set BASELINE env var" name: 'Conditionally set BASELINE env var'
command: | command: |
echo 'export SEMGREP_BASELINE_REF=${TEMPORARY_BASELINE_REF}' >> $BASH_ENV echo 'export SEMGREP_BASELINE_REF=${TEMPORARY_BASELINE_REF}' >> $BASH_ENV
- run: - run:
name: "Set environment variables" # for PR comments and in-app hyperlinks to findings name: 'Set environment variables' # for PR comments and in-app hyperlinks to findings
command: | command: |
echo 'export SEMGREP_PR_ID=${CIRCLE_PULL_REQUEST##*/}' >> $BASH_ENV echo 'export SEMGREP_PR_ID=${CIRCLE_PULL_REQUEST##*/}' >> $BASH_ENV
echo 'export SEMGREP_JOB_URL=$CIRCLE_BUILD_URL' >> $BASH_ENV echo 'export SEMGREP_JOB_URL=$CIRCLE_BUILD_URL' >> $BASH_ENV
echo 'export SEMGREP_REPO_NAME=$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME' >> $BASH_ENV echo 'export SEMGREP_REPO_NAME=$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME' >> $BASH_ENV
- run: - run:
name: "Semgrep scan" name: 'Semgrep scan'
command: semgrep ci command: semgrep ci
go-mod-tidy: go-mod-tidy:
...@@ -986,7 +998,7 @@ jobs: ...@@ -986,7 +998,7 @@ jobs:
steps: steps:
- checkout - checkout
- run: - run:
name: "Go mod tidy" name: 'Go mod tidy'
command: make mod-tidy && git diff --exit-code command: make mod-tidy && git diff --exit-code
hive-test: hive-test:
...@@ -1014,7 +1026,7 @@ jobs: ...@@ -1014,7 +1026,7 @@ jobs:
- go/load-cache - go/load-cache
- go/mod-download - go/mod-download
- go/save-cache - go/save-cache
- run: { command: "go build ." } - run: { command: 'go build .' }
- run: - run:
command: | command: |
./hive \ ./hive \
...@@ -1024,7 +1036,7 @@ jobs: ...@@ -1024,7 +1036,7 @@ jobs:
- run: - run:
command: | command: |
tar -cvf /tmp/workspace.tgz -C /home/circleci/project /home/circleci/project/workspace tar -cvf /tmp/workspace.tgz -C /home/circleci/project /home/circleci/project/workspace
name: "Archive workspace" name: 'Archive workspace'
- store_artifacts: - store_artifacts:
path: /tmp/workspace.tgz path: /tmp/workspace.tgz
destination: hive-workspace.tgz destination: hive-workspace.tgz
...@@ -1083,49 +1095,49 @@ workflows: ...@@ -1083,49 +1095,49 @@ workflows:
name: actor-tests-tests name: actor-tests-tests
coverage_flag: actor-tests-tests coverage_flag: actor-tests-tests
package_name: actor-tests package_name: actor-tests
dependencies: "(core-utils|sdk)" dependencies: '(core-utils|sdk)'
requires: requires:
- yarn-monorepo - yarn-monorepo
- js-lint-test: - js-lint-test:
name: contracts-periphery-tests name: contracts-periphery-tests
coverage_flag: contracts-periphery-tests coverage_flag: contracts-periphery-tests
package_name: contracts-periphery package_name: contracts-periphery
dependencies: "(contracts|contracts-bedrock|core-utils|hardhat-deploy-config)" dependencies: '(contracts|contracts-bedrock|core-utils|hardhat-deploy-config)'
requires: requires:
- yarn-monorepo - yarn-monorepo
- js-lint-test: - js-lint-test:
name: dtl-tests name: dtl-tests
coverage_flag: dtl-tests coverage_flag: dtl-tests
package_name: data-transport-layer package_name: data-transport-layer
dependencies: "(common-ts|contracts|core-utils)" dependencies: '(common-ts|contracts|core-utils)'
requires: requires:
- yarn-monorepo - yarn-monorepo
- js-lint-test: - js-lint-test:
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-periphery|core-utils|sdk)" dependencies: '(common-ts|contracts-periphery|core-utils|sdk)'
requires: requires:
- yarn-monorepo - yarn-monorepo
- js-lint-test: - js-lint-test:
name: fault-detector-tests name: fault-detector-tests
coverage_flag: fault-detector-tests coverage_flag: fault-detector-tests
package_name: fault-detector package_name: fault-detector
dependencies: "(common-ts|contracts|core-utils|sdk)" dependencies: '(common-ts|contracts|core-utils|sdk)'
requires: requires:
- yarn-monorepo - yarn-monorepo
- js-lint-test: - js-lint-test:
name: message-relayer-tests name: message-relayer-tests
coverage_flag: message-relayer-tests coverage_flag: message-relayer-tests
package_name: message-relayer package_name: message-relayer
dependencies: "(common-ts|core-utils|sdk)" dependencies: '(common-ts|core-utils|sdk)'
requires: requires:
- yarn-monorepo - yarn-monorepo
- js-lint-test: - js-lint-test:
name: replica-healthcheck-tests name: replica-healthcheck-tests
coverage_flag: replica-healthcheck-tests coverage_flag: replica-healthcheck-tests
package_name: replica-healthcheck package_name: replica-healthcheck
dependencies: "(common-ts|core-utils)" dependencies: '(common-ts|core-utils)'
requires: requires:
- yarn-monorepo - yarn-monorepo
- sdk-next-tests: - sdk-next-tests:
...@@ -1136,7 +1148,7 @@ workflows: ...@@ -1136,7 +1148,7 @@ workflows:
name: sdk-tests name: sdk-tests
coverage_flag: sdk-tests coverage_flag: sdk-tests
package_name: sdk package_name: sdk
dependencies: "(contracts|core-utils)" dependencies: '(contracts|core-utils)'
requires: requires:
- yarn-monorepo - yarn-monorepo
- js-lint-test: - js-lint-test:
...@@ -1239,11 +1251,11 @@ workflows: ...@@ -1239,11 +1251,11 @@ workflows:
- go-e2e-test: - go-e2e-test:
name: op-e2e-WS-tests name: op-e2e-WS-tests
module: op-e2e module: op-e2e
use_http: "false" use_http: 'false'
- go-e2e-test: - go-e2e-test:
name: op-e2e-HTTP-tests name: op-e2e-HTTP-tests
module: op-e2e module: op-e2e
use_http: "true" use_http: 'true'
- bedrock-go-tests: - bedrock-go-tests:
requires: requires:
- op-batcher-lint - op-batcher-lint
...@@ -1276,7 +1288,7 @@ workflows: ...@@ -1276,7 +1288,7 @@ workflows:
docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>> docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>>
context: context:
- oplabs-gcr - oplabs-gcr
platforms: "linux/amd64,linux/arm64" platforms: 'linux/amd64,linux/arm64'
- docker-build: - docker-build:
name: op-batcher-docker-build name: op-batcher-docker-build
docker_file: op-batcher/Dockerfile docker_file: op-batcher/Dockerfile
...@@ -1290,7 +1302,7 @@ workflows: ...@@ -1290,7 +1302,7 @@ workflows:
docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>> docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>>
context: context:
- oplabs-gcr - oplabs-gcr
platforms: "linux/amd64,linux/arm64" platforms: 'linux/amd64,linux/arm64'
- docker-build: - docker-build:
name: op-proposer-docker-build name: op-proposer-docker-build
docker_file: op-proposer/Dockerfile docker_file: op-proposer/Dockerfile
...@@ -1304,7 +1316,7 @@ workflows: ...@@ -1304,7 +1316,7 @@ workflows:
docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>> docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>>
context: context:
- oplabs-gcr - oplabs-gcr
platforms: "linux/amd64,linux/arm64" platforms: 'linux/amd64,linux/arm64'
- docker-build: - docker-build:
name: op-heartbeat-docker-build name: op-heartbeat-docker-build
docker_file: op-heartbeat/Dockerfile docker_file: op-heartbeat/Dockerfile
...@@ -1383,7 +1395,7 @@ workflows: ...@@ -1383,7 +1395,7 @@ workflows:
docker_name: op-node docker_name: op-node
docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>> docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>>
docker_context: . docker_context: .
platforms: "linux/amd64,linux/arm64" platforms: 'linux/amd64,linux/arm64'
context: context:
- oplabs-gcr-release - oplabs-gcr-release
requires: requires:
...@@ -1399,7 +1411,7 @@ workflows: ...@@ -1399,7 +1411,7 @@ workflows:
docker_name: op-batcher docker_name: op-batcher
docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>> docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>>
docker_context: . docker_context: .
platforms: "linux/amd64,linux/arm64" platforms: 'linux/amd64,linux/arm64'
context: context:
- oplabs-gcr-release - oplabs-gcr-release
requires: requires:
...@@ -1415,7 +1427,7 @@ workflows: ...@@ -1415,7 +1427,7 @@ workflows:
docker_name: op-proposer docker_name: op-proposer
docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>> docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>>
docker_context: . docker_context: .
platforms: "linux/amd64,linux/arm64" platforms: 'linux/amd64,linux/arm64'
context: context:
- oplabs-gcr-release - oplabs-gcr-release
requires: requires:
......
...@@ -68,6 +68,7 @@ import { ...@@ -68,6 +68,7 @@ import {
migratedWithdrawalGasLimit, migratedWithdrawalGasLimit,
DEPOSIT_CONFIRMATION_BLOCKS, DEPOSIT_CONFIRMATION_BLOCKS,
CHAIN_BLOCK_TIMES, CHAIN_BLOCK_TIMES,
hashMessageHash,
} from './utils' } from './utils'
export class CrossChainMessenger { export class CrossChainMessenger {
...@@ -351,14 +352,12 @@ export class CrossChainMessenger { ...@@ -351,14 +352,12 @@ export class CrossChainMessenger {
} }
} }
const minGasLimit = migratedWithdrawalGasLimit(resolved.message)
return { return {
...resolved, ...resolved,
value, value,
minGasLimit, minGasLimit: BigNumber.from(0),
messageNonce: encodeVersionedNonce( messageNonce: encodeVersionedNonce(
BigNumber.from(1), BigNumber.from(0),
resolved.messageNonce resolved.messageNonce
), ),
} }
...@@ -388,13 +387,23 @@ export class CrossChainMessenger { ...@@ -388,13 +387,23 @@ export class CrossChainMessenger {
updated = resolved updated = resolved
} }
// Encode the updated message, we need this for legacy messages.
const encoded = encodeCrossDomainMessageV1(
updated.messageNonce,
updated.sender,
updated.target,
updated.value,
updated.minGasLimit,
updated.message
)
// We need to figure out the final withdrawal data that was used to compute the withdrawal hash // We need to figure out the final withdrawal data that was used to compute the withdrawal hash
// inside the L2ToL1Message passer contract. Exact mechanism here depends on whether or not // inside the L2ToL1Message passer contract. Exact mechanism here depends on whether or not
// this is a legacy message or a new Bedrock message. // this is a legacy message or a new Bedrock message.
let gasLimit: BigNumber let gasLimit: BigNumber
let messageNonce: BigNumber let messageNonce: BigNumber
if (version.eq(0)) { if (version.eq(0)) {
gasLimit = BigNumber.from(0) gasLimit = migratedWithdrawalGasLimit(encoded)
messageNonce = resolved.messageNonce messageNonce = resolved.messageNonce
} else { } else {
const receipt = await this.l2Provider.getTransactionReceipt( const receipt = await this.l2Provider.getTransactionReceipt(
...@@ -433,14 +442,7 @@ export class CrossChainMessenger { ...@@ -433,14 +442,7 @@ export class CrossChainMessenger {
target: this.contracts.l1.L1CrossDomainMessenger.address, target: this.contracts.l1.L1CrossDomainMessenger.address,
value: updated.value, value: updated.value,
minGasLimit: gasLimit, minGasLimit: gasLimit,
message: encodeCrossDomainMessageV1( message: encoded,
updated.messageNonce,
updated.sender,
updated.target,
updated.value,
updated.minGasLimit,
updated.message
),
} }
} }
...@@ -1360,12 +1362,8 @@ export class CrossChainMessenger { ...@@ -1360,12 +1362,8 @@ export class CrossChainMessenger {
} }
const withdrawal = await this.toLowLevelMessage(resolved) const withdrawal = await this.toLowLevelMessage(resolved)
const messageSlot = ethers.utils.keccak256( const hash = hashLowLevelMessage(withdrawal)
ethers.utils.defaultAbiCoder.encode( const messageSlot = hashMessageHash(hash)
['bytes32', 'uint256'],
[hashLowLevelMessage(withdrawal), ethers.constants.HashZero]
)
)
const stateTrieProof = await makeStateTrieProof( const stateTrieProof = await makeStateTrieProof(
this.l2Provider as ethers.providers.JsonRpcProvider, this.l2Provider as ethers.providers.JsonRpcProvider,
...@@ -1465,9 +1463,8 @@ export class CrossChainMessenger { ...@@ -1465,9 +1463,8 @@ export class CrossChainMessenger {
overrides?: Overrides overrides?: Overrides
} }
): Promise<TransactionResponse> { ): Promise<TransactionResponse> {
return (opts?.signer || this.l1Signer).sendTransaction( const tx = await this.populateTransaction.proveMessage(message, opts)
await this.populateTransaction.proveMessage(message, opts) return (opts?.signer || this.l1Signer).sendTransaction(tx)
)
} }
/** /**
...@@ -1771,7 +1768,8 @@ export class CrossChainMessenger { ...@@ -1771,7 +1768,8 @@ export class CrossChainMessenger {
const withdrawal = await this.toLowLevelMessage(resolved) const withdrawal = await this.toLowLevelMessage(resolved)
const proof = await this.getBedrockMessageProof(resolved) const proof = await this.getBedrockMessageProof(resolved)
return this.contracts.l1.OptimismPortal.populateTransaction.proveWithdrawalTransaction(
const args = [
[ [
withdrawal.messageNonce, withdrawal.messageNonce,
withdrawal.sender, withdrawal.sender,
...@@ -1788,7 +1786,11 @@ export class CrossChainMessenger { ...@@ -1788,7 +1786,11 @@ export class CrossChainMessenger {
proof.outputRootProof.latestBlockhash, proof.outputRootProof.latestBlockhash,
], ],
proof.withdrawalProof, proof.withdrawalProof,
opts?.overrides || {} opts?.overrides || {},
] as const
return this.contracts.l1.OptimismPortal.populateTransaction.proveWithdrawalTransaction(
...args
) )
}, },
......
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
Provider,
BlockTag,
TransactionReceipt,
TransactionResponse,
TransactionRequest,
} from '@ethersproject/abstract-provider'
import { Signer } from '@ethersproject/abstract-signer'
import {
ethers,
BigNumber,
Overrides,
CallOverrides,
PayableOverrides,
} from 'ethers'
import {
sleep,
remove0x,
toHexString,
toRpcHexString,
hashCrossDomainMessage,
encodeCrossDomainMessageV0,
encodeCrossDomainMessageV1,
BedrockOutputData,
BedrockCrossChainMessageProof,
decodeVersionedNonce,
encodeVersionedNonce,
} from '@eth-optimism/core-utils'
import { getContractInterface, predeploys } from '@eth-optimism/contracts'
import * as rlp from 'rlp'
import {
OEContracts,
OEContractsLike,
MessageLike,
MessageRequestLike,
TransactionLike,
AddressLike,
NumberLike,
SignerOrProviderLike,
CrossChainMessage,
CrossChainMessageRequest,
CrossChainMessageProof,
MessageDirection,
MessageStatus,
TokenBridgeMessage,
MessageReceipt,
MessageReceiptStatus,
BridgeAdapterData,
BridgeAdapters,
StateRoot,
StateRootBatch,
IBridgeAdapter,
ProvenWithdrawal,
LowLevelMessage,
} from './interfaces'
import {
toSignerOrProvider,
toNumber,
toTransactionHash,
DeepPartial,
getAllOEContracts,
getBridgeAdapters,
makeMerkleTreeProof,
makeStateTrieProof,
hashLowLevelMessage,
migratedWithdrawalGasLimit,
DEPOSIT_CONFIRMATION_BLOCKS,
CHAIN_BLOCK_TIMES,
} from './utils'
export class CrossChainMessenger {
/**
* Provider connected to the L1 chain.
*/
public l1SignerOrProvider: Signer | Provider
/**
* Provider connected to the L2 chain.
*/
public l2SignerOrProvider: Signer | Provider
/**
* Chain ID for the L1 network.
*/
public l1ChainId: number
/**
* Chain ID for the L2 network.
*/
public l2ChainId: number
/**
* Contract objects attached to their respective providers and addresses.
*/
public contracts: OEContracts
/**
* List of custom bridges for the given network.
*/
public bridges: BridgeAdapters
/**
* Number of blocks before a deposit is considered confirmed.
*/
public depositConfirmationBlocks: number
/**
* Estimated average L1 block time in seconds.
*/
public l1BlockTimeSeconds: number
/**
* Whether or not Bedrock compatibility is enabled.
*/
public bedrock: boolean
/**
* Creates a new CrossChainProvider instance.
*
* @param opts Options for the provider.
* @param opts.l1SignerOrProvider Signer or Provider for the L1 chain, or a JSON-RPC url.
* @param opts.l2SignerOrProvider Signer or Provider for the L2 chain, or a JSON-RPC url.
* @param opts.l1ChainId Chain ID for the L1 chain.
* @param opts.l2ChainId Chain ID for the L2 chain.
* @param opts.depositConfirmationBlocks Optional number of blocks before a deposit is confirmed.
* @param opts.l1BlockTimeSeconds Optional estimated block time in seconds for the L1 chain.
* @param opts.contracts Optional contract address overrides.
* @param opts.bridges Optional bridge address list.
* @param opts.bedrock Whether or not to enable Bedrock compatibility.
*/
constructor(opts: {
l1SignerOrProvider: SignerOrProviderLike
l2SignerOrProvider: SignerOrProviderLike
l1ChainId: NumberLike
l2ChainId: NumberLike
depositConfirmationBlocks?: NumberLike
l1BlockTimeSeconds?: NumberLike
contracts?: DeepPartial<OEContractsLike>
bridges?: BridgeAdapterData
bedrock?: boolean
}) {
this.bedrock = opts.bedrock ?? false
this.l1SignerOrProvider = toSignerOrProvider(opts.l1SignerOrProvider)
this.l2SignerOrProvider = toSignerOrProvider(opts.l2SignerOrProvider)
try {
this.l1ChainId = toNumber(opts.l1ChainId)
} catch (err) {
throw new Error(`L1 chain ID is missing or invalid: ${opts.l1ChainId}`)
}
try {
this.l2ChainId = toNumber(opts.l2ChainId)
} catch (err) {
throw new Error(`L2 chain ID is missing or invalid: ${opts.l2ChainId}`)
}
this.depositConfirmationBlocks =
opts?.depositConfirmationBlocks !== undefined
? toNumber(opts.depositConfirmationBlocks)
: DEPOSIT_CONFIRMATION_BLOCKS[this.l2ChainId] || 0
this.l1BlockTimeSeconds =
opts?.l1BlockTimeSeconds !== undefined
? toNumber(opts.l1BlockTimeSeconds)
: CHAIN_BLOCK_TIMES[this.l1ChainId] || 1
this.contracts = getAllOEContracts(this.l2ChainId, {
l1SignerOrProvider: this.l1SignerOrProvider,
l2SignerOrProvider: this.l2SignerOrProvider,
overrides: opts.contracts,
})
this.bridges = getBridgeAdapters(this.l2ChainId, this, {
overrides: opts.bridges,
contracts: opts.contracts,
})
}
/**
* Provider connected to the L1 chain.
*/
get l1Provider(): Provider {
if (Provider.isProvider(this.l1SignerOrProvider)) {
return this.l1SignerOrProvider
} else {
return this.l1SignerOrProvider.provider
}
}
/**
* Provider connected to the L2 chain.
*/
get l2Provider(): Provider {
if (Provider.isProvider(this.l2SignerOrProvider)) {
return this.l2SignerOrProvider
} else {
return this.l2SignerOrProvider.provider
}
}
/**
* Signer connected to the L1 chain.
*/
get l1Signer(): Signer {
if (Provider.isProvider(this.l1SignerOrProvider)) {
throw new Error(`messenger has no L1 signer`)
} else {
return this.l1SignerOrProvider
}
}
/**
* Signer connected to the L2 chain.
*/
get l2Signer(): Signer {
if (Provider.isProvider(this.l2SignerOrProvider)) {
throw new Error(`messenger has no L2 signer`)
} else {
return this.l2SignerOrProvider
}
}
/**
* Retrieves all cross chain messages sent within a given transaction.
*
* @param transaction Transaction hash or receipt to find messages from.
* @param opts Options object.
* @param opts.direction Direction to search for messages in. If not provided, will attempt to
* automatically search both directions under the assumption that a transaction hash will only
* exist on one chain. If the hash exists on both chains, will throw an error.
* @returns All cross chain messages sent within the transaction.
*/
public async getMessagesByTransaction(
transaction: TransactionLike,
opts: {
direction?: MessageDirection
} = {}
): Promise<CrossChainMessage[]> {
// Wait for the transaction receipt if the input is waitable.
await (transaction as TransactionResponse).wait?.()
// Convert the input to a transaction hash.
const txHash = toTransactionHash(transaction)
let receipt: TransactionReceipt
if (opts.direction !== undefined) {
// Get the receipt for the requested direction.
if (opts.direction === MessageDirection.L1_TO_L2) {
receipt = await this.l1Provider.getTransactionReceipt(txHash)
} else {
receipt = await this.l2Provider.getTransactionReceipt(txHash)
}
} else {
// Try both directions, starting with L1 => L2.
receipt = await this.l1Provider.getTransactionReceipt(txHash)
if (receipt) {
opts.direction = MessageDirection.L1_TO_L2
} else {
receipt = await this.l2Provider.getTransactionReceipt(txHash)
opts.direction = MessageDirection.L2_TO_L1
}
}
if (!receipt) {
throw new Error(`unable to find transaction receipt for ${txHash}`)
}
// By this point opts.direction will always be defined.
const messenger =
opts.direction === MessageDirection.L1_TO_L2
? this.contracts.l1.L1CrossDomainMessenger
: this.contracts.l2.L2CrossDomainMessenger
return receipt.logs
.filter((log) => {
// Only look at logs emitted by the messenger address
return log.address === messenger.address
})
.filter((log) => {
// Only look at SentMessage logs specifically
const parsed = messenger.interface.parseLog(log)
return parsed.name === 'SentMessage'
})
.map((log) => {
// Try to pull out the value field, but only if the very next log is a SentMessageExtension1
// event which was introduced in the Bedrock upgrade.
let value = ethers.BigNumber.from(0)
const next = receipt.logs.find((l) => {
return (
l.logIndex === log.logIndex + 1 && l.address === messenger.address
)
})
if (next) {
const nextParsed = messenger.interface.parseLog(next)
if (nextParsed.name === 'SentMessageExtension1') {
value = nextParsed.args.value
}
}
// Convert each SentMessage log into a message object
const parsed = messenger.interface.parseLog(log)
return {
direction: opts.direction,
target: parsed.args.target,
sender: parsed.args.sender,
message: parsed.args.message,
messageNonce: parsed.args.messageNonce,
value,
minGasLimit: parsed.args.gasLimit,
logIndex: log.logIndex,
blockNumber: log.blockNumber,
transactionHash: log.transactionHash,
}
})
}
/**
* Transforms a legacy message into its corresponding Bedrock representation.
*
* @param message Legacy message to transform.
* @returns Bedrock representation of the message.
*/
public async toBedrockCrossChainMessage(
message: MessageLike
): Promise<CrossChainMessage> {
console.log('converting toBedrockCrossChainMessage', { message })
const resolved = await this.toCrossChainMessage(message)
// Bedrock messages are already in the correct format.
const { version } = decodeVersionedNonce(resolved.messageNonce)
if (version.eq(1)) {
console.log('bedrock message resolving', resolved)
return resolved
}
let value = BigNumber.from(0)
if (
resolved.direction === MessageDirection.L2_TO_L1 &&
resolved.sender === this.contracts.l2.L2StandardBridge.address &&
resolved.target === this.contracts.l1.L1StandardBridge.address
) {
try {
console.log(
'calling contracts.l1.L1StandardBridge.interface.decodeFunctionData',
{
data: resolved.message,
}
)
const decodedData =
this.contracts.l1.L1StandardBridge.interface.decodeFunctionData(
'finalizeETHWithdrawal',
resolved.message
)
;[, , value] = decodedData
} catch (err) {
// No problem, not a message with value.
}
}
console.log('calling migratedWithdrawalGasLimit()', resolved.message)
const minGasLimit = migratedWithdrawalGasLimit(resolved.message)
return {
...resolved,
value,
minGasLimit,
messageNonce: encodeVersionedNonce(
BigNumber.from(1),
resolved.messageNonce
),
}
}
/**
* Transforms a CrossChainMessenger message into its low-level representation inside the
* L2ToL1MessagePasser contract on L2.
*
* @param message Message to transform.
* @return Transformed message.
*/
public async toLowLevelMessage(
message: MessageLike
): Promise<LowLevelMessage> {
// calling toLowLevelMessgage again? why? Likely can delete.
const resolved = await this.toCrossChainMessage(message)
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`can only convert L2 to L1 messages to low level`)
}
// We may have to update the message if it's a legacy message.c
console.log('calling deccodeVersionNonce()', resolved.messageNonce)
const { version } = decodeVersionedNonce(resolved.messageNonce)
console.log('version', version)
let updated: CrossChainMessage
if (version.eq(0)) {
console.log('converting toBedrockCrossChainMessage')
updated = await this.toBedrockCrossChainMessage(resolved)
} else {
console.log('already a bedrock message? (bug if this line gets hit)')
updated = resolved
}
// We need to figure out the final withdrawal data that was used to compute the withdrawal hash
// inside the L2ToL1Message passer contract. Exact mechanism here depends on whether or not
// this is a legacy message or a new Bedrock message.
let gasLimit: BigNumber
let messageNonce: BigNumber
if (version.eq(0)) {
gasLimit = BigNumber.from(0)
messageNonce = resolved.messageNonce
} else {
console.log('in a loop getting withdrawals')
const receipt = await this.l2Provider.getTransactionReceipt(
resolved.transactionHash
)
const withdrawals: any[] = []
for (const log of receipt.logs) {
if (log.address === this.contracts.l2.BedrockMessagePasser.address) {
const decoded =
this.contracts.l2.L2ToL1MessagePasser.interface.parseLog(log)
if (decoded.name === 'MessagePassed') {
withdrawals.push(decoded.args)
}
}
}
console.log('withdrawals', withdrawals)
// Should not happen.
if (withdrawals.length === 0) {
throw new Error(`no withdrawals found in receipt`)
}
// TODO: Add support for multiple withdrawals.
if (withdrawals.length > 1) {
throw new Error(`multiple withdrawals found in receipt`)
}
const withdrawal = withdrawals[0]
messageNonce = withdrawal.nonce
gasLimit = withdrawal.gasLimit
}
return {
messageNonce,
sender: this.contracts.l2.L2CrossDomainMessenger.address,
target: this.contracts.l1.L1CrossDomainMessenger.address,
value: updated.value,
minGasLimit: gasLimit,
message: encodeCrossDomainMessageV1(
updated.messageNonce,
updated.sender,
updated.target,
updated.value,
updated.minGasLimit,
updated.message
),
}
}
// public async getMessagesByAddress(
// address: AddressLike,
// opts?: {
// direction?: MessageDirection
// fromBlock?: NumberLike
// toBlock?: NumberLike
// }
// ): Promise<CrossChainMessage[]> {
// throw new Error(`
// The function getMessagesByAddress is currently not enabled because the sender parameter of
// the SentMessage event is not indexed within the CrossChainMessenger contracts.
// getMessagesByAddress will be enabled by plugging in an Optimism Indexer (coming soon).
// See the following issue on GitHub for additional context:
// https://github.com/ethereum-optimism/optimism/issues/2129
// `)
// }
/**
* Finds the appropriate bridge adapter for a given L1<>L2 token pair. Will throw if no bridges
* support the token pair or if more than one bridge supports the token pair.
*
* @param l1Token L1 token address.
* @param l2Token L2 token address.
* @returns The appropriate bridge adapter for the given token pair.
*/
public async getBridgeForTokenPair(
l1Token: AddressLike,
l2Token: AddressLike
): Promise<IBridgeAdapter> {
const bridges: IBridgeAdapter[] = []
for (const bridge of Object.values(this.bridges)) {
if (await bridge.supportsTokenPair(l1Token, l2Token)) {
bridges.push(bridge)
}
}
if (bridges.length === 0) {
throw new Error(`no supported bridge for token pair`)
}
if (bridges.length > 1) {
throw new Error(`found more than one bridge for token pair`)
}
return bridges[0]
}
/**
* Gets all deposits for a given address.
*
* @param address Address to search for messages from.
* @param opts Options object.
* @param opts.fromBlock Block to start searching for messages from. If not provided, will start
* from the first block (block #0).
* @param opts.toBlock Block to stop searching for messages at. If not provided, will stop at the
* latest known block ("latest").
* @returns All deposit token bridge messages sent by the given address.
*/
public async getDepositsByAddress(
address: AddressLike,
opts: {
fromBlock?: BlockTag
toBlock?: BlockTag
} = {}
): Promise<TokenBridgeMessage[]> {
return (
await Promise.all(
Object.values(this.bridges).map(async (bridge) => {
return bridge.getDepositsByAddress(address, opts)
})
)
)
.reduce((acc, val) => {
return acc.concat(val)
}, [])
.sort((a, b) => {
// Sort descending by block number
return b.blockNumber - a.blockNumber
})
}
/**
* Gets all withdrawals for a given address.
*
* @param address Address to search for messages from.
* @param opts Options object.
* @param opts.fromBlock Block to start searching for messages from. If not provided, will start
* from the first block (block #0).
* @param opts.toBlock Block to stop searching for messages at. If not provided, will stop at the
* latest known block ("latest").
* @returns All withdrawal token bridge messages sent by the given address.
*/
public async getWithdrawalsByAddress(
address: AddressLike,
opts: {
fromBlock?: BlockTag
toBlock?: BlockTag
} = {}
): Promise<TokenBridgeMessage[]> {
return (
await Promise.all(
Object.values(this.bridges).map(async (bridge) => {
return bridge.getWithdrawalsByAddress(address, opts)
})
)
)
.reduce((acc, val) => {
return acc.concat(val)
}, [])
.sort((a, b) => {
// Sort descending by block number
return b.blockNumber - a.blockNumber
})
}
/**
* Resolves a MessageLike into a CrossChainMessage object.
* Unlike other coercion functions, this function is stateful and requires making additional
* requests. For now I'm going to keep this function here, but we could consider putting a
* similar function inside of utils/coercion.ts if people want to use this without having to
* create an entire CrossChainProvider object.
*
* @param message MessageLike to resolve into a CrossChainMessage.
* @returns Message coerced into a CrossChainMessage.
*/
public async toCrossChainMessage(
message: MessageLike
): Promise<CrossChainMessage> {
// TODO: Convert these checks into proper type checks.
if ((message as CrossChainMessage).message) {
return message as CrossChainMessage
} else if (
(message as TokenBridgeMessage).l1Token &&
(message as TokenBridgeMessage).l2Token &&
(message as TokenBridgeMessage).transactionHash
) {
console.log(
'CrossChainMessenger.getMessgesByTransaction()',
(message as TokenBridgeMessage).transactionHash
)
const messages = await this.getMessagesByTransaction(
(message as TokenBridgeMessage).transactionHash
)
console.log(
'CrossChainMessenger.getMessgesByTransaction() returned',
messages
)
// The `messages` object corresponds to a list of SentMessage events that were triggered by
// the same transaction. We want to find the specific SentMessage event that corresponds to
// the TokenBridgeMessage (either a ETHDepositInitiated, ERC20DepositInitiated, or
// WithdrawalInitiated event). We expect the behavior of bridge contracts to be that these
// TokenBridgeMessage events are triggered and then a SentMessage event is triggered. Our
// goal here is therefore to find the first SentMessage event that comes after the input
// event.
const found = messages
.sort((a, b) => {
// Sort all messages in ascending order by log index.
return a.logIndex - b.logIndex
})
.find((m) => {
return m.logIndex > (message as TokenBridgeMessage).logIndex
})
if (!found) {
throw new Error(`could not find SentMessage event for message`)
}
return found
} else {
console.log('CrossChainMessenger.getMessgesByTransaction()', { message })
// TODO: Explicit TransactionLike check and throw if not TransactionLike
const messages = await this.getMessagesByTransaction(
message as TransactionLike
)
// We only want to treat TransactionLike objects as MessageLike if they only emit a single
// message (very common). It's unintuitive to treat a TransactionLike as a MessageLike if
// they emit more than one message (which message do you pick?), so we throw an error.
if (messages.length !== 1) {
throw new Error(`expected 1 message, got ${messages.length}`)
}
return messages[0]
}
}
/**
* Retrieves the status of a particular message as an enum.
*
* @param message Cross chain message to check the status of.
* @returns Status of the message.
*/
public async getMessageStatus(message: MessageLike): Promise<MessageStatus> {
const resolved = await this.toCrossChainMessage(message)
const receipt = await this.getMessageReceipt(resolved)
if (resolved.direction === MessageDirection.L1_TO_L2) {
if (receipt === null) {
return MessageStatus.UNCONFIRMED_L1_TO_L2_MESSAGE
} else {
if (receipt.receiptStatus === MessageReceiptStatus.RELAYED_SUCCEEDED) {
return MessageStatus.RELAYED
} else {
return MessageStatus.FAILED_L1_TO_L2_MESSAGE
}
}
} else {
if (receipt === null) {
let timestamp: number
if (this.bedrock) {
const output = await this.getMessageBedrockOutput(resolved)
if (output === null) {
return MessageStatus.STATE_ROOT_NOT_PUBLISHED
}
// Convert the message to the low level message that was proven.
const withdrawal = await this.toLowLevelMessage(resolved)
// Attempt to fetch the proven withdrawal.
const provenWithdrawal =
await this.contracts.l1.OptimismPortal.provenWithdrawals(
hashLowLevelMessage(withdrawal)
)
// If the withdrawal hash has not been proven on L1,
// return `READY_TO_PROVE`
if (provenWithdrawal.timestamp.eq(BigNumber.from(0))) {
return MessageStatus.READY_TO_PROVE
}
// Set the timestamp to the provenWithdrawal's timestamp
timestamp = provenWithdrawal.timestamp.toNumber()
} else {
const stateRoot = await this.getMessageStateRoot(resolved)
if (stateRoot === null) {
return MessageStatus.STATE_ROOT_NOT_PUBLISHED
}
const bn = stateRoot.batch.blockNumber
const block = await this.l1Provider.getBlock(bn)
timestamp = block.timestamp
}
const challengePeriod = await this.getChallengePeriodSeconds()
const latestBlock = await this.l1Provider.getBlock('latest')
if (timestamp + challengePeriod > latestBlock.timestamp) {
return MessageStatus.IN_CHALLENGE_PERIOD
} else {
return MessageStatus.READY_FOR_RELAY
}
} else {
if (receipt.receiptStatus === MessageReceiptStatus.RELAYED_SUCCEEDED) {
return MessageStatus.RELAYED
} else {
return MessageStatus.READY_FOR_RELAY
}
}
}
}
/**
* Finds the receipt of the transaction that executed a particular cross chain message.
*
* @param message Message to find the receipt of.
* @returns CrossChainMessage receipt including receipt of the transaction that relayed the
* given message.
*/
public async getMessageReceipt(
message: MessageLike
): Promise<MessageReceipt> {
const resolved = await this.toCrossChainMessage(message)
const messageHash = hashCrossDomainMessage(
resolved.messageNonce,
resolved.sender,
resolved.target,
resolved.value,
resolved.minGasLimit,
resolved.message
)
// Here we want the messenger that will receive the message, not the one that sent it.
const messenger =
resolved.direction === MessageDirection.L1_TO_L2
? this.contracts.l2.L2CrossDomainMessenger
: this.contracts.l1.L1CrossDomainMessenger
const relayedMessageEvents = await messenger.queryFilter(
messenger.filters.RelayedMessage(messageHash)
)
// Great, we found the message. Convert it into a transaction receipt.
if (relayedMessageEvents.length === 1) {
return {
receiptStatus: MessageReceiptStatus.RELAYED_SUCCEEDED,
transactionReceipt:
await relayedMessageEvents[0].getTransactionReceipt(),
}
} else if (relayedMessageEvents.length > 1) {
// Should never happen!
throw new Error(`multiple successful relays for message`)
}
// We didn't find a transaction that relayed the message. We now attempt to find
// FailedRelayedMessage events instead.
const failedRelayedMessageEvents = await messenger.queryFilter(
messenger.filters.FailedRelayedMessage(messageHash)
)
// A transaction can fail to be relayed multiple times. We'll always return the last
// transaction that attempted to relay the message.
// TODO: Is this the best way to handle this?
if (failedRelayedMessageEvents.length > 0) {
return {
receiptStatus: MessageReceiptStatus.RELAYED_FAILED,
transactionReceipt: await failedRelayedMessageEvents[
failedRelayedMessageEvents.length - 1
].getTransactionReceipt(),
}
}
// TODO: If the user doesn't provide enough gas then there's a chance that FailedRelayedMessage
// will never be triggered. We should probably fix this at the contract level by requiring a
// minimum amount of input gas and designing the contracts such that the gas will always be
// enough to trigger the event. However, for now we need a temporary way to find L1 => L2
// transactions that fail but don't alert us because they didn't provide enough gas.
// TODO: Talk with the systems and protocol team about coordinating a hard fork that fixes this
// on both L1 and L2.
// Just return null if we didn't find a receipt. Slightly nicer than throwing an error.
return null
}
/**
* Waits for a message to be executed and returns the receipt of the transaction that executed
* the given message.
*
* @param message Message to wait for.
* @param opts Options to pass to the waiting function.
* @param opts.confirmations Number of transaction confirmations to wait for before returning.
* @param opts.pollIntervalMs Number of milliseconds to wait between polling for the receipt.
* @param opts.timeoutMs Milliseconds to wait before timing out.
* @returns CrossChainMessage receipt including receipt of the transaction that relayed the
* given message.
*/
public async waitForMessageReceipt(
message: MessageLike,
opts: {
confirmations?: number
pollIntervalMs?: number
timeoutMs?: number
} = {}
): Promise<MessageReceipt> {
// Resolving once up-front is slightly more efficient.
const resolved = await this.toCrossChainMessage(message)
let totalTimeMs = 0
while (totalTimeMs < (opts.timeoutMs || Infinity)) {
const tick = Date.now()
const receipt = await this.getMessageReceipt(resolved)
if (receipt !== null) {
return receipt
} else {
await sleep(opts.pollIntervalMs || 4000)
totalTimeMs += Date.now() - tick
}
}
throw new Error(`timed out waiting for message receipt`)
}
/**
* Waits until the status of a given message changes to the expected status. Note that if the
* status of the given message changes to a status that implies the expected status, this will
* still return. If the status of the message changes to a status that exclues the expected
* status, this will throw an error.
*
* @param message Message to wait for.
* @param status Expected status of the message.
* @param opts Options to pass to the waiting function.
* @param opts.pollIntervalMs Number of milliseconds to wait when polling.
* @param opts.timeoutMs Milliseconds to wait before timing out.
*/
public async waitForMessageStatus(
message: MessageLike,
status: MessageStatus,
opts: {
pollIntervalMs?: number
timeoutMs?: number
} = {}
): Promise<void> {
// Resolving once up-front is slightly more efficient.
const resolved = await this.toCrossChainMessage(message)
let totalTimeMs = 0
while (totalTimeMs < (opts.timeoutMs || Infinity)) {
const tick = Date.now()
const currentStatus = await this.getMessageStatus(resolved)
// Handle special cases for L1 to L2 messages.
if (resolved.direction === MessageDirection.L1_TO_L2) {
// If we're at the expected status, we're done.
if (currentStatus === status) {
return
}
if (
status === MessageStatus.UNCONFIRMED_L1_TO_L2_MESSAGE &&
currentStatus > status
) {
// Anything other than UNCONFIRMED_L1_TO_L2_MESSAGE implies that the message was at one
// point "unconfirmed", so we can stop waiting.
return
}
if (
status === MessageStatus.FAILED_L1_TO_L2_MESSAGE &&
currentStatus === MessageStatus.RELAYED
) {
throw new Error(
`incompatible message status, expected FAILED_L1_TO_L2_MESSAGE got RELAYED`
)
}
if (
status === MessageStatus.RELAYED &&
currentStatus === MessageStatus.FAILED_L1_TO_L2_MESSAGE
) {
throw new Error(
`incompatible message status, expected RELAYED got FAILED_L1_TO_L2_MESSAGE`
)
}
}
// Handle special cases for L2 to L1 messages.
if (resolved.direction === MessageDirection.L2_TO_L1) {
if (currentStatus >= status) {
// For L2 to L1 messages, anything after the expected status implies the previous status,
// so we can safely return if the current status enum is larger than the expected one.
return
}
}
await sleep(opts.pollIntervalMs || 4000)
totalTimeMs += Date.now() - tick
}
throw new Error(`timed out waiting for message status change`)
}
/**
* Estimates the amount of gas required to fully execute a given message on L2. Only applies to
* L1 => L2 messages. You would supply this gas limit when sending the message to L2.
*
* @param message Message get a gas estimate for.
* @param opts Options object.
* @param opts.bufferPercent Percentage of gas to add to the estimate. Defaults to 20.
* @param opts.from Address to use as the sender.
* @returns Estimates L2 gas limit.
*/
public async estimateL2MessageGasLimit(
message: MessageRequestLike,
opts?: {
bufferPercent?: number
from?: string
}
): Promise<BigNumber> {
let resolved: CrossChainMessage | CrossChainMessageRequest
let from: string
if ((message as CrossChainMessage).messageNonce === undefined) {
resolved = message as CrossChainMessageRequest
from = opts?.from
} else {
resolved = await this.toCrossChainMessage(message as MessageLike)
from = opts?.from || (resolved as CrossChainMessage).sender
}
// L2 message gas estimation is only used for L1 => L2 messages.
if (resolved.direction === MessageDirection.L2_TO_L1) {
throw new Error(`cannot estimate gas limit for L2 => L1 message`)
}
const estimate = await this.l2Provider.estimateGas({
from,
to: resolved.target,
data: resolved.message,
})
// Return the estimate plus a buffer of 20% just in case.
const bufferPercent = opts?.bufferPercent || 20
return estimate.mul(100 + bufferPercent).div(100)
}
/**
* Returns the estimated amount of time before the message can be executed. When this is a
* message being sent to L1, this will return the estimated time until the message will complete
* its challenge period. When this is a message being sent to L2, this will return the estimated
* amount of time until the message will be picked up and executed on L2.
*
* @param message Message to estimate the time remaining for.
* @returns Estimated amount of time remaining (in seconds) before the message can be executed.
*/
public async estimateMessageWaitTimeSeconds(
message: MessageLike
): Promise<number> {
const resolved = await this.toCrossChainMessage(message)
const status = await this.getMessageStatus(resolved)
if (resolved.direction === MessageDirection.L1_TO_L2) {
if (
status === MessageStatus.RELAYED ||
status === MessageStatus.FAILED_L1_TO_L2_MESSAGE
) {
// Transactions that are relayed or failed are considered completed, so the wait time is 0.
return 0
} else {
// Otherwise we need to estimate the number of blocks left until the transaction will be
// considered confirmed by the Layer 2 system. Then we multiply this by the estimated
// average L1 block time.
const receipt = await this.l1Provider.getTransactionReceipt(
resolved.transactionHash
)
const blocksLeft = Math.max(
this.depositConfirmationBlocks - receipt.confirmations,
0
)
return blocksLeft * this.l1BlockTimeSeconds
}
} else {
if (
status === MessageStatus.RELAYED ||
status === MessageStatus.READY_FOR_RELAY
) {
// Transactions that are relayed or ready for relay are considered complete.
return 0
} else if (status === MessageStatus.STATE_ROOT_NOT_PUBLISHED) {
// If the state root hasn't been published yet, just assume it'll be published relatively
// quickly and return the challenge period for now. In the future we could use more
// advanced techniques to figure out average time between transaction execution and
// state root publication.
return this.getChallengePeriodSeconds()
} else if (status === MessageStatus.IN_CHALLENGE_PERIOD) {
// If the message is still within the challenge period, then we need to estimate exactly
// the amount of time left until the challenge period expires. The challenge period starts
// when the state root is published.
const stateRoot = await this.getMessageStateRoot(resolved)
const challengePeriod = await this.getChallengePeriodSeconds()
const targetBlock = await this.l1Provider.getBlock(
stateRoot.batch.blockNumber
)
const latestBlock = await this.l1Provider.getBlock('latest')
return Math.max(
challengePeriod - (latestBlock.timestamp - targetBlock.timestamp),
0
)
} else {
// Should not happen
throw new Error(`unexpected message status`)
}
}
}
/**
* Queries the current challenge period in seconds from the StateCommitmentChain.
*
* @returns Current challenge period in seconds.
*/
public async getChallengePeriodSeconds(): Promise<number> {
if (!this.bedrock) {
return (
await this.contracts.l1.StateCommitmentChain.FRAUD_PROOF_WINDOW()
).toNumber()
}
const oracleVersion = await this.contracts.l1.L2OutputOracle.version()
const challengePeriod =
oracleVersion === '1.0.0'
? // The ABI in the SDK does not contain FINALIZATION_PERIOD_SECONDS
// in OptimismPortal, so making an explicit call instead.
BigNumber.from(
await this.contracts.l1.OptimismPortal.provider.call({
to: this.contracts.l1.OptimismPortal.address,
data: '0xf4daa291', // FINALIZATION_PERIOD_SECONDS
})
)
: await this.contracts.l1.L2OutputOracle.FINALIZATION_PERIOD_SECONDS()
return challengePeriod.toNumber()
}
/**
* Queries the OptimismPortal contract's `provenWithdrawals` mapping
* for a ProvenWithdrawal that matches the passed withdrawalHash
*
* @bedrock
* Note: This function is bedrock-specific.
*
* @returns A ProvenWithdrawal object
*/
public async getProvenWithdrawal(
withdrawalHash: string
): Promise<ProvenWithdrawal> {
if (!this.bedrock) {
throw new Error('message proving only applies after the bedrock upgrade')
}
return this.contracts.l1.OptimismPortal.provenWithdrawals(withdrawalHash)
}
/**
* Returns the Bedrock output root that corresponds to the given message.
*
* @param message Message to get the Bedrock output root for.
* @returns Bedrock output root.
*/
public async getMessageBedrockOutput(
message: MessageLike
): Promise<BedrockOutputData | null> {
const resolved = await this.toCrossChainMessage(message)
// Outputs are only a thing for L2 to L1 messages.
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`cannot get a state root for an L1 to L2 message`)
}
// Try to find the output index that corresponds to the block number attached to the message.
// We'll explicitly handle "cannot get output" errors as a null return value, but anything else
// needs to get thrown. Might need to revisit this in the future to be a little more robust
// when connected to RPCs that don't return nice error messages.
let l2OutputIndex: BigNumber
try {
l2OutputIndex =
await this.contracts.l1.L2OutputOracle.getL2OutputIndexAfter(
resolved.blockNumber
)
} catch (err) {
if (err.message.includes('L2OutputOracle: cannot get output')) {
return null
} else {
throw err
}
}
// Now pull the proposal out given the output index. Should always work as long as the above
// codepath completed successfully.
const proposal = await this.contracts.l1.L2OutputOracle.getL2Output(
l2OutputIndex
)
// Format everything and return it nicely.
return {
outputRoot: proposal.outputRoot,
l1Timestamp: proposal.timestamp.toNumber(),
l2BlockNumber: proposal.l2BlockNumber.toNumber(),
l2OutputIndex: l2OutputIndex.toNumber(),
}
}
/**
* Returns the state root that corresponds to a given message. This is the state root for the
* block in which the transaction was included, as published to the StateCommitmentChain. If the
* state root for the given message has not been published yet, this function returns null.
*
* @param message Message to find a state root for.
* @returns State root for the block in which the message was created.
*/
public async getMessageStateRoot(
message: MessageLike
): Promise<StateRoot | null> {
const resolved = await this.toCrossChainMessage(message)
// State roots are only a thing for L2 to L1 messages.
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`cannot get a state root for an L1 to L2 message`)
}
// We need the block number of the transaction that triggered the message so we can look up the
// state root batch that corresponds to that block number.
const messageTxReceipt = await this.l2Provider.getTransactionReceipt(
resolved.transactionHash
)
// Every block has exactly one transaction in it. Since there's a genesis block, the
// transaction index will always be one less than the block number.
const messageTxIndex = messageTxReceipt.blockNumber - 1
// Pull down the state root batch, we'll try to pick out the specific state root that
// corresponds to our message.
const stateRootBatch = await this.getStateRootBatchByTransactionIndex(
messageTxIndex
)
// No state root batch, no state root.
if (stateRootBatch === null) {
return null
}
// We have a state root batch, now we need to find the specific state root for our transaction.
// First we need to figure out the index of the state root within the batch we found. This is
// going to be the original transaction index offset by the total number of previous state
// roots.
const indexInBatch =
messageTxIndex - stateRootBatch.header.prevTotalElements.toNumber()
// Just a sanity check.
if (stateRootBatch.stateRoots.length <= indexInBatch) {
// Should never happen!
throw new Error(`state root does not exist in batch`)
}
return {
stateRoot: stateRootBatch.stateRoots[indexInBatch],
stateRootIndexInBatch: indexInBatch,
batch: stateRootBatch,
}
}
/**
* Returns the StateBatchAppended event that was emitted when the batch with a given index was
* created. Returns null if no such event exists (the batch has not been submitted).
*
* @param batchIndex Index of the batch to find an event for.
* @returns StateBatchAppended event for the batch, or null if no such batch exists.
*/
public async getStateBatchAppendedEventByBatchIndex(
batchIndex: number
): Promise<ethers.Event | null> {
const events = await this.contracts.l1.StateCommitmentChain.queryFilter(
this.contracts.l1.StateCommitmentChain.filters.StateBatchAppended(
batchIndex
)
)
if (events.length === 0) {
return null
} else if (events.length > 1) {
// Should never happen!
throw new Error(`found more than one StateBatchAppended event`)
} else {
return events[0]
}
}
/**
* Returns the StateBatchAppended event for the batch that includes the transaction with the
* given index. Returns null if no such event exists.
*
* @param transactionIndex Index of the L2 transaction to find an event for.
* @returns StateBatchAppended event for the batch that includes the given transaction by index.
*/
public async getStateBatchAppendedEventByTransactionIndex(
transactionIndex: number
): Promise<ethers.Event | null> {
const isEventHi = (event: ethers.Event, index: number) => {
const prevTotalElements = event.args._prevTotalElements.toNumber()
return index < prevTotalElements
}
const isEventLo = (event: ethers.Event, index: number) => {
const prevTotalElements = event.args._prevTotalElements.toNumber()
const batchSize = event.args._batchSize.toNumber()
return index >= prevTotalElements + batchSize
}
const totalBatches: ethers.BigNumber =
await this.contracts.l1.StateCommitmentChain.getTotalBatches()
if (totalBatches.eq(0)) {
return null
}
let lowerBound = 0
let upperBound = totalBatches.toNumber() - 1
let batchEvent: ethers.Event | null =
await this.getStateBatchAppendedEventByBatchIndex(upperBound)
// Only happens when no batches have been submitted yet.
if (batchEvent === null) {
return null
}
if (isEventLo(batchEvent, transactionIndex)) {
// Upper bound is too low, means this transaction doesn't have a corresponding state batch yet.
return null
} else if (!isEventHi(batchEvent, transactionIndex)) {
// Upper bound is not too low and also not too high. This means the upper bound event is the
// one we're looking for! Return it.
return batchEvent
}
// Binary search to find the right event. The above checks will guarantee that the event does
// exist and that we'll find it during this search.
while (lowerBound < upperBound) {
const middleOfBounds = Math.floor((lowerBound + upperBound) / 2)
batchEvent = await this.getStateBatchAppendedEventByBatchIndex(
middleOfBounds
)
if (isEventHi(batchEvent, transactionIndex)) {
upperBound = middleOfBounds
} else if (isEventLo(batchEvent, transactionIndex)) {
lowerBound = middleOfBounds
} else {
break
}
}
return batchEvent
}
/**
* Returns information about the state root batch that included the state root for the given
* transaction by index. Returns null if no such state root has been published yet.
*
* @param transactionIndex Index of the L2 transaction to find a state root batch for.
* @returns State root batch for the given transaction index, or null if none exists yet.
*/
public async getStateRootBatchByTransactionIndex(
transactionIndex: number
): Promise<StateRootBatch | null> {
const stateBatchAppendedEvent =
await this.getStateBatchAppendedEventByTransactionIndex(transactionIndex)
if (stateBatchAppendedEvent === null) {
return null
}
const stateBatchTransaction = await stateBatchAppendedEvent.getTransaction()
const [stateRoots] =
this.contracts.l1.StateCommitmentChain.interface.decodeFunctionData(
'appendStateBatch',
stateBatchTransaction.data
)
return {
blockNumber: stateBatchAppendedEvent.blockNumber,
stateRoots,
header: {
batchIndex: stateBatchAppendedEvent.args._batchIndex,
batchRoot: stateBatchAppendedEvent.args._batchRoot,
batchSize: stateBatchAppendedEvent.args._batchSize,
prevTotalElements: stateBatchAppendedEvent.args._prevTotalElements,
extraData: stateBatchAppendedEvent.args._extraData,
},
}
}
/**
* Generates the proof required to finalize an L2 to L1 message.
*
* @param message Message to generate a proof for.
* @returns Proof that can be used to finalize the message.
*/
public async getMessageProof(
message: MessageLike
): Promise<CrossChainMessageProof> {
const resolved = await this.toCrossChainMessage(message)
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`can only generate proofs for L2 to L1 messages`)
}
const stateRoot = await this.getMessageStateRoot(resolved)
if (stateRoot === null) {
throw new Error(`state root for message not yet published`)
}
// We need to calculate the specific storage slot that demonstrates that this message was
// actually included in the L2 chain. The following calculation is based on the fact that
// messages are stored in the following mapping on L2:
// https://github.com/ethereum-optimism/optimism/blob/c84d3450225306abbb39b4e7d6d82424341df2be/packages/contracts/contracts/L2/predeploys/OVM_L2ToL1MessagePasser.sol#L23
// You can read more about how Solidity storage slots are computed for mappings here:
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#mappings-and-dynamic-arrays
const messageSlot = ethers.utils.keccak256(
ethers.utils.keccak256(
encodeCrossDomainMessageV0(
resolved.target,
resolved.sender,
resolved.message,
resolved.messageNonce
) + remove0x(this.contracts.l2.L2CrossDomainMessenger.address)
) + '00'.repeat(32)
)
const stateTrieProof = await makeStateTrieProof(
this.l2Provider as ethers.providers.JsonRpcProvider,
resolved.blockNumber,
this.contracts.l2.OVM_L2ToL1MessagePasser.address,
messageSlot
)
return {
stateRoot: stateRoot.stateRoot,
stateRootBatchHeader: stateRoot.batch.header,
stateRootProof: {
index: stateRoot.stateRootIndexInBatch,
siblings: makeMerkleTreeProof(
stateRoot.batch.stateRoots,
stateRoot.stateRootIndexInBatch
),
},
stateTrieWitness: toHexString(rlp.encode(stateTrieProof.accountProof)),
storageTrieWitness: toHexString(rlp.encode(stateTrieProof.storageProof)),
}
}
/**
* Generates the bedrock proof required to finalize an L2 to L1 message.
*
* @param message Message to generate a proof for.
* @returns Proof that can be used to finalize the message.
*/
public async getBedrockMessageProof(
message: MessageLike
): Promise<BedrockCrossChainMessageProof> {
const resolved = await this.toCrossChainMessage(message)
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`can only generate proofs for L2 to L1 messages`)
}
const output = await this.getMessageBedrockOutput(resolved)
if (output === null) {
throw new Error(`state root for message not yet published`)
}
const withdrawal = await this.toLowLevelMessage(resolved)
const messageSlot = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
['bytes32', 'uint256'],
[hashLowLevelMessage(withdrawal), ethers.constants.HashZero]
)
)
const stateTrieProof = await makeStateTrieProof(
this.l2Provider as ethers.providers.JsonRpcProvider,
output.l2BlockNumber,
this.contracts.l2.BedrockMessagePasser.address,
messageSlot
)
const block = await (
this.l2Provider as ethers.providers.JsonRpcProvider
).send('eth_getBlockByNumber', [
toRpcHexString(output.l2BlockNumber),
false,
])
return {
outputRootProof: {
version: ethers.constants.HashZero,
stateRoot: block.stateRoot,
messagePasserStorageRoot: stateTrieProof.storageRoot,
latestBlockhash: block.hash,
},
withdrawalProof: stateTrieProof.storageProof,
l2OutputIndex: output.l2OutputIndex,
}
}
/**
* Sends a given cross chain message. Where the message is sent depends on the direction attached
* to the message itself.
*
* @param message Cross chain message to send.
* @param opts Additional options.
* @param opts.signer Optional signer to use to send the transaction.
* @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the message sending transaction.
*/
public async sendMessage(
message: CrossChainMessageRequest,
opts?: {
signer?: Signer
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionResponse> {
const tx = await this.populateTransaction.sendMessage(message, opts)
if (message.direction === MessageDirection.L1_TO_L2) {
return (opts?.signer || this.l1Signer).sendTransaction(tx)
} else {
return (opts?.signer || this.l2Signer).sendTransaction(tx)
}
}
/**
* Resends a given cross chain message with a different gas limit. Only applies to L1 to L2
* messages. If provided an L2 to L1 message, this function will throw an error.
*
* @param message Cross chain message to resend.
* @param messageGasLimit New gas limit to use for the message.
* @param opts Additional options.
* @param opts.signer Optional signer to use to send the transaction.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the message resending transaction.
*/
public async resendMessage(
message: MessageLike,
messageGasLimit: NumberLike,
opts?: {
signer?: Signer
overrides?: Overrides
}
): Promise<TransactionResponse> {
return (opts?.signer || this.l1Signer).sendTransaction(
await this.populateTransaction.resendMessage(
message,
messageGasLimit,
opts
)
)
}
/**
* Proves a cross chain message that was sent from L2 to L1. Only applicable for L2 to L1
* messages.
*
* @param message Message to finalize.
* @param opts Additional options.
* @param opts.signer Optional signer to use to send the transaction.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the finalization transaction.
*/
public async proveMessage(
message: MessageLike,
opts?: {
signer?: Signer
overrides?: Overrides
}
): Promise<TransactionResponse> {
console.log('CrossChainMessenger.populateTransaction() called', {
message,
opts,
})
const proveMessageCall = await this.populateTransaction.proveMessage(
message,
opts
)
console.log('populateTransaction() return', { proveMessageCall })
return (opts?.signer || this.l1Signer).sendTransaction(proveMessageCall)
}
/**
* Finalizes a cross chain message that was sent from L2 to L1. Only applicable for L2 to L1
* messages. Will throw an error if the message has not completed its challenge period yet.
*
* @param message Message to finalize.
* @param opts Additional options.
* @param opts.signer Optional signer to use to send the transaction.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the finalization transaction.
*/
public async finalizeMessage(
message: MessageLike,
opts?: {
signer?: Signer
overrides?: PayableOverrides
}
): Promise<TransactionResponse> {
return (opts?.signer || this.l1Signer).sendTransaction(
await this.populateTransaction.finalizeMessage(message, opts)
)
}
/**
* Deposits some ETH into the L2 chain.
*
* @param amount Amount of ETH to deposit (in wei).
* @param opts Additional options.
* @param opts.signer Optional signer to use to send the transaction.
* @param opts.recipient Optional address to receive the funds on L2. Defaults to sender.
* @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the deposit transaction.
*/
public async depositETH(
amount: NumberLike,
opts?: {
recipient?: AddressLike
signer?: Signer
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionResponse> {
return (opts?.signer || this.l1Signer).sendTransaction(
await this.populateTransaction.depositETH(amount, opts)
)
}
/**
* Withdraws some ETH back to the L1 chain.
*
* @param amount Amount of ETH to withdraw.
* @param opts Additional options.
* @param opts.signer Optional signer to use to send the transaction.
* @param opts.recipient Optional address to receive the funds on L1. Defaults to sender.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the withdraw transaction.
*/
public async withdrawETH(
amount: NumberLike,
opts?: {
recipient?: AddressLike
signer?: Signer
overrides?: Overrides
}
): Promise<TransactionResponse> {
return (opts?.signer || this.l2Signer).sendTransaction(
await this.populateTransaction.withdrawETH(amount, opts)
)
}
/**
* Queries the account's approval amount for a given L1 token.
*
* @param l1Token The L1 token address.
* @param l2Token The L2 token address.
* @param opts Additional options.
* @param opts.signer Optional signer to get the approval for.
* @returns Amount of tokens approved for deposits from the account.
*/
public async approval(
l1Token: AddressLike,
l2Token: AddressLike,
opts?: {
signer?: Signer
}
): Promise<BigNumber> {
const bridge = await this.getBridgeForTokenPair(l1Token, l2Token)
return bridge.approval(l1Token, l2Token, opts?.signer || this.l1Signer)
}
/**
* Approves a deposit into the L2 chain.
*
* @param l1Token The L1 token address.
* @param l2Token The L2 token address.
* @param amount Amount of the token to approve.
* @param opts Additional options.
* @param opts.signer Optional signer to use to send the transaction.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the approval transaction.
*/
public async approveERC20(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
signer?: Signer
overrides?: Overrides
}
): Promise<TransactionResponse> {
return (opts?.signer || this.l1Signer).sendTransaction(
await this.populateTransaction.approveERC20(
l1Token,
l2Token,
amount,
opts
)
)
}
/**
* Deposits some ERC20 tokens into the L2 chain.
*
* @param l1Token Address of the L1 token.
* @param l2Token Address of the L2 token.
* @param amount Amount to deposit.
* @param opts Additional options.
* @param opts.signer Optional signer to use to send the transaction.
* @param opts.recipient Optional address to receive the funds on L2. Defaults to sender.
* @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the deposit transaction.
*/
public async depositERC20(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
recipient?: AddressLike
signer?: Signer
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionResponse> {
return (opts?.signer || this.l1Signer).sendTransaction(
await this.populateTransaction.depositERC20(
l1Token,
l2Token,
amount,
opts
)
)
}
/**
* Withdraws some ERC20 tokens back to the L1 chain.
*
* @param l1Token Address of the L1 token.
* @param l2Token Address of the L2 token.
* @param amount Amount to withdraw.
* @param opts Additional options.
* @param opts.signer Optional signer to use to send the transaction.
* @param opts.recipient Optional address to receive the funds on L1. Defaults to sender.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the withdraw transaction.
*/
public async withdrawERC20(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
recipient?: AddressLike
signer?: Signer
overrides?: Overrides
}
): Promise<TransactionResponse> {
return (opts?.signer || this.l2Signer).sendTransaction(
await this.populateTransaction.withdrawERC20(
l1Token,
l2Token,
amount,
opts
)
)
}
/**
* Object that holds the functions that generate transactions to be signed by the user.
* Follows the pattern used by ethers.js.
*/
populateTransaction = {
/**
* Generates a transaction that sends a given cross chain message. This transaction can be signed
* and executed by a signer.
*
* @param message Cross chain message to send.
* @param opts Additional options.
* @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to send the message.
*/
sendMessage: async (
message: CrossChainMessageRequest,
opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionRequest> => {
if (message.direction === MessageDirection.L1_TO_L2) {
return this.contracts.l1.L1CrossDomainMessenger.populateTransaction.sendMessage(
message.target,
message.message,
opts?.l2GasLimit || (await this.estimateL2MessageGasLimit(message)),
opts?.overrides || {}
)
} else {
return this.contracts.l2.L2CrossDomainMessenger.populateTransaction.sendMessage(
message.target,
message.message,
0, // Gas limit goes unused when sending from L2 to L1
opts?.overrides || {}
)
}
},
/**
* Generates a transaction that resends a given cross chain message. Only applies to L1 to L2
* messages. This transaction can be signed and executed by a signer.
*
* @param message Cross chain message to resend.
* @param messageGasLimit New gas limit to use for the message.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to resend the message.
*/
resendMessage: async (
message: MessageLike,
messageGasLimit: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<TransactionRequest> => {
const resolved = await this.toCrossChainMessage(message)
if (resolved.direction === MessageDirection.L2_TO_L1) {
throw new Error(`cannot resend L2 to L1 message`)
}
if (this.bedrock) {
return this.populateTransaction.finalizeMessage(resolved, {
...(opts || {}),
overrides: {
...opts?.overrides,
gasLimit: messageGasLimit,
},
})
} else {
const legacyL1XDM = new ethers.Contract(
this.contracts.l1.L1CrossDomainMessenger.address,
getContractInterface('L1CrossDomainMessenger'),
this.l1SignerOrProvider
)
return legacyL1XDM.populateTransaction.replayMessage(
resolved.target,
resolved.sender,
resolved.message,
resolved.messageNonce,
resolved.minGasLimit,
messageGasLimit,
opts?.overrides || {}
)
}
},
/**
* Generates a message proving transaction that can be signed and executed. Only
* applicable for L2 to L1 messages.
*
* @param message Message to generate the proving transaction for.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to prove the message.
*/
proveMessage: async (
message: MessageLike,
opts?: {
overrides?: PayableOverrides
}
): Promise<TransactionRequest> => {
console.log('CrossChainMessenger.toCrossChainMessage() called', {
message,
})
const resolved = await this.toCrossChainMessage(message)
console.log('CrossChainMessenger.toCrossChainMessage() returned', {
resolved,
})
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error('cannot finalize L1 to L2 message')
}
if (!this.bedrock) {
throw new Error(
'message proving only applies after the bedrock upgrade'
)
}
console.log('CrossChainMessenger.toLowLevelMessage() called', {
resolved,
})
const withdrawal = await this.toLowLevelMessage(resolved)
console.log('CrossChainMessenger.toLowLevelMessage() returned', {
withdrawal,
})
console.log('CrossChainMessenger.getBedrockMessageProof() called', {
resolved,
})
const proof = await this.getBedrockMessageProof(resolved)
console.log('CrossChainMessenger.getBedrockMessageProof() returned', {
proof,
})
console.log(
'CrossChainMessenger.populateTransaction.proveMessage() called',
{
withdrawal,
proof,
}
)
return this.contracts.l1.OptimismPortal.populateTransaction.proveWithdrawalTransaction(
[
withdrawal.messageNonce,
withdrawal.sender,
withdrawal.target,
withdrawal.value,
withdrawal.minGasLimit,
withdrawal.message,
],
proof.l2OutputIndex,
[
proof.outputRootProof.version,
proof.outputRootProof.stateRoot,
proof.outputRootProof.messagePasserStorageRoot,
proof.outputRootProof.latestBlockhash,
],
proof.withdrawalProof,
opts?.overrides || {}
)
},
/**
* Generates a message finalization transaction that can be signed and executed. Only
* applicable for L2 to L1 messages. Will throw an error if the message has not completed
* its challenge period yet.
*
* @param message Message to generate the finalization transaction for.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to finalize the message.
*/
finalizeMessage: async (
message: MessageLike,
opts?: {
overrides?: PayableOverrides
}
): Promise<TransactionRequest> => {
const resolved = await this.toCrossChainMessage(message)
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`cannot finalize L1 to L2 message`)
}
if (this.bedrock) {
const withdrawal = await this.toLowLevelMessage(resolved)
return this.contracts.l1.OptimismPortal.populateTransaction.finalizeWithdrawalTransaction(
[
withdrawal.messageNonce,
withdrawal.sender,
withdrawal.target,
withdrawal.value,
withdrawal.minGasLimit,
withdrawal.message,
],
opts?.overrides || {}
)
} else {
// L1CrossDomainMessenger relayMessage is the only method that isn't fully backwards
// compatible, so we need to use the legacy interface. When we fully upgrade to Bedrock we
// should be able to remove this code.
const proof = await this.getMessageProof(resolved)
const legacyL1XDM = new ethers.Contract(
this.contracts.l1.L1CrossDomainMessenger.address,
getContractInterface('L1CrossDomainMessenger'),
this.l1SignerOrProvider
)
return legacyL1XDM.populateTransaction.relayMessage(
resolved.target,
resolved.sender,
resolved.message,
resolved.messageNonce,
proof,
opts?.overrides || {}
)
}
},
/**
* Generates a transaction for depositing some ETH into the L2 chain.
*
* @param amount Amount of ETH to deposit.
* @param opts Additional options.
* @param opts.recipient Optional address to receive the funds on L2. Defaults to sender.
* @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to deposit the ETH.
*/
depositETH: async (
amount: NumberLike,
opts?: {
recipient?: AddressLike
l2GasLimit?: NumberLike
overrides?: PayableOverrides
}
): Promise<TransactionRequest> => {
return this.bridges.ETH.populateTransaction.deposit(
ethers.constants.AddressZero,
predeploys.OVM_ETH,
amount,
opts
)
},
/**
* Generates a transaction for withdrawing some ETH back to the L1 chain.
*
* @param amount Amount of ETH to withdraw.
* @param opts Additional options.
* @param opts.recipient Optional address to receive the funds on L1. Defaults to sender.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to withdraw the ETH.
*/
withdrawETH: async (
amount: NumberLike,
opts?: {
recipient?: AddressLike
overrides?: Overrides
}
): Promise<TransactionRequest> => {
return this.bridges.ETH.populateTransaction.withdraw(
ethers.constants.AddressZero,
predeploys.OVM_ETH,
amount,
opts
)
},
/**
* Generates a transaction for approving some tokens to deposit into the L2 chain.
*
* @param l1Token The L1 token address.
* @param l2Token The L2 token address.
* @param amount Amount of the token to approve.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the approval transaction.
*/
approveERC20: async (
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<TransactionRequest> => {
const bridge = await this.getBridgeForTokenPair(l1Token, l2Token)
return bridge.populateTransaction.approve(l1Token, l2Token, amount, opts)
},
/**
* Generates a transaction for depositing some ERC20 tokens into the L2 chain.
*
* @param l1Token Address of the L1 token.
* @param l2Token Address of the L2 token.
* @param amount Amount to deposit.
* @param opts Additional options.
* @param opts.recipient Optional address to receive the funds on L2. Defaults to sender.
* @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to deposit the tokens.
*/
depositERC20: async (
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
recipient?: AddressLike
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionRequest> => {
const bridge = await this.getBridgeForTokenPair(l1Token, l2Token)
return bridge.populateTransaction.deposit(l1Token, l2Token, amount, opts)
},
/**
* Generates a transaction for withdrawing some ERC20 tokens back to the L1 chain.
*
* @param l1Token Address of the L1 token.
* @param l2Token Address of the L2 token.
* @param amount Amount to withdraw.
* @param opts Additional options.
* @param opts.recipient Optional address to receive the funds on L1. Defaults to sender.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to withdraw the tokens.
*/
withdrawERC20: async (
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
recipient?: AddressLike
overrides?: Overrides
}
): Promise<TransactionRequest> => {
const bridge = await this.getBridgeForTokenPair(l1Token, l2Token)
return bridge.populateTransaction.withdraw(l1Token, l2Token, amount, opts)
},
}
/**
* Object that holds the functions that estimates the gas required for a given transaction.
* Follows the pattern used by ethers.js.
*/
estimateGas = {
/**
* Estimates gas required to send a cross chain message.
*
* @param message Cross chain message to send.
* @param opts Additional options.
* @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
* @param opts.overrides Optional transaction overrides.
* @returns Gas estimate for the transaction.
*/
sendMessage: async (
message: CrossChainMessageRequest,
opts?: {
l2GasLimit?: NumberLike
overrides?: CallOverrides
}
): Promise<BigNumber> => {
const tx = await this.populateTransaction.sendMessage(message, opts)
if (message.direction === MessageDirection.L1_TO_L2) {
return this.l1Provider.estimateGas(tx)
} else {
return this.l2Provider.estimateGas(tx)
}
},
/**
* Estimates gas required to resend a cross chain message. Only applies to L1 to L2 messages.
*
* @param message Cross chain message to resend.
* @param messageGasLimit New gas limit to use for the message.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Gas estimate for the transaction.
*/
resendMessage: async (
message: MessageLike,
messageGasLimit: NumberLike,
opts?: {
overrides?: CallOverrides
}
): Promise<BigNumber> => {
return this.l1Provider.estimateGas(
await this.populateTransaction.resendMessage(
message,
messageGasLimit,
opts
)
)
},
/**
* Estimates gas required to prove a cross chain message. Only applies to L2 to L1 messages.
*
* @param message Message to generate the proving transaction for.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Gas estimate for the transaction.
*/
proveMessage: async (
message: MessageLike,
opts?: {
overrides?: CallOverrides
}
): Promise<BigNumber> => {
return this.l1Provider.estimateGas(
await this.populateTransaction.proveMessage(message, opts)
)
},
/**
* Estimates gas required to finalize a cross chain message. Only applies to L2 to L1 messages.
*
* @param message Message to generate the finalization transaction for.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Gas estimate for the transaction.
*/
finalizeMessage: async (
message: MessageLike,
opts?: {
overrides?: CallOverrides
}
): Promise<BigNumber> => {
return this.l1Provider.estimateGas(
await this.populateTransaction.finalizeMessage(message, opts)
)
},
/**
* Estimates gas required to deposit some ETH into the L2 chain.
*
* @param amount Amount of ETH to deposit.
* @param opts Additional options.
* @param opts.recipient Optional address to receive the funds on L2. Defaults to sender.
* @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
* @param opts.overrides Optional transaction overrides.
* @returns Gas estimate for the transaction.
*/
depositETH: async (
amount: NumberLike,
opts?: {
recipient?: AddressLike
l2GasLimit?: NumberLike
overrides?: CallOverrides
}
): Promise<BigNumber> => {
return this.l1Provider.estimateGas(
await this.populateTransaction.depositETH(amount, opts)
)
},
/**
* Estimates gas required to withdraw some ETH back to the L1 chain.
*
* @param amount Amount of ETH to withdraw.
* @param opts Additional options.
* @param opts.recipient Optional address to receive the funds on L1. Defaults to sender.
* @param opts.overrides Optional transaction overrides.
* @returns Gas estimate for the transaction.
*/
withdrawETH: async (
amount: NumberLike,
opts?: {
recipient?: AddressLike
overrides?: CallOverrides
}
): Promise<BigNumber> => {
return this.l2Provider.estimateGas(
await this.populateTransaction.withdrawETH(amount, opts)
)
},
/**
* Estimates gas required to approve some tokens to deposit into the L2 chain.
*
* @param l1Token The L1 token address.
* @param l2Token The L2 token address.
* @param amount Amount of the token to approve.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the approval transaction.
*/
approveERC20: async (
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
overrides?: CallOverrides
}
): Promise<BigNumber> => {
return this.l1Provider.estimateGas(
await this.populateTransaction.approveERC20(
l1Token,
l2Token,
amount,
opts
)
)
},
/**
* Estimates gas required to deposit some ERC20 tokens into the L2 chain.
*
* @param l1Token Address of the L1 token.
* @param l2Token Address of the L2 token.
* @param amount Amount to deposit.
* @param opts Additional options.
* @param opts.recipient Optional address to receive the funds on L2. Defaults to sender.
* @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
* @param opts.overrides Optional transaction overrides.
* @returns Gas estimate for the transaction.
*/
depositERC20: async (
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
recipient?: AddressLike
l2GasLimit?: NumberLike
overrides?: CallOverrides
}
): Promise<BigNumber> => {
return this.l1Provider.estimateGas(
await this.populateTransaction.depositERC20(
l1Token,
l2Token,
amount,
opts
)
)
},
/**
* Estimates gas required to withdraw some ERC20 tokens back to the L1 chain.
*
* @param l1Token Address of the L1 token.
* @param l2Token Address of the L2 token.
* @param amount Amount to withdraw.
* @param opts Additional options.
* @param opts.recipient Optional address to receive the funds on L1. Defaults to sender.
* @param opts.overrides Optional transaction overrides.
* @returns Gas estimate for the transaction.
*/
withdrawERC20: async (
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
recipient?: AddressLike
overrides?: CallOverrides
}
): Promise<BigNumber> => {
return this.l2Provider.estimateGas(
await this.populateTransaction.withdrawERC20(
l1Token,
l2Token,
amount,
opts
)
)
},
}
}
import { hashWithdrawal } from '@eth-optimism/core-utils' import { hashWithdrawal } from '@eth-optimism/core-utils'
import { BigNumber, utils } from 'ethers' import { BigNumber, utils, ethers } from 'ethers'
import { LowLevelMessage } from '../interfaces' import { LowLevelMessage } from '../interfaces'
...@@ -22,6 +22,22 @@ export const hashLowLevelMessage = (message: LowLevelMessage): string => { ...@@ -22,6 +22,22 @@ export const hashLowLevelMessage = (message: LowLevelMessage): string => {
) )
} }
/**
* Utility for hashing a message hash. This computes the storage slot
* where the message hash will be stored in state. HashZero is used
* because the first mapping in the contract is used.
*
* @param messageHash Message hash to hash.
* @returns Hash of the given message hash.
*/
export const hashMessageHash = (messageHash: string): string => {
const data = ethers.utils.defaultAbiCoder.encode(
['bytes32', 'uint256'],
[messageHash, ethers.constants.HashZero]
)
return ethers.utils.keccak256(data)
}
/** /**
* Compute the min gas limit for a migrated withdrawal. * Compute the min gas limit for a migrated withdrawal.
*/ */
......
...@@ -99,8 +99,10 @@ describe('prove message', () => { ...@@ -99,8 +99,10 @@ describe('prove message', () => {
expect(txReceipt).toBeDefined() expect(txReceipt).toBeDefined()
expect( const tx = await crossChainMessenger.proveMessage(txWithdrawalHash)
await crossChainMessenger.proveMessage(txWithdrawalHash) const receipt = await tx.wait()
).toMatchInlineSnapshot()
// A 1 means the transaction was successful
expect(receipt.status).toBe(1)
}, 20_000) }, 20_000)
}) })
import { BigNumber } from 'ethers' import { BigNumber } from 'ethers'
import { expect } from '../setup' import { expect } from '../setup'
import { migratedWithdrawalGasLimit } from '../../src/utils/message-utils' import {
migratedWithdrawalGasLimit,
hashLowLevelMessage,
hashMessageHash,
} from '../../src/utils/message-utils'
describe('Message Utils', () => { describe('Message Utils', () => {
describe('migratedWithdrawalGasLimit', () => { describe('migratedWithdrawalGasLimit', () => {
...@@ -26,4 +30,47 @@ describe('Message Utils', () => { ...@@ -26,4 +30,47 @@ describe('Message Utils', () => {
} }
}) })
}) })
/**
* Test that storage slot computation is correct. The test vectors are
* from actual migrated withdrawals on goerli.
*/
describe('Withdrawal Hashing', () => {
it('should work', () => {
const tests = [
{
input: {
messageNonce: BigNumber.from(100000),
sender: '0x4200000000000000000000000000000000000007',
target: '0x5086d1eEF304eb5284A0f6720f79403b4e9bE294',
value: BigNumber.from(0),
minGasLimit: BigNumber.from(207744),
message:
'0xd764ad0b00000000000000000000000000000000000000000000000000000000000186a00000000000000000000000004200000000000000000000000000000000000010000000000000000000000000636af16bf2f682dd3109e60102b8e1a089fedaa80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e4a9f9e67500000000000000000000000007865c6e87b9f70255377e024ace6630c1eaa37f0000000000000000000000003b8e53b3ab8e01fb57d0c9e893bc4d655aa67d84000000000000000000000000b91882244f7f82540f2941a759724523c7b9a166000000000000000000000000b91882244f7f82540f2941a759724523c7b9a166000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
},
result:
'0x7c83d39edf60c0ab61bc7cfd2e5f741efdf02fd6e2da0f12318f0d1858d3773b',
},
{
input: {
messageNonce: BigNumber.from(100001),
sender: '0x4200000000000000000000000000000000000007',
target: '0x5086d1eEF304eb5284A0f6720f79403b4e9bE294',
value: BigNumber.from(0),
minGasLimit: BigNumber.from(207744),
message:
'0xd764ad0b00000000000000000000000000000000000000000000000000000000000186a10000000000000000000000004200000000000000000000000000000000000010000000000000000000000000636af16bf2f682dd3109e60102b8e1a089fedaa80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e4a9f9e67500000000000000000000000007865c6e87b9f70255377e024ace6630c1eaa37f0000000000000000000000004e62882864fb8ce54affcaf8d899a286762b011b000000000000000000000000b91882244f7f82540f2941a759724523c7b9a166000000000000000000000000b91882244f7f82540f2941a759724523c7b9a166000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
},
result:
'0x17c90d87508a23d806962f4c5f366ef505e8d80e5cc2a5c87242560c21d7c588',
},
]
for (const test of tests) {
const hash = hashLowLevelMessage(test.input)
const messageSlot = hashMessageHash(hash)
expect(messageSlot).to.eq(test.result)
}
})
})
}) })
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