Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
N
nebula
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
exchain
nebula
Commits
6c87ec92
Commit
6c87ec92
authored
Apr 13, 2023
by
Will Cory
Committed by
Mark Tyneway
Apr 18, 2023
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
sdk: bugfixes
parent
a00e9eb4
Changes
6
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
196 additions
and
2371 deletions
+196
-2371
config.yml
.circleci/config.yml
+100
-88
cross-chain-messenger.ts
packages/sdk/src/cross-chain-messenger.ts
+26
-24
cross-chain-messenger.with-logs.ts
packages/sdk/src/cross-chain-messenger.with-logs.ts
+0
-2254
message-utils.ts
packages/sdk/src/utils/message-utils.ts
+17
-1
proveMessage.spec.ts
packages/sdk/test-next/proveMessage.spec.ts
+5
-3
message-utils.spec.ts
packages/sdk/test/utils/message-utils.spec.ts
+48
-1
No files found.
.circleci/config.yml
View file @
6c87ec92
...
...
@@ -5,7 +5,7 @@ orbs:
gcp-cli
:
circleci/gcp-cli@3.0.1
commands
:
gcp-oidc-authenticate
:
description
:
"
Authenticate
with
GCP
using
a
CircleCI
OIDC
token."
description
:
'
Authenticate
with
GCP
using
a
CircleCI
OIDC
token.'
parameters
:
project_id
:
type
:
env_var_name
...
...
@@ -27,7 +27,7 @@ commands:
default
:
/home/circleci/oidc_token.json
steps
:
-
run
:
name
:
"
Create
OIDC
credential
configuration"
name
:
'
Create
OIDC
credential
configuration'
command
:
|
# Store OIDC token in temp file
echo $CIRCLE_OIDC_TOKEN > << parameters.oidc_token_file_path >>
...
...
@@ -38,21 +38,21 @@ commands:
--service-account="${<< parameters.service_account_email >>}" \
--credential-source-file=<< parameters.oidc_token_file_path >>
-
run
:
name
:
"
Authenticate
with
GCP
using
OIDC"
name
:
'
Authenticate
with
GCP
using
OIDC'
command
:
|
# Configure gcloud to leverage the generated credential configuration
gcloud auth login --brief --cred-file "<< parameters.gcp_cred_config_file_path >>"
# Configure ADC
echo "export GOOGLE_APPLICATION_CREDENTIALS='<< parameters.gcp_cred_config_file_path >>'" | tee -a "$BASH_ENV"
check-changed
:
description
:
"
Conditionally
halts
a
step
if
certain
modules
change"
description
:
'
Conditionally
halts
a
step
if
certain
modules
change'
parameters
:
patterns
:
type
:
string
description
:
"
Comma-separated
list
of
dependencies"
description
:
'
Comma-separated
list
of
dependencies'
steps
:
-
run
:
name
:
"
Check
for
changes"
name
:
'
Check
for
changes'
command
:
|
cd ops/check-changed
pip3 install -r requirements.txt
...
...
@@ -77,26 +77,26 @@ jobs:
name
:
Save Yarn Package Cache
key
:
yarn-packages-v2-{{ checksum "yarn.lock" }}
paths
:
-
"
node_modules"
-
"
packages/actor-tests/node_modules"
-
"
packages/atst/node_modules"
-
"
packages/balance-monitor/node_modules"
-
"
packages/chain-mon/node_modules"
-
"
packages/common-ts/node_modules"
-
"
packages/contracts/node_modules"
-
"
packages/contracts-bedrock/node_modules"
-
"
packages/contracts-governance/node_modules"
-
"
packages/contracts-periphery/node_modules"
-
"
packages/core-utils/node_modules"
-
"
packages/data-transport-layer/node_modules"
-
"
packages/drippie-mon/node_modules"
-
"
packages/fault-detector/node_modules"
-
"
packages/hardhat-deploy-config/node_modules"
-
"
packages/integration-tests-bedrock/node_modules"
-
"
packages/message-relayer/node_modules"
-
"
packages/migration-data/node_modules"
-
"
packages/replica-healthcheck/node_modules"
-
"
packages/sdk/node_modules"
-
'
node_modules'
-
'
packages/actor-tests/node_modules'
-
'
packages/atst/node_modules'
-
'
packages/balance-monitor/node_modules'
-
'
packages/chain-mon/node_modules'
-
'
packages/common-ts/node_modules'
-
'
packages/contracts/node_modules'
-
'
packages/contracts-bedrock/node_modules'
-
'
packages/contracts-governance/node_modules'
-
'
packages/contracts-periphery/node_modules'
-
'
packages/core-utils/node_modules'
-
'
packages/data-transport-layer/node_modules'
-
'
packages/drippie-mon/node_modules'
-
'
packages/fault-detector/node_modules'
-
'
packages/hardhat-deploy-config/node_modules'
-
'
packages/integration-tests-bedrock/node_modules'
-
'
packages/message-relayer/node_modules'
-
'
packages/migration-data/node_modules'
-
'
packages/replica-healthcheck/node_modules'
-
'
packages/sdk/node_modules'
-
run
:
name
:
print forge version
command
:
forge --version
...
...
@@ -104,17 +104,17 @@ jobs:
name
:
Build monorepo
command
:
yarn build
-
persist_to_workspace
:
root
:
"
."
root
:
'
.'
paths
:
-
"
packages/*/dist"
-
"
packages/*/artifacts"
-
"
packages/contracts/src/contract-artifacts.ts"
-
"
packages/contracts/src/contract-deployed-artifacts.ts"
-
"
packages/contracts/chugsplash"
-
"
packages/contracts/L1"
-
"
packages/contracts/L2"
-
"
packages/contracts/libraries"
-
"
packages/contracts/standards"
-
'
packages/*/dist'
-
'
packages/*/artifacts'
-
'
packages/contracts/src/contract-artifacts.ts'
-
'
packages/contracts/src/contract-deployed-artifacts.ts'
-
'
packages/contracts/chugsplash'
-
'
packages/contracts/L1'
-
'
packages/contracts/L2'
-
'
packages/contracts/libraries'
-
'
packages/contracts/standards'
docker-build
:
environment
:
...
...
@@ -135,11 +135,11 @@ jobs:
registry
:
description
:
Docker registry
type
:
string
default
:
"
us-docker.pkg.dev"
default
:
'
us-docker.pkg.dev'
repo
:
description
:
Docker repo
type
:
string
default
:
"
oplabs-tools-artifacts/images"
default
:
'
oplabs-tools-artifacts/images'
machine
:
image
:
ubuntu-2204:2022.07.1
resource_class
:
medium
...
...
@@ -170,7 +170,7 @@ jobs:
-
persist_to_workspace
:
root
:
/tmp/docker_images
paths
:
-
"
."
-
'
.'
docker-publish
:
environment
:
...
...
@@ -188,23 +188,23 @@ jobs:
docker_context
:
description
:
Docker build context
type
:
string
default
:
"
."
default
:
'
.'
docker_target
:
description
:
"
target
build
stage"
description
:
'
target
build
stage'
type
:
string
default
:
"
"
default
:
'
'
registry
:
description
:
Docker registry
type
:
string
default
:
"
us-docker.pkg.dev"
default
:
'
us-docker.pkg.dev'
repo
:
description
:
Docker repo
type
:
string
default
:
"
oplabs-tools-artifacts/images"
default
:
'
oplabs-tools-artifacts/images'
platforms
:
description
:
Platforms to build for
type
:
string
default
:
"
linux/amd64"
default
:
'
linux/amd64'
machine
:
image
:
ubuntu-2204:2022.07.1
resource_class
:
medium
...
...
@@ -250,15 +250,15 @@ jobs:
registry
:
description
:
Docker registry
type
:
string
default
:
"
us-docker.pkg.dev"
default
:
'
us-docker.pkg.dev'
repo
:
description
:
Docker repo
type
:
string
default
:
"
oplabs-tools-artifacts/images"
default
:
'
oplabs-tools-artifacts/images'
platforms
:
description
:
Platforms to build for
type
:
string
default
:
"
linux/amd64"
default
:
'
linux/amd64'
machine
:
image
:
ubuntu-2204:2022.07.1
resource_class
:
medium
...
...
@@ -321,7 +321,7 @@ jobs:
resource_class
:
large
steps
:
-
checkout
-
attach_workspace
:
{
at
:
"
."
}
-
attach_workspace
:
{
at
:
'
.'
}
-
restore_cache
:
name
:
Restore Yarn Package Cache
keys
:
...
...
@@ -344,7 +344,7 @@ jobs:
-
image
:
us-docker.pkg.dev/oplabs-tools-artifacts/images/ci-builder:latest
steps
:
-
checkout
-
attach_workspace
:
{
at
:
"
."
}
-
attach_workspace
:
{
at
:
'
.'
}
-
restore_cache
:
name
:
Restore Yarn Package Cache
keys
:
...
...
@@ -405,7 +405,7 @@ jobs:
resource_class
:
large
steps
:
-
checkout
-
attach_workspace
:
{
at
:
"
."
}
-
attach_workspace
:
{
at
:
'
.'
}
-
restore_cache
:
name
:
Restore Yarn Package Cache
keys
:
...
...
@@ -424,7 +424,7 @@ jobs:
-
image
:
us-docker.pkg.dev/oplabs-tools-artifacts/images/ci-builder:latest
steps
:
-
checkout
-
attach_workspace
:
{
at
:
"
."
}
-
attach_workspace
:
{
at
:
'
.'
}
-
restore_cache
:
name
:
Restore Yarn Package Cache
keys
:
...
...
@@ -441,7 +441,7 @@ jobs:
-
image
:
us-docker.pkg.dev/oplabs-tools-artifacts/images/ci-builder:latest
steps
:
-
checkout
-
attach_workspace
:
{
at
:
"
."
}
-
attach_workspace
:
{
at
:
'
.'
}
-
check-changed
:
patterns
:
contracts-bedrock,contracts
-
run
:
...
...
@@ -451,7 +451,7 @@ jobs:
-
persist_to_workspace
:
root
:
.
paths
:
-
"
node_modules"
-
'
node_modules'
-
packages/contracts-bedrock
bedrock-echidna-run
:
...
...
@@ -468,7 +468,7 @@ jobs:
resource_class
:
<<parameters.size>>
steps
:
-
checkout
-
attach_workspace
:
{
at
:
"
."
}
-
attach_workspace
:
{
at
:
'
.'
}
-
restore_cache
:
name
:
Restore Yarn Package Cache
keys
:
...
...
@@ -487,7 +487,7 @@ jobs:
resource_class
:
medium
steps
:
-
checkout
-
attach_workspace
:
{
at
:
"
."
}
-
attach_workspace
:
{
at
:
'
.'
}
-
restore_cache
:
name
:
Restore Yarn Package Cache
keys
:
...
...
@@ -516,7 +516,7 @@ jobs:
resource_class
:
large
steps
:
-
checkout
-
attach_workspace
:
{
at
:
"
."
}
-
attach_workspace
:
{
at
:
'
.'
}
-
restore_cache
:
name
:
Restore Yarn Package Cache
keys
:
...
...
@@ -544,6 +544,10 @@ jobs:
-
attach_workspace
:
{
at
:
'
.'
}
-
check-changed
:
patterns
:
sdk,contracts-bedrock,contracts
-
restore_cache
:
name
:
Restore Yarn Package Cache
keys
:
-
yarn-packages-v2-{{ checksum "yarn.lock" }}
-
run
:
name
:
anvil-l1
background
:
true
...
...
@@ -552,6 +556,14 @@ jobs:
name
:
anvil-l2
background
:
true
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
:
name
:
make sure anvil l1 is up
command
:
cast block-number --rpc-url http://localhost:8545
...
...
@@ -565,9 +577,9 @@ jobs:
working_directory
:
packages/sdk
environment
:
# anvil[0] test private key
VITE_E2E_PRIVATE_KEY
:
0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
VITE_E2E_RPC_URL
S
_L1
:
http://localhost:8545
VITE_E2E_RPC_URL
S
_L2
:
http://localhost:9545
VITE_E2E_PRIVATE_KEY
:
'
0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
VITE_E2E_RPC_URL_L1
:
http://localhost:8545
VITE_E2E_RPC_URL_L2
:
http://localhost:9545
bedrock-markdown
:
machine
:
...
...
@@ -607,7 +619,7 @@ jobs:
-
image
:
us-docker.pkg.dev/oplabs-tools-artifacts/images/ci-builder:latest
steps
:
-
checkout
-
attach_workspace
:
{
at
:
"
."
}
-
attach_workspace
:
{
at
:
'
.'
}
-
restore_cache
:
name
:
Restore Yarn Package Cache
keys
:
...
...
@@ -753,7 +765,7 @@ jobs:
working_directory
:
<<parameters.working_directory>>
-
when
:
condition
:
equal
:
[
true
,
<<parameters.build>>
]
equal
:
[
true
,
<<parameters.build>>
]
steps
:
-
run
:
name
:
Build
...
...
@@ -809,7 +821,7 @@ jobs:
-
when
:
condition
:
and
:
-
equal
:
[
true
,
<<parameters.deploy>>
]
-
equal
:
[
true
,
<<parameters.deploy>>
]
steps
:
-
run
:
name
:
Bring up the stack
...
...
@@ -859,7 +871,7 @@ jobs:
-
when
:
condition
:
and
:
-
equal
:
[
false
,
<<parameters.deploy>>
]
-
equal
:
[
false
,
<<parameters.deploy>>
]
steps
:
-
run
:
name
:
Bring up the stack
...
...
@@ -962,22 +974,22 @@ jobs:
-
checkout
-
unless
:
condition
:
equal
:
[
"
develop"
,
<< pipeline.git.branch >>
]
equal
:
[
'
develop'
,
<< pipeline.git.branch >>
]
steps
:
-
run
:
# 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.
name
:
"
Conditionally
set
BASELINE
env
var"
name
:
'
Conditionally
set
BASELINE
env
var'
command
:
|
echo 'export SEMGREP_BASELINE_REF=${TEMPORARY_BASELINE_REF}' >> $BASH_ENV
-
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
:
|
echo 'export SEMGREP_PR_ID=${CIRCLE_PULL_REQUEST##*/}' >> $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
-
run
:
name
:
"
Semgrep
scan"
name
:
'
Semgrep
scan'
command
:
semgrep ci
go-mod-tidy
:
...
...
@@ -986,7 +998,7 @@ jobs:
steps
:
-
checkout
-
run
:
name
:
"
Go
mod
tidy"
name
:
'
Go
mod
tidy'
command
:
make mod-tidy && git diff --exit-code
hive-test
:
...
...
@@ -1014,7 +1026,7 @@ jobs:
-
go/load-cache
-
go/mod-download
-
go/save-cache
-
run
:
{
command
:
"
go
build
."
}
-
run
:
{
command
:
'
go
build
.'
}
-
run
:
command
:
|
./hive \
...
...
@@ -1024,7 +1036,7 @@ jobs:
-
run
:
command
:
|
tar -cvf /tmp/workspace.tgz -C /home/circleci/project /home/circleci/project/workspace
name
:
"
Archive
workspace"
name
:
'
Archive
workspace'
-
store_artifacts
:
path
:
/tmp/workspace.tgz
destination
:
hive-workspace.tgz
...
...
@@ -1083,49 +1095,49 @@ workflows:
name
:
actor-tests-tests
coverage_flag
:
actor-tests-tests
package_name
:
actor-tests
dependencies
:
"
(core-utils|sdk)"
dependencies
:
'
(core-utils|sdk)'
requires
:
-
yarn-monorepo
-
js-lint-test
:
name
:
contracts-periphery-tests
coverage_flag
:
contracts-periphery-tests
package_name
:
contracts-periphery
dependencies
:
"
(contracts|contracts-bedrock|core-utils|hardhat-deploy-config)"
dependencies
:
'
(contracts|contracts-bedrock|core-utils|hardhat-deploy-config)'
requires
:
-
yarn-monorepo
-
js-lint-test
:
name
:
dtl-tests
coverage_flag
:
dtl-tests
package_name
:
data-transport-layer
dependencies
:
"
(common-ts|contracts|core-utils)"
dependencies
:
'
(common-ts|contracts|core-utils)'
requires
:
-
yarn-monorepo
-
js-lint-test
:
name
:
chain-mon-tests
coverage_flag
:
chain-mon-tests
package_name
:
chain-mon
dependencies
:
"
(common-ts|contracts-periphery|core-utils|sdk)"
dependencies
:
'
(common-ts|contracts-periphery|core-utils|sdk)'
requires
:
-
yarn-monorepo
-
js-lint-test
:
name
:
fault-detector-tests
coverage_flag
:
fault-detector-tests
package_name
:
fault-detector
dependencies
:
"
(common-ts|contracts|core-utils|sdk)"
dependencies
:
'
(common-ts|contracts|core-utils|sdk)'
requires
:
-
yarn-monorepo
-
js-lint-test
:
name
:
message-relayer-tests
coverage_flag
:
message-relayer-tests
package_name
:
message-relayer
dependencies
:
"
(common-ts|core-utils|sdk)"
dependencies
:
'
(common-ts|core-utils|sdk)'
requires
:
-
yarn-monorepo
-
js-lint-test
:
name
:
replica-healthcheck-tests
coverage_flag
:
replica-healthcheck-tests
package_name
:
replica-healthcheck
dependencies
:
"
(common-ts|core-utils)"
dependencies
:
'
(common-ts|core-utils)'
requires
:
-
yarn-monorepo
-
sdk-next-tests
:
...
...
@@ -1136,7 +1148,7 @@ workflows:
name
:
sdk-tests
coverage_flag
:
sdk-tests
package_name
:
sdk
dependencies
:
"
(contracts|core-utils)"
dependencies
:
'
(contracts|core-utils)'
requires
:
-
yarn-monorepo
-
js-lint-test
:
...
...
@@ -1239,11 +1251,11 @@ workflows:
-
go-e2e-test
:
name
:
op-e2e-WS-tests
module
:
op-e2e
use_http
:
"
false"
use_http
:
'
false'
-
go-e2e-test
:
name
:
op-e2e-HTTP-tests
module
:
op-e2e
use_http
:
"
true"
use_http
:
'
true'
-
bedrock-go-tests
:
requires
:
-
op-batcher-lint
...
...
@@ -1276,7 +1288,7 @@ workflows:
docker_tags
:
<<pipeline.git.revision>>,<<pipeline.git.branch>>
context
:
-
oplabs-gcr
platforms
:
"
linux/amd64,linux/arm64"
platforms
:
'
linux/amd64,linux/arm64'
-
docker-build
:
name
:
op-batcher-docker-build
docker_file
:
op-batcher/Dockerfile
...
...
@@ -1290,7 +1302,7 @@ workflows:
docker_tags
:
<<pipeline.git.revision>>,<<pipeline.git.branch>>
context
:
-
oplabs-gcr
platforms
:
"
linux/amd64,linux/arm64"
platforms
:
'
linux/amd64,linux/arm64'
-
docker-build
:
name
:
op-proposer-docker-build
docker_file
:
op-proposer/Dockerfile
...
...
@@ -1304,7 +1316,7 @@ workflows:
docker_tags
:
<<pipeline.git.revision>>,<<pipeline.git.branch>>
context
:
-
oplabs-gcr
platforms
:
"
linux/amd64,linux/arm64"
platforms
:
'
linux/amd64,linux/arm64'
-
docker-build
:
name
:
op-heartbeat-docker-build
docker_file
:
op-heartbeat/Dockerfile
...
...
@@ -1383,7 +1395,7 @@ workflows:
docker_name
:
op-node
docker_tags
:
<<pipeline.git.revision>>,<<pipeline.git.branch>>
docker_context
:
.
platforms
:
"
linux/amd64,linux/arm64"
platforms
:
'
linux/amd64,linux/arm64'
context
:
-
oplabs-gcr-release
requires
:
...
...
@@ -1399,7 +1411,7 @@ workflows:
docker_name
:
op-batcher
docker_tags
:
<<pipeline.git.revision>>,<<pipeline.git.branch>>
docker_context
:
.
platforms
:
"
linux/amd64,linux/arm64"
platforms
:
'
linux/amd64,linux/arm64'
context
:
-
oplabs-gcr-release
requires
:
...
...
@@ -1415,7 +1427,7 @@ workflows:
docker_name
:
op-proposer
docker_tags
:
<<pipeline.git.revision>>,<<pipeline.git.branch>>
docker_context
:
.
platforms
:
"
linux/amd64,linux/arm64"
platforms
:
'
linux/amd64,linux/arm64'
context
:
-
oplabs-gcr-release
requires
:
...
...
packages/sdk/src/cross-chain-messenger.ts
View file @
6c87ec92
...
...
@@ -68,6 +68,7 @@ import {
migratedWithdrawalGasLimit
,
DEPOSIT_CONFIRMATION_BLOCKS
,
CHAIN_BLOCK_TIMES
,
hashMessageHash
,
}
from
'
./utils
'
export
class
CrossChainMessenger
{
...
...
@@ -351,14 +352,12 @@ export class CrossChainMessenger {
}
}
const
minGasLimit
=
migratedWithdrawalGasLimit
(
resolved
.
message
)
return
{
...
resolved
,
value
,
minGasLimit
,
minGasLimit
:
BigNumber
.
from
(
0
)
,
messageNonce
:
encodeVersionedNonce
(
BigNumber
.
from
(
1
),
BigNumber
.
from
(
0
),
resolved
.
messageNonce
),
}
...
...
@@ -388,13 +387,23 @@ export class CrossChainMessenger {
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
// 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
)
gasLimit
=
migratedWithdrawalGasLimit
(
encoded
)
messageNonce
=
resolved
.
messageNonce
}
else
{
const
receipt
=
await
this
.
l2Provider
.
getTransactionReceipt
(
...
...
@@ -433,14 +442,7 @@ export class CrossChainMessenger {
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
),
message
:
encoded
,
}
}
...
...
@@ -1360,12 +1362,8 @@ export class CrossChainMessenger {
}
const
withdrawal
=
await
this
.
toLowLevelMessage
(
resolved
)
const
messageSlot
=
ethers
.
utils
.
keccak256
(
ethers
.
utils
.
defaultAbiCoder
.
encode
(
[
'
bytes32
'
,
'
uint256
'
],
[
hashLowLevelMessage
(
withdrawal
),
ethers
.
constants
.
HashZero
]
)
)
const
hash
=
hashLowLevelMessage
(
withdrawal
)
const
messageSlot
=
hashMessageHash
(
hash
)
const
stateTrieProof
=
await
makeStateTrieProof
(
this
.
l2Provider
as
ethers
.
providers
.
JsonRpcProvider
,
...
...
@@ -1465,9 +1463,8 @@ export class CrossChainMessenger {
overrides
?:
Overrides
}
):
Promise
<
TransactionResponse
>
{
return
(
opts
?.
signer
||
this
.
l1Signer
).
sendTransaction
(
await
this
.
populateTransaction
.
proveMessage
(
message
,
opts
)
)
const
tx
=
await
this
.
populateTransaction
.
proveMessage
(
message
,
opts
)
return
(
opts
?.
signer
||
this
.
l1Signer
).
sendTransaction
(
tx
)
}
/**
...
...
@@ -1771,7 +1768,8 @@ export class CrossChainMessenger {
const
withdrawal
=
await
this
.
toLowLevelMessage
(
resolved
)
const
proof
=
await
this
.
getBedrockMessageProof
(
resolved
)
return
this
.
contracts
.
l1
.
OptimismPortal
.
populateTransaction
.
proveWithdrawalTransaction
(
const
args
=
[
[
withdrawal
.
messageNonce
,
withdrawal
.
sender
,
...
...
@@ -1788,7 +1786,11 @@ export class CrossChainMessenger {
proof
.
outputRootProof
.
latestBlockhash
,
],
proof
.
withdrawalProof
,
opts
?.
overrides
||
{}
opts
?.
overrides
||
{},
]
as
const
return
this
.
contracts
.
l1
.
OptimismPortal
.
populateTransaction
.
proveWithdrawalTransaction
(
...
args
)
},
...
...
packages/sdk/src/cross-chain-messenger.with-logs.ts
deleted
100644 → 0
View file @
a00e9eb4
/* 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
)
)
},
}
}
packages/sdk/src/utils/message-utils.ts
View file @
6c87ec92
import
{
hashWithdrawal
}
from
'
@eth-optimism/core-utils
'
import
{
BigNumber
,
utils
}
from
'
ethers
'
import
{
BigNumber
,
utils
,
ethers
}
from
'
ethers
'
import
{
LowLevelMessage
}
from
'
../interfaces
'
...
...
@@ -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.
*/
...
...
packages/sdk/test-next/proveMessage.spec.ts
View file @
6c87ec92
...
...
@@ -99,8 +99,10 @@ describe('prove message', () => {
expect
(
txReceipt
).
toBeDefined
()
expect
(
await
crossChainMessenger
.
proveMessage
(
txWithdrawalHash
)
).
toMatchInlineSnapshot
()
const
tx
=
await
crossChainMessenger
.
proveMessage
(
txWithdrawalHash
)
const
receipt
=
await
tx
.
wait
()
// A 1 means the transaction was successful
expect
(
receipt
.
status
).
toBe
(
1
)
},
20
_000
)
})
packages/sdk/test/utils/message-utils.spec.ts
View file @
6c87ec92
import
{
BigNumber
}
from
'
ethers
'
import
{
expect
}
from
'
../setup
'
import
{
migratedWithdrawalGasLimit
}
from
'
../../src/utils/message-utils
'
import
{
migratedWithdrawalGasLimit
,
hashLowLevelMessage
,
hashMessageHash
,
}
from
'
../../src/utils/message-utils
'
describe
(
'
Message Utils
'
,
()
=>
{
describe
(
'
migratedWithdrawalGasLimit
'
,
()
=>
{
...
...
@@ -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
)
}
})
})
})
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment