Created
January 18, 2025 22:47
-
-
Save anegg0/4dff314ee6599ea71b3e01ae81ba3652 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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