Skip to content

Instantly share code, notes, and snippets.

@anegg0
Created January 18, 2025 22:47
Show Gist options
  • Save anegg0/4dff314ee6599ea71b3e01ae81ba3652 to your computer and use it in GitHub Desktop.
Save anegg0/4dff314ee6599ea71b3e01ae81ba3652 to your computer and use it in GitHub Desktop.
Directory structure:
└── offchainlabs-arbitrum-sdk/
├── LICENSE
├── audit-ci.jsonc
├── hardhat.config.ts
├── docs/
└── packages/
├── ethers-viem-compat/
└── sdk/
├── typedoc_md.js
├── .eslintignore
├── .eslintrc
├── .prettierignore
├── .prettierrc.js
├── scripts/
│ ├── genAbi.ts
│ └── genNetwork.ts
├── src/
│ ├── index.ts
│ └── lib/
│ ├── abi-bold/
│ │ ├── BoldRollupUserLogic.ts
│ │ └── factories/
│ │ └── BoldRollupUserLogic__factory.ts
│ ├── assetBridger/
│ │ ├── assetBridger.ts
│ │ ├── erc20Bridger.ts
│ │ ├── ethBridger.ts
│ │ └── l1l3Bridger.ts
│ ├── dataEntities/
│ │ ├── address.ts
│ │ ├── constants.ts
│ │ ├── errors.ts
│ │ ├── event.ts
│ │ ├── message.ts
│ │ ├── networks.ts
│ │ ├── retryableData.ts
│ │ ├── rpc.ts
│ │ ├── signerOrProvider.ts
│ │ └── transactionRequest.ts
│ ├── inbox/
│ │ └── inbox.ts
│ ├── message/
│ │ ├── ChildToParentMessage.ts
│ │ ├── ChildToParentMessageClassic.ts
│ │ ├── ChildToParentMessageNitro.ts
│ │ ├── ChildTransaction.ts
│ │ ├── ParentToChildMessage.ts
│ │ ├── ParentToChildMessageCreator.ts
│ │ ├── ParentToChildMessageGasEstimator.ts
│ │ ├── ParentTransaction.ts
│ │ └── messageDataParser.ts
│ └── utils/
│ ├── arbProvider.ts
│ ├── byte_serialize_params.ts
│ ├── calldata.ts
│ ├── env.ts
│ ├── eventFetcher.ts
│ ├── lib.ts
│ ├── multicall.ts
│ └── types.ts
└── tests/
================================================
File: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
File: audit-ci.jsonc
================================================
{
"$schema": "https://github.com/IBM/audit-ci/raw/main/docs/schema.json",
"low": true,
"allowlist": [
// Open Zepplin
////////////
// https://github.com/advisories/GHSA-4g63-c64m-25w9
// OpenZeppelin Contracts's SignatureChecker may revert on invalid EIP-1271 signers
// We dont use EIP-1271
"GHSA-4g63-c64m-25w9",
// https://github.com/advisories/GHSA-qh9x-gcfh-pcrw
// OpenZeppelin Contracts's ERC165Checker may revert instead of returning false
// We don't use ERC165Checker
"GHSA-qh9x-gcfh-pcrw",
// https://github.com/advisories/GHSA-7grf-83vw-6f5x
// OpenZeppelin Contracts ERC165Checker unbounded gas consumption
// We don't use ERC165Checker
"GHSA-7grf-83vw-6f5x",
// https://github.com/advisories/GHSA-xrc4-737v-9q75
// OpenZeppelin Contracts's GovernorVotesQuorumFraction updates to quorum may affect past defeated proposals
// We don't use GovernorVotesQuorumFraction
"GHSA-xrc4-737v-9q75",
// https://github.com/advisories/GHSA-4h98-2769-gh6h
// OpenZeppelin Contracts vulnerable to ECDSA signature malleability
// We don’t use signatures for replay protection anywhere
"GHSA-4h98-2769-gh6h",
// https://github.com/advisories/GHSA-mx2q-35m2-x2rh
// OpenZeppelin Contracts TransparentUpgradeableProxy clashing selector calls may not be delegated
// from: @arbitrum/nitro-contracts>@openzeppelin/contracts-upgradeable
// from: arb-bridge-peripherals>@openzeppelin/contracts-upgradeable
// from: arb-bridge-peripherals>arb-bridge-eth>@openzeppelin/contracts-upgradeable
// from: @arbitrum/nitro-contracts>@openzeppelin/contracts
// from: arb-bridge-peripherals>@openzeppelin/contracts
// from: arb-bridge-peripherals>arb-bridge-eth>@openzeppelin/contracts
// Clashing selector between proxy and implementation can only be caused deliberately
"GHSA-mx2q-35m2-x2rh",
// https://github.com/advisories/GHSA-93hq-5wgc-jc82
// GovernorCompatibilityBravo may trim proposal calldata
// from: @arbitrum/nitro-contracts>@openzeppelin/contracts-upgradeable
// from: @arbitrum/nitro-contracts>@openzeppelin/contracts
// We don't use GovernorCompatibilityBravo
"GHSA-93hq-5wgc-jc82",
// https://github.com/advisories/GHSA-5h3x-9wvq-w4m2
// OpenZeppelin Contracts's governor proposal creation may be blocked by frontrunning
// from: @arbitrum/nitro-contracts>@openzeppelin/contracts-upgradeable
// from: @arbitrum/nitro-contracts>@openzeppelin/contracts
// We don't use Governor or GovernorCompatibilityBravo
"GHSA-5h3x-9wvq-w4m2",
// https://github.com/advisories/GHSA-g4vp-m682-qqmp
// OpenZeppelin Contracts vulnerable to Improper Escaping of Output
// from: @arbitrum/nitro-contracts>@openzeppelin/contracts-upgradeable
// from @arbitrum/nitro-contracts>@openzeppelin/contracts
// We don't use ERC2771Context
"GHSA-g4vp-m682-qqmp",
// https://github.com/advisories/GHSA-wprv-93r4-jj2p
// OpenZeppelin Contracts using MerkleProof multiproofs may allow proving arbitrary leaves for specific trees
// we don't use oz/merkle-trees anywhere
// from @arbitrum/nitro-contracts>@offchainlabs/upgrade-executor>@openzeppelin/contracts-upgradeable
// from @arbitrum/nitro-contracts>@offchainlabs/upgrade-executor>@openzeppelin/contracts
"GHSA-wprv-93r4-jj2p",
// https://github.com/advisories/GHSA-3787-6prv-h9w3
// Undici proxy-authorization header not cleared on cross-origin redirect in fetch
"GHSA-3787-6prv-h9w3",
// https://github.com/advisories/GHSA-699g-q6qh-q4v8
// OpenZeppelin Contracts and Contracts Upgradeable duplicated execution of subcalls in v4.9.4
// from: @offchainlabs/l1-l3-teleport-contracts>@openzeppelin/contracts
"GHSA-699g-q6qh-q4v8",
// https://github.com/advisories/GHSA-9vx6-7xxf-x967
// OpenZeppelin Contracts base64 encoding may read from potentially dirty memory
// we don't use the base64 functions
// from: @arbitrum/nitro-contracts>@openzeppelin/contracts-upgradeable
// from: @arbitrum/token-bridge-contracts>@openzeppelin/contracts-upgradeable
// from: @arbitrum/nitro-contracts>@openzeppelin/contracts
// from: @arbitrum/token-bridge-contracts>@openzeppelin/contracts
"GHSA-9vx6-7xxf-x967",
// https://github.com/advisories/GHSA-cxjh-pqwp-8mfp
// axios can leak auth headers when using `Proxy-Authentication` header. We do not use that header.
// from: axios>follow-redirects
// from: hardhat>solc>follow-redirects
"GHSA-cxjh-pqwp-8mfp",
// https://github.com/advisories/GHSA-9qxr-qj54-h672
// Undici's fetch with integrity option is too lax when algorithm is specified but hash value is incorrect
// hardhat requests are only done during development
// from: hardhat>undici
"GHSA-9qxr-qj54-h672",
// https://github.com/advisories/GHSA-m4v8-wqvr-p9f7
// Undici's Proxy-Authorization header not cleared on cross-origin redirect for dispatch, request, stream, pipeline
// hardhat requests are only done during development
// from: hardhat>undici
"GHSA-m4v8-wqvr-p9f7",
// https://github.com/advisories/GHSA-grv7-fg5c-xmjg
// Uncontrolled resource consumption in braces
// eslint and hardhat dependency, only used in dev
// from: hardhat>braces & eslint>braces
"GHSA-grv7-fg5c-xmjg",
// https://github.com/advisories/GHSA-3h5v-q93c-6h6q
// Exposure of Sensitive Information in ws
// Issue with sol2uml library that generates UML diagrams from Solidity code. Only used at build time.
// from: @offchainlabs/l1-l3-teleport-contracts>@arbitrum/nitro-contracts>sol2uml>convert-svg-to-png>convert-svg-core>puppeteer>ws
// from: @offchainlabs/l1-l3-teleport-contracts>@arbitrum/token-bridge-contracts>@arbitrum/nitro-contracts>sol2uml>convert-svg-to-png>convert-svg-core>puppeteer>ws
"GHSA-3h5v-q93c-6h6q",
// https://github.com/advisories/GHSA-wf5p-g6vw-rhxx
// Axios: Server-Side Request Forgery vulnerability
// Issue with sol2uml library that generates UML diagrams from Solidity code. Only used at build time.
// from: @offchainlabs/l1-l3-teleport-contracts>@arbitrum/nitro-contracts>sol2uml>axios
// from: @offchainlabs/l1-l3-teleport-contracts>@arbitrum/token-bridge-contracts>@arbitrum/nitro-contracts>sol2uml>axios
"GHSA-wf5p-g6vw-rhxx",
// https://github.com/advisories/GHSA-3xgq-45jj-v275
// cross-spawn command injection vulnerability
// Only used during development via audit-ci, nyc, and patch-package
// from: audit-ci>cross-spawn
// from: nyc>foreground-child>cross-spawn
// from: nyc>spawn-wrap>foreground-child>cross-spawn
// from: @arbitrum/nitro-contracts>patch-package>cross-spawn
// from: @arbitrum/token-bridge-contracts>@arbitrum/nitro-contracts>patch-package>cross-spawn
// from: @offchainlabs/l1-l3-teleport-contracts>@arbitrum/token-bridge-contracts>@arbitrum/nitro-contracts>patch-package>cross-spawn
"GHSA-3xgq-45jj-v275",
// https://github.com/advisories/GHSA-mwcw-c2x4-8c55
// nanoid infinite loop vulnerability when handling non-integer values
// Only used by mocha for test file IDs during test execution, not in production code
// from: hardhat>mocha>nanoid
// from: mocha>nanoid
"GHSA-mwcw-c2x4-8c55"
]
}
================================================
File: hardhat.config.ts
================================================
import '@nomiclabs/hardhat-ethers'
import dotenv from 'dotenv'
dotenv.config()
const config = {
defaultNetwork: 'hardhat',
paths: {
artifacts: 'build/contracts',
},
solidity: {
compilers: [
{
version: '0.6.11',
settings: {
optimizer: {
enabled: true,
runs: 100,
},
},
},
{
version: '0.8.7',
settings: {
optimizer: {
enabled: true,
runs: 100,
},
},
},
],
},
networks: {
hardhat: {
chainId: 1337,
throwOnTransactionFailures: true,
allowUnlimitedContractSize: true,
accounts: {
accountsBalance: '1000000000000000000000000000',
},
blockGasLimit: 200000000,
// mining: {
// auto: false,
// interval: 1000,
// },
forking: {
url: 'https://mainnet.infura.io/v3/' + process.env['INFURA_KEY'],
enabled: process.env['SHOULD_FORK'] === '1',
},
},
},
spdxLicenseIdentifier: {
overwrite: false,
runOnCompile: true,
},
namedAccounts: {
deployer: {
default: 0,
},
},
mocha: {
timeout: 30000000,
bail: true,
},
}
module.exports = config
================================================
File: packages/sdk/typedoc_md.js
================================================
// References:
// - https://typedoc.org/guides/overview/
// - https://github.com/tgreyuk/typedoc-plugin-markdown
/** @type {import('typedoc').TypeDocOptions} */
module.exports = {
// Input options
entryPoints: ['./src/lib'],
entryPointStrategy: 'expand',
exclude: ['./src/lib/abi'],
excludeNotDocumented: true,
excludeInternal: true,
// Output options
out: 'docs',
// Plugins
plugin: ['typedoc-plugin-markdown'],
// typedoc-plugin-markdown options
// entryDocument: 'modules.md',
hideBreadcrumbs: true,
hideInPageTOC: true,
hideMembersSymbol: true,
}
================================================
File: packages/sdk/.eslintignore
================================================
dist/**
node_modules/**
coverage/**
src/lib/abi
docs/**
================================================
File: packages/sdk/.eslintrc
================================================
{
"root": false,
"extends": ["../../.eslintrc.js"],
"parserOptions": {
"files": ["src/**/*.ts", "src/**/*.js"]
},
"ignorePatterns": ["dist/**/*", "node_modules/**/*"]
}
================================================
File: packages/sdk/.prettierignore
================================================
build/**
cache/**
dist/**
src/lib/abi/**
.nyc_output
================================================
File: packages/sdk/.prettierrc.js
================================================
const baseConfig = require('../../.prettierrc.js')
module.exports = {
...baseConfig,
}
================================================
File: packages/sdk/scripts/genAbi.ts
================================================
import { runTypeChain, glob } from 'typechain'
import { execSync } from 'child_process'
import { unlinkSync, rmSync } from 'fs'
import * as path from 'path'
const ABI_PATH = path.resolve(__dirname, '../src/lib/abi')
const getPackagePath = (packageName: string): string => {
const path = require.resolve(`${packageName}/package.json`)
return path.substr(0, path.indexOf('package.json'))
}
async function main() {
console.log('Removing previously generated ABIs.\n')
rmSync(`${ABI_PATH}`, { recursive: true, force: true })
rmSync(`${ABI_PATH}/classic`, { recursive: true, force: true })
const cwd = process.cwd()
const nitroPath = getPackagePath('@arbitrum/nitro-contracts')
const tokenBridgePath = getPackagePath('@arbitrum/token-bridge-contracts')
const teleporterPath = getPackagePath(
'@offchainlabs/l1-l3-teleport-contracts'
)
console.log('Compiling paths.')
const npmExec = process.env['npm_execpath']
if (!npmExec || npmExec === '')
throw new Error(
'No support for npm_execpath env variable in package manager'
)
// TODO: use `HARDHAT_ARTIFACT_PATH` to write files to arbitrum sdk instead of the packages themselves.
// this is currently broken since hardhat throws a weird error:
// `Error HH702: Invalid artifact path [...] its correct case-sensitive path is...`
// https://yarnpkg.com/advanced/rulebook#packages-should-never-write-inside-their-own-folder-outside-of-postinstall
// instead of writing in postinstall in each of those packages, we should target a local folder in sdk's postinstall
console.log('building @arbitrum/nitro-contracts')
execSync(`${npmExec} run build`, { cwd: nitroPath })
console.log('building @arbitrum/token-bridge-contracts')
execSync(`${npmExec} run build`, { cwd: tokenBridgePath })
console.log('building @offchainlabs/l1-l3-teleport-contracts')
execSync(`${npmExec} run build`, {
cwd: teleporterPath,
})
console.log('Done compiling')
const nitroFiles = glob(cwd, [
`${tokenBridgePath}/build/contracts/!(build-info)/**/+([a-zA-Z0-9_]).json`,
`${nitroPath}/build/contracts/!(build-info)/**/+([a-zA-Z0-9_]).json`,
`${teleporterPath}/build/contracts/!(build-info)/**/+([a-zA-Z0-9_]).json`,
])
// TODO: generate files into different subfolders (ie `/nitro/*`) to avoid overwrite of contracts with the same name
await runTypeChain({
cwd,
filesToProcess: nitroFiles,
allFiles: nitroFiles,
outDir: `${ABI_PATH}`,
target: 'ethers-v5',
})
const classicFiles = glob(cwd, [
// we have a hardcoded abi for the old outbox
`./src/lib/dataEntities/Outbox.json`,
])
await runTypeChain({
cwd,
filesToProcess: classicFiles,
allFiles: classicFiles,
outDir: `${ABI_PATH}/classic`,
target: 'ethers-v5',
})
// we delete the index file since it doesn't play well with tree shaking
unlinkSync(`${ABI_PATH}/index.ts`)
unlinkSync(`${ABI_PATH}/classic/index.ts`)
console.log('Typechain generated')
}
main()
.then(() => console.log('Done.'))
.catch(console.error)
================================================
File: packages/sdk/scripts/genNetwork.ts
================================================
import { loadEnv } from '../src/lib/utils/env'
import { execSync } from 'child_process'
import * as fs from 'fs'
import { IERC20Bridge__factory } from '../src/lib/abi/factories/IERC20Bridge__factory'
import { ethers } from 'ethers'
import {
L2Network,
ArbitrumNetwork,
mapL2NetworkToArbitrumNetwork,
} from '../src/lib/dataEntities/networks'
loadEnv()
const isTestingOrbitChains = process.env.ORBIT_TEST === '1'
function getLocalNetworksFromContainer(which: 'l1l2' | 'l2l3'): any {
const dockerNames = [
'nitro_sequencer_1',
'nitro-sequencer-1',
'nitro-testnode-sequencer-1',
'nitro-testnode_sequencer_1',
]
for (const dockerName of dockerNames) {
try {
return JSON.parse(
execSync(
`docker exec ${dockerName} cat /tokenbridge-data/${which}_network.json`
).toString()
)
} catch {
// empty on purpose
}
}
throw new Error('nitro-testnode sequencer not found')
}
/**
* the container's files are written by the token bridge deployment step of the test node, which runs a script in token-bridge-contracts.
* once the script in token-bridge-contracts repo uses an sdk version with the same types and is updated to populate those fields,
* we can remove this patchwork
*/
async function patchNetworks(
l2Network: L2Network,
l3Network: L2Network | undefined,
l2Provider: ethers.providers.Provider | undefined
): Promise<{
patchedL2Network: ArbitrumNetwork
patchedL3Network?: ArbitrumNetwork
}> {
const patchedL2Network = mapL2NetworkToArbitrumNetwork(l2Network)
// native token for l3
if (l3Network && l2Provider) {
const patchedL3Network = mapL2NetworkToArbitrumNetwork(l3Network)
try {
patchedL3Network.nativeToken = await IERC20Bridge__factory.connect(
l3Network.ethBridge.bridge,
l2Provider
).nativeToken()
} catch (e) {
// l3 network doesn't have a native token
}
return { patchedL2Network, patchedL3Network }
}
return { patchedL2Network }
}
async function main() {
fs.rmSync('localNetwork.json', { force: true })
let output = getLocalNetworksFromContainer('l1l2')
if (isTestingOrbitChains) {
// When running with L3 active, the container calls the L3 network L2 so we rename it here
const { l2Network: l3Network } = getLocalNetworksFromContainer('l2l3')
const { patchedL2Network, patchedL3Network } = await patchNetworks(
output.l2Network,
l3Network,
new ethers.providers.JsonRpcProvider(process.env['ARB_URL'])
)
output = {
l2Network: patchedL2Network,
l3Network: patchedL3Network,
}
} else {
const { patchedL2Network } = await patchNetworks(
output.l2Network,
undefined,
undefined
)
output.l2Network = patchedL2Network
}
fs.writeFileSync('localNetwork.json', JSON.stringify(output, null, 2))
console.log('localnetwork.json updated')
}
main()
.then(() => console.log('Done.'))
.catch(console.error)
================================================
File: packages/sdk/src/index.ts
================================================
/*
* Copyright 2019-2021, Offchain Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env node */
'use strict'
export {
EthL1L3Bridger,
EthL1L3DepositStatus,
EthL1L3DepositRequestParams,
Erc20L1L3Bridger,
Erc20L1L3DepositStatus,
Erc20L1L3DepositRequestParams,
Erc20L1L3DepositRequestRetryableOverrides,
GetL1L3DepositStatusParams,
} from './lib/assetBridger/l1l3Bridger'
export { EthBridger } from './lib/assetBridger/ethBridger'
export {
Erc20Bridger,
AdminErc20Bridger,
} from './lib/assetBridger/erc20Bridger'
export {
ChildTransactionReceipt,
ChildContractTransaction,
} from './lib/message/ChildTransaction'
export {
ChildToParentMessage,
ChildToParentMessageWriter,
ChildToParentMessageReader,
ChildToParentMessageReaderOrWriter,
ChildToParentTransactionEvent,
} from './lib/message/ChildToParentMessage'
export {
ParentEthDepositTransaction,
ParentEthDepositTransactionReceipt,
ParentContractCallTransaction,
ParentContractCallTransactionReceipt,
ParentContractTransaction,
ParentTransactionReceipt,
} from './lib/message/ParentTransaction'
export {
EthDepositMessage,
EthDepositMessageStatus,
EthDepositMessageWaitForStatusResult,
ParentToChildMessage,
ParentToChildMessageReader,
ParentToChildMessageReaderClassic,
ParentToChildMessageWriter,
ParentToChildMessageStatus,
ParentToChildMessageWaitForStatusResult,
} from './lib/message/ParentToChildMessage'
export { ParentToChildMessageGasEstimator } from './lib/message/ParentToChildMessageGasEstimator'
export { argSerializerConstructor } from './lib/utils/byte_serialize_params'
export { CallInput, MultiCaller } from './lib/utils/multicall'
export {
ArbitrumNetwork,
getArbitrumNetwork,
getArbitrumNetworks,
ArbitrumNetworkInformationFromRollup,
getArbitrumNetworkInformationFromRollup,
getChildrenForNetwork,
registerCustomArbitrumNetwork,
// deprecated, but here for backwards compatibility
L2Network,
L2NetworkTokenBridge,
mapL2NetworkToArbitrumNetwork,
mapL2NetworkTokenBridgeToTokenBridge,
} from './lib/dataEntities/networks'
export { InboxTools } from './lib/inbox/inbox'
export { EventFetcher } from './lib/utils/eventFetcher'
export { ArbitrumProvider } from './lib/utils/arbProvider'
export * as constants from './lib/dataEntities/constants'
export {
ChildToParentMessageStatus,
RetryableMessageParams,
} from './lib/dataEntities/message'
export {
RetryableData,
RetryableDataTools,
} from './lib/dataEntities/retryableData'
export { EventArgs } from './lib/dataEntities/event'
export { Address } from './lib/dataEntities/address'
export {
ParentToChildTransactionRequest,
isParentToChildTransactionRequest,
ChildToParentTransactionRequest,
isChildToParentTransactionRequest,
} from './lib/dataEntities/transactionRequest'
export {
scaleFrom18DecimalsToNativeTokenDecimals,
scaleFromNativeTokenDecimalsTo18Decimals,
} from './lib/utils/lib'
================================================
File: packages/sdk/src/lib/assetBridger/assetBridger.ts
================================================
/*
* Copyright 2021, Offchain Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env node */
'use strict'
import { ParentContractTransaction } from '../message/ParentTransaction'
import { ChildContractTransaction } from '../message/ChildTransaction'
import {
ArbitrumNetwork,
isArbitrumNetworkNativeTokenEther,
} from '../dataEntities/networks'
import {
SignerOrProvider,
SignerProviderUtils,
} from '../dataEntities/signerOrProvider'
/**
* Base for bridging assets from parent-to-child and back
*/
export abstract class AssetBridger<DepositParams, WithdrawParams> {
/**
* In case of a chain that uses ETH as its native/gas token, this is either `undefined` or the zero address
*
* In case of a chain that uses an ERC-20 token from the parent network as its native/gas token, this is the address of said token on the parent network
*/
public readonly nativeToken?: string
public constructor(public readonly childNetwork: ArbitrumNetwork) {
this.nativeToken = childNetwork.nativeToken
}
/**
* Check the signer/provider matches the parent network, throws if not
* @param sop
*/
protected async checkParentNetwork(sop: SignerOrProvider): Promise<void> {
await SignerProviderUtils.checkNetworkMatches(
sop,
this.childNetwork.parentChainId
)
}
/**
* Check the signer/provider matches the child network, throws if not
* @param sop
*/
protected async checkChildNetwork(sop: SignerOrProvider): Promise<void> {
await SignerProviderUtils.checkNetworkMatches(
sop,
this.childNetwork.chainId
)
}
/**
* Whether the chain uses ETH as its native/gas token
* @returns {boolean}
*/
protected get nativeTokenIsEth() {
return isArbitrumNetworkNativeTokenEther(this.childNetwork)
}
/**
* Transfer assets from parent-to-child
* @param params
*/
public abstract deposit(
params: DepositParams
): Promise<ParentContractTransaction>
/**
* Transfer assets from child-to-parent
* @param params
*/
public abstract withdraw(
params: WithdrawParams
): Promise<ChildContractTransaction>
}
================================================
File: packages/sdk/src/lib/assetBridger/erc20Bridger.ts
================================================
/*
* Copyright 2021, Offchain Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env node */
'use strict'
import { Signer } from '@ethersproject/abstract-signer'
import {
Provider,
BlockTag,
TransactionRequest,
} from '@ethersproject/abstract-provider'
import { PayableOverrides, Overrides } from '@ethersproject/contracts'
import { MaxUint256 } from '@ethersproject/constants'
import { ErrorCode, Logger } from '@ethersproject/logger'
import { BigNumber, BigNumberish, ethers, BytesLike, constants } from 'ethers'
import { L1GatewayRouter__factory } from '../abi/factories/L1GatewayRouter__factory'
import { L2GatewayRouter__factory } from '../abi/factories/L2GatewayRouter__factory'
import { L1WethGateway__factory } from '../abi/factories/L1WethGateway__factory'
import { L2ArbitrumGateway__factory } from '../abi/factories/L2ArbitrumGateway__factory'
import { ERC20__factory } from '../abi/factories/ERC20__factory'
import { ERC20 } from '../abi/ERC20'
import { L2GatewayToken__factory } from '../abi/factories/L2GatewayToken__factory'
import { L2GatewayToken } from '../abi/L2GatewayToken'
import { ICustomToken__factory } from '../abi/factories/ICustomToken__factory'
import { IArbToken__factory } from '../abi/factories/IArbToken__factory'
import { WithdrawalInitiatedEvent } from '../abi/L2ArbitrumGateway'
import { GatewaySetEvent } from '../abi/L1GatewayRouter'
import {
GasOverrides,
ParentToChildMessageGasEstimator,
} from '../message/ParentToChildMessageGasEstimator'
import { SignerProviderUtils } from '../dataEntities/signerOrProvider'
import {
ArbitrumNetwork,
TokenBridge,
assertArbitrumNetworkHasTokenBridge,
getArbitrumNetwork,
} from '../dataEntities/networks'
import { ArbSdkError, MissingProviderArbSdkError } from '../dataEntities/errors'
import { DISABLED_GATEWAY } from '../dataEntities/constants'
import { EventFetcher } from '../utils/eventFetcher'
import { EthDepositParams, EthWithdrawParams } from './ethBridger'
import { AssetBridger } from './assetBridger'
import {
ParentContractCallTransaction,
ParentContractTransaction,
ParentTransactionReceipt,
} from '../message/ParentTransaction'
import {
ChildContractTransaction,
ChildTransactionReceipt,
} from '../message/ChildTransaction'
import {
isParentToChildTransactionRequest,
isChildToParentTransactionRequest,
ChildToParentTransactionRequest,
ParentToChildTransactionRequest,
} from '../dataEntities/transactionRequest'
import { defaultAbiCoder } from 'ethers/lib/utils'
import { OmitTyped, RequiredPick } from '../utils/types'
import { RetryableDataTools } from '../dataEntities/retryableData'
import { EventArgs } from '../dataEntities/event'
import { ParentToChildMessageGasParams } from '../message/ParentToChildMessageCreator'
import {
getNativeTokenDecimals,
isArbitrumChain,
scaleFrom18DecimalsToNativeTokenDecimals,
} from '../utils/lib'
import { L2ERC20Gateway__factory } from '../abi/factories/L2ERC20Gateway__factory'
import { getErc20ParentAddressFromParentToChildTxRequest } from '../utils/calldata'
export interface TokenApproveParams {
/**
* Parent network address of the ERC20 token contract
*/
erc20ParentAddress: string
/**
* Amount to approve. Defaults to max int.
*/
amount?: BigNumber
/**
* Transaction overrides
*/
overrides?: PayableOverrides
}
export interface Erc20DepositParams extends EthDepositParams {
/**
* A child provider
*/
childProvider: Provider
/**
* Parent network address of the token ERC20 contract
*/
erc20ParentAddress: string
/**
* Child network address of the entity receiving the funds. Defaults to the l1FromAddress
*/
destinationAddress?: string
/**
* The maximum cost to be paid for submitting the transaction
*/
maxSubmissionCost?: BigNumber
/**
* The address to return the any gas that was not spent on fees
*/
excessFeeRefundAddress?: string
/**
* The address to refund the call value to in the event the retryable is cancelled, or expires
*/
callValueRefundAddress?: string
/**
* Overrides for the retryable ticket parameters
*/
retryableGasOverrides?: GasOverrides
/**
* Transaction overrides
*/
overrides?: Overrides
}
export interface Erc20WithdrawParams extends EthWithdrawParams {
/**
* Parent network address of the token ERC20 contract
*/
erc20ParentAddress: string
}
export type ParentToChildTxReqAndSignerProvider =
ParentToChildTransactionRequest & {
parentSigner: Signer
childProvider: Provider
overrides?: Overrides
}
export type ChildToParentTxReqAndSigner = ChildToParentTransactionRequest & {
childSigner: Signer
overrides?: Overrides
}
type SignerTokenApproveParams = TokenApproveParams & { parentSigner: Signer }
type ProviderTokenApproveParams = TokenApproveParams & {
parentProvider: Provider
}
export type ApproveParamsOrTxRequest =
| SignerTokenApproveParams
| {
txRequest: Required<Pick<TransactionRequest, 'to' | 'data' | 'value'>>
parentSigner: Signer
overrides?: Overrides
}
/**
* The deposit request takes the same args as the actual deposit. Except we don't require a signer object
* only a provider
*/
type DepositRequest = OmitTyped<
Erc20DepositParams,
'overrides' | 'parentSigner'
> & {
parentProvider: Provider
/**
* Address that is depositing the assets
*/
from: string
}
type DefaultedDepositRequest = RequiredPick<
DepositRequest,
'callValueRefundAddress' | 'excessFeeRefundAddress' | 'destinationAddress'
>
/**
* Bridger for moving ERC20 tokens back and forth between parent-to-child
*/
export class Erc20Bridger extends AssetBridger<
Erc20DepositParams | ParentToChildTxReqAndSignerProvider,
OmitTyped<Erc20WithdrawParams, 'from'> | ChildToParentTransactionRequest
> {
public static MAX_APPROVAL: BigNumber = MaxUint256
public static MIN_CUSTOM_DEPOSIT_GAS_LIMIT = BigNumber.from(275000)
public readonly childNetwork: ArbitrumNetwork & {
tokenBridge: TokenBridge
}
/**
* Bridger for moving ERC20 tokens back and forth between parent-to-child
*/
public constructor(childNetwork: ArbitrumNetwork) {
super(childNetwork)
assertArbitrumNetworkHasTokenBridge(childNetwork)
this.childNetwork = childNetwork
}
/**
* Instantiates a new Erc20Bridger from a child provider
* @param childProvider
* @returns
*/
public static async fromProvider(childProvider: Provider) {
return new Erc20Bridger(await getArbitrumNetwork(childProvider))
}
/**
* Get the address of the parent gateway for this token
* @param erc20ParentAddress
* @param parentProvider
* @returns
*/
public async getParentGatewayAddress(
erc20ParentAddress: string,
parentProvider: Provider
): Promise<string> {
await this.checkParentNetwork(parentProvider)
return await L1GatewayRouter__factory.connect(
this.childNetwork.tokenBridge.parentGatewayRouter,
parentProvider
).getGateway(erc20ParentAddress)
}
/**
* Get the address of the child gateway for this token
* @param erc20ParentAddress
* @param childProvider
* @returns
*/
public async getChildGatewayAddress(
erc20ParentAddress: string,
childProvider: Provider
): Promise<string> {
await this.checkChildNetwork(childProvider)
return await L2GatewayRouter__factory.connect(
this.childNetwork.tokenBridge.childGatewayRouter,
childProvider
).getGateway(erc20ParentAddress)
}
/**
* Creates a transaction request for approving the custom gas token to be spent by the relevant gateway on the parent network
* @param params
*/
public async getApproveGasTokenRequest(
params: ProviderTokenApproveParams
): Promise<Required<Pick<TransactionRequest, 'to' | 'data' | 'value'>>> {
if (this.nativeTokenIsEth) {
throw new Error('chain uses ETH as its native/gas token')
}
const txRequest = await this.getApproveTokenRequest(params)
// just reuse the approve token request but direct it towards the native token contract
return { ...txRequest, to: this.nativeToken! }
}
/**
* Approves the custom gas token to be spent by the relevant gateway on the parent network
* @param params
*/
public async approveGasToken(
params: ApproveParamsOrTxRequest
): Promise<ethers.ContractTransaction> {
if (this.nativeTokenIsEth) {
throw new Error('chain uses ETH as its native/gas token')
}
await this.checkParentNetwork(params.parentSigner)
const approveGasTokenRequest = this.isApproveParams(params)
? await this.getApproveGasTokenRequest({
...params,
parentProvider: SignerProviderUtils.getProviderOrThrow(
params.parentSigner
),
})
: params.txRequest
return params.parentSigner.sendTransaction({
...approveGasTokenRequest,
...params.overrides,
})
}
/**
* Get a tx request to approve tokens for deposit to the bridge.
* The tokens will be approved for the relevant gateway.
* @param params
* @returns
*/
public async getApproveTokenRequest(
params: ProviderTokenApproveParams
): Promise<Required<Pick<TransactionRequest, 'to' | 'data' | 'value'>>> {
// you approve tokens to the gateway that the router will use
const gatewayAddress = await this.getParentGatewayAddress(
params.erc20ParentAddress,
SignerProviderUtils.getProviderOrThrow(params.parentProvider)
)
const iErc20Interface = ERC20__factory.createInterface()
const data = iErc20Interface.encodeFunctionData('approve', [
gatewayAddress,
params.amount || Erc20Bridger.MAX_APPROVAL,
])
return {
to: params.erc20ParentAddress,
data,
value: BigNumber.from(0),
}
}
protected isApproveParams(
params: ApproveParamsOrTxRequest
): params is SignerTokenApproveParams {
return (params as SignerTokenApproveParams).erc20ParentAddress != undefined
}
/**
* Approve tokens for deposit to the bridge. The tokens will be approved for the relevant gateway.
* @param params
* @returns
*/
public async approveToken(
params: ApproveParamsOrTxRequest
): Promise<ethers.ContractTransaction> {
await this.checkParentNetwork(params.parentSigner)
const approveRequest = this.isApproveParams(params)
? await this.getApproveTokenRequest({
...params,
parentProvider: SignerProviderUtils.getProviderOrThrow(
params.parentSigner
),
})
: params.txRequest
return await params.parentSigner.sendTransaction({
...approveRequest,
...params.overrides,
})
}
/**
* Get the child network events created by a withdrawal
* @param childProvider
* @param gatewayAddress
* @param parentTokenAddress
* @param fromAddress
* @param filter
* @returns
*/
public async getWithdrawalEvents(
childProvider: Provider,
gatewayAddress: string,
filter: { fromBlock: BlockTag; toBlock: BlockTag },
parentTokenAddress?: string,
fromAddress?: string,
toAddress?: string
): Promise<(EventArgs<WithdrawalInitiatedEvent> & { txHash: string })[]> {
await this.checkChildNetwork(childProvider)
const eventFetcher = new EventFetcher(childProvider)
const events = (
await eventFetcher.getEvents(
L2ArbitrumGateway__factory,
contract =>
contract.filters.WithdrawalInitiated(
null, // parentToken
fromAddress || null, // _from
toAddress || null // _to
),
{ ...filter, address: gatewayAddress }
)
).map(a => ({ txHash: a.transactionHash, ...a.event }))
return parentTokenAddress
? events.filter(
log =>
log.l1Token.toLocaleLowerCase() ===
parentTokenAddress.toLocaleLowerCase()
)
: events
}
/**
* Does the provided address look like a weth gateway
* @param potentialWethGatewayAddress
* @param parentProvider
* @returns
*/
private async looksLikeWethGateway(
potentialWethGatewayAddress: string,
parentProvider: Provider
) {
try {
const potentialWethGateway = L1WethGateway__factory.connect(
potentialWethGatewayAddress,
parentProvider
)
await potentialWethGateway.callStatic.l1Weth()
return true
} catch (err) {
if (
err instanceof Error &&
(err as unknown as { code: ErrorCode }).code ===
Logger.errors.CALL_EXCEPTION
) {
return false
} else {
throw err
}
}
}
/**
* Is this a known or unknown WETH gateway
* @param gatewayAddress
* @param parentProvider
* @returns
*/
private async isWethGateway(
gatewayAddress: string,
parentProvider: Provider
): Promise<boolean> {
const wethAddress = this.childNetwork.tokenBridge.parentWethGateway
if (this.childNetwork.isCustom) {
// For custom network, we do an ad-hoc check to see if it's a WETH gateway
if (await this.looksLikeWethGateway(gatewayAddress, parentProvider)) {
return true
}
// ...otherwise we directly check it against the config file
} else if (wethAddress === gatewayAddress) {
return true
}
return false
}
/**
* Get the child network token contract at the provided address
* Note: This function just returns a typed ethers object for the provided address, it doesn't
* check the underlying form of the contract bytecode to see if it's an erc20, and doesn't ensure the validity
* of any of the underlying functions on that contract.
* @param childProvider
* @param childTokenAddr
* @returns
*/
public getChildTokenContract(
childProvider: Provider,
childTokenAddr: string
): L2GatewayToken {
return L2GatewayToken__factory.connect(childTokenAddr, childProvider)
}
/**
* Get the parent token contract at the provided address
* Note: This function just returns a typed ethers object for the provided address, it doesnt
* check the underlying form of the contract bytecode to see if it's an erc20, and doesn't ensure the validity
* of any of the underlying functions on that contract.
* @param parentProvider
* @param parentTokenAddr
* @returns
*/
public getParentTokenContract(
parentProvider: Provider,
parentTokenAddr: string
): ERC20 {
return ERC20__factory.connect(parentTokenAddr, parentProvider)
}
/**
* Get the corresponding child network token address for the provided parent network token
* @param erc20ParentAddress
* @param parentProvider
* @returns
*/
public async getChildErc20Address(
erc20ParentAddress: string,
parentProvider: Provider
): Promise<string> {
await this.checkParentNetwork(parentProvider)
const parentGatewayRouter = L1GatewayRouter__factory.connect(
this.childNetwork.tokenBridge.parentGatewayRouter,
parentProvider
)
return await parentGatewayRouter.functions
.calculateL2TokenAddress(erc20ParentAddress)
.then(([res]) => res)
}
/**
* Get the corresponding parent network address for the provided child network token
* Validates the returned address against the child network router to ensure it is correctly mapped to the provided erc20ChildChainAddress
* @param erc20ChildChainAddress
* @param childProvider
* @returns
*/
public async getParentErc20Address(
erc20ChildChainAddress: string,
childProvider: Provider
): Promise<string> {
await this.checkChildNetwork(childProvider)
// child network WETH contract doesn't have the parentAddress method on it
if (
erc20ChildChainAddress.toLowerCase() ===
this.childNetwork.tokenBridge.childWeth.toLowerCase()
) {
return this.childNetwork.tokenBridge.parentWeth
}
const arbERC20 = L2GatewayToken__factory.connect(
erc20ChildChainAddress,
childProvider
)
const parentAddress = await arbERC20.functions
.l1Address()
.then(([res]) => res)
// check that this l1 address is indeed registered to this child token
const childGatewayRouter = L2GatewayRouter__factory.connect(
this.childNetwork.tokenBridge.childGatewayRouter,
childProvider
)
const childAddress = await childGatewayRouter.calculateL2TokenAddress(
parentAddress
)
if (childAddress.toLowerCase() !== erc20ChildChainAddress.toLowerCase()) {
throw new ArbSdkError(
`Unexpected parent address. Parent address from token is not registered to the provided child address. ${parentAddress} ${childAddress} ${erc20ChildChainAddress}`
)
}
return parentAddress
}
/**
* Whether the token has been disabled on the router
* @param parentTokenAddress
* @param parentProvider
* @returns
*/
public async isDepositDisabled(
parentTokenAddress: string,
parentProvider: Provider
): Promise<boolean> {
await this.checkParentNetwork(parentProvider)
const parentGatewayRouter = L1GatewayRouter__factory.connect(
this.childNetwork.tokenBridge.parentGatewayRouter,
parentProvider
)
return (
(await parentGatewayRouter.l1TokenToGateway(parentTokenAddress)) ===
DISABLED_GATEWAY
)
}
private applyDefaults<T extends DepositRequest>(
params: T
): DefaultedDepositRequest {
return {
...params,
excessFeeRefundAddress: params.excessFeeRefundAddress || params.from,
callValueRefundAddress: params.callValueRefundAddress || params.from,
destinationAddress: params.destinationAddress || params.from,
}
}
/**
* Get the call value for the deposit transaction request
* @param depositParams
* @returns
*/
private getDepositRequestCallValue(
depositParams: OmitTyped<ParentToChildMessageGasParams, 'deposit'>
) {
// the call value should be zero when paying with a custom gas token,
// as the fee amount is packed inside the last parameter (`data`) of the call to `outboundTransfer`, see `getDepositRequestOutboundTransferInnerData`
if (!this.nativeTokenIsEth) {
return constants.Zero
}
// we dont include the child call value for token deposits because
// they either have 0 call value, or their call value is withdrawn from
// a contract by the gateway (weth). So in both of these cases the child call value
// is not actually deposited in the value field
return depositParams.gasLimit
.mul(depositParams.maxFeePerGas)
.add(depositParams.maxSubmissionCost)
}
/**
* Get the `data` param for call to `outboundTransfer`
* @param depositParams
* @returns
*/
private getDepositRequestOutboundTransferInnerData(
depositParams: OmitTyped<ParentToChildMessageGasParams, 'deposit'>,
decimals: number
) {
if (!this.nativeTokenIsEth) {
return defaultAbiCoder.encode(
['uint256', 'bytes', 'uint256'],
[
// maxSubmissionCost
depositParams.maxSubmissionCost, // will be zero
// callHookData
'0x',
// nativeTokenTotalFee
scaleFrom18DecimalsToNativeTokenDecimals({
amount: depositParams.gasLimit
.mul(depositParams.maxFeePerGas)
.add(depositParams.maxSubmissionCost), // will be zero
decimals,
}),
]
)
}
return defaultAbiCoder.encode(
['uint256', 'bytes'],
[
// maxSubmissionCost
depositParams.maxSubmissionCost,
// callHookData
'0x',
]
)
}
/**
* Get the arguments for calling the deposit function
* @param params
* @returns
*/
public async getDepositRequest(
params: DepositRequest
): Promise<ParentToChildTransactionRequest> {
await this.checkParentNetwork(params.parentProvider)
await this.checkChildNetwork(params.childProvider)
const defaultedParams = this.applyDefaults(params)
const {
amount,
destinationAddress,
erc20ParentAddress,
parentProvider,
childProvider,
retryableGasOverrides,
} = defaultedParams
const parentGatewayAddress = await this.getParentGatewayAddress(
erc20ParentAddress,
parentProvider
)
let tokenGasOverrides: GasOverrides | undefined = retryableGasOverrides
// we also add a hardcoded minimum gas limit for custom gateway deposits
if (
parentGatewayAddress === this.childNetwork.tokenBridge.parentCustomGateway
) {
if (!tokenGasOverrides) tokenGasOverrides = {}
if (!tokenGasOverrides.gasLimit) tokenGasOverrides.gasLimit = {}
if (!tokenGasOverrides.gasLimit.min) {
tokenGasOverrides.gasLimit.min =
Erc20Bridger.MIN_CUSTOM_DEPOSIT_GAS_LIMIT
}
}
const decimals = await getNativeTokenDecimals({
parentProvider,
childNetwork: this.childNetwork,
})
const depositFunc = (
depositParams: OmitTyped<ParentToChildMessageGasParams, 'deposit'>
) => {
depositParams.maxSubmissionCost =
params.maxSubmissionCost || depositParams.maxSubmissionCost
const iGatewayRouter = L1GatewayRouter__factory.createInterface()
const innerData = this.getDepositRequestOutboundTransferInnerData(
depositParams,
decimals
)
const functionData =
defaultedParams.excessFeeRefundAddress !== defaultedParams.from
? iGatewayRouter.encodeFunctionData('outboundTransferCustomRefund', [
erc20ParentAddress,
defaultedParams.excessFeeRefundAddress,
destinationAddress,
amount,
depositParams.gasLimit,
depositParams.maxFeePerGas,
innerData,
])
: iGatewayRouter.encodeFunctionData('outboundTransfer', [
erc20ParentAddress,
destinationAddress,
amount,
depositParams.gasLimit,
depositParams.maxFeePerGas,
innerData,
])
return {
data: functionData,
to: this.childNetwork.tokenBridge.parentGatewayRouter,
from: defaultedParams.from,
value: this.getDepositRequestCallValue(depositParams),
}
}
const gasEstimator = new ParentToChildMessageGasEstimator(childProvider)
const estimates = await gasEstimator.populateFunctionParams(
depositFunc,
parentProvider,
tokenGasOverrides
)
return {
txRequest: {
to: this.childNetwork.tokenBridge.parentGatewayRouter,
data: estimates.data,
value: estimates.value,
from: params.from,
},
retryableData: {
...estimates.retryable,
...estimates.estimates,
},
isValid: async () => {
const reEstimates = await gasEstimator.populateFunctionParams(
depositFunc,
parentProvider,
tokenGasOverrides
)
return ParentToChildMessageGasEstimator.isValid(
estimates.estimates,
reEstimates.estimates
)
},
}
}
/**
* Execute a token deposit from parent to child network
* @param params
* @returns
*/
public async deposit(
params: Erc20DepositParams | ParentToChildTxReqAndSignerProvider
): Promise<ParentContractCallTransaction> {
await this.checkParentNetwork(params.parentSigner)
// Although the types prevent should alert callers that value is not
// a valid override, it is possible that they pass it in anyway as it's a common override
// We do a safety check here
if ((params.overrides as PayableOverrides | undefined)?.value) {
throw new ArbSdkError(
'Parent call value should be set through `l1CallValue` param'
)
}
const parentProvider = SignerProviderUtils.getProviderOrThrow(
params.parentSigner
)
const erc20ParentAddress = isParentToChildTransactionRequest(params)
? getErc20ParentAddressFromParentToChildTxRequest(params)
: params.erc20ParentAddress
const isRegistered = await this.isRegistered({
erc20ParentAddress,
parentProvider,
childProvider: params.childProvider,
})
if (!isRegistered) {
const parentChainId = (await parentProvider.getNetwork()).chainId
throw new Error(
`Token ${erc20ParentAddress} on chain ${parentChainId} is not registered on the gateways`
)
}
const tokenDeposit = isParentToChildTransactionRequest(params)
? params
: await this.getDepositRequest({
...params,
parentProvider,
from: await params.parentSigner.getAddress(),
})
const tx = await params.parentSigner.sendTransaction({
...tokenDeposit.txRequest,
...params.overrides,
})
return ParentTransactionReceipt.monkeyPatchContractCallWait(tx)
}
/**
* Get the arguments for calling the token withdrawal function
* @param params
* @returns
*/
public async getWithdrawalRequest(
params: Erc20WithdrawParams
): Promise<ChildToParentTransactionRequest> {
const to = params.destinationAddress
const routerInterface = L2GatewayRouter__factory.createInterface()
const functionData =
// we need to do this since typechain doesnt seem to correctly create
// encodeFunctionData for functions with overrides
(
routerInterface as unknown as {
encodeFunctionData(
functionFragment: 'outboundTransfer(address,address,uint256,bytes)',
values: [string, string, BigNumberish, BytesLike]
): string
}
).encodeFunctionData('outboundTransfer(address,address,uint256,bytes)', [
params.erc20ParentAddress,
to,
params.amount,
'0x',
])
return {
txRequest: {
data: functionData,
to: this.childNetwork.tokenBridge.childGatewayRouter,
value: BigNumber.from(0),
from: params.from,
},
// todo: do proper estimation
estimateParentGasLimit: async (parentProvider: Provider) => {
if (await isArbitrumChain(parentProvider)) {
// values for L3 are dependent on the L1 base fee, so hardcoding can never be accurate
// however, this is only an estimate used for display, so should be good enough
//
// measured with token withdrawals from Rari then added some padding
return BigNumber.from(8_000_000)
}
const parentGatewayAddress = await this.getParentGatewayAddress(
params.erc20ParentAddress,
parentProvider
)
// The WETH gateway is the only deposit that requires callvalue in the Child user-tx (i.e., the recently un-wrapped ETH)
// Here we check if this is a WETH deposit, and include the callvalue for the gas estimate query if so
const isWeth = await this.isWethGateway(
parentGatewayAddress,
parentProvider
)
// measured 157421 - add some padding
return isWeth ? BigNumber.from(190000) : BigNumber.from(160000)
},
}
}
/**
* Withdraw tokens from child to parent network
* @param params
* @returns
*/
public async withdraw(
params:
| (OmitTyped<Erc20WithdrawParams, 'from'> & { childSigner: Signer })
| ChildToParentTxReqAndSigner
): Promise<ChildContractTransaction> {
if (!SignerProviderUtils.signerHasProvider(params.childSigner)) {
throw new MissingProviderArbSdkError('childSigner')
}
await this.checkChildNetwork(params.childSigner)
const withdrawalRequest = isChildToParentTransactionRequest<
OmitTyped<Erc20WithdrawParams, 'from'> & { childSigner: Signer }
>(params)
? params
: await this.getWithdrawalRequest({
...params,
from: await params.childSigner.getAddress(),
})
const tx = await params.childSigner.sendTransaction({
...withdrawalRequest.txRequest,
...params.overrides,
})
return ChildTransactionReceipt.monkeyPatchWait(tx)
}
/**
* Checks if the token has been properly registered on both gateways. Mostly useful for tokens that use a custom gateway.
*
* @param {Object} params
* @param {string} params.erc20ParentAddress
* @param {Provider} params.parentProvider
* @param {Provider} params.childProvider
* @returns
*/
public async isRegistered({
erc20ParentAddress,
parentProvider,
childProvider,
}: {
erc20ParentAddress: string
parentProvider: Provider
childProvider: Provider
}) {
const parentStandardGatewayAddressFromChainConfig =
this.childNetwork.tokenBridge.parentErc20Gateway
const parentGatewayAddressFromParentGatewayRouter =
await this.getParentGatewayAddress(erc20ParentAddress, parentProvider)
// token uses standard gateway; no need to check further
if (
parentStandardGatewayAddressFromChainConfig.toLowerCase() ===
parentGatewayAddressFromParentGatewayRouter.toLowerCase()
) {
return true
}
const childTokenAddressFromParentGatewayRouter =
await this.getChildErc20Address(erc20ParentAddress, parentProvider)
const childGatewayAddressFromChildRouter =
await this.getChildGatewayAddress(erc20ParentAddress, childProvider)
const childTokenAddressFromChildGateway =
await L2ERC20Gateway__factory.connect(
childGatewayAddressFromChildRouter,
childProvider
).calculateL2TokenAddress(erc20ParentAddress)
return (
childTokenAddressFromParentGatewayRouter.toLowerCase() ===
childTokenAddressFromChildGateway.toLowerCase()
)
}
}
/**
* A token and gateway pair
*/
interface TokenAndGateway {
tokenAddr: string
gatewayAddr: string
}
/**
* Admin functionality for the token bridge
*/
export class AdminErc20Bridger extends Erc20Bridger {
private percentIncrease(num: BigNumber, increase: BigNumber): BigNumber {
return num.add(num.mul(increase).div(100))
}
public getApproveGasTokenForCustomTokenRegistrationRequest(
params: ProviderTokenApproveParams
): Required<Pick<TransactionRequest, 'to' | 'data' | 'value'>> {
if (this.nativeTokenIsEth) {
throw new Error('chain uses ETH as its native/gas token')
}
const iErc20Interface = ERC20__factory.createInterface()
const data = iErc20Interface.encodeFunctionData('approve', [
params.erc20ParentAddress,
params.amount || Erc20Bridger.MAX_APPROVAL,
])
return {
data,
value: BigNumber.from(0),
to: this.nativeToken!,
}
}
public async approveGasTokenForCustomTokenRegistration(
params: ApproveParamsOrTxRequest
): Promise<ethers.ContractTransaction> {
if (this.nativeTokenIsEth) {
throw new Error('chain uses ETH as its native/gas token')
}
await this.checkParentNetwork(params.parentSigner)
const approveGasTokenRequest = this.isApproveParams(params)
? this.getApproveGasTokenForCustomTokenRegistrationRequest({
...params,
parentProvider: SignerProviderUtils.getProviderOrThrow(
params.parentSigner
),
})
: params.txRequest
return params.parentSigner.sendTransaction({
...approveGasTokenRequest,
...params.overrides,
})
}
/**
* Register a custom token on the Arbitrum bridge
* See https://developer.offchainlabs.com/docs/bridging_assets#the-arbitrum-generic-custom-gateway for more details
* @param parentTokenAddress Address of the already deployed parent token. Must inherit from https://developer.offchainlabs.com/docs/sol_contract_docs/md_docs/arb-bridge-peripherals/tokenbridge/ethereum/icustomtoken.
* @param childTokenAddress Address of the already deployed child token. Must inherit from https://developer.offchainlabs.com/docs/sol_contract_docs/md_docs/arb-bridge-peripherals/tokenbridge/arbitrum/iarbtoken.
* @param parentSigner The signer with the rights to call `registerTokenOnL2` on the parent token
* @param childProvider Arbitrum rpc provider
* @returns
*/
public async registerCustomToken(
parentTokenAddress: string,
childTokenAddress: string,
parentSigner: Signer,
childProvider: Provider
): Promise<ParentContractTransaction> {
if (!SignerProviderUtils.signerHasProvider(parentSigner)) {
throw new MissingProviderArbSdkError('parentSigner')
}
await this.checkParentNetwork(parentSigner)
await this.checkChildNetwork(childProvider)
const parentProvider = parentSigner.provider!
const parentSenderAddress = await parentSigner.getAddress()
const parentToken = ICustomToken__factory.connect(
parentTokenAddress,
parentSigner
)
const childToken = IArbToken__factory.connect(
childTokenAddress,
childProvider
)
// sanity checks
await parentToken.deployed()
await childToken.deployed()
if (!this.nativeTokenIsEth) {
const nativeTokenContract = ERC20__factory.connect(
this.nativeToken!,
parentProvider
)
const allowance = await nativeTokenContract.allowance(
parentSenderAddress,
parentToken.address
)
const maxFeePerGasOnChild = (await childProvider.getFeeData())
.maxFeePerGas
const maxFeePerGasOnChildWithBuffer = this.percentIncrease(
maxFeePerGasOnChild!,
BigNumber.from(500)
)
// hardcode gas limit to 60k
const estimatedGasFee = BigNumber.from(60_000).mul(
maxFeePerGasOnChildWithBuffer
)
if (allowance.lt(estimatedGasFee)) {
throw new Error(
`Insufficient allowance. Please increase spending for: owner - ${parentSenderAddress}, spender - ${parentToken.address}.`
)
}
}
const parentAddressFromChild = await childToken.l1Address()
if (parentAddressFromChild !== parentTokenAddress) {
throw new ArbSdkError(
`child token does not have parent address set. Set address: ${parentAddressFromChild}, expected address: ${parentTokenAddress}.`
)
}
const nativeTokenDecimals = await getNativeTokenDecimals({
parentProvider,
childNetwork: this.childNetwork,
})
type GasParams = {
maxSubmissionCost: BigNumber
gasLimit: BigNumber
}
const from = await parentSigner.getAddress()
const encodeFuncData = (
setTokenGas: GasParams,
setGatewayGas: GasParams,
maxFeePerGas: BigNumber
) => {
// if we set maxFeePerGas to be the error triggering param then it will
// always trigger for the setToken call and never make it ti setGateways
// so we here we just use the gas limit to trigger retryable data
const doubleFeePerGas = maxFeePerGas.eq(
RetryableDataTools.ErrorTriggeringParams.maxFeePerGas
)
? RetryableDataTools.ErrorTriggeringParams.maxFeePerGas.mul(2)
: maxFeePerGas
const setTokenDeposit = setTokenGas.gasLimit
.mul(doubleFeePerGas)
.add(setTokenGas.maxSubmissionCost)
const setGatewayDeposit = setGatewayGas.gasLimit
.mul(doubleFeePerGas)
.add(setGatewayGas.maxSubmissionCost)
const data = parentToken.interface.encodeFunctionData(
'registerTokenOnL2',
[
childTokenAddress,
setTokenGas.maxSubmissionCost,
setGatewayGas.maxSubmissionCost,
setTokenGas.gasLimit,
setGatewayGas.gasLimit,
doubleFeePerGas,
scaleFrom18DecimalsToNativeTokenDecimals({
amount: setTokenDeposit,
decimals: nativeTokenDecimals,
}),
scaleFrom18DecimalsToNativeTokenDecimals({
amount: setGatewayDeposit,
decimals: nativeTokenDecimals,
}),
parentSenderAddress,
]
)
return {
data,
value: this.nativeTokenIsEth
? setTokenDeposit.add(setGatewayDeposit)
: BigNumber.from(0),
to: parentToken.address,
from,
}
}
const gEstimator = new ParentToChildMessageGasEstimator(childProvider)
const setTokenEstimates2 = await gEstimator.populateFunctionParams(
(params: OmitTyped<ParentToChildMessageGasParams, 'deposit'>) =>
encodeFuncData(
{
gasLimit: params.gasLimit,
maxSubmissionCost: params.maxSubmissionCost,
},
{
gasLimit: RetryableDataTools.ErrorTriggeringParams.gasLimit,
maxSubmissionCost: BigNumber.from(1),
},
params.maxFeePerGas
),
parentProvider
)
const setGatewayEstimates2 = await gEstimator.populateFunctionParams(
(params: OmitTyped<ParentToChildMessageGasParams, 'deposit'>) =>
encodeFuncData(
{
gasLimit: setTokenEstimates2.estimates.gasLimit,
maxSubmissionCost: setTokenEstimates2.estimates.maxSubmissionCost,
},
{
gasLimit: params.gasLimit,
maxSubmissionCost: params.maxSubmissionCost,
},
params.maxFeePerGas
),
parentProvider
)
const registerTx = await parentSigner.sendTransaction({
to: parentToken.address,
data: setGatewayEstimates2.data,
value: setGatewayEstimates2.value,
})
return ParentTransactionReceipt.monkeyPatchWait(registerTx)
}
/**
* Get all the gateway set events on the Parent gateway router
* @param parentProvider The provider for the parent network
* @param filter An object containing fromBlock and toBlock to filter events
* @returns An array of GatewaySetEvent event arguments
*/
public async getParentGatewaySetEvents(
parentProvider: Provider,
filter: { fromBlock: BlockTag; toBlock: BlockTag }
): Promise<EventArgs<GatewaySetEvent>[]> {
await this.checkParentNetwork(parentProvider)
const parentGatewayRouterAddress =
this.childNetwork.tokenBridge.parentGatewayRouter
const eventFetcher = new EventFetcher(parentProvider)
return (
await eventFetcher.getEvents(
L1GatewayRouter__factory,
t => t.filters.GatewaySet(),
{ ...filter, address: parentGatewayRouterAddress }
)
).map(a => a.event)
}
/**
* Get all the gateway set events on the child gateway router
* @param childProvider The provider for the child network
* @param filter An object containing fromBlock and toBlock to filter events
* @param customNetworkChildGatewayRouter Optional address of the custom network child gateway router
* @returns An array of GatewaySetEvent event arguments
* @throws {ArbSdkError} If the network is custom and customNetworkChildGatewayRouter is not provided
*/
public async getChildGatewaySetEvents(
childProvider: Provider,
filter: { fromBlock: BlockTag; toBlock: BlockTag },
customNetworkChildGatewayRouter?: string
): Promise<EventArgs<GatewaySetEvent>[]> {
if (this.childNetwork.isCustom && !customNetworkChildGatewayRouter) {
throw new ArbSdkError(
'Must supply customNetworkChildGatewayRouter for custom network '
)
}
await this.checkChildNetwork(childProvider)
const childGatewayRouterAddress =
customNetworkChildGatewayRouter ||
this.childNetwork.tokenBridge.childGatewayRouter
const eventFetcher = new EventFetcher(childProvider)
return (
await eventFetcher.getEvents(
L2GatewayRouter__factory,
t => t.filters.GatewaySet(),
{ ...filter, address: childGatewayRouterAddress }
)
).map(a => a.event)
}
/**
* Register the provided token addresses against the provided gateways
* @param parentSigner
* @param childProvider
* @param tokenGateways
* @returns
*/
public async setGateways(
parentSigner: Signer,
childProvider: Provider,
tokenGateways: TokenAndGateway[],
options?: GasOverrides
): Promise<ParentContractCallTransaction> {
if (!SignerProviderUtils.signerHasProvider(parentSigner)) {
throw new MissingProviderArbSdkError('parentSigner')
}
await this.checkParentNetwork(parentSigner)
await this.checkChildNetwork(childProvider)
const from = await parentSigner.getAddress()
const parentGatewayRouter = L1GatewayRouter__factory.connect(
this.childNetwork.tokenBridge.parentGatewayRouter,
parentSigner
)
const setGatewaysFunc = (
params: OmitTyped<ParentToChildMessageGasParams, 'deposit'>
) => {
return {
data: parentGatewayRouter.interface.encodeFunctionData('setGateways', [
tokenGateways.map(tG => tG.tokenAddr),
tokenGateways.map(tG => tG.gatewayAddr),
params.gasLimit,
params.maxFeePerGas,
params.maxSubmissionCost,
]),
from,
value: params.gasLimit
.mul(params.maxFeePerGas)
.add(params.maxSubmissionCost),
to: parentGatewayRouter.address,
}
}
const gEstimator = new ParentToChildMessageGasEstimator(childProvider)
const estimates = await gEstimator.populateFunctionParams(
setGatewaysFunc,
parentSigner.provider,
options
)
const res = await parentSigner.sendTransaction({
to: estimates.to,
data: estimates.data,
value: estimates.estimates.deposit,
})
return ParentTransactionReceipt.monkeyPatchContractCallWait(res)
}
}
================================================
File: packages/sdk/src/lib/assetBridger/ethBridger.ts
================================================
/*
* Copyright 2021, Offchain Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env node */
'use strict'
import { Signer } from '@ethersproject/abstract-signer'
import { Provider, TransactionRequest } from '@ethersproject/abstract-provider'
import { PayableOverrides, Overrides } from '@ethersproject/contracts'
import { BigNumber, constants } from 'ethers'
import { Inbox__factory } from '../abi/factories/Inbox__factory'
import { ERC20Inbox__factory } from '../abi/factories/ERC20Inbox__factory'
import { ArbSys__factory } from '../abi/factories/ArbSys__factory'
import { ARB_SYS_ADDRESS } from '../dataEntities/constants'
import { AssetBridger } from './assetBridger'
import {
ParentEthDepositTransaction,
ParentContractCallTransaction,
ParentTransactionReceipt,
} from '../message/ParentTransaction'
import {
ChildContractTransaction,
ChildTransactionReceipt,
} from '../message/ChildTransaction'
import { ParentToChildMessageCreator } from '../message/ParentToChildMessageCreator'
import { GasOverrides } from '../message/ParentToChildMessageGasEstimator'
import {
isParentToChildTransactionRequest,
isChildToParentTransactionRequest,
ParentToChildTransactionRequest,
ChildToParentTransactionRequest,
} from '../dataEntities/transactionRequest'
import { OmitTyped } from '../utils/types'
import { SignerProviderUtils } from '../dataEntities/signerOrProvider'
import { MissingProviderArbSdkError } from '../dataEntities/errors'
import { getArbitrumNetwork } from '../dataEntities/networks'
import { ERC20__factory } from '../abi/factories/ERC20__factory'
import {
getNativeTokenDecimals,
isArbitrumChain,
scaleFromNativeTokenDecimalsTo18Decimals,
} from '../utils/lib'
export type ApproveGasTokenParams = {
/**
* Amount to approve. Defaults to max int.
*/
amount?: BigNumber
/**
* Transaction overrides
*/
overrides?: PayableOverrides
}
export type ApproveGasTokenTxRequest = {
/**
* Transaction request
*/
txRequest: Required<Pick<TransactionRequest, 'to' | 'data' | 'value'>>
/**
* Transaction overrides
*/
overrides?: Overrides
}
export type ApproveGasTokenParamsOrTxRequest =
| ApproveGasTokenParams
| ApproveGasTokenTxRequest
type WithParentSigner<T extends ApproveGasTokenParamsOrTxRequest> = T & {
parentSigner: Signer
}
export interface EthWithdrawParams {
/**
* The amount of ETH or tokens to be withdrawn
*/
amount: BigNumber
/**
* The parent network address to receive the value.
*/
destinationAddress: string
/**
* The address of the withdrawal sender
*/
from: string
/**
* Transaction overrides
*/
overrides?: PayableOverrides
}
export type EthDepositParams = {
/**
* Parent network provider or signer
*/
parentSigner: Signer
/**
* The amount of ETH or tokens to be deposited
*/
amount: BigNumber
/**
* Transaction overrides
*/
overrides?: PayableOverrides
}
export type EthDepositToParams = EthDepositParams & {
/**
* Child network provider
*/
childProvider: Provider
/**
* Child network address of the entity receiving the funds
*/
destinationAddress: string
/**
* Overrides for the retryable ticket parameters
*/
retryableGasOverrides?: GasOverrides
}
export type ParentToChildTxReqAndSigner = ParentToChildTransactionRequest & {
parentSigner: Signer
overrides?: Overrides
}
export type ChildToParentTxReqAndSigner = ChildToParentTransactionRequest & {
childSigner: Signer
overrides?: Overrides
}
type EthDepositRequestParams = OmitTyped<
EthDepositParams,
'overrides' | 'parentSigner'
> & { from: string }
type EthDepositToRequestParams = OmitTyped<
EthDepositToParams,
'overrides' | 'parentSigner'
> & {
/**
* Parent network provider
*/
parentProvider: Provider
/**
* Address that is depositing the ETH
*/
from: string
}
/**
* Bridger for moving either ETH or custom gas tokens back and forth between parent and child networks
*/
export class EthBridger extends AssetBridger<
EthDepositParams | EthDepositToParams | ParentToChildTxReqAndSigner,
EthWithdrawParams | ChildToParentTxReqAndSigner
> {
/**
* Instantiates a new EthBridger from a child network Provider
* @param childProvider
* @returns
*/
public static async fromProvider(childProvider: Provider) {
return new EthBridger(await getArbitrumNetwork(childProvider))
}
/**
* Asserts that the provided argument is of type `ApproveGasTokenParams` and not `ApproveGasTokenTxRequest`.
* @param params
*/
private isApproveGasTokenParams(
params: ApproveGasTokenParamsOrTxRequest
): params is WithParentSigner<ApproveGasTokenParams> {
return typeof (params as ApproveGasTokenTxRequest).txRequest === 'undefined'
}
/**
* Creates a transaction request for approving the custom gas token to be spent by the inbox on the parent network
* @param params
*/
public getApproveGasTokenRequest(
params?: ApproveGasTokenParams
): Required<Pick<TransactionRequest, 'to' | 'data' | 'value'>> {
if (this.nativeTokenIsEth) {
throw new Error('chain uses ETH as its native/gas token')
}
const data = ERC20__factory.createInterface().encodeFunctionData(
'approve',
[
// spender
this.childNetwork.ethBridge.inbox,
// value
params?.amount ?? constants.MaxUint256,
]
)
return {
to: this.nativeToken!,
data,
value: BigNumber.from(0),
}
}
/**
* Approves the custom gas token to be spent by the Inbox on the parent network.
* @param params
*/
public async approveGasToken(
params: WithParentSigner<ApproveGasTokenParamsOrTxRequest>
) {
if (this.nativeTokenIsEth) {
throw new Error('chain uses ETH as its native/gas token')
}
const approveGasTokenRequest = this.isApproveGasTokenParams(params)
? this.getApproveGasTokenRequest(params)
: params.txRequest
return params.parentSigner.sendTransaction({
...approveGasTokenRequest,
...params.overrides,
})
}
/**
* Gets transaction calldata for a tx request for depositing ETH or custom gas token
* @param params
* @returns
*/
private getDepositRequestData(params: EthDepositRequestParams) {
if (!this.nativeTokenIsEth) {
return (
ERC20Inbox__factory.createInterface() as unknown as {
encodeFunctionData(
functionFragment: 'depositERC20(uint256)',
values: [BigNumber]
): string
}
).encodeFunctionData('depositERC20(uint256)', [params.amount])
}
return (
Inbox__factory.createInterface() as unknown as {
encodeFunctionData(
functionFragment: 'depositEth()',
values?: undefined
): string
}
).encodeFunctionData('depositEth()')
}
/**
* Gets tx request for depositing ETH or custom gas token
* @param params
* @returns
*/
public async getDepositRequest(
params: EthDepositRequestParams
): Promise<OmitTyped<ParentToChildTransactionRequest, 'retryableData'>> {
return {
txRequest: {
to: this.childNetwork.ethBridge.inbox,
value: this.nativeTokenIsEth ? params.amount : 0,
data: this.getDepositRequestData(params),
from: params.from,
},
isValid: async () => true,
}
}
/**
* Deposit ETH from Parent onto Child network
* @param params
* @returns
*/
public async deposit(
params: EthDepositParams | ParentToChildTxReqAndSigner
): Promise<ParentEthDepositTransaction> {
await this.checkParentNetwork(params.parentSigner)
const ethDeposit = isParentToChildTransactionRequest(params)
? params
: await this.getDepositRequest({
...params,
from: await params.parentSigner.getAddress(),
})
const tx = await params.parentSigner.sendTransaction({
...ethDeposit.txRequest,
...params.overrides,
})
return ParentTransactionReceipt.monkeyPatchEthDepositWait(tx)
}
/**
* Get a transaction request for an ETH deposit to a different child network address using Retryables
* @param params
* @returns
*/
public async getDepositToRequest(
params: EthDepositToRequestParams
): Promise<ParentToChildTransactionRequest> {
const decimals = await getNativeTokenDecimals({
parentProvider: params.parentProvider,
childNetwork: this.childNetwork,
})
const amountToBeMintedOnChildChain =
scaleFromNativeTokenDecimalsTo18Decimals({
amount: params.amount,
decimals,
})
const requestParams = {
...params,
to: params.destinationAddress,
l2CallValue: amountToBeMintedOnChildChain,
callValueRefundAddress: params.destinationAddress,
data: '0x',
}
// Gas overrides can be passed in the parameters
const gasOverrides = params.retryableGasOverrides || undefined
return ParentToChildMessageCreator.getTicketCreationRequest(
requestParams,
params.parentProvider,
params.childProvider,
gasOverrides
)
}
/**
* Deposit ETH from parent network onto a different child network address
* @param params
* @returns
*/
public async depositTo(
params:
| EthDepositToParams
| (ParentToChildTxReqAndSigner & { childProvider: Provider })
): Promise<ParentContractCallTransaction> {
await this.checkParentNetwork(params.parentSigner)
await this.checkChildNetwork(params.childProvider)
const retryableTicketRequest = isParentToChildTransactionRequest(params)
? params
: await this.getDepositToRequest({
...params,
from: await params.parentSigner.getAddress(),
parentProvider: params.parentSigner.provider!,
})
const parentToChildMessageCreator = new ParentToChildMessageCreator(
params.parentSigner
)
const tx = await parentToChildMessageCreator.createRetryableTicket(
retryableTicketRequest,
params.childProvider
)
return ParentTransactionReceipt.monkeyPatchContractCallWait(tx)
}
/**
* Get a transaction request for an eth withdrawal
* @param params
* @returns
*/
public async getWithdrawalRequest(
params: EthWithdrawParams
): Promise<ChildToParentTransactionRequest> {
const iArbSys = ArbSys__factory.createInterface()
const functionData = iArbSys.encodeFunctionData('withdrawEth', [
params.destinationAddress,
])
return {
txRequest: {
to: ARB_SYS_ADDRESS,
data: functionData,
value: params.amount,
from: params.from,
},
// todo: do proper estimation
estimateParentGasLimit: async (parentProvider: Provider) => {
if (await isArbitrumChain(parentProvider)) {
// values for L3 are dependent on the L1 base fee, so hardcoding can never be accurate
// however, this is only an estimate used for display, so should be good enough
//
// measured with withdrawals from Xai and Rari then added some padding
return BigNumber.from(4_000_000)
}
// measured 126998 - add some padding
return BigNumber.from(130000)
},
}
}
/**
* Withdraw ETH from child network onto parent network
* @param params
* @returns
*/
public async withdraw(
params:
| (EthWithdrawParams & { childSigner: Signer })
| ChildToParentTxReqAndSigner
): Promise<ChildContractTransaction> {
if (!SignerProviderUtils.signerHasProvider(params.childSigner)) {
throw new MissingProviderArbSdkError('childSigner')
}
await this.checkChildNetwork(params.childSigner)
const request = isChildToParentTransactionRequest<
EthWithdrawParams & { childSigner: Signer }
>(params)
? params
: await this.getWithdrawalRequest(params)
const tx = await params.childSigner.sendTransaction({
...request.txRequest,
...params.overrides,
})
return ChildTransactionReceipt.monkeyPatchWait(tx)
}
}
================================================
File: packages/sdk/src/lib/assetBridger/l1l3Bridger.ts
================================================
import { Provider, TransactionRequest } from '@ethersproject/abstract-provider'
import {
BigNumber,
BigNumberish,
Overrides,
PayableOverrides,
Signer,
ethers,
} from 'ethers'
import { IERC20 } from '../abi/IERC20'
import { L2GatewayToken } from '../abi/L2GatewayToken'
import { IL1Teleporter } from '../abi/IL1Teleporter'
import { IERC20__factory } from '../abi/factories/IERC20__factory'
import { L1GatewayRouter__factory } from '../abi/factories/L1GatewayRouter__factory'
import { IL2ForwarderFactory__factory } from '../abi/factories/IL2ForwarderFactory__factory'
import { L2GatewayToken__factory } from '../abi/factories/L2GatewayToken__factory'
import { IL1Teleporter__factory } from '../abi/factories/IL1Teleporter__factory'
import { Address } from '../dataEntities/address'
import { ArbSdkError } from '../dataEntities/errors'
import {
ArbitrumNetwork,
Teleporter,
assertArbitrumNetworkHasTokenBridge,
getArbitrumNetworks,
} from '../dataEntities/networks'
import {
SignerOrProvider,
SignerProviderUtils,
} from '../dataEntities/signerOrProvider'
import { ParentToChildTransactionRequest } from '../dataEntities/transactionRequest'
import {
ParentToChildMessageReader,
ParentToChildMessageStatus,
} from '../message/ParentToChildMessage'
import { ParentToChildMessageCreator } from '../message/ParentToChildMessageCreator'
import {
GasOverrides,
ParentToChildMessageGasEstimator,
PercentIncrease,
} from '../message/ParentToChildMessageGasEstimator'
import {
ParentContractCallTransaction,
ParentContractCallTransactionReceipt,
ParentEthDepositTransactionReceipt,
ParentTransactionReceipt,
} from '../message/ParentTransaction'
import { Erc20Bridger } from './erc20Bridger'
import { Inbox__factory } from '../abi/factories/Inbox__factory'
import { OmitTyped } from '../utils/types'
import { getAddress } from 'ethers/lib/utils'
import { IL2Forwarder } from '../abi/IL2Forwarder'
import { ERC20__factory } from '../abi/factories/ERC20__factory'
import { IL2ForwarderPredictor__factory } from '../abi/factories/IL2ForwarderPredictor__factory'
import { IInbox__factory } from '../abi/factories/IInbox__factory'
import { RetryableMessageParams } from '../dataEntities/message'
type PickedTransactionRequest = Required<
Pick<TransactionRequest, 'to' | 'data' | 'value'>
>
export enum TeleportationType {
/**
* Teleporting to an ETH fee L3
*/
Standard,
/**
* Teleporting the fee token to a custom fee L3
*/
OnlyGasToken,
/**
* Teleporting a non-fee token to a custom fee L3
*/
NonGasTokenToCustomGas,
}
export type TxRequestParams = {
txRequest: PickedTransactionRequest
l1Signer: Signer
overrides?: PayableOverrides
}
type RetryableGasValues = {
gasLimit: BigNumber
maxSubmissionFee: BigNumber
}
export type DepositRequestResult = {
txRequest: PickedTransactionRequest
gasTokenAmount: BigNumber
}
export type TeleporterRetryableGasOverride = {
gasLimit?: PercentIncrease & {
/**
* Set a minimum max gas
*/
min?: BigNumber
}
maxSubmissionFee?: PercentIncrease
}
export type TokenApproveParams = {
/**
* L1 address of the ERC20 token contract
*/
erc20L1Address: string
/**
* Amount to approve. Defaults to max int.
*/
amount?: BigNumber
}
export type Erc20L1L3DepositRequestRetryableOverrides = {
/**
* Optional L1 gas price override. Used to estimate submission fees.
*/
l1GasPrice?: PercentIncrease
/**
* Optional L2 gas price override
*/
l2GasPrice?: PercentIncrease
/**
* Optional L3 gas price override
*/
l3GasPrice?: PercentIncrease
/**
* L2ForwarderFactory retryable gas override
*/
l2ForwarderFactoryRetryableGas?: TeleporterRetryableGasOverride
/**
* L1 to L2 fee token bridge retryable gas override
*/
l1l2GasTokenBridgeRetryableGas?: TeleporterRetryableGasOverride
/**
* L1 to L2 token bridge retryable gas override
*/
l1l2TokenBridgeRetryableGas?: TeleporterRetryableGasOverride
/**
* L2 to L3 token bridge retryable gas override
*/
l2l3TokenBridgeRetryableGas?: TeleporterRetryableGasOverride
}
export type Erc20L1L3DepositRequestParams = {
/**
* Address of L1 token
*/
erc20L1Address: string
/**
* Amount of L1 token to send to L3
*/
amount: BigNumber
/**
* L2 provider
*/
l2Provider: Provider
/**
* L3 provider
*/
l3Provider: Provider
/**
* If the L3 uses a custom fee token, skip payment for L2 to L3 retryable even if the fee token is available on L1
*
* If payment is skipped, the teleportation will not be completed until the L2 to L3 retryable is manually redeemed
*
* Defaults to false
*/
skipGasToken?: boolean
/**
* Optional recipient on L3, defaults to signer's address
*/
destinationAddress?: string
/**
* Optional overrides for retryable gas parameters
*/
retryableOverrides?: Erc20L1L3DepositRequestRetryableOverrides
}
export type TxReference =
| { txHash: string }
| { tx: ParentContractCallTransaction }
| { txReceipt: ParentContractCallTransactionReceipt }
export type GetL1L3DepositStatusParams = {
l1Provider: Provider
l2Provider: Provider
l3Provider: Provider
} & TxReference
export type Erc20L1L3DepositStatus = {
/**
* L1 to L2 token bridge message
*/
l1l2TokenBridgeRetryable: ParentToChildMessageReader
/**
* L1 to L2 fee token bridge message
*/
l1l2GasTokenBridgeRetryable: ParentToChildMessageReader | undefined
/**
* L2ForwarderFactory message
*/
l2ForwarderFactoryRetryable: ParentToChildMessageReader
/**
* L2 to L3 token bridge message
*/
l2l3TokenBridgeRetryable: ParentToChildMessageReader | undefined
/**
* Indicates that the L2ForwarderFactory call was front ran by another teleportation.
*
* This is true if:
* - l1l2TokenBridgeRetryable status is REDEEMED; AND
* - l2ForwarderFactoryRetryable status is FUNDS_DEPOSITED_ON_CHILD; AND
* - L2Forwarder token balance is 0
*
* The first teleportation with l2ForwarderFactoryRetryable redemption *after* this teleportation's l1l2TokenBridgeRetryable redemption
* is the one that completes this teleportation.
*
* If that subsequent teleportation is complete, this one is considered complete as well.
*/
l2ForwarderFactoryRetryableFrontRan: boolean
/**
* Whether the teleportation has completed.
*/
completed: boolean
}
export type EthL1L3DepositRequestParams = {
/**
* Amount of ETH to send to L3
*/
amount: BigNumberish
/**
* L2 provider
*/
l2Provider: Provider
/**
* L3 provider
*/
l3Provider: Provider
/**
* Optional recipient on L3, defaults to signer's address
*/
destinationAddress?: string
/**
* Optional fee refund address on L2, defaults to signer's address
*/
l2RefundAddress?: string
/**
* Optional gas overrides for L1 to L2 message
*/
l2TicketGasOverrides?: Omit<GasOverrides, 'deposit'>
/**
* Optional gas overrides for L2 to L3 message
*/
l3TicketGasOverrides?: Omit<GasOverrides, 'deposit'>
}
export type EthL1L3DepositStatus = {
/**
* L1 to L2 message
*/
l2Retryable: ParentToChildMessageReader
/**
* L2 to L3 message
*/
l3Retryable: ParentToChildMessageReader | undefined
/**
* Whether the teleportation has completed
*/
completed: boolean
}
/**
* Base functionality for L1 to L3 bridging.
*/
class BaseL1L3Bridger {
public readonly l1Network: { chainId: number }
public readonly l2Network: ArbitrumNetwork
public readonly l3Network: ArbitrumNetwork
public readonly defaultGasPricePercentIncrease: BigNumber =
BigNumber.from(500)
public readonly defaultGasLimitPercentIncrease: BigNumber =
BigNumber.from(100)
constructor(l3Network: ArbitrumNetwork) {
const l2Network = getArbitrumNetworks().find(
network => network.chainId === l3Network.parentChainId
)
if (!l2Network) {
throw new ArbSdkError(
`Unknown arbitrum network chain id: ${l3Network.parentChainId}`
)
}
this.l1Network = { chainId: l2Network.parentChainId }
this.l2Network = l2Network
this.l3Network = l3Network
}
/**
* Check the signer/provider matches the l1Network, throws if not
* @param sop
*/
protected async _checkL1Network(sop: SignerOrProvider): Promise<void> {
await SignerProviderUtils.checkNetworkMatches(sop, this.l1Network.chainId)
}
/**
* Check the signer/provider matches the l2Network, throws if not
* @param sop
*/
protected async _checkL2Network(sop: SignerOrProvider): Promise<void> {
await SignerProviderUtils.checkNetworkMatches(sop, this.l2Network.chainId)
}
/**
* Check the signer/provider matches the l3Network, throws if not
* @param sop
*/
protected async _checkL3Network(sop: SignerOrProvider): Promise<void> {
await SignerProviderUtils.checkNetworkMatches(sop, this.l3Network.chainId)
}
protected _percentIncrease(num: BigNumber, increase: BigNumber): BigNumber {
return num.add(num.mul(increase).div(100))
}
protected _getTxHashFromTxRef(txRef: TxReference): string {
if ('txHash' in txRef) {
return txRef.txHash
} else if ('tx' in txRef) {
return txRef.tx.hash
} else {
return txRef.txReceipt.transactionHash
}
}
protected async _getTxFromTxRef(
txRef: TxReference,
provider: Provider
): Promise<ParentContractCallTransaction> {
if ('tx' in txRef) {
return txRef.tx
}
return ParentTransactionReceipt.monkeyPatchContractCallWait(
await provider.getTransaction(this._getTxHashFromTxRef(txRef))
)
}
protected async _getTxReceiptFromTxRef(
txRef: TxReference,
provider: Provider
): Promise<ParentContractCallTransactionReceipt> {
if ('txReceipt' in txRef) {
return txRef.txReceipt
}
return new ParentContractCallTransactionReceipt(
await provider.getTransactionReceipt(this._getTxHashFromTxRef(txRef))
)
}
}
/**
* Bridger for moving ERC20 tokens from L1 to L3
*/
export class Erc20L1L3Bridger extends BaseL1L3Bridger {
/**
* Addresses of teleporter contracts on L2
*/
public readonly teleporter: Teleporter
/**
* Default gas limit for L2ForwarderFactory.callForwarder of 1,000,000
*
* Measured Standard: 361746
*
* Measured OnlyGasToken: 220416
*
* Measured NonGasTokenToCustomGas: 373449
*/
public readonly l2ForwarderFactoryDefaultGasLimit = BigNumber.from(1_000_000)
public readonly skipL1GasTokenMagic = ethers.utils.getAddress(
ethers.utils.hexDataSlice(
ethers.utils.keccak256(ethers.utils.toUtf8Bytes('SKIP_FEE_TOKEN')),
0,
20
)
)
/**
* If the L3 network uses a custom (non-eth) fee token, this is the address of that token on L2
*/
public readonly l2GasTokenAddress: string | undefined
protected readonly l2Erc20Bridger = new Erc20Bridger(this.l2Network)
protected readonly l3Erc20Bridger = new Erc20Bridger(this.l3Network)
/**
* If the L3 network uses a custom fee token, this is the address of that token on L1
*/
protected _l1FeeTokenAddress: string | undefined
public constructor(l3Network: ArbitrumNetwork) {
super(l3Network)
if (!this.l2Network.teleporter) {
throw new ArbSdkError(
`L2 network ${this.l2Network.name} does not have teleporter contracts`
)
}
if (
this.l3Network.nativeToken &&
this.l3Network.nativeToken !== ethers.constants.AddressZero
) {
this.l2GasTokenAddress = this.l3Network.nativeToken
}
this.teleporter = this.l2Network.teleporter
}
/**
* If the L3 network uses a custom gas token, return the address of that token on L1.
* If the fee token is not available on L1, does not use 18 decimals on L1 and L2, or the L3 network uses ETH for fees, throw.
*/
public async getGasTokenOnL1(
l1Provider: Provider,
l2Provider: Provider
): Promise<string> {
// if the L3 network uses ETH for fees, early return zero
if (!this.l2GasTokenAddress) throw new ArbSdkError('L3 uses ETH for gas')
// if we've already fetched the L1 fee token address, early return it
if (this._l1FeeTokenAddress) return this._l1FeeTokenAddress
await this._checkL1Network(l1Provider)
await this._checkL2Network(l2Provider)
let l1FeeTokenAddress: string | undefined
try {
l1FeeTokenAddress = await this.l2Erc20Bridger.getParentErc20Address(
this.l2GasTokenAddress,
l2Provider
)
} catch (e: any) {
// if the error is a CALL_EXCEPTION, we can't find the token on L1
// if the error is something else, rethrow
if (e.code !== 'CALL_EXCEPTION') {
throw e
}
}
if (
!l1FeeTokenAddress ||
l1FeeTokenAddress === ethers.constants.AddressZero
) {
throw new ArbSdkError(
'L1 gas token not found. Use skipGasToken when depositing'
)
}
// make sure both the L1 and L2 tokens have 18 decimals
if (
(await ERC20__factory.connect(
l1FeeTokenAddress,
l1Provider
).decimals()) !== 18
) {
throw new ArbSdkError(
'L1 gas token has incorrect decimals. Use skipGasToken when depositing'
)
}
if (
(await ERC20__factory.connect(
this.l2GasTokenAddress,
l2Provider
).decimals()) !== 18
) {
throw new ArbSdkError(
'L2 gas token has incorrect decimals. Use skipGasToken when depositing'
)
}
if (await this.l1TokenIsDisabled(l1FeeTokenAddress, l1Provider)) {
throw new ArbSdkError(
'L1 gas token is disabled on the L1 to L2 token bridge. Use skipGasToken when depositing'
)
}
if (await this.l2TokenIsDisabled(this.l2GasTokenAddress, l2Provider)) {
throw new ArbSdkError(
'L2 gas token is disabled on the L2 to L3 token bridge. Use skipGasToken when depositing'
)
}
return (this._l1FeeTokenAddress = l1FeeTokenAddress)
}
/**
* Get the corresponding L2 token address for the provided L1 token
*/
public getL2Erc20Address(
erc20L1Address: string,
l1Provider: Provider
): Promise<string> {
return this.l2Erc20Bridger.getChildErc20Address(erc20L1Address, l1Provider)
}
/**
* Get the corresponding L3 token address for the provided L1 token
*/
public async getL3Erc20Address(
erc20L1Address: string,
l1Provider: Provider,
l2Provider: Provider
): Promise<string> {
return this.l3Erc20Bridger.getChildErc20Address(
await this.getL2Erc20Address(erc20L1Address, l1Provider),
l2Provider
)
}
/**
* Given an L1 token's address, get the address of the token's L1 <-> L2 gateway on L1
*/
public getL1L2GatewayAddress(
erc20L1Address: string,
l1Provider: Provider
): Promise<string> {
return this.l2Erc20Bridger.getParentGatewayAddress(
erc20L1Address,
l1Provider
)
}
/**
* Get the address of the L2 <-> L3 gateway on L2 given an L1 token address
*/
public async getL2L3GatewayAddress(
erc20L1Address: string,
l1Provider: Provider,
l2Provider: Provider
): Promise<string> {
const l2Token = await this.getL2Erc20Address(erc20L1Address, l1Provider)
return this.l3Erc20Bridger.getParentGatewayAddress(l2Token, l2Provider)
}
/**
* Get the L1 token contract at the provided address
* Note: This function just returns a typed ethers object for the provided address, it doesn't
* check the underlying form of the contract bytecode to see if it's an erc20, and doesn't ensure the validity
* of any of the underlying functions on that contract.
*/
public getL1TokenContract(l1TokenAddr: string, l1Provider: Provider): IERC20 {
return IERC20__factory.connect(l1TokenAddr, l1Provider)
}
/**
* Get the L2 token contract at the provided address
* Note: This function just returns a typed ethers object for the provided address, it doesn't
* check the underlying form of the contract bytecode to see if it's an erc20, and doesn't ensure the validity
* of any of the underlying functions on that contract.
*/
public getL2TokenContract(
l2TokenAddr: string,
l2Provider: Provider
): L2GatewayToken {
return L2GatewayToken__factory.connect(l2TokenAddr, l2Provider)
}
/**
* Get the L3 token contract at the provided address
* Note: This function just returns a typed ethers object for the provided address, it doesn't
* check the underlying form of the contract bytecode to see if it's an erc20, and doesn't ensure the validity
* of any of the underlying functions on that contract.
*/
public getL3TokenContract(
l3TokenAddr: string,
l3Provider: Provider
): L2GatewayToken {
return L2GatewayToken__factory.connect(l3TokenAddr, l3Provider)
}
/**
* Whether the L1 token has been disabled on the L1 <-> L2 router given an L1 token address
*/
public async l1TokenIsDisabled(
l1TokenAddress: string,
l1Provider: Provider
): Promise<boolean> {
return this.l2Erc20Bridger.isDepositDisabled(l1TokenAddress, l1Provider)
}
/**
* Whether the L2 token has been disabled on the L2 <-> L3 router given an L2 token address
*/
public async l2TokenIsDisabled(
l2TokenAddress: string,
l2Provider: Provider
): Promise<boolean> {
return this.l3Erc20Bridger.isDepositDisabled(l2TokenAddress, l2Provider)
}
/**
* Given some L2Forwarder parameters, get the address of the L2Forwarder contract
*/
public async l2ForwarderAddress(
owner: string,
routerOrInbox: string,
destinationAddress: string,
l1OrL2Provider: Provider
): Promise<string> {
const chainId = (await l1OrL2Provider.getNetwork()).chainId
let predictor
if (chainId === this.l1Network.chainId) {
predictor = this.teleporter.l1Teleporter
} else if (chainId === this.l2Network.chainId) {
predictor = this.teleporter.l2ForwarderFactory
} else {
throw new ArbSdkError(`Unknown chain id: ${chainId}`)
}
return IL2ForwarderPredictor__factory.connect(
predictor,
l1OrL2Provider
).l2ForwarderAddress(owner, routerOrInbox, destinationAddress)
}
/**
* Get a tx request to approve tokens for teleportation.
* The tokens will be approved for L1Teleporter.
*/
public async getApproveTokenRequest(
params: TokenApproveParams
): Promise<PickedTransactionRequest> {
const iface = IERC20__factory.createInterface()
const data = iface.encodeFunctionData('approve', [
this.teleporter.l1Teleporter,
params.amount || ethers.constants.MaxUint256,
])
return {
to: params.erc20L1Address,
data,
value: 0,
}
}
/**
* Approve tokens for teleportation.
* The tokens will be approved for L1Teleporter.
*/
public async approveToken(
params:
| (TokenApproveParams & { l1Signer: Signer; overrides?: Overrides })
| TxRequestParams
): Promise<ethers.ContractTransaction> {
await this._checkL1Network(params.l1Signer)
const approveRequest =
'txRequest' in params
? params.txRequest
: await this.getApproveTokenRequest(params)
return params.l1Signer.sendTransaction({
...approveRequest,
...params.overrides,
})
}
/**
* Get a tx request to approve the L3's fee token for teleportation.
* The tokens will be approved for L1Teleporter.
* Will throw if the L3 network uses ETH for fees or the fee token doesn't exist on L1.
*/
public async getApproveGasTokenRequest(params: {
l1Provider: Provider
l2Provider: Provider
amount?: BigNumber
}): Promise<PickedTransactionRequest> {
return this.getApproveTokenRequest({
erc20L1Address: await this.getGasTokenOnL1(
params.l1Provider,
params.l2Provider
),
amount: params.amount,
})
}
/**
* Approve the L3's fee token for teleportation.
* The tokens will be approved for L1Teleporter.
* Will throw if the L3 network uses ETH for fees or the fee token doesn't exist on L1.
*/
public async approveGasToken(
params:
| {
l1Signer: Signer
l2Provider: Provider
amount?: BigNumber
overrides?: Overrides
}
| TxRequestParams
): Promise<ethers.ContractTransaction> {
await this._checkL1Network(params.l1Signer)
const approveRequest =
'txRequest' in params
? params.txRequest
: await this.getApproveGasTokenRequest({
l1Provider: params.l1Signer.provider!,
l2Provider: params.l2Provider,
amount: params.amount,
})
return params.l1Signer.sendTransaction({
...approveRequest,
...params.overrides,
})
}
/**
* Get a tx request for teleporting some tokens from L1 to L3.
* Also returns the amount of fee tokens required for teleportation.
*/
public async getDepositRequest(
params: Erc20L1L3DepositRequestParams &
(
| {
from: string
l1Provider: Provider
}
| { l1Signer: Signer }
)
): Promise<DepositRequestResult> {
assertArbitrumNetworkHasTokenBridge(this.l2Network)
assertArbitrumNetworkHasTokenBridge(this.l3Network)
const l1Provider =
'l1Provider' in params ? params.l1Provider : params.l1Signer.provider!
await this._checkL1Network(l1Provider)
await this._checkL2Network(params.l2Provider)
await this._checkL3Network(params.l3Provider)
const from =
'from' in params ? params.from : await params.l1Signer.getAddress()
let l1FeeToken
// if the l3 uses eth for fees, set to zero
if (!this.l2GasTokenAddress) {
l1FeeToken = ethers.constants.AddressZero
}
// if the l3 uses custom fee but the user opts to skip payment, set to magic address
else if (params.skipGasToken) {
l1FeeToken = this.skipL1GasTokenMagic
}
// if the l3 uses custom fee and the user opts to not skip, try to get the token
// will throw if the token doesn't exist on L1 or is unsupported
else {
l1FeeToken = await this.getGasTokenOnL1(l1Provider, params.l2Provider)
}
const partialTeleportParams: OmitTyped<
IL1Teleporter.TeleportParamsStruct,
'gasParams'
> = {
l1Token: params.erc20L1Address,
l3FeeTokenL1Addr: l1FeeToken,
l1l2Router: this.l2Network.tokenBridge.parentGatewayRouter,
l2l3RouterOrInbox:
l1FeeToken &&
getAddress(params.erc20L1Address) === getAddress(l1FeeToken)
? this.l3Network.ethBridge.inbox
: this.l3Network.tokenBridge.parentGatewayRouter,
to: params.destinationAddress || from,
amount: params.amount,
}
const { teleportParams, costs } = await this._fillPartialTeleportParams(
partialTeleportParams,
params.retryableOverrides || {},
l1Provider,
params.l2Provider,
params.l3Provider
)
const data = IL1Teleporter__factory.createInterface().encodeFunctionData(
'teleport',
[teleportParams]
)
return {
txRequest: {
to: this.teleporter.l1Teleporter,
data,
value: costs.ethAmount,
},
gasTokenAmount: costs.feeTokenAmount,
}
}
/**
* Execute a teleportation of some tokens from L1 to L3.
*/
public async deposit(
params:
| (Erc20L1L3DepositRequestParams & {
l1Signer: Signer
overrides?: PayableOverrides
})
| TxRequestParams
): Promise<ParentContractCallTransaction> {
await this._checkL1Network(params.l1Signer)
const depositRequest =
'txRequest' in params
? params.txRequest
: (await this.getDepositRequest(params)).txRequest
const tx = await params.l1Signer.sendTransaction({
...depositRequest,
...params.overrides,
})
return ParentTransactionReceipt.monkeyPatchContractCallWait(tx)
}
/**
* Given a teleportation tx, get the L1Teleporter parameters, L2Forwarder parameters, and L2Forwarder address
*/
public async getDepositParameters(
params: {
l1Provider: Provider
l2Provider: Provider
} & TxReference
) {
await this._checkL1Network(params.l1Provider)
await this._checkL2Network(params.l2Provider)
const tx = await this._getTxFromTxRef(params, params.l1Provider)
const txReceipt = await tx.wait()
const l1l2Messages = await this._getL1ToL2Messages(
txReceipt,
params.l2Provider
)
const l2ForwarderParams = this._decodeCallForwarderCalldata(
l1l2Messages.l2ForwarderFactoryRetryable.messageData.data
)
const l2ForwarderAddress = this.l2ForwarderAddress(
l2ForwarderParams.owner,
l2ForwarderParams.routerOrInbox,
l2ForwarderParams.to,
params.l2Provider
)
const teleportParams = this._decodeTeleportCalldata(tx.data)
return {
teleportParams,
l2ForwarderParams,
l2ForwarderAddress,
}
}
/**
* Fetch the cross chain messages and their status
*
* Can provide either the txHash, the tx, or the txReceipt
*/
public async getDepositStatus(
params: GetL1L3DepositStatusParams
): Promise<Erc20L1L3DepositStatus> {
await this._checkL1Network(params.l1Provider)
await this._checkL2Network(params.l2Provider)
await this._checkL3Network(params.l3Provider)
const l1TxReceipt = await this._getTxReceiptFromTxRef(
params,
params.l1Provider
)
const partialResult = await this._getL1ToL2Messages(
l1TxReceipt,
params.l2Provider
)
const factoryRedeem =
await partialResult.l2ForwarderFactoryRetryable.getSuccessfulRedeem()
const l2l3Message =
factoryRedeem.status === ParentToChildMessageStatus.REDEEMED
? (
await new ParentTransactionReceipt(
factoryRedeem.childTxReceipt
).getParentToChildMessages(params.l3Provider)
)[0]
: undefined
// check if we got a race condition where another teleportation front ran the l2 forwarder factory call
// if the balance is 0, l1l2TokenBridgeRetryable is redeemed, and l2ForwarderFactoryRetryable failed,
// then another teleportation front ran the l2 forwarder factory call
// set a flag to indicate this
let l2ForwarderFactoryRetryableFrontRan = false
const l1l2TokenBridgeRetryableStatus =
await partialResult.l1l2TokenBridgeRetryable.status()
if (
l1l2TokenBridgeRetryableStatus === ParentToChildMessageStatus.REDEEMED &&
factoryRedeem.status ===
ParentToChildMessageStatus.FUNDS_DEPOSITED_ON_CHILD
) {
// decoding the factory call is the most reliable way to get the owner and other parameters
const decodedFactoryCall = this._decodeCallForwarderCalldata(
partialResult.l2ForwarderFactoryRetryable.messageData.data
)
// get the token balance of the l2 forwarder
// we only do this check if the token bridge retryable has been redeemed, otherwise the token might not exist
const balance = await IERC20__factory.connect(
decodedFactoryCall.l2Token,
params.l2Provider
).balanceOf(
await this.l2ForwarderAddress(
decodedFactoryCall.owner,
decodedFactoryCall.routerOrInbox,
decodedFactoryCall.to,
params.l2Provider
)
)
l2ForwarderFactoryRetryableFrontRan = balance.isZero()
}
return {
...partialResult,
l2l3TokenBridgeRetryable: l2l3Message,
l2ForwarderFactoryRetryableFrontRan,
completed:
(await l2l3Message?.status()) === ParentToChildMessageStatus.REDEEMED,
}
}
/**
* Get the type of teleportation from the l1Token and l3FeeTokenL1Addr teleport parameters
*/
public teleportationType(
partialTeleportParams: Pick<
IL1Teleporter.TeleportParamsStruct,
'l3FeeTokenL1Addr' | 'l1Token'
>
) {
if (
partialTeleportParams.l3FeeTokenL1Addr === ethers.constants.AddressZero
) {
return TeleportationType.Standard
} else if (
getAddress(partialTeleportParams.l1Token) ===
getAddress(partialTeleportParams.l3FeeTokenL1Addr)
) {
return TeleportationType.OnlyGasToken
} else {
return TeleportationType.NonGasTokenToCustomGas
}
}
/**
* Estimate the gasLimit and maxSubmissionFee for a token bridge retryable
*/
protected async _getTokenBridgeGasEstimates(params: {
parentProvider: Provider
childProvider: Provider
parentGasPrice: BigNumber
parentErc20Address: string
parentGatewayAddress: string
from: string
to: string
amount: BigNumber
isWeth: boolean
}): Promise<RetryableGasValues> {
const parentGateway = L1GatewayRouter__factory.connect(
params.parentGatewayAddress,
params.parentProvider
)
const outboundCalldata = await parentGateway.getOutboundCalldata(
params.parentErc20Address,
params.from,
params.to,
params.amount,
'0x'
)
const estimates = await new ParentToChildMessageGasEstimator(
params.childProvider
).estimateAll(
{
to: await parentGateway.counterpartGateway(),
data: outboundCalldata,
from: parentGateway.address,
l2CallValue: params.isWeth ? params.amount : BigNumber.from(0),
excessFeeRefundAddress: params.to,
callValueRefundAddress: new Address(params.from).applyAlias().value,
},
params.parentGasPrice,
params.parentProvider
)
return {
gasLimit: estimates.gasLimit,
maxSubmissionFee: estimates.maxSubmissionCost,
}
}
/**
* Estimate the gasLimit and maxSubmissionFee for the L1 to L2 token bridge leg of a teleportation
*/
protected async _getL1L2TokenBridgeGasEstimates(params: {
l1Token: string
amount: BigNumberish
l1GasPrice: BigNumber
l2ForwarderAddress: string
l1Provider: Provider
l2Provider: Provider
}): Promise<RetryableGasValues> {
assertArbitrumNetworkHasTokenBridge(this.l2Network)
const parentGatewayAddress = await this.getL1L2GatewayAddress(
params.l1Token,
params.l1Provider
)
return this._getTokenBridgeGasEstimates({
parentProvider: params.l1Provider,
childProvider: params.l2Provider,
parentGasPrice: params.l1GasPrice,
parentErc20Address: params.l1Token,
parentGatewayAddress,
from: this.teleporter.l1Teleporter,
to: params.l2ForwarderAddress,
amount: BigNumber.from(params.amount),
isWeth:
getAddress(parentGatewayAddress) ===
getAddress(this.l2Network.tokenBridge.parentWethGateway),
})
}
/**
* Estimate the gasLimit and maxSubmissionFee for the L1 to L2 fee token bridge leg of a teleportation
*/
protected async _getL1L2FeeTokenBridgeGasEstimates(params: {
l1GasPrice: BigNumber
feeTokenAmount: BigNumber
l3FeeTokenL1Addr: string
l2ForwarderAddress: string
l1Provider: Provider
l2Provider: Provider
}): Promise<RetryableGasValues> {
assertArbitrumNetworkHasTokenBridge(this.l2Network)
if (params.l3FeeTokenL1Addr === this.skipL1GasTokenMagic) {
return {
gasLimit: BigNumber.from(0),
maxSubmissionFee: BigNumber.from(0),
}
}
const parentGatewayAddress = await this.getL1L2GatewayAddress(
params.l3FeeTokenL1Addr,
params.l1Provider
)
return this._getTokenBridgeGasEstimates({
parentProvider: params.l1Provider,
childProvider: params.l2Provider,
parentGasPrice: params.l1GasPrice,
parentErc20Address: params.l3FeeTokenL1Addr,
parentGatewayAddress,
from: this.teleporter.l1Teleporter,
to: params.l2ForwarderAddress,
amount: params.feeTokenAmount,
isWeth:
getAddress(parentGatewayAddress) ===
getAddress(this.l2Network.tokenBridge.parentWethGateway),
})
}
/**
* Estimate the gasLimit and maxSubmissionFee for L2ForwarderFactory.callForwarder leg of a teleportation.
* Gas limit is hardcoded to 1,000,000
*/
protected async _getL2ForwarderFactoryGasEstimates(
l1GasPrice: BigNumber,
l1Provider: Provider
): Promise<RetryableGasValues> {
const inbox = Inbox__factory.connect(
this.l2Network.ethBridge.inbox,
l1Provider
)
const maxSubmissionFee = await inbox.calculateRetryableSubmissionFee(
this._l2ForwarderFactoryCalldataSize(),
l1GasPrice
)
return {
gasLimit: this.l2ForwarderFactoryDefaultGasLimit,
maxSubmissionFee,
}
}
/**
* Estimate the gasLimit and maxSubmissionFee for the L2 -> L3 leg of a teleportation.
*/
protected async _getL2L3BridgeGasEstimates(params: {
partialTeleportParams: OmitTyped<
IL1Teleporter.TeleportParamsStruct,
'gasParams'
>
l2GasPrice: BigNumber
l1Provider: Provider
l2Provider: Provider
l3Provider: Provider
l2ForwarderAddress: string
}): Promise<RetryableGasValues> {
assertArbitrumNetworkHasTokenBridge(this.l3Network)
const teleportationType = this.teleportationType(
params.partialTeleportParams
)
if (
teleportationType === TeleportationType.NonGasTokenToCustomGas &&
params.partialTeleportParams.l3FeeTokenL1Addr === this.skipL1GasTokenMagic
) {
// we aren't paying for the retryable to L3
return {
gasLimit: BigNumber.from(0),
maxSubmissionFee: BigNumber.from(0),
}
} else if (teleportationType === TeleportationType.OnlyGasToken) {
// we are bridging the fee token to l3, this will not go through the l2l3 token bridge, instead it's just a regular retryable
const estimate = await new ParentToChildMessageGasEstimator(
params.l3Provider
).estimateAll(
{
to: params.partialTeleportParams.to,
data: '0x',
from: params.l2ForwarderAddress,
// l2CallValue will be amount less the fees in reality
l2CallValue: BigNumber.from(params.partialTeleportParams.amount),
excessFeeRefundAddress: params.partialTeleportParams.to,
callValueRefundAddress: params.partialTeleportParams.to,
},
params.l2GasPrice,
params.l2Provider
)
return {
gasLimit: estimate.gasLimit,
maxSubmissionFee: estimate.maxSubmissionCost,
}
} else {
// we are bridging a non fee token to l3, this will go through the token bridge
const parentGatewayAddress = await this.getL2L3GatewayAddress(
params.partialTeleportParams.l1Token,
params.l1Provider,
params.l2Provider
)
return this._getTokenBridgeGasEstimates({
parentProvider: params.l2Provider,
childProvider: params.l3Provider,
parentGasPrice: params.l2GasPrice,
parentErc20Address: await this.getL2Erc20Address(
params.partialTeleportParams.l1Token,
params.l1Provider
),
parentGatewayAddress,
from: params.l2ForwarderAddress,
to: params.partialTeleportParams.to,
amount: BigNumber.from(params.partialTeleportParams.amount),
isWeth:
getAddress(parentGatewayAddress) ===
getAddress(this.l3Network.tokenBridge.parentWethGateway),
})
}
}
/**
* Given TeleportParams without the gas parameters, return TeleportParams with gas parameters populated.
* Does not modify the input parameters.
*/
protected async _fillPartialTeleportParams(
partialTeleportParams: OmitTyped<
IL1Teleporter.TeleportParamsStruct,
'gasParams'
>,
retryableOverrides: Erc20L1L3DepositRequestRetryableOverrides,
l1Provider: Provider,
l2Provider: Provider,
l3Provider: Provider
) {
// get gasLimit and submission cost for a retryable while respecting overrides
const getRetryableGasValuesWithOverrides = async (
overrides: TeleporterRetryableGasOverride | undefined,
getEstimates: () => Promise<RetryableGasValues>
): Promise<RetryableGasValues> => {
let base: RetryableGasValues
if (overrides?.gasLimit?.base && overrides?.maxSubmissionFee?.base) {
base = {
gasLimit: overrides.gasLimit.base,
maxSubmissionFee: overrides.maxSubmissionFee.base,
}
} else {
const calculated = await getEstimates()
base = {
gasLimit: overrides?.gasLimit?.base || calculated.gasLimit,
maxSubmissionFee:
overrides?.maxSubmissionFee?.base || calculated.maxSubmissionFee,
}
}
const gasLimit = this._percentIncrease(
base.gasLimit,
overrides?.gasLimit?.percentIncrease ||
this.defaultGasLimitPercentIncrease
)
const submissionFee = this._percentIncrease(
base.maxSubmissionFee,
overrides?.maxSubmissionFee?.percentIncrease || BigNumber.from(0)
)
const minGasLimit = overrides?.gasLimit?.min || BigNumber.from(0)
return {
gasLimit: gasLimit.gt(minGasLimit) ? gasLimit : minGasLimit,
maxSubmissionFee: submissionFee,
}
}
// get gas price while respecting overrides
const applyGasPercentIncrease = async (
overrides: PercentIncrease | undefined,
getEstimate: () => Promise<BigNumber>
) => {
return this._percentIncrease(
overrides?.base || (await getEstimate()),
overrides?.percentIncrease || this.defaultGasPricePercentIncrease
)
}
const l1GasPrice = await applyGasPercentIncrease(
retryableOverrides.l1GasPrice,
() => l1Provider.getGasPrice()
)
const l2GasPrice = await applyGasPercentIncrease(
retryableOverrides.l2GasPrice,
() => l2Provider.getGasPrice()
)
const l3GasPrice =
partialTeleportParams.l3FeeTokenL1Addr === this.skipL1GasTokenMagic
? BigNumber.from(0)
: await applyGasPercentIncrease(retryableOverrides.l3GasPrice, () =>
l3Provider.getGasPrice()
)
const fakeRandomL2Forwarder = ethers.utils.hexlify(
ethers.utils.randomBytes(20)
)
const l1l2TokenBridgeGasValues = await getRetryableGasValuesWithOverrides(
retryableOverrides.l1l2TokenBridgeRetryableGas,
() =>
this._getL1L2TokenBridgeGasEstimates({
l1Token: partialTeleportParams.l1Token,
amount: partialTeleportParams.amount,
l1GasPrice,
l2ForwarderAddress: fakeRandomL2Forwarder,
l1Provider,
l2Provider,
})
)
const l2ForwarderFactoryGasValues =
await getRetryableGasValuesWithOverrides(
retryableOverrides.l2ForwarderFactoryRetryableGas,
() => this._getL2ForwarderFactoryGasEstimates(l1GasPrice, l1Provider)
)
const l2l3TokenBridgeGasValues = await getRetryableGasValuesWithOverrides(
retryableOverrides.l2l3TokenBridgeRetryableGas,
() =>
this._getL2L3BridgeGasEstimates({
partialTeleportParams,
l2GasPrice,
l1Provider,
l2Provider,
l3Provider,
l2ForwarderAddress: fakeRandomL2Forwarder,
})
)
let l1l2FeeTokenBridgeGasValues: RetryableGasValues
if (
this.teleportationType(partialTeleportParams) ===
TeleportationType.NonGasTokenToCustomGas
) {
l1l2FeeTokenBridgeGasValues = await getRetryableGasValuesWithOverrides(
retryableOverrides.l1l2GasTokenBridgeRetryableGas,
() =>
this._getL1L2FeeTokenBridgeGasEstimates({
l1GasPrice,
feeTokenAmount: l2l3TokenBridgeGasValues.gasLimit
.mul(l3GasPrice)
.add(l2l3TokenBridgeGasValues.maxSubmissionFee),
l3FeeTokenL1Addr: partialTeleportParams.l3FeeTokenL1Addr,
l2ForwarderAddress: fakeRandomL2Forwarder,
l1Provider,
l2Provider,
})
)
} else {
// eth fee l3, or only bridging fee token. this retryable will not be created
l1l2FeeTokenBridgeGasValues = {
gasLimit: BigNumber.from(0),
maxSubmissionFee: BigNumber.from(0),
}
}
const gasParams: IL1Teleporter.RetryableGasParamsStruct = {
l2GasPriceBid: l2GasPrice,
l3GasPriceBid: l3GasPrice,
l1l2TokenBridgeGasLimit: l1l2TokenBridgeGasValues.gasLimit,
l1l2FeeTokenBridgeGasLimit: l1l2FeeTokenBridgeGasValues.gasLimit,
l2l3TokenBridgeGasLimit: l2l3TokenBridgeGasValues.gasLimit,
l2ForwarderFactoryGasLimit: l2ForwarderFactoryGasValues.gasLimit,
l2ForwarderFactoryMaxSubmissionCost:
l2ForwarderFactoryGasValues.maxSubmissionFee,
l1l2TokenBridgeMaxSubmissionCost:
l1l2TokenBridgeGasValues.maxSubmissionFee,
l1l2FeeTokenBridgeMaxSubmissionCost:
l1l2FeeTokenBridgeGasValues.maxSubmissionFee,
l2l3TokenBridgeMaxSubmissionCost:
l2l3TokenBridgeGasValues.maxSubmissionFee,
}
const teleportParams = {
...partialTeleportParams,
gasParams,
}
const costs = await IL1Teleporter__factory.connect(
this.teleporter.l1Teleporter,
l1Provider
).determineTypeAndFees(teleportParams)
return {
teleportParams,
costs,
}
}
/**
* @returns The size of the calldata for a call to L2ForwarderFactory.callForwarder
*/
protected _l2ForwarderFactoryCalldataSize() {
const struct: IL2Forwarder.L2ForwarderParamsStruct = {
owner: ethers.constants.AddressZero,
l2Token: ethers.constants.AddressZero,
l3FeeTokenL2Addr: ethers.constants.AddressZero,
routerOrInbox: ethers.constants.AddressZero,
to: ethers.constants.AddressZero,
gasLimit: 0,
gasPriceBid: 0,
maxSubmissionCost: 0,
}
const dummyCalldata =
IL2ForwarderFactory__factory.createInterface().encodeFunctionData(
'callForwarder',
[struct]
)
return ethers.utils.hexDataLength(dummyCalldata) - 4
}
/**
* Given raw calldata for a teleport tx, decode the teleport parameters
*/
protected _decodeTeleportCalldata(
data: string
): IL1Teleporter.TeleportParamsStruct {
const iface = IL1Teleporter__factory.createInterface()
const decoded = iface.parseTransaction({ data })
if (decoded.functionFragment.name !== 'teleport') {
throw new ArbSdkError(`not a teleport tx`)
}
return decoded.args[0]
}
/**
* Given raw calldata for a callForwarder call, decode the parameters
*/
protected _decodeCallForwarderCalldata(
data: string
): IL2Forwarder.L2ForwarderParamsStruct {
const iface = IL2ForwarderFactory__factory.createInterface()
const decoded = iface.parseTransaction({ data })
if (decoded.functionFragment.name !== 'callForwarder') {
throw new ArbSdkError(`not callForwarder data`)
}
return decoded.args[0]
}
protected async _getL1ToL2Messages(
l1TxReceipt: ParentContractCallTransactionReceipt,
l2Provider: Provider
) {
const l1l2Messages = await l1TxReceipt.getParentToChildMessages(l2Provider)
let partialResult: {
l1l2TokenBridgeRetryable: ParentToChildMessageReader
l1l2GasTokenBridgeRetryable: ParentToChildMessageReader | undefined
l2ForwarderFactoryRetryable: ParentToChildMessageReader
}
if (l1l2Messages.length === 2) {
partialResult = {
l1l2TokenBridgeRetryable: l1l2Messages[0],
l2ForwarderFactoryRetryable: l1l2Messages[1],
l1l2GasTokenBridgeRetryable: undefined,
}
} else {
partialResult = {
l1l2GasTokenBridgeRetryable: l1l2Messages[0],
l1l2TokenBridgeRetryable: l1l2Messages[1],
l2ForwarderFactoryRetryable: l1l2Messages[2],
}
}
return partialResult
}
}
/**
* Bridge ETH from L1 to L3 using a double retryable ticket
*/
export class EthL1L3Bridger extends BaseL1L3Bridger {
constructor(l3Network: ArbitrumNetwork) {
super(l3Network)
if (
l3Network.nativeToken &&
l3Network.nativeToken !== ethers.constants.AddressZero
) {
throw new ArbSdkError(
`L3 network ${l3Network.name} uses a custom fee token`
)
}
}
/**
* Get a tx request to deposit ETH to L3 via a double retryable ticket
*/
public async getDepositRequest(
params: EthL1L3DepositRequestParams &
(
| {
from: string
l1Provider: Provider
}
| { l1Signer: Signer }
)
): Promise<ParentToChildTransactionRequest> {
const l1Provider =
'l1Provider' in params ? params.l1Provider : params.l1Signer.provider!
await this._checkL1Network(l1Provider)
await this._checkL2Network(params.l2Provider)
await this._checkL3Network(params.l3Provider)
const from =
'from' in params ? params.from : await params.l1Signer.getAddress()
const l3DestinationAddress = params.destinationAddress || from
const l2RefundAddress = params.l2RefundAddress || from
const l3TicketRequest =
await ParentToChildMessageCreator.getTicketCreationRequest(
{
to: l3DestinationAddress,
data: '0x',
from: new Address(from).applyAlias().value,
l2CallValue: BigNumber.from(params.amount),
excessFeeRefundAddress: l3DestinationAddress,
callValueRefundAddress: l3DestinationAddress,
},
params.l2Provider,
params.l3Provider,
params.l3TicketGasOverrides
)
const l2TicketRequest =
await ParentToChildMessageCreator.getTicketCreationRequest(
{
from,
to: l3TicketRequest.txRequest.to,
l2CallValue: BigNumber.from(l3TicketRequest.txRequest.value),
data: ethers.utils.hexlify(l3TicketRequest.txRequest.data),
excessFeeRefundAddress: l2RefundAddress,
callValueRefundAddress: l2RefundAddress,
},
l1Provider,
params.l2Provider,
params.l2TicketGasOverrides
)
return l2TicketRequest
}
/**
* Deposit ETH to L3 via a double retryable ticket
*/
public async deposit(
params:
| (EthL1L3DepositRequestParams & {
l1Signer: Signer
overrides?: PayableOverrides
})
| TxRequestParams
): Promise<ParentContractCallTransaction> {
await this._checkL1Network(params.l1Signer)
const depositRequest =
'txRequest' in params
? params.txRequest
: (await this.getDepositRequest(params)).txRequest
const tx = await params.l1Signer.sendTransaction({
...depositRequest,
...params.overrides,
})
return ParentTransactionReceipt.monkeyPatchContractCallWait(tx)
}
/**
* Given an L1 transaction, get the retryable parameters for both l2 and l3 tickets
*/
public async getDepositParameters(
params: {
l1Provider: Provider
} & TxReference
) {
await this._checkL1Network(params.l1Provider)
const tx = await this._getTxFromTxRef(params, params.l1Provider)
const l1l2TicketData: RetryableMessageParams = {
...this._decodeCreateRetryableTicket(tx.data),
l1Value: tx.value,
}
const l2l3TicketData: RetryableMessageParams = {
...this._decodeCreateRetryableTicket(l1l2TicketData.data),
l1Value: l1l2TicketData.l2CallValue,
}
return {
l1l2TicketData,
l2l3TicketData,
}
}
/**
* Get the status of a deposit given an L1 tx receipt. Does not check if the tx is actually a deposit tx.
*
* @return Information regarding each step of the deposit
* and `EthL1L3DepositStatus.completed` which indicates whether the deposit has fully completed.
*/
public async getDepositStatus(
params: GetL1L3DepositStatusParams
): Promise<EthL1L3DepositStatus> {
await this._checkL1Network(params.l1Provider)
await this._checkL2Network(params.l2Provider)
await this._checkL3Network(params.l3Provider)
const l1TxReceipt = await this._getTxReceiptFromTxRef(
params,
params.l1Provider
)
const l1l2Message = (
await l1TxReceipt.getParentToChildMessages(params.l2Provider)
)[0]
const l1l2Redeem = await l1l2Message.getSuccessfulRedeem()
if (l1l2Redeem.status != ParentToChildMessageStatus.REDEEMED) {
return {
l2Retryable: l1l2Message,
l3Retryable: undefined,
completed: false,
}
}
const l2l3Message = (
await new ParentEthDepositTransactionReceipt(
l1l2Redeem.childTxReceipt
).getParentToChildMessages(params.l3Provider)
)[0]
if (l2l3Message === undefined) {
throw new ArbSdkError(`L2 to L3 message not found`)
}
return {
l2Retryable: l1l2Message,
l3Retryable: l2l3Message,
completed:
(await l2l3Message.status()) === ParentToChildMessageStatus.REDEEMED,
}
}
protected _decodeCreateRetryableTicket(
data: string
): OmitTyped<RetryableMessageParams, 'l1Value'> {
const iface = IInbox__factory.createInterface()
const decoded = iface.parseTransaction({ data })
if (decoded.functionFragment.name !== 'createRetryableTicket') {
throw new ArbSdkError(`not createRetryableTicket data`)
}
const args = decoded.args
return {
destAddress: args[0],
l2CallValue: args[1],
maxSubmissionFee: args[2],
excessFeeRefundAddress: args[3],
callValueRefundAddress: args[4],
gasLimit: args[5],
maxFeePerGas: args[6],
data: args[7],
}
}
}
================================================
File: packages/sdk/src/lib/dataEntities/address.ts
================================================
import { getAddress } from '@ethersproject/address'
import { utils } from 'ethers'
import { ADDRESS_ALIAS_OFFSET } from './constants'
import { ArbSdkError } from './errors'
/**
* Ethereum/Arbitrum address class
*/
export class Address {
private readonly ADDRESS_ALIAS_OFFSET_BIG_INT = BigInt(ADDRESS_ALIAS_OFFSET)
private readonly ADDRESS_BIT_LENGTH = 160
private readonly ADDRESS_NIBBLE_LENGTH = this.ADDRESS_BIT_LENGTH / 4
/**
* Ethereum/Arbitrum address class
* @param value A valid Ethereum address. Doesn't need to be checksum cased.
*/
constructor(public readonly value: string) {
if (!utils.isAddress(value))
throw new ArbSdkError(`'${value}' is not a valid address`)
}
private alias(address: string, forward: boolean) {
// we use BigInts in here to allow for proper under/overflow behaviour
// BigInt.asUintN calculates the correct positive modulus
return getAddress(
'0x' +
BigInt.asUintN(
this.ADDRESS_BIT_LENGTH,
forward
? BigInt(address) + this.ADDRESS_ALIAS_OFFSET_BIG_INT
: BigInt(address) - this.ADDRESS_ALIAS_OFFSET_BIG_INT
)
.toString(16)
.padStart(this.ADDRESS_NIBBLE_LENGTH, '0')
)
}
/**
* Find the L2 alias of an L1 address
* @returns
*/
public applyAlias(): Address {
return new Address(this.alias(this.value, true))
}
/**
* Find the L1 alias of an L2 address
* @returns
*/
public undoAlias(): Address {
return new Address(this.alias(this.value, false))
}
public equals(other: Address): boolean {
return this.value.toLowerCase() === other.value.toLowerCase()
}
}
================================================
File: packages/sdk/src/lib/dataEntities/constants.ts
================================================
/*
* Copyright 2021, Offchain Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env node */
'use strict'
export const NODE_INTERFACE_ADDRESS =
'0x00000000000000000000000000000000000000C8'
export const ARB_SYS_ADDRESS = '0x0000000000000000000000000000000000000064'
export const ARB_RETRYABLE_TX_ADDRESS =
'0x000000000000000000000000000000000000006E'
export const ARB_ADDRESS_TABLE_ADDRESS =
'0x0000000000000000000000000000000000000066'
export const ARB_OWNER_PUBLIC = '0x000000000000000000000000000000000000006B'
export const ARB_GAS_INFO = '0x000000000000000000000000000000000000006C'
export const ARB_STATISTICS = '0x000000000000000000000000000000000000006F'
export const ARB_MINIMUM_BLOCK_TIME_IN_SECONDS = 0.25
/**
* The offset added to an L1 address to get the corresponding L2 address
*/
export const ADDRESS_ALIAS_OFFSET = '0x1111000000000000000000000000000000001111'
/**
* Address of the gateway a token will be assigned to if it is disabled
*/
export const DISABLED_GATEWAY = '0x0000000000000000000000000000000000000001'
/**
* If a custom token is enabled for arbitrum it will implement a function called
* isArbitrumEnabled which returns this value. Intger: 0xa4b1
*/
export const CUSTOM_TOKEN_IS_ENABLED = 42161
export const SEVEN_DAYS_IN_SECONDS = 7 * 24 * 60 * 60
/**
* How long to wait (in milliseconds) for a deposit to arrive before timing out a request.
*
* Finalisation on mainnet can be up to 2 epochs = 64 blocks.
* We add 10 minutes for the system to create and redeem the ticket, plus some extra buffer of time.
*
* Total timeout: 30 minutes.
*/
export const DEFAULT_DEPOSIT_TIMEOUT = 30 * 60 * 1000
/**
* The L1 block at which Nitro was activated for Arbitrum One.
*
* @see https://etherscan.io/block/15447158
*/
export const ARB1_NITRO_GENESIS_L1_BLOCK = 15447158
/**
* The L2 block at which Nitro was activated for Arbitrum One.
*
* @see https://arbiscan.io/block/22207817
*/
export const ARB1_NITRO_GENESIS_L2_BLOCK = 22207817
================================================
File: packages/sdk/src/lib/dataEntities/errors.ts
================================================
/*
* Copyright 2021, Offchain Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env node */
'use strict'
/**
* Errors originating in Arbitrum SDK
*/
export class ArbSdkError extends Error {
constructor(message: string, public readonly inner?: Error) {
super(message)
if (inner) {
this.stack += '\nCaused By: ' + inner.stack
}
}
}
/**
* Thrown when a signer does not have a connected provider
*/
export class MissingProviderArbSdkError extends ArbSdkError {
constructor(signerName: string) {
super(
`${signerName} does not have a connected provider and one is required.`
)
}
}
================================================
File: packages/sdk/src/lib/dataEntities/event.ts
================================================
import { TypedEvent, TypedEventFilter } from '../abi/common'
import { Contract } from 'ethers'
import { Provider, Log } from '@ethersproject/abstract-provider'
import { Interface } from 'ethers/lib/utils'
/**
* The type of the event arguments.
* Gets the second generic arg
*/
export type EventArgs<T> = T extends TypedEvent<infer _, infer TObj>
? TObj
: never
/**
* The event type of a filter
* Gets the first generic arg
*/
export type EventFromFilter<TFilter> = TFilter extends TypedEventFilter<
infer TEvent
>
? TEvent
: never
/**
* All filter keys for the provided contract
*/
type FilterName<TContract extends Contract> = keyof TContract['filters'] &
string
/**
* The event type of a given filter
*/
type EventType<
TContract extends Contract,
TFilterName extends keyof TContract['filters']
> = EventArgs<EventFromFilter<ReturnType<TContract['filters'][TFilterName]>>>
/**
* Typechain contract factories have additional properties
*/
export type TypeChainContractFactory<TContract extends Contract> = {
connect(address: string, provider: Provider): TContract
createInterface(): Interface
}
/**
* Parse a log that matches a given filter name.
* @param contractFactory
* @param log The log to parse
* @param filterName
* @returns Null if filter name topic does not match log topic
*/
export const parseTypedLog = <
TContract extends Contract,
TFilterName extends FilterName<TContract>
>(
contractFactory: TypeChainContractFactory<TContract>,
log: Log,
filterName: TFilterName
): EventType<TContract, TFilterName> | null => {
const iFace = contractFactory.createInterface()
const event = iFace.getEvent(filterName)
const topic = iFace.getEventTopic(event)
if (log.topics[0] === topic) {
return iFace.parseLog(log).args as EventType<TContract, TFilterName>
} else return null
}
/**
* Parses an array of logs.
* Filters out any logs whose topic does not match provided the filter name topic.
* @param contractFactory
* @param logs The logs to parse
* @param filterName
* @returns
*/
export const parseTypedLogs = <
TContract extends Contract,
TFilterName extends FilterName<TContract>
>(
contractFactory: TypeChainContractFactory<TContract>,
logs: Log[],
filterName: TFilterName
): EventType<TContract, TFilterName>[] => {
return logs
.map(l => parseTypedLog(contractFactory, l, filterName))
.filter((i): i is NonNullable<typeof i> => i !== null)
}
================================================
File: packages/sdk/src/lib/dataEntities/message.ts
================================================
import { BigNumber } from '@ethersproject/bignumber'
/**
* The components of a submit retryable message. Can be parsed from the
* events emitted from the Inbox.
*/
export interface RetryableMessageParams {
/**
* Destination address for L2 message
*/
destAddress: string
/**
* Call value in L2 message
*/
l2CallValue: BigNumber
/**
* Value sent at L1
*/
l1Value: BigNumber
/**
* Max gas deducted from L2 balance to cover base submission fee
*/
maxSubmissionFee: BigNumber
/**
* L2 address address to credit (gaslimit x gasprice - execution cost)
*/
excessFeeRefundAddress: string
/**
* Address to credit l2Callvalue on L2 if retryable txn times out or gets cancelled
*/
callValueRefundAddress: string
/**
* Max gas deducted from user's L2 balance to cover L2 execution
*/
gasLimit: BigNumber
/**
* Gas price for L2 execution
*/
maxFeePerGas: BigNumber
/**
* Calldata for of the L2 message
*/
data: string
}
/**
* The inbox message kind as defined in:
* https://github.com/OffchainLabs/nitro/blob/c7f3429e2456bf5ca296a49cec3bb437420bc2bb/contracts/src/libraries/MessageTypes.sol
*/
export enum InboxMessageKind {
L1MessageType_submitRetryableTx = 9,
L1MessageType_ethDeposit = 12,
L2MessageType_signedTx = 4,
}
export enum ChildToParentMessageStatus {
/**
* ArbSys.sendTxToL1 called, but assertion not yet confirmed
*/
UNCONFIRMED,
/**
* Assertion for outgoing message confirmed, but message not yet executed
*/
CONFIRMED,
/**
* Outgoing message executed (terminal state)
*/
EXECUTED,
}
================================================
File: packages/sdk/src/lib/dataEntities/networks.ts
================================================
/*
* Copyright 2021, Offchain Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env node */
;('use strict')
import { Provider } from '@ethersproject/abstract-provider'
import { constants } from 'ethers'
import { SignerOrProvider, SignerProviderUtils } from './signerOrProvider'
import { ArbSdkError } from '../dataEntities/errors'
import { ARB1_NITRO_GENESIS_L2_BLOCK } from './constants'
import { RollupAdminLogic__factory } from '../abi/factories/RollupAdminLogic__factory'
import { Prettify } from '../utils/types'
import { IERC20Bridge__factory } from '../abi/factories/IERC20Bridge__factory'
/**
* Represents an Arbitrum chain, e.g. Arbitrum One, Arbitrum Sepolia, or an L3 chain.
*/
export interface ArbitrumNetwork {
/**
* Name of the chain.
*/
name: string
/**
* Id of the chain.
*/
chainId: number
/**
* Chain id of the parent chain, i.e. the chain on which this chain settles to.
*/
parentChainId: number
/**
* The core contracts
*/
ethBridge: EthBridge
/**
* The token bridge contracts.
*/
tokenBridge?: TokenBridge
/**
* The teleporter contracts.
*/
teleporter?: Teleporter
/**
* The time allowed for validators to dispute or challenge state assertions. Measured in L1 blocks.
*/
confirmPeriodBlocks: number
/**
* Represents how long a retryable ticket lasts for before it expires (in seconds). Defaults to 7 days.
*/
retryableLifetimeSeconds?: number
/**
* In case of a chain that uses ETH as its native/gas token, this is either `undefined` or the zero address
*
* In case of a chain that uses an ERC-20 token from the parent chain as its native/gas token, this is the address of said token on the parent chain
*/
nativeToken?: string
/**
* Whether or not it is a testnet chain.
*/
isTestnet: boolean
/**
* Whether or not the chain was registered by the user.
*/
isCustom: boolean
/**
* Has the network been upgraded to bold. True if yes, otherwise undefined
* This is a temporary property and will be removed in future if Bold is widely adopted and
* the legacy challenge protocol is deprecated
*/
isBold?: boolean
}
/**
* This type is only here for when you want to achieve backwards compatibility between SDK v3 and v4.
*
* Please see {@link ArbitrumNetwork} for the latest type.
*
* @deprecated since v4
*/
export type L2Network = Prettify<
Omit<ArbitrumNetwork, 'chainId' | 'parentChainId' | 'tokenBridge'> & {
chainID: number
partnerChainID: number
tokenBridge: L2NetworkTokenBridge
}
>
export interface Teleporter {
l1Teleporter: string
l2ForwarderFactory: string
}
export interface TokenBridge {
parentGatewayRouter: string
childGatewayRouter: string
parentErc20Gateway: string
childErc20Gateway: string
parentCustomGateway: string
childCustomGateway: string
parentWethGateway: string
childWethGateway: string
parentWeth: string
childWeth: string
parentProxyAdmin?: string
childProxyAdmin?: string
parentMultiCall: string
childMultiCall: string
}
/**
* This type is only here for when you want to achieve backwards compatibility between SDK v3 and v4.
*
* Please see {@link TokenBridge} for the latest type.
*
* @deprecated since v4
*/
export interface L2NetworkTokenBridge {
l1GatewayRouter: string
l2GatewayRouter: string
l1ERC20Gateway: string
l2ERC20Gateway: string
l1CustomGateway: string
l2CustomGateway: string
l1WethGateway: string
l2WethGateway: string
l2Weth: string
l1Weth: string
l1ProxyAdmin: string
l2ProxyAdmin: string
l1MultiCall: string
l2Multicall: string
}
export interface EthBridge {
bridge: string
inbox: string
sequencerInbox: string
outbox: string
rollup: string
classicOutboxes?: {
[addr: string]: number
}
}
const mainnetTokenBridge: TokenBridge = {
parentGatewayRouter: '0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef',
childGatewayRouter: '0x5288c571Fd7aD117beA99bF60FE0846C4E84F933',
parentErc20Gateway: '0xa3A7B6F88361F48403514059F1F16C8E78d60EeC',
childErc20Gateway: '0x09e9222E96E7B4AE2a407B98d48e330053351EEe',
parentCustomGateway: '0xcEe284F754E854890e311e3280b767F80797180d',
childCustomGateway: '0x096760F208390250649E3e8763348E783AEF5562',
parentWethGateway: '0xd92023E9d9911199a6711321D1277285e6d4e2db',
childWethGateway: '0x6c411aD3E74De3E7Bd422b94A27770f5B86C623B',
childWeth: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
parentWeth: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
parentProxyAdmin: '0x9aD46fac0Cf7f790E5be05A0F15223935A0c0aDa',
childProxyAdmin: '0xd570aCE65C43af47101fC6250FD6fC63D1c22a86',
parentMultiCall: '0x5ba1e12693dc8f9c48aad8770482f4739beed696',
childMultiCall: '0x842eC2c7D803033Edf55E478F461FC547Bc54EB2',
}
const mainnetETHBridge: EthBridge = {
bridge: '0x8315177aB297bA92A06054cE80a67Ed4DBd7ed3a',
inbox: '0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f',
sequencerInbox: '0x1c479675ad559DC151F6Ec7ed3FbF8ceE79582B6',
outbox: '0x0B9857ae2D4A3DBe74ffE1d7DF045bb7F96E4840',
rollup: '0x5eF0D09d1E6204141B4d37530808eD19f60FBa35',
classicOutboxes: {
'0x667e23ABd27E623c11d4CC00ca3EC4d0bD63337a': 0,
'0x760723CD2e632826c38Fef8CD438A4CC7E7E1A40': 30,
},
}
/**
* Storage for all Arbitrum networks, either L2 or L3.
*/
const networks: {
[id: string]: ArbitrumNetwork
} = {
42161: {
chainId: 42161,
name: 'Arbitrum One',
parentChainId: 1,
tokenBridge: mainnetTokenBridge,
ethBridge: mainnetETHBridge,
teleporter: {
l1Teleporter: '0xCBd9c6e310D6AaDeF9F025f716284162F0158992',
l2ForwarderFactory: '0x791d2AbC6c3A459E13B9AdF54Fb5e97B7Af38f87',
},
confirmPeriodBlocks: 45818,
isCustom: false,
isTestnet: false,
},
42170: {
chainId: 42170,
confirmPeriodBlocks: 45818,
ethBridge: {
bridge: '0xC1Ebd02f738644983b6C4B2d440b8e77DdE276Bd',
inbox: '0xc4448b71118c9071Bcb9734A0EAc55D18A153949',
outbox: '0xD4B80C3D7240325D18E645B49e6535A3Bf95cc58',
rollup: '0xFb209827c58283535b744575e11953DCC4bEAD88',
sequencerInbox: '0x211E1c4c7f1bF5351Ac850Ed10FD68CFfCF6c21b',
},
isCustom: false,
isTestnet: false,
name: 'Arbitrum Nova',
parentChainId: 1,
tokenBridge: {
parentCustomGateway: '0x23122da8C581AA7E0d07A36Ff1f16F799650232f',
parentErc20Gateway: '0xB2535b988dcE19f9D71dfB22dB6da744aCac21bf',
parentGatewayRouter: '0xC840838Bc438d73C16c2f8b22D2Ce3669963cD48',
parentMultiCall: '0x8896D23AfEA159a5e9b72C9Eb3DC4E2684A38EA3',
parentProxyAdmin: '0xa8f7DdEd54a726eB873E98bFF2C95ABF2d03e560',
parentWeth: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
parentWethGateway: '0xE4E2121b479017955Be0b175305B35f312330BaE',
childCustomGateway: '0xbf544970E6BD77b21C6492C281AB60d0770451F4',
childErc20Gateway: '0xcF9bAb7e53DDe48A6DC4f286CB14e05298799257',
childGatewayRouter: '0x21903d3F8176b1a0c17E953Cd896610Be9fFDFa8',
childMultiCall: '0x5e1eE626420A354BbC9a95FeA1BAd4492e3bcB86',
childProxyAdmin: '0xada790b026097BfB36a5ed696859b97a96CEd92C',
childWeth: '0x722E8BdD2ce80A4422E880164f2079488e115365',
childWethGateway: '0x7626841cB6113412F9c88D3ADC720C9FAC88D9eD',
},
teleporter: {
l1Teleporter: '0xCBd9c6e310D6AaDeF9F025f716284162F0158992',
l2ForwarderFactory: '0x791d2AbC6c3A459E13B9AdF54Fb5e97B7Af38f87',
},
},
421614: {
chainId: 421614,
confirmPeriodBlocks: 20,
ethBridge: {
bridge: '0x38f918D0E9F1b721EDaA41302E399fa1B79333a9',
inbox: '0xaAe29B0366299461418F5324a79Afc425BE5ae21',
outbox: '0x65f07C7D521164a4d5DaC6eB8Fac8DA067A3B78F',
rollup: '0xd80810638dbDF9081b72C1B33c65375e807281C8',
sequencerInbox: '0x6c97864CE4bEf387dE0b3310A44230f7E3F1be0D',
},
isCustom: false,
isTestnet: true,
name: 'Arbitrum Rollup Sepolia Testnet',
parentChainId: 11155111,
tokenBridge: {
parentCustomGateway: '0xba2F7B6eAe1F9d174199C5E4867b563E0eaC40F3',
parentErc20Gateway: '0x902b3E5f8F19571859F4AB1003B960a5dF693aFF',
parentGatewayRouter: '0xcE18836b233C83325Cc8848CA4487e94C6288264',
parentMultiCall: '0xded9AD2E65F3c4315745dD915Dbe0A4Df61b2320',
parentProxyAdmin: '0xDBFC2FfB44A5D841aB42b0882711ed6e5A9244b0',
parentWeth: '0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9',
parentWethGateway: '0xA8aD8d7e13cbf556eE75CB0324c13535d8100e1E',
childCustomGateway: '0x8Ca1e1AC0f260BC4dA7Dd60aCA6CA66208E642C5',
childErc20Gateway: '0x6e244cD02BBB8a6dbd7F626f05B2ef82151Ab502',
childGatewayRouter: '0x9fDD1C4E4AA24EEc1d913FABea925594a20d43C7',
childMultiCall: '0xA115146782b7143fAdB3065D86eACB54c169d092',
childProxyAdmin: '0x715D99480b77A8d9D603638e593a539E21345FdF',
childWeth: '0x980B62Da83eFf3D4576C647993b0c1D7faf17c73',
childWethGateway: '0xCFB1f08A4852699a979909e22c30263ca249556D',
},
teleporter: {
l1Teleporter: '0x9E86BbF020594D7FFe05bF32EEDE5b973579A968',
l2ForwarderFactory: '0x88feBaFBb4E36A4E7E8874E4c9Fd73A9D59C2E7c',
},
},
}
/**
* Determines if a chain is a parent of *any* other chain. Could be an L1 or an L2 chain.
*/
export const isParentNetwork = (
parentChainOrChainId: ArbitrumNetwork | number
): boolean => {
const parentChainId =
typeof parentChainOrChainId === 'number'
? parentChainOrChainId
: parentChainOrChainId.chainId
// Check if there are any chains that have this chain as its parent chain
return getArbitrumNetworks().some(c => c.parentChainId === parentChainId)
}
/**
* Returns a list of children chains for the given chain or chain id.
*/
export const getChildrenForNetwork = (
parentChainOrChainId: ArbitrumNetwork | number
): ArbitrumNetwork[] => {
const parentChainId =
typeof parentChainOrChainId === 'number'
? parentChainOrChainId
: parentChainOrChainId.chainId
return getArbitrumNetworks().filter(
arbitrumChain => arbitrumChain.parentChainId === parentChainId
)
}
/**
* Returns the Arbitrum chain associated with the given signer, provider or chain id.
*
* @note Throws if the chain is not an Arbitrum chain.
*/
export function getArbitrumNetwork(chainId: number): ArbitrumNetwork
export function getArbitrumNetwork(
signerOrProvider: SignerOrProvider
): Promise<ArbitrumNetwork>
export function getArbitrumNetwork(
signerOrProviderOrChainId: SignerOrProvider | number
): ArbitrumNetwork | Promise<ArbitrumNetwork> {
if (typeof signerOrProviderOrChainId === 'number') {
return getArbitrumNetworkByChainId(signerOrProviderOrChainId)
}
return getArbitrumNetworkBySignerOrProvider(signerOrProviderOrChainId)
}
function getArbitrumNetworkByChainId(chainId: number): ArbitrumNetwork {
const network = getArbitrumNetworks().find(n => n.chainId === chainId)
if (!network) {
throw new ArbSdkError(`Unrecognized network ${chainId}.`)
}
return network
}
async function getArbitrumNetworkBySignerOrProvider(
signerOrProvider: SignerOrProvider
): Promise<ArbitrumNetwork> {
const provider = SignerProviderUtils.getProviderOrThrow(signerOrProvider)
const { chainId } = await provider.getNetwork()
return getArbitrumNetworkByChainId(chainId)
}
async function getNativeToken(
bridge: string,
provider: Provider
): Promise<string> {
try {
return await IERC20Bridge__factory.connect(bridge, provider).nativeToken()
} catch (err) {
return constants.AddressZero
}
}
/**
* Returns all Arbitrum networks registered in the SDK, both default and custom.
*/
export function getArbitrumNetworks(): ArbitrumNetwork[] {
return Object.values(networks)
}
export type ArbitrumNetworkInformationFromRollup = Pick<
ArbitrumNetwork,
'parentChainId' | 'confirmPeriodBlocks' | 'ethBridge' | 'nativeToken'
>
/**
* Returns all the information about an Arbitrum network that can be fetched from its Rollup contract.
*
* @param rollupAddress Address of the Rollup contract on the parent chain
* @param parentProvider Provider for the parent chain
*
* @returns An {@link ArbitrumNetworkInformationFromRollup} object
*/
export async function getArbitrumNetworkInformationFromRollup(
rollupAddress: string,
parentProvider: Provider
): Promise<ArbitrumNetworkInformationFromRollup> {
const rollup = RollupAdminLogic__factory.connect(
rollupAddress,
parentProvider
)
const [bridge, inbox, sequencerInbox, outbox, confirmPeriodBlocks] =
await Promise.all([
rollup.bridge(),
rollup.inbox(),
rollup.sequencerInbox(),
rollup.outbox(),
rollup.confirmPeriodBlocks(),
])
return {
parentChainId: (await parentProvider.getNetwork()).chainId,
confirmPeriodBlocks: confirmPeriodBlocks.toNumber(),
ethBridge: {
bridge,
inbox,
sequencerInbox,
outbox,
rollup: rollupAddress,
},
nativeToken: await getNativeToken(bridge, parentProvider),
}
}
/**
* Registers a custom Arbitrum network.
*
* @param network {@link ArbitrumNetwork} to be registered
* @param options Additional options
* @param options.throwIfAlreadyRegistered Whether or not the function should throw if the network is already registered, defaults to `false`
*/
export function registerCustomArbitrumNetwork(
network: ArbitrumNetwork,
options?: { throwIfAlreadyRegistered?: boolean }
): ArbitrumNetwork {
const throwIfAlreadyRegistered = options?.throwIfAlreadyRegistered ?? false
if (!network.isCustom) {
throw new ArbSdkError(
`Custom network ${network.chainId} must have isCustom flag set to true`
)
}
if (typeof networks[network.chainId] !== 'undefined') {
const message = `Network ${network.chainId} already included`
if (throwIfAlreadyRegistered) {
throw new ArbSdkError(message)
}
console.warn(message)
}
// store the network with the rest of the networks
networks[network.chainId] = network
return network
}
/**
* Creates a function that resets the networks index to default. Useful in development.
*/
const createNetworkStateHandler = () => {
const initialState = JSON.parse(JSON.stringify(networks))
return {
resetNetworksToDefault: () => {
Object.keys(networks).forEach(key => delete networks[key])
Object.assign(networks, JSON.parse(JSON.stringify(initialState)))
},
}
}
export function getNitroGenesisBlock(
arbitrumChainOrChainId: ArbitrumNetwork | number
) {
const arbitrumChainId =
typeof arbitrumChainOrChainId === 'number'
? arbitrumChainOrChainId
: arbitrumChainOrChainId.chainId
// all networks except Arbitrum One started off with Nitro
if (arbitrumChainId === 42161) {
return ARB1_NITRO_GENESIS_L2_BLOCK
}
return 0
}
export async function getMulticallAddress(
providerOrChainId: Provider | number
): Promise<string> {
const chains = getArbitrumNetworks()
const chainId =
typeof providerOrChainId === 'number'
? providerOrChainId
: (await providerOrChainId.getNetwork()).chainId
const chain = chains.find(c => c.chainId === chainId)
// The provided chain is found in the list
if (typeof chain !== 'undefined') {
assertArbitrumNetworkHasTokenBridge(chain)
// Return the address of Multicall on the chain
return chain.tokenBridge.childMultiCall
}
// The provided chain is not found in the list
// Try to find a chain that references this chain as its parent
const childChain = chains.find(c => c.parentChainId === chainId)
// No chains reference this chain as its parent
if (typeof childChain === 'undefined') {
throw new Error(
`Failed to retrieve Multicall address for chain: ${chainId}`
)
}
assertArbitrumNetworkHasTokenBridge(childChain)
// Return the address of Multicall on the parent chain
return childChain.tokenBridge.parentMultiCall
}
/**
* Maps the old {@link L2Network.tokenBridge} (from SDK v3) to {@link ArbitrumNetwork.tokenBridge} (from SDK v4).
*/
export function mapL2NetworkTokenBridgeToTokenBridge(
input: L2NetworkTokenBridge
): TokenBridge {
return {
parentGatewayRouter: input.l1GatewayRouter,
childGatewayRouter: input.l2GatewayRouter,
parentErc20Gateway: input.l1ERC20Gateway,
childErc20Gateway: input.l2ERC20Gateway,
parentCustomGateway: input.l1CustomGateway,
childCustomGateway: input.l2CustomGateway,
parentWethGateway: input.l1WethGateway,
childWethGateway: input.l2WethGateway,
parentWeth: input.l1Weth,
childWeth: input.l2Weth,
parentProxyAdmin: input.l1ProxyAdmin,
childProxyAdmin: input.l2ProxyAdmin,
parentMultiCall: input.l1MultiCall,
childMultiCall: input.l2Multicall,
}
}
/**
* Maps the old {@link L2Network} (from SDK v3) to {@link ArbitrumNetwork} (from SDK v4).
*/
export function mapL2NetworkToArbitrumNetwork(
l2Network: L2Network
): ArbitrumNetwork {
return {
// Spread properties
...l2Network,
// Map properties that were changed
chainId: l2Network.chainID,
parentChainId: l2Network.partnerChainID,
tokenBridge: mapL2NetworkTokenBridgeToTokenBridge(l2Network.tokenBridge),
}
}
/**
* Asserts that the given object has a token bridge. This is useful because not all Arbitrum network
* operations require a token bridge.
*
* @param network {@link ArbitrumNetwork} object
* @throws ArbSdkError if the object does not have a token bridge
*/
export function assertArbitrumNetworkHasTokenBridge<T extends ArbitrumNetwork>(
network: T
): asserts network is T & { tokenBridge: TokenBridge } {
if (
typeof network === 'undefined' ||
!('tokenBridge' in network) ||
typeof network.tokenBridge === 'undefined'
) {
throw new ArbSdkError(
`The ArbitrumNetwork object with chainId ${network.chainId} is missing the token bridge contracts addresses. Please add them in the "tokenBridge" property.`
)
}
}
export function isArbitrumNetworkNativeTokenEther(
network: ArbitrumNetwork
): boolean {
return (
typeof network.nativeToken === 'undefined' ||
network.nativeToken === constants.AddressZero
)
}
const { resetNetworksToDefault } = createNetworkStateHandler()
export { resetNetworksToDefault }
================================================
File: packages/sdk/src/lib/dataEntities/retryableData.ts
================================================
import { Interface } from '@ethersproject/abi'
import { BigNumber } from 'ethers'
import { isDefined } from '../utils/lib'
// TODO: add typechain support
const errorInterface = new Interface([
'error RetryableData(address from, address to, uint256 l2CallValue, uint256 deposit, uint256 maxSubmissionCost, address excessFeeRefundAddress, address callValueRefundAddress, uint256 gasLimit, uint256 maxFeePerGas, bytes data)',
])
// CAUTION: this type mirrors the error type above
// The property names must exactly match those above
export interface RetryableData {
from: string
/**
* The address to be called on L2
*/
to: string
/**
* The value to call the L2 address with
*/
l2CallValue: BigNumber
/**
* The total amount to deposit on L1 to cover L2 gas and L2 call value
*/
deposit: BigNumber
/**
* The maximum cost to be paid for submitting the transaction
*/
maxSubmissionCost: BigNumber
/**
* The address to return the any gas that was not spent on fees
*/
excessFeeRefundAddress: string
/**
* The address to refund the call value to in the event the retryable is cancelled, or expires
*/
callValueRefundAddress: string
/**
* The L2 gas limit
*/
gasLimit: BigNumber
/**
* The max gas price to pay on L2
*/
maxFeePerGas: BigNumber
/**
* The data to call the L2 address with
*/
data: string
}
/**
* Tools for parsing retryable data from errors.
* When calling createRetryableTicket on Inbox.sol special values
* can be passed for gasLimit and maxFeePerGas. This causes the call to revert
* with the info needed to estimate the gas needed for a retryable ticket using
* L1ToL2GasPriceEstimator.
*/
export class RetryableDataTools {
/**
* The parameters that should be passed to createRetryableTicket in order to induce
* a revert with retryable data
*/
public static ErrorTriggeringParams = {
gasLimit: BigNumber.from(1),
maxFeePerGas: BigNumber.from(1),
}
private static isErrorData(
maybeErrorData: Error | { errorData: string }
): maybeErrorData is { errorData: string } {
return isDefined((maybeErrorData as { errorData: string }).errorData)
}
private static tryGetErrorData(ethersJsError: Error | { errorData: string }) {
if (this.isErrorData(ethersJsError)) {
return ethersJsError.errorData
} else {
const typedError = ethersJsError as {
data?: string
error?: {
error?: {
body?: string
data?: string
}
}
}
if (typedError.data) {
return typedError.data
} else if (typedError.error?.error?.body) {
const maybeData = (
JSON.parse(typedError.error?.error?.body) as {
error?: {
data?: string
}
}
).error?.data
if (!maybeData) return null
return maybeData
} else if (typedError.error?.error?.data) {
return typedError.error?.error?.data
} else {
return null
}
}
}
/**
* Try to parse a retryable data struct from the supplied ethersjs error, or any explicitly supplied error data
* @param ethersJsErrorOrData
* @returns
*/
public static tryParseError(
ethersJsErrorOrData: Error | { errorData: string } | string
): RetryableData | null {
const errorData =
typeof ethersJsErrorOrData === 'string'
? ethersJsErrorOrData
: this.tryGetErrorData(ethersJsErrorOrData)
if (!errorData) return null
return errorInterface.parseError(errorData).args as unknown as RetryableData
}
}
================================================
File: packages/sdk/src/lib/dataEntities/rpc.ts
================================================
import { TransactionReceipt, Block } from '@ethersproject/providers'
import { BlockWithTransactions } from '@ethersproject/abstract-provider'
import { BigNumber } from 'ethers'
export interface ArbBlockProps {
/**
* The merkle root of the withdrawals tree
*/
sendRoot: string
/**
* Cumulative number of withdrawals since genesis
*/
sendCount: BigNumber
/**
* The l1 block number as seen from within this l2 block
*/
l1BlockNumber: number
}
export type ArbBlock = ArbBlockProps & Block
export type ArbBlockWithTransactions = ArbBlockProps & BlockWithTransactions
/**
* Eth transaction receipt with additional arbitrum specific fields
*/
export interface ArbTransactionReceipt extends TransactionReceipt {
/**
* The l1 block number that would be used for block.number calls
* that occur within this transaction.
* See https://developer.offchainlabs.com/docs/time_in_arbitrum
*/
l1BlockNumber: number
/**
* Amount of gas spent on l1 computation in units of l2 gas
*/
gasUsedForL1: BigNumber
}
================================================
File: packages/sdk/src/lib/dataEntities/signerOrProvider.ts
================================================
import { Provider } from '@ethersproject/abstract-provider'
import { Signer } from '@ethersproject/abstract-signer'
import { ArbSdkError, MissingProviderArbSdkError } from '../dataEntities/errors'
import { isDefined } from '../utils/lib'
export type SignerOrProvider = Signer | Provider
/**
* Utility functions for signer/provider union types
*/
export class SignerProviderUtils {
public static isSigner(
signerOrProvider: SignerOrProvider
): signerOrProvider is Signer {
return isDefined((signerOrProvider as Signer).signMessage)
}
/**
* If signerOrProvider is a provider then return itself.
* If signerOrProvider is a signer then return signer.provider
* @param signerOrProvider
* @returns
*/
public static getProvider(
signerOrProvider: SignerOrProvider
): Provider | undefined {
return this.isSigner(signerOrProvider)
? signerOrProvider.provider
: signerOrProvider
}
public static getProviderOrThrow(
signerOrProvider: SignerOrProvider
): Provider {
const maybeProvider = this.getProvider(signerOrProvider)
if (!maybeProvider) throw new MissingProviderArbSdkError('signerOrProvider')
return maybeProvider
}
/**
* Check if the signer has a connected provider
* @param signer
*/
public static signerHasProvider(
signer: Signer
): signer is Signer & { provider: Provider } {
return isDefined(signer.provider)
}
/**
* Checks that the signer/provider that's provider matches the chain id
* Throws if not.
* @param signerOrProvider
* @param chainId
*/
public static async checkNetworkMatches(
signerOrProvider: SignerOrProvider,
chainId: number
): Promise<void> {
const provider = this.getProvider(signerOrProvider)
if (!provider) throw new MissingProviderArbSdkError('signerOrProvider')
const providerChainId = (await provider.getNetwork()).chainId
if (providerChainId !== chainId) {
throw new ArbSdkError(
`Signer/provider chain id: ${providerChainId} doesn't match provided chain id: ${chainId}.`
)
}
}
}
================================================
File: packages/sdk/src/lib/dataEntities/transactionRequest.ts
================================================
import { TransactionRequest, Provider } from '@ethersproject/providers'
import { BigNumber } from 'ethers'
import {
ParentToChildMessageGasParams,
ParentToChildMessageParams,
} from '../message/ParentToChildMessageCreator'
import { isDefined } from '../utils/lib'
/**
* A transaction request for a transaction that will trigger some sort of
* execution on the child chain
*/
export interface ParentToChildTransactionRequest {
/**
* Core fields needed to form the parent component of the transaction request
*/
txRequest: Required<
Pick<TransactionRequest, 'to' | 'data' | 'value' | 'from'>
>
/**
* Information about the retryable ticket, and it's subsequent execution, that
* will occur on the child chain
*/
retryableData: ParentToChildMessageParams & ParentToChildMessageGasParams
/**
* If this request were sent now, would it have enough margin to reliably succeed
*/
isValid(): Promise<boolean>
}
/**
* A transaction request for a transaction that will trigger a child to parent message
*/
export interface ChildToParentTransactionRequest {
txRequest: Required<
Pick<TransactionRequest, 'to' | 'data' | 'value' | 'from'>
>
/**
* Estimate the gas limit required to execute the withdrawal on the parent chain.
* Note that this is only a rough estimate as it may not be possible to know
* the exact size of the proof straight away, however the real value should be
* within a few thousand gas of this estimate.
*/
estimateParentGasLimit: (l1Provider: Provider) => Promise<BigNumber>
}
/**
* Ensure the T is not of TransactionRequest type by ensure it doesn't have a specific TransactionRequest property
*/
type IsNotTransactionRequest<T> = T extends { txRequest: any } ? never : T
/**
* Check if an object is of ParentToChildTransactionRequest type
* @param possibleRequest
* @returns
*/
export const isParentToChildTransactionRequest = <T>(
possibleRequest: IsNotTransactionRequest<T> | ParentToChildTransactionRequest
): possibleRequest is ParentToChildTransactionRequest => {
return isDefined(
(possibleRequest as ParentToChildTransactionRequest).txRequest
)
}
/**
* Check if an object is of ChildToParentTransactionRequest type
* @param possibleRequest
* @returns
*/
export const isChildToParentTransactionRequest = <T>(
possibleRequest: IsNotTransactionRequest<T> | ChildToParentTransactionRequest
): possibleRequest is ChildToParentTransactionRequest => {
return (
(possibleRequest as ChildToParentTransactionRequest).txRequest != undefined
)
}
================================================
File: packages/sdk/src/lib/inbox/inbox.ts
================================================
/*
* Copyright 2021, Offchain Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env node */
'use strict'
import { Signer } from '@ethersproject/abstract-signer'
import { Block, Provider } from '@ethersproject/abstract-provider'
import { BigNumber, ContractTransaction, ethers, Overrides } from 'ethers'
import { TransactionRequest, JsonRpcProvider } from '@ethersproject/providers'
import { Bridge } from '../abi/Bridge'
import { Bridge__factory } from '../abi/factories/Bridge__factory'
import { SequencerInbox } from '../abi/SequencerInbox'
import { SequencerInbox__factory } from '../abi/factories/SequencerInbox__factory'
import { IInbox__factory } from '../abi/factories/IInbox__factory'
import { RequiredPick } from '../utils/types'
import { MessageDeliveredEvent } from '../abi/Bridge'
import { ArbitrumNetwork } from '../dataEntities/networks'
import { SignerProviderUtils } from '../dataEntities/signerOrProvider'
import { FetchedEvent, EventFetcher } from '../utils/eventFetcher'
import { MultiCaller, CallInput } from '../utils/multicall'
import { ArbSdkError } from '../dataEntities/errors'
import { NodeInterface__factory } from '../abi/factories/NodeInterface__factory'
import { NODE_INTERFACE_ADDRESS } from '../dataEntities/constants'
import { InboxMessageKind } from '../dataEntities/message'
import {
getBlockRangesForL1Block,
isArbitrumChain,
isDefined,
} from '../utils/lib'
import { ArbitrumProvider } from '../utils/arbProvider'
type ForceInclusionParams = FetchedEvent<MessageDeliveredEvent> & {
delayedAcc: string
}
type GasComponentsWithChildPart = {
gasEstimate: BigNumber
gasEstimateForL1: BigNumber
baseFee: BigNumber
l1BaseFeeEstimate: BigNumber
gasEstimateForChild: BigNumber
}
type RequiredTransactionRequestType = RequiredPick<
TransactionRequest,
'data' | 'value'
>
/**
* Tools for interacting with the inbox and bridge contracts
*/
export class InboxTools {
/**
* Parent chain provider
*/
private readonly parentProvider: Provider
constructor(
private readonly parentSigner: Signer,
private readonly childChain: ArbitrumNetwork
) {
this.parentProvider = SignerProviderUtils.getProviderOrThrow(
this.parentSigner
)
}
/**
* Find the first (or close to first) block whose number
* is below the provided number, and whose timestamp is below
* the provided timestamp
* @param blockNumber
* @param blockTimestamp
* @returns
*/
private async findFirstBlockBelow(
blockNumber: number,
blockTimestamp: number
): Promise<Block> {
const isParentChainArbitrum = await isArbitrumChain(this.parentProvider)
if (isParentChainArbitrum) {
const nodeInterface = NodeInterface__factory.connect(
NODE_INTERFACE_ADDRESS,
this.parentProvider
)
try {
blockNumber = (
await nodeInterface.l2BlockRangeForL1(blockNumber - 1)
).firstBlock.toNumber()
} catch (e) {
// l2BlockRangeForL1 reverts if no L2 block exist with the given L1 block number,
// since l1 block is updated in batch sometimes block can be skipped even when there are activities
// alternatively we use binary search to get the nearest block
const _blockNum = (
await getBlockRangesForL1Block({
arbitrumProvider: this.parentProvider as JsonRpcProvider,
forL1Block: blockNumber - 1,
allowGreater: true,
})
)[0]
if (!_blockNum) {
throw e
}
blockNumber = _blockNum
}
}
const block = await this.parentProvider.getBlock(blockNumber)
const diff = block.timestamp - blockTimestamp
if (diff < 0) return block
// we take a long average block time of 12s
// and always move at least 10 blocks
const diffBlocks = Math.max(Math.ceil(diff / 12), 10)
return await this.findFirstBlockBelow(
blockNumber - diffBlocks,
blockTimestamp
)
}
// Check if this request is contract creation or not.
private isContractCreation(
childTransactionRequest: TransactionRequest
): boolean {
if (
childTransactionRequest.to === '0x' ||
!isDefined(childTransactionRequest.to) ||
childTransactionRequest.to === ethers.constants.AddressZero
) {
return true
}
return false
}
/**
* We should use nodeInterface to get the gas estimate is because we
* are making a delayed inbox message which doesn't need parent calldata
* gas fee part.
*/
private async estimateArbitrumGas(
childTransactionRequest: RequiredTransactionRequestType,
childProvider: Provider
): Promise<GasComponentsWithChildPart> {
const nodeInterface = NodeInterface__factory.connect(
NODE_INTERFACE_ADDRESS,
childProvider
)
const contractCreation = this.isContractCreation(childTransactionRequest)
const gasComponents = await nodeInterface.callStatic.gasEstimateComponents(
childTransactionRequest.to || ethers.constants.AddressZero,
contractCreation,
childTransactionRequest.data,
{
from: childTransactionRequest.from,
value: childTransactionRequest.value,
}
)
const gasEstimateForChild: BigNumber = gasComponents.gasEstimate.sub(
gasComponents.gasEstimateForL1
)
return { ...gasComponents, gasEstimateForChild }
}
/**
* Get a range of blocks within messages eligible for force inclusion emitted events
* @param blockNumberRangeSize
* @returns
*/
private async getForceIncludableBlockRange(blockNumberRangeSize: number) {
let currentL1BlockNumber: number | undefined
const sequencerInbox = SequencerInbox__factory.connect(
this.childChain.ethBridge.sequencerInbox,
this.parentProvider
)
const isParentChainArbitrum = await isArbitrumChain(this.parentProvider)
if (isParentChainArbitrum) {
const arbProvider = new ArbitrumProvider(
this.parentProvider as JsonRpcProvider
)
const currentArbBlock = await arbProvider.getBlock('latest')
currentL1BlockNumber = currentArbBlock.l1BlockNumber
}
const multicall = await MultiCaller.fromProvider(this.parentProvider)
const multicallInput: [
CallInput<Awaited<ReturnType<SequencerInbox['maxTimeVariation']>>>,
ReturnType<MultiCaller['getBlockNumberInput']>,
ReturnType<MultiCaller['getCurrentBlockTimestampInput']>
] = [
{
targetAddr: sequencerInbox.address,
encoder: () =>
sequencerInbox.interface.encodeFunctionData('maxTimeVariation'),
decoder: (returnData: string) =>
sequencerInbox.interface.decodeFunctionResult(
'maxTimeVariation',
returnData
)[0],
},
multicall.getBlockNumberInput(),
multicall.getCurrentBlockTimestampInput(),
]
const [maxTimeVariation, currentBlockNumber, currentBlockTimestamp] =
await multicall.multiCall(multicallInput, true)
const blockNumber = isParentChainArbitrum
? currentL1BlockNumber!
: currentBlockNumber.toNumber()
const firstEligibleBlockNumber =
blockNumber - maxTimeVariation.delayBlocks.toNumber()
const firstEligibleTimestamp =
currentBlockTimestamp.toNumber() -
maxTimeVariation.delaySeconds.toNumber()
const firstEligibleBlock = await this.findFirstBlockBelow(
firstEligibleBlockNumber,
firstEligibleTimestamp
)
return {
endBlock: firstEligibleBlock.number,
startBlock: firstEligibleBlock.number - blockNumberRangeSize,
}
}
/**
* Look for force includable events in the search range blocks, if no events are found the search range is
* increased incrementally up to the max search range blocks.
* @param bridge
* @param searchRangeBlocks
* @param maxSearchRangeBlocks
* @returns
*/
private async getEventsAndIncreaseRange(
bridge: Bridge,
searchRangeBlocks: number,
maxSearchRangeBlocks: number,
rangeMultiplier: number
): Promise<FetchedEvent<MessageDeliveredEvent>[]> {
const eFetcher = new EventFetcher(this.parentProvider)
// events don't become eligible until they pass a delay
// find a block range which will emit eligible events
const cappedSearchRangeBlocks = Math.min(
searchRangeBlocks,
maxSearchRangeBlocks
)
const blockRange = await this.getForceIncludableBlockRange(
cappedSearchRangeBlocks
)
// get all the events in this range
const events = await eFetcher.getEvents(
Bridge__factory,
b => b.filters.MessageDelivered(),
{
fromBlock: blockRange.startBlock,
toBlock: blockRange.endBlock,
address: bridge.address,
}
)
if (events.length !== 0) return events
else if (cappedSearchRangeBlocks === maxSearchRangeBlocks) return []
else {
return await this.getEventsAndIncreaseRange(
bridge,
searchRangeBlocks * rangeMultiplier,
maxSearchRangeBlocks,
rangeMultiplier
)
}
}
/**
* Find the event of the latest message that can be force include
* @param maxSearchRangeBlocks The max range of blocks to search in.
* Defaults to 3 * 6545 ( = ~3 days) prior to the first eligible block
* @param startSearchRangeBlocks The start range of block to search in.
* Moves incrementally up to the maxSearchRangeBlocks. Defaults to 100;
* @param rangeMultiplier The multiplier to use when increasing the block range
* Defaults to 2.
* @returns Null if non can be found.
*/
public async getForceIncludableEvent(
maxSearchRangeBlocks: number = 3 * 6545,
startSearchRangeBlocks = 100,
rangeMultiplier = 2
): Promise<ForceInclusionParams | null> {
const bridge = Bridge__factory.connect(
this.childChain.ethBridge.bridge,
this.parentProvider
)
// events dont become eligible until they pass a delay
// find a block range which will emit eligible events
const events = await this.getEventsAndIncreaseRange(
bridge,
startSearchRangeBlocks,
maxSearchRangeBlocks,
rangeMultiplier
)
// no events appeared within that time period
if (events.length === 0) return null
// take the last event - as including this one will include all previous events
const eventInfo = events[events.length - 1]
const sequencerInbox = SequencerInbox__factory.connect(
this.childChain.ethBridge.sequencerInbox,
this.parentProvider
)
// has the sequencer inbox already read this latest message
const totalDelayedRead = await sequencerInbox.totalDelayedMessagesRead()
if (totalDelayedRead.gt(eventInfo.event.messageIndex)) {
// nothing to read - more delayed messages have been read than this current index
return null
}
const delayedAcc = await bridge.delayedInboxAccs(
eventInfo.event.messageIndex
)
return { ...eventInfo, delayedAcc: delayedAcc }
}
/**
* Force includes all eligible messages in the delayed inbox.
* The inbox contract doesn't allow a message to be force-included
* until after a delay period has been completed.
* @param messageDeliveredEvent Provide this to include all messages up to this one. Responsibility is on the caller to check the eligibility of this event.
* @returns The force include transaction, or null if no eligible message were found for inclusion
*/
public async forceInclude<T extends ForceInclusionParams | undefined>(
messageDeliveredEvent?: T,
overrides?: Overrides
): Promise<
// if a message delivered event was supplied then we'll definitely return
// a contract transaction or throw an error. If one isnt supplied then we may
// find no eligible events, and so return null
T extends ForceInclusionParams
? ContractTransaction
: ContractTransaction | null
>
public async forceInclude<T extends ForceInclusionParams | undefined>(
messageDeliveredEvent?: T,
overrides?: Overrides
): Promise<ContractTransaction | null> {
const sequencerInbox = SequencerInbox__factory.connect(
this.childChain.ethBridge.sequencerInbox,
this.parentSigner
)
const eventInfo =
messageDeliveredEvent || (await this.getForceIncludableEvent())
if (!eventInfo) return null
const block = await this.parentProvider.getBlock(eventInfo.blockHash)
return await sequencerInbox.functions.forceInclusion(
eventInfo.event.messageIndex.add(1),
eventInfo.event.kind,
[eventInfo.blockNumber, block.timestamp],
eventInfo.event.baseFeeL1,
eventInfo.event.sender,
eventInfo.event.messageDataHash,
// we need to pass in {} because if overrides is undefined it thinks we've provided too many params
overrides || {}
)
}
/**
* Send Child Chain signed tx using delayed inbox, which won't alias the sender's address
* It will be automatically included by the sequencer on Chain, if it isn't included
* within 24 hours, you can force include it
* @param signedTx A signed transaction which can be sent directly to chain,
* you can call inboxTools.signChainMessage to get.
* @returns The parent delayed inbox's transaction itself.
*/
public async sendChildSignedTx(
signedTx: string
): Promise<ContractTransaction | null> {
const delayedInbox = IInbox__factory.connect(
this.childChain.ethBridge.inbox,
this.parentSigner
)
const sendData = ethers.utils.solidityPack(
['uint8', 'bytes'],
[ethers.utils.hexlify(InboxMessageKind.L2MessageType_signedTx), signedTx]
)
return await delayedInbox.functions.sendL2Message(sendData)
}
/**
* Sign a transaction with msg.to, msg.value and msg.data.
* You can use this as a helper to call inboxTools.sendChainSignedMessage
* above.
* @param txRequest A signed transaction which can be sent directly to chain,
* tx.to, tx.data, tx.value must be provided when not contract creation, if
* contractCreation is true, no need provide tx.to. tx.gasPrice and tx.nonce
* can be overrided. (You can also send contract creation transaction by set tx.to
* to zero address or null)
* @param childSigner ethers Signer type, used to sign Chain transaction
* @returns The parent delayed inbox's transaction signed data.
*/
public async signChildTx(
txRequest: RequiredTransactionRequestType,
childSigner: Signer
): Promise<string> {
const tx: RequiredTransactionRequestType = { ...txRequest }
const contractCreation = this.isContractCreation(tx)
if (!isDefined(tx.nonce)) {
tx.nonce = await childSigner.getTransactionCount()
}
//check transaction type (if no transaction type or gasPrice provided, use eip1559 type)
if (tx.type === 1 || tx.gasPrice) {
if (tx.gasPrice) {
tx.gasPrice = await childSigner.getGasPrice()
}
} else {
if (!isDefined(tx.maxFeePerGas)) {
const feeData = await childSigner.getFeeData()
tx.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas!
tx.maxFeePerGas = feeData.maxFeePerGas!
}
tx.type = 2
}
tx.from = await childSigner.getAddress()
tx.chainId = await childSigner.getChainId()
// if this is contract creation, user might not input the to address,
// however, it is needed when we call to estimateArbitrumGas, so
// we add a zero address here.
if (!isDefined(tx.to)) {
tx.to = ethers.constants.AddressZero
}
//estimate gas on child chain
try {
tx.gasLimit = (
await this.estimateArbitrumGas(tx, childSigner.provider!)
).gasEstimateForChild
} catch (error) {
throw new ArbSdkError('execution failed (estimate gas failed)')
}
if (contractCreation) {
delete tx.to
}
return await childSigner.signTransaction(tx)
}
}
================================================
File: packages/sdk/src/lib/message/ChildToParentMessage.ts
================================================
/*
* Copyright 2021, Offchain Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env node */
'use strict'
import { Provider } from '@ethersproject/abstract-provider'
import { Signer } from '@ethersproject/abstract-signer'
import { BigNumber } from '@ethersproject/bignumber'
import { BlockTag } from '@ethersproject/abstract-provider'
import { ContractTransaction, Overrides } from 'ethers'
import {
SignerProviderUtils,
SignerOrProvider,
} from '../dataEntities/signerOrProvider'
import * as classic from './ChildToParentMessageClassic'
import * as nitro from './ChildToParentMessageNitro'
import {
L2ToL1TransactionEvent as ClassicChildToParentTransactionEvent,
L2ToL1TxEvent as NitroChildToParentTransactionEvent,
} from '../abi/ArbSys'
import { isDefined } from '../utils/lib'
import { EventArgs } from '../dataEntities/event'
import { ChildToParentMessageStatus } from '../dataEntities/message'
import {
getArbitrumNetwork,
getNitroGenesisBlock,
} from '../dataEntities/networks'
import { ArbSdkError } from '../dataEntities/errors'
export type ChildToParentTransactionEvent =
| EventArgs<ClassicChildToParentTransactionEvent>
| EventArgs<NitroChildToParentTransactionEvent>
/**
* Conditional type for Signer or Provider. If T is of type Provider
* then ChildToParentMessageReaderOrWriter<T> will be of type ChildToParentMessageReader.
* If T is of type Signer then ChildToParentMessageReaderOrWriter<T> will be of
* type ChildToParentMessageWriter.
*/
export type ChildToParentMessageReaderOrWriter<T extends SignerOrProvider> =
T extends Provider ? ChildToParentMessageReader : ChildToParentMessageWriter
/**
* Base functionality for Child-to-Parent messages
*/
export class ChildToParentMessage {
protected isClassic(
e: ChildToParentTransactionEvent
): e is EventArgs<ClassicChildToParentTransactionEvent> {
return isDefined(
(e as EventArgs<ClassicChildToParentTransactionEvent>).indexInBatch
)
}
/**
* Instantiates a new `ChildToParentMessageWriter` or `ChildToParentMessageReader` object.
*
* @param {SignerOrProvider} parentSignerOrProvider Signer or provider to be used for executing or reading the Child-to-Parent message.
* @param {ChildToParentTransactionEvent} event The event containing the data of the Child-to-Parent message.
* @param {Provider} [parentProvider] Optional. Used to override the Provider which is attached to `ParentSignerOrProvider` in case you need more control. This will be a required parameter in a future major version update.
*/
public static fromEvent<T extends SignerOrProvider>(
parentSignerOrProvider: T,
event: ChildToParentTransactionEvent,
parentProvider?: Provider
): ChildToParentMessageReaderOrWriter<T>
static fromEvent<T extends SignerOrProvider>(
parentSignerOrProvider: T,
event: ChildToParentTransactionEvent,
parentProvider?: Provider
): ChildToParentMessageReader | ChildToParentMessageWriter {
return SignerProviderUtils.isSigner(parentSignerOrProvider)
? new ChildToParentMessageWriter(
parentSignerOrProvider,
event,
parentProvider
)
: new ChildToParentMessageReader(parentSignerOrProvider, event)
}
/**
* Get event logs for ChildToParent transactions.
* @param childProvider
* @param filter Block range filter
* @param position The batchnumber indexed field was removed in nitro and a position indexed field was added.
* For pre-nitro events the value passed in here will be used to find events with the same batchnumber.
* For post nitro events it will be used to find events with the same position.
* @param destination The parent destination of the ChildToParent message
* @param hash The uniqueId indexed field was removed in nitro and a hash indexed field was added.
* For pre-nitro events the value passed in here will be used to find events with the same uniqueId.
* For post nitro events it will be used to find events with the same hash.
* @param indexInBatch The index in the batch, only valid for pre-nitro events. This parameter is ignored post-nitro
* @returns Any classic and nitro events that match the provided filters.
*/
public static async getChildToParentEvents(
childProvider: Provider,
filter: { fromBlock: BlockTag; toBlock: BlockTag },
position?: BigNumber,
destination?: string,
hash?: BigNumber,
indexInBatch?: BigNumber
): Promise<(ChildToParentTransactionEvent & { transactionHash: string })[]> {
const childChain = await getArbitrumNetwork(childProvider)
const childNitroGenesisBlock = getNitroGenesisBlock(childChain)
const inClassicRange = (blockTag: BlockTag, nitroGenBlock: number) => {
if (typeof blockTag === 'string') {
// taking classic of "earliest", "latest", "earliest" and the nitro gen block
// yields 0, nitro gen, nitro gen since the classic range is always between 0 and nitro gen
switch (blockTag) {
case 'earliest':
return 0
case 'latest':
return nitroGenBlock
case 'pending':
return nitroGenBlock
default:
throw new ArbSdkError(`Unrecognised block tag. ${blockTag}`)
}
}
return Math.min(blockTag, nitroGenBlock)
}
const inNitroRange = (blockTag: BlockTag, nitroGenBlock: number) => {
// taking nitro range of "earliest", "latest", "earliest" and the nitro gen block
// yields nitro gen, latest, pending since the nitro range is always between nitro gen and latest/pending
if (typeof blockTag === 'string') {
switch (blockTag) {
case 'earliest':
return nitroGenBlock
case 'latest':
return 'latest'
case 'pending':
return 'pending'
default:
throw new ArbSdkError(`Unrecognised block tag. ${blockTag}`)
}
}
return Math.max(blockTag, nitroGenBlock)
}
// only fetch nitro events after the genesis block
const classicFilter = {
fromBlock: inClassicRange(filter.fromBlock, childNitroGenesisBlock),
toBlock: inClassicRange(filter.toBlock, childNitroGenesisBlock),
}
const logQueries = []
if (classicFilter.fromBlock !== classicFilter.toBlock) {
logQueries.push(
classic.ChildToParentMessageClassic.getChildToParentEvents(
childProvider,
classicFilter,
position,
destination,
hash,
indexInBatch
)
)
}
const nitroFilter = {
fromBlock: inNitroRange(filter.fromBlock, childNitroGenesisBlock),
toBlock: inNitroRange(filter.toBlock, childNitroGenesisBlock),
}
if (nitroFilter.fromBlock !== nitroFilter.toBlock) {
logQueries.push(
nitro.ChildToParentMessageNitro.getChildToParentEvents(
childProvider,
nitroFilter,
position,
destination,
hash
)
)
}
return (await Promise.all(logQueries)).flat(1)
}
}
/**
* Provides read-only access for Child-to-Parent messages
*/
export class ChildToParentMessageReader extends ChildToParentMessage {
private readonly classicReader?: classic.ChildToParentMessageReaderClassic
private readonly nitroReader?: nitro.ChildToParentMessageReaderNitro
constructor(
protected readonly parentProvider: Provider,
event: ChildToParentTransactionEvent
) {
super()
if (this.isClassic(event)) {
this.classicReader = new classic.ChildToParentMessageReaderClassic(
parentProvider,
event.batchNumber,
event.indexInBatch
)
} else {
this.nitroReader = new nitro.ChildToParentMessageReaderNitro(
parentProvider,
event
)
}
}
public async getOutboxProof(
childProvider: Provider
): Promise<classic.MessageBatchProofInfo | null | string[]> {
if (this.nitroReader) {
return await this.nitroReader.getOutboxProof(childProvider)
} else return await this.classicReader!.tryGetProof(childProvider)
}
/**
* Get the status of this message
* In order to check if the message has been executed proof info must be provided.
* @returns
*/
public async status(
childProvider: Provider
): Promise<ChildToParentMessageStatus> {
// can we create a ChildToParentMessage here, we need to - the constructor is what we need
if (this.nitroReader) return await this.nitroReader.status(childProvider)
else return await this.classicReader!.status(childProvider)
}
/**
* Waits until the outbox entry has been created, and will not return until it has been.
* WARNING: Outbox entries are only created when the corresponding node is confirmed. Which
* can take 1 week+, so waiting here could be a very long operation.
* @param retryDelay
* @returns outbox entry status (either executed or confirmed but not pending)
*/
public async waitUntilReadyToExecute(
childProvider: Provider,
retryDelay = 500
): Promise<
ChildToParentMessageStatus.EXECUTED | ChildToParentMessageStatus.CONFIRMED
> {
if (this.nitroReader)
return this.nitroReader.waitUntilReadyToExecute(childProvider, retryDelay)
else
return this.classicReader!.waitUntilOutboxEntryCreated(
childProvider,
retryDelay
)
}
/**
* Estimates the Parent block number in which this Child-to-Parent tx will be available for execution.
* If the message can or already has been executed, this returns null
* @param childProvider
* @returns expected Parent block number where the Child-to-Parent message will be executable. Returns null if the message can or already has been executed
*/
public async getFirstExecutableBlock(
childProvider: Provider
): Promise<BigNumber | null> {
if (this.nitroReader)
return this.nitroReader.getFirstExecutableBlock(childProvider)
else return this.classicReader!.getFirstExecutableBlock(childProvider)
}
}
/**
* Provides read and write access for Child-to-Parent messages
*/
export class ChildToParentMessageWriter extends ChildToParentMessageReader {
private readonly classicWriter?: classic.ChildToParentMessageWriterClassic
private readonly nitroWriter?: nitro.ChildToParentMessageWriterNitro
/**
* Instantiates a new `ChildToParentMessageWriter` object.
*
* @param {Signer} parentSigner The signer to be used for executing the Child-to-Parent message.
* @param {ChildToParentTransactionEvent} event The event containing the data of the Child-to-Parent message.
* @param {Provider} [parentProvider] Optional. Used to override the Provider which is attached to `parentSigner` in case you need more control. This will be a required parameter in a future major version update.
*/
constructor(
parentSigner: Signer,
event: ChildToParentTransactionEvent,
parentProvider?: Provider
) {
super(parentProvider ?? parentSigner.provider!, event)
if (this.isClassic(event)) {
this.classicWriter = new classic.ChildToParentMessageWriterClassic(
parentSigner,
event.batchNumber,
event.indexInBatch,
parentProvider
)
} else {
this.nitroWriter = new nitro.ChildToParentMessageWriterNitro(
parentSigner,
event,
parentProvider
)
}
}
/**
* Executes the ChildToParentMessage on Parent chain.
* Will throw an error if the outbox entry has not been created, which happens when the
* corresponding assertion is confirmed.
* @returns
*/
public async execute(
childProvider: Provider,
overrides?: Overrides
): Promise<ContractTransaction> {
if (this.nitroWriter)
return this.nitroWriter.execute(childProvider, overrides)
else return await this.classicWriter!.execute(childProvider, overrides)
}
}
================================================
File: packages/sdk/src/lib/message/ChildToParentMessageClassic.ts
================================================
/*
* Copyright 2021, Offchain Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env node */
'use strict'
import {
ARB_SYS_ADDRESS,
NODE_INTERFACE_ADDRESS,
} from '../dataEntities/constants'
import { Provider } from '@ethersproject/abstract-provider'
import { Signer } from '@ethersproject/abstract-signer'
import { BigNumber } from '@ethersproject/bignumber'
import { BlockTag } from '@ethersproject/abstract-provider'
import { ArbSys__factory } from '../abi/factories/ArbSys__factory'
import { Outbox__factory } from '../abi/classic/factories/Outbox__factory'
import { NodeInterface__factory } from '../abi/factories/NodeInterface__factory'
import { L2ToL1TransactionEvent as ChildToParentTransactionEvent } from '../abi/ArbSys'
import { ContractTransaction, Overrides } from 'ethers'
import { EventFetcher } from '../utils/eventFetcher'
import {
SignerProviderUtils,
SignerOrProvider,
} from '../dataEntities/signerOrProvider'
import { isDefined, wait } from '../utils/lib'
import { ArbSdkError } from '../dataEntities/errors'
import { EventArgs } from '../dataEntities/event'
import { ChildToParentMessageStatus } from '../dataEntities/message'
import { getArbitrumNetwork } from '../dataEntities/networks'
export interface MessageBatchProofInfo {
/**
* Merkle proof of message inclusion in outbox entry
*/
proof: string[]
/**
* Merkle path to message
*/
path: BigNumber
/**
* Sender of original message (i.e., caller of ArbSys.sendTxToL1)
*/
l2Sender: string
/**
* Destination address for L1 contract call
*/
l1Dest: string
/**
* L2 block number at which sendTxToL1 call was made
*/
l2Block: BigNumber
/**
* L1 block number at which sendTxToL1 call was made
*/
l1Block: BigNumber
/**
* L2 Timestamp at which sendTxToL1 call was made
*/
timestamp: BigNumber
/**
* Value in L1 message in wei
*/
amount: BigNumber
/**
* ABI-encoded L1 message data
*/
calldataForL1: string
}
/**
* Conditional type for Signer or Provider. If T is of type Provider
* then ChildToParentMessageReaderOrWriter<T> will be of type ChildToParentMessageReader.
* If T is of type Signer then ChildToParentMessageReaderOrWriter<T> will be of
* type ChildToParentMessageWriter.
*/
export type ChildToParentMessageReaderOrWriterClassic<
T extends SignerOrProvider
> = T extends Provider
? ChildToParentMessageReaderClassic
: ChildToParentMessageWriterClassic
export class ChildToParentMessageClassic {
/**
* The number of the batch this message is part of
*/
public readonly batchNumber: BigNumber
/**
* The index of this message in the batch
*/
public readonly indexInBatch: BigNumber
protected constructor(batchNumber: BigNumber, indexInBatch: BigNumber) {
this.batchNumber = batchNumber
this.indexInBatch = indexInBatch
}
/**
* Instantiates a new `ChildToParentMessageWriterClassic` or `ChildToParentMessageReaderClassic` object.
*
* @param {SignerOrProvider} parentSignerOrProvider Signer or provider to be used for executing or reading the Child-to-Parent message.
* @param {BigNumber} batchNumber The number of the batch containing the Child-to-Parent message.
* @param {BigNumber} indexInBatch The index of the Child-to-Parent message within the batch.
* @param {Provider} [parentProvider] Optional. Used to override the Provider which is attached to `parentSignerOrProvider` in case you need more control. This will be a required parameter in a future major version update.
*/
public static fromBatchNumber<T extends SignerOrProvider>(
parentSignerOrProvider: T,
batchNumber: BigNumber,
indexInBatch: BigNumber,
parentProvider?: Provider
): ChildToParentMessageReaderOrWriterClassic<T>
public static fromBatchNumber<T extends SignerOrProvider>(
parentSignerOrProvider: T,
batchNumber: BigNumber,
indexInBatch: BigNumber,
parentProvider?: Provider
): ChildToParentMessageReaderClassic | ChildToParentMessageWriterClassic {
return SignerProviderUtils.isSigner(parentSignerOrProvider)
? new ChildToParentMessageWriterClassic(
parentSignerOrProvider,
batchNumber,
indexInBatch,
parentProvider
)
: new ChildToParentMessageReaderClassic(
parentSignerOrProvider,
batchNumber,
indexInBatch
)
}
public static async getChildToParentEvents(
childProvider: Provider,
filter: { fromBlock: BlockTag; toBlock: BlockTag },
batchNumber?: BigNumber,
destination?: string,
uniqueId?: BigNumber,
indexInBatch?: BigNumber
): Promise<
(EventArgs<ChildToParentTransactionEvent> & {
transactionHash: string
})[]
> {
const eventFetcher = new EventFetcher(childProvider)
const events = (
await eventFetcher.getEvents(
ArbSys__factory,
t =>
t.filters.L2ToL1Transaction(null, destination, uniqueId, batchNumber),
{ ...filter, address: ARB_SYS_ADDRESS }
)
).map(l => ({ ...l.event, transactionHash: l.transactionHash }))
if (indexInBatch) {
const indexItems = events.filter(b => b.indexInBatch.eq(indexInBatch))
if (indexItems.length === 1) {
return indexItems
} else if (indexItems.length > 1) {
throw new ArbSdkError('More than one indexed item found in batch.')
} else return []
} else return events
}
}
/**
* Provides read-only access for classic Child-to-Parent-messages
*/
export class ChildToParentMessageReaderClassic extends ChildToParentMessageClassic {
constructor(
protected readonly parentProvider: Provider,
batchNumber: BigNumber,
indexInBatch: BigNumber
) {
super(batchNumber, indexInBatch)
}
/**
* Contains the classic outbox address, or set to zero address if this network
* did not have a classic outbox deployed
*/
protected outboxAddress: string | null = null
/**
* Classic had 2 outboxes, we need to find the correct one for the provided batch number
* @param childProvider
* @param batchNumber
* @returns
*/
protected async getOutboxAddress(
childProvider: Provider,
batchNumber: number
) {
if (!isDefined(this.outboxAddress)) {
const childChain = await getArbitrumNetwork(childProvider)
// find the outbox where the activation batch number of the next outbox
// is greater than the supplied batch
const outboxes = isDefined(childChain.ethBridge.classicOutboxes)
? Object.entries(childChain.ethBridge.classicOutboxes)
: []
const res = outboxes
.sort((a, b) => {
if (a[1] < b[1]) return -1
else if (a[1] === b[1]) return 0
else return 1
})
.find(
(_, index, array) =>
array[index + 1] === undefined || array[index + 1][1] > batchNumber
)
if (!res) {
this.outboxAddress = '0x0000000000000000000000000000000000000000'
} else {
this.outboxAddress = res[0]
}
}
return this.outboxAddress
}
private async outboxEntryExists(childProvider: Provider) {
const outboxAddress = await this.getOutboxAddress(
childProvider,
this.batchNumber.toNumber()
)
const outbox = Outbox__factory.connect(outboxAddress, this.parentProvider)
return await outbox.outboxEntryExists(this.batchNumber)
}
public static async tryGetProof(
childProvider: Provider,
batchNumber: BigNumber,
indexInBatch: BigNumber
): Promise<MessageBatchProofInfo | null> {
const nodeInterface = NodeInterface__factory.connect(
NODE_INTERFACE_ADDRESS,
childProvider
)
try {
return await nodeInterface.legacyLookupMessageBatchProof(
batchNumber,
indexInBatch
)
} catch (e) {
const expectedError = "batch doesn't exist"
const err = e as Error & { error: Error }
const actualError =
err && (err.message || (err.error && err.error.message))
if (actualError.includes(expectedError)) return null
else throw e
}
}
private proof: MessageBatchProofInfo | null = null
/**
* Get the execution proof for this message. Returns null if the batch does not exist yet.
* @param childProvider
* @returns
*/
public async tryGetProof(
childProvider: Provider
): Promise<MessageBatchProofInfo | null> {
if (!isDefined(this.proof)) {
this.proof = await ChildToParentMessageReaderClassic.tryGetProof(
childProvider,
this.batchNumber,
this.indexInBatch
)
}
return this.proof
}
/**
* Check if given outbox message has already been executed
*/
public async hasExecuted(childProvider: Provider): Promise<boolean> {
const proofInfo = await this.tryGetProof(childProvider)
if (!isDefined(proofInfo)) return false
const outboxAddress = await this.getOutboxAddress(
childProvider,
this.batchNumber.toNumber()
)
const outbox = Outbox__factory.connect(outboxAddress, this.parentProvider)
try {
await outbox.callStatic.executeTransaction(
this.batchNumber,
proofInfo.proof,
proofInfo.path,
proofInfo.l2Sender,
proofInfo.l1Dest,
proofInfo.l2Block,
proofInfo.l1Block,
proofInfo.timestamp,
proofInfo.amount,
proofInfo.calldataForL1
)
return false
} catch (err) {
const e = err as Error
if (e?.message?.toString().includes('ALREADY_SPENT')) return true
if (e?.message?.toString().includes('NO_OUTBOX_ENTRY')) return false
throw e
}
}
/**
* Get the status of this message
* In order to check if the message has been executed proof info must be provided.
* @param childProvider
* @returns
*/
public async status(
childProvider: Provider
): Promise<ChildToParentMessageStatus> {
try {
const messageExecuted = await this.hasExecuted(childProvider)
if (messageExecuted) {
return ChildToParentMessageStatus.EXECUTED
}
const outboxEntryExists = await this.outboxEntryExists(childProvider)
return outboxEntryExists
? ChildToParentMessageStatus.CONFIRMED
: ChildToParentMessageStatus.UNCONFIRMED
} catch (e) {
return ChildToParentMessageStatus.UNCONFIRMED
}
}
/**
* Waits until the outbox entry has been created, and will not return until it has been.
* WARNING: Outbox entries are only created when the corresponding node is confirmed. Which
* can take 1 week+, so waiting here could be a very long operation.
* @param retryDelay
* @returns outbox entry status (either executed or confirmed but not pending)
*/
public async waitUntilOutboxEntryCreated(
childProvider: Provider,
retryDelay = 500
): Promise<
ChildToParentMessageStatus.EXECUTED | ChildToParentMessageStatus.CONFIRMED
> {
const exists = await this.outboxEntryExists(childProvider)
if (exists) {
return (await this.hasExecuted(childProvider))
? ChildToParentMessageStatus.EXECUTED
: ChildToParentMessageStatus.CONFIRMED
} else {
await wait(retryDelay)
return await this.waitUntilOutboxEntryCreated(childProvider, retryDelay)
}
}
/**
* Estimates the Parent Chain block number in which this Child-to-Parent tx will be available for execution
* @param childProvider
* @returns Always returns null for classic chainToParentChain messages since they can be executed in any block now.
*/
public async getFirstExecutableBlock(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
childProvider: Provider
): Promise<BigNumber | null> {
return null
}
}
/**
* Provides read and write access for classic Child-to-Parent-messages
*/
export class ChildToParentMessageWriterClassic extends ChildToParentMessageReaderClassic {
/**
* Instantiates a new `ChildToParentMessageWriterClassic` object.
*
* @param {Signer} parentSigner The signer to be used for executing the Child-to-Parent message.
* @param {BigNumber} batchNumber The number of the batch containing the Child-to-Parent message.
* @param {BigNumber} indexInBatch The index of the Child-to-Parent message within the batch.
* @param {Provider} [parentProvider] Optional. Used to override the Provider which is attached to `parentSigner` in case you need more control. This will be a required parameter in a future major version update.
*/
constructor(
private readonly parentSigner: Signer,
batchNumber: BigNumber,
indexInBatch: BigNumber,
parentProvider?: Provider
) {
super(parentProvider ?? parentSigner.provider!, batchNumber, indexInBatch)
}
/**
* Executes the ChildToParentMessage on Parent Chain.
* Will throw an error if the outbox entry has not been created, which happens when the
* corresponding assertion is confirmed.
* @returns
*/
public async execute(
childProvider: Provider,
overrides?: Overrides
): Promise<ContractTransaction> {
const status = await this.status(childProvider)
if (status !== ChildToParentMessageStatus.CONFIRMED) {
throw new ArbSdkError(
`Cannot execute message. Status is: ${status} but must be ${ChildToParentMessageStatus.CONFIRMED}.`
)
}
const proofInfo = await this.tryGetProof(childProvider)
if (!isDefined(proofInfo)) {
throw new ArbSdkError(
`Unexpected missing proof: ${this.batchNumber.toString()} ${this.indexInBatch.toString()}}`
)
}
const outboxAddress = await this.getOutboxAddress(
childProvider,
this.batchNumber.toNumber()
)
const outbox = Outbox__factory.connect(outboxAddress, this.parentSigner)
// We can predict and print number of missing blocks
// if not challenged
return await outbox.functions.executeTransaction(
this.batchNumber,
proofInfo.proof,
proofInfo.path,
proofInfo.l2Sender,
proofInfo.l1Dest,
proofInfo.l2Block,
proofInfo.l1Block,
proofInfo.timestamp,
proofInfo.amount,
proofInfo.calldataForL1,
overrides || {}
)
}
}
================================================
File: packages/sdk/src/lib/message/ChildToParentMessageNitro.ts
================================================
/*
* Copyright 2021, Offchain Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env node */
'use strict'
import {
ARB_SYS_ADDRESS,
NODE_INTERFACE_ADDRESS,
} from '../dataEntities/constants'
import { Provider } from '@ethersproject/abstract-provider'
import { Signer } from '@ethersproject/abstract-signer'
import { BigNumber } from '@ethersproject/bignumber'
import { BlockTag } from '@ethersproject/abstract-provider'
import { ErrorCode, Logger } from '@ethersproject/logger'
import { ArbSys__factory } from '../abi/factories/ArbSys__factory'
import { RollupUserLogic__factory } from '../abi/factories/RollupUserLogic__factory'
import { BoldRollupUserLogic__factory } from '../abi-bold/factories/BoldRollupUserLogic__factory'
import { Outbox__factory } from '../abi/factories/Outbox__factory'
import { NodeInterface__factory } from '../abi/factories/NodeInterface__factory'
import { L2ToL1TxEvent as ChildToParentTxEvent } from '../abi/ArbSys'
import { ContractTransaction, Overrides } from 'ethers'
import { Mutex } from 'async-mutex'
import { EventFetcher, FetchedEvent } from '../utils/eventFetcher'
import { ArbSdkError } from '../dataEntities/errors'
import {
SignerProviderUtils,
SignerOrProvider,
} from '../dataEntities/signerOrProvider'
import { getBlockRangesForL1Block, isArbitrumChain, wait } from '../utils/lib'
import { ArbitrumNetwork, getArbitrumNetwork } from '../dataEntities/networks'
import { NodeCreatedEvent, RollupUserLogic } from '../abi/RollupUserLogic'
import {
AssertionCreatedEvent,
BoldRollupUserLogic,
} from '../abi-bold/BoldRollupUserLogic'
import { ArbitrumProvider } from '../utils/arbProvider'
import { ArbBlock } from '../dataEntities/rpc'
import { JsonRpcProvider } from '@ethersproject/providers'
import { EventArgs } from '../dataEntities/event'
import { ChildToParentMessageStatus } from '../dataEntities/message'
import { Bridge__factory } from '../abi/factories/Bridge__factory'
/**
* Conditional type for Signer or Provider. If T is of type Provider
* then ChildToParentMessageReaderOrWriter<T> will be of type ChildToParentMessageReader.
* If T is of type Signer then ChildToParentMessageReaderOrWriter<T> will be of
* type ChildToParentMessageWriter.
*/
export type ChildToParentMessageReaderOrWriterNitro<
T extends SignerOrProvider
> = T extends Provider
? ChildToParentMessageReaderNitro
: ChildToParentMessageWriterNitro
// expected number of parent chain blocks that it takes for a Child chain tx to be included in a parent chain assertion
const ASSERTION_CREATED_PADDING = 50
// expected number of parent blocks that it takes for a validator to confirm a parent block after the assertion deadline is passed
const ASSERTION_CONFIRMED_PADDING = 20
const childBlockRangeCache: { [key in string]: (number | undefined)[] } = {}
const mutex = new Mutex()
function getChildBlockRangeCacheKey({
childChainId,
l1BlockNumber,
}: {
childChainId: number
l1BlockNumber: number
}) {
return `${childChainId}-${l1BlockNumber}`
}
function setChildBlockRangeCache(key: string, value: (number | undefined)[]) {
childBlockRangeCache[key] = value
}
async function getBlockRangesForL1BlockWithCache({
parentProvider,
childProvider,
forL1Block,
}: {
parentProvider: JsonRpcProvider
childProvider: JsonRpcProvider
forL1Block: number
}) {
const childChainId = (await childProvider.getNetwork()).chainId
const key = getChildBlockRangeCacheKey({
childChainId,
l1BlockNumber: forL1Block,
})
if (childBlockRangeCache[key]) {
return childBlockRangeCache[key]
}
// implements a lock that only fetches cache once
const release = await mutex.acquire()
// if cache has been acquired while awaiting the lock
if (childBlockRangeCache[key]) {
release()
return childBlockRangeCache[key]
}
try {
const childBlockRange = await getBlockRangesForL1Block({
forL1Block,
arbitrumProvider: parentProvider,
})
setChildBlockRangeCache(key, childBlockRange)
} finally {
release()
}
return childBlockRangeCache[key]
}
/**
* Base functionality for nitro Child->Parent messages
*/
export class ChildToParentMessageNitro {
protected constructor(
public readonly event: EventArgs<ChildToParentTxEvent>
) {}
/**
* Instantiates a new `ChildToParentMessageWriterNitro` or `ChildToParentMessageReaderNitro` object.
*
* @param {SignerOrProvider} parentSignerOrProvider Signer or provider to be used for executing or reading the Child-to-Parent message.
* @param {EventArgs<ChildToParentTxEvent>} event The event containing the data of the Child-to-Parent message.
* @param {Provider} [parentProvider] Optional. Used to override the Provider which is attached to `parentSignerOrProvider` in case you need more control. This will be a required parameter in a future major version update.
*/
public static fromEvent<T extends SignerOrProvider>(
parentSignerOrProvider: T,
event: EventArgs<ChildToParentTxEvent>,
parentProvider?: Provider
): ChildToParentMessageReaderOrWriterNitro<T>
public static fromEvent<T extends SignerOrProvider>(
parentSignerOrProvider: T,
event: EventArgs<ChildToParentTxEvent>,
parentProvider?: Provider
): ChildToParentMessageReaderNitro | ChildToParentMessageWriterNitro {
return SignerProviderUtils.isSigner(parentSignerOrProvider)
? new ChildToParentMessageWriterNitro(
parentSignerOrProvider,
event,
parentProvider
)
: new ChildToParentMessageReaderNitro(parentSignerOrProvider, event)
}
public static async getChildToParentEvents(
childProvider: Provider,
filter: { fromBlock: BlockTag; toBlock: BlockTag },
position?: BigNumber,
destination?: string,
hash?: BigNumber
): Promise<
(EventArgs<ChildToParentTxEvent> & { transactionHash: string })[]
> {
const eventFetcher = new EventFetcher(childProvider)
return (
await eventFetcher.getEvents(
ArbSys__factory,
t => t.filters.L2ToL1Tx(null, destination, hash, position),
{ ...filter, address: ARB_SYS_ADDRESS }
)
).map(l => ({ ...l.event, transactionHash: l.transactionHash }))
}
}
/**
* Provides read-only access nitro for child-to-parent-messages
*/
export class ChildToParentMessageReaderNitro extends ChildToParentMessageNitro {
protected sendRootHash?: string
protected sendRootSize?: BigNumber
protected sendRootConfirmed?: boolean
protected outboxAddress?: string
protected l1BatchNumber?: number
constructor(
protected readonly parentProvider: Provider,
event: EventArgs<ChildToParentTxEvent>
) {
super(event)
}
public async getOutboxProof(childProvider: Provider) {
const { sendRootSize } = await this.getSendProps(childProvider)
if (!sendRootSize)
throw new ArbSdkError('Assertion not yet created, cannot get proof.')
const nodeInterface = NodeInterface__factory.connect(
NODE_INTERFACE_ADDRESS,
childProvider
)
const outboxProofParams =
await nodeInterface.callStatic.constructOutboxProof(
sendRootSize.toNumber(),
this.event.position.toNumber()
)
return outboxProofParams.proof
}
/**
* Check if this message has already been executed in the Outbox
*/
protected async hasExecuted(childProvider: Provider): Promise<boolean> {
const childChain = await getArbitrumNetwork(childProvider)
const outbox = Outbox__factory.connect(
childChain.ethBridge.outbox,
this.parentProvider
)
return outbox.callStatic.isSpent(this.event.position)
}
/**
* Get the status of this message
* In order to check if the message has been executed proof info must be provided.
* @returns
*/
public async status(
childProvider: Provider
): Promise<ChildToParentMessageStatus> {
const { sendRootConfirmed } = await this.getSendProps(childProvider)
if (!sendRootConfirmed) return ChildToParentMessageStatus.UNCONFIRMED
return (await this.hasExecuted(childProvider))
? ChildToParentMessageStatus.EXECUTED
: ChildToParentMessageStatus.CONFIRMED
}
private parseNodeCreatedAssertion(event: FetchedEvent<NodeCreatedEvent>) {
return {
afterState: {
blockHash: event.event.assertion.afterState.globalState.bytes32Vals[0],
sendRoot: event.event.assertion.afterState.globalState.bytes32Vals[1],
},
}
}
private parseAssertionCreatedEvent(e: FetchedEvent<AssertionCreatedEvent>) {
return {
afterState: {
blockHash: (e as FetchedEvent<AssertionCreatedEvent>).event.assertion
.afterState.globalState.bytes32Vals[0],
sendRoot: (e as FetchedEvent<AssertionCreatedEvent>).event.assertion
.afterState.globalState.bytes32Vals[1],
},
}
}
private isAssertionCreatedLog(
log: FetchedEvent<NodeCreatedEvent> | FetchedEvent<AssertionCreatedEvent>
): log is FetchedEvent<AssertionCreatedEvent> {
return (
(log as FetchedEvent<AssertionCreatedEvent>).event.challengeManager !=
undefined
)
}
private async getBlockFromAssertionLog(
childProvider: JsonRpcProvider,
log: FetchedEvent<NodeCreatedEvent> | FetchedEvent<AssertionCreatedEvent>
) {
const arbitrumProvider = new ArbitrumProvider(childProvider)
if (!log) {
console.warn('No AssertionCreated events found, defaulting to block 0')
return arbitrumProvider.getBlock(0)
}
const parsedLog = this.isAssertionCreatedLog(log)
? this.parseAssertionCreatedEvent(log)
: this.parseNodeCreatedAssertion(log)
const childBlock = await arbitrumProvider.getBlock(
parsedLog.afterState.blockHash
)
if (!childBlock) {
throw new ArbSdkError(
`Block not found. ${parsedLog.afterState.blockHash}`
)
}
if (childBlock.sendRoot !== parsedLog.afterState.sendRoot) {
throw new ArbSdkError(
`Child chain block send root doesn't match parsed log. ${childBlock.sendRoot} ${parsedLog.afterState.sendRoot}`
)
}
return childBlock
}
private isBoldRollupUserLogic(
rollup: RollupUserLogic | BoldRollupUserLogic
): rollup is BoldRollupUserLogic {
return (rollup as BoldRollupUserLogic).getAssertion !== undefined
}
private async getBlockFromAssertionId(
rollup: RollupUserLogic | BoldRollupUserLogic,
assertionId: BigNumber | string,
childProvider: Provider
): Promise<ArbBlock> {
const createdAtBlock: BigNumber = this.isBoldRollupUserLogic(rollup)
? (
await (rollup as BoldRollupUserLogic).getAssertion(
assertionId as string
)
).createdAtBlock
: (await (rollup as RollupUserLogic).getNode(assertionId)).createdAtBlock
let createdFromBlock = createdAtBlock
let createdToBlock = createdAtBlock
// If L1 is Arbitrum, then L2 is an Orbit chain.
if (await isArbitrumChain(this.parentProvider)) {
try {
const nodeInterface = NodeInterface__factory.connect(
NODE_INTERFACE_ADDRESS,
this.parentProvider
)
const l2BlockRangeFromNode = await nodeInterface.l2BlockRangeForL1(
createdAtBlock
)
createdFromBlock = l2BlockRangeFromNode.firstBlock
createdToBlock = l2BlockRangeFromNode.lastBlock
} catch {
// defaults to binary search
try {
const l2BlockRange = await getBlockRangesForL1BlockWithCache({
parentProvider: this.parentProvider as JsonRpcProvider,
childProvider: childProvider as JsonRpcProvider,
forL1Block: createdAtBlock.toNumber(),
})
const startBlock = l2BlockRange[0]
const endBlock = l2BlockRange[1]
if (!startBlock || !endBlock) {
throw new Error()
}
createdFromBlock = BigNumber.from(startBlock)
createdToBlock = BigNumber.from(endBlock)
} catch {
// fallback to the original method
createdFromBlock = createdAtBlock
createdToBlock = createdAtBlock
}
}
}
// now get the block hash and sendroot for that node
const eventFetcher = new EventFetcher(rollup.provider)
const logs:
| FetchedEvent<NodeCreatedEvent>[]
| FetchedEvent<AssertionCreatedEvent>[] = this.isBoldRollupUserLogic(
rollup
)
? await eventFetcher.getEvents(
BoldRollupUserLogic__factory,
t => t.filters.AssertionCreated(assertionId as string),
{
fromBlock: createdFromBlock.toNumber(),
toBlock: createdToBlock.toNumber(),
address: rollup.address,
}
)
: await eventFetcher.getEvents(
RollupUserLogic__factory,
t => t.filters.NodeCreated(assertionId),
{
fromBlock: createdFromBlock.toNumber(),
toBlock: createdToBlock.toNumber(),
address: rollup.address,
}
)
if (logs.length > 1)
throw new ArbSdkError(
`Unexpected number of AssertionCreated events. Expected 0 or 1, got ${logs.length}.`
)
return await this.getBlockFromAssertionLog(
childProvider as JsonRpcProvider,
logs[0]
)
}
protected async getBatchNumber(childProvider: Provider) {
if (this.l1BatchNumber == undefined) {
// findBatchContainingBlock errors if block number does not exist
try {
const nodeInterface = NodeInterface__factory.connect(
NODE_INTERFACE_ADDRESS,
childProvider
)
const res = await nodeInterface.findBatchContainingBlock(
this.event.arbBlockNum
)
this.l1BatchNumber = res.toNumber()
} catch (err) {
// do nothing - errors are expected here
}
}
return this.l1BatchNumber
}
protected async getSendProps(childProvider: Provider) {
if (!this.sendRootConfirmed) {
const childChain = await getArbitrumNetwork(childProvider)
const rollup = await this.getRollupAndUpdateNetwork(childChain)
const latestConfirmedAssertionId =
await rollup.callStatic.latestConfirmed()
const childBlockConfirmed = await this.getBlockFromAssertionId(
rollup,
latestConfirmedAssertionId,
childProvider
)
const sendRootSizeConfirmed = BigNumber.from(
childBlockConfirmed.sendCount
)
if (sendRootSizeConfirmed.gt(this.event.position)) {
this.sendRootSize = sendRootSizeConfirmed
this.sendRootHash = childBlockConfirmed.sendRoot
this.sendRootConfirmed = true
} else {
let latestCreatedAssertionId: BigNumber | string
if (this.isBoldRollupUserLogic(rollup)) {
const latestConfirmed = await rollup.latestConfirmed()
const latestConfirmedAssertion = await rollup.getAssertion(
latestConfirmed
)
const eventFetcher = new EventFetcher(rollup.provider)
const assertionCreatedEvents = await eventFetcher.getEvents(
BoldRollupUserLogic__factory,
t => t.filters.AssertionCreated(),
{
fromBlock: latestConfirmedAssertion.createdAtBlock.toNumber(),
toBlock: 'latest',
address: rollup.address,
}
)
latestCreatedAssertionId =
assertionCreatedEvents[assertionCreatedEvents.length - 1].event
.assertionHash
} else {
latestCreatedAssertionId = await rollup.callStatic.latestNodeCreated()
}
const latestEquals =
typeof latestCreatedAssertionId === 'string'
? latestCreatedAssertionId === latestConfirmedAssertionId
: latestCreatedAssertionId.eq(latestConfirmedAssertionId)
// if the node has yet to be confirmed we'll still try to find proof info from unconfirmed nodes
if (!latestEquals) {
// In rare case latestNodeNum can be equal to latestConfirmedNodeNum
// eg immediately after an upgrade, or at genesis, or on a chain where confirmation time = 0 like AnyTrust may have
const childBlock = await this.getBlockFromAssertionId(
rollup,
latestCreatedAssertionId,
childProvider
)
const sendRootSize = BigNumber.from(childBlock.sendCount)
if (sendRootSize.gt(this.event.position)) {
this.sendRootSize = sendRootSize
this.sendRootHash = childBlock.sendRoot
}
}
}
}
return {
sendRootSize: this.sendRootSize,
sendRootHash: this.sendRootHash,
sendRootConfirmed: this.sendRootConfirmed,
}
}
/**
* Waits until the outbox entry has been created, and will not return until it has been.
* WARNING: Outbox entries are only created when the corresponding node is confirmed. Which
* can take 1 week+, so waiting here could be a very long operation.
* @param retryDelay
* @returns outbox entry status (either executed or confirmed but not pending)
*/
public async waitUntilReadyToExecute(
childProvider: Provider,
retryDelay = 500
): Promise<
ChildToParentMessageStatus.EXECUTED | ChildToParentMessageStatus.CONFIRMED
> {
const status = await this.status(childProvider)
if (
status === ChildToParentMessageStatus.CONFIRMED ||
status === ChildToParentMessageStatus.EXECUTED
) {
return status
} else {
await wait(retryDelay)
return await this.waitUntilReadyToExecute(childProvider, retryDelay)
}
}
/**
* Check whether the provided network has a BoLD rollup
* @param arbitrumNetwork
* @param parentProvider
* @returns
*/
private async isBold(
arbitrumNetwork: ArbitrumNetwork,
parentProvider: Provider
): Promise<string | undefined> {
const bridge = Bridge__factory.connect(
arbitrumNetwork.ethBridge.bridge,
parentProvider
)
const remoteRollupAddr = await bridge.rollup()
const rollup = RollupUserLogic__factory.connect(
remoteRollupAddr,
parentProvider
)
try {
// bold rollup does not have an extraChallengeTimeBlocks function
await rollup.callStatic.extraChallengeTimeBlocks()
return undefined
} catch (err) {
if (
err instanceof Error &&
(err as unknown as { code: ErrorCode }).code ===
Logger.errors.CALL_EXCEPTION
) {
return remoteRollupAddr
}
throw err
}
}
/**
* If the local network is not currently bold, checks if the remote network is bold
* and if so updates the local network with a new rollup address
* @param arbitrumNetwork
* @returns The rollup contract, bold or legacy
*/
private async getRollupAndUpdateNetwork(arbitrumNetwork: ArbitrumNetwork) {
if (!arbitrumNetwork.isBold) {
const boldRollupAddr = await this.isBold(
arbitrumNetwork,
this.parentProvider
)
if (boldRollupAddr) {
arbitrumNetwork.isBold = true
arbitrumNetwork.ethBridge.rollup = boldRollupAddr
}
}
return arbitrumNetwork.isBold
? BoldRollupUserLogic__factory.connect(
arbitrumNetwork.ethBridge.rollup,
this.parentProvider
)
: RollupUserLogic__factory.connect(
arbitrumNetwork.ethBridge.rollup,
this.parentProvider
)
}
/**
* Estimates the L1 block number in which this L2 to L1 tx will be available for execution.
* If the message can or already has been executed, this returns null
* @param childProvider
* @returns expected parent chain block number where the child chain to parent chain message will be executable. Returns null if the message can be or already has been executed
*/
public async getFirstExecutableBlock(
childProvider: Provider
): Promise<BigNumber | null> {
const arbitrumNetwork = await getArbitrumNetwork(childProvider)
const rollup = await this.getRollupAndUpdateNetwork(arbitrumNetwork)
const status = await this.status(childProvider)
if (status === ChildToParentMessageStatus.EXECUTED) return null
if (status === ChildToParentMessageStatus.CONFIRMED) return null
// consistency check in case we change the enum in the future
if (status !== ChildToParentMessageStatus.UNCONFIRMED)
throw new ArbSdkError('ChildToParentMsg expected to be unconfirmed')
const latestBlock = await this.parentProvider.getBlockNumber()
const eventFetcher = new EventFetcher(this.parentProvider)
let logs:
| FetchedEvent<NodeCreatedEvent>[]
| FetchedEvent<AssertionCreatedEvent>[]
if (arbitrumNetwork.isBold) {
logs = (
await eventFetcher.getEvents(
BoldRollupUserLogic__factory,
t => t.filters.AssertionCreated(),
{
fromBlock: Math.max(
latestBlock -
BigNumber.from(arbitrumNetwork.confirmPeriodBlocks)
.add(ASSERTION_CONFIRMED_PADDING)
.toNumber(),
0
),
toBlock: 'latest',
address: rollup.address,
}
)
).sort((a, b) => a.blockNumber - b.blockNumber)
} else {
logs = (
await eventFetcher.getEvents(
RollupUserLogic__factory,
t => t.filters.NodeCreated(),
{
fromBlock: Math.max(
latestBlock -
BigNumber.from(arbitrumNetwork.confirmPeriodBlocks)
.add(ASSERTION_CONFIRMED_PADDING)
.toNumber(),
0
),
toBlock: 'latest',
address: rollup.address,
}
)
).sort((a, b) => a.event.nodeNum.toNumber() - b.event.nodeNum.toNumber())
}
const lastChildBlock =
logs.length === 0
? undefined
: await this.getBlockFromAssertionLog(
childProvider as JsonRpcProvider,
logs[logs.length - 1]
)
const lastSendCount = lastChildBlock
? BigNumber.from(lastChildBlock.sendCount)
: BigNumber.from(0)
// here we assume the child-to-parent tx is actually valid, so the user needs to wait the max time
// since there isn't a pending assertion that includes this message yet
if (lastSendCount.lte(this.event.position))
return BigNumber.from(arbitrumNetwork.confirmPeriodBlocks)
.add(ASSERTION_CREATED_PADDING)
.add(ASSERTION_CONFIRMED_PADDING)
.add(latestBlock)
// use binary search to find the first assertion with sendCount > this.event.position
// default to the last assertion since we already checked above
let foundLog = logs[logs.length - 1]
let left = 0
let right = logs.length - 1
while (left <= right) {
const mid = Math.floor((left + right) / 2)
const log = logs[mid]
const childBlock = await this.getBlockFromAssertionLog(
childProvider as JsonRpcProvider,
log
)
const sendCount = BigNumber.from(childBlock.sendCount)
if (sendCount.gt(this.event.position)) {
foundLog = log
right = mid - 1
} else {
left = mid + 1
}
}
if (arbitrumNetwork.isBold) {
const assertionHash = (foundLog as FetchedEvent<AssertionCreatedEvent>)
.event.assertionHash
const assertion = await (rollup as BoldRollupUserLogic).getAssertion(
assertionHash
)
return assertion.createdAtBlock
.add(arbitrumNetwork.confirmPeriodBlocks)
.add(ASSERTION_CONFIRMED_PADDING)
} else {
const earliestNodeWithExit = (foundLog as FetchedEvent<NodeCreatedEvent>)
.event.nodeNum
const node = await (rollup as RollupUserLogic).getNode(
earliestNodeWithExit
)
return node.deadlineBlock.add(ASSERTION_CONFIRMED_PADDING)
}
}
}
/**
* Provides read and write access for nitro child-to-Parent-messages
*/
export class ChildToParentMessageWriterNitro extends ChildToParentMessageReaderNitro {
/**
* Instantiates a new `ChildToParentMessageWriterNitro` object.
*
* @param {Signer} parentSigner The signer to be used for executing the Child-to-Parent message.
* @param {EventArgs<ChildToParentTxEvent>} event The event containing the data of the Child-to-Parent message.
* @param {Provider} [parentProvider] Optional. Used to override the Provider which is attached to `parentSigner` in case you need more control. This will be a required parameter in a future major version update.
*/
constructor(
private readonly parentSigner: Signer,
event: EventArgs<ChildToParentTxEvent>,
parentProvider?: Provider
) {
super(parentProvider ?? parentSigner.provider!, event)
}
/**
* Executes the ChildToParentMessage on Parent Chain.
* Will throw an error if the outbox entry has not been created, which happens when the
* corresponding assertion is confirmed.
* @returns
*/
public async execute(
childProvider: Provider,
overrides?: Overrides
): Promise<ContractTransaction> {
const status = await this.status(childProvider)
if (status !== ChildToParentMessageStatus.CONFIRMED) {
throw new ArbSdkError(
`Cannot execute message. Status is: ${status} but must be ${ChildToParentMessageStatus.CONFIRMED}.`
)
}
const proof = await this.getOutboxProof(childProvider)
const childChain = await getArbitrumNetwork(childProvider)
const outbox = Outbox__factory.connect(
childChain.ethBridge.outbox,
this.parentSigner
)
return await outbox.executeTransaction(
proof,
this.event.position,
this.event.caller,
this.event.destination,
this.event.arbBlockNum,
this.event.ethBlockNum,
this.event.timestamp,
this.event.callvalue,
this.event.data,
overrides || {}
)
}
}
================================================
File: packages/sdk/src/lib/message/ChildTransaction.ts
================================================
/*
* Copyright 2021, Offchain Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env node */
'use strict'
import { TransactionReceipt } from '@ethersproject/providers'
import { BigNumber } from '@ethersproject/bignumber'
import { Log } from '@ethersproject/abstract-provider'
import { ContractTransaction, providers } from 'ethers'
import {
SignerProviderUtils,
SignerOrProvider,
} from '../dataEntities/signerOrProvider'
import {
ChildToParentMessageReader,
ChildToParentMessageReaderOrWriter,
ChildToParentMessage,
ChildToParentMessageWriter,
ChildToParentTransactionEvent,
} from './ChildToParentMessage'
import { ArbSys__factory } from '../abi/factories/ArbSys__factory'
import { ArbRetryableTx__factory } from '../abi/factories/ArbRetryableTx__factory'
import { NodeInterface__factory } from '../abi/factories/NodeInterface__factory'
import { RedeemScheduledEvent } from '../abi/ArbRetryableTx'
import { ArbSdkError } from '../dataEntities/errors'
import { NODE_INTERFACE_ADDRESS } from '../dataEntities/constants'
import { EventArgs, parseTypedLogs } from '../dataEntities/event'
import { ArbitrumProvider } from '../utils/arbProvider'
export interface ChildContractTransaction extends ContractTransaction {
wait(confirmations?: number): Promise<ChildTransactionReceipt>
}
export interface RedeemTransaction extends ChildContractTransaction {
waitForRedeem: () => Promise<TransactionReceipt>
}
/**
* Extension of ethers-js TransactionReceipt, adding Arbitrum-specific functionality
*/
export class ChildTransactionReceipt implements TransactionReceipt {
public readonly to: string
public readonly from: string
public readonly contractAddress: string
public readonly transactionIndex: number
public readonly root?: string
public readonly gasUsed: BigNumber
public readonly logsBloom: string
public readonly blockHash: string
public readonly transactionHash: string
public readonly logs: Array<Log>
public readonly blockNumber: number
public readonly confirmations: number
public readonly cumulativeGasUsed: BigNumber
public readonly effectiveGasPrice: BigNumber
public readonly byzantium: boolean
public readonly type: number
public readonly status?: number
constructor(tx: TransactionReceipt) {
this.to = tx.to
this.from = tx.from
this.contractAddress = tx.contractAddress
this.transactionIndex = tx.transactionIndex
this.root = tx.root
this.gasUsed = tx.gasUsed
this.logsBloom = tx.logsBloom
this.blockHash = tx.blockHash
this.transactionHash = tx.transactionHash
this.logs = tx.logs
this.blockNumber = tx.blockNumber
this.confirmations = tx.confirmations
this.cumulativeGasUsed = tx.cumulativeGasUsed
this.effectiveGasPrice = tx.effectiveGasPrice
this.byzantium = tx.byzantium
this.type = tx.type
this.status = tx.status
}
/**
* Get {@link ChildToParentTransactionEvent} events created by this transaction
* @returns
*/
public getChildToParentEvents(): ChildToParentTransactionEvent[] {
const classicLogs = parseTypedLogs(
ArbSys__factory,
this.logs,
'L2ToL1Transaction'
)
const nitroLogs = parseTypedLogs(ArbSys__factory, this.logs, 'L2ToL1Tx')
return [...classicLogs, ...nitroLogs]
}
/**
* Get event data for any redeems that were scheduled in this transaction
* @returns
*/
public getRedeemScheduledEvents(): EventArgs<RedeemScheduledEvent>[] {
return parseTypedLogs(ArbRetryableTx__factory, this.logs, 'RedeemScheduled')
}
/**
* Get any child-to-parent-messages created by this transaction
* @param parentSignerOrProvider
*/
public async getChildToParentMessages<T extends SignerOrProvider>(
parentSignerOrProvider: T
): Promise<ChildToParentMessageReaderOrWriter<T>[]>
public async getChildToParentMessages<T extends SignerOrProvider>(
parentSignerOrProvider: T
): Promise<ChildToParentMessageReader[] | ChildToParentMessageWriter[]> {
const provider = SignerProviderUtils.getProvider(parentSignerOrProvider)
if (!provider) throw new ArbSdkError('Signer not connected to provider.')
return this.getChildToParentEvents().map(log =>
ChildToParentMessage.fromEvent(parentSignerOrProvider, log)
)
}
/**
* Get number of parent chain confirmations that the batch including this tx has
* @param childProvider
* @returns number of confirmations of batch including tx, or 0 if no batch included this tx
*/
public getBatchConfirmations(childProvider: providers.JsonRpcProvider) {
const nodeInterface = NodeInterface__factory.connect(
NODE_INTERFACE_ADDRESS,
childProvider
)
return nodeInterface.getL1Confirmations(this.blockHash)
}
/**
* Get the number of the batch that included this tx (will throw if no such batch exists)
* @param childProvider
* @returns number of batch in which tx was included, or errors if no batch includes the current tx
*/
public async getBatchNumber(childProvider: providers.JsonRpcProvider) {
const nodeInterface = NodeInterface__factory.connect(
NODE_INTERFACE_ADDRESS,
childProvider
)
const arbProvider = new ArbitrumProvider(childProvider)
const rec = await arbProvider.getTransactionReceipt(this.transactionHash)
if (rec == null)
throw new ArbSdkError(
'No receipt receipt available for current transaction'
)
// findBatchContainingBlock errors if block number does not exist
return nodeInterface.findBatchContainingBlock(rec.blockNumber)
}
/**
* Whether the data associated with this transaction has been
* made available on parent chain
* @param childProvider
* @param confirmations The number of confirmations on the batch before data is to be considered available
* @returns
*/
public async isDataAvailable(
childProvider: providers.JsonRpcProvider,
confirmations = 10
): Promise<boolean> {
const res = await this.getBatchConfirmations(childProvider)
// is there a batch with enough confirmations
return res.toNumber() > confirmations
}
/**
* Replaces the wait function with one that returns an L2TransactionReceipt
* @param contractTransaction
* @returns
*/
public static monkeyPatchWait = (
contractTransaction: ContractTransaction
): ChildContractTransaction => {
const wait = contractTransaction.wait
contractTransaction.wait = async (_confirmations?: number) => {
// we ignore the confirmations for now since child chain transactions shouldn't re-org
// in future we should give users a more fine grained way to check the finality of
// an child chain transaction - check if a batch is on a parent chain, if an assertion has been made, and if
// it has been confirmed.
const result = await wait()
return new ChildTransactionReceipt(result)
}
return contractTransaction as ChildContractTransaction
}
/**
* Adds a waitForRedeem function to a redeem transaction
* @param redeemTx
* @param childProvider
* @returns
*/
public static toRedeemTransaction(
redeemTx: ChildContractTransaction,
childProvider: providers.Provider
): RedeemTransaction {
const returnRec = redeemTx as RedeemTransaction
returnRec.waitForRedeem = async () => {
const rec = await redeemTx.wait()
const redeemScheduledEvents = await rec.getRedeemScheduledEvents()
if (redeemScheduledEvents.length !== 1) {
throw new ArbSdkError(
`Transaction is not a redeem transaction: ${rec.transactionHash}`
)
}
return await childProvider.getTransactionReceipt(
redeemScheduledEvents[0].retryTxHash
)
}
return returnRec
}
}
================================================
File: packages/sdk/src/lib/message/ParentToChildMessage.ts
================================================
/*
* Copyright 2021, Offchain Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env node */
'use strict'
import { TransactionReceipt } from '@ethersproject/providers'
import { Provider } from '@ethersproject/abstract-provider'
import { Signer } from '@ethersproject/abstract-signer'
import { ContractTransaction } from '@ethersproject/contracts'
import { BigNumber } from '@ethersproject/bignumber'
import { concat, zeroPad } from '@ethersproject/bytes'
import { getAddress } from '@ethersproject/address'
import { keccak256 } from '@ethersproject/keccak256'
import { ArbRetryableTx__factory } from '../abi/factories/ArbRetryableTx__factory'
import {
ARB_RETRYABLE_TX_ADDRESS,
DEFAULT_DEPOSIT_TIMEOUT,
SEVEN_DAYS_IN_SECONDS,
} from '../dataEntities/constants'
import {
SignerProviderUtils,
SignerOrProvider,
} from '../dataEntities/signerOrProvider'
import { ArbSdkError } from '../dataEntities/errors'
import { ethers, Overrides } from 'ethers'
import { ChildTransactionReceipt, RedeemTransaction } from './ChildTransaction'
import { RetryableMessageParams } from '../dataEntities/message'
import { getTransactionReceipt, isDefined } from '../utils/lib'
import { EventFetcher } from '../utils/eventFetcher'
import { ErrorCode, Logger } from '@ethersproject/logger'
import { getArbitrumNetwork } from '../dataEntities/networks'
export enum ParentToChildMessageStatus {
/**
* The retryable ticket has yet to be created
*/
NOT_YET_CREATED = 1,
/**
* An attempt was made to create the retryable ticket, but it failed.
* This could be due to not enough submission cost being paid by the Parent transaction
*/
CREATION_FAILED = 2,
/**
* The retryable ticket has been created but has not been redeemed. This could be due to the
* auto redeem failing, or if the params (max chain gas price) * (max chain gas) = 0 then no auto
* redeem tx is ever issued. An auto redeem is also never issued for ETH deposits.
* A manual redeem is now required.
*/
FUNDS_DEPOSITED_ON_CHILD = 3,
/**
* The retryable ticket has been redeemed (either by auto, or manually) and the
* chain transaction has been executed
*/
REDEEMED = 4,
/**
* The message has either expired or has been canceled. It can no longer be redeemed.
*/
EXPIRED = 5,
}
export enum EthDepositMessageStatus {
/**
* ETH is not deposited on Chain yet
*/
PENDING = 1,
/**
* ETH is deposited successfully on Chain
*/
DEPOSITED = 2,
}
// for handling errors thrown in retryableExists()
interface RetryableExistsError extends Error {
code: ErrorCode
errorName: string
}
/**
* Conditional type for Signer or Provider. If T is of type Provider
* then ParentToChildMessageReaderOrWriter<T> will be of type ParentToChildMessageReader.
* If T is of type Signer then ParentToChildMessageReaderOrWriter<T> will be of
* type ParentToChildMessageWriter.
*/
export type ParentToChildMessageReaderOrWriter<T extends SignerOrProvider> =
T extends Provider ? ParentToChildMessageReader : ParentToChildMessageWriter
export abstract class ParentToChildMessage {
/**
* When messages are sent from Parent to Child a retryable ticket is created on the child chain.
* The retryableCreationId can be used to retrieve information about the success or failure of the
* creation of the retryable ticket.
*/
public readonly retryableCreationId: string
/**
* The submit retryable transactions use the typed transaction envelope 2718.
* The id of these transactions is the hash of the RLP encoded transaction.
* @param childChainId
* @param fromAddress the aliased address that called the Parent inbox as emitted in the bridge event.
* @param messageNumber
* @param parentBaseFee
* @param destAddress
* @param childCallValue
* @param parentCallValue
* @param maxSubmissionFee
* @param excessFeeRefundAddress refund address specified in the retryable creation. Note the Parent inbox aliases this address if it is a Parent smart contract. The user is expected to provide this value already aliased when needed.
* @param callValueRefundAddress refund address specified in the retryable creation. Note the Parent inbox aliases this address if it is a Parent smart contract. The user is expected to provide this value already aliased when needed.
* @param gasLimit
* @param maxFeePerGas
* @param data
* @returns
*/
public static calculateSubmitRetryableId(
childChainId: number,
fromAddress: string,
messageNumber: BigNumber,
parentBaseFee: BigNumber,
destAddress: string,
childCallValue: BigNumber,
parentCallValue: BigNumber,
maxSubmissionFee: BigNumber,
excessFeeRefundAddress: string,
callValueRefundAddress: string,
gasLimit: BigNumber,
maxFeePerGas: BigNumber,
data: string
): string {
const formatNumber = (value: BigNumber): Uint8Array => {
return ethers.utils.stripZeros(value.toHexString())
}
const chainId = BigNumber.from(childChainId)
const msgNum = BigNumber.from(messageNumber)
const fields: any[] = [
formatNumber(chainId),
zeroPad(formatNumber(msgNum), 32),
fromAddress,
formatNumber(parentBaseFee),
formatNumber(parentCallValue),
formatNumber(maxFeePerGas),
formatNumber(gasLimit),
// when destAddress is 0x0, arbos treat that as nil
destAddress === ethers.constants.AddressZero ? '0x' : destAddress,
formatNumber(childCallValue),
callValueRefundAddress,
formatNumber(maxSubmissionFee),
excessFeeRefundAddress,
data,
]
// arbitrum submit retry transactions have type 0x69
const rlpEnc = ethers.utils.hexConcat([
'0x69',
ethers.utils.RLP.encode(fields),
])
return ethers.utils.keccak256(rlpEnc)
}
public static fromEventComponents<T extends SignerOrProvider>(
chainSignerOrProvider: T,
chainId: number,
sender: string,
messageNumber: BigNumber,
parentBaseFee: BigNumber,
messageData: RetryableMessageParams
): ParentToChildMessageReaderOrWriter<T>
public static fromEventComponents<T extends SignerOrProvider>(
chainSignerOrProvider: T,
chainId: number,
sender: string,
messageNumber: BigNumber,
parentBaseFee: BigNumber,
messageData: RetryableMessageParams
): ParentToChildMessageReader | ParentToChildMessageWriter {
return SignerProviderUtils.isSigner(chainSignerOrProvider)
? new ParentToChildMessageWriter(
chainSignerOrProvider,
chainId,
sender,
messageNumber,
parentBaseFee,
messageData
)
: new ParentToChildMessageReader(
chainSignerOrProvider,
chainId,
sender,
messageNumber,
parentBaseFee,
messageData
)
}
protected constructor(
public readonly chainId: number,
public readonly sender: string,
public readonly messageNumber: BigNumber,
public readonly parentBaseFee: BigNumber,
public readonly messageData: RetryableMessageParams
) {
this.retryableCreationId = ParentToChildMessage.calculateSubmitRetryableId(
chainId,
sender,
messageNumber,
parentBaseFee,
messageData.destAddress,
messageData.l2CallValue,
messageData.l1Value,
messageData.maxSubmissionFee,
messageData.excessFeeRefundAddress,
messageData.callValueRefundAddress,
messageData.gasLimit,
messageData.maxFeePerGas,
messageData.data
)
}
}
/**
* If the status is redeemed, childTxReceipt is populated.
* For all other statuses childTxReceipt is not populated
*/
export type ParentToChildMessageWaitForStatusResult =
| {
status: ParentToChildMessageStatus.REDEEMED
childTxReceipt: TransactionReceipt
}
| {
status: Exclude<
ParentToChildMessageStatus,
ParentToChildMessageStatus.REDEEMED
>
}
export type EthDepositMessageWaitForStatusResult = {
childTxReceipt: TransactionReceipt | null
}
export class ParentToChildMessageReader extends ParentToChildMessage {
private retryableCreationReceipt: TransactionReceipt | undefined | null
public constructor(
public readonly childProvider: Provider,
chainId: number,
sender: string,
messageNumber: BigNumber,
parentBaseFee: BigNumber,
messageData: RetryableMessageParams
) {
super(chainId, sender, messageNumber, parentBaseFee, messageData)
}
/**
* Try to get the receipt for the retryable ticket creation.
* This is the Chain transaction that creates the retryable ticket.
* If confirmations or timeout is provided, this will wait for the ticket to be created
* @returns Null if retryable has not been created
*/
public async getRetryableCreationReceipt(
confirmations?: number,
timeout?: number
): Promise<TransactionReceipt | null> {
if (!this.retryableCreationReceipt) {
this.retryableCreationReceipt = await getTransactionReceipt(
this.childProvider,
this.retryableCreationId,
confirmations,
timeout
)
}
return this.retryableCreationReceipt || null
}
/**
* When retryable tickets are created, and gas is supplied to it, an attempt is
* made to redeem the ticket straight away. This is called an auto redeem.
* @returns TransactionReceipt of the auto redeem attempt if exists, otherwise null
*/
public async getAutoRedeemAttempt(): Promise<TransactionReceipt | null> {
const creationReceipt = await this.getRetryableCreationReceipt()
if (creationReceipt) {
const chainReceipt = new ChildTransactionReceipt(creationReceipt)
const redeemEvents = chainReceipt.getRedeemScheduledEvents()
if (redeemEvents.length === 1) {
return await this.childProvider.getTransactionReceipt(
redeemEvents[0].retryTxHash
)
} else if (redeemEvents.length > 1) {
throw new ArbSdkError(
`Unexpected number of redeem events for retryable creation tx. ${creationReceipt} ${redeemEvents}`
)
}
}
return null
}
/**
* Receipt for the successful chain transaction created by this message.
* @returns TransactionReceipt of the first successful redeem if exists, otherwise the current status of the message.
*/
public async getSuccessfulRedeem(): Promise<ParentToChildMessageWaitForStatusResult> {
const chainNetwork = await getArbitrumNetwork(this.childProvider)
const eventFetcher = new EventFetcher(this.childProvider)
const creationReceipt = await this.getRetryableCreationReceipt()
if (!isDefined(creationReceipt)) {
// retryable was never created, or not created yet
// therefore it cant have been redeemed or be expired
return { status: ParentToChildMessageStatus.NOT_YET_CREATED }
}
if (creationReceipt.status === 0) {
return { status: ParentToChildMessageStatus.CREATION_FAILED }
}
// check the auto redeem first to avoid doing costly log queries in the happy case
const autoRedeem = await this.getAutoRedeemAttempt()
if (autoRedeem && autoRedeem.status === 1) {
return {
childTxReceipt: autoRedeem,
status: ParentToChildMessageStatus.REDEEMED,
}
}
if (await this.retryableExists()) {
// the retryable was created and still exists
// therefore it cant have been redeemed or be expired
return {
status: ParentToChildMessageStatus.FUNDS_DEPOSITED_ON_CHILD,
}
}
// from this point on we know that the retryable was created but does not exist,
// so the retryable was either successfully redeemed, or it expired
// the auto redeem didnt exist or wasnt successful, look for a later manual redeem
// to do this we need to filter through the whole lifetime of the ticket looking
// for relevant redeem scheduled events
let increment = 1000
let fromBlock = await this.childProvider.getBlock(
creationReceipt.blockNumber
)
let timeout =
fromBlock.timestamp +
(chainNetwork.retryableLifetimeSeconds ?? SEVEN_DAYS_IN_SECONDS)
const queriedRange: { from: number; to: number }[] = []
const maxBlock = await this.childProvider.getBlockNumber()
while (fromBlock.number < maxBlock) {
const toBlockNumber = Math.min(fromBlock.number + increment, maxBlock)
// using fromBlock.number would lead to 1 block overlap
// not fixing it here to keep the code simple
const outerBlockRange = { from: fromBlock.number, to: toBlockNumber }
queriedRange.push(outerBlockRange)
const redeemEvents = await eventFetcher.getEvents(
ArbRetryableTx__factory,
contract => contract.filters.RedeemScheduled(this.retryableCreationId),
{
fromBlock: outerBlockRange.from,
toBlock: outerBlockRange.to,
address: ARB_RETRYABLE_TX_ADDRESS,
}
)
const successfulRedeem = (
await Promise.all(
redeemEvents.map(e =>
this.childProvider.getTransactionReceipt(e.event.retryTxHash)
)
)
).filter(r => isDefined(r) && r.status === 1)
if (successfulRedeem.length > 1)
throw new ArbSdkError(
`Unexpected number of successful redeems. Expected only one redeem for ticket ${this.retryableCreationId}, but found ${successfulRedeem.length}.`
)
if (successfulRedeem.length == 1)
return {
childTxReceipt: successfulRedeem[0],
status: ParentToChildMessageStatus.REDEEMED,
}
const toBlock = await this.childProvider.getBlock(toBlockNumber)
if (toBlock.timestamp > timeout) {
// Check for LifetimeExtended event
while (queriedRange.length > 0) {
const blockRange = queriedRange.shift()
const keepaliveEvents = await eventFetcher.getEvents(
ArbRetryableTx__factory,
contract =>
contract.filters.LifetimeExtended(this.retryableCreationId),
{
fromBlock: blockRange!.from,
toBlock: blockRange!.to,
address: ARB_RETRYABLE_TX_ADDRESS,
}
)
if (keepaliveEvents.length > 0) {
timeout = keepaliveEvents
.map(e => e.event.newTimeout.toNumber())
.sort()
.reverse()[0]
break
}
}
// the retryable no longer exists, but we've searched beyond the timeout
// so it must have expired
if (toBlock.timestamp > timeout) break
// It is possible to have another keepalive in the last range as it might include block after previous timeout
while (queriedRange.length > 1) queriedRange.shift()
}
const processedSeconds = toBlock.timestamp - fromBlock.timestamp
if (processedSeconds != 0) {
// find the increment that cover ~ 1 day
increment = Math.ceil((increment * 86400) / processedSeconds)
}
fromBlock = toBlock
}
// we know from earlier that the retryable no longer exists, so if we havent found the redemption
// we know that it must have expired
return { status: ParentToChildMessageStatus.EXPIRED }
}
/**
* Has this message expired. Once expired the retryable ticket can no longer be redeemed.
* @deprecated Will be removed in v3.0.0
* @returns
*/
public async isExpired(): Promise<boolean> {
return await this.retryableExists()
}
private async retryableExists(): Promise<boolean> {
const currentTimestamp = BigNumber.from(
(await this.childProvider.getBlock('latest')).timestamp
)
try {
const timeoutTimestamp = await this.getTimeout()
// timeoutTimestamp returns the timestamp at which the retryable ticket expires
// it can also return revert if the ticket chainTx does not exist
return currentTimestamp.lte(timeoutTimestamp)
} catch (err) {
if (
err instanceof Error &&
(err as unknown as RetryableExistsError).code ===
Logger.errors.CALL_EXCEPTION &&
(err as unknown as RetryableExistsError).errorName === 'NoTicketWithID'
) {
return false
}
throw err
}
}
public async status(): Promise<ParentToChildMessageStatus> {
return (await this.getSuccessfulRedeem()).status
}
/**
* Wait for the retryable ticket to be created, for it to be redeemed, and for the chainTx to be executed.
* Note: The terminal status of a transaction that only does an eth deposit is FUNDS_DEPOSITED_ON_CHILD as
* no Chain transaction needs to be executed, however the terminal state of any other transaction is REDEEMED
* which represents that the retryable ticket has been redeemed and the Chain tx has been executed.
* @param confirmations Amount of confirmations the retryable ticket and the auto redeem receipt should have
* @param timeout Amount of time to wait for the retryable ticket to be created
* Defaults to 15 minutes, as by this time all transactions are expected to be included on Chain. Throws on timeout.
* @returns The wait result contains a status, and optionally the chainTxReceipt.
* If the status is "REDEEMED" then a chainTxReceipt is also available on the result.
* If the status has any other value then chainTxReceipt is not populated.
*/
public async waitForStatus(
confirmations?: number,
timeout?: number
): Promise<ParentToChildMessageWaitForStatusResult> {
const chosenTimeout = isDefined(timeout) ? timeout : DEFAULT_DEPOSIT_TIMEOUT
// try to wait for the retryable ticket to be created
const _retryableCreationReceipt = await this.getRetryableCreationReceipt(
confirmations,
chosenTimeout
)
if (!_retryableCreationReceipt) {
if (confirmations || chosenTimeout) {
throw new ArbSdkError(
`Timed out waiting to retrieve retryable creation receipt: ${this.retryableCreationId}.`
)
} else {
throw new ArbSdkError(
`Retryable creation receipt not found ${this.retryableCreationId}.`
)
}
}
return await this.getSuccessfulRedeem()
}
/**
* The minimium lifetime of a retryable tx
* @returns
*/
public static async getLifetime(childProvider: Provider): Promise<BigNumber> {
const arbRetryableTx = ArbRetryableTx__factory.connect(
ARB_RETRYABLE_TX_ADDRESS,
childProvider
)
return await arbRetryableTx.getLifetime()
}
/**
* Timestamp at which this message expires
* @returns
*/
public async getTimeout(): Promise<BigNumber> {
const arbRetryableTx = ArbRetryableTx__factory.connect(
ARB_RETRYABLE_TX_ADDRESS,
this.childProvider
)
return await arbRetryableTx.getTimeout(this.retryableCreationId)
}
/**
* Address to which CallValue will be credited to on Chain if the retryable ticket times out or is cancelled.
* The Beneficiary is also the address with the right to cancel a Retryable Ticket (if the ticket hasn’t been redeemed yet).
* @returns
*/
public getBeneficiary(): Promise<string> {
const arbRetryableTx = ArbRetryableTx__factory.connect(
ARB_RETRYABLE_TX_ADDRESS,
this.childProvider
)
return arbRetryableTx.getBeneficiary(this.retryableCreationId)
}
}
export class ParentToChildMessageReaderClassic {
private retryableCreationReceipt: TransactionReceipt | undefined | null
public readonly messageNumber: BigNumber
public readonly retryableCreationId: string
public readonly autoRedeemId: string
public readonly childTxHash: string
public readonly childProvider: Provider
constructor(
childProvider: Provider,
chainId: number,
messageNumber: BigNumber
) {
const bitFlip = (num: BigNumber) => num.or(BigNumber.from(1).shl(255))
this.messageNumber = messageNumber
this.childProvider = childProvider
this.retryableCreationId = keccak256(
concat([
zeroPad(BigNumber.from(chainId).toHexString(), 32),
zeroPad(bitFlip(this.messageNumber).toHexString(), 32),
])
)
this.autoRedeemId = keccak256(
concat([
zeroPad(this.retryableCreationId, 32),
zeroPad(BigNumber.from(1).toHexString(), 32),
])
)
this.childTxHash = keccak256(
concat([
zeroPad(this.retryableCreationId, 32),
zeroPad(BigNumber.from(0).toHexString(), 32),
])
)
}
private calculateChainDerivedHash(retryableCreationId: string): string {
return keccak256(
concat([
zeroPad(retryableCreationId, 32),
// BN 0 meaning Chain TX
zeroPad(BigNumber.from(0).toHexString(), 32),
])
)
}
/**
* Try to get the receipt for the retryable ticket creation.
* This is the Chain transaction that creates the retryable ticket.
* If confirmations or timeout is provided, this will wait for the ticket to be created
* @returns Null if retryable has not been created
*/
public async getRetryableCreationReceipt(
confirmations?: number,
timeout?: number
): Promise<TransactionReceipt | null> {
if (!this.retryableCreationReceipt) {
this.retryableCreationReceipt = await getTransactionReceipt(
this.childProvider,
this.retryableCreationId,
confirmations,
timeout
)
}
return this.retryableCreationReceipt || null
}
public async status(): Promise<ParentToChildMessageStatus> {
const creationReceipt = await this.getRetryableCreationReceipt()
if (!isDefined(creationReceipt)) {
return ParentToChildMessageStatus.NOT_YET_CREATED
}
if (creationReceipt.status === 0) {
return ParentToChildMessageStatus.CREATION_FAILED
}
const chainDerivedHash = this.calculateChainDerivedHash(
this.retryableCreationId
)
const chainTxReceipt = await this.childProvider.getTransactionReceipt(
chainDerivedHash
)
if (chainTxReceipt && chainTxReceipt.status === 1) {
return ParentToChildMessageStatus.REDEEMED
}
return ParentToChildMessageStatus.EXPIRED
}
}
export class ParentToChildMessageWriter extends ParentToChildMessageReader {
public constructor(
public readonly chainSigner: Signer,
chainId: number,
sender: string,
messageNumber: BigNumber,
parentBaseFee: BigNumber,
messageData: RetryableMessageParams
) {
super(
chainSigner.provider!,
chainId,
sender,
messageNumber,
parentBaseFee,
messageData
)
if (!chainSigner.provider)
throw new ArbSdkError('Signer not connected to provider.')
}
/**
* Manually redeem the retryable ticket.
* Throws if message status is not ParentToChildMessageStatus.FUNDS_DEPOSITED_ON_CHILD
*/
public async redeem(overrides?: Overrides): Promise<RedeemTransaction> {
const status = await this.status()
if (status === ParentToChildMessageStatus.FUNDS_DEPOSITED_ON_CHILD) {
const arbRetryableTx = ArbRetryableTx__factory.connect(
ARB_RETRYABLE_TX_ADDRESS,
this.chainSigner
)
const redeemTx = await arbRetryableTx.redeem(this.retryableCreationId, {
...overrides,
})
return ChildTransactionReceipt.toRedeemTransaction(
ChildTransactionReceipt.monkeyPatchWait(redeemTx),
this.childProvider
)
} else {
throw new ArbSdkError(
`Cannot redeem as retryable does not exist. Message status: ${
ParentToChildMessageStatus[status]
} must be: ${
ParentToChildMessageStatus[
ParentToChildMessageStatus.FUNDS_DEPOSITED_ON_CHILD
]
}.`
)
}
}
/**
* Cancel the retryable ticket.
* Throws if message status is not ParentToChildMessageStatus.FUNDS_DEPOSITED_ON_CHILD
*/
public async cancel(overrides?: Overrides): Promise<ContractTransaction> {
const status = await this.status()
if (status === ParentToChildMessageStatus.FUNDS_DEPOSITED_ON_CHILD) {
const arbRetryableTx = ArbRetryableTx__factory.connect(
ARB_RETRYABLE_TX_ADDRESS,
this.chainSigner
)
return await arbRetryableTx.cancel(this.retryableCreationId, overrides)
} else {
throw new ArbSdkError(
`Cannot cancel as retryable does not exist. Message status: ${
ParentToChildMessageStatus[status]
} must be: ${
ParentToChildMessageStatus[
ParentToChildMessageStatus.FUNDS_DEPOSITED_ON_CHILD
]
}.`
)
}
}
/**
* Increase the timeout of a retryable ticket.
* Throws if message status is not ParentToChildMessageStatus.FUNDS_DEPOSITED_ON_CHILD
*/
public async keepAlive(overrides?: Overrides): Promise<ContractTransaction> {
const status = await this.status()
if (status === ParentToChildMessageStatus.FUNDS_DEPOSITED_ON_CHILD) {
const arbRetryableTx = ArbRetryableTx__factory.connect(
ARB_RETRYABLE_TX_ADDRESS,
this.chainSigner
)
return await arbRetryableTx.keepalive(this.retryableCreationId, overrides)
} else {
throw new ArbSdkError(
`Cannot keep alive as retryable does not exist. Message status: ${
ParentToChildMessageStatus[status]
} must be: ${
ParentToChildMessageStatus[
ParentToChildMessageStatus.FUNDS_DEPOSITED_ON_CHILD
]
}.`
)
}
}
}
/**
* A message for Eth deposits from Parent to Child
*/
export class EthDepositMessage {
public readonly childTxHash: string
private childTxReceipt: TransactionReceipt | undefined | null
public static calculateDepositTxId(
childChainId: number,
messageNumber: BigNumber,
fromAddress: string,
toAddress: string,
value: BigNumber
): string {
const formatNumber = (numberVal: BigNumber): Uint8Array => {
return ethers.utils.stripZeros(numberVal.toHexString())
}
const chainId = BigNumber.from(childChainId)
const msgNum = BigNumber.from(messageNumber)
// https://github.com/OffchainLabs/go-ethereum/blob/07e017aa73e32be92aadb52fa327c552e1b7b118/core/types/arb_types.go#L302-L308
const fields = [
formatNumber(chainId),
zeroPad(formatNumber(msgNum), 32),
getAddress(fromAddress),
getAddress(toAddress),
formatNumber(value),
]
// arbitrum eth deposit transactions have type 0x64
const rlpEnc = ethers.utils.hexConcat([
'0x64',
ethers.utils.RLP.encode(fields),
])
return ethers.utils.keccak256(rlpEnc)
}
/**
* Parse the data field in
* event InboxMessageDelivered(uint256 indexed messageNum, bytes data);
* @param eventData
* @returns destination and amount
*/
private static parseEthDepositData(eventData: string): {
to: string
value: BigNumber
} {
// https://github.com/OffchainLabs/nitro/blob/aa84e899cbc902bf6da753b1d66668a1def2c106/contracts/src/bridge/Inbox.sol#Chain42
// ethers.defaultAbiCoder doesnt decode packed args, so we do a hardcoded parsing
const addressEnd = 2 + 20 * 2
const to = getAddress('0x' + eventData.substring(2, addressEnd))
const value = BigNumber.from('0x' + eventData.substring(addressEnd))
return { to, value }
}
/**
* Create an EthDepositMessage from data emitted in event when calling ethDeposit on Inbox.sol
* @param childProvider
* @param messageNumber The message number in the Inbox.InboxMessageDelivered event
* @param senderAddr The sender address from Bridge.MessageDelivered event
* @param inboxMessageEventData The data field from the Inbox.InboxMessageDelivered event
* @returns
*/
public static async fromEventComponents(
childProvider: Provider,
messageNumber: BigNumber,
senderAddr: string,
inboxMessageEventData: string
) {
const chainId = (await childProvider.getNetwork()).chainId
const { to, value } = EthDepositMessage.parseEthDepositData(
inboxMessageEventData
)
return new EthDepositMessage(
childProvider,
chainId,
messageNumber,
senderAddr,
to,
value
)
}
/**
*
* @param childProvider
* @param childChainId
* @param messageNumber
* @param to Recipient address of the ETH on Chain
* @param value
*/
constructor(
private readonly childProvider: Provider,
public readonly childChainId: number,
public readonly messageNumber: BigNumber,
public readonly from: string,
public readonly to: string,
public readonly value: BigNumber
) {
this.childTxHash = EthDepositMessage.calculateDepositTxId(
childChainId,
messageNumber,
from,
to,
value
)
}
public async status(): Promise<EthDepositMessageStatus> {
const receipt = await this.childProvider.getTransactionReceipt(
this.childTxHash
)
if (receipt === null) return EthDepositMessageStatus.PENDING
else return EthDepositMessageStatus.DEPOSITED
}
public async wait(confirmations?: number, timeout?: number) {
const chosenTimeout = isDefined(timeout) ? timeout : DEFAULT_DEPOSIT_TIMEOUT
if (!this.childTxReceipt) {
this.childTxReceipt = await getTransactionReceipt(
this.childProvider,
this.childTxHash,
confirmations,
chosenTimeout
)
}
return this.childTxReceipt || null
}
}
================================================
File: packages/sdk/src/lib/message/ParentToChildMessageCreator.ts
================================================
import { constants } from 'ethers'
import { Signer } from '@ethersproject/abstract-signer'
import { Provider } from '@ethersproject/abstract-provider'
import {
GasOverrides,
ParentToChildMessageGasEstimator,
} from './ParentToChildMessageGasEstimator'
import {
ParentContractTransaction,
ParentTransactionReceipt,
} from './ParentTransaction'
import { Inbox__factory } from '../abi/factories/Inbox__factory'
import {
getArbitrumNetwork,
isArbitrumNetworkNativeTokenEther,
} from '../dataEntities/networks'
import { ERC20Inbox__factory } from '../abi/factories/ERC20Inbox__factory'
import { PayableOverrides } from '@ethersproject/contracts'
import { SignerProviderUtils } from '../dataEntities/signerOrProvider'
import { MissingProviderArbSdkError } from '../dataEntities/errors'
import { getBaseFee } from '../utils/lib'
import {
isParentToChildTransactionRequest,
ParentToChildTransactionRequest,
} from '../dataEntities/transactionRequest'
import { RetryableData } from '../dataEntities/retryableData'
import { OmitTyped, PartialPick } from '../utils/types'
type ParentToChildGasKeys =
| 'maxSubmissionCost'
| 'maxFeePerGas'
| 'gasLimit'
| 'deposit'
export type ParentToChildMessageGasParams = Pick<
RetryableData,
ParentToChildGasKeys
>
export type ParentToChildMessageNoGasParams = OmitTyped<
RetryableData,
ParentToChildGasKeys
>
export type ParentToChildMessageParams = PartialPick<
ParentToChildMessageNoGasParams,
'excessFeeRefundAddress' | 'callValueRefundAddress'
>
/**
* Creates retryable tickets by directly calling the Inbox contract on Parent chain
*/
export class ParentToChildMessageCreator {
constructor(public readonly parentSigner: Signer) {
if (!SignerProviderUtils.signerHasProvider(parentSigner)) {
throw new MissingProviderArbSdkError('parentSigner')
}
}
/**
* Gets a current estimate for the supplied params
* @param params
* @param parentProvider
* @param childProvider
* @param retryableGasOverrides
* @returns
*/
protected static async getTicketEstimate(
params: ParentToChildMessageNoGasParams,
parentProvider: Provider,
childProvider: Provider,
retryableGasOverrides?: GasOverrides
): Promise<Pick<RetryableData, ParentToChildGasKeys>> {
const baseFee = await getBaseFee(parentProvider)
const gasEstimator = new ParentToChildMessageGasEstimator(childProvider)
return await gasEstimator.estimateAll(
params,
baseFee,
parentProvider,
retryableGasOverrides
)
}
/**
* Prepare calldata for a call to create a retryable ticket
* @param params
* @param estimates
* @param excessFeeRefundAddress
* @param callValueRefundAddress
* @param nativeTokenIsEth
* @returns
*/
protected static getTicketCreationRequestCallData(
params: ParentToChildMessageParams,
estimates: Pick<RetryableData, ParentToChildGasKeys>,
excessFeeRefundAddress: string,
callValueRefundAddress: string,
nativeTokenIsEth: boolean
) {
if (!nativeTokenIsEth) {
return ERC20Inbox__factory.createInterface().encodeFunctionData(
'createRetryableTicket',
[
params.to,
params.l2CallValue,
estimates.maxSubmissionCost,
excessFeeRefundAddress,
callValueRefundAddress,
estimates.gasLimit,
estimates.maxFeePerGas,
estimates.deposit, // tokenTotalFeeAmount
params.data,
]
)
}
return Inbox__factory.createInterface().encodeFunctionData(
'createRetryableTicket',
[
params.to,
params.l2CallValue,
estimates.maxSubmissionCost,
excessFeeRefundAddress,
callValueRefundAddress,
estimates.gasLimit,
estimates.maxFeePerGas,
params.data,
]
)
}
/**
* Generate a transaction request for creating a retryable ticket
* @param params
* @param parentProvider
* @param childProvider
* @param options
* @returns
*/
public static async getTicketCreationRequest(
params: ParentToChildMessageParams,
parentProvider: Provider,
childProvider: Provider,
options?: GasOverrides
): Promise<ParentToChildTransactionRequest> {
const excessFeeRefundAddress = params.excessFeeRefundAddress || params.from
const callValueRefundAddress = params.callValueRefundAddress || params.from
const parsedParams: ParentToChildMessageNoGasParams = {
...params,
excessFeeRefundAddress,
callValueRefundAddress,
}
const estimates = await ParentToChildMessageCreator.getTicketEstimate(
parsedParams,
parentProvider,
childProvider,
options
)
const childChain = await getArbitrumNetwork(childProvider)
const nativeTokenIsEth = isArbitrumNetworkNativeTokenEther(childChain)
const data = ParentToChildMessageCreator.getTicketCreationRequestCallData(
params,
estimates,
excessFeeRefundAddress,
callValueRefundAddress,
nativeTokenIsEth
)
return {
txRequest: {
to: childChain.ethBridge.inbox,
data,
value: nativeTokenIsEth ? estimates.deposit : constants.Zero,
from: params.from,
},
retryableData: {
data: params.data,
from: params.from,
to: params.to,
excessFeeRefundAddress: excessFeeRefundAddress,
callValueRefundAddress: callValueRefundAddress,
l2CallValue: params.l2CallValue,
maxSubmissionCost: estimates.maxSubmissionCost,
maxFeePerGas: estimates.maxFeePerGas,
gasLimit: estimates.gasLimit,
deposit: estimates.deposit,
},
isValid: async () => {
const reEstimates = await ParentToChildMessageCreator.getTicketEstimate(
parsedParams,
parentProvider,
childProvider,
options
)
return ParentToChildMessageGasEstimator.isValid(estimates, reEstimates)
},
}
}
/**
* Creates a retryable ticket by directly calling the Inbox contract on Parent chain
*/
public async createRetryableTicket(
params:
| (ParentToChildMessageParams & { overrides?: PayableOverrides })
| (ParentToChildTransactionRequest & {
overrides?: PayableOverrides
}),
childProvider: Provider,
options?: GasOverrides
): Promise<ParentContractTransaction> {
const parentProvider = SignerProviderUtils.getProviderOrThrow(
this.parentSigner
)
const createRequest = isParentToChildTransactionRequest(params)
? params
: await ParentToChildMessageCreator.getTicketCreationRequest(
params,
parentProvider,
childProvider,
options
)
const tx = await this.parentSigner.sendTransaction({
...createRequest.txRequest,
...params.overrides,
})
return ParentTransactionReceipt.monkeyPatchWait(tx)
}
}
================================================
File: packages/sdk/src/lib/message/ParentToChildMessageGasEstimator.ts
================================================
import { Provider } from '@ethersproject/abstract-provider'
import { BigNumber, BigNumberish } from '@ethersproject/bignumber'
import { BytesLike, constants, utils } from 'ethers'
import { Inbox__factory } from '../abi/factories/Inbox__factory'
import { NodeInterface__factory } from '../abi/factories/NodeInterface__factory'
import { NODE_INTERFACE_ADDRESS } from '../dataEntities/constants'
import { ArbSdkError } from '../dataEntities/errors'
import { getArbitrumNetwork } from '../dataEntities/networks'
import {
RetryableData,
RetryableDataTools,
} from '../dataEntities/retryableData'
import { ParentToChildTransactionRequest } from '../dataEntities/transactionRequest'
import {
getBaseFee,
getNativeTokenDecimals,
isDefined,
scaleFrom18DecimalsToNativeTokenDecimals,
} from '../utils/lib'
import { OmitTyped } from '../utils/types'
import {
ParentToChildMessageGasParams,
ParentToChildMessageNoGasParams,
} from './ParentToChildMessageCreator'
/**
* The default amount to increase the maximum submission cost. Submission cost is calculated
* from (call data size * some const * parent chain base fee). So we need to provide some leeway for
* base fee increase. Since submission fee is a small amount it isn't too bas for UX to increase
* it by a large amount, and provide better safety.
*/
const DEFAULT_SUBMISSION_FEE_PERCENT_INCREASE = BigNumber.from(300)
/**
* When submitting a retryable we need to estimate what the gas price for it will be when we actually come
* to execute it. Since the l2 price can move due to congestion we should provide some padding here
*/
const DEFAULT_GAS_PRICE_PERCENT_INCREASE = BigNumber.from(500)
/**
* An optional big number percentage increase
*/
export type PercentIncrease = {
/**
* If provided, will override the estimated base
*/
base?: BigNumber
/**
* How much to increase the base by. If not provided system defaults may be used.
*/
percentIncrease?: BigNumber
}
export interface GasOverrides {
gasLimit?: PercentIncrease & {
/**
* Set a minimum max gas
*/
min?: BigNumber
}
maxSubmissionFee?: PercentIncrease
maxFeePerGas?: PercentIncrease
/**
* funds deposited along with the retryable (ie the msg.value that called the inbox)
*/
deposit?: Pick<PercentIncrease, 'base'>
}
const defaultParentToChildMessageEstimateOptions = {
maxSubmissionFeePercentIncrease: DEFAULT_SUBMISSION_FEE_PERCENT_INCREASE,
// gas limit for Parent->Child messages should be predictable. If it isn't due to the nature
// of the specific transaction, then the caller should provide a 'min' override
gasLimitPercentIncrease: constants.Zero,
maxFeePerGasPercentIncrease: DEFAULT_GAS_PRICE_PERCENT_INCREASE,
}
export class ParentToChildMessageGasEstimator {
constructor(public readonly childProvider: Provider) {}
private percentIncrease(num: BigNumber, increase: BigNumber): BigNumber {
return num.add(num.mul(increase).div(100))
}
private applySubmissionPriceDefaults(
maxSubmissionFeeOptions?: PercentIncrease
) {
return {
base: maxSubmissionFeeOptions?.base,
percentIncrease:
maxSubmissionFeeOptions?.percentIncrease ||
defaultParentToChildMessageEstimateOptions.maxSubmissionFeePercentIncrease,
}
}
private applyMaxFeePerGasDefaults(maxFeePerGasOptions?: PercentIncrease) {
return {
base: maxFeePerGasOptions?.base,
percentIncrease:
maxFeePerGasOptions?.percentIncrease ||
defaultParentToChildMessageEstimateOptions.maxFeePerGasPercentIncrease,
}
}
private applyGasLimitDefaults(
gasLimitDefaults?: PercentIncrease & { min?: BigNumber }
) {
return {
base: gasLimitDefaults?.base,
percentIncrease:
gasLimitDefaults?.percentIncrease ||
defaultParentToChildMessageEstimateOptions.gasLimitPercentIncrease,
min: gasLimitDefaults?.min || constants.Zero,
}
}
/**
* Return the fee, in wei, of submitting a new retryable tx with a given calldata size.
* @param parentProvider
* @param parentBaseFee
* @param callDataSize
* @param options
* @returns
*/
public async estimateSubmissionFee(
parentProvider: Provider,
parentBaseFee: BigNumber,
callDataSize: BigNumber | number,
options?: PercentIncrease
): Promise<ParentToChildMessageGasParams['maxSubmissionCost']> {
const defaultedOptions = this.applySubmissionPriceDefaults(options)
const network = await getArbitrumNetwork(this.childProvider)
const inbox = Inbox__factory.connect(
network.ethBridge.inbox,
parentProvider
)
return this.percentIncrease(
defaultedOptions.base ||
(await inbox.calculateRetryableSubmissionFee(
callDataSize,
parentBaseFee
)),
defaultedOptions.percentIncrease
)
}
/**
* Estimate the amount of child chain gas required for putting the transaction in the L2 inbox, and executing it.
* @param retryableData object containing retryable ticket data
* @param senderDeposit we dont know how much gas the transaction will use when executing
* so by default we supply a dummy amount of call value that will definately be more than we need
* @returns
*/
public async estimateRetryableTicketGasLimit(
{
from,
to,
l2CallValue: l2CallValue,
excessFeeRefundAddress,
callValueRefundAddress,
data,
}: ParentToChildMessageNoGasParams,
senderDeposit: BigNumber = utils.parseEther('1').add(l2CallValue)
): Promise<ParentToChildMessageGasParams['gasLimit']> {
const nodeInterface = NodeInterface__factory.connect(
NODE_INTERFACE_ADDRESS,
this.childProvider
)
return await nodeInterface.estimateGas.estimateRetryableTicket(
from,
senderDeposit,
to,
l2CallValue,
excessFeeRefundAddress,
callValueRefundAddress,
data
)
}
/**
* Provides an estimate for the child chain maxFeePerGas, adding some margin to allow for gas price variation
* @param options
* @returns
*/
public async estimateMaxFeePerGas(
options?: PercentIncrease
): Promise<ParentToChildMessageGasParams['maxFeePerGas']> {
const maxFeePerGasDefaults = this.applyMaxFeePerGasDefaults(options)
// estimate the child gas price
return this.percentIncrease(
maxFeePerGasDefaults.base || (await this.childProvider.getGasPrice()),
maxFeePerGasDefaults.percentIncrease
)
}
/**
* Checks if the estimate is valid when compared with a new one
* @param estimates Original estimate
* @param reEstimates Estimate to compare against
* @returns
*/
public static async isValid(
estimates: ParentToChildMessageGasParams,
reEstimates: ParentToChildMessageGasParams
): Promise<boolean> {
// L2 base fee and minimum submission cost which affect the success of the tx
return (
estimates.maxFeePerGas.gte(reEstimates.maxFeePerGas) &&
estimates.maxSubmissionCost.gte(reEstimates.maxSubmissionCost)
)
}
/**
* Get gas limit, gas price and submission price estimates for sending a Parent->Child message
* @param retryableData Data of retryable ticket transaction
* @param parentBaseFee Current parent chain base fee
* @param parentProvider
* @param options
* @returns
*/
public async estimateAll(
retryableEstimateData: ParentToChildMessageNoGasParams,
parentBaseFee: BigNumber,
parentProvider: Provider,
options?: GasOverrides
): Promise<ParentToChildMessageGasParams> {
const { data } = retryableEstimateData
const gasLimitDefaults = this.applyGasLimitDefaults(options?.gasLimit)
const childNetwork = await getArbitrumNetwork(this.childProvider)
const decimals = await getNativeTokenDecimals({
parentProvider,
childNetwork,
})
// estimate the child gas price
const maxFeePerGasPromise = this.estimateMaxFeePerGas(options?.maxFeePerGas)
// estimate the submission fee
const maxSubmissionFeePromise = this.estimateSubmissionFee(
parentProvider,
parentBaseFee,
utils.hexDataLength(data),
options?.maxSubmissionFee
)
// estimate the gas limit
const calculatedGasLimit = this.percentIncrease(
gasLimitDefaults.base ||
(await this.estimateRetryableTicketGasLimit(
retryableEstimateData,
options?.deposit?.base
)),
gasLimitDefaults.percentIncrease
)
const [maxFeePerGas, maxSubmissionFee] = await Promise.all([
maxFeePerGasPromise,
maxSubmissionFeePromise,
])
// always ensure the max gas is greater than the min - this can be useful if we know that
// gas estimation is bad for the provided transaction
const gasLimit = calculatedGasLimit.gt(gasLimitDefaults.min)
? calculatedGasLimit
: gasLimitDefaults.min
const deposit =
options?.deposit?.base ||
scaleFrom18DecimalsToNativeTokenDecimals({
amount: gasLimit
.mul(maxFeePerGas)
.add(maxSubmissionFee)
.add(retryableEstimateData.l2CallValue),
decimals,
})
return {
gasLimit,
maxSubmissionCost: maxSubmissionFee,
maxFeePerGas,
deposit,
}
}
/**
* Transactions that make a Parent->Child message need to estimate L2 gas parameters
* This function does that, and populates those parameters into a transaction request
* @param dataFunc
* @param parentProvider
* @param gasOverrides
* @returns
*/
public async populateFunctionParams(
/**
* Function that will internally make a Parent->Child transaction
* Will initially be called with dummy values to trigger a special revert containing
* the real params. Then called again with the real params to form the final data to be submitted
*/
dataFunc: (
params: OmitTyped<ParentToChildMessageGasParams, 'deposit'>
) => ParentToChildTransactionRequest['txRequest'],
parentProvider: Provider,
gasOverrides?: GasOverrides
): Promise<{
estimates: ParentToChildMessageGasParams
retryable: RetryableData
data: BytesLike
to: string
value: BigNumberish
}> {
// get function data that should trigger a retryable data error
const {
data: nullData,
to,
value,
from,
} = dataFunc({
gasLimit: RetryableDataTools.ErrorTriggeringParams.gasLimit,
maxFeePerGas: RetryableDataTools.ErrorTriggeringParams.maxFeePerGas,
maxSubmissionCost: BigNumber.from(1),
})
let retryable: RetryableData | null
try {
// get retryable data from the null call
const res = await parentProvider.call({
to: to,
data: nullData,
value: value,
from: from,
})
retryable = RetryableDataTools.tryParseError(res)
if (!isDefined(retryable)) {
throw new ArbSdkError(`No retryable data found in error: ${res}`)
}
} catch (err) {
// ethersjs currently doesnt throw for custom solidity errors, so we shouldn't end up here
// however we try to catch and parse the error anyway in case ethersjs changes
// behaviour and we dont pick up on it
retryable = RetryableDataTools.tryParseError(err as Error)
if (!isDefined(retryable)) {
throw new ArbSdkError('No retryable data found in error', err as Error)
}
}
// use retryable data to get gas estimates
const baseFee = await getBaseFee(parentProvider)
const estimates = await this.estimateAll(
{
from: retryable.from,
to: retryable.to,
data: retryable.data,
l2CallValue: retryable.l2CallValue,
excessFeeRefundAddress: retryable.excessFeeRefundAddress,
callValueRefundAddress: retryable.callValueRefundAddress,
},
baseFee,
parentProvider,
gasOverrides
)
// form the real data for the transaction
const {
data: realData,
to: realTo,
value: realValue,
} = dataFunc({
gasLimit: estimates.gasLimit,
maxFeePerGas: estimates.maxFeePerGas,
maxSubmissionCost: estimates.maxSubmissionCost,
})
return {
estimates,
retryable,
data: realData,
to: realTo,
value: realValue,
}
}
}
================================================
File: packages/sdk/src/lib/message/ParentTransaction.ts
================================================
/*
* Copyright 2021, Offchain Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env node */
'use strict'
import { TransactionReceipt } from '@ethersproject/providers'
import { Log, Provider } from '@ethersproject/abstract-provider'
import { ContractTransaction } from '@ethersproject/contracts'
import { BigNumber } from '@ethersproject/bignumber'
import {
ParentToChildMessage,
ParentToChildMessageReaderOrWriter,
ParentToChildMessageReader,
ParentToChildMessageReaderClassic,
ParentToChildMessageWriter,
ParentToChildMessageStatus,
ParentToChildMessageWaitForStatusResult,
EthDepositMessage,
EthDepositMessageWaitForStatusResult,
} from './ParentToChildMessage'
import { L1ERC20Gateway__factory } from '../abi/factories/L1ERC20Gateway__factory'
import {
SignerProviderUtils,
SignerOrProvider,
} from '../dataEntities/signerOrProvider'
import { ArbSdkError } from '../dataEntities/errors'
import { Inbox__factory } from '../abi/factories/Inbox__factory'
import { InboxMessageDeliveredEvent } from '../abi/Inbox'
import { InboxMessageKind } from '../dataEntities/message'
import { Bridge__factory } from '../abi/factories/Bridge__factory'
import { MessageDeliveredEvent } from '../abi/Bridge'
import { EventArgs, parseTypedLogs } from '../dataEntities/event'
import { isDefined } from '../utils/lib'
import { SubmitRetryableMessageDataParser } from './messageDataParser'
import { getArbitrumNetwork } from '../dataEntities/networks'
import { ARB1_NITRO_GENESIS_L1_BLOCK } from '../dataEntities/constants'
export interface ParentContractTransaction<
TReceipt extends ParentTransactionReceipt = ParentTransactionReceipt
> extends ContractTransaction {
wait(confirmations?: number): Promise<TReceipt>
}
// some helper interfaces to reduce the verbosity elsewhere
export type ParentEthDepositTransaction =
ParentContractTransaction<ParentEthDepositTransactionReceipt>
export type ParentContractCallTransaction =
ParentContractTransaction<ParentContractCallTransactionReceipt>
export class ParentTransactionReceipt implements TransactionReceipt {
public readonly to: string
public readonly from: string
public readonly contractAddress: string
public readonly transactionIndex: number
public readonly root?: string
public readonly gasUsed: BigNumber
public readonly logsBloom: string
public readonly blockHash: string
public readonly transactionHash: string
public readonly logs: Array<Log>
public readonly blockNumber: number
public readonly confirmations: number
public readonly cumulativeGasUsed: BigNumber
public readonly effectiveGasPrice: BigNumber
public readonly byzantium: boolean
public readonly type: number
public readonly status?: number
constructor(tx: TransactionReceipt) {
this.to = tx.to
this.from = tx.from
this.contractAddress = tx.contractAddress
this.transactionIndex = tx.transactionIndex
this.root = tx.root
this.gasUsed = tx.gasUsed
this.logsBloom = tx.logsBloom
this.blockHash = tx.blockHash
this.transactionHash = tx.transactionHash
this.logs = tx.logs
this.blockNumber = tx.blockNumber
this.confirmations = tx.confirmations
this.cumulativeGasUsed = tx.cumulativeGasUsed
this.effectiveGasPrice = tx.effectiveGasPrice
this.byzantium = tx.byzantium
this.type = tx.type
this.status = tx.status
}
/**
* Check if is a classic transaction
* @param childSignerOrProvider
*/
public async isClassic<T extends SignerOrProvider>(
childSignerOrProvider: T
): Promise<boolean> {
const provider = SignerProviderUtils.getProviderOrThrow(
childSignerOrProvider
)
const network = await getArbitrumNetwork(provider)
// all networks except Arbitrum One started off with Nitro
if (network.chainId === 42161) {
return this.blockNumber < ARB1_NITRO_GENESIS_L1_BLOCK
}
return false
}
/**
* Get any MessageDelivered events that were emitted during this transaction
* @returns
*/
public getMessageDeliveredEvents(): EventArgs<MessageDeliveredEvent>[] {
return parseTypedLogs(Bridge__factory, this.logs, 'MessageDelivered')
}
/**
* Get any InboxMessageDelivered events that were emitted during this transaction
* @returns
*/
public getInboxMessageDeliveredEvents() {
return parseTypedLogs(
Inbox__factory,
this.logs,
'InboxMessageDelivered(uint256,bytes)'
)
}
/**
* Get combined data for any InboxMessageDelivered and MessageDelivered events
* emitted during this transaction
* @returns
*/
public getMessageEvents(): {
inboxMessageEvent: EventArgs<InboxMessageDeliveredEvent>
bridgeMessageEvent: EventArgs<MessageDeliveredEvent>
}[] {
const bridgeMessages = this.getMessageDeliveredEvents()
const inboxMessages = this.getInboxMessageDeliveredEvents()
if (bridgeMessages.length !== inboxMessages.length) {
throw new ArbSdkError(
`Unexpected missing events. Inbox message count: ${
inboxMessages.length
} does not equal bridge message count: ${
bridgeMessages.length
}. ${JSON.stringify(inboxMessages)} ${JSON.stringify(bridgeMessages)}`
)
}
const messages: {
inboxMessageEvent: EventArgs<InboxMessageDeliveredEvent>
bridgeMessageEvent: EventArgs<MessageDeliveredEvent>
}[] = []
for (const bm of bridgeMessages) {
const im = inboxMessages.filter(i => i.messageNum.eq(bm.messageIndex))[0]
if (!im) {
throw new ArbSdkError(
`Unexepected missing event for message index: ${bm.messageIndex.toString()}. ${JSON.stringify(
inboxMessages
)}`
)
}
messages.push({
inboxMessageEvent: im,
bridgeMessageEvent: bm,
})
}
return messages
}
/**
* Get any eth deposit messages created by this transaction
* @param childProvider
*/
public async getEthDeposits(
childProvider: Provider
): Promise<EthDepositMessage[]> {
return Promise.all(
this.getMessageEvents()
.filter(
e =>
e.bridgeMessageEvent.kind ===
InboxMessageKind.L1MessageType_ethDeposit
)
.map(m =>
EthDepositMessage.fromEventComponents(
childProvider,
m.inboxMessageEvent.messageNum,
m.bridgeMessageEvent.sender,
m.inboxMessageEvent.data
)
)
)
}
/**
* Get classic parent-to-child messages created by this transaction
* @param childProvider
*/
public async getParentToChildMessagesClassic(
childProvider: Provider
): Promise<ParentToChildMessageReaderClassic[]> {
const network = await getArbitrumNetwork(childProvider)
const chainId = network.chainId.toString()
const isClassic = await this.isClassic(childProvider)
// throw on nitro events
if (!isClassic) {
throw new Error(
"This method is only for classic transactions. Use 'getParentToChildMessages' for nitro transactions."
)
}
const messageNums = this.getInboxMessageDeliveredEvents().map(
msg => msg.messageNum
)
return messageNums.map(
messageNum =>
new ParentToChildMessageReaderClassic(
childProvider,
BigNumber.from(chainId).toNumber(),
messageNum
)
)
}
/**
* Get any parent-to-child messages created by this transaction
* @param childSignerOrProvider
*/
public async getParentToChildMessages<T extends SignerOrProvider>(
childSignerOrProvider: T
): Promise<ParentToChildMessageReaderOrWriter<T>[]>
public async getParentToChildMessages<T extends SignerOrProvider>(
childSignerOrProvider: T
): Promise<ParentToChildMessageReader[] | ParentToChildMessageWriter[]> {
const provider = SignerProviderUtils.getProviderOrThrow(
childSignerOrProvider
)
const network = await getArbitrumNetwork(provider)
const chainId = network.chainId.toString()
const isClassic = await this.isClassic(provider)
// throw on classic events
if (isClassic) {
throw new Error(
"This method is only for nitro transactions. Use 'getParentToChildMessagesClassic' for classic transactions."
)
}
const events = this.getMessageEvents()
return events
.filter(
e =>
e.bridgeMessageEvent.kind ===
InboxMessageKind.L1MessageType_submitRetryableTx &&
e.bridgeMessageEvent.inbox.toLowerCase() ===
network.ethBridge.inbox.toLowerCase()
)
.map(mn => {
const messageDataParser = new SubmitRetryableMessageDataParser()
const inboxMessageData = messageDataParser.parse(
mn.inboxMessageEvent.data
)
return ParentToChildMessage.fromEventComponents(
childSignerOrProvider,
BigNumber.from(chainId).toNumber(),
mn.bridgeMessageEvent.sender,
mn.inboxMessageEvent.messageNum,
mn.bridgeMessageEvent.baseFeeL1,
inboxMessageData
)
})
}
/**
* Get any token deposit events created by this transaction
* @returns
*/
public getTokenDepositEvents() {
return parseTypedLogs(
L1ERC20Gateway__factory,
this.logs,
'DepositInitiated'
)
}
/**
* Replaces the wait function with one that returns a {@link ParentTransactionReceipt}
* @param contractTransaction
* @returns
*/
public static monkeyPatchWait = (
contractTransaction: ContractTransaction
): ParentContractTransaction => {
const wait = contractTransaction.wait
contractTransaction.wait = async (confirmations?: number) => {
const result = await wait(confirmations)
return new ParentTransactionReceipt(result)
}
return contractTransaction as ParentContractTransaction
}
/**
* Replaces the wait function with one that returns a {@link ParentEthDepositTransactionReceipt}
* @param contractTransaction
* @returns
*/
public static monkeyPatchEthDepositWait = (
contractTransaction: ContractTransaction
): ParentEthDepositTransaction => {
const wait = contractTransaction.wait
contractTransaction.wait = async (confirmations?: number) => {
const result = await wait(confirmations)
return new ParentEthDepositTransactionReceipt(result)
}
return contractTransaction as ParentEthDepositTransaction
}
/**
* Replaces the wait function with one that returns a {@link ParentContractCallTransactionReceipt}
* @param contractTransaction
* @returns
*/
public static monkeyPatchContractCallWait = (
contractTransaction: ContractTransaction
): ParentContractCallTransaction => {
const wait = contractTransaction.wait
contractTransaction.wait = async (confirmations?: number) => {
const result = await wait(confirmations)
return new ParentContractCallTransactionReceipt(result)
}
return contractTransaction as ParentContractCallTransaction
}
}
/**
* A {@link ParentTransactionReceipt} with additional functionality that only exists
* if the transaction created a single eth deposit.
*/
export class ParentEthDepositTransactionReceipt extends ParentTransactionReceipt {
/**
* Wait for the funds to arrive on the child chain
* @param confirmations Amount of confirmations the retryable ticket and the auto redeem receipt should have
* @param timeout Amount of time to wait for the retryable ticket to be created
* Defaults to 15 minutes, as by this time all transactions are expected to be included on the child chain. Throws on timeout.
* @returns The wait result contains `complete`, a `status`, the ParentToChildMessage and optionally the `childTxReceipt`
* If `complete` is true then this message is in the terminal state.
* For eth deposits complete this is when the status is FUNDS_DEPOSITED, EXPIRED or REDEEMED.
*/
public async waitForChildTransactionReceipt(
childProvider: Provider,
confirmations?: number,
timeout?: number
): Promise<
{
complete: boolean
message: EthDepositMessage
} & EthDepositMessageWaitForStatusResult
> {
const message = (await this.getEthDeposits(childProvider))[0]
if (!message)
throw new ArbSdkError('Unexpected missing Eth Deposit message.')
const res = await message.wait(confirmations, timeout)
return {
complete: isDefined(res),
childTxReceipt: res,
message,
}
}
}
/**
* A {@link ParentTransactionReceipt} with additional functionality that only exists
* if the transaction created a single call to a child chain contract - this includes
* token deposits.
*/
export class ParentContractCallTransactionReceipt extends ParentTransactionReceipt {
/**
* Wait for the transaction to arrive and be executed on the child chain
* @param confirmations Amount of confirmations the retryable ticket and the auto redeem receipt should have
* @param timeout Amount of time to wait for the retryable ticket to be created
* Defaults to 15 minutes, as by this time all transactions are expected to be included on the child chain. Throws on timeout.
* @returns The wait result contains `complete`, a `status`, a {@link ParentToChildMessage} and optionally the `childTxReceipt`.
* If `complete` is true then this message is in the terminal state.
* For contract calls this is true only if the status is REDEEMED.
*/
public async waitForChildTransactionReceipt<T extends SignerOrProvider>(
childSignerOrProvider: T,
confirmations?: number,
timeout?: number
): Promise<
{
complete: boolean
message: ParentToChildMessageReaderOrWriter<T>
} & ParentToChildMessageWaitForStatusResult
> {
const message = (
await this.getParentToChildMessages(childSignerOrProvider)
)[0]
if (!message)
throw new ArbSdkError('Unexpected missing Parent-to-child message.')
const res = await message.waitForStatus(confirmations, timeout)
return {
complete: res.status === ParentToChildMessageStatus.REDEEMED,
...res,
message,
}
}
}
================================================
File: packages/sdk/src/lib/message/messageDataParser.ts
================================================
import { getAddress } from '@ethersproject/address'
import { defaultAbiCoder } from '@ethersproject/abi'
import { BigNumber } from '@ethersproject/bignumber'
import { hexZeroPad } from '@ethersproject/bytes'
export class SubmitRetryableMessageDataParser {
/**
* Parse the event data emitted in the InboxMessageDelivered event
* for messages of type L1MessageType_submitRetryableTx
* @param eventData The data field in InboxMessageDelivered for messages of kind L1MessageType_submitRetryableTx
* @returns
*/
public parse(eventData: string) {
// decode the data field - is been packed so we cant decode the bytes field this way
const parsed = defaultAbiCoder.decode(
[
'uint256', // dest
'uint256', // l2 call balue
'uint256', // msg val
'uint256', // max submission
'uint256', // excess fee refund addr
'uint256', // call value refund addr
'uint256', // max gas
'uint256', // gas price bid
'uint256', // data length
],
eventData
) as BigNumber[]
const addressFromBigNumber = (bn: BigNumber) =>
getAddress(hexZeroPad(bn.toHexString(), 20))
const destAddress = addressFromBigNumber(parsed[0])
const l2CallValue = parsed[1]
const l1Value = parsed[2]
const maxSubmissionFee = parsed[3]
const excessFeeRefundAddress = addressFromBigNumber(parsed[4])
const callValueRefundAddress = addressFromBigNumber(parsed[5])
const gasLimit = parsed[6]
const maxFeePerGas = parsed[7]
const callDataLength = parsed[8]
const data =
'0x' +
eventData.substring(eventData.length - callDataLength.mul(2).toNumber())
return {
destAddress,
l2CallValue,
l1Value,
maxSubmissionFee: maxSubmissionFee,
excessFeeRefundAddress,
callValueRefundAddress,
gasLimit,
maxFeePerGas,
data,
}
}
}
================================================
File: packages/sdk/src/lib/utils/arbProvider.ts
================================================
import {
JsonRpcProvider,
Formatter,
BlockTag,
Web3Provider,
JsonRpcFetchFunc,
} from '@ethersproject/providers'
import { Formats } from '@ethersproject/providers/lib/formatter'
import { Networkish } from '@ethersproject/networks'
import {
ArbBlock,
ArbBlockWithTransactions,
ArbTransactionReceipt,
} from '../dataEntities/rpc'
class ArbFormatter extends Formatter {
readonly formats!: Formats
public getDefaultFormats(): Formats {
// formats was already initialised in super, so we can just access here
const superFormats = super.getDefaultFormats()
const bigNumber = this.bigNumber.bind(this)
const hash = this.hash.bind(this)
const number = this.number.bind(this)
const arbBlockProps = {
sendRoot: hash,
sendCount: bigNumber,
l1BlockNumber: number,
}
const arbReceiptFormat = {
...superFormats.receipt,
l1BlockNumber: number,
gasUsedForL1: bigNumber,
}
return {
...superFormats,
receipt: arbReceiptFormat,
block: { ...superFormats.block, ...arbBlockProps },
blockWithTransactions: {
...superFormats.blockWithTransactions,
...arbBlockProps,
},
}
}
public receipt(value: any): ArbTransactionReceipt {
return super.receipt(value) as ArbTransactionReceipt
}
public block(block: any): ArbBlock {
return super.block(block) as ArbBlock
}
public blockWithTransactions(block: any): ArbBlock {
// ethersjs chose the wrong type for the super - it should have been BlockWithTransactions
// but was instead just Block. This means that when we override we cant use ArbBlockWithTransactions
// but must instead use just ArbBlock.
return super.blockWithTransactions(block) as ArbBlock
}
}
/**
* Arbitrum specific formats
*/
export class ArbitrumProvider extends Web3Provider {
private static arbFormatter = new ArbFormatter()
/**
* Arbitrum specific formats
* @param provider Must be connected to an Arbitrum network
* @param network Must be an Arbitrum network
*/
public constructor(provider: JsonRpcProvider, network?: Networkish) {
super(provider.send.bind(provider) as JsonRpcFetchFunc, network)
}
static override getFormatter(): Formatter {
return this.arbFormatter
}
public override async getTransactionReceipt(
transactionHash: string | Promise<string>
): Promise<ArbTransactionReceipt> {
return (await super.getTransactionReceipt(
transactionHash
)) as ArbTransactionReceipt
}
public override async getBlockWithTransactions(
blockHashOrBlockTag: BlockTag | Promise<BlockTag>
): Promise<ArbBlockWithTransactions> {
return (await super.getBlockWithTransactions(
blockHashOrBlockTag
)) as ArbBlockWithTransactions
}
public override async getBlock(
blockHashOrBlockTag: BlockTag | Promise<BlockTag>
): Promise<ArbBlock> {
return (await super.getBlock(blockHashOrBlockTag)) as ArbBlock
}
}
================================================
File: packages/sdk/src/lib/utils/byte_serialize_params.ts
================================================
/*
* Copyright 2021, Offchain Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env node */
'use strict'
/**
#### Byte Serializing Solidity Arguments Schema
Arbitrum SDK includes methods for [serializing parameters](https://developer.offchainlabs.com/docs/special_features#parameter-byte-serialization) for a solidity method into a single byte array to minimize calldata. It uses the following schema:
#### address[]:
| field | size (bytes) | Description |
| ------------- | ------------------ | ----------------------------------------------------------------------- |
| length | 1 | Size of array |
| is-registered | 1 | 1 = all registered, 0 = not all registered |
| addresses | 4 or 20 (x length) | If is registered, left-padded 4-byte integers; otherwise, eth addresses |
#### non-address[]:
| field | size (bytes) | Description |
| ------ | ------------ | ------------------------ |
| length | 1 | Size of array |
| items | (variable) | All items (concatenated) |
#### address:
| field | size (bytes) | Description |
| ------------- | ------------ | ----------------------------------------------------------------- |
| is-registered | 1 | 1 = registered, 0 = not registered |
| address | 4 or 20 | If registered, left-padded 4-byte integer; otherwise, eth address |
* @module Byte-Serialization
*/
import { Provider } from '@ethersproject/abstract-provider'
import { Signer } from '@ethersproject/abstract-signer'
import { isAddress as _isAddress } from '@ethersproject/address'
import { concat, hexZeroPad } from '@ethersproject/bytes'
import { BigNumber } from '@ethersproject/bignumber'
import { ArbAddressTable__factory } from '../abi/factories/ArbAddressTable__factory'
import { ArbAddressTable } from '../abi/ArbAddressTable'
import { ARB_ADDRESS_TABLE_ADDRESS } from '../dataEntities/constants'
import { ArbSdkError } from '../dataEntities/errors'
type PrimativeType = string | number | boolean | BigNumber
type PrimativeOrPrimativeArray = PrimativeType | PrimativeType[]
type BytesNumber = 1 | 4 | 8 | 16 | 32
interface AddressIndexMemo {
[address: string]: number
}
export const getAddressIndex = (() => {
const addressToIndexMemo: AddressIndexMemo = {}
let arbAddressTable: ArbAddressTable | undefined
return async (address: string, signerOrProvider: Signer | Provider) => {
if (addressToIndexMemo[address]) {
return addressToIndexMemo[address]
}
arbAddressTable =
arbAddressTable ||
ArbAddressTable__factory.connect(
ARB_ADDRESS_TABLE_ADDRESS,
signerOrProvider
)
const isRegistered = await arbAddressTable.addressExists(address)
if (isRegistered) {
const index = (await arbAddressTable.lookup(address)).toNumber()
addressToIndexMemo[address] = index
return index
} else {
return -1
}
}
})()
/**
to use:
```js
const mySerializeParamsFunction = argSerializerConstructor("rpcurl")
mySerializeParamsFunction(["4","5", "6"])
```
*/
export const argSerializerConstructor = (
arbProvider: Provider
): ((params: PrimativeOrPrimativeArray[]) => Promise<Uint8Array>) => {
return async (params: PrimativeOrPrimativeArray[]) => {
return await serializeParams(params, async (address: string) => {
return await getAddressIndex(address, arbProvider)
})
}
}
const isAddress = (input: PrimativeType) =>
typeof input === 'string' && _isAddress(input)
const toUint = (val: PrimativeType, bytes: BytesNumber) =>
hexZeroPad(BigNumber.from(val).toHexString(), bytes)
// outputs string suitable for formatting
const formatPrimative = (value: PrimativeType) => {
if (isAddress(value)) {
return value as string
} else if (typeof value === 'boolean') {
return toUint(value ? 1 : 0, 1)
} else if (
typeof value === 'number' ||
Number(value) ||
BigNumber.isBigNumber(value)
) {
return toUint(value, 32)
} else {
throw new ArbSdkError('unsupported type')
}
}
/**
* @param params array of serializable types to
* @param addressToIndex optional getter of address index registered in table
*/
export const serializeParams = async (
params: PrimativeOrPrimativeArray[],
addressToIndex: (address: string) => Promise<number> = () =>
new Promise(exec => exec(-1))
): Promise<Uint8Array> => {
const formattedParams: string[] = []
for (const param of params) {
// handle arrays
if (Array.isArray(param)) {
let paramArray: PrimativeType[] = param as PrimativeType[]
formattedParams.push(toUint(paramArray.length, 1))
if (isAddress(paramArray[0])) {
const indices = await Promise.all(
paramArray.map(
async address => await addressToIndex(address as string)
)
)
// If all addresses are registered, serialize as indices
if (indices.every(i => i > -1)) {
paramArray = indices as number[]
formattedParams.push(toUint(1, 1))
paramArray.forEach(value => {
formattedParams.push(toUint(value, 4))
})
// otherwise serialize as address
} else {
formattedParams.push(toUint(0, 1))
paramArray.forEach(value => {
formattedParams.push(formatPrimative(value))
})
}
} else {
paramArray.forEach(value => {
formattedParams.push(formatPrimative(value))
})
}
} else {
// handle non-arrays
if (isAddress(param)) {
const index = await addressToIndex(param as string)
if (index > -1) {
formattedParams.push(toUint(1, 1), toUint(index, 4))
} else {
formattedParams.push(toUint(0, 1), formatPrimative(param))
}
} else {
formattedParams.push(formatPrimative(param))
}
}
}
return concat(formattedParams)
}
================================================
File: packages/sdk/src/lib/utils/calldata.ts
================================================
import { L1GatewayRouter__factory } from '../abi/factories/L1GatewayRouter__factory'
import { ParentToChildTxReqAndSigner } from '../assetBridger/ethBridger'
export const getErc20ParentAddressFromParentToChildTxRequest = (
txReq: ParentToChildTxReqAndSigner
): string => {
const {
txRequest: { data },
} = txReq
const iGatewayRouter = L1GatewayRouter__factory.createInterface()
try {
const decodedData = iGatewayRouter.decodeFunctionData(
'outboundTransfer',
data
)
return decodedData['_token']
} catch {
try {
const decodedData = iGatewayRouter.decodeFunctionData(
'outboundTransferCustomRefund',
data
)
return decodedData['_token']
} catch {
throw new Error('data signature not matching deposits methods')
}
}
}
================================================
File: packages/sdk/src/lib/utils/env.ts
================================================
import * as dotenv from 'dotenv'
import * as path from 'path'
export const loadEnv = () => {
dotenv.config({ path: path.resolve(__dirname, '../../../../../.env') })
}
================================================
File: packages/sdk/src/lib/utils/eventFetcher.ts
================================================
/*
* Copyright 2021, Offchain Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env node */
'use strict'
import { Provider, BlockTag, Filter } from '@ethersproject/abstract-provider'
import { Contract, Event } from '@ethersproject/contracts'
import { constants } from 'ethers'
import { TypedEvent, TypedEventFilter } from '../abi/common'
import { EventArgs, TypeChainContractFactory } from '../dataEntities/event'
export type FetchedEvent<TEvent extends Event> = {
event: EventArgs<TEvent>
topic: string
name: string
blockNumber: number
blockHash: string
transactionHash: string
address: string
topics: string[]
data: string
}
// I'm not sure why, but I wasn't able to get the getEvents function to properly
// infer the Event return type. It would always infer it as TypedEvent<any, any>
// instead of the strong typed event that should be available. This type correctly
// infers the event type so we can force getEvents to return the correct type
// using this.
type TEventOf<T> = T extends TypedEventFilter<infer TEvent> ? TEvent : never
/**
* Fetches and parses blockchain logs
*/
export class EventFetcher {
public constructor(public readonly provider: Provider) {}
/**
* Fetch logs and parse logs
* @param contractFactory A contract factory for generating a contract of type TContract at the addr
* @param topicGenerator Generator function for creating
* @param filter Block and address filter parameters
* @returns
*/
public async getEvents<
TContract extends Contract,
TEventFilter extends TypedEventFilter<TypedEvent>
>(
contractFactory: TypeChainContractFactory<TContract>,
topicGenerator: (t: TContract) => TEventFilter,
filter: {
fromBlock: BlockTag
toBlock: BlockTag
address?: string
}
): Promise<FetchedEvent<TEventOf<TEventFilter>>[]> {
const contract = contractFactory.connect(
filter.address || constants.AddressZero,
this.provider
)
const eventFilter = topicGenerator(contract)
const fullFilter: Filter = {
...eventFilter,
address: filter.address,
fromBlock: filter.fromBlock,
toBlock: filter.toBlock,
}
const logs = await this.provider.getLogs(fullFilter)
return logs
.filter(l => l.removed === false)
.map(l => {
const pLog = contract.interface.parseLog(l)
return {
event: pLog.args,
topic: pLog.topic,
name: pLog.name,
blockNumber: l.blockNumber,
blockHash: l.blockHash,
transactionHash: l.transactionHash,
address: l.address,
topics: l.topics,
data: l.data,
}
}) as FetchedEvent<TEventOf<TEventFilter>>[]
}
}
================================================
File: packages/sdk/src/lib/utils/lib.ts
================================================
import { BigNumber, constants } from 'ethers'
import { Provider } from '@ethersproject/abstract-provider'
import { TransactionReceipt, JsonRpcProvider } from '@ethersproject/providers'
import { ArbSdkError } from '../dataEntities/errors'
import { ArbitrumProvider } from './arbProvider'
import { ArbSys__factory } from '../abi/factories/ArbSys__factory'
import { ARB_SYS_ADDRESS } from '../dataEntities/constants'
import { ArbitrumNetwork, getNitroGenesisBlock } from '../dataEntities/networks'
import { ERC20__factory } from '../abi/factories/ERC20__factory'
export const wait = (ms: number): Promise<void> =>
new Promise(res => setTimeout(res, ms))
export const getBaseFee = async (provider: Provider): Promise<BigNumber> => {
const baseFee = (await provider.getBlock('latest')).baseFeePerGas
if (!baseFee) {
throw new ArbSdkError(
'Latest block did not contain base fee, ensure provider is connected to a network that supports EIP 1559.'
)
}
return baseFee
}
/**
* Waits for a transaction receipt if confirmations or timeout is provided
* Otherwise tries to fetch straight away.
* @param provider
* @param txHash
* @param confirmations
* @param timeout
* @returns
*/
export const getTransactionReceipt = async (
provider: Provider,
txHash: string,
confirmations?: number,
timeout?: number
): Promise<TransactionReceipt | null> => {
if (confirmations || timeout) {
try {
const receipt = await provider.waitForTransaction(
txHash,
confirmations,
timeout
)
return receipt || null
} catch (err) {
if ((err as Error).message.includes('timeout exceeded')) {
// return null
return null
} else throw err
}
} else {
const receipt = await provider.getTransactionReceipt(txHash)
return receipt || null
}
}
export const isDefined = <T>(val: T | null | undefined): val is T =>
typeof val !== 'undefined' && val !== null
export const isArbitrumChain = async (provider: Provider): Promise<boolean> => {
try {
await ArbSys__factory.connect(ARB_SYS_ADDRESS, provider).arbOSVersion()
} catch (error) {
return false
}
return true
}
type GetFirstBlockForL1BlockProps = {
arbitrumProvider: JsonRpcProvider
forL1Block: number
allowGreater?: boolean
minArbitrumBlock?: number
maxArbitrumBlock?: number | 'latest'
}
/**
* This function performs a binary search to find the first Arbitrum block that corresponds to a given L1 block number.
* The function returns a Promise that resolves to a number if a block is found, or undefined otherwise.
*
* @param {JsonRpcProvider} arbitrumProvider - The Arbitrum provider to use for the search.
* @param {number} forL1Block - The L1 block number to search for.
* @param {boolean} [allowGreater=false] - Whether to allow the search to go past the specified `forL1Block`.
* @param {number|string} minArbitrumBlock - The minimum Arbitrum block number to start the search from. Cannot be below the network's Nitro genesis block.
* @param {number|string} [maxArbitrumBlock='latest'] - The maximum Arbitrum block number to end the search at. Can be a `number` or `'latest'`. `'latest'` is the current block.
* @returns {Promise<number | undefined>} - A Promise that resolves to a number if a block is found, or undefined otherwise.
*/
export async function getFirstBlockForL1Block({
arbitrumProvider,
forL1Block,
allowGreater = false,
minArbitrumBlock,
maxArbitrumBlock = 'latest',
}: GetFirstBlockForL1BlockProps): Promise<number | undefined> {
if (!(await isArbitrumChain(arbitrumProvider))) {
// Provider is L1.
return forL1Block
}
const arbProvider = new ArbitrumProvider(arbitrumProvider)
const currentArbBlock = await arbProvider.getBlockNumber()
const arbitrumChainId = (await arbProvider.getNetwork()).chainId
const nitroGenesisBlock = getNitroGenesisBlock(arbitrumChainId)
async function getL1Block(forL2Block: number) {
const { l1BlockNumber } = await arbProvider.getBlock(forL2Block)
return l1BlockNumber
}
if (!minArbitrumBlock) {
minArbitrumBlock = nitroGenesisBlock
}
if (maxArbitrumBlock === 'latest') {
maxArbitrumBlock = currentArbBlock
}
if (minArbitrumBlock >= maxArbitrumBlock) {
throw new Error(
`'minArbitrumBlock' (${minArbitrumBlock}) must be lower than 'maxArbitrumBlock' (${maxArbitrumBlock}).`
)
}
if (minArbitrumBlock < nitroGenesisBlock) {
throw new Error(
`'minArbitrumBlock' (${minArbitrumBlock}) cannot be below the Nitro genesis block, which is ${nitroGenesisBlock} for the current network.`
)
}
let start = minArbitrumBlock
let end = maxArbitrumBlock
let resultForTargetBlock
let resultForGreaterBlock
while (start <= end) {
// Calculate the midpoint of the current range.
const mid = start + Math.floor((end - start) / 2)
const l1Block = await getL1Block(mid)
// If the midpoint matches the target, we've found a match.
// Adjust the range to search for the first occurrence.
if (l1Block === forL1Block) {
end = mid - 1
} else if (l1Block < forL1Block) {
start = mid + 1
} else {
end = mid - 1
}
// Stores last valid Arbitrum block corresponding to the current, or greater, L1 block.
if (l1Block) {
if (l1Block === forL1Block) {
resultForTargetBlock = mid
}
if (allowGreater && l1Block > forL1Block) {
resultForGreaterBlock = mid
}
}
}
return resultForTargetBlock ?? resultForGreaterBlock
}
export const getBlockRangesForL1Block = async (
props: GetFirstBlockForL1BlockProps
) => {
const arbProvider = new ArbitrumProvider(props.arbitrumProvider)
const currentArbitrumBlock = await arbProvider.getBlockNumber()
if (!props.maxArbitrumBlock || props.maxArbitrumBlock === 'latest') {
props.maxArbitrumBlock = currentArbitrumBlock
}
const result = await Promise.all([
getFirstBlockForL1Block({ ...props, allowGreater: false }),
getFirstBlockForL1Block({
...props,
forL1Block: props.forL1Block + 1,
allowGreater: true,
}),
])
if (!result[0]) {
// If there's no start of the range, there won't be the end either.
return [undefined, undefined]
}
if (result[0] && result[1]) {
// If both results are defined, we can assume that the previous Arbitrum block for the end of the range will be for 'forL1Block'.
return [result[0], result[1] - 1]
}
return [result[0], props.maxArbitrumBlock]
}
export async function getNativeTokenDecimals({
parentProvider,
childNetwork,
}: {
parentProvider: Provider
childNetwork: ArbitrumNetwork
}) {
const nativeTokenAddress = childNetwork.nativeToken
if (!nativeTokenAddress || nativeTokenAddress === constants.AddressZero) {
return 18
}
const nativeTokenContract = ERC20__factory.connect(
nativeTokenAddress,
parentProvider
)
try {
return await nativeTokenContract.decimals()
} catch {
return 0
}
}
export function scaleFrom18DecimalsToNativeTokenDecimals({
amount,
decimals,
}: {
amount: BigNumber
decimals: number
}) {
// do nothing for 18 decimals
if (decimals === 18) {
return amount
}
if (decimals < 18) {
const scaledAmount = amount.div(
BigNumber.from(10).pow(BigNumber.from(18 - decimals))
)
// round up if necessary
if (
scaledAmount
.mul(BigNumber.from(10).pow(BigNumber.from(18 - decimals)))
.lt(amount)
) {
return scaledAmount.add(BigNumber.from(1))
}
return scaledAmount
}
// decimals > 18
return amount.mul(BigNumber.from(10).pow(BigNumber.from(decimals - 18)))
}
export function scaleFromNativeTokenDecimalsTo18Decimals({
amount,
decimals,
}: {
amount: BigNumber
decimals: number
}) {
if (decimals < 18) {
return amount.mul(BigNumber.from(10).pow(18 - decimals))
} else if (decimals > 18) {
return amount.div(BigNumber.from(10).pow(decimals - 18))
}
return amount
}
================================================
File: packages/sdk/src/lib/utils/multicall.ts
================================================
/*
* Copyright 2021, Offchain Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env node */
'use strict'
import { Provider } from '@ethersproject/abstract-provider'
import { BigNumber, utils } from 'ethers'
import { ERC20__factory } from '../abi/factories/ERC20__factory'
import { Multicall2 } from '../abi/Multicall2'
import { Multicall2__factory } from '../abi/factories/Multicall2__factory'
import { getMulticallAddress } from '../dataEntities/networks'
/**
* Input to multicall aggregator
*/
export type CallInput<T> = {
/**
* Address of the target contract to be called
*/
targetAddr: string
/**
* Function to produce encoded call data
*/
encoder: () => string
/**
* Function to decode the result of the call
*/
decoder: (returnData: string) => T
}
/**
* For each item in T this DecoderReturnType<T> yields the return
* type of the decoder property.
* If we require success then the result cannot be undefined
*/
type DecoderReturnType<
T extends CallInput<unknown>[],
TRequireSuccess extends boolean
> = {
[P in keyof T]: T[P] extends CallInput<unknown>
? TRequireSuccess extends true
? ReturnType<T[P]['decoder']>
: ReturnType<T[P]['decoder']> | undefined
: never
}
///////////////////////////////////////
/////// TOKEN CONDITIONAL TYPES ///////
///////////////////////////////////////
// these conditional types return check T, and if it matches
// the input type then they return a known output type
type AllowanceInputOutput<T> = T extends {
allowance: { owner: string; spender: string }
}
? { allowance: BigNumber | undefined }
: Record<string, never>
type BalanceInputOutput<T> = T extends { balanceOf: { account: string } }
? { balance: BigNumber | undefined }
: Record<string, never>
type DecimalsInputOutput<T> = T extends { decimals: true }
? { decimals: number | undefined }
: Record<string, never>
type NameInputOutput<T> = T extends { name: true }
? { name: string | undefined }
: Record<string, never>
type SymbolInputOutput<T> = T extends { symbol: true }
? { symbol: string | undefined }
: Record<string, never>
type TokenMultiInput = {
balanceOf?: {
account: string
}
allowance?: {
owner: string
spender: string
}
symbol?: true
decimals?: true
name?: true
}
// if we were given options at all then we convert
// those options to outputs
type TokenInputOutput<T> = T extends TokenMultiInput
? AllowanceInputOutput<T> &
BalanceInputOutput<T> &
DecimalsInputOutput<T> &
NameInputOutput<T> &
SymbolInputOutput<T>
: { name: string }
//\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
//\\\\\ TOKEN CONDITIONAL TYPES \\\\\\\
//\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
/**
* Util for executing multi calls against the MultiCallV2 contract
*/
export class MultiCaller {
constructor(
private readonly provider: Provider,
/**
* Address of multicall contract
*/
public readonly address: string
) {}
/**
* Finds the correct multicall address for the given provider and instantiates a multicaller
* @param provider
* @returns
*/
public static async fromProvider(provider: Provider): Promise<MultiCaller> {
return new MultiCaller(provider, await getMulticallAddress(provider))
}
/**
* Get the call input for the current block number
* @returns
*/
public getBlockNumberInput(): CallInput<
Awaited<ReturnType<Multicall2['getBlockNumber']>>
> {
const iFace = Multicall2__factory.createInterface()
return {
targetAddr: this.address,
encoder: () => iFace.encodeFunctionData('getBlockNumber'),
decoder: (returnData: string) =>
iFace.decodeFunctionResult('getBlockNumber', returnData)[0],
}
}
/**
* Get the call input for the current block timestamp
* @returns
*/
public getCurrentBlockTimestampInput(): CallInput<
Awaited<ReturnType<Multicall2['getCurrentBlockTimestamp']>>
> {
const iFace = Multicall2__factory.createInterface()
return {
targetAddr: this.address,
encoder: () => iFace.encodeFunctionData('getCurrentBlockTimestamp'),
decoder: (returnData: string) =>
iFace.decodeFunctionResult('getCurrentBlockTimestamp', returnData)[0],
}
}
/**
* Executes a multicall for the given parameters
* Return values are order the same as the inputs.
* If a call failed undefined is returned instead of the value.
*
* To get better type inference when the individual calls are of different types
* create your inputs as a tuple and pass the tuple in. The return type will be
* a tuple of the decoded return types. eg.
*
*
* ```typescript
* const inputs: [
* CallInput<Awaited<ReturnType<ERC20['functions']['balanceOf']>>[0]>,
* CallInput<Awaited<ReturnType<ERC20['functions']['name']>>[0]>
* ] = [
* {
* targetAddr: token.address,
* encoder: () => token.interface.encodeFunctionData('balanceOf', ['']),
* decoder: (returnData: string) =>
* token.interface.decodeFunctionResult('balanceOf', returnData)[0],
* },
* {
* targetAddr: token.address,
* encoder: () => token.interface.encodeFunctionData('name'),
* decoder: (returnData: string) =>
* token.interface.decodeFunctionResult('name', returnData)[0],
* },
* ]
*
* const res = await multiCaller.call(inputs)
* ```
* @param provider
* @param params
* @param requireSuccess Fail the whole call if any internal call fails
* @returns
*/
public async multiCall<
T extends CallInput<unknown>[],
TRequireSuccess extends boolean
>(
params: T,
requireSuccess?: TRequireSuccess
): Promise<DecoderReturnType<T, TRequireSuccess>> {
const defaultedRequireSuccess = requireSuccess || false
const multiCall = Multicall2__factory.connect(this.address, this.provider)
const args = params.map(p => ({
target: p.targetAddr,
callData: p.encoder(),
}))
const outputs = await multiCall.callStatic.tryAggregate(
defaultedRequireSuccess,
args
)
return outputs.map(({ success, returnData }, index) => {
if (success && returnData && returnData != '0x') {
return params[index].decoder(returnData)
}
return undefined
}) as DecoderReturnType<T, TRequireSuccess>
}
/**
* Multicall for token properties. Will collect all the requested properies for each of the
* supplied token addresses.
* @param erc20Addresses
* @param options Defaults to just 'name'
* @returns
*/
public async getTokenData<T extends TokenMultiInput | undefined>(
erc20Addresses: string[],
options?: T
): // based on the type of options we return only the fields that were specified
Promise<TokenInputOutput<T>[]>
public async getTokenData<T extends TokenMultiInput | undefined>(
erc20Addresses: string[],
options?: T
): Promise<
| { name: string }[]
| {
balance?: BigNumber
allowance?: BigNumber
symbol?: string
decimals?: number
name?: string
}[]
> {
// if no options are supplied, then we just multicall for the names
const defaultedOptions: TokenMultiInput = options || { name: true }
const erc20Iface = ERC20__factory.createInterface()
const isBytes32 = (data: string) =>
utils.isHexString(data) && utils.hexDataLength(data) === 32
const input = []
for (const t of erc20Addresses) {
if (defaultedOptions.allowance) {
input.push({
targetAddr: t,
encoder: () =>
erc20Iface.encodeFunctionData('allowance', [
defaultedOptions.allowance!.owner,
defaultedOptions.allowance!.spender,
]),
decoder: (returnData: string) =>
erc20Iface.decodeFunctionResult(
'allowance',
returnData
)[0] as BigNumber,
})
}
if (defaultedOptions.balanceOf) {
input.push({
targetAddr: t,
encoder: () =>
erc20Iface.encodeFunctionData('balanceOf', [
defaultedOptions.balanceOf!.account,
]),
decoder: (returnData: string) =>
erc20Iface.decodeFunctionResult(
'balanceOf',
returnData
)[0] as BigNumber,
})
}
if (defaultedOptions.decimals) {
input.push({
targetAddr: t,
encoder: () => erc20Iface.encodeFunctionData('decimals'),
decoder: (returnData: string) =>
erc20Iface.decodeFunctionResult(
'decimals',
returnData
)[0] as number,
})
}
if (defaultedOptions.name) {
input.push({
targetAddr: t,
encoder: () => erc20Iface.encodeFunctionData('name'),
decoder: (returnData: string) => {
// Maker doesn't follow the erc20 spec and returns bytes32 data.
// https://etherscan.io/token/0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2#readContract
if (isBytes32(returnData)) {
return utils.parseBytes32String(returnData) as string
} else
return erc20Iface.decodeFunctionResult(
'name',
returnData
)[0] as string
},
})
}
if (defaultedOptions.symbol) {
input.push({
targetAddr: t,
encoder: () => erc20Iface.encodeFunctionData('symbol'),
decoder: (returnData: string) => {
// Maker doesn't follow the erc20 spec and returns bytes32 data.
// https://etherscan.io/token/0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2#readContract
if (isBytes32(returnData)) {
return utils.parseBytes32String(returnData) as string
} else
return erc20Iface.decodeFunctionResult(
'symbol',
returnData
)[0] as string
},
})
}
}
const res = await this.multiCall(input)
let i = 0
const tokens = []
while (i < res.length) {
tokens.push({
allowance: defaultedOptions.allowance
? (res[i++] as BigNumber)
: undefined,
balance: defaultedOptions.balanceOf
? (res[i++] as BigNumber)
: undefined,
decimals: defaultedOptions.decimals ? (res[i++] as number) : undefined,
name: defaultedOptions.name ? (res[i++] as string) : undefined,
symbol: defaultedOptions.symbol ? (res[i++] as string) : undefined,
})
}
return tokens
}
}
================================================
File: packages/sdk/src/lib/utils/types.ts
================================================
/**
* Omit doesnt enforce that the seconds generic is a keyof the first
* OmitTyped guards against the underlying type prop names
* being refactored, and not being updated in the usage of OmitTyped
*/
export type OmitTyped<T, K extends keyof T> = Omit<T, K>
/**
* Make the specified properties optional
*/
export type PartialPick<T, K extends keyof T> = OmitTyped<T, K> & Partial<T>
/**
* Make the specified properties required
*/
export type RequiredPick<T, K extends keyof T> = Required<Pick<T, K>> & T
// https://twitter.com/mattpocockuk/status/1622730173446557697
export type Prettify<T> = {
[K in keyof T]: T[K]
// eslint-disable-next-line @typescript-eslint/ban-types
} & {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment