├── .clabot ├── .env-sample ├── .eslintrc.js ├── .github └── workflows │ └── build-test.yml ├── .gitignore ├── .mocharc.json ├── .npmrc ├── .prettierrc.js ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── audit-ci.jsonc ├── docs ├── 1-introduction.mdx └── 2-migrate.mdx ├── hardhat.config.ts ├── package.json ├── packages ├── ethers-viem-compat │ ├── .eslintignore │ ├── .eslintrc │ ├── .prettierignore │ ├── .prettierrc.js │ ├── package.json │ ├── src │ │ ├── compatibility.ts │ │ └── index.ts │ ├── tests │ │ └── compatibility.test.ts │ └── tsconfig.json └── sdk │ ├── .eslintignore │ ├── .eslintrc │ ├── .prettierignore │ ├── .prettierrc.js │ ├── package.json │ ├── 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 │ │ ├── Outbox.json │ │ ├── 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 │ ├── fork │ │ └── inbox.test.ts │ ├── integration │ │ ├── childTransactionReceipt.test.ts │ │ ├── custom-fee-token │ │ │ ├── customFeeTokenEthBridger.test.ts │ │ │ ├── customFeeTokenTestHelpers.ts │ │ │ └── mochaExtensions.ts │ │ ├── customerc20.test.ts │ │ ├── eth.test.ts │ │ ├── getArbitrumNetworkInformationFromRollup.test.ts │ │ ├── helper │ │ │ └── greeter.ts │ │ ├── l1l3Bridger.test.ts │ │ ├── parentToChildMessageCreator.test.ts │ │ ├── parentToChildMessageGasEstimator.test.ts │ │ ├── retryableData.test.ts │ │ ├── sanity.test.ts │ │ ├── sendChildmsg.test.ts │ │ ├── standarderc20.test.ts │ │ ├── testHelpers.ts │ │ └── weth.test.ts │ ├── testSetup.ts │ └── unit │ │ ├── addressAlias.test.ts │ │ ├── calldata.test.ts │ │ ├── childBlocksForL1Block.test.ts │ │ ├── childToParentMessageEvents.test.ts │ │ ├── messageDataParser.test.ts │ │ ├── multicall.test.ts │ │ ├── nativeToken.test.ts │ │ ├── network.test.ts │ │ └── parentToChildMessageEvents.test.ts │ ├── tsconfig.json │ ├── typedoc.json │ └── typedoc_md.js ├── tsconfig.json └── yarn.lock /.clabot: -------------------------------------------------------------------------------- 1 | { 2 | "contributors": "https://api.github.com/repos/OffchainLabs/clabot-config/contents/apache-contributors.json", 3 | "message": "We require contributors to sign our Contributor License Agreement. In order for us to review and merge your code, please sign one of the linked documents below to get yourself added. If you're an independent Individual please sign this form: https://na3.docusign.net/Member/PowerFormSigning.aspx?PowerFormId=1353a816-a9c1-47ba-847e-ec79f0f23d31&env=na3&acct=6e152afc-6284-44af-a4c1-d8ef291db402&v=2. If you're with a company (corporate) please sign this form: https://na3.docusign.net/Member/PowerFormSigning.aspx?PowerFormId=2b5fe8ba-51d4-4980-b4ee-605d66e675d4&env=na3&acct=6e152afc-6284-44af-a4c1-d8ef291db402&v=2. To agree to the CLA license, please fill out one of the attached forms." 4 | } 5 | -------------------------------------------------------------------------------- /.env-sample: -------------------------------------------------------------------------------- 1 | ARB_URL="http://127.0.0.1:8547" 2 | ETH_URL="http://127.0.0.1:8545" 3 | ORBIT_URL="http://127.0.0.1:3347" 4 | 5 | ARB_KEY="b6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659" 6 | ETH_KEY="b6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659" 7 | ORBIT_KEY="b6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659" 8 | 9 | INFURA_KEY="2323232323232323232" 10 | SHOULD_FORK="0" 11 | 12 | MAINNET_RPC="https://mainnet.infura.io/v3/2323" 13 | ARB_ONE_RPC="https://arb1.arbitrum.io/rpc" 14 | 15 | SEPOLIA_RPC="https://sepolia.infura.io/v3/2323" 16 | SEPOLIA_ROLLUP_TESTNET_RPC="https://sepolia-rollup.arbitrum.io/rpc" 17 | 18 | NOVA_RPC="soontm" 19 | 20 | ORBIT_TEST="0" 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | commonjs: true, 5 | es6: true, 6 | node: true, 7 | }, 8 | plugins: ['prettier'], 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 12 | ], 13 | parserOptions: { 14 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 15 | sourceType: 'module', // Allows for the use of imports 16 | }, 17 | rules: { 18 | 'prettier/prettier': 'error', 19 | 'no-unused-vars': 'off', 20 | 'prefer-const': [2, { destructuring: 'all' }], 21 | 'object-curly-spacing': ['error', 'always'], 22 | }, 23 | overrides: [ 24 | { 25 | // this config is run against test files (same as the one bellow but not limited to `src` folder) 26 | files: ['*.ts', '*.tsx'], 27 | parser: '@typescript-eslint/parser', 28 | extends: [ 29 | 'eslint:recommended', 30 | 'plugin:@typescript-eslint/recommended', 31 | 'plugin:prettier/recommended', 32 | ], 33 | plugins: ['@typescript-eslint', 'prettier'], 34 | rules: { 35 | 'no-empty-pattern': 'warn', 36 | 'prettier/prettier': ['error', { singleQuote: true }], 37 | '@typescript-eslint/member-delimiter-style': ['off'], 38 | '@typescript-eslint/no-explicit-any': ['off'], 39 | '@typescript-eslint/no-use-before-define': ['off'], 40 | '@typescript-eslint/no-non-null-assertion': ['off'], 41 | '@typescript-eslint/ban-ts-comment': ['warn'], 42 | '@typescript-eslint/no-unused-vars': [ 43 | 'warn', 44 | { 45 | argsIgnorePattern: '^_', 46 | varsIgnorePattern: '^_', 47 | caughtErrorsIgnorePattern: '^_', 48 | }, 49 | ], 50 | 'no-implicit-coercion': 'error', 51 | }, 52 | }, 53 | { 54 | files: ['src/**/*.ts', 'src/**/*.tsx'], 55 | parser: '@typescript-eslint/parser', 56 | parserOptions: { 57 | project: 'tsconfig.json', 58 | }, 59 | extends: [ 60 | 'eslint:recommended', 61 | 'plugin:@typescript-eslint/recommended', 62 | 'plugin:prettier/recommended', 63 | ], 64 | plugins: ['@typescript-eslint', 'prettier', '@typescript-eslint/tslint'], 65 | rules: { 66 | 'no-empty-pattern': 'warn', 67 | 'prettier/prettier': ['error', { singleQuote: true }], 68 | '@typescript-eslint/member-delimiter-style': ['off'], 69 | '@typescript-eslint/no-explicit-any': ['off'], 70 | '@typescript-eslint/no-use-before-define': ['off'], 71 | '@typescript-eslint/no-non-null-assertion': ['off'], 72 | '@typescript-eslint/ban-ts-comment': ['warn'], 73 | '@typescript-eslint/no-unused-vars': [ 74 | 'warn', 75 | { 76 | argsIgnorePattern: '^_', 77 | varsIgnorePattern: '^_', 78 | caughtErrorsIgnorePattern: '^_', 79 | }, 80 | ], 81 | '@typescript-eslint/tslint/config': [ 82 | 'error', 83 | { 84 | rules: { 'strict-comparisons': true }, 85 | }, 86 | ], 87 | 'no-implicit-coercion': 'error', 88 | '@typescript-eslint/no-shadow': ['error'], 89 | }, 90 | }, 91 | ], 92 | } 93 | -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: Build, Test 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | env: 8 | MAINNET_RPC: ${{ secrets.MAINNET_RPC }} 9 | 10 | jobs: 11 | install: 12 | name: Install on Node.js v${{ matrix.node-version }} 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: [18, 20] 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - name: Install node_modules 27 | uses: OffchainLabs/actions/node-modules/install@main 28 | with: 29 | cache-key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}-monorepo 30 | 31 | lint: 32 | name: Lint on Node.js v${{ matrix.node-version }} 33 | runs-on: ubuntu-latest 34 | strategy: 35 | matrix: 36 | node-version: [18, 20] 37 | needs: install 38 | permissions: 39 | checks: write # https://github.com/mikepenz/action-junit-report/issues/23#issuecomment-1412597753 40 | env: 41 | TEST_PATH: /tmp/test-results 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@v4 45 | 46 | - name: Set up Node.js 47 | uses: actions/setup-node@v4 48 | with: 49 | node-version: ${{ matrix.node-version }} 50 | 51 | - name: Install node_modules 52 | uses: OffchainLabs/actions/node-modules/install@main 53 | with: 54 | cache-key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}-monorepo 55 | 56 | - name: Lint sdk 57 | run: | 58 | yarn gen:abi 59 | yarn build 60 | yarn lint --format junit -o $TEST_PATH/sdk-lint.xml 61 | 62 | - name: Upload build artifacts 63 | uses: actions/upload-artifact@v4 64 | with: 65 | name: lint-results-${{ github.run_id }}-node-v${{ matrix.node-version }} 66 | path: ${{ env.TEST_PATH }} 67 | 68 | - name: Publish Test Report 69 | uses: mikepenz/action-junit-report@v5 70 | if: always() # always run even if the previous step fails 71 | with: 72 | report_paths: '${{ env.TEST_PATH }}/sdk-lint.xml' 73 | fail_on_failure: false 74 | 75 | audit: 76 | name: Audit on Node.js v${{ matrix.node-version }} 77 | runs-on: ubuntu-latest 78 | strategy: 79 | matrix: 80 | node-version: [18, 20] 81 | needs: install 82 | steps: 83 | - name: Checkout 84 | uses: actions/checkout@v4 85 | 86 | - name: Set up Node.js 87 | uses: actions/setup-node@v4 88 | with: 89 | node-version: ${{ matrix.node-version }} 90 | 91 | - name: Install node_modules 92 | uses: OffchainLabs/actions/node-modules/install@main 93 | with: 94 | cache-key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}-monorepo 95 | 96 | - run: yarn audit:ci 97 | 98 | test-unit: 99 | name: Test (Unit) on Node.js v${{ matrix.node-version }} 100 | runs-on: ubuntu-latest 101 | strategy: 102 | matrix: 103 | node-version: [18, 20] 104 | needs: install 105 | steps: 106 | - name: Checkout 107 | uses: actions/checkout@v4 108 | 109 | - name: Set up Node.js 110 | uses: actions/setup-node@v4 111 | with: 112 | node-version: ${{ matrix.node-version }} 113 | 114 | - name: Install node_modules 115 | uses: OffchainLabs/actions/node-modules/install@main 116 | with: 117 | cache-key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}-monorepo 118 | 119 | - name: Build 120 | run: | 121 | yarn gen:abi 122 | yarn build 123 | 124 | - name: Run unit tests 125 | run: CI=true yarn test:unit 126 | 127 | test-integration: 128 | name: Test (Integration) on Node.js v${{ matrix.node-version }}${{ matrix.orbit-test == '1' && ' with L3' || '' }}${{ matrix.decimals == '16' && ' with custom gas token (16 decimals)' || matrix.decimals == '20' && ' with custom gas token (20 decimals)' || matrix.decimals == '18' && ' with custom gas token (18 decimals)' || '' }} 129 | runs-on: ubuntu-latest 130 | strategy: 131 | fail-fast: false # runs all tests to completion even if one fails 132 | matrix: 133 | include: 134 | - orbit-test: '0' 135 | node-version: 18 136 | - orbit-test: '0' 137 | node-version: 20 138 | 139 | - orbit-test: '1' 140 | node-version: 18 141 | - orbit-test: '1' 142 | node-version: 20 143 | 144 | - orbit-test: '1' 145 | decimals: 16 146 | node-version: 18 147 | - orbit-test: '1' 148 | decimals: 16 149 | node-version: 20 150 | 151 | - orbit-test: '1' 152 | decimals: 18 153 | node-version: 18 154 | - orbit-test: '1' 155 | decimals: 18 156 | node-version: 20 157 | 158 | - orbit-test: '1' 159 | decimals: 20 160 | node-version: 18 161 | - orbit-test: '1' 162 | decimals: 20 163 | node-version: 20 164 | 165 | needs: install 166 | env: 167 | ORBIT_TEST: ${{ matrix.orbit-test }} 168 | DECIMALS: ${{ matrix.decimals || '18' }} 169 | steps: 170 | - name: Checkout 171 | uses: actions/checkout@v4 172 | 173 | - name: Set up Node.js 174 | uses: actions/setup-node@v4 175 | with: 176 | node-version: ${{ matrix.node-version }} 177 | 178 | - name: Install node_modules 179 | uses: OffchainLabs/actions/node-modules/install@main 180 | with: 181 | cache-key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}-monorepo 182 | 183 | - name: Set up the local node 184 | uses: OffchainLabs/actions/run-nitro-test-node@main 185 | with: 186 | nitro-testnode-ref: f5a54d679733c65b81d5106488feb957ec579a46 187 | l3-node: ${{ matrix.orbit-test == '1' }} 188 | args: ${{ matrix.decimals == 16 && '--l3-fee-token --l3-fee-token-decimals 16' || matrix.decimals == 20 && '--l3-fee-token --l3-fee-token-decimals 20' || matrix.decimals == 18 && '--l3-fee-token' || '' }} 189 | 190 | - name: Copy .env 191 | run: cp ./.env-sample ./.env 192 | 193 | - name: Build 194 | run: | 195 | yarn gen:abi 196 | yarn build 197 | 198 | - name: Generate network file 199 | run: yarn gen:network 200 | 201 | - name: Run integration tests 202 | run: CI=true yarn test:integration 203 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom and then Node, Go, Cpp, Python 2 | .DS_Store 3 | .idea 4 | .vscode 5 | !.vscode/launch.json 6 | *.ao 7 | compiled.json 8 | ci 9 | debug 10 | build 11 | build-fuzz 12 | release 13 | xcode 14 | validator-states 15 | docker-compose.yml 16 | packages/arb-avm-cpp/tests/config.hpp 17 | packages/arb-avm-cpp/rocksdb 18 | packages/arb-avm-cpp/tsan 19 | packages/arb-avm-cpp/cmachine/flags.go.in2 20 | packages/arb-avm-cpp/cmachine/flags.go 21 | bridge_eth_addresses.json 22 | packages/arb-bridge-eth/rollup-* 23 | keys.json 24 | /workspace 25 | arbitrum_flat.sol 26 | vconan 27 | gasReporterOutput.json 28 | rollups 29 | /packages/arb-node-core/arb-validator 30 | /packages/arb-node-core/proof_test_server 31 | /packages/arb-rpc-node/arb-node 32 | /packages/arb-rpc-node/cmd/arb-dev-node/arb-dev-node 33 | /packages/arb-rpc-node/cmd/arb-dev-node/arbitrum* 34 | cache 35 | compile_commands.json 36 | .envrc 37 | localNetwork.json 38 | 39 | # Logs 40 | logs 41 | *.log 42 | npm-debug.log* 43 | yarn-debug.log* 44 | yarn-error.log* 45 | lerna-debug.log* 46 | 47 | # Diagnostic reports (https://nodejs.org/api/report.html) 48 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 49 | junit.xml 50 | 51 | # Runtime data 52 | pids 53 | *.pid 54 | *.seed 55 | *.pid.lock 56 | 57 | # Directory for instrumented libs generated by jscoverage/JSCover 58 | lib-cov 59 | 60 | # Coverage directory used by tools like istanbul 61 | coverage 62 | *.lcov 63 | 64 | # nyc test coverage 65 | .nyc_output 66 | 67 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 68 | .grunt 69 | 70 | # Bower dependency directory (https://bower.io/) 71 | bower_components 72 | 73 | # node-waf configuration 74 | .lock-wscript 75 | 76 | # Compiled binary addons (https://nodejs.org/api/addons.html) 77 | build/Release 78 | 79 | # Dependency directories 80 | node_modules/ 81 | jspm_packages/ 82 | 83 | # TypeScript v1 declaration files 84 | typings/ 85 | 86 | # TypeScript cache 87 | *.tsbuildinfo 88 | 89 | # Optional npm cache directory 90 | .npm 91 | 92 | # Optional eslint cache 93 | .eslintcache 94 | 95 | # Optional REPL history 96 | .node_repl_history 97 | 98 | # Output of 'npm pack' 99 | *.tgz 100 | 101 | # Yarn Integrity file 102 | .yarn-integrity 103 | 104 | # dotenv environment variables file 105 | .env 106 | .env.test 107 | 108 | # parcel-bundler cache (https://parceljs.org/) 109 | .cache 110 | 111 | # next.js build output 112 | .next 113 | 114 | # nuxt.js build output 115 | .nuxt 116 | 117 | # vuepress build output 118 | .vuepress/dist 119 | 120 | # Serverless directories 121 | .serverless/ 122 | 123 | # FuseBox cache 124 | .fusebox/ 125 | 126 | # DynamoDB Local files 127 | .dynamodb/ 128 | 129 | # Binaries for programs and plugins 130 | *.exe 131 | *.exe~ 132 | *.dll 133 | *.so 134 | *.dylib 135 | 136 | # Test binary, built with `go test -c` 137 | *.test 138 | 139 | # Output of the go coverage tool, specifically when used with LiteIDE 140 | *.out 141 | 142 | # Dependency directories (remove the comment below to include it) 143 | # vendor/ 144 | 145 | # Prerequisites 146 | *.d 147 | 148 | # Compiled Object files 149 | *.slo 150 | *.lo 151 | *.o 152 | *.obj 153 | 154 | # Precompiled Headers 155 | *.gch 156 | *.pch 157 | 158 | # Compiled Dynamic libraries 159 | *.so 160 | *.dylib 161 | *.dll 162 | 163 | # Fortran module files 164 | # *.mod # conflicts with go.mod 165 | *.smod 166 | 167 | # Compiled Static libraries 168 | *.lai 169 | *.la 170 | *.a 171 | *.lib 172 | 173 | # Executables 174 | *.exe 175 | *.out 176 | *.app 177 | 178 | # Byte-compiled / optimized / DLL files 179 | __pycache__/ 180 | *.py[cod] 181 | *$py.class 182 | 183 | # C extensions 184 | *.so 185 | 186 | # Distribution / packaging 187 | .Python 188 | build/ 189 | develop-eggs/ 190 | dist/ 191 | downloads/ 192 | eggs/ 193 | .eggs/ 194 | # lib/ # conflictes with arb-avm-cpp 195 | lib64/ 196 | parts/ 197 | sdist/ 198 | var/ 199 | wheels/ 200 | pip-wheel-metadata/ 201 | share/python-wheels/ 202 | *.egg-info/ 203 | .installed.cfg 204 | *.egg 205 | MANIFEST 206 | 207 | # PyInstaller 208 | # Usually these files are written by a python script from a template 209 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 210 | *.manifest 211 | *.spec 212 | 213 | # Installer logs 214 | pip-log.txt 215 | pip-delete-this-directory.txt 216 | 217 | # Unit test / coverage reports 218 | htmlcov/ 219 | .tox/ 220 | .nox/ 221 | .coverage 222 | .coverage.* 223 | .cache 224 | nosetests.xml 225 | coverage.xml 226 | *.cover 227 | .hypothesis/ 228 | .pytest_cache/ 229 | 230 | # Translations 231 | *.mo 232 | *.pot 233 | 234 | # Django stuff: 235 | *.log 236 | local_settings.py 237 | db.sqlite3 238 | db.sqlite3-journal 239 | 240 | # Flask stuff: 241 | instance/ 242 | .webassets-cache 243 | 244 | # Scrapy stuff: 245 | .scrapy 246 | 247 | # PyBuilder 248 | target/ 249 | 250 | # Jupyter Notebook 251 | .ipynb_checkpoints 252 | 253 | # IPython 254 | profile_default/ 255 | ipython_config.py 256 | 257 | # pyenv 258 | .python-version 259 | 260 | # pipenv 261 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 262 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 263 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 264 | # install all needed dependencies. 265 | #Pipfile.lock 266 | 267 | # celery beat schedule file 268 | celerybeat-schedule 269 | 270 | # SageMath parsed files 271 | *.sage.py 272 | 273 | # Environments 274 | .env 275 | .venv 276 | env/ 277 | venv/ 278 | ENV/ 279 | env.bak/ 280 | venv.bak/ 281 | 282 | # Spyder project settings 283 | .spyderproject 284 | .spyproject 285 | 286 | # Rope project settings 287 | .ropeproject 288 | 289 | # mkdocs documentation 290 | /site 291 | 292 | # mypy 293 | .mypy_cache/ 294 | .dmypy.json 295 | dmypy.json 296 | 297 | # Pyre type checker 298 | .pyre/ 299 | packages/arb-validator/cmd/rollupServer/rollupServer 300 | packages/arb-validator/cmd/evilRollupServer/evilRollupServer 301 | packages/arb-validator/cmd/evilRollupServer/evilRollupServer 302 | packages/arb-validator/cmd/evilRollupServer/evilRollupServer 303 | 304 | # geth setup 305 | packages/arb-bridge-eth/data/ 306 | packages/arb-bridge-eth/password.txt 307 | 308 | # GitHub CI 309 | packages/issues 310 | packages/**/issues 311 | 312 | json_data 313 | abi 314 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": ["ts"], 3 | "package": "./package.json", 4 | "require": "ts-node/register" 5 | } 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict = true 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | trailingComma: 'es5', 4 | singleQuote: true, 5 | printWidth: 80, 6 | tabWidth: 2, 7 | arrowParens: 'avoid', 8 | bracketSpacing: true, 9 | overrides: [ 10 | { 11 | files: '*.sol', 12 | options: { 13 | printWidth: 100, 14 | tabWidth: 4, 15 | useTabs: false, 16 | singleQuote: false, 17 | bracketSpacing: true, 18 | explicitTypes: 'always', 19 | }, 20 | }, 21 | ], 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Run Unit Tests", 8 | "runtimeExecutable": "yarn", 9 | "runtimeVersion": "18", 10 | "runtimeArgs": ["test:unit"], 11 | "skipFiles": ["/**"] 12 | }, 13 | { 14 | "type": "node", 15 | "request": "launch", 16 | "name": "Run Integration Tests", 17 | "runtimeExecutable": "yarn", 18 | "runtimeVersion": "18", 19 | "runtimeArgs": ["test:integration"], 20 | "skipFiles": ["/**"] 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arbitrum SDK 2 | 3 | [![npm version](https://badge.fury.io/js/%40arbitrum%2Fsdk.svg)](https://badge.fury.io/js/@arbitrum%2Fsdk.svg) 4 | [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 5 | 6 | A TypeScript library for client-side interactions with Arbitrum. The Arbitrum SDK provides essential helper functionality and direct access to underlying smart contract interfaces, enabling developers to build powerful applications on the Arbitrum network. 7 | 8 | > [!IMPORTANT] 9 | > 10 | > This is the code and documentation for `@arbitrum/sdk` v4. 11 | > 12 | > If you're looking for v3, check out [this branch](https://github.com/OffchainLabs/arbitrum-sdk/tree/v3). 13 | > 14 | > If you're looking to migrate from v3 to v4, check out [this guide](./docs/2-migrate.mdx). 15 | 16 | ## Table of Contents 17 | 18 | - [Arbitrum SDK](#arbitrum-sdk) 19 | - [Table of Contents](#table-of-contents) 20 | - [Overview](#overview) 21 | - [Installation](#installation) 22 | - [Key Features](#key-features) 23 | - [Bridging Assets](#bridging-assets) 24 | - [Cross-Chain Messages](#cross-chain-messages) 25 | - [Network Configuration](#network-configuration) 26 | - [Usage](#usage) 27 | - [Running Integration Tests](#running-integration-tests) 28 | - [Documentation](#documentation) 29 | - [License](#license) 30 | 31 | ## Overview 32 | 33 | Arbitrum SDK simplifies the process of interacting with Arbitrum chains, offering a robust set of tools for asset bridging and cross-chain messaging. 34 | 35 | ## Installation 36 | 37 | ```bash 38 | npm install @arbitrum/sdk 39 | 40 | # or 41 | 42 | yarn add @arbitrum/sdk 43 | ``` 44 | 45 | ## Key Features 46 | 47 | ### Bridging Assets 48 | 49 | Arbitrum SDK facilitates the bridging of assets between an Arbitrum chain and its parent chain. Currently supported asset bridgers: 50 | 51 | - `EthBridger`: For bridging ETH to and from an Arbitrum chain (L2 or L3) 52 | - `Erc20Bridger`: For bridging ERC-20 tokens to and from an Arbitrum chain (L2 or L3) 53 | - `EthL1L3Bridger`: For bridging ETH to an L3 directly from L1 54 | - `Erc20L1L3Bridger`: For bridging ERC-20 tokens to an L3 directly from L1 55 | 56 | ### Cross-Chain Messages 57 | 58 | Cross-chain communication is handled through `ParentToChildMessage` and `ChildToParentMessage` classes. These encapsulate the lifecycle of messages sent between chains, typically created from transaction receipts that initiate cross-chain messages. 59 | 60 | ### Network Configuration 61 | 62 | The SDK comes preconfigured for Arbitrum One, Arbitrum Nova and Arbitrum Sepolia. Custom Arbitrum networks can be registered using `registerCustomArbitrumNetwork`, which is required before utilizing other SDK features. 63 | 64 | ## Usage 65 | 66 | Here's a basic example of using the SDK to bridge ETH: 67 | 68 | ```ts 69 | import { ethers } from 'ethers' 70 | import { EthBridger, getArbitrumNetwork } from '@arbitrum/sdk' 71 | 72 | async function bridgeEth(parentSigner: ethers.Signer, childChainId: number) { 73 | const childNetwork = await getArbitrumNetwork(childChainId) 74 | const ethBridger = new EthBridger(childNetwork) 75 | 76 | const deposit = await ethBridger.deposit({ 77 | amount: ethers.utils.parseEther('0.1'), 78 | parentSigner, 79 | }) 80 | 81 | const txReceipt = await deposit.wait() 82 | console.log(`Deposit initiated: ${txReceipt.transactionHash}`) 83 | } 84 | ``` 85 | 86 | For more detailed usage examples and API references, please refer to the [Arbitrum SDK documentation](https://docs.arbitrum.io/sdk). 87 | 88 | ## Running Integration Tests 89 | 90 | 1. Set up a Nitro test node by following the instructions [here](https://docs.arbitrum.io/node-running/how-tos/local-dev-node). 91 | 2. Copy `.env.example` to `.env` and update relevant environment variables. 92 | 3. Generate the network configuration against your active Nitro test node: 93 | 94 | ```sh 95 | yarn gen:network 96 | ``` 97 | 98 | 4. Execute the integration tests: 99 | 100 | ```sh 101 | yarn test:integration 102 | ``` 103 | 104 | ## Documentation 105 | 106 | For comprehensive guides and API documentation, visit the [Arbitrum SDK Documentation](https://docs.arbitrum.io/sdk). 107 | 108 | ## License 109 | 110 | Arbitrum SDK is released under the [Apache 2.0 License](LICENSE). 111 | -------------------------------------------------------------------------------- /audit-ci.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://github.com/IBM/audit-ci/raw/main/docs/schema.json", 3 | "low": true, 4 | "allowlist": [ 5 | // Open Zepplin 6 | //////////// 7 | // https://github.com/advisories/GHSA-4g63-c64m-25w9 8 | // OpenZeppelin Contracts's SignatureChecker may revert on invalid EIP-1271 signers 9 | // We dont use EIP-1271 10 | "GHSA-4g63-c64m-25w9", 11 | // https://github.com/advisories/GHSA-qh9x-gcfh-pcrw 12 | // OpenZeppelin Contracts's ERC165Checker may revert instead of returning false 13 | // We don't use ERC165Checker 14 | "GHSA-qh9x-gcfh-pcrw", 15 | // https://github.com/advisories/GHSA-7grf-83vw-6f5x 16 | // OpenZeppelin Contracts ERC165Checker unbounded gas consumption 17 | // We don't use ERC165Checker 18 | "GHSA-7grf-83vw-6f5x", 19 | // https://github.com/advisories/GHSA-xrc4-737v-9q75 20 | // OpenZeppelin Contracts's GovernorVotesQuorumFraction updates to quorum may affect past defeated proposals 21 | // We don't use GovernorVotesQuorumFraction 22 | "GHSA-xrc4-737v-9q75", 23 | // https://github.com/advisories/GHSA-4h98-2769-gh6h 24 | // OpenZeppelin Contracts vulnerable to ECDSA signature malleability 25 | // We don’t use signatures for replay protection anywhere 26 | "GHSA-4h98-2769-gh6h", 27 | // https://github.com/advisories/GHSA-mx2q-35m2-x2rh 28 | // OpenZeppelin Contracts TransparentUpgradeableProxy clashing selector calls may not be delegated 29 | // from: @arbitrum/nitro-contracts>@openzeppelin/contracts-upgradeable 30 | // from: arb-bridge-peripherals>@openzeppelin/contracts-upgradeable 31 | // from: arb-bridge-peripherals>arb-bridge-eth>@openzeppelin/contracts-upgradeable 32 | // from: @arbitrum/nitro-contracts>@openzeppelin/contracts 33 | // from: arb-bridge-peripherals>@openzeppelin/contracts 34 | // from: arb-bridge-peripherals>arb-bridge-eth>@openzeppelin/contracts 35 | // Clashing selector between proxy and implementation can only be caused deliberately 36 | "GHSA-mx2q-35m2-x2rh", 37 | // https://github.com/advisories/GHSA-93hq-5wgc-jc82 38 | // GovernorCompatibilityBravo may trim proposal calldata 39 | // from: @arbitrum/nitro-contracts>@openzeppelin/contracts-upgradeable 40 | // from: @arbitrum/nitro-contracts>@openzeppelin/contracts 41 | // We don't use GovernorCompatibilityBravo 42 | "GHSA-93hq-5wgc-jc82", 43 | // https://github.com/advisories/GHSA-5h3x-9wvq-w4m2 44 | // OpenZeppelin Contracts's governor proposal creation may be blocked by frontrunning 45 | // from: @arbitrum/nitro-contracts>@openzeppelin/contracts-upgradeable 46 | // from: @arbitrum/nitro-contracts>@openzeppelin/contracts 47 | // We don't use Governor or GovernorCompatibilityBravo 48 | "GHSA-5h3x-9wvq-w4m2", 49 | // https://github.com/advisories/GHSA-g4vp-m682-qqmp 50 | // OpenZeppelin Contracts vulnerable to Improper Escaping of Output 51 | // from: @arbitrum/nitro-contracts>@openzeppelin/contracts-upgradeable 52 | // from @arbitrum/nitro-contracts>@openzeppelin/contracts 53 | // We don't use ERC2771Context 54 | "GHSA-g4vp-m682-qqmp", 55 | // https://github.com/advisories/GHSA-wprv-93r4-jj2p 56 | // OpenZeppelin Contracts using MerkleProof multiproofs may allow proving arbitrary leaves for specific trees 57 | // we don't use oz/merkle-trees anywhere 58 | // from @arbitrum/nitro-contracts>@offchainlabs/upgrade-executor>@openzeppelin/contracts-upgradeable 59 | // from @arbitrum/nitro-contracts>@offchainlabs/upgrade-executor>@openzeppelin/contracts 60 | "GHSA-wprv-93r4-jj2p", 61 | // https://github.com/advisories/GHSA-699g-q6qh-q4v8 62 | // OpenZeppelin Contracts and Contracts Upgradeable duplicated execution of subcalls in v4.9.4 63 | // from: @offchainlabs/l1-l3-teleport-contracts>@openzeppelin/contracts 64 | "GHSA-699g-q6qh-q4v8", 65 | // https://github.com/advisories/GHSA-9vx6-7xxf-x967 66 | // OpenZeppelin Contracts base64 encoding may read from potentially dirty memory 67 | // we don't use the base64 functions 68 | // from: @arbitrum/nitro-contracts>@openzeppelin/contracts-upgradeable 69 | // from: @arbitrum/token-bridge-contracts>@openzeppelin/contracts-upgradeable 70 | // from: @arbitrum/nitro-contracts>@openzeppelin/contracts 71 | // from: @arbitrum/token-bridge-contracts>@openzeppelin/contracts 72 | "GHSA-9vx6-7xxf-x967", 73 | // https://github.com/advisories/GHSA-xq7p-g2vc-g82p 74 | // Homograph attack allows Unicode lookalike characters to bypass validation. 75 | // we don't use them in this repo, they are nested dependencies 76 | // from: @arbitrum/token-bridge-contracts>@openzeppelin/upgrades-core>ethereumjs-util>ethereum-cryptography>bs58check>bs58>base-x 77 | // from: @offchainlabs/l1-l3-teleport-contracts>@arbitrum/token-bridge-contracts>@openzeppelin/upgrades-core>ethereumjs-util>ethereum-cryptography>bs58check>bs58>base-x 78 | "GHSA-xq7p-g2vc-g82p", 79 | // https://github.com/advisories/GHSA-cxrh-j4jr-qwg3 80 | // undici Denial of Service attack via bad certificate data 81 | // we only use hardhat in a test and we don't use undici in the sdk 82 | // from: hardhat>undici 83 | "GHSA-cxrh-j4jr-qwg3" 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /docs/1-introduction.mdx: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | The Arbitrum SDK is a powerful TypeScript library that streamlines interactions with Arbitrum networks. It offers robust tools for bridging tokens and passing messages between networks through an intuitive interface to the underlying smart contracts. 4 | 5 | **Key Features** 6 | 7 | - Token Bridging: Effortlessly bridge tokens between Ethereum and Arbitrum. 8 | - Message Passing: Seamlessly pass messages across networks. 9 | - Contracts Interface: Leverage a strongly-typed interface for interacting with smart contracts. 10 | 11 | Below is an overview of the Arbitrum SDK functionality. See the [tutorials](https://github.com/OffchainLabs/arbitrum-tutorials) for more examples. 12 | 13 | ## Getting Started 14 | 15 | Install dependencies 16 | 17 | import Tabs from '@theme/Tabs'; 18 | import TabItem from '@theme/TabItem'; 19 | 20 | 21 | 22 | 23 | ```sh 24 | npm install @arbitrum/sdk 25 | ``` 26 | 27 | 28 | 29 | 30 | ```sh 31 | yarn add @arbitrum/sdk 32 | ``` 33 | 34 | 35 | 36 | 37 | ```sh 38 | pnpm install @arbitrum/sdk 39 | ``` 40 | 41 | 42 | 43 | 44 | ## Using the Arbitrum SDK 45 | 46 | ### Bridging assets 47 | 48 | Arbitrum SDK can be used to bridge assets to or from an Arbitrum Network. The following asset bridgers are currently available: 49 | 50 | - [`EthBridger`](./reference/assetBridger/ethBridger.md) 51 | - [`Erc20Bridger`](./reference/assetBridger/erc20Bridger.md) 52 | 53 | All asset bridgers have the following methods which accept different parameters depending on the asset bridger type: 54 | 55 | - [`deposit`](./reference/assetBridger/assetBridger.md#deposit) - moves assets from the Parent to the Child chain 56 | - [`withdraw`](./reference/assetBridger/assetBridger.md#withdraw) - moves assets from the Child to the Parent chain 57 | 58 | #### Example ETH Deposit to Arbitrum One 59 | 60 | ```ts 61 | import { getArbitrumNetwork, EthBridger } from '@arbitrum/sdk' 62 | 63 | // get the `@arbitrum/sdk` ArbitrumNetwork object using the chain id of the Arbitrum One chain 64 | const childNetwork = await getArbitrumNetwork(42161) 65 | const ethBridger = new EthBridger(childNetwork) 66 | 67 | const ethDepositTxResponse = await ethBridger.deposit({ 68 | amount: utils.parseEther('23'), 69 | parentSigner, // an ethers v5 signer connected to mainnet ethereum 70 | childProvider, // an ethers v5 provider connected to Arbitrum One 71 | }) 72 | 73 | const ethDepositTxReceipt = await ethDepositTxResponse.wait() 74 | ``` 75 | 76 | [Learn more in the Eth Deposit tutorial](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/eth-deposit) 77 | 78 | #### Example ETH Withdrawal from Arbitrum One 79 | 80 | ```ts 81 | import { getArbitrumNetwork, EthBridger } from '@arbitrum/sdk' 82 | 83 | // get the `@arbitrum/sdk` ArbitrumNetwork object using the chain id of the Arbitrum One chain 84 | const childNetwork = await getArbitrumNetwork(42161) 85 | const ethBridger = new EthBridger(childNetwork) 86 | 87 | const withdrawTx = await ethBridger.withdraw({ 88 | amount: utils.parseEther('23'), 89 | childSigner, // an ethers v5 signer connected to Arbitrum One 90 | destinationAddress: childWallet.address, 91 | }) 92 | const withdrawRec = await withdrawTx.wait() 93 | ``` 94 | 95 | [Learn more in the Eth Withdraw tutorial](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/eth-withdraw) 96 | 97 | ### Networks 98 | 99 | Arbitrum SDK comes pre-configured for Mainnet and Sepolia, and their Arbitrum counterparts. Any other networks that are not pre-configured **must** be registered before being used. 100 | 101 | #### Configuring Network 102 | 103 | To interact with a custom [`ArbitrumNetwork`](./reference/dataEntities/networks), you can register it using the [`registerCustomArbitrumNetwork`](./reference/dataEntities/networks.md#registercustomarbitrumnetwork) function. 104 | 105 | ```ts 106 | import { registerCustomArbitrumNetwork } from '@arbitrum/sdk' 107 | 108 | registerCustomArbitrumNetwork({ 109 | chainID: 123456, 110 | name: 'Custom Arbitrum Network', 111 | }) 112 | ``` 113 | 114 | ### Cross chain messages 115 | 116 | When assets are moved by the Parent and Child cross chain messages are sent. The lifecycles of these messages are encapsulated in the classes [`ParentToChildMessage`](./reference/message/ParentToChildMessage) and [`ChildToParentMessage`](./reference/message/ParentToChildMessage). These objects are commonly created from the receipts of transactions that send cross chain messages. A cross chain message will eventually result in a transaction being executed on the destination chain, and these message classes provide the ability to wait for that finalizing transaction to occur. 117 | 118 | #### Redeem a Parent-to-Child Message 119 | 120 | ```ts 121 | import { 122 | ParentTransactionReceipt, 123 | ParentToChildMessageStatus, 124 | } from '@arbitrum/sdk' 125 | 126 | const parentTxnReceipt = new ParentTransactionReceipt( 127 | txnReceipt // ethers-js TransactionReceipt of an ethereum tx that triggered a Parent-to-Child message (say depositing a token via a bridge) 128 | ) 129 | 130 | const parentToChildMessage = ( 131 | await parentTxnReceipt.getParentToChildMessages( 132 | childSigner // connected ethers-js Wallet 133 | ) 134 | )[0] 135 | 136 | const res = await parentToChildMessage.waitForStatus() 137 | 138 | if (res.status === ParentToChildMessageStatus.Child) { 139 | // Message wasn't auto-redeemed; redeem it now: 140 | const response = await parentToChildMessage.redeem() 141 | const receipt = await response.wait() 142 | } else if (res.status === ParentToChildMessageStatus.REDEEMED) { 143 | // Message successfully redeemed 144 | } 145 | ``` 146 | 147 | [Learn more in the Redeem Failed Retryable Tickets tutorial](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/redeem-failed-retryable) 148 | 149 | ### Inbox Tools 150 | 151 | As part of normal operation, the Arbitrum sequencer will send messages into the rollup chain. However, if the sequencer is unavailable and not posting batches, the inbox tools can be used to force the inclusion of transactions into the Arbitrum network. 152 | 153 | Here's how you can use the inbox tools to withdraw ether from Arbitrum One without waiting for the sequencer: 154 | 155 | ```ts 156 | const childNetwork = await getArbitrumNetwork(await childWallet.getChainId()) 157 | 158 | const inboxSdk = new InboxTools(parentWallet, childNetwork) 159 | const arbSys = ArbSys__factory.connect(ARB_SYS_ADDRESS, childProvider) 160 | const arbSysIface = arbSys.interface 161 | const childCalldata = arbSysIface.encodeFunctionData('withdrawEth', [ 162 | parentWallet.address, 163 | ]) 164 | 165 | const txChildRequest = { 166 | data: childCalldata, 167 | to: ARB_SYS_ADDRESS, 168 | value: 1, 169 | } 170 | 171 | const childSignedTx = await inboxSdk.signChildTx(txChildRequest, childWallet) 172 | const childTxhash = ethers.utils.parseTransaction(childSignedTx).hash 173 | const resultsParent = await inboxSdk.sendChildSignedTx(childSignedTx) 174 | 175 | const inboxRec = await resultsParent.wait() 176 | ``` 177 | 178 | [Learn more in the Delayed Inbox tutorial](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/delayedInbox-l2msg). 179 | 180 | ### Utils 181 | 182 | - [`EventFetcher`](./reference/utils/eventFetcher) - A utility to provide typing for the fetching of events 183 | - [`MultiCaller`](./reference/utils/multicall#multicaller) - A utility for executing multiple calls as part of a single RPC request. This can be useful for reducing round trips. 184 | - [`constants`](./reference/dataEntities/constants) - A list of useful Arbitrum related constants 185 | 186 | ## Development 187 | 188 | ### Run Integration tests 189 | 190 | 1. Copy the `.env-sample` file to `.env` and update the values with your own. 191 | 1. First, make sure you have a [Nitro test node](https://github.com/Offchainlabs/nitro-testnode) running. Follow the instructions [here](https://docs.arbitrum.io/node-running/how-tos/local-dev-node). 192 | 1. After the node has started up (that could take up to 20-30 mins), run `yarn gen:network`. 193 | 1. Once done, finally run `yarn test:integration` to run the integration tests. 194 | 195 | Defaults to `Arbitrum Sepolia`, for custom network use `--network` flag. 196 | 197 | `Arbitrum Sepolia` expects env var `ARB_KEY` to be prefunded with at least 0.02 ETH, and env var `INFURA_KEY` to be set. 198 | (see `integration_test/config.ts`) 199 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import '@nomiclabs/hardhat-ethers' 2 | import dotenv from 'dotenv' 3 | dotenv.config() 4 | 5 | const config = { 6 | defaultNetwork: 'hardhat', 7 | paths: { 8 | artifacts: 'build/contracts', 9 | }, 10 | solidity: { 11 | compilers: [ 12 | { 13 | version: '0.6.11', 14 | settings: { 15 | optimizer: { 16 | enabled: true, 17 | runs: 100, 18 | }, 19 | }, 20 | }, 21 | { 22 | version: '0.8.7', 23 | settings: { 24 | optimizer: { 25 | enabled: true, 26 | runs: 100, 27 | }, 28 | }, 29 | }, 30 | ], 31 | }, 32 | networks: { 33 | hardhat: { 34 | chainId: 1337, 35 | throwOnTransactionFailures: true, 36 | allowUnlimitedContractSize: true, 37 | accounts: { 38 | accountsBalance: '1000000000000000000000000000', 39 | }, 40 | blockGasLimit: 200000000, 41 | // mining: { 42 | // auto: false, 43 | // interval: 1000, 44 | // }, 45 | forking: { 46 | url: 'https://mainnet.infura.io/v3/' + process.env['INFURA_KEY'], 47 | enabled: process.env['SHOULD_FORK'] === '1', 48 | }, 49 | }, 50 | }, 51 | spdxLicenseIdentifier: { 52 | overwrite: false, 53 | runOnCompile: true, 54 | }, 55 | namedAccounts: { 56 | deployer: { 57 | default: 0, 58 | }, 59 | }, 60 | mocha: { 61 | timeout: 30000000, 62 | bail: true, 63 | }, 64 | } 65 | 66 | module.exports = config 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "audit:ci": "audit-ci --config ./audit-ci.jsonc", 5 | "build": "yarn workspace @arbitrum/sdk build", 6 | "lint": "yarn workspaces run lint", 7 | "format": "yarn workspaces run format", 8 | "test:unit": "yarn workspaces run test:unit", 9 | "test:integration": "yarn workspace @arbitrum/sdk test:integration", 10 | "gen:abi": "yarn workspace @arbitrum/sdk gen:abi", 11 | "gen:network": "yarn workspace @arbitrum/sdk gen:network" 12 | }, 13 | "workspaces": { 14 | "packages": [ 15 | "packages/*" 16 | ] 17 | }, 18 | "devDependencies": { 19 | "@arbitrum/nitro-contracts": "^1.1.1", 20 | "@arbitrum/token-bridge-contracts": "^1.2.1", 21 | "@nomiclabs/hardhat-ethers": "^2.0.4", 22 | "@offchainlabs/l1-l3-teleport-contracts": "1.0.1", 23 | "@typechain/ethers-v5": "9.0.0", 24 | "@types/chai": "^4.2.11", 25 | "@types/mocha": "^9.0.0", 26 | "@types/prompts": "^2.0.14", 27 | "@types/yargs": "^17.0.9", 28 | "@typescript-eslint/eslint-plugin": "^5.14.0", 29 | "@typescript-eslint/eslint-plugin-tslint": "^5.27.1", 30 | "@typescript-eslint/parser": "^5.14.0", 31 | "audit-ci": "^6.6.1", 32 | "axios": "^1.8.4", 33 | "chai": "^4.2.0", 34 | "chalk": "^4.1.0", 35 | "dotenv": "^10.0.0", 36 | "eslint": "^7.32.0", 37 | "eslint-config-prettier": "^8.3.0", 38 | "eslint-plugin-mocha": "^9.0.0", 39 | "eslint-plugin-prettier": "^4.0.0", 40 | "ethers": "^5.0.0", 41 | "hardhat": "^2.24.0", 42 | "mocha": "^9.2.1", 43 | "nyc": "^15.1.0", 44 | "prettier": "^2.3.2", 45 | "prettier-plugin-solidity": "^1.0.0-beta.17", 46 | "prompts": "^2.4.2", 47 | "ts-mockito": "^2.6.1", 48 | "ts-node": "^10.2.1", 49 | "tslint": "^6.1.3", 50 | "typechain": "7.0.0", 51 | "typescript": "^5.7.2", 52 | "viem": "^2.0.0", 53 | "yargs": "^17.3.1" 54 | }, 55 | "resolutions": { 56 | "lodash.pick": "https://github.com/lodash/lodash/archive/refs/tags/4.17.21.tar.gz", 57 | "**/@ethersproject/providers/ws": "7.5.10", 58 | "**/puppeteer/ws": "8.17.1", 59 | "**/puppeteer/tar-fs": "2.1.2", 60 | "**/hardhat/ws": "7.5.10", 61 | "**/hardhat/@sentry/node/cookie": "0.7.0", 62 | "**/follow-redirects": "1.15.6", 63 | "**/micromatch": "4.0.8", 64 | "**/elliptic": "6.6.1", 65 | "**/secp256k1": "4.0.4", 66 | "**/nanoid": "3.3.8", 67 | "**/serialize-javascript": "6.0.2", 68 | "**/patch-package/cross-spawn": "6.0.6", 69 | "**/eslint/cross-spawn": "7.0.5", 70 | "**/foreground-child/cross-spawn": "7.0.5", 71 | "**/istanbul-lib-processinfo/cross-spawn": "7.0.5", 72 | "**/istanbul-lib-instrument/@babel/core/@babel/helpers": "7.26.10", 73 | "**/hardhat/undici": "5.28.5", 74 | "**/chokidar/braces": "3.0.3", 75 | "**/sol2uml/axios": "0.30.0" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/ethers-viem-compat/.eslintignore: -------------------------------------------------------------------------------- 1 | dist/** 2 | node_modules/** 3 | coverage/** 4 | src/lib/abi 5 | docs/** 6 | -------------------------------------------------------------------------------- /packages/ethers-viem-compat/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": false, 3 | "extends": ["../../.eslintrc.js"], 4 | "parserOptions": { 5 | "files": ["src/**/*.ts", "src/**/*.js"] 6 | }, 7 | "ignorePatterns": ["dist/**/*", "node_modules/**/*"] 8 | } -------------------------------------------------------------------------------- /packages/ethers-viem-compat/.prettierignore: -------------------------------------------------------------------------------- 1 | build/** 2 | cache/** 3 | dist/** 4 | src/lib/abi/** 5 | .nyc_output 6 | -------------------------------------------------------------------------------- /packages/ethers-viem-compat/.prettierrc.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('../../.prettierrc.js') 2 | 3 | module.exports = { 4 | ...baseConfig, 5 | } 6 | -------------------------------------------------------------------------------- /packages/ethers-viem-compat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@offchainlabs/ethers-viem-compat", 3 | "version": "0.0.1", 4 | "description": "Typescript library for ethers compatibility with viem", 5 | "author": "Offchain Labs, Inc.", 6 | "license": "Apache-2.0", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/offchainlabs/arbitrum-sdk.git" 12 | }, 13 | "engines": { 14 | "node": ">=v11", 15 | "npm": "please-use-yarn", 16 | "yarn": ">= 1.0.0" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/offchainlabs/arbitrum-sdk/issues" 20 | }, 21 | "homepage": "https://offchainlabs.com", 22 | "files": [ 23 | "dist/**/*" 24 | ], 25 | "scripts": { 26 | "build": "rm -rf dist && tsc -p tsconfig.json", 27 | "test:unit": "mocha -r ts-node/register 'tests/**/*.test.ts'", 28 | "lint": "eslint .", 29 | "format": "prettier './**/*.{js,json,md,ts,yml}' '!./src/lib/abi' --write && yarn run lint --fix" 30 | }, 31 | "peerDependencies": { 32 | "ethers": "^5.0.0", 33 | "viem": "^2.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/ethers-viem-compat/src/compatibility.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Log as EthersLog, 3 | TransactionReceipt as EthersTransactionReceipt, 4 | } from '@ethersproject/abstract-provider' 5 | import { BigNumber, providers } from 'ethers' 6 | import { 7 | PublicClient, 8 | Log as ViemLog, 9 | TransactionReceipt as ViemTransactionReceipt, 10 | } from 'viem' 11 | 12 | interface HttpTransportConfig { 13 | url: string 14 | } 15 | 16 | // based on https://wagmi.sh/react/ethers-adapters#reference-implementation 17 | export function publicClientToProvider(publicClient: PublicClient) { 18 | const { chain } = publicClient 19 | 20 | if (typeof chain === 'undefined') { 21 | throw new Error(`[publicClientToProvider] "chain" is undefined`) 22 | } 23 | 24 | const network = { 25 | chainId: chain.id, 26 | name: chain.name, 27 | ensAddress: chain.contracts?.ensRegistry?.address, 28 | } 29 | 30 | const transport = publicClient.transport as unknown as HttpTransportConfig 31 | const url = transport.url ?? chain.rpcUrls.default.http[0] 32 | 33 | return new providers.StaticJsonRpcProvider(url, network) 34 | } 35 | 36 | function viemLogToEthersLog(log: ViemLog): EthersLog { 37 | return { 38 | blockNumber: Number(log.blockNumber), 39 | blockHash: log.blockHash!, 40 | transactionIndex: log.transactionIndex!, 41 | removed: log.removed, 42 | address: log.address, 43 | data: log.data, 44 | topics: log.topics, 45 | transactionHash: log.transactionHash!, 46 | logIndex: log.logIndex!, 47 | } 48 | } 49 | 50 | export function viemTransactionReceiptToEthersTransactionReceipt( 51 | receipt: ViemTransactionReceipt 52 | ): EthersTransactionReceipt { 53 | return { 54 | to: receipt.to!, 55 | from: receipt.from!, 56 | contractAddress: receipt.contractAddress!, 57 | transactionIndex: receipt.transactionIndex, 58 | gasUsed: BigNumber.from(receipt.gasUsed), 59 | logsBloom: receipt.logsBloom, 60 | blockHash: receipt.blockHash, 61 | transactionHash: receipt.transactionHash, 62 | logs: receipt.logs.map(log => viemLogToEthersLog(log)), 63 | blockNumber: Number(receipt.blockNumber), 64 | // todo: if we need this we can add it later 65 | confirmations: -1, 66 | cumulativeGasUsed: BigNumber.from(receipt.cumulativeGasUsed), 67 | effectiveGasPrice: BigNumber.from(receipt.effectiveGasPrice), 68 | // all transactions that we care about are well past byzantium 69 | byzantium: true, 70 | type: Number(receipt.type), 71 | status: receipt.status === 'success' ? 1 : 0, 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/ethers-viem-compat/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './compatibility' 2 | -------------------------------------------------------------------------------- /packages/ethers-viem-compat/tests/compatibility.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { BigNumber, providers } from 'ethers' 3 | import { createPublicClient, defineChain, http, TransactionReceipt } from 'viem' 4 | import { arbitrumSepolia, mainnet } from 'viem/chains' 5 | 6 | import { 7 | publicClientToProvider, 8 | viemTransactionReceiptToEthersTransactionReceipt, 9 | } from '../src/compatibility' 10 | 11 | const testChain = defineChain({ 12 | ...mainnet, 13 | rpcUrls: { 14 | default: { 15 | http: ['https://example.com'], 16 | }, 17 | public: { 18 | http: ['https://example.com'], 19 | }, 20 | }, 21 | }) 22 | 23 | describe('viem compatibility', () => { 24 | describe('publicClientToProvider', () => { 25 | it('converts a public client to a provider', () => { 26 | const transport = http('https://example.com') 27 | const publicClient = createPublicClient({ 28 | chain: testChain, 29 | transport, 30 | }) 31 | 32 | const provider = publicClientToProvider(publicClient) 33 | expect(provider).to.be.instanceOf(providers.StaticJsonRpcProvider) 34 | expect(provider.network.chainId).to.equal(testChain.id) 35 | expect(provider.network.name).to.equal(testChain.name) 36 | expect(provider.connection.url).to.equal('https://example.com') 37 | }) 38 | 39 | it('successfully converts PublicClient to Provider', () => { 40 | const publicClient = createPublicClient({ 41 | chain: arbitrumSepolia, 42 | transport: http(), 43 | }) 44 | 45 | const provider = publicClientToProvider(publicClient) 46 | 47 | expect(provider.network.chainId).to.equal(publicClient.chain!.id) 48 | expect(provider.network.name).to.equal(publicClient.chain!.name) 49 | expect(provider.connection.url).to.equal( 50 | 'https://sepolia-rollup.arbitrum.io/rpc' 51 | ) 52 | }) 53 | 54 | it('successfully converts PublicClient to Provider (custom Transport)', () => { 55 | const publicClient = createPublicClient({ 56 | chain: arbitrumSepolia, 57 | transport: http('https://arbitrum-sepolia.gateway.tenderly.co'), 58 | }) 59 | 60 | const provider = publicClientToProvider(publicClient) 61 | 62 | expect(provider.network.chainId).to.equal(publicClient.chain!.id) 63 | expect(provider.network.name).to.equal(publicClient.chain!.name) 64 | expect(provider.connection.url).to.equal( 65 | 'https://arbitrum-sepolia.gateway.tenderly.co' 66 | ) 67 | }) 68 | 69 | it('throws error when chain is undefined', () => { 70 | const transport = http('https://example.com') 71 | const publicClient = createPublicClient({ 72 | chain: undefined, 73 | transport, 74 | }) 75 | 76 | expect(() => publicClientToProvider(publicClient)).to.throw( 77 | '[publicClientToProvider] "chain" is undefined' 78 | ) 79 | }) 80 | }) 81 | 82 | describe('viemTransactionReceiptToEthersTransactionReceipt', () => { 83 | it('converts viem transaction receipt to ethers format', () => { 84 | const viemReceipt: TransactionReceipt = { 85 | to: '0x1234', 86 | from: '0x5678', 87 | contractAddress: '0xabcd', 88 | transactionIndex: 1, 89 | gasUsed: BigInt(21000), 90 | logsBloom: '0x', 91 | blockHash: '0xblock', 92 | transactionHash: '0xtx', 93 | logs: [ 94 | { 95 | address: '0xcontract', 96 | blockHash: '0xblock', 97 | blockNumber: BigInt(123), 98 | data: '0xdata', 99 | logIndex: 0, 100 | removed: false, 101 | transactionHash: '0xtx', 102 | transactionIndex: 1, 103 | topics: [], 104 | }, 105 | ], 106 | blockNumber: BigInt(123), 107 | cumulativeGasUsed: BigInt(42000), 108 | effectiveGasPrice: BigInt(2000000000), 109 | status: 'success', 110 | type: 'eip1559', 111 | } 112 | 113 | const ethersReceipt = 114 | viemTransactionReceiptToEthersTransactionReceipt(viemReceipt) 115 | 116 | expect(ethersReceipt.to).to.equal('0x1234') 117 | expect(ethersReceipt.from).to.equal('0x5678') 118 | expect(ethersReceipt.contractAddress).to.equal('0xabcd') 119 | expect(ethersReceipt.transactionIndex).to.equal(1) 120 | expect(ethersReceipt.gasUsed.eq(BigNumber.from(21000))).to.equal(true) 121 | expect(ethersReceipt.blockNumber).to.equal(123) 122 | expect(ethersReceipt.status).to.equal(1) 123 | expect(ethersReceipt.logs[0].address).to.equal('0xcontract') 124 | expect(ethersReceipt.byzantium).to.equal(true) 125 | }) 126 | 127 | it('handles failed transaction status', () => { 128 | const viemReceipt: TransactionReceipt = { 129 | to: '0x1234', 130 | from: '0x5678', 131 | contractAddress: '0xabcd', 132 | transactionIndex: 1, 133 | gasUsed: BigInt(21000), 134 | logsBloom: '0x', 135 | blockHash: '0xblock', 136 | transactionHash: '0xtx', 137 | logs: [], 138 | blockNumber: BigInt(123), 139 | cumulativeGasUsed: BigInt(42000), 140 | effectiveGasPrice: BigInt(2000000000), 141 | status: 'reverted', 142 | type: 'eip1559' as const, 143 | } 144 | 145 | const ethersReceipt = 146 | viemTransactionReceiptToEthersTransactionReceipt(viemReceipt) 147 | expect(ethersReceipt.status).to.equal(0) 148 | }) 149 | }) 150 | }) 151 | -------------------------------------------------------------------------------- /packages/ethers-viem-compat/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES2020", 5 | "rootDir": "./src", 6 | "outDir": "./dist", 7 | "skipLibCheck": true 8 | }, 9 | "include": ["src/**/*.ts", "src/**/*.d.ts"], 10 | "exclude": ["node_modules", "dist"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/sdk/.eslintignore: -------------------------------------------------------------------------------- 1 | dist/** 2 | node_modules/** 3 | coverage/** 4 | src/lib/abi 5 | docs/** 6 | -------------------------------------------------------------------------------- /packages/sdk/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": false, 3 | "extends": ["../../.eslintrc.js"], 4 | "parserOptions": { 5 | "files": ["src/**/*.ts", "src/**/*.js"] 6 | }, 7 | "ignorePatterns": ["dist/**/*", "node_modules/**/*"] 8 | } -------------------------------------------------------------------------------- /packages/sdk/.prettierignore: -------------------------------------------------------------------------------- 1 | build/** 2 | cache/** 3 | dist/** 4 | src/lib/abi/** 5 | .nyc_output 6 | -------------------------------------------------------------------------------- /packages/sdk/.prettierrc.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('../../.prettierrc.js') 2 | 3 | module.exports = { 4 | ...baseConfig, 5 | } 6 | -------------------------------------------------------------------------------- /packages/sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@arbitrum/sdk", 3 | "version": "4.0.4", 4 | "description": "Typescript library client-side interactions with Arbitrum", 5 | "author": "Offchain Labs, Inc.", 6 | "license": "Apache-2.0", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/offchainlabs/arbitrum-sdk.git" 12 | }, 13 | "engines": { 14 | "node": ">=v11", 15 | "npm": "please-use-yarn", 16 | "yarn": ">= 1.0.0" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/offchainlabs/arbitrum-sdk/issues" 20 | }, 21 | "homepage": "https://offchainlabs.com", 22 | "files": [ 23 | "dist/**/*" 24 | ], 25 | "scripts": { 26 | "audit:ci": "audit-ci --config ./audit-ci.jsonc", 27 | "prepare": "yarn run gen:abi", 28 | "gen:abi": "ts-node ./scripts/genAbi.ts", 29 | "gen:network": "ts-node ./scripts/genNetwork.ts", 30 | "prepublishOnly": "yarn build && yarn format", 31 | "preversion": "yarn lint", 32 | "prebuild": "yarn gen:abi", 33 | "build": "rm -rf dist && tsc -p tsconfig.json", 34 | "watch": "tsc --watch", 35 | "test": "mocha", 36 | "test:coverage": "nyc mocha", 37 | "test:fork": "SHOULD_FORK=1 hardhat test tests/fork/*.test.ts", 38 | "test:integration": "mocha tests/integration/ --timeout 30000000 --bail", 39 | "test:unit": "mocha --parallel tests/unit/ --timeout 30000 --bail", 40 | "test:ci": "nyc --reporter=lcovonly mocha --reporter xunit", 41 | "lint": "eslint .", 42 | "format": "prettier './**/*.{js,json,md,ts,yml}' '!./src/lib/abi' --write && yarn run lint --fix", 43 | "clean:compile": "ts-node scripts/cleanCompileContracts.ts", 44 | "checkRetryable": "ts-node scripts/checkRetryableStatus.ts", 45 | "redeemRetryable": "ts-node scripts/redeemRetryable.ts", 46 | "setStandard": "ts-node scripts/setStandardGateways.ts", 47 | "setCustom": "ts-node scripts/setArbCustomGateways.ts", 48 | "cancelRetryable": "ts-node scripts/cancelRetryable.ts", 49 | "bridgeStandardToken": "ts-node scripts/deployStandard.ts" 50 | }, 51 | "dependencies": { 52 | "@ethersproject/address": "^5.0.8", 53 | "@ethersproject/bignumber": "^5.1.1", 54 | "@ethersproject/bytes": "^5.0.8", 55 | "async-mutex": "^0.4.0", 56 | "ethers": "^5.1.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/sdk/scripts/genAbi.ts: -------------------------------------------------------------------------------- 1 | import { runTypeChain, glob } from 'typechain' 2 | import { execSync } from 'child_process' 3 | import { unlinkSync, rmSync } from 'fs' 4 | import * as path from 'path' 5 | 6 | const ABI_PATH = path.resolve(__dirname, '../src/lib/abi') 7 | 8 | const getPackagePath = (packageName: string): string => { 9 | const path = require.resolve(`${packageName}/package.json`) 10 | return path.substr(0, path.indexOf('package.json')) 11 | } 12 | 13 | async function main() { 14 | console.log('Removing previously generated ABIs.\n') 15 | rmSync(`${ABI_PATH}`, { recursive: true, force: true }) 16 | rmSync(`${ABI_PATH}/classic`, { recursive: true, force: true }) 17 | 18 | const cwd = process.cwd() 19 | 20 | const nitroPath = getPackagePath('@arbitrum/nitro-contracts') 21 | const tokenBridgePath = getPackagePath('@arbitrum/token-bridge-contracts') 22 | const teleporterPath = getPackagePath( 23 | '@offchainlabs/l1-l3-teleport-contracts' 24 | ) 25 | 26 | console.log('Compiling paths.') 27 | 28 | const npmExec = process.env['npm_execpath'] 29 | if (!npmExec || npmExec === '') 30 | throw new Error( 31 | 'No support for npm_execpath env variable in package manager' 32 | ) 33 | 34 | // TODO: use `HARDHAT_ARTIFACT_PATH` to write files to arbitrum sdk instead of the packages themselves. 35 | // this is currently broken since hardhat throws a weird error: 36 | // `Error HH702: Invalid artifact path [...] its correct case-sensitive path is...` 37 | // https://yarnpkg.com/advanced/rulebook#packages-should-never-write-inside-their-own-folder-outside-of-postinstall 38 | // instead of writing in postinstall in each of those packages, we should target a local folder in sdk's postinstall 39 | 40 | console.log('building @arbitrum/nitro-contracts') 41 | execSync(`${npmExec} run build`, { cwd: nitroPath }) 42 | 43 | console.log('building @arbitrum/token-bridge-contracts') 44 | execSync(`${npmExec} run build`, { cwd: tokenBridgePath }) 45 | 46 | console.log('building @offchainlabs/l1-l3-teleport-contracts') 47 | execSync(`${npmExec} run build`, { 48 | cwd: teleporterPath, 49 | }) 50 | 51 | console.log('Done compiling') 52 | 53 | const nitroFiles = glob(cwd, [ 54 | `${tokenBridgePath}/build/contracts/!(build-info)/**/+([a-zA-Z0-9_]).json`, 55 | `${nitroPath}/build/contracts/!(build-info)/**/+([a-zA-Z0-9_]).json`, 56 | `${teleporterPath}/build/contracts/!(build-info)/**/+([a-zA-Z0-9_]).json`, 57 | ]) 58 | 59 | // TODO: generate files into different subfolders (ie `/nitro/*`) to avoid overwrite of contracts with the same name 60 | await runTypeChain({ 61 | cwd, 62 | filesToProcess: nitroFiles, 63 | allFiles: nitroFiles, 64 | outDir: `${ABI_PATH}`, 65 | target: 'ethers-v5', 66 | }) 67 | 68 | const classicFiles = glob(cwd, [ 69 | // we have a hardcoded abi for the old outbox 70 | `./src/lib/dataEntities/Outbox.json`, 71 | ]) 72 | 73 | await runTypeChain({ 74 | cwd, 75 | filesToProcess: classicFiles, 76 | allFiles: classicFiles, 77 | outDir: `${ABI_PATH}/classic`, 78 | target: 'ethers-v5', 79 | }) 80 | 81 | // we delete the index file since it doesn't play well with tree shaking 82 | unlinkSync(`${ABI_PATH}/index.ts`) 83 | unlinkSync(`${ABI_PATH}/classic/index.ts`) 84 | 85 | console.log('Typechain generated') 86 | } 87 | 88 | main() 89 | .then(() => console.log('Done.')) 90 | .catch(console.error) 91 | -------------------------------------------------------------------------------- /packages/sdk/scripts/genNetwork.ts: -------------------------------------------------------------------------------- 1 | import { loadEnv } from '../src/lib/utils/env' 2 | import { execSync } from 'child_process' 3 | import * as fs from 'fs' 4 | 5 | import { IERC20Bridge__factory } from '../src/lib/abi/factories/IERC20Bridge__factory' 6 | import { ethers } from 'ethers' 7 | import { 8 | L2Network, 9 | ArbitrumNetwork, 10 | mapL2NetworkToArbitrumNetwork, 11 | } from '../src/lib/dataEntities/networks' 12 | 13 | loadEnv() 14 | 15 | const isTestingOrbitChains = process.env.ORBIT_TEST === '1' 16 | 17 | function getLocalNetworksFromContainer(which: 'l1l2' | 'l2l3'): any { 18 | const dockerNames = [ 19 | 'nitro_sequencer_1', 20 | 'nitro-sequencer-1', 21 | 'nitro-testnode-sequencer-1', 22 | 'nitro-testnode_sequencer_1', 23 | ] 24 | for (const dockerName of dockerNames) { 25 | try { 26 | return JSON.parse( 27 | execSync( 28 | `docker exec ${dockerName} cat /tokenbridge-data/${which}_network.json` 29 | ).toString() 30 | ) 31 | } catch { 32 | // empty on purpose 33 | } 34 | } 35 | throw new Error('nitro-testnode sequencer not found') 36 | } 37 | 38 | /** 39 | * the container's files are written by the token bridge deployment step of the test node, which runs a script in token-bridge-contracts. 40 | * once the script in token-bridge-contracts repo uses an sdk version with the same types and is updated to populate those fields, 41 | * we can remove this patchwork 42 | */ 43 | async function patchNetworks( 44 | l2Network: L2Network, 45 | l3Network: L2Network | undefined, 46 | l2Provider: ethers.providers.Provider | undefined 47 | ): Promise<{ 48 | patchedL2Network: ArbitrumNetwork 49 | patchedL3Network?: ArbitrumNetwork 50 | }> { 51 | const patchedL2Network = mapL2NetworkToArbitrumNetwork(l2Network) 52 | 53 | // native token for l3 54 | if (l3Network && l2Provider) { 55 | const patchedL3Network = mapL2NetworkToArbitrumNetwork(l3Network) 56 | 57 | try { 58 | patchedL3Network.nativeToken = await IERC20Bridge__factory.connect( 59 | l3Network.ethBridge.bridge, 60 | l2Provider 61 | ).nativeToken() 62 | } catch (e) { 63 | // l3 network doesn't have a native token 64 | } 65 | 66 | return { patchedL2Network, patchedL3Network } 67 | } 68 | 69 | return { patchedL2Network } 70 | } 71 | 72 | async function main() { 73 | fs.rmSync('localNetwork.json', { force: true }) 74 | 75 | let output = getLocalNetworksFromContainer('l1l2') 76 | 77 | if (isTestingOrbitChains) { 78 | // When running with L3 active, the container calls the L3 network L2 so we rename it here 79 | const { l2Network: l3Network } = getLocalNetworksFromContainer('l2l3') 80 | const { patchedL2Network, patchedL3Network } = await patchNetworks( 81 | output.l2Network, 82 | l3Network, 83 | new ethers.providers.JsonRpcProvider(process.env['ARB_URL']) 84 | ) 85 | 86 | output = { 87 | l2Network: patchedL2Network, 88 | l3Network: patchedL3Network, 89 | } 90 | } else { 91 | const { patchedL2Network } = await patchNetworks( 92 | output.l2Network, 93 | undefined, 94 | undefined 95 | ) 96 | 97 | output.l2Network = patchedL2Network 98 | } 99 | 100 | fs.writeFileSync('localNetwork.json', JSON.stringify(output, null, 2)) 101 | console.log('localnetwork.json updated') 102 | } 103 | 104 | main() 105 | .then(() => console.log('Done.')) 106 | .catch(console.error) 107 | -------------------------------------------------------------------------------- /packages/sdk/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2021, Offchain Labs, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /* eslint-env node */ 17 | 'use strict' 18 | 19 | export { 20 | EthL1L3Bridger, 21 | EthL1L3DepositStatus, 22 | EthL1L3DepositRequestParams, 23 | Erc20L1L3Bridger, 24 | Erc20L1L3DepositStatus, 25 | Erc20L1L3DepositRequestParams, 26 | Erc20L1L3DepositRequestRetryableOverrides, 27 | GetL1L3DepositStatusParams, 28 | } from './lib/assetBridger/l1l3Bridger' 29 | export { EthBridger } from './lib/assetBridger/ethBridger' 30 | export { 31 | Erc20Bridger, 32 | AdminErc20Bridger, 33 | } from './lib/assetBridger/erc20Bridger' 34 | export { 35 | ChildTransactionReceipt, 36 | ChildContractTransaction, 37 | } from './lib/message/ChildTransaction' 38 | export { 39 | ChildToParentMessage, 40 | ChildToParentMessageWriter, 41 | ChildToParentMessageReader, 42 | ChildToParentMessageReaderOrWriter, 43 | ChildToParentTransactionEvent, 44 | } from './lib/message/ChildToParentMessage' 45 | export { 46 | ParentEthDepositTransaction, 47 | ParentEthDepositTransactionReceipt, 48 | ParentContractCallTransaction, 49 | ParentContractCallTransactionReceipt, 50 | ParentContractTransaction, 51 | ParentTransactionReceipt, 52 | } from './lib/message/ParentTransaction' 53 | export { 54 | EthDepositMessage, 55 | EthDepositMessageStatus, 56 | EthDepositMessageWaitForStatusResult, 57 | ParentToChildMessage, 58 | ParentToChildMessageReader, 59 | ParentToChildMessageReaderClassic, 60 | ParentToChildMessageWriter, 61 | ParentToChildMessageStatus, 62 | ParentToChildMessageWaitForStatusResult, 63 | } from './lib/message/ParentToChildMessage' 64 | export { ParentToChildMessageGasEstimator } from './lib/message/ParentToChildMessageGasEstimator' 65 | export { argSerializerConstructor } from './lib/utils/byte_serialize_params' 66 | export { CallInput, MultiCaller } from './lib/utils/multicall' 67 | export { 68 | ArbitrumNetwork, 69 | getArbitrumNetwork, 70 | getArbitrumNetworks, 71 | ArbitrumNetworkInformationFromRollup, 72 | getArbitrumNetworkInformationFromRollup, 73 | getChildrenForNetwork, 74 | registerCustomArbitrumNetwork, 75 | // deprecated, but here for backwards compatibility 76 | L2Network, 77 | L2NetworkTokenBridge, 78 | mapL2NetworkToArbitrumNetwork, 79 | mapL2NetworkTokenBridgeToTokenBridge, 80 | } from './lib/dataEntities/networks' 81 | export { InboxTools } from './lib/inbox/inbox' 82 | export { EventFetcher } from './lib/utils/eventFetcher' 83 | export { ArbitrumProvider } from './lib/utils/arbProvider' 84 | export * as constants from './lib/dataEntities/constants' 85 | export { 86 | ChildToParentMessageStatus, 87 | RetryableMessageParams, 88 | } from './lib/dataEntities/message' 89 | export { 90 | RetryableData, 91 | RetryableDataTools, 92 | } from './lib/dataEntities/retryableData' 93 | export { EventArgs } from './lib/dataEntities/event' 94 | export { Address } from './lib/dataEntities/address' 95 | export { 96 | ParentToChildTransactionRequest, 97 | isParentToChildTransactionRequest, 98 | ChildToParentTransactionRequest, 99 | isChildToParentTransactionRequest, 100 | } from './lib/dataEntities/transactionRequest' 101 | export { 102 | scaleFrom18DecimalsToNativeTokenDecimals, 103 | scaleFromNativeTokenDecimalsTo18Decimals, 104 | } from './lib/utils/lib' 105 | -------------------------------------------------------------------------------- /packages/sdk/src/lib/assetBridger/assetBridger.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, Offchain Labs, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /* eslint-env node */ 17 | 'use strict' 18 | 19 | import { ParentContractTransaction } from '../message/ParentTransaction' 20 | import { ChildContractTransaction } from '../message/ChildTransaction' 21 | 22 | import { 23 | ArbitrumNetwork, 24 | isArbitrumNetworkNativeTokenEther, 25 | } from '../dataEntities/networks' 26 | import { 27 | SignerOrProvider, 28 | SignerProviderUtils, 29 | } from '../dataEntities/signerOrProvider' 30 | 31 | /** 32 | * Base for bridging assets from parent-to-child and back 33 | */ 34 | export abstract class AssetBridger { 35 | /** 36 | * In case of a chain that uses ETH as its native/gas token, this is either `undefined` or the zero address 37 | * 38 | * 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 39 | */ 40 | public readonly nativeToken?: string 41 | 42 | public constructor(public readonly childNetwork: ArbitrumNetwork) { 43 | this.nativeToken = childNetwork.nativeToken 44 | } 45 | 46 | /** 47 | * Check the signer/provider matches the parent network, throws if not 48 | * @param sop 49 | */ 50 | protected async checkParentNetwork(sop: SignerOrProvider): Promise { 51 | await SignerProviderUtils.checkNetworkMatches( 52 | sop, 53 | this.childNetwork.parentChainId 54 | ) 55 | } 56 | 57 | /** 58 | * Check the signer/provider matches the child network, throws if not 59 | * @param sop 60 | */ 61 | protected async checkChildNetwork(sop: SignerOrProvider): Promise { 62 | await SignerProviderUtils.checkNetworkMatches( 63 | sop, 64 | this.childNetwork.chainId 65 | ) 66 | } 67 | 68 | /** 69 | * Whether the chain uses ETH as its native/gas token 70 | * @returns {boolean} 71 | */ 72 | protected get nativeTokenIsEth() { 73 | return isArbitrumNetworkNativeTokenEther(this.childNetwork) 74 | } 75 | 76 | /** 77 | * Transfer assets from parent-to-child 78 | * @param params 79 | */ 80 | public abstract deposit( 81 | params: DepositParams 82 | ): Promise 83 | 84 | /** 85 | * Transfer assets from child-to-parent 86 | * @param params 87 | */ 88 | public abstract withdraw( 89 | params: WithdrawParams 90 | ): Promise 91 | } 92 | -------------------------------------------------------------------------------- /packages/sdk/src/lib/dataEntities/address.ts: -------------------------------------------------------------------------------- 1 | import { getAddress } from '@ethersproject/address' 2 | import { utils } from 'ethers' 3 | import { ADDRESS_ALIAS_OFFSET } from './constants' 4 | import { ArbSdkError } from './errors' 5 | 6 | /** 7 | * Ethereum/Arbitrum address class 8 | */ 9 | export class Address { 10 | private readonly ADDRESS_ALIAS_OFFSET_BIG_INT = BigInt(ADDRESS_ALIAS_OFFSET) 11 | private readonly ADDRESS_BIT_LENGTH = 160 12 | private readonly ADDRESS_NIBBLE_LENGTH = this.ADDRESS_BIT_LENGTH / 4 13 | 14 | /** 15 | * Ethereum/Arbitrum address class 16 | * @param value A valid Ethereum address. Doesn't need to be checksum cased. 17 | */ 18 | constructor(public readonly value: string) { 19 | if (!utils.isAddress(value)) 20 | throw new ArbSdkError(`'${value}' is not a valid address`) 21 | } 22 | 23 | private alias(address: string, forward: boolean) { 24 | // we use BigInts in here to allow for proper under/overflow behaviour 25 | // BigInt.asUintN calculates the correct positive modulus 26 | return getAddress( 27 | '0x' + 28 | BigInt.asUintN( 29 | this.ADDRESS_BIT_LENGTH, 30 | forward 31 | ? BigInt(address) + this.ADDRESS_ALIAS_OFFSET_BIG_INT 32 | : BigInt(address) - this.ADDRESS_ALIAS_OFFSET_BIG_INT 33 | ) 34 | .toString(16) 35 | .padStart(this.ADDRESS_NIBBLE_LENGTH, '0') 36 | ) 37 | } 38 | 39 | /** 40 | * Find the L2 alias of an L1 address 41 | * @returns 42 | */ 43 | public applyAlias(): Address { 44 | return new Address(this.alias(this.value, true)) 45 | } 46 | 47 | /** 48 | * Find the L1 alias of an L2 address 49 | * @returns 50 | */ 51 | public undoAlias(): Address { 52 | return new Address(this.alias(this.value, false)) 53 | } 54 | 55 | public equals(other: Address): boolean { 56 | return this.value.toLowerCase() === other.value.toLowerCase() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/sdk/src/lib/dataEntities/constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, Offchain Labs, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /* eslint-env node */ 17 | 'use strict' 18 | 19 | export const NODE_INTERFACE_ADDRESS = 20 | '0x00000000000000000000000000000000000000C8' 21 | 22 | export const ARB_SYS_ADDRESS = '0x0000000000000000000000000000000000000064' 23 | 24 | export const ARB_RETRYABLE_TX_ADDRESS = 25 | '0x000000000000000000000000000000000000006E' 26 | 27 | export const ARB_ADDRESS_TABLE_ADDRESS = 28 | '0x0000000000000000000000000000000000000066' 29 | 30 | export const ARB_OWNER_PUBLIC = '0x000000000000000000000000000000000000006B' 31 | 32 | export const ARB_GAS_INFO = '0x000000000000000000000000000000000000006C' 33 | 34 | export const ARB_STATISTICS = '0x000000000000000000000000000000000000006F' 35 | 36 | export const ARB_MINIMUM_BLOCK_TIME_IN_SECONDS = 0.25 37 | 38 | /** 39 | * The offset added to an L1 address to get the corresponding L2 address 40 | */ 41 | export const ADDRESS_ALIAS_OFFSET = '0x1111000000000000000000000000000000001111' 42 | 43 | /** 44 | * Address of the gateway a token will be assigned to if it is disabled 45 | */ 46 | export const DISABLED_GATEWAY = '0x0000000000000000000000000000000000000001' 47 | 48 | /** 49 | * If a custom token is enabled for arbitrum it will implement a function called 50 | * isArbitrumEnabled which returns this value. Intger: 0xa4b1 51 | */ 52 | export const CUSTOM_TOKEN_IS_ENABLED = 42161 53 | 54 | export const SEVEN_DAYS_IN_SECONDS = 7 * 24 * 60 * 60 55 | 56 | /** 57 | * How long to wait (in milliseconds) for a deposit to arrive before timing out a request. 58 | * 59 | * Finalisation on mainnet can be up to 2 epochs = 64 blocks. 60 | * We add 10 minutes for the system to create and redeem the ticket, plus some extra buffer of time. 61 | * 62 | * Total timeout: 30 minutes. 63 | */ 64 | export const DEFAULT_DEPOSIT_TIMEOUT = 30 * 60 * 1000 65 | 66 | /** 67 | * The L1 block at which Nitro was activated for Arbitrum One. 68 | * 69 | * @see https://etherscan.io/block/15447158 70 | */ 71 | export const ARB1_NITRO_GENESIS_L1_BLOCK = 15447158 72 | 73 | /** 74 | * The L2 block at which Nitro was activated for Arbitrum One. 75 | * 76 | * @see https://arbiscan.io/block/22207817 77 | */ 78 | export const ARB1_NITRO_GENESIS_L2_BLOCK = 22207817 79 | -------------------------------------------------------------------------------- /packages/sdk/src/lib/dataEntities/errors.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, Offchain Labs, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /* eslint-env node */ 17 | 'use strict' 18 | 19 | /** 20 | * Errors originating in Arbitrum SDK 21 | */ 22 | export class ArbSdkError extends Error { 23 | constructor(message: string, public readonly inner?: Error) { 24 | super(message) 25 | 26 | if (inner) { 27 | this.stack += '\nCaused By: ' + inner.stack 28 | } 29 | } 30 | } 31 | 32 | /** 33 | * Thrown when a signer does not have a connected provider 34 | */ 35 | export class MissingProviderArbSdkError extends ArbSdkError { 36 | constructor(signerName: string) { 37 | super( 38 | `${signerName} does not have a connected provider and one is required.` 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/sdk/src/lib/dataEntities/event.ts: -------------------------------------------------------------------------------- 1 | import { TypedEvent, TypedEventFilter } from '../abi/common' 2 | import { Contract } from 'ethers' 3 | import { Provider, Log } from '@ethersproject/abstract-provider' 4 | import { Interface } from 'ethers/lib/utils' 5 | 6 | /** 7 | * The type of the event arguments. 8 | * Gets the second generic arg 9 | */ 10 | export type EventArgs = T extends TypedEvent 11 | ? TObj 12 | : never 13 | 14 | /** 15 | * The event type of a filter 16 | * Gets the first generic arg 17 | */ 18 | export type EventFromFilter = TFilter extends TypedEventFilter< 19 | infer TEvent 20 | > 21 | ? TEvent 22 | : never 23 | 24 | /** 25 | * All filter keys for the provided contract 26 | */ 27 | type FilterName = keyof TContract['filters'] & 28 | string 29 | 30 | /** 31 | * The event type of a given filter 32 | */ 33 | type EventType< 34 | TContract extends Contract, 35 | TFilterName extends keyof TContract['filters'] 36 | > = EventArgs>> 37 | 38 | /** 39 | * Typechain contract factories have additional properties 40 | */ 41 | export type TypeChainContractFactory = { 42 | connect(address: string, provider: Provider): TContract 43 | createInterface(): Interface 44 | } 45 | 46 | /** 47 | * Parse a log that matches a given filter name. 48 | * @param contractFactory 49 | * @param log The log to parse 50 | * @param filterName 51 | * @returns Null if filter name topic does not match log topic 52 | */ 53 | export const parseTypedLog = < 54 | TContract extends Contract, 55 | TFilterName extends FilterName 56 | >( 57 | contractFactory: TypeChainContractFactory, 58 | log: Log, 59 | filterName: TFilterName 60 | ): EventType | null => { 61 | const iFace = contractFactory.createInterface() 62 | const event = iFace.getEvent(filterName) 63 | const topic = iFace.getEventTopic(event) 64 | 65 | if (log.topics[0] === topic) { 66 | return iFace.parseLog(log).args as EventType 67 | } else return null 68 | } 69 | 70 | /** 71 | * Parses an array of logs. 72 | * Filters out any logs whose topic does not match provided the filter name topic. 73 | * @param contractFactory 74 | * @param logs The logs to parse 75 | * @param filterName 76 | * @returns 77 | */ 78 | export const parseTypedLogs = < 79 | TContract extends Contract, 80 | TFilterName extends FilterName 81 | >( 82 | contractFactory: TypeChainContractFactory, 83 | logs: Log[], 84 | filterName: TFilterName 85 | ): EventType[] => { 86 | return logs 87 | .map(l => parseTypedLog(contractFactory, l, filterName)) 88 | .filter((i): i is NonNullable => i !== null) 89 | } 90 | -------------------------------------------------------------------------------- /packages/sdk/src/lib/dataEntities/message.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@ethersproject/bignumber' 2 | 3 | /** 4 | * The components of a submit retryable message. Can be parsed from the 5 | * events emitted from the Inbox. 6 | */ 7 | export interface RetryableMessageParams { 8 | /** 9 | * Destination address for L2 message 10 | */ 11 | destAddress: string 12 | /** 13 | * Call value in L2 message 14 | */ 15 | l2CallValue: BigNumber 16 | /** 17 | * Value sent at L1 18 | */ 19 | l1Value: BigNumber 20 | /** 21 | * Max gas deducted from L2 balance to cover base submission fee 22 | */ 23 | maxSubmissionFee: BigNumber 24 | /** 25 | * L2 address address to credit (gaslimit x gasprice - execution cost) 26 | */ 27 | excessFeeRefundAddress: string 28 | /** 29 | * Address to credit l2Callvalue on L2 if retryable txn times out or gets cancelled 30 | */ 31 | callValueRefundAddress: string 32 | /** 33 | * Max gas deducted from user's L2 balance to cover L2 execution 34 | */ 35 | gasLimit: BigNumber 36 | /** 37 | * Gas price for L2 execution 38 | */ 39 | maxFeePerGas: BigNumber 40 | /** 41 | * Calldata for of the L2 message 42 | */ 43 | data: string 44 | } 45 | 46 | /** 47 | * The inbox message kind as defined in: 48 | * https://github.com/OffchainLabs/nitro/blob/c7f3429e2456bf5ca296a49cec3bb437420bc2bb/contracts/src/libraries/MessageTypes.sol 49 | */ 50 | export enum InboxMessageKind { 51 | L1MessageType_submitRetryableTx = 9, 52 | L1MessageType_ethDeposit = 12, 53 | L2MessageType_signedTx = 4, 54 | } 55 | 56 | export enum ChildToParentMessageStatus { 57 | /** 58 | * ArbSys.sendTxToL1 called, but assertion not yet confirmed 59 | */ 60 | UNCONFIRMED, 61 | /** 62 | * Assertion for outgoing message confirmed, but message not yet executed 63 | */ 64 | CONFIRMED, 65 | /** 66 | * Outgoing message executed (terminal state) 67 | */ 68 | EXECUTED, 69 | } 70 | -------------------------------------------------------------------------------- /packages/sdk/src/lib/dataEntities/retryableData.ts: -------------------------------------------------------------------------------- 1 | import { Interface } from '@ethersproject/abi' 2 | import { BigNumber } from 'ethers' 3 | import { isDefined } from '../utils/lib' 4 | 5 | // TODO: add typechain support 6 | const errorInterface = new Interface([ 7 | 'error RetryableData(address from, address to, uint256 l2CallValue, uint256 deposit, uint256 maxSubmissionCost, address excessFeeRefundAddress, address callValueRefundAddress, uint256 gasLimit, uint256 maxFeePerGas, bytes data)', 8 | ]) 9 | // CAUTION: this type mirrors the error type above 10 | // The property names must exactly match those above 11 | export interface RetryableData { 12 | from: string 13 | /** 14 | * The address to be called on L2 15 | */ 16 | to: string 17 | /** 18 | * The value to call the L2 address with 19 | */ 20 | l2CallValue: BigNumber 21 | /** 22 | * The total amount to deposit on L1 to cover L2 gas and L2 call value 23 | */ 24 | deposit: BigNumber 25 | /** 26 | * The maximum cost to be paid for submitting the transaction 27 | */ 28 | maxSubmissionCost: BigNumber 29 | /** 30 | * The address to return the any gas that was not spent on fees 31 | */ 32 | excessFeeRefundAddress: string 33 | /** 34 | * The address to refund the call value to in the event the retryable is cancelled, or expires 35 | */ 36 | callValueRefundAddress: string 37 | /** 38 | * The L2 gas limit 39 | */ 40 | gasLimit: BigNumber 41 | /** 42 | * The max gas price to pay on L2 43 | */ 44 | maxFeePerGas: BigNumber 45 | /** 46 | * The data to call the L2 address with 47 | */ 48 | data: string 49 | } 50 | 51 | /** 52 | * Tools for parsing retryable data from errors. 53 | * When calling createRetryableTicket on Inbox.sol special values 54 | * can be passed for gasLimit and maxFeePerGas. This causes the call to revert 55 | * with the info needed to estimate the gas needed for a retryable ticket using 56 | * L1ToL2GasPriceEstimator. 57 | */ 58 | export class RetryableDataTools { 59 | /** 60 | * The parameters that should be passed to createRetryableTicket in order to induce 61 | * a revert with retryable data 62 | */ 63 | public static ErrorTriggeringParams = { 64 | gasLimit: BigNumber.from(1), 65 | maxFeePerGas: BigNumber.from(1), 66 | } 67 | 68 | private static isErrorData( 69 | maybeErrorData: Error | { errorData: string } 70 | ): maybeErrorData is { errorData: string } { 71 | return isDefined((maybeErrorData as { errorData: string }).errorData) 72 | } 73 | 74 | private static tryGetErrorData(ethersJsError: Error | { errorData: string }) { 75 | if (this.isErrorData(ethersJsError)) { 76 | return ethersJsError.errorData 77 | } else { 78 | const typedError = ethersJsError as { 79 | data?: string 80 | error?: { 81 | error?: { 82 | body?: string 83 | data?: string 84 | } 85 | } 86 | } 87 | 88 | if (typedError.data) { 89 | return typedError.data 90 | } else if (typedError.error?.error?.body) { 91 | const maybeData = ( 92 | JSON.parse(typedError.error?.error?.body) as { 93 | error?: { 94 | data?: string 95 | } 96 | } 97 | ).error?.data 98 | 99 | if (!maybeData) return null 100 | return maybeData 101 | } else if (typedError.error?.error?.data) { 102 | return typedError.error?.error?.data 103 | } else { 104 | return null 105 | } 106 | } 107 | } 108 | 109 | /** 110 | * Try to parse a retryable data struct from the supplied ethersjs error, or any explicitly supplied error data 111 | * @param ethersJsErrorOrData 112 | * @returns 113 | */ 114 | public static tryParseError( 115 | ethersJsErrorOrData: Error | { errorData: string } | string 116 | ): RetryableData | null { 117 | const errorData = 118 | typeof ethersJsErrorOrData === 'string' 119 | ? ethersJsErrorOrData 120 | : this.tryGetErrorData(ethersJsErrorOrData) 121 | if (!errorData) return null 122 | return errorInterface.parseError(errorData).args as unknown as RetryableData 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /packages/sdk/src/lib/dataEntities/rpc.ts: -------------------------------------------------------------------------------- 1 | import { TransactionReceipt, Block } from '@ethersproject/providers' 2 | import { BlockWithTransactions } from '@ethersproject/abstract-provider' 3 | import { BigNumber } from 'ethers' 4 | 5 | export interface ArbBlockProps { 6 | /** 7 | * The merkle root of the withdrawals tree 8 | */ 9 | sendRoot: string 10 | 11 | /** 12 | * Cumulative number of withdrawals since genesis 13 | */ 14 | sendCount: BigNumber 15 | 16 | /** 17 | * The l1 block number as seen from within this l2 block 18 | */ 19 | l1BlockNumber: number 20 | } 21 | 22 | export type ArbBlock = ArbBlockProps & Block 23 | export type ArbBlockWithTransactions = ArbBlockProps & BlockWithTransactions 24 | 25 | /** 26 | * Eth transaction receipt with additional arbitrum specific fields 27 | */ 28 | export interface ArbTransactionReceipt extends TransactionReceipt { 29 | /** 30 | * The l1 block number that would be used for block.number calls 31 | * that occur within this transaction. 32 | * See https://developer.offchainlabs.com/docs/time_in_arbitrum 33 | */ 34 | l1BlockNumber: number 35 | /** 36 | * Amount of gas spent on l1 computation in units of l2 gas 37 | */ 38 | gasUsedForL1: BigNumber 39 | } 40 | -------------------------------------------------------------------------------- /packages/sdk/src/lib/dataEntities/signerOrProvider.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@ethersproject/abstract-provider' 2 | import { Signer } from '@ethersproject/abstract-signer' 3 | import { ArbSdkError, MissingProviderArbSdkError } from '../dataEntities/errors' 4 | import { isDefined } from '../utils/lib' 5 | 6 | export type SignerOrProvider = Signer | Provider 7 | 8 | /** 9 | * Utility functions for signer/provider union types 10 | */ 11 | export class SignerProviderUtils { 12 | public static isSigner( 13 | signerOrProvider: SignerOrProvider 14 | ): signerOrProvider is Signer { 15 | return isDefined((signerOrProvider as Signer).signMessage) 16 | } 17 | 18 | /** 19 | * If signerOrProvider is a provider then return itself. 20 | * If signerOrProvider is a signer then return signer.provider 21 | * @param signerOrProvider 22 | * @returns 23 | */ 24 | public static getProvider( 25 | signerOrProvider: SignerOrProvider 26 | ): Provider | undefined { 27 | return this.isSigner(signerOrProvider) 28 | ? signerOrProvider.provider 29 | : signerOrProvider 30 | } 31 | 32 | public static getProviderOrThrow( 33 | signerOrProvider: SignerOrProvider 34 | ): Provider { 35 | const maybeProvider = this.getProvider(signerOrProvider) 36 | if (!maybeProvider) throw new MissingProviderArbSdkError('signerOrProvider') 37 | return maybeProvider 38 | } 39 | 40 | /** 41 | * Check if the signer has a connected provider 42 | * @param signer 43 | */ 44 | public static signerHasProvider( 45 | signer: Signer 46 | ): signer is Signer & { provider: Provider } { 47 | return isDefined(signer.provider) 48 | } 49 | 50 | /** 51 | * Checks that the signer/provider that's provider matches the chain id 52 | * Throws if not. 53 | * @param signerOrProvider 54 | * @param chainId 55 | */ 56 | public static async checkNetworkMatches( 57 | signerOrProvider: SignerOrProvider, 58 | chainId: number 59 | ): Promise { 60 | const provider = this.getProvider(signerOrProvider) 61 | if (!provider) throw new MissingProviderArbSdkError('signerOrProvider') 62 | 63 | const providerChainId = (await provider.getNetwork()).chainId 64 | if (providerChainId !== chainId) { 65 | throw new ArbSdkError( 66 | `Signer/provider chain id: ${providerChainId} doesn't match provided chain id: ${chainId}.` 67 | ) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/sdk/src/lib/dataEntities/transactionRequest.ts: -------------------------------------------------------------------------------- 1 | import { TransactionRequest, Provider } from '@ethersproject/providers' 2 | import { BigNumber } from 'ethers' 3 | import { 4 | ParentToChildMessageGasParams, 5 | ParentToChildMessageParams, 6 | } from '../message/ParentToChildMessageCreator' 7 | import { isDefined } from '../utils/lib' 8 | 9 | /** 10 | * A transaction request for a transaction that will trigger some sort of 11 | * execution on the child chain 12 | */ 13 | export interface ParentToChildTransactionRequest { 14 | /** 15 | * Core fields needed to form the parent component of the transaction request 16 | */ 17 | txRequest: Required< 18 | Pick 19 | > 20 | /** 21 | * Information about the retryable ticket, and it's subsequent execution, that 22 | * will occur on the child chain 23 | */ 24 | retryableData: ParentToChildMessageParams & ParentToChildMessageGasParams 25 | /** 26 | * If this request were sent now, would it have enough margin to reliably succeed 27 | */ 28 | isValid(): Promise 29 | } 30 | 31 | /** 32 | * A transaction request for a transaction that will trigger a child to parent message 33 | */ 34 | export interface ChildToParentTransactionRequest { 35 | txRequest: Required< 36 | Pick 37 | > 38 | /** 39 | * Estimate the gas limit required to execute the withdrawal on the parent chain. 40 | * Note that this is only a rough estimate as it may not be possible to know 41 | * the exact size of the proof straight away, however the real value should be 42 | * within a few thousand gas of this estimate. 43 | */ 44 | estimateParentGasLimit: (l1Provider: Provider) => Promise 45 | } 46 | 47 | /** 48 | * Ensure the T is not of TransactionRequest type by ensure it doesn't have a specific TransactionRequest property 49 | */ 50 | type IsNotTransactionRequest = T extends { txRequest: any } ? never : T 51 | 52 | /** 53 | * Check if an object is of ParentToChildTransactionRequest type 54 | * @param possibleRequest 55 | * @returns 56 | */ 57 | export const isParentToChildTransactionRequest = ( 58 | possibleRequest: IsNotTransactionRequest | ParentToChildTransactionRequest 59 | ): possibleRequest is ParentToChildTransactionRequest => { 60 | return isDefined( 61 | (possibleRequest as ParentToChildTransactionRequest).txRequest 62 | ) 63 | } 64 | 65 | /** 66 | * Check if an object is of ChildToParentTransactionRequest type 67 | * @param possibleRequest 68 | * @returns 69 | */ 70 | export const isChildToParentTransactionRequest = ( 71 | possibleRequest: IsNotTransactionRequest | ChildToParentTransactionRequest 72 | ): possibleRequest is ChildToParentTransactionRequest => { 73 | return ( 74 | (possibleRequest as ChildToParentTransactionRequest).txRequest != undefined 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /packages/sdk/src/lib/message/ParentToChildMessageCreator.ts: -------------------------------------------------------------------------------- 1 | import { constants } from 'ethers' 2 | import { Signer } from '@ethersproject/abstract-signer' 3 | import { Provider } from '@ethersproject/abstract-provider' 4 | 5 | import { 6 | GasOverrides, 7 | ParentToChildMessageGasEstimator, 8 | } from './ParentToChildMessageGasEstimator' 9 | import { 10 | ParentContractTransaction, 11 | ParentTransactionReceipt, 12 | } from './ParentTransaction' 13 | import { Inbox__factory } from '../abi/factories/Inbox__factory' 14 | import { 15 | getArbitrumNetwork, 16 | isArbitrumNetworkNativeTokenEther, 17 | } from '../dataEntities/networks' 18 | import { ERC20Inbox__factory } from '../abi/factories/ERC20Inbox__factory' 19 | import { PayableOverrides } from '@ethersproject/contracts' 20 | import { SignerProviderUtils } from '../dataEntities/signerOrProvider' 21 | import { MissingProviderArbSdkError } from '../dataEntities/errors' 22 | import { getBaseFee } from '../utils/lib' 23 | import { 24 | isParentToChildTransactionRequest, 25 | ParentToChildTransactionRequest, 26 | } from '../dataEntities/transactionRequest' 27 | import { RetryableData } from '../dataEntities/retryableData' 28 | import { OmitTyped, PartialPick } from '../utils/types' 29 | 30 | type ParentToChildGasKeys = 31 | | 'maxSubmissionCost' 32 | | 'maxFeePerGas' 33 | | 'gasLimit' 34 | | 'deposit' 35 | export type ParentToChildMessageGasParams = Pick< 36 | RetryableData, 37 | ParentToChildGasKeys 38 | > 39 | export type ParentToChildMessageNoGasParams = OmitTyped< 40 | RetryableData, 41 | ParentToChildGasKeys 42 | > 43 | export type ParentToChildMessageParams = PartialPick< 44 | ParentToChildMessageNoGasParams, 45 | 'excessFeeRefundAddress' | 'callValueRefundAddress' 46 | > 47 | 48 | /** 49 | * Creates retryable tickets by directly calling the Inbox contract on Parent chain 50 | */ 51 | export class ParentToChildMessageCreator { 52 | constructor(public readonly parentSigner: Signer) { 53 | if (!SignerProviderUtils.signerHasProvider(parentSigner)) { 54 | throw new MissingProviderArbSdkError('parentSigner') 55 | } 56 | } 57 | 58 | /** 59 | * Gets a current estimate for the supplied params 60 | * @param params 61 | * @param parentProvider 62 | * @param childProvider 63 | * @param retryableGasOverrides 64 | * @returns 65 | */ 66 | protected static async getTicketEstimate( 67 | params: ParentToChildMessageNoGasParams, 68 | parentProvider: Provider, 69 | childProvider: Provider, 70 | retryableGasOverrides?: GasOverrides 71 | ): Promise> { 72 | const baseFee = await getBaseFee(parentProvider) 73 | 74 | const gasEstimator = new ParentToChildMessageGasEstimator(childProvider) 75 | return await gasEstimator.estimateAll( 76 | params, 77 | baseFee, 78 | parentProvider, 79 | retryableGasOverrides 80 | ) 81 | } 82 | 83 | /** 84 | * Prepare calldata for a call to create a retryable ticket 85 | * @param params 86 | * @param estimates 87 | * @param excessFeeRefundAddress 88 | * @param callValueRefundAddress 89 | * @param nativeTokenIsEth 90 | * @returns 91 | */ 92 | protected static getTicketCreationRequestCallData( 93 | params: ParentToChildMessageParams, 94 | estimates: Pick, 95 | excessFeeRefundAddress: string, 96 | callValueRefundAddress: string, 97 | nativeTokenIsEth: boolean 98 | ) { 99 | if (!nativeTokenIsEth) { 100 | return ERC20Inbox__factory.createInterface().encodeFunctionData( 101 | 'createRetryableTicket', 102 | [ 103 | params.to, 104 | params.l2CallValue, 105 | estimates.maxSubmissionCost, 106 | excessFeeRefundAddress, 107 | callValueRefundAddress, 108 | estimates.gasLimit, 109 | estimates.maxFeePerGas, 110 | estimates.deposit, // tokenTotalFeeAmount 111 | params.data, 112 | ] 113 | ) 114 | } 115 | 116 | return Inbox__factory.createInterface().encodeFunctionData( 117 | 'createRetryableTicket', 118 | [ 119 | params.to, 120 | params.l2CallValue, 121 | estimates.maxSubmissionCost, 122 | excessFeeRefundAddress, 123 | callValueRefundAddress, 124 | estimates.gasLimit, 125 | estimates.maxFeePerGas, 126 | params.data, 127 | ] 128 | ) 129 | } 130 | 131 | /** 132 | * Generate a transaction request for creating a retryable ticket 133 | * @param params 134 | * @param parentProvider 135 | * @param childProvider 136 | * @param options 137 | * @returns 138 | */ 139 | public static async getTicketCreationRequest( 140 | params: ParentToChildMessageParams, 141 | parentProvider: Provider, 142 | childProvider: Provider, 143 | options?: GasOverrides 144 | ): Promise { 145 | const excessFeeRefundAddress = params.excessFeeRefundAddress || params.from 146 | const callValueRefundAddress = params.callValueRefundAddress || params.from 147 | 148 | const parsedParams: ParentToChildMessageNoGasParams = { 149 | ...params, 150 | excessFeeRefundAddress, 151 | callValueRefundAddress, 152 | } 153 | 154 | const estimates = await ParentToChildMessageCreator.getTicketEstimate( 155 | parsedParams, 156 | parentProvider, 157 | childProvider, 158 | options 159 | ) 160 | 161 | const childChain = await getArbitrumNetwork(childProvider) 162 | const nativeTokenIsEth = isArbitrumNetworkNativeTokenEther(childChain) 163 | 164 | const data = ParentToChildMessageCreator.getTicketCreationRequestCallData( 165 | params, 166 | estimates, 167 | excessFeeRefundAddress, 168 | callValueRefundAddress, 169 | nativeTokenIsEth 170 | ) 171 | 172 | return { 173 | txRequest: { 174 | to: childChain.ethBridge.inbox, 175 | data, 176 | value: nativeTokenIsEth ? estimates.deposit : constants.Zero, 177 | from: params.from, 178 | }, 179 | retryableData: { 180 | data: params.data, 181 | from: params.from, 182 | to: params.to, 183 | excessFeeRefundAddress: excessFeeRefundAddress, 184 | callValueRefundAddress: callValueRefundAddress, 185 | l2CallValue: params.l2CallValue, 186 | maxSubmissionCost: estimates.maxSubmissionCost, 187 | maxFeePerGas: estimates.maxFeePerGas, 188 | gasLimit: estimates.gasLimit, 189 | deposit: estimates.deposit, 190 | }, 191 | isValid: async () => { 192 | const reEstimates = await ParentToChildMessageCreator.getTicketEstimate( 193 | parsedParams, 194 | parentProvider, 195 | childProvider, 196 | options 197 | ) 198 | return ParentToChildMessageGasEstimator.isValid(estimates, reEstimates) 199 | }, 200 | } 201 | } 202 | 203 | /** 204 | * Creates a retryable ticket by directly calling the Inbox contract on Parent chain 205 | */ 206 | public async createRetryableTicket( 207 | params: 208 | | (ParentToChildMessageParams & { overrides?: PayableOverrides }) 209 | | (ParentToChildTransactionRequest & { 210 | overrides?: PayableOverrides 211 | }), 212 | childProvider: Provider, 213 | options?: GasOverrides 214 | ): Promise { 215 | const parentProvider = SignerProviderUtils.getProviderOrThrow( 216 | this.parentSigner 217 | ) 218 | const createRequest = isParentToChildTransactionRequest(params) 219 | ? params 220 | : await ParentToChildMessageCreator.getTicketCreationRequest( 221 | params, 222 | parentProvider, 223 | childProvider, 224 | options 225 | ) 226 | 227 | const tx = await this.parentSigner.sendTransaction({ 228 | ...createRequest.txRequest, 229 | ...params.overrides, 230 | }) 231 | 232 | return ParentTransactionReceipt.monkeyPatchWait(tx) 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /packages/sdk/src/lib/message/messageDataParser.ts: -------------------------------------------------------------------------------- 1 | import { getAddress } from '@ethersproject/address' 2 | import { defaultAbiCoder } from '@ethersproject/abi' 3 | import { BigNumber } from '@ethersproject/bignumber' 4 | import { hexZeroPad } from '@ethersproject/bytes' 5 | 6 | export class SubmitRetryableMessageDataParser { 7 | /** 8 | * Parse the event data emitted in the InboxMessageDelivered event 9 | * for messages of type L1MessageType_submitRetryableTx 10 | * @param eventData The data field in InboxMessageDelivered for messages of kind L1MessageType_submitRetryableTx 11 | * @returns 12 | */ 13 | public parse(eventData: string) { 14 | // decode the data field - is been packed so we cant decode the bytes field this way 15 | const parsed = defaultAbiCoder.decode( 16 | [ 17 | 'uint256', // dest 18 | 'uint256', // l2 call balue 19 | 'uint256', // msg val 20 | 'uint256', // max submission 21 | 'uint256', // excess fee refund addr 22 | 'uint256', // call value refund addr 23 | 'uint256', // max gas 24 | 'uint256', // gas price bid 25 | 'uint256', // data length 26 | ], 27 | eventData 28 | ) as BigNumber[] 29 | 30 | const addressFromBigNumber = (bn: BigNumber) => 31 | getAddress(hexZeroPad(bn.toHexString(), 20)) 32 | 33 | const destAddress = addressFromBigNumber(parsed[0]) 34 | const l2CallValue = parsed[1] 35 | const l1Value = parsed[2] 36 | const maxSubmissionFee = parsed[3] 37 | const excessFeeRefundAddress = addressFromBigNumber(parsed[4]) 38 | const callValueRefundAddress = addressFromBigNumber(parsed[5]) 39 | const gasLimit = parsed[6] 40 | const maxFeePerGas = parsed[7] 41 | const callDataLength = parsed[8] 42 | const data = 43 | '0x' + 44 | eventData.substring(eventData.length - callDataLength.mul(2).toNumber()) 45 | 46 | return { 47 | destAddress, 48 | l2CallValue, 49 | l1Value, 50 | maxSubmissionFee: maxSubmissionFee, 51 | excessFeeRefundAddress, 52 | callValueRefundAddress, 53 | gasLimit, 54 | maxFeePerGas, 55 | data, 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/sdk/src/lib/utils/arbProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JsonRpcProvider, 3 | Formatter, 4 | BlockTag, 5 | Web3Provider, 6 | JsonRpcFetchFunc, 7 | } from '@ethersproject/providers' 8 | import { Formats } from '@ethersproject/providers/lib/formatter' 9 | import { Networkish } from '@ethersproject/networks' 10 | import { 11 | ArbBlock, 12 | ArbBlockWithTransactions, 13 | ArbTransactionReceipt, 14 | } from '../dataEntities/rpc' 15 | 16 | class ArbFormatter extends Formatter { 17 | readonly formats!: Formats 18 | 19 | public getDefaultFormats(): Formats { 20 | // formats was already initialised in super, so we can just access here 21 | const superFormats = super.getDefaultFormats() 22 | 23 | const bigNumber = this.bigNumber.bind(this) 24 | const hash = this.hash.bind(this) 25 | const number = this.number.bind(this) 26 | 27 | const arbBlockProps = { 28 | sendRoot: hash, 29 | sendCount: bigNumber, 30 | l1BlockNumber: number, 31 | } 32 | 33 | const arbReceiptFormat = { 34 | ...superFormats.receipt, 35 | l1BlockNumber: number, 36 | gasUsedForL1: bigNumber, 37 | } 38 | 39 | return { 40 | ...superFormats, 41 | receipt: arbReceiptFormat, 42 | block: { ...superFormats.block, ...arbBlockProps }, 43 | blockWithTransactions: { 44 | ...superFormats.blockWithTransactions, 45 | ...arbBlockProps, 46 | }, 47 | } 48 | } 49 | 50 | public receipt(value: any): ArbTransactionReceipt { 51 | return super.receipt(value) as ArbTransactionReceipt 52 | } 53 | 54 | public block(block: any): ArbBlock { 55 | return super.block(block) as ArbBlock 56 | } 57 | 58 | public blockWithTransactions(block: any): ArbBlock { 59 | // ethersjs chose the wrong type for the super - it should have been BlockWithTransactions 60 | // but was instead just Block. This means that when we override we cant use ArbBlockWithTransactions 61 | // but must instead use just ArbBlock. 62 | return super.blockWithTransactions(block) as ArbBlock 63 | } 64 | } 65 | 66 | /** 67 | * Arbitrum specific formats 68 | */ 69 | export class ArbitrumProvider extends Web3Provider { 70 | private static arbFormatter = new ArbFormatter() 71 | 72 | /** 73 | * Arbitrum specific formats 74 | * @param provider Must be connected to an Arbitrum network 75 | * @param network Must be an Arbitrum network 76 | */ 77 | public constructor(provider: JsonRpcProvider, network?: Networkish) { 78 | super(provider.send.bind(provider) as JsonRpcFetchFunc, network) 79 | } 80 | 81 | static override getFormatter(): Formatter { 82 | return this.arbFormatter 83 | } 84 | 85 | public override async getTransactionReceipt( 86 | transactionHash: string | Promise 87 | ): Promise { 88 | return (await super.getTransactionReceipt( 89 | transactionHash 90 | )) as ArbTransactionReceipt 91 | } 92 | 93 | public override async getBlockWithTransactions( 94 | blockHashOrBlockTag: BlockTag | Promise 95 | ): Promise { 96 | return (await super.getBlockWithTransactions( 97 | blockHashOrBlockTag 98 | )) as ArbBlockWithTransactions 99 | } 100 | 101 | public override async getBlock( 102 | blockHashOrBlockTag: BlockTag | Promise 103 | ): Promise { 104 | return (await super.getBlock(blockHashOrBlockTag)) as ArbBlock 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /packages/sdk/src/lib/utils/byte_serialize_params.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, Offchain Labs, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /* eslint-env node */ 17 | 'use strict' 18 | 19 | /** 20 | #### Byte Serializing Solidity Arguments Schema 21 | 22 | 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: 23 | 24 | #### address[]: 25 | 26 | | field | size (bytes) | Description | 27 | | ------------- | ------------------ | ----------------------------------------------------------------------- | 28 | | length | 1 | Size of array | 29 | | is-registered | 1 | 1 = all registered, 0 = not all registered | 30 | | addresses | 4 or 20 (x length) | If is registered, left-padded 4-byte integers; otherwise, eth addresses | 31 | 32 | #### non-address[]: 33 | 34 | | field | size (bytes) | Description | 35 | | ------ | ------------ | ------------------------ | 36 | | length | 1 | Size of array | 37 | | items | (variable) | All items (concatenated) | 38 | 39 | #### address: 40 | 41 | | field | size (bytes) | Description | 42 | | ------------- | ------------ | ----------------------------------------------------------------- | 43 | | is-registered | 1 | 1 = registered, 0 = not registered | 44 | | address | 4 or 20 | If registered, left-padded 4-byte integer; otherwise, eth address | 45 | 46 | * @module Byte-Serialization 47 | */ 48 | 49 | import { Provider } from '@ethersproject/abstract-provider' 50 | import { Signer } from '@ethersproject/abstract-signer' 51 | import { isAddress as _isAddress } from '@ethersproject/address' 52 | import { concat, hexZeroPad } from '@ethersproject/bytes' 53 | import { BigNumber } from '@ethersproject/bignumber' 54 | 55 | import { ArbAddressTable__factory } from '../abi/factories/ArbAddressTable__factory' 56 | import { ArbAddressTable } from '../abi/ArbAddressTable' 57 | 58 | import { ARB_ADDRESS_TABLE_ADDRESS } from '../dataEntities/constants' 59 | import { ArbSdkError } from '../dataEntities/errors' 60 | 61 | type PrimativeType = string | number | boolean | BigNumber 62 | type PrimativeOrPrimativeArray = PrimativeType | PrimativeType[] 63 | type BytesNumber = 1 | 4 | 8 | 16 | 32 64 | 65 | interface AddressIndexMemo { 66 | [address: string]: number 67 | } 68 | 69 | export const getAddressIndex = (() => { 70 | const addressToIndexMemo: AddressIndexMemo = {} 71 | let arbAddressTable: ArbAddressTable | undefined 72 | 73 | return async (address: string, signerOrProvider: Signer | Provider) => { 74 | if (addressToIndexMemo[address]) { 75 | return addressToIndexMemo[address] 76 | } 77 | arbAddressTable = 78 | arbAddressTable || 79 | ArbAddressTable__factory.connect( 80 | ARB_ADDRESS_TABLE_ADDRESS, 81 | signerOrProvider 82 | ) 83 | const isRegistered = await arbAddressTable.addressExists(address) 84 | if (isRegistered) { 85 | const index = (await arbAddressTable.lookup(address)).toNumber() 86 | addressToIndexMemo[address] = index 87 | return index 88 | } else { 89 | return -1 90 | } 91 | } 92 | })() 93 | 94 | /** 95 | to use: 96 | 97 | ```js 98 | const mySerializeParamsFunction = argSerializerConstructor("rpcurl") 99 | mySerializeParamsFunction(["4","5", "6"]) 100 | ``` 101 | */ 102 | export const argSerializerConstructor = ( 103 | arbProvider: Provider 104 | ): ((params: PrimativeOrPrimativeArray[]) => Promise) => { 105 | return async (params: PrimativeOrPrimativeArray[]) => { 106 | return await serializeParams(params, async (address: string) => { 107 | return await getAddressIndex(address, arbProvider) 108 | }) 109 | } 110 | } 111 | 112 | const isAddress = (input: PrimativeType) => 113 | typeof input === 'string' && _isAddress(input) 114 | 115 | const toUint = (val: PrimativeType, bytes: BytesNumber) => 116 | hexZeroPad(BigNumber.from(val).toHexString(), bytes) 117 | 118 | // outputs string suitable for formatting 119 | const formatPrimative = (value: PrimativeType) => { 120 | if (isAddress(value)) { 121 | return value as string 122 | } else if (typeof value === 'boolean') { 123 | return toUint(value ? 1 : 0, 1) 124 | } else if ( 125 | typeof value === 'number' || 126 | Number(value) || 127 | BigNumber.isBigNumber(value) 128 | ) { 129 | return toUint(value, 32) 130 | } else { 131 | throw new ArbSdkError('unsupported type') 132 | } 133 | } 134 | /** 135 | * @param params array of serializable types to 136 | * @param addressToIndex optional getter of address index registered in table 137 | */ 138 | export const serializeParams = async ( 139 | params: PrimativeOrPrimativeArray[], 140 | addressToIndex: (address: string) => Promise = () => 141 | new Promise(exec => exec(-1)) 142 | ): Promise => { 143 | const formattedParams: string[] = [] 144 | 145 | for (const param of params) { 146 | // handle arrays 147 | if (Array.isArray(param)) { 148 | let paramArray: PrimativeType[] = param as PrimativeType[] 149 | formattedParams.push(toUint(paramArray.length, 1)) 150 | 151 | if (isAddress(paramArray[0])) { 152 | const indices = await Promise.all( 153 | paramArray.map( 154 | async address => await addressToIndex(address as string) 155 | ) 156 | ) 157 | // If all addresses are registered, serialize as indices 158 | if (indices.every(i => i > -1)) { 159 | paramArray = indices as number[] 160 | formattedParams.push(toUint(1, 1)) 161 | paramArray.forEach(value => { 162 | formattedParams.push(toUint(value, 4)) 163 | }) 164 | // otherwise serialize as address 165 | } else { 166 | formattedParams.push(toUint(0, 1)) 167 | paramArray.forEach(value => { 168 | formattedParams.push(formatPrimative(value)) 169 | }) 170 | } 171 | } else { 172 | paramArray.forEach(value => { 173 | formattedParams.push(formatPrimative(value)) 174 | }) 175 | } 176 | } else { 177 | // handle non-arrays 178 | if (isAddress(param)) { 179 | const index = await addressToIndex(param as string) 180 | if (index > -1) { 181 | formattedParams.push(toUint(1, 1), toUint(index, 4)) 182 | } else { 183 | formattedParams.push(toUint(0, 1), formatPrimative(param)) 184 | } 185 | } else { 186 | formattedParams.push(formatPrimative(param)) 187 | } 188 | } 189 | } 190 | return concat(formattedParams) 191 | } 192 | -------------------------------------------------------------------------------- /packages/sdk/src/lib/utils/calldata.ts: -------------------------------------------------------------------------------- 1 | import { L1GatewayRouter__factory } from '../abi/factories/L1GatewayRouter__factory' 2 | import { ParentToChildTxReqAndSigner } from '../assetBridger/ethBridger' 3 | 4 | export const getErc20ParentAddressFromParentToChildTxRequest = ( 5 | txReq: ParentToChildTxReqAndSigner 6 | ): string => { 7 | const { 8 | txRequest: { data }, 9 | } = txReq 10 | 11 | const iGatewayRouter = L1GatewayRouter__factory.createInterface() 12 | 13 | try { 14 | const decodedData = iGatewayRouter.decodeFunctionData( 15 | 'outboundTransfer', 16 | data 17 | ) 18 | 19 | return decodedData['_token'] 20 | } catch { 21 | try { 22 | const decodedData = iGatewayRouter.decodeFunctionData( 23 | 'outboundTransferCustomRefund', 24 | data 25 | ) 26 | 27 | return decodedData['_token'] 28 | } catch { 29 | throw new Error('data signature not matching deposits methods') 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/sdk/src/lib/utils/env.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv' 2 | import * as path from 'path' 3 | 4 | export const loadEnv = () => { 5 | dotenv.config({ path: path.resolve(__dirname, '../../../../../.env') }) 6 | } 7 | -------------------------------------------------------------------------------- /packages/sdk/src/lib/utils/eventFetcher.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, Offchain Labs, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /* eslint-env node */ 17 | 'use strict' 18 | 19 | import { Provider, BlockTag, Filter } from '@ethersproject/abstract-provider' 20 | import { Contract, Event } from '@ethersproject/contracts' 21 | import { constants } from 'ethers' 22 | import { TypedEvent, TypedEventFilter } from '../abi/common' 23 | import { EventArgs, TypeChainContractFactory } from '../dataEntities/event' 24 | 25 | export type FetchedEvent = { 26 | event: EventArgs 27 | topic: string 28 | name: string 29 | blockNumber: number 30 | blockHash: string 31 | transactionHash: string 32 | address: string 33 | topics: string[] 34 | data: string 35 | } 36 | 37 | // I'm not sure why, but I wasn't able to get the getEvents function to properly 38 | // infer the Event return type. It would always infer it as TypedEvent 39 | // instead of the strong typed event that should be available. This type correctly 40 | // infers the event type so we can force getEvents to return the correct type 41 | // using this. 42 | type TEventOf = T extends TypedEventFilter ? TEvent : never 43 | 44 | /** 45 | * Fetches and parses blockchain logs 46 | */ 47 | export class EventFetcher { 48 | public constructor(public readonly provider: Provider) {} 49 | 50 | /** 51 | * Fetch logs and parse logs 52 | * @param contractFactory A contract factory for generating a contract of type TContract at the addr 53 | * @param topicGenerator Generator function for creating 54 | * @param filter Block and address filter parameters 55 | * @returns 56 | */ 57 | public async getEvents< 58 | TContract extends Contract, 59 | TEventFilter extends TypedEventFilter 60 | >( 61 | contractFactory: TypeChainContractFactory, 62 | topicGenerator: (t: TContract) => TEventFilter, 63 | filter: { 64 | fromBlock: BlockTag 65 | toBlock: BlockTag 66 | address?: string 67 | } 68 | ): Promise>[]> { 69 | const contract = contractFactory.connect( 70 | filter.address || constants.AddressZero, 71 | this.provider 72 | ) 73 | const eventFilter = topicGenerator(contract) 74 | const fullFilter: Filter = { 75 | ...eventFilter, 76 | address: filter.address, 77 | fromBlock: filter.fromBlock, 78 | toBlock: filter.toBlock, 79 | } 80 | const logs = await this.provider.getLogs(fullFilter) 81 | return logs 82 | .filter(l => l.removed === false) 83 | .map(l => { 84 | const pLog = contract.interface.parseLog(l) 85 | 86 | return { 87 | event: pLog.args, 88 | topic: pLog.topic, 89 | name: pLog.name, 90 | blockNumber: l.blockNumber, 91 | blockHash: l.blockHash, 92 | transactionHash: l.transactionHash, 93 | 94 | address: l.address, 95 | topics: l.topics, 96 | data: l.data, 97 | } 98 | }) as FetchedEvent>[] 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /packages/sdk/src/lib/utils/lib.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, constants } from 'ethers' 2 | import { Provider } from '@ethersproject/abstract-provider' 3 | import { TransactionReceipt, JsonRpcProvider } from '@ethersproject/providers' 4 | import { ArbSdkError } from '../dataEntities/errors' 5 | import { ArbitrumProvider } from './arbProvider' 6 | import { ArbSys__factory } from '../abi/factories/ArbSys__factory' 7 | import { ARB_SYS_ADDRESS } from '../dataEntities/constants' 8 | import { ArbitrumNetwork, getNitroGenesisBlock } from '../dataEntities/networks' 9 | import { ERC20__factory } from '../abi/factories/ERC20__factory' 10 | 11 | export const wait = (ms: number): Promise => 12 | new Promise(res => setTimeout(res, ms)) 13 | 14 | export const getBaseFee = async (provider: Provider): Promise => { 15 | const baseFee = (await provider.getBlock('latest')).baseFeePerGas 16 | if (!baseFee) { 17 | throw new ArbSdkError( 18 | 'Latest block did not contain base fee, ensure provider is connected to a network that supports EIP 1559.' 19 | ) 20 | } 21 | return baseFee 22 | } 23 | 24 | /** 25 | * Waits for a transaction receipt if confirmations or timeout is provided 26 | * Otherwise tries to fetch straight away. 27 | * @param provider 28 | * @param txHash 29 | * @param confirmations 30 | * @param timeout 31 | * @returns 32 | */ 33 | export const getTransactionReceipt = async ( 34 | provider: Provider, 35 | txHash: string, 36 | confirmations?: number, 37 | timeout?: number 38 | ): Promise => { 39 | if (confirmations || timeout) { 40 | try { 41 | const receipt = await provider.waitForTransaction( 42 | txHash, 43 | confirmations, 44 | timeout 45 | ) 46 | return receipt || null 47 | } catch (err) { 48 | if ((err as Error).message.includes('timeout exceeded')) { 49 | // return null 50 | return null 51 | } else throw err 52 | } 53 | } else { 54 | const receipt = await provider.getTransactionReceipt(txHash) 55 | return receipt || null 56 | } 57 | } 58 | 59 | export const isDefined = (val: T | null | undefined): val is T => 60 | typeof val !== 'undefined' && val !== null 61 | 62 | export const isArbitrumChain = async (provider: Provider): Promise => { 63 | try { 64 | await ArbSys__factory.connect(ARB_SYS_ADDRESS, provider).arbOSVersion() 65 | } catch (error) { 66 | return false 67 | } 68 | return true 69 | } 70 | 71 | type GetFirstBlockForL1BlockProps = { 72 | arbitrumProvider: JsonRpcProvider 73 | forL1Block: number 74 | allowGreater?: boolean 75 | minArbitrumBlock?: number 76 | maxArbitrumBlock?: number | 'latest' 77 | } 78 | 79 | /** 80 | * This function performs a binary search to find the first Arbitrum block that corresponds to a given L1 block number. 81 | * The function returns a Promise that resolves to a number if a block is found, or undefined otherwise. 82 | * 83 | * @param {JsonRpcProvider} arbitrumProvider - The Arbitrum provider to use for the search. 84 | * @param {number} forL1Block - The L1 block number to search for. 85 | * @param {boolean} [allowGreater=false] - Whether to allow the search to go past the specified `forL1Block`. 86 | * @param {number|string} minArbitrumBlock - The minimum Arbitrum block number to start the search from. Cannot be below the network's Nitro genesis block. 87 | * @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. 88 | * @returns {Promise} - A Promise that resolves to a number if a block is found, or undefined otherwise. 89 | */ 90 | export async function getFirstBlockForL1Block({ 91 | arbitrumProvider, 92 | forL1Block, 93 | allowGreater = false, 94 | minArbitrumBlock, 95 | maxArbitrumBlock = 'latest', 96 | }: GetFirstBlockForL1BlockProps): Promise { 97 | if (!(await isArbitrumChain(arbitrumProvider))) { 98 | // Provider is L1. 99 | return forL1Block 100 | } 101 | 102 | const arbProvider = new ArbitrumProvider(arbitrumProvider) 103 | const currentArbBlock = await arbProvider.getBlockNumber() 104 | const arbitrumChainId = (await arbProvider.getNetwork()).chainId 105 | const nitroGenesisBlock = getNitroGenesisBlock(arbitrumChainId) 106 | 107 | async function getL1Block(forL2Block: number) { 108 | const { l1BlockNumber } = await arbProvider.getBlock(forL2Block) 109 | return l1BlockNumber 110 | } 111 | 112 | if (!minArbitrumBlock) { 113 | minArbitrumBlock = nitroGenesisBlock 114 | } 115 | 116 | if (maxArbitrumBlock === 'latest') { 117 | maxArbitrumBlock = currentArbBlock 118 | } 119 | 120 | if (minArbitrumBlock >= maxArbitrumBlock) { 121 | throw new Error( 122 | `'minArbitrumBlock' (${minArbitrumBlock}) must be lower than 'maxArbitrumBlock' (${maxArbitrumBlock}).` 123 | ) 124 | } 125 | 126 | if (minArbitrumBlock < nitroGenesisBlock) { 127 | throw new Error( 128 | `'minArbitrumBlock' (${minArbitrumBlock}) cannot be below the Nitro genesis block, which is ${nitroGenesisBlock} for the current network.` 129 | ) 130 | } 131 | 132 | let start = minArbitrumBlock 133 | let end = maxArbitrumBlock 134 | 135 | let resultForTargetBlock 136 | let resultForGreaterBlock 137 | 138 | while (start <= end) { 139 | // Calculate the midpoint of the current range. 140 | const mid = start + Math.floor((end - start) / 2) 141 | 142 | const l1Block = await getL1Block(mid) 143 | 144 | // If the midpoint matches the target, we've found a match. 145 | // Adjust the range to search for the first occurrence. 146 | if (l1Block === forL1Block) { 147 | end = mid - 1 148 | } else if (l1Block < forL1Block) { 149 | start = mid + 1 150 | } else { 151 | end = mid - 1 152 | } 153 | 154 | // Stores last valid Arbitrum block corresponding to the current, or greater, L1 block. 155 | if (l1Block) { 156 | if (l1Block === forL1Block) { 157 | resultForTargetBlock = mid 158 | } 159 | if (allowGreater && l1Block > forL1Block) { 160 | resultForGreaterBlock = mid 161 | } 162 | } 163 | } 164 | 165 | return resultForTargetBlock ?? resultForGreaterBlock 166 | } 167 | 168 | export const getBlockRangesForL1Block = async ( 169 | props: GetFirstBlockForL1BlockProps 170 | ) => { 171 | const arbProvider = new ArbitrumProvider(props.arbitrumProvider) 172 | const currentArbitrumBlock = await arbProvider.getBlockNumber() 173 | 174 | if (!props.maxArbitrumBlock || props.maxArbitrumBlock === 'latest') { 175 | props.maxArbitrumBlock = currentArbitrumBlock 176 | } 177 | 178 | const result = await Promise.all([ 179 | getFirstBlockForL1Block({ ...props, allowGreater: false }), 180 | getFirstBlockForL1Block({ 181 | ...props, 182 | forL1Block: props.forL1Block + 1, 183 | allowGreater: true, 184 | }), 185 | ]) 186 | 187 | if (!result[0]) { 188 | // If there's no start of the range, there won't be the end either. 189 | return [undefined, undefined] 190 | } 191 | 192 | if (result[0] && result[1]) { 193 | // If both results are defined, we can assume that the previous Arbitrum block for the end of the range will be for 'forL1Block'. 194 | return [result[0], result[1] - 1] 195 | } 196 | 197 | return [result[0], props.maxArbitrumBlock] 198 | } 199 | 200 | export async function getNativeTokenDecimals({ 201 | parentProvider, 202 | childNetwork, 203 | }: { 204 | parentProvider: Provider 205 | childNetwork: ArbitrumNetwork 206 | }) { 207 | const nativeTokenAddress = childNetwork.nativeToken 208 | 209 | if (!nativeTokenAddress || nativeTokenAddress === constants.AddressZero) { 210 | return 18 211 | } 212 | 213 | const nativeTokenContract = ERC20__factory.connect( 214 | nativeTokenAddress, 215 | parentProvider 216 | ) 217 | 218 | try { 219 | return await nativeTokenContract.decimals() 220 | } catch { 221 | return 0 222 | } 223 | } 224 | 225 | export function scaleFrom18DecimalsToNativeTokenDecimals({ 226 | amount, 227 | decimals, 228 | }: { 229 | amount: BigNumber 230 | decimals: number 231 | }) { 232 | // do nothing for 18 decimals 233 | if (decimals === 18) { 234 | return amount 235 | } 236 | 237 | if (decimals < 18) { 238 | const scaledAmount = amount.div( 239 | BigNumber.from(10).pow(BigNumber.from(18 - decimals)) 240 | ) 241 | // round up if necessary 242 | if ( 243 | scaledAmount 244 | .mul(BigNumber.from(10).pow(BigNumber.from(18 - decimals))) 245 | .lt(amount) 246 | ) { 247 | return scaledAmount.add(BigNumber.from(1)) 248 | } 249 | return scaledAmount 250 | } 251 | 252 | // decimals > 18 253 | return amount.mul(BigNumber.from(10).pow(BigNumber.from(decimals - 18))) 254 | } 255 | 256 | export function scaleFromNativeTokenDecimalsTo18Decimals({ 257 | amount, 258 | decimals, 259 | }: { 260 | amount: BigNumber 261 | decimals: number 262 | }) { 263 | if (decimals < 18) { 264 | return amount.mul(BigNumber.from(10).pow(18 - decimals)) 265 | } else if (decimals > 18) { 266 | return amount.div(BigNumber.from(10).pow(decimals - 18)) 267 | } 268 | 269 | return amount 270 | } 271 | -------------------------------------------------------------------------------- /packages/sdk/src/lib/utils/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Omit doesnt enforce that the seconds generic is a keyof the first 3 | * OmitTyped guards against the underlying type prop names 4 | * being refactored, and not being updated in the usage of OmitTyped 5 | */ 6 | export type OmitTyped = Omit 7 | 8 | /** 9 | * Make the specified properties optional 10 | */ 11 | export type PartialPick = OmitTyped & Partial 12 | 13 | /** 14 | * Make the specified properties required 15 | */ 16 | export type RequiredPick = Required> & T 17 | 18 | // https://twitter.com/mattpocockuk/status/1622730173446557697 19 | export type Prettify = { 20 | [K in keyof T]: T[K] 21 | // eslint-disable-next-line @typescript-eslint/ban-types 22 | } & {} 23 | -------------------------------------------------------------------------------- /packages/sdk/tests/fork/inbox.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, Offchain Labs, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /* eslint-env node */ 17 | 'use strict' 18 | 19 | import { expect } from 'chai' 20 | 21 | import { BigNumber } from '@ethersproject/bignumber' 22 | import { Logger, LogLevel } from '@ethersproject/logger' 23 | Logger.setLogLevel(LogLevel.ERROR) 24 | 25 | import { Bridge__factory } from '../../src/lib/abi/factories/Bridge__factory' 26 | import { Inbox__factory } from '../../src/lib/abi/factories/Inbox__factory' 27 | import { SequencerInbox__factory } from '../../src/lib/abi/factories/SequencerInbox__factory' 28 | 29 | import { InboxTools } from '../../src' 30 | 31 | import { ethers, network } from 'hardhat' 32 | import { hexZeroPad } from '@ethersproject/bytes' 33 | import { 34 | ArbitrumNetwork, 35 | getArbitrumNetwork, 36 | } from '../../src/lib/dataEntities/networks' 37 | import { solidityKeccak256 } from 'ethers/lib/utils' 38 | import { ContractTransaction, Signer } from 'ethers' 39 | 40 | const submitL2Tx = async ( 41 | tx: { 42 | to: string 43 | value?: BigNumber 44 | data?: string 45 | nonce: number 46 | maxFeePerGas: BigNumber 47 | gasLimit: BigNumber 48 | }, 49 | l2Network: ArbitrumNetwork, 50 | l1Signer: Signer 51 | ): Promise => { 52 | const inbox = Inbox__factory.connect(l2Network.ethBridge.inbox, l1Signer) 53 | 54 | return await inbox.sendUnsignedTransaction( 55 | tx.gasLimit, 56 | tx.maxFeePerGas, 57 | tx.nonce, 58 | tx.to, 59 | tx.value || BigNumber.from(0), 60 | tx.data || '0x' 61 | ) 62 | } 63 | 64 | describe('Inbox tools', () => { 65 | const setup = async () => { 66 | const signers = await ethers.getSigners() 67 | const signer = signers[0] 68 | const provider = signer.provider! 69 | 70 | const arbitrumOne = await getArbitrumNetwork(42161) 71 | 72 | const sequencerInbox = SequencerInbox__factory.connect( 73 | arbitrumOne.ethBridge.sequencerInbox, 74 | provider 75 | ) 76 | 77 | const bridge = Bridge__factory.connect( 78 | arbitrumOne.ethBridge.bridge, 79 | provider 80 | ) 81 | 82 | return { 83 | l1Signer: signer, 84 | l1Provider: provider, 85 | l2Network: arbitrumOne, 86 | sequencerInbox, 87 | bridge, 88 | } 89 | } 90 | 91 | let forkBlockNumber: number 92 | let forkProviderUrl: string 93 | before(async () => { 94 | const { l1Provider } = await setup() 95 | forkBlockNumber = await l1Provider.getBlockNumber() 96 | forkProviderUrl = 'http://localhost:8545' 97 | }) 98 | 99 | const resetFork = async (blockNumber: number, jsonRpcUrl: string) => { 100 | await network.provider.request({ 101 | method: 'hardhat_reset', 102 | params: [{ forking: { jsonRpcUrl, blockNumber } }], 103 | }) 104 | } 105 | 106 | beforeEach(async () => { 107 | // we reset the fork between each test so that the tests don't 108 | // interfere with one another 109 | await resetFork(forkBlockNumber, forkProviderUrl) 110 | }) 111 | 112 | it('can force include', async () => { 113 | const { l1Signer, l2Network, sequencerInbox, bridge } = await setup() 114 | 115 | const inboxTools = new InboxTools(l1Signer, l2Network) 116 | const startInboxLength = await bridge.messageCount() 117 | const l2Tx = await submitL2Tx( 118 | { 119 | to: await l1Signer.getAddress(), 120 | value: BigNumber.from(0), 121 | gasLimit: BigNumber.from(100000), 122 | maxFeePerGas: BigNumber.from(21000000000), 123 | nonce: 0, 124 | }, 125 | l2Network, 126 | l1Signer 127 | ) 128 | await l2Tx.wait() 129 | 130 | const block = await l1Signer.provider!.getBlock('latest') 131 | await mineBlocks(6600, block.timestamp) 132 | 133 | const forceInclusionTx = await inboxTools.forceInclude() 134 | 135 | expect(forceInclusionTx, 'Null force inclusion').to.not.be.null 136 | await forceInclusionTx!.wait() 137 | 138 | const messagesReadAfter = await sequencerInbox.totalDelayedMessagesRead() 139 | 140 | expect(messagesReadAfter.toNumber(), 'Message not read').to.eq( 141 | startInboxLength.add(1).toNumber() 142 | ) 143 | }) 144 | 145 | it('can force include many', async () => { 146 | const { l1Signer, l2Network, sequencerInbox, bridge } = await setup() 147 | 148 | const startInboxLength = await bridge.messageCount() 149 | const l2Tx1 = await submitL2Tx( 150 | { 151 | to: await l1Signer.getAddress(), 152 | value: BigNumber.from(0), 153 | gasLimit: BigNumber.from(100000), 154 | maxFeePerGas: BigNumber.from(21000000000), 155 | nonce: 0, 156 | }, 157 | l2Network, 158 | l1Signer 159 | ) 160 | await l2Tx1.wait() 161 | 162 | const l2Tx2 = await submitL2Tx( 163 | { 164 | to: await l1Signer.getAddress(), 165 | value: BigNumber.from(10), 166 | gasLimit: BigNumber.from(100000), 167 | maxFeePerGas: BigNumber.from(21000000000), 168 | nonce: 1, 169 | }, 170 | l2Network, 171 | l1Signer 172 | ) 173 | await l2Tx2.wait() 174 | 175 | const block = await l1Signer.provider!.getBlock('latest') 176 | await mineBlocks(6600, block.timestamp) 177 | 178 | const inboxTools = new InboxTools(l1Signer, l2Network) 179 | const forceInclusionTx = await inboxTools.forceInclude() 180 | 181 | expect(forceInclusionTx, 'Null force inclusion').to.not.be.null 182 | await forceInclusionTx!.wait() 183 | 184 | const messagesReadAfter = await sequencerInbox.totalDelayedMessagesRead() 185 | expect(messagesReadAfter.toNumber(), 'Message not read').to.eq( 186 | startInboxLength.add(2).toNumber() 187 | ) 188 | }) 189 | 190 | it('does find eligible events', async () => { 191 | const { l1Signer, l2Network } = await setup() 192 | const inboxTools = new InboxTools(l1Signer, l2Network) 193 | 194 | const l2Tx1 = await submitL2Tx( 195 | { 196 | to: await l1Signer.getAddress(), 197 | value: BigNumber.from(5), 198 | gasLimit: BigNumber.from(100000), 199 | maxFeePerGas: BigNumber.from(21000000000), 200 | nonce: 0, 201 | }, 202 | l2Network, 203 | l1Signer 204 | ) 205 | await l2Tx1.wait() 206 | const txParams = { 207 | to: await l1Signer.getAddress(), 208 | value: BigNumber.from(10), 209 | gasLimit: BigNumber.from(100000), 210 | maxFeePerGas: BigNumber.from(21000000000), 211 | nonce: 1, 212 | } 213 | const messageDataHash = solidityKeccak256( 214 | ['uint8', 'uint256', 'uint256', 'uint256', 'uint256', 'uint256', 'bytes'], 215 | [ 216 | 0, 217 | txParams.gasLimit, 218 | txParams.maxFeePerGas, 219 | txParams.nonce, 220 | hexZeroPad(txParams.to, 32), 221 | txParams.value, 222 | '0x', 223 | ] 224 | ) 225 | const l2Tx2 = await submitL2Tx(txParams, l2Network, l1Signer) 226 | await l2Tx2.wait() 227 | 228 | const block = await l1Signer.provider!.getBlock('latest') 229 | await mineBlocks(6600, block.timestamp) 230 | 231 | const event = await inboxTools.getForceIncludableEvent() 232 | expect(event?.event.messageDataHash, 'Invalid message hash.').to.eq( 233 | messageDataHash 234 | ) 235 | }) 236 | 237 | it('doesnt find non-eligible events', async () => { 238 | const { l1Signer, l2Network } = await setup() 239 | const inboxTools = new InboxTools(l1Signer, l2Network) 240 | 241 | const event = await inboxTools.getForceIncludableEvent() 242 | expect(event, 'Event not null').to.be.null 243 | }) 244 | 245 | const mineBlocks = async ( 246 | count: number, 247 | startTimestamp?: number, 248 | timeDiffPerBlock = 14 249 | ) => { 250 | let timestamp = startTimestamp 251 | for (let i = 0; i < count; i++) { 252 | timestamp = Math.max( 253 | Math.floor(Date.now() / 1000) + (timeDiffPerBlock || 1), 254 | (timestamp || 0) + (timeDiffPerBlock || 1) 255 | ) 256 | await network.provider.send('evm_mine', [timestamp]) 257 | } 258 | } 259 | }) 260 | -------------------------------------------------------------------------------- /packages/sdk/tests/integration/childTransactionReceipt.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, Offchain Labs, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /* eslint-env node */ 17 | 'use strict' 18 | 19 | import { expect } from 'chai' 20 | 21 | import { 22 | fundParentSigner, 23 | fundChildSigner, 24 | mineUntilStop, 25 | skipIfMainnet, 26 | wait, 27 | } from './testHelpers' 28 | import { ChildTransactionReceipt } from '../../src' 29 | import { JsonRpcProvider } from '@ethersproject/providers' 30 | import { BigNumber, Wallet } from 'ethers' 31 | import { parseEther } from 'ethers/lib/utils' 32 | import { testSetup } from '../testSetup' 33 | 34 | async function waitForL1BatchConfirmations( 35 | arbTxReceipt: ChildTransactionReceipt, 36 | l2Provider: JsonRpcProvider, 37 | timeoutMs: number 38 | ) { 39 | let polls = 0 40 | let l1BatchConfirmations = 0 41 | 42 | const MAX_POLLS = 10 43 | 44 | while (polls < MAX_POLLS) { 45 | l1BatchConfirmations = ( 46 | await arbTxReceipt.getBatchConfirmations(l2Provider) 47 | ).toNumber() 48 | 49 | // exit out of the while loop after fetching a non-zero number of batch confirmations 50 | if (l1BatchConfirmations !== 0) { 51 | break 52 | } 53 | 54 | // otherwise, increment the number of polls and wait 55 | polls += 1 56 | await wait(timeoutMs / MAX_POLLS) 57 | } 58 | 59 | return l1BatchConfirmations 60 | } 61 | 62 | describe('ArbProvider', () => { 63 | beforeEach('skipIfMainnet', async function () { 64 | await skipIfMainnet(this) 65 | }) 66 | 67 | it('does find l1 batch info', async () => { 68 | const { childSigner, parentSigner } = await testSetup() 69 | const l2Provider = childSigner.provider! as JsonRpcProvider 70 | 71 | // set up miners 72 | const miner1 = Wallet.createRandom().connect(parentSigner.provider!) 73 | const miner2 = Wallet.createRandom().connect(childSigner.provider!) 74 | await fundParentSigner(miner1, parseEther('0.1')) 75 | await fundChildSigner(miner2, parseEther('0.1')) 76 | const state = { mining: true } 77 | mineUntilStop(miner1, state) 78 | mineUntilStop(miner2, state) 79 | 80 | await fundChildSigner(childSigner) 81 | const randomAddress = Wallet.createRandom().address 82 | const amountToSend = parseEther('0.000005') 83 | 84 | // send an l2 transaction, and get the receipt 85 | const tx = await childSigner.sendTransaction({ 86 | to: randomAddress, 87 | value: amountToSend, 88 | }) 89 | const rec = await tx.wait() 90 | 91 | // wait for the batch data 92 | // eslint-disable-next-line no-constant-condition 93 | while (true) { 94 | await wait(300) 95 | const arbTxReceipt = new ChildTransactionReceipt(rec) 96 | 97 | const l1BatchNumber = ( 98 | await arbTxReceipt.getBatchNumber(l2Provider).catch(() => { 99 | // findBatchContainingBlock errors if block number does not exist 100 | return BigNumber.from(0) 101 | }) 102 | ).toNumber() 103 | 104 | if (l1BatchNumber && l1BatchNumber > 0) { 105 | const l1BatchConfirmations = await waitForL1BatchConfirmations( 106 | arbTxReceipt, 107 | l2Provider, 108 | // for L3s, we also have to wait for the batch to land on L1, so we poll for max 60s until that happens 109 | 60_000 110 | ) 111 | 112 | expect(l1BatchConfirmations, 'missing confirmations').to.be.gt(0) 113 | 114 | if (l1BatchConfirmations > 8) { 115 | break 116 | } 117 | } 118 | } 119 | 120 | state.mining = false 121 | }) 122 | }) 123 | -------------------------------------------------------------------------------- /packages/sdk/tests/integration/custom-fee-token/customFeeTokenTestHelpers.ts: -------------------------------------------------------------------------------- 1 | import { StaticJsonRpcProvider } from '@ethersproject/providers' 2 | import { Signer, Wallet, ethers, utils } from 'ethers' 3 | 4 | import { 5 | testSetup as _testSetup, 6 | config, 7 | getLocalNetworksFromFile, 8 | } from '../../testSetup' 9 | import { Erc20Bridger, EthBridger } from '../../../src' 10 | import { ERC20__factory } from '../../../src/lib/abi/factories/ERC20__factory' 11 | import { getNativeTokenDecimals } from '../../../src/lib/utils/lib' 12 | 13 | // `config` isn't initialized yet, so we have to wrap these in functions 14 | const ethProvider = () => new StaticJsonRpcProvider(config.ethUrl) 15 | const arbProvider = () => new StaticJsonRpcProvider(config.arbUrl) 16 | const localNetworks = () => getLocalNetworksFromFile() 17 | 18 | export function isArbitrumNetworkWithCustomFeeToken(): boolean { 19 | const nt = localNetworks().l3Network?.nativeToken 20 | return typeof nt !== 'undefined' && nt !== ethers.constants.AddressZero 21 | } 22 | 23 | export async function testSetup() { 24 | const result = await _testSetup() 25 | const { childChain, parentProvider } = result 26 | 27 | const nativeToken = childChain.nativeToken! 28 | const nativeTokenContract = ERC20__factory.connect( 29 | nativeToken, 30 | parentProvider 31 | ) 32 | 33 | return { ...result, nativeTokenContract } 34 | } 35 | 36 | export async function fundParentCustomFeeToken( 37 | parentSignerOrAddress: Signer | string 38 | ) { 39 | const nativeToken = localNetworks().l3Network?.nativeToken 40 | const address = 41 | typeof parentSignerOrAddress === 'string' 42 | ? parentSignerOrAddress 43 | : await parentSignerOrAddress.getAddress() 44 | 45 | if (typeof nativeToken === 'undefined') { 46 | throw new Error( 47 | `can't call "fundParentCustomFeeToken" for network that uses eth as native token` 48 | ) 49 | } 50 | 51 | const deployerWallet = new Wallet( 52 | utils.sha256(utils.toUtf8Bytes('user_fee_token_deployer')), 53 | ethProvider() 54 | ) 55 | 56 | const tokenContract = ERC20__factory.connect(nativeToken, deployerWallet) 57 | const decimals = await tokenContract.decimals() 58 | 59 | const tx = await tokenContract.transfer( 60 | address, 61 | utils.parseUnits('10', decimals) 62 | ) 63 | await tx.wait() 64 | } 65 | 66 | export async function approveParentCustomFeeToken(parentSigner: Signer) { 67 | const ethBridger = await EthBridger.fromProvider(arbProvider()) 68 | 69 | const tx = await ethBridger.approveGasToken({ parentSigner }) 70 | await tx.wait() 71 | } 72 | 73 | export async function getParentCustomFeeTokenAllowance( 74 | owner: string, 75 | spender: string 76 | ) { 77 | const nativeToken = localNetworks().l3Network?.nativeToken 78 | const nativeTokenContract = ERC20__factory.connect( 79 | nativeToken!, 80 | ethProvider() 81 | ) 82 | return nativeTokenContract.allowance(owner, spender) 83 | } 84 | 85 | export async function approveParentCustomFeeTokenForErc20Deposit( 86 | parentSigner: Signer, 87 | erc20ParentAddress: string 88 | ) { 89 | const erc20Bridger = await Erc20Bridger.fromProvider(arbProvider()) 90 | 91 | const tx = await erc20Bridger.approveGasToken({ 92 | erc20ParentAddress: erc20ParentAddress, 93 | parentSigner, 94 | }) 95 | await tx.wait() 96 | } 97 | 98 | export async function fundChildCustomFeeToken(childSigner: Signer) { 99 | const deployerWallet = new Wallet(config.arbKey, arbProvider()) 100 | 101 | const decimals = await getNativeTokenDecimals({ 102 | parentProvider: ethProvider(), 103 | childNetwork: localNetworks().l2Network, 104 | }) 105 | 106 | const tx = await deployerWallet.sendTransaction({ 107 | to: await childSigner.getAddress(), 108 | value: utils.parseUnits('1', decimals), 109 | }) 110 | await tx.wait() 111 | } 112 | -------------------------------------------------------------------------------- /packages/sdk/tests/integration/custom-fee-token/mochaExtensions.ts: -------------------------------------------------------------------------------- 1 | import { isArbitrumNetworkWithCustomFeeToken } from './customFeeTokenTestHelpers' 2 | 3 | const customGasTokenEnvironment = isArbitrumNetworkWithCustomFeeToken() 4 | 5 | /** 6 | * Only run when in an eth chain environment 7 | */ 8 | export const describeOnlyWhenEth = customGasTokenEnvironment 9 | ? describe.skip 10 | : describe 11 | 12 | /** 13 | * Only run when in a custom gas token chain environment 14 | */ 15 | export const describeOnlyWhenCustomGasToken = customGasTokenEnvironment 16 | ? describe 17 | : describe.skip 18 | 19 | /** 20 | * Only run when in an eth chain environment 21 | */ 22 | export const itOnlyWhenEth = customGasTokenEnvironment ? it.skip : it 23 | 24 | /** 25 | * Only run when in a custom gas token chain environment 26 | */ 27 | export const itOnlyWhenCustomGasToken = customGasTokenEnvironment ? it : it.skip 28 | -------------------------------------------------------------------------------- /packages/sdk/tests/integration/getArbitrumNetworkInformationFromRollup.test.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcProvider } from '@ethersproject/providers' 2 | import { constants } from 'ethers' 3 | import { expect } from 'chai' 4 | import { loadEnv } from '../../src/lib/utils/env' 5 | 6 | import { 7 | getArbitrumNetwork, 8 | getArbitrumNetworkInformationFromRollup, 9 | } from '../../src/lib/dataEntities/networks' 10 | 11 | loadEnv() 12 | 13 | describe('getArbitrumNetworkInformationFromRollup', () => { 14 | it('fetches information about arbitrum one', async () => { 15 | const arb1 = getArbitrumNetwork(42161) 16 | const ethProvider = new JsonRpcProvider( 17 | process.env['MAINNET_RPC'] as string 18 | ) 19 | 20 | const { parentChainId, confirmPeriodBlocks, ethBridge, nativeToken } = 21 | await getArbitrumNetworkInformationFromRollup( 22 | arb1.ethBridge.rollup, 23 | ethProvider 24 | ) 25 | 26 | expect(parentChainId, 'parentChainId is not correct').to.eq( 27 | arb1.parentChainId 28 | ) 29 | 30 | expect(confirmPeriodBlocks, 'confirmPeriodBlocks is not correct').to.eq( 31 | arb1.confirmPeriodBlocks 32 | ) 33 | 34 | const { bridge, inbox, sequencerInbox, outbox, rollup } = ethBridge 35 | const arb1EthBridge = arb1.ethBridge 36 | 37 | expect(bridge, 'Bridge contract is not correct').to.eq(arb1EthBridge.bridge) 38 | expect(inbox, 'Inbox contract is not correct').to.eq(arb1EthBridge.inbox) 39 | expect(sequencerInbox, 'SequencerInbox contract is not correct').to.eq( 40 | arb1EthBridge.sequencerInbox 41 | ) 42 | expect(outbox, 'Outbox contract is not correct').to.eq(arb1EthBridge.outbox) 43 | expect(rollup, 'Rollup contract is not correct').to.eq(arb1EthBridge.rollup) 44 | 45 | expect(nativeToken, 'Native token is not correct').to.eq( 46 | constants.AddressZero 47 | ) 48 | }) 49 | 50 | it('fetches information about xai', async () => { 51 | const { parentChainId, confirmPeriodBlocks, ethBridge, nativeToken } = 52 | await getArbitrumNetworkInformationFromRollup( 53 | '0xC47DacFbAa80Bd9D8112F4e8069482c2A3221336', 54 | new JsonRpcProvider('https://arb1.arbitrum.io/rpc') 55 | ) 56 | 57 | expect(parentChainId, 'parentChainId is not correct').to.eq(42161) 58 | 59 | expect(confirmPeriodBlocks, 'confirmPeriodBlocks is not correct').to.eq( 60 | 45818 61 | ) 62 | 63 | const { bridge, inbox, sequencerInbox, outbox, rollup } = ethBridge 64 | 65 | expect(bridge, 'Bridge contract is not correct').to.eq( 66 | '0x7dd8A76bdAeBE3BBBaCD7Aa87f1D4FDa1E60f94f' 67 | ) 68 | expect(inbox, 'Inbox contract is not correct').to.eq( 69 | '0xaE21fDA3de92dE2FDAF606233b2863782Ba046F9' 70 | ) 71 | expect(sequencerInbox, 'SequencerInbox contract is not correct').to.eq( 72 | '0x995a9d3ca121D48d21087eDE20bc8acb2398c8B1' 73 | ) 74 | expect(outbox, 'Outbox contract is not correct').to.eq( 75 | '0x1E400568AD4840dbE50FB32f306B842e9ddeF726' 76 | ) 77 | expect(rollup, 'Rollup contract is not correct').to.eq( 78 | '0xC47DacFbAa80Bd9D8112F4e8069482c2A3221336' 79 | ) 80 | 81 | expect(nativeToken, 'Native token is not correct').to.eq( 82 | '0x4Cb9a7AE498CEDcBb5EAe9f25736aE7d428C9D66' 83 | ) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /packages/sdk/tests/integration/parentToChildMessageCreator.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, Offchain Labs, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /* eslint-env node */ 17 | 'use strict' 18 | 19 | import { expect } from 'chai' 20 | import { providers, utils } from 'ethers' 21 | import { fundParentSigner, skipIfMainnet } from './testHelpers' 22 | import { testSetup } from '../testSetup' 23 | import { ParentToChildMessageCreator } from '../../src/lib/message/ParentToChildMessageCreator' 24 | import { ParentToChildMessageStatus } from '../../src/lib/message/ParentToChildMessage' 25 | 26 | import { 27 | fundParentCustomFeeToken, 28 | approveParentCustomFeeToken, 29 | isArbitrumNetworkWithCustomFeeToken, 30 | } from './custom-fee-token/customFeeTokenTestHelpers' 31 | 32 | describe('ParentToChildMessageCreator', () => { 33 | beforeEach('skipIfMainnet', async function () { 34 | await skipIfMainnet(this) 35 | }) 36 | 37 | // Testing amount 38 | const testAmount = utils.parseEther('0.01') 39 | 40 | it('allows the creation of Retryable Tickets sending parameters', async () => { 41 | const { parentSigner, childSigner } = await testSetup() 42 | const signerAddress = await parentSigner.getAddress() 43 | const arbProvider = childSigner.provider as providers.Provider 44 | 45 | // Funding parent chain wallet 46 | await fundParentSigner(parentSigner) 47 | 48 | if (isArbitrumNetworkWithCustomFeeToken()) { 49 | await fundParentCustomFeeToken(parentSigner) 50 | await approveParentCustomFeeToken(parentSigner) 51 | } 52 | 53 | // Instantiate the object 54 | const parentToChildMessageCreator = new ParentToChildMessageCreator( 55 | parentSigner 56 | ) 57 | 58 | // Getting balances 59 | const initialChildChainBalance = await childSigner.getBalance() 60 | 61 | // Define parameters for Retryable 62 | const retryableTicketParams = { 63 | from: signerAddress, 64 | to: signerAddress, 65 | l2CallValue: testAmount, 66 | callValueRefundAddress: signerAddress, 67 | data: '0x', 68 | } 69 | 70 | // And submitting the ticket 71 | const parentSubmissionTx = 72 | await parentToChildMessageCreator.createRetryableTicket( 73 | retryableTicketParams, 74 | arbProvider 75 | ) 76 | const parentSubmissionTxReceipt = await parentSubmissionTx.wait() 77 | 78 | // Getting the ParentToChildMessage 79 | const parentToChildMessages = 80 | await parentSubmissionTxReceipt.getParentToChildMessages(arbProvider) 81 | expect(parentToChildMessages.length).to.eq(1) 82 | const parentToChildMessage = parentToChildMessages[0] 83 | 84 | // And waiting for it to be redeemed 85 | const retryableTicketResult = await parentToChildMessage.waitForStatus() 86 | expect(retryableTicketResult.status).to.eq( 87 | ParentToChildMessageStatus.REDEEMED 88 | ) 89 | 90 | // Getting and checking updated balances 91 | const finalChildChainBalance = await childSigner.getBalance() 92 | 93 | // When sending ETH through retryables, the same address will receive the ETH sent through the callvalue 94 | // plus any gas that was not used in the operation. 95 | expect( 96 | initialChildChainBalance.add(testAmount).lt(finalChildChainBalance), 97 | 'Child chain balance not updated' 98 | ).to.be.true 99 | }) 100 | 101 | it('allows the creation of Retryable Tickets sending a request', async () => { 102 | const { parentSigner, childSigner } = await testSetup() 103 | const signerAddress = await parentSigner.getAddress() 104 | const ethProvider = parentSigner.provider as providers.Provider 105 | const arbProvider = childSigner.provider as providers.Provider 106 | 107 | // Funding parent chain wallet 108 | await fundParentSigner(parentSigner) 109 | 110 | if (isArbitrumNetworkWithCustomFeeToken()) { 111 | await fundParentCustomFeeToken(parentSigner) 112 | await approveParentCustomFeeToken(parentSigner) 113 | } 114 | 115 | // Instantiate the object 116 | const parentToChildMessageCreator = new ParentToChildMessageCreator( 117 | parentSigner 118 | ) 119 | 120 | // Getting balances 121 | const initialChildChainBalance = await childSigner.getBalance() 122 | 123 | // In this case, we will try to send directly a ParentToChildTransactionRequest 124 | const parentToChildTransactionRequestParams = { 125 | from: signerAddress, 126 | to: signerAddress, 127 | l2CallValue: testAmount, 128 | callValueRefundAddress: signerAddress, 129 | data: '0x', 130 | } 131 | 132 | const parentToChildTransactionRequest = 133 | await ParentToChildMessageCreator.getTicketCreationRequest( 134 | parentToChildTransactionRequestParams, 135 | ethProvider, 136 | arbProvider 137 | ) 138 | 139 | // And create the retryable ticket 140 | const parentSubmissionTx = 141 | await parentToChildMessageCreator.createRetryableTicket( 142 | parentToChildTransactionRequest, 143 | arbProvider 144 | ) 145 | const parentSubmissionTxReceipt = await parentSubmissionTx.wait() 146 | 147 | // Getting the ParentToChildMessage 148 | const parentToChildMessages = 149 | await parentSubmissionTxReceipt.getParentToChildMessages(arbProvider) 150 | expect(parentToChildMessages.length).to.eq(1) 151 | const parentToChildMessage = parentToChildMessages[0] 152 | 153 | // And waiting for it to be redeemed 154 | const retryableTicketResult = await parentToChildMessage.waitForStatus() 155 | expect(retryableTicketResult.status).to.eq( 156 | ParentToChildMessageStatus.REDEEMED 157 | ) 158 | 159 | // Getting and checking updated balances 160 | const finalChildChainBalance = await childSigner.getBalance() 161 | 162 | // When sending ETH through retryables, the same address will receive the ETH sent through the callvalue 163 | // plus any gas that was not used in the operation. 164 | expect( 165 | initialChildChainBalance.add(testAmount).lt(finalChildChainBalance), 166 | 'Child chain balance not updated' 167 | ).to.be.true 168 | }) 169 | }) 170 | -------------------------------------------------------------------------------- /packages/sdk/tests/integration/parentToChildMessageGasEstimator.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, Offchain Labs, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /* eslint-env node */ 17 | 'use strict' 18 | 19 | import { expect } from 'chai' 20 | import { BigNumber } from 'ethers' 21 | 22 | import { skipIfMainnet } from './testHelpers' 23 | import { testSetup } from '../testSetup' 24 | import { ParentToChildMessageGasEstimator } from '../../src/lib/message/ParentToChildMessageGasEstimator' 25 | import { 26 | itOnlyWhenEth, 27 | itOnlyWhenCustomGasToken, 28 | } from './custom-fee-token/mochaExtensions' 29 | 30 | describe('ParentToChildMessageGasEstimator', () => { 31 | beforeEach('skipIfMainnet', async function () { 32 | await skipIfMainnet(this) 33 | }) 34 | 35 | itOnlyWhenEth( 36 | `"estimateSubmissionFee" returns non-0 for eth chain`, 37 | async () => { 38 | const { parentProvider, childProvider } = await testSetup() 39 | 40 | const submissionFee = await new ParentToChildMessageGasEstimator( 41 | childProvider 42 | ).estimateSubmissionFee( 43 | parentProvider, 44 | await parentProvider.getGasPrice(), 45 | 123456 46 | ) 47 | 48 | expect(submissionFee.toString()).to.not.eq(BigNumber.from(0).toString()) 49 | } 50 | ) 51 | 52 | itOnlyWhenCustomGasToken( 53 | `"estimateSubmissionFee" returns 0 for custom gas token chain`, 54 | async () => { 55 | const { parentProvider, childProvider } = await testSetup() 56 | 57 | const submissionFee = await new ParentToChildMessageGasEstimator( 58 | childProvider 59 | ).estimateSubmissionFee( 60 | parentProvider, 61 | await parentProvider.getGasPrice(), 62 | 123456 63 | ) 64 | 65 | expect(submissionFee.toString()).to.eq(BigNumber.from(0).toString()) 66 | } 67 | ) 68 | }) 69 | -------------------------------------------------------------------------------- /packages/sdk/tests/integration/sanity.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, Offchain Labs, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /* eslint-env node */ 17 | 'use strict' 18 | 19 | import { expect } from 'chai' 20 | 21 | import { AeWETH__factory } from '../../src/lib/abi/factories/AeWETH__factory' 22 | import { L2GatewayRouter__factory } from '../../src/lib/abi/factories/L2GatewayRouter__factory' 23 | import { L2WethGateway__factory } from '../../src/lib/abi/factories/L2WethGateway__factory' 24 | import { L1WethGateway__factory } from '../../src/lib/abi/factories/L1WethGateway__factory' 25 | import { L2CustomGateway__factory } from '../../src/lib/abi/factories/L2CustomGateway__factory' 26 | import { L1CustomGateway__factory } from '../../src/lib/abi/factories/L1CustomGateway__factory' 27 | import { L2ERC20Gateway__factory } from '../../src/lib/abi/factories/L2ERC20Gateway__factory' 28 | import { L1ERC20Gateway__factory } from '../../src/lib/abi/factories/L1ERC20Gateway__factory' 29 | 30 | import { testSetup } from '../testSetup' 31 | import { randomBytes, hexlify } from 'ethers/lib/utils' 32 | import { itOnlyWhenEth } from './custom-fee-token/mochaExtensions' 33 | 34 | const expectIgnoreCase = (expected: string, actual: string) => { 35 | expect(expected.toLocaleLowerCase()).to.equal(actual.toLocaleLowerCase()) 36 | } 37 | 38 | describe('sanity checks (read-only)', async () => { 39 | it('standard gateways public storage vars properly set', async () => { 40 | const { parentSigner, childSigner, childChain } = await testSetup() 41 | const l1Gateway = await L1ERC20Gateway__factory.connect( 42 | childChain.tokenBridge.parentErc20Gateway, 43 | parentSigner 44 | ) 45 | const l2Gateway = await L2ERC20Gateway__factory.connect( 46 | childChain.tokenBridge.childErc20Gateway, 47 | childSigner 48 | ) 49 | 50 | const l1ClonableProxyHash = await l1Gateway.cloneableProxyHash() 51 | const l2ClonableProxyHash = await l2Gateway.cloneableProxyHash() 52 | expect(l1ClonableProxyHash).to.equal(l2ClonableProxyHash) 53 | 54 | const l1BeaconProxyHash = await l1Gateway.l2BeaconProxyFactory() 55 | const l2BeaconProxyHash = await l2Gateway.beaconProxyFactory() 56 | expect(l1BeaconProxyHash).to.equal(l2BeaconProxyHash) 57 | 58 | const l1GatewayCounterParty = await l1Gateway.counterpartGateway() 59 | expect(l1GatewayCounterParty).to.equal( 60 | childChain.tokenBridge.childErc20Gateway 61 | ) 62 | 63 | const l2GatewayCounterParty = await l2Gateway.counterpartGateway() 64 | expect(l2GatewayCounterParty).to.equal( 65 | childChain.tokenBridge.parentErc20Gateway 66 | ) 67 | 68 | const l1Router = await l1Gateway.router() 69 | expect(l1Router).to.equal(childChain.tokenBridge.parentGatewayRouter) 70 | 71 | const l2Router = await l2Gateway.router() 72 | expect(l2Router).to.equal(childChain.tokenBridge.childGatewayRouter) 73 | }) 74 | 75 | it('custom gateways public storage vars properly set', async () => { 76 | const { parentSigner, childSigner, childChain } = await testSetup() 77 | const l1Gateway = await L1CustomGateway__factory.connect( 78 | childChain.tokenBridge.parentCustomGateway, 79 | parentSigner 80 | ) 81 | const l2Gateway = await L2CustomGateway__factory.connect( 82 | childChain.tokenBridge.childCustomGateway, 83 | childSigner 84 | ) 85 | const l1GatewayCounterParty = await l1Gateway.counterpartGateway() 86 | expect(l1GatewayCounterParty).to.equal( 87 | childChain.tokenBridge.childCustomGateway 88 | ) 89 | 90 | const l2GatewayCounterParty = await l2Gateway.counterpartGateway() 91 | expect(l2GatewayCounterParty).to.equal( 92 | childChain.tokenBridge.parentCustomGateway 93 | ) 94 | 95 | const l1Router = await l1Gateway.router() 96 | expect(l1Router).to.equal(childChain.tokenBridge.parentGatewayRouter) 97 | 98 | const l2Router = await l2Gateway.router() 99 | expect(l2Router).to.equal(childChain.tokenBridge.childGatewayRouter) 100 | }) 101 | 102 | itOnlyWhenEth( 103 | 'weth gateways gateways public storage vars properly set', 104 | async () => { 105 | const { parentSigner, childSigner, childChain } = await testSetup() 106 | 107 | const l1Gateway = await L1WethGateway__factory.connect( 108 | childChain.tokenBridge.parentWethGateway, 109 | parentSigner 110 | ) 111 | const l2Gateway = await L2WethGateway__factory.connect( 112 | childChain.tokenBridge.childWethGateway, 113 | childSigner 114 | ) 115 | 116 | const parentWeth = await l1Gateway.l1Weth() 117 | expectIgnoreCase(parentWeth, childChain.tokenBridge.parentWeth) 118 | 119 | const childWeth = await l2Gateway.l2Weth() 120 | expectIgnoreCase(childWeth, childChain.tokenBridge.childWeth) 121 | 122 | const l1GatewayCounterParty = await l1Gateway.counterpartGateway() 123 | expectIgnoreCase( 124 | l1GatewayCounterParty, 125 | childChain.tokenBridge.childWethGateway 126 | ) 127 | 128 | const l2GatewayCounterParty = await l2Gateway.counterpartGateway() 129 | expectIgnoreCase( 130 | l2GatewayCounterParty, 131 | childChain.tokenBridge.parentWethGateway 132 | ) 133 | 134 | const l1Router = await l1Gateway.router() 135 | expectIgnoreCase(l1Router, childChain.tokenBridge.parentGatewayRouter) 136 | 137 | const l2Router = await l2Gateway.router() 138 | expectIgnoreCase(l2Router, childChain.tokenBridge.childGatewayRouter) 139 | } 140 | ) 141 | 142 | itOnlyWhenEth('aeWETh public vars properly set', async () => { 143 | const { childSigner, childChain } = await testSetup() 144 | 145 | const aeWeth = AeWETH__factory.connect( 146 | childChain.tokenBridge.childWeth, 147 | childSigner 148 | ) 149 | 150 | const l2GatewayOnAeWeth = await aeWeth.l2Gateway() 151 | expectIgnoreCase(l2GatewayOnAeWeth, childChain.tokenBridge.childWethGateway) 152 | 153 | const l1AddressOnAeWeth = await aeWeth.l1Address() 154 | expectIgnoreCase(l1AddressOnAeWeth, childChain.tokenBridge.parentWeth) 155 | }) 156 | 157 | itOnlyWhenEth('l1 gateway router points to right weth gateways', async () => { 158 | const { adminErc20Bridger, parentSigner, childChain } = await testSetup() 159 | 160 | const gateway = await adminErc20Bridger.getParentGatewayAddress( 161 | childChain.tokenBridge.parentWeth, 162 | parentSigner.provider! 163 | ) 164 | 165 | expect(gateway).to.equal(childChain.tokenBridge.parentWethGateway) 166 | }) 167 | 168 | it('parent and child chain implementations of calculateL2ERC20Address match', async () => { 169 | const { parentSigner, childSigner, childChain, erc20Bridger } = 170 | await testSetup() 171 | 172 | const address = hexlify(randomBytes(20)) 173 | 174 | const erc20ChildAddressAsPerParent = 175 | await erc20Bridger.getChildErc20Address(address, parentSigner.provider!) 176 | const childGatewayRouter = L2GatewayRouter__factory.connect( 177 | childChain.tokenBridge.childGatewayRouter, 178 | childSigner.provider! 179 | ) 180 | const erc20ChildAddressAsPerChild = 181 | await childGatewayRouter.calculateL2TokenAddress(address) 182 | 183 | expect(erc20ChildAddressAsPerChild).to.equal(erc20ChildAddressAsPerParent) 184 | }) 185 | }) 186 | -------------------------------------------------------------------------------- /packages/sdk/tests/integration/sendChildmsg.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, Offchain Labs, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /* eslint-env node */ 17 | // import { instantiateBridge } from './instantiate_bridge' 18 | ;('use strict') 19 | 20 | import { BigNumber, ethers, Signer } from 'ethers' 21 | import { InboxTools } from '../../src/lib/inbox/inbox' 22 | import { 23 | getArbitrumNetwork, 24 | ArbitrumNetwork, 25 | } from '../../src/lib/dataEntities/networks' 26 | import { testSetup } from '../testSetup' 27 | import { greeter } from './helper/greeter' 28 | import { expect } from 'chai' 29 | import { AdminErc20Bridger } from '../../src/lib/assetBridger/erc20Bridger' 30 | 31 | const sendSignedTx = async (testState: any, info?: any) => { 32 | const { parentDeployer, childDeployer } = testState 33 | const childChain = await getArbitrumNetwork(await childDeployer.getChainId()) 34 | const inbox = new InboxTools(parentDeployer, childChain) 35 | const message = { 36 | ...info, 37 | value: BigNumber.from(0), 38 | } 39 | const signedTx = await inbox.signChildTx(message, childDeployer) 40 | 41 | const parentTx = await inbox.sendChildSignedTx(signedTx) 42 | return { 43 | signedMsg: signedTx, 44 | parentTransactionReceipt: await parentTx?.wait(), 45 | } 46 | } 47 | 48 | describe('Send signedTx to child chain using inbox', async () => { 49 | // test globals 50 | let testState: { 51 | parentDeployer: Signer 52 | childDeployer: Signer 53 | adminErc20Bridger: AdminErc20Bridger 54 | childChain: ArbitrumNetwork 55 | } 56 | 57 | before('init', async () => { 58 | testState = await testSetup() 59 | }) 60 | 61 | it('can deploy contract', async () => { 62 | const childDeployer = testState.childDeployer 63 | const Greeter = new ethers.ContractFactory( 64 | greeter.abi, 65 | greeter.bytecode 66 | ).connect(childDeployer) 67 | 68 | const info = { 69 | value: BigNumber.from(0), 70 | } 71 | const contractCreationData = Greeter.getDeployTransaction(info) 72 | const { signedMsg, parentTransactionReceipt } = await sendSignedTx( 73 | testState, 74 | contractCreationData 75 | ) 76 | const parentStatus = parentTransactionReceipt?.status 77 | expect(parentStatus).to.equal(1, 'parent txn failed') 78 | const childTx = ethers.utils.parseTransaction(signedMsg) 79 | const childTxhash = childTx.hash! 80 | const childTxReceipt = await childDeployer.provider!.waitForTransaction( 81 | childTxhash 82 | ) 83 | const childStatus = childTxReceipt.status 84 | expect(childStatus).to.equal(1, 'child txn failed') 85 | const contractAddress = ethers.ContractFactory.getContractAddress({ 86 | from: childTx.from!, 87 | nonce: childTx.nonce, 88 | }) 89 | const greeterImp = Greeter.attach(contractAddress) 90 | const greetResult = await greeterImp.greet() 91 | expect(greetResult).to.equal('hello world', 'contract returns not expected') 92 | }) 93 | 94 | it('should confirm the same tx on child chain', async () => { 95 | const childDeployer = testState.childDeployer 96 | const info = { 97 | data: '0x12', 98 | to: await childDeployer.getAddress(), 99 | } 100 | const { signedMsg, parentTransactionReceipt: parentTransactionReceipt } = 101 | await sendSignedTx(testState, info) 102 | const parentStatus = parentTransactionReceipt?.status 103 | expect(parentStatus).to.equal(1) 104 | const childTxhash = ethers.utils.parseTransaction(signedMsg).hash! 105 | const childTxReceipt = await childDeployer.provider!.waitForTransaction( 106 | childTxhash 107 | ) 108 | const childStatus = childTxReceipt.status 109 | expect(childStatus).to.equal(1) 110 | }) 111 | 112 | it('send two tx share the same nonce but with different gas price, should confirm the one which gas price higher than child base price', async () => { 113 | const childDeployer = testState.childDeployer 114 | const currentNonce = await childDeployer.getTransactionCount() 115 | 116 | const lowFeeInfo = { 117 | data: '0x12', 118 | nonce: currentNonce, 119 | to: await childDeployer.getAddress(), 120 | maxFeePerGas: BigNumber.from(10000000), //0.01gwei 121 | maxPriorityFeePerGas: BigNumber.from(1000000), //0.001gwei 122 | } 123 | const lowFeeTx = await sendSignedTx(testState, lowFeeInfo) 124 | const lowFeeParentStatus = lowFeeTx.parentTransactionReceipt?.status 125 | expect(lowFeeParentStatus).to.equal(1) 126 | const info = { 127 | data: '0x12', 128 | to: await childDeployer.getAddress(), 129 | nonce: currentNonce, 130 | } 131 | const enoughFeeTx = await sendSignedTx(testState, info) 132 | const enoughFeeParentStatus = enoughFeeTx.parentTransactionReceipt?.status 133 | expect(enoughFeeParentStatus).to.equal(1) 134 | const childLowFeeTxhash = ethers.utils.parseTransaction(lowFeeTx.signedMsg) 135 | .hash! 136 | const childEnoughFeeTxhash = ethers.utils.parseTransaction( 137 | enoughFeeTx.signedMsg 138 | ).hash! 139 | 140 | const childTEnoughFeeReceipt = 141 | await childDeployer.provider!.waitForTransaction(childEnoughFeeTxhash) 142 | const childStatus = childTEnoughFeeReceipt.status 143 | expect(childStatus).to.equal(1) 144 | const res = await childDeployer.provider?.getTransactionReceipt( 145 | childLowFeeTxhash 146 | ) 147 | expect(res).to.be.null 148 | }) 149 | }) 150 | -------------------------------------------------------------------------------- /packages/sdk/tests/integration/weth.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, Offchain Labs, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /* eslint-env node */ 17 | 'use strict' 18 | 19 | import { expect } from 'chai' 20 | import { parseEther } from '@ethersproject/units' 21 | import { AeWETH__factory } from '../../src/lib/abi/factories/AeWETH__factory' 22 | import { 23 | fundParentSigner, 24 | fundChildSigner, 25 | skipIfMainnet, 26 | withdrawToken, 27 | GatewayType, 28 | depositToken, 29 | } from './testHelpers' 30 | import { ParentToChildMessageStatus } from '../../src' 31 | import { Wallet } from 'ethers' 32 | import { testSetup } from '../testSetup' 33 | import { ERC20__factory } from '../../src/lib/abi/factories/ERC20__factory' 34 | import { describeOnlyWhenEth } from './custom-fee-token/mochaExtensions' 35 | 36 | describeOnlyWhenEth('WETH', async () => { 37 | beforeEach('skipIfMainnet', async function () { 38 | await skipIfMainnet(this) 39 | }) 40 | 41 | it('deposit WETH', async () => { 42 | const { childChain, parentSigner, childSigner, erc20Bridger } = 43 | await testSetup() 44 | 45 | const parentWethAddress = childChain.tokenBridge.parentWeth 46 | 47 | const wethToWrap = parseEther('0.00001') 48 | const wethToDeposit = parseEther('0.0000001') 49 | 50 | await fundParentSigner(parentSigner, parseEther('1')) 51 | 52 | const childWETH = AeWETH__factory.connect( 53 | childChain.tokenBridge.childWeth, 54 | childSigner.provider! 55 | ) 56 | expect( 57 | (await childWETH.balanceOf(await childSigner.getAddress())).toString(), 58 | 'start balance weth' 59 | ).to.eq('0') 60 | 61 | const parentWETH = AeWETH__factory.connect(parentWethAddress, parentSigner) 62 | const res = await parentWETH.deposit({ 63 | value: wethToWrap, 64 | }) 65 | await res.wait() 66 | await depositToken({ 67 | depositAmount: wethToDeposit, 68 | parentTokenAddress: parentWethAddress, 69 | erc20Bridger, 70 | parentSigner, 71 | childSigner, 72 | expectedStatus: ParentToChildMessageStatus.REDEEMED, 73 | expectedGatewayType: GatewayType.WETH, 74 | }) 75 | 76 | const childWethGateway = await erc20Bridger.getChildGatewayAddress( 77 | parentWethAddress, 78 | childSigner.provider! 79 | ) 80 | expect(childWethGateway, 'child weth gateway').to.eq( 81 | childChain.tokenBridge.childWethGateway 82 | ) 83 | const childToken = erc20Bridger.getChildTokenContract( 84 | childSigner.provider!, 85 | childChain.tokenBridge.childWeth 86 | ) 87 | expect(childToken.address, 'child weth').to.eq( 88 | childChain.tokenBridge.childWeth 89 | ) 90 | 91 | // now try to withdraw the funds 92 | await fundChildSigner(childSigner) 93 | const childWeth = AeWETH__factory.connect(childToken.address, childSigner) 94 | const randomAddr = Wallet.createRandom().address 95 | await ( 96 | await childWeth.connect(childSigner).withdrawTo(randomAddr, wethToDeposit) 97 | ).wait() 98 | const afterBalance = await childSigner.provider!.getBalance(randomAddr) 99 | 100 | expect(afterBalance.toString(), 'balance after').to.eq( 101 | wethToDeposit.toString() 102 | ) 103 | }) 104 | 105 | it('withdraw WETH', async () => { 106 | const wethToWrap = parseEther('0.00001') 107 | const wethToWithdraw = parseEther('0.00000001') 108 | 109 | const { childChain, parentSigner, childSigner, erc20Bridger } = 110 | await testSetup() 111 | await fundParentSigner(parentSigner) 112 | await fundChildSigner(childSigner) 113 | 114 | const childWeth = AeWETH__factory.connect( 115 | childChain.tokenBridge.childWeth, 116 | childSigner 117 | ) 118 | const res = await childWeth.deposit({ 119 | value: wethToWrap, 120 | }) 121 | const rec = await res.wait() 122 | expect(rec.status).to.equal(1, 'deposit txn failed') 123 | 124 | await withdrawToken({ 125 | amount: wethToWithdraw, 126 | erc20Bridger: erc20Bridger, 127 | gatewayType: GatewayType.WETH, 128 | parentSigner: parentSigner, 129 | parentToken: ERC20__factory.connect( 130 | childChain.tokenBridge.parentWeth, 131 | parentSigner.provider! 132 | ), 133 | childSigner: childSigner, 134 | startBalance: wethToWrap, 135 | }) 136 | }) 137 | }) 138 | -------------------------------------------------------------------------------- /packages/sdk/tests/testSetup.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, Offchain Labs, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /* eslint-env node */ 17 | 'use strict' 18 | 19 | import { JsonRpcProvider } from '@ethersproject/providers' 20 | import { Wallet } from '@ethersproject/wallet' 21 | import { Provider } from '@ethersproject/abstract-provider' 22 | import { loadEnv } from '../src/lib/utils/env' 23 | 24 | import { EthBridger, InboxTools, Erc20Bridger } from '../src' 25 | import { 26 | ArbitrumNetwork, 27 | getArbitrumNetwork, 28 | registerCustomArbitrumNetwork, 29 | TokenBridge, 30 | assertArbitrumNetworkHasTokenBridge, 31 | } from '../src/lib/dataEntities/networks' 32 | import { Signer } from 'ethers' 33 | import { AdminErc20Bridger } from '../src/lib/assetBridger/erc20Bridger' 34 | import * as path from 'path' 35 | import * as fs from 'fs' 36 | import { ArbSdkError } from '../src/lib/dataEntities/errors' 37 | import { 38 | approveParentCustomFeeToken, 39 | fundParentCustomFeeToken, 40 | isArbitrumNetworkWithCustomFeeToken, 41 | } from './integration/custom-fee-token/customFeeTokenTestHelpers' 42 | import { fundParentSigner } from './integration/testHelpers' 43 | 44 | loadEnv() 45 | 46 | const isTestingOrbitChains = process.env.ORBIT_TEST === '1' 47 | 48 | /** 49 | * The RPC urls and private keys using during testing 50 | * 51 | * @note When the `ORBIT_TEST` env variable is `true`, we treat `ethUrl` as the L2 and `arbUrl` as the L3 52 | */ 53 | export const config = isTestingOrbitChains 54 | ? { 55 | arbUrl: process.env['ORBIT_URL'] as string, 56 | ethUrl: process.env['ARB_URL'] as string, 57 | arbKey: process.env['ORBIT_KEY'] as string, 58 | ethKey: process.env['ARB_KEY'] as string, 59 | } 60 | : { 61 | arbUrl: process.env['ARB_URL'] as string, 62 | ethUrl: process.env['ETH_URL'] as string, 63 | arbKey: process.env['ARB_KEY'] as string, 64 | ethKey: process.env['ETH_KEY'] as string, 65 | } 66 | 67 | export const getSigner = (provider: JsonRpcProvider, key?: string) => { 68 | if (!key && !provider) 69 | throw new ArbSdkError('Provide at least one of key or provider.') 70 | if (key) return new Wallet(key).connect(provider) 71 | else return provider.getSigner(0) 72 | } 73 | 74 | export const testSetup = async (): Promise<{ 75 | childChain: ArbitrumNetwork & { 76 | tokenBridge: TokenBridge 77 | } 78 | parentSigner: Signer 79 | childSigner: Signer 80 | parentProvider: Provider 81 | childProvider: Provider 82 | erc20Bridger: Erc20Bridger 83 | ethBridger: EthBridger 84 | adminErc20Bridger: AdminErc20Bridger 85 | inboxTools: InboxTools 86 | parentDeployer: Signer 87 | childDeployer: Signer 88 | }> => { 89 | const ethProvider = new JsonRpcProvider(config.ethUrl) 90 | const arbProvider = new JsonRpcProvider(config.arbUrl) 91 | 92 | const parentDeployer = getSigner(ethProvider, config.ethKey) 93 | const childDeployer = getSigner(arbProvider, config.arbKey) 94 | 95 | const seed = Wallet.createRandom() 96 | const parentSigner = seed.connect(ethProvider) 97 | const childSigner = seed.connect(arbProvider) 98 | 99 | let setChildChain: ArbitrumNetwork 100 | 101 | try { 102 | const l2Network = await getArbitrumNetwork(childDeployer) 103 | setChildChain = l2Network 104 | } catch (err) { 105 | const localNetworks = getLocalNetworksFromFile() 106 | // the networks havent been added yet 107 | // check if theres an existing network available 108 | const childChain = ( 109 | isTestingOrbitChains ? localNetworks.l3Network : localNetworks.l2Network 110 | ) as ArbitrumNetwork 111 | setChildChain = registerCustomArbitrumNetwork(childChain) 112 | } 113 | 114 | assertArbitrumNetworkHasTokenBridge(setChildChain) 115 | 116 | const erc20Bridger = new Erc20Bridger(setChildChain) 117 | const adminErc20Bridger = new AdminErc20Bridger(setChildChain) 118 | const ethBridger = new EthBridger(setChildChain) 119 | const inboxTools = new InboxTools(parentSigner, setChildChain) 120 | 121 | if (isArbitrumNetworkWithCustomFeeToken()) { 122 | await fundParentSigner(parentSigner) 123 | await fundParentCustomFeeToken(parentSigner) 124 | await approveParentCustomFeeToken(parentSigner) 125 | } 126 | 127 | return { 128 | parentSigner, 129 | childSigner, 130 | parentProvider: ethProvider, 131 | childProvider: arbProvider, 132 | childChain: setChildChain, 133 | erc20Bridger, 134 | adminErc20Bridger, 135 | ethBridger, 136 | inboxTools, 137 | parentDeployer, 138 | childDeployer, 139 | } 140 | } 141 | 142 | export function getLocalNetworksFromFile(): { 143 | l2Network: ArbitrumNetwork 144 | l3Network?: ArbitrumNetwork 145 | } { 146 | const pathToLocalNetworkFile = path.join(__dirname, '..', 'localNetwork.json') 147 | if (!fs.existsSync(pathToLocalNetworkFile)) { 148 | throw new ArbSdkError('localNetwork.json not found, must gen:network first') 149 | } 150 | const localNetworksFile = fs.readFileSync(pathToLocalNetworkFile, 'utf8') 151 | const localL2: ArbitrumNetwork = JSON.parse(localNetworksFile).l2Network 152 | const localL3: ArbitrumNetwork = JSON.parse(localNetworksFile).l3Network 153 | 154 | return { l2Network: localL2, l3Network: localL3 } 155 | } 156 | -------------------------------------------------------------------------------- /packages/sdk/tests/unit/addressAlias.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, Offchain Labs, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /* eslint-env node */ 17 | 'use strict' 18 | 19 | import { expect } from 'chai' 20 | 21 | import { Address } from '../../src/lib/dataEntities/address' 22 | import { BigNumber } from 'ethers' 23 | import { ADDRESS_ALIAS_OFFSET } from '../../src/lib/dataEntities/constants' 24 | import { hexZeroPad } from '@ethersproject/bytes' 25 | import { getAddress } from '@ethersproject/address' 26 | const offset = BigNumber.from(ADDRESS_ALIAS_OFFSET) 27 | const maxAddr = BigNumber.from('0xffffffffffffffffffffffffffffffffffffffff') 28 | 29 | describe('Address', () => { 30 | const testApplyUndo = ( 31 | addr: string, 32 | expectedApply: string, 33 | expectedUndo: string 34 | ) => { 35 | const address = new Address(addr) 36 | 37 | const afterApply = address.applyAlias() 38 | expect(afterApply.value, 'invalid apply alias').to.eq(expectedApply) 39 | 40 | const afterApplyUndo = afterApply.undoAlias() 41 | expect(afterApplyUndo.value, 'invalid undo after apply alias').to.eq(addr) 42 | 43 | const afterUndo = address.undoAlias() 44 | expect(afterUndo.value, 'invalid undo alias').to.eq(expectedUndo) 45 | 46 | const afterUndoApply = afterUndo.applyAlias() 47 | expect(afterUndoApply.value, 'invalid apply after undo alias').to.eq(addr) 48 | } 49 | 50 | it('does alias correctly below offset', async () => { 51 | // 0xeeeeffffffffffffffffffffffffffffffffeee4 52 | const belowOffset = hexZeroPad( 53 | maxAddr.sub(offset).sub(10).toHexString(), 54 | 20 55 | ) 56 | 57 | testApplyUndo( 58 | getAddress(belowOffset), 59 | getAddress('0xfffffffffffffffffffffffffffffffffffffff5'), 60 | getAddress('0xddddffffffffffffffffffffffffffffffffddd3') 61 | ) 62 | }) 63 | 64 | it('does alias correctly on', async () => { 65 | // 0xeeeeffffffffffffffffffffffffffffffffeeee 66 | const onOffset = hexZeroPad(maxAddr.sub(offset).add(0).toHexString(), 20) 67 | 68 | testApplyUndo( 69 | getAddress(onOffset), 70 | getAddress('0xffffffffffffffffffffffffffffffffffffffff'), 71 | getAddress('0xddddffffffffffffffffffffffffffffffffdddd') 72 | ) 73 | }) 74 | 75 | it('does alias correctly above offset', async () => { 76 | // 0xeeeeffffffffffffffffffffffffffffffffeef8 77 | const aboveOffset = hexZeroPad( 78 | maxAddr.sub(offset).add(10).toHexString(), 79 | 20 80 | ) 81 | 82 | testApplyUndo( 83 | getAddress(aboveOffset), 84 | getAddress('0x0000000000000000000000000000000000000009'), 85 | getAddress('0xddddffffffffffffffffffffffffffffffffdde7') 86 | ) 87 | }) 88 | 89 | it('does alias special case', async () => { 90 | // this is the address that initially caused the overflow bug in 91 | // in address aliasing, so we just keep it here as a test case 92 | const special = '0xFfC98231ef2fd1F77106E10581A1faC14E29d014' 93 | 94 | testApplyUndo( 95 | getAddress(special), 96 | getAddress('0x10da8231ef2fd1f77106e10581a1fac14e29e125'), 97 | getAddress('0xeeb88231ef2fd1f77106e10581a1fac14e29bf03') 98 | ) 99 | }) 100 | }) 101 | -------------------------------------------------------------------------------- /packages/sdk/tests/unit/calldata.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { getErc20ParentAddressFromParentToChildTxRequest } from '../../src/lib/utils/calldata' 3 | import { ParentToChildTxReqAndSigner } from '../../src/lib/assetBridger/ethBridger' 4 | 5 | describe('Calldata', () => { 6 | describe('getErc20ParentAddressFromParentToChildTxRequest', () => { 7 | it('decodes calldata to get token address from `outboundTransfer` method call on gateway router', async () => { 8 | const calldata = 9 | '0xd2ce7d65000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000df7fa906da092cc30f868c5730c944f4d5431e17000000000000000000000000000000000000000000000000dea56a0c808e9b6a0000000000000000000000000000000000000000000000000000000000026257000000000000000000000000000000000000000000000000000000000393870000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000580cedab294000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000' 10 | const expectedAddress = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' 11 | const output = getErc20ParentAddressFromParentToChildTxRequest({ 12 | txRequest: { data: calldata }, 13 | } as ParentToChildTxReqAndSigner) 14 | expect(output).to.eq(expectedAddress) 15 | }) 16 | 17 | it('decodes calldata to get token address from `outboundTransferCustomRefund` method call on gateway router', async () => { 18 | const calldata = 19 | '0x4fb1a07b000000000000000000000000429881672b9ae42b8eba0e26cd9c73711b891ca50000000000000000000000000f571d2625b503bb7c1d2b5655b483a2fa696fef0000000000000000000000007ecc7163469f37b777d7b8f45a667314030ace240000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000186a00000000000000000000000000000000000000000000000000000000011e1a30000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000e35fa931a00000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000' 20 | const expectedAddress = '0x429881672B9AE42b8EbA0E26cD9C73711b891Ca5' 21 | const output = getErc20ParentAddressFromParentToChildTxRequest({ 22 | txRequest: { data: calldata }, 23 | } as ParentToChildTxReqAndSigner) 24 | expect(output).to.eq(expectedAddress) 25 | }) 26 | 27 | it('throws when handling bad calldata', async () => { 28 | try { 29 | const calldata = '0xInvalidCalldata' 30 | getErc20ParentAddressFromParentToChildTxRequest({ 31 | txRequest: { data: calldata }, 32 | } as ParentToChildTxReqAndSigner) 33 | } catch (err: any) { 34 | expect(err.message).to.eq( 35 | 'data signature not matching deposits methods' 36 | ) 37 | } 38 | }) 39 | 40 | it('throws when handling an empty string', async () => { 41 | try { 42 | getErc20ParentAddressFromParentToChildTxRequest({ 43 | txRequest: { data: '' }, 44 | } as ParentToChildTxReqAndSigner) 45 | } catch (err: any) { 46 | expect(err.message).to.eq( 47 | 'data signature not matching deposits methods' 48 | ) 49 | } 50 | }) 51 | 52 | it('throws when handling `undefined` data ', async () => { 53 | try { 54 | getErc20ParentAddressFromParentToChildTxRequest({ 55 | txRequest: { data: undefined }, 56 | } as any as ParentToChildTxReqAndSigner) 57 | } catch (err: any) { 58 | expect(err.message).to.eq( 59 | 'data signature not matching deposits methods' 60 | ) 61 | } 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /packages/sdk/tests/unit/childBlocksForL1Block.test.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers' 2 | import { expect } from 'chai' 3 | import { JsonRpcProvider } from '@ethersproject/providers' 4 | import { 5 | getBlockRangesForL1Block, 6 | getFirstBlockForL1Block, 7 | } from '../../src/lib/utils/lib' 8 | import { ArbitrumProvider } from '../../src/lib/utils/arbProvider' 9 | import { ArbBlock } from '../../src/lib/dataEntities/rpc' 10 | 11 | describe('Child blocks lookup for a Parent block', () => { 12 | const provider = new JsonRpcProvider('https://arb1.arbitrum.io/rpc') 13 | const arbProvider = new ArbitrumProvider(provider) 14 | 15 | async function validateChildBlocks({ 16 | childBlocks, 17 | childBlocksCount, 18 | type = 'number', 19 | }: { 20 | childBlocks: (number | undefined)[] 21 | childBlocksCount: number 22 | type?: 'number' | 'undefined' 23 | }) { 24 | if (childBlocks.length !== childBlocksCount) { 25 | throw new Error( 26 | `Expected Child block range to have the array length of ${childBlocksCount}, got ${childBlocks.length}.` 27 | ) 28 | } 29 | 30 | if (childBlocks.some(block => typeof block !== type)) { 31 | throw new Error(`Expected all blocks to be ${type}.`) 32 | } 33 | 34 | if (type === 'undefined') { 35 | return 36 | } 37 | 38 | const promises: Promise[] = [] 39 | 40 | childBlocks.forEach((childBlock, index) => { 41 | if (!childBlock) { 42 | throw new Error('Child block is undefined.') 43 | } 44 | const isStartBlock = index === 0 45 | promises.push(arbProvider.getBlock(childBlock)) 46 | // Search for previous or next block. 47 | promises.push(arbProvider.getBlock(childBlock + (isStartBlock ? -1 : 1))) 48 | }) 49 | 50 | const [startBlock, blockBeforeStartBlock, endBlock, blockAfterEndBlock] = 51 | await Promise.all(promises).then(result => 52 | result.map(block => BigNumber.from(block.l1BlockNumber)) 53 | ) 54 | 55 | if (startBlock && blockBeforeStartBlock) { 56 | const startBlockCondition = startBlock.gt(blockBeforeStartBlock) 57 | 58 | // Check if Arbitrum start block is the first block for this parent block. 59 | expect( 60 | startBlockCondition, 61 | `Child block is not the first block in range for parent block.` 62 | ).to.be.true 63 | } 64 | 65 | if (endBlock && blockAfterEndBlock) { 66 | const endBlockCondition = endBlock.lt(blockAfterEndBlock) 67 | 68 | // Check if Arbitrum end block is the last block for this parent block. 69 | expect( 70 | endBlockCondition, 71 | `Child block is not the last block in range for parent block.` 72 | ).to.be.true 73 | } 74 | } 75 | 76 | it('successfully searches for an Child block range', async function () { 77 | const childBlocks = await getBlockRangesForL1Block({ 78 | arbitrumProvider: arbProvider, 79 | forL1Block: 17926532, 80 | // Expected result: 121907680. Narrows down the range to speed up the search. 81 | minArbitrumBlock: 121800000, 82 | maxArbitrumBlock: 122000000, 83 | }) 84 | await validateChildBlocks({ childBlocks, childBlocksCount: 2 }) 85 | }) 86 | 87 | it('fails to search for an Child block range', async function () { 88 | const childBlocks = await getBlockRangesForL1Block({ 89 | arbitrumProvider: arbProvider, 90 | forL1Block: 17926533, 91 | minArbitrumBlock: 121800000, 92 | maxArbitrumBlock: 122000000, 93 | }) 94 | await validateChildBlocks({ 95 | childBlocks, 96 | childBlocksCount: 2, 97 | type: 'undefined', 98 | }) 99 | }) 100 | 101 | it('successfully searches for the first Child block', async function () { 102 | const childBlocks = [ 103 | await getFirstBlockForL1Block({ 104 | arbitrumProvider: arbProvider, 105 | forL1Block: 17926532, 106 | // Expected result: 121907680. Narrows down the range to speed up the search. 107 | minArbitrumBlock: 121800000, 108 | maxArbitrumBlock: 122000000, 109 | }), 110 | ] 111 | await validateChildBlocks({ childBlocks, childBlocksCount: 1 }) 112 | }) 113 | 114 | it('fails to search for the first Child block, while not using `allowGreater` flag', async function () { 115 | const childBlocks = [ 116 | await getFirstBlockForL1Block({ 117 | arbitrumProvider: arbProvider, 118 | forL1Block: 17926533, 119 | allowGreater: false, 120 | minArbitrumBlock: 121800000, 121 | maxArbitrumBlock: 122000000, 122 | }), 123 | ] 124 | await validateChildBlocks({ 125 | childBlocks, 126 | childBlocksCount: 1, 127 | type: 'undefined', 128 | }) 129 | }) 130 | 131 | it('successfully searches for the first Child block, while using `allowGreater` flag', async function () { 132 | const childBlocks = [ 133 | await getFirstBlockForL1Block({ 134 | arbitrumProvider: arbProvider, 135 | forL1Block: 17926533, 136 | allowGreater: true, 137 | // Expected result: 121907740. Narrows down the range to speed up the search. 138 | minArbitrumBlock: 121800000, 139 | maxArbitrumBlock: 122000000, 140 | }), 141 | ] 142 | await validateChildBlocks({ childBlocks, childBlocksCount: 1 }) 143 | }) 144 | }) 145 | -------------------------------------------------------------------------------- /packages/sdk/tests/unit/childToParentMessageEvents.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, Offchain Labs, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /* eslint-env node */ 17 | 'use strict' 18 | 19 | import { Logger, LogLevel } from '@ethersproject/logger' 20 | Logger.setLogLevel(LogLevel.ERROR) 21 | import { ChildToParentMessage } from '../../src/lib/message/ChildToParentMessage' 22 | import { 23 | getArbitrumNetwork, 24 | getNitroGenesisBlock, 25 | } from '../../src/lib/dataEntities/networks' 26 | import { providers } from 'ethers' 27 | import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito' 28 | 29 | describe('ChildToParentMessage events', () => { 30 | // ChildToParentTransaction 31 | const classicTopic = 32 | '0x5baaa87db386365b5c161be377bc3d8e317e8d98d71a3ca7ed7d555340c8f767' 33 | // ChildToParentTx 34 | const nitroTopic = 35 | '0x3e7aafa77dbf186b7fd488006beff893744caa3c4f6f299e8a709fa2087374fc' 36 | 37 | const arbSys = '0x0000000000000000000000000000000000000064' 38 | 39 | const createProviderMock = async (networkChoiceOverride?: number) => { 40 | const l2Network = await getArbitrumNetwork(networkChoiceOverride || 42161) 41 | 42 | const l2ProviderMock = mock(providers.JsonRpcProvider) 43 | const latestBlock = getNitroGenesisBlock(l2Network) + 1000 44 | when(l2ProviderMock.getBlockNumber()).thenResolve(latestBlock) 45 | when(l2ProviderMock.getNetwork()).thenResolve({ 46 | chainId: l2Network.chainId, 47 | } as any) 48 | when(l2ProviderMock._isProvider).thenReturn(true) 49 | when(l2ProviderMock.getLogs(anything())).thenResolve([]) 50 | const l2Provider = instance(l2ProviderMock) 51 | 52 | return { 53 | l2ProviderMock, 54 | l2Provider, 55 | l2Network, 56 | latestBlock, 57 | } 58 | } 59 | 60 | it('does call for classic events', async () => { 61 | const { l2Provider, l2ProviderMock } = await createProviderMock() 62 | const fromBlock = 0 63 | const toBlock = 1000 64 | 65 | await ChildToParentMessage.getChildToParentEvents(l2Provider, { 66 | fromBlock: fromBlock, 67 | toBlock: toBlock, 68 | }) 69 | 70 | verify(l2ProviderMock.getLogs(anything())).once() 71 | verify( 72 | l2ProviderMock.getLogs( 73 | deepEqual({ 74 | address: arbSys, 75 | topics: [classicTopic], 76 | fromBlock: 0, 77 | toBlock: 1000, 78 | }) 79 | ) 80 | ).once() 81 | }) 82 | 83 | it('does call for nitro events', async () => { 84 | const { l2Network, l2Provider, l2ProviderMock } = await createProviderMock() 85 | const fromBlock = getNitroGenesisBlock(l2Network) 86 | const toBlock = getNitroGenesisBlock(l2Network) + 500 87 | 88 | await ChildToParentMessage.getChildToParentEvents(l2Provider, { 89 | fromBlock: fromBlock, 90 | toBlock: toBlock, 91 | }) 92 | 93 | verify(l2ProviderMock.getLogs(anything())).once() 94 | verify( 95 | l2ProviderMock.getLogs( 96 | deepEqual({ 97 | address: arbSys, 98 | topics: [nitroTopic], 99 | fromBlock: fromBlock, 100 | toBlock: toBlock, 101 | }) 102 | ) 103 | ).once() 104 | }) 105 | 106 | it('does call for classic and nitro events', async () => { 107 | const { l2Network, l2Provider, l2ProviderMock } = await createProviderMock() 108 | const fromBlock = 0 109 | const toBlock = getNitroGenesisBlock(l2Network) + 500 110 | 111 | await ChildToParentMessage.getChildToParentEvents(l2Provider, { 112 | fromBlock: fromBlock, 113 | toBlock: toBlock, 114 | }) 115 | 116 | verify(l2ProviderMock.getLogs(anything())).twice() 117 | verify( 118 | l2ProviderMock.getLogs( 119 | deepEqual({ 120 | address: arbSys, 121 | topics: [classicTopic], 122 | fromBlock: fromBlock, 123 | toBlock: getNitroGenesisBlock(l2Network), 124 | }) 125 | ) 126 | ).once() 127 | verify( 128 | l2ProviderMock.getLogs( 129 | deepEqual({ 130 | address: arbSys, 131 | topics: [nitroTopic], 132 | fromBlock: getNitroGenesisBlock(l2Network), 133 | toBlock: toBlock, 134 | }) 135 | ) 136 | ).once() 137 | }) 138 | 139 | it('does call for classic and nitro events from earliest to latest', async () => { 140 | const { l2Network, l2Provider, l2ProviderMock } = await createProviderMock() 141 | const fromBlock = 'earliest' 142 | const toBlock = 'latest' 143 | 144 | await ChildToParentMessage.getChildToParentEvents(l2Provider, { 145 | fromBlock: fromBlock, 146 | toBlock: toBlock, 147 | }) 148 | 149 | verify(l2ProviderMock.getLogs(anything())).twice() 150 | verify( 151 | l2ProviderMock.getLogs( 152 | deepEqual({ 153 | address: arbSys, 154 | topics: [classicTopic], 155 | fromBlock: 0, 156 | toBlock: getNitroGenesisBlock(l2Network), 157 | }) 158 | ) 159 | ).once() 160 | verify( 161 | l2ProviderMock.getLogs( 162 | deepEqual({ 163 | address: arbSys, 164 | topics: [nitroTopic], 165 | fromBlock: getNitroGenesisBlock(l2Network), 166 | toBlock: 'latest', 167 | }) 168 | ) 169 | ).once() 170 | }) 171 | 172 | it('does call for only nitro for latest', async () => { 173 | const { l2Network, l2Provider, l2ProviderMock } = await createProviderMock() 174 | const fromBlock = getNitroGenesisBlock(l2Network) + 2 175 | const toBlock = 'latest' 176 | 177 | await ChildToParentMessage.getChildToParentEvents(l2Provider, { 178 | fromBlock: fromBlock, 179 | toBlock: toBlock, 180 | }) 181 | 182 | verify(l2ProviderMock.getLogs(anything())).once() 183 | verify( 184 | l2ProviderMock.getLogs( 185 | deepEqual({ 186 | address: arbSys, 187 | topics: [nitroTopic], 188 | fromBlock: fromBlock, 189 | toBlock: 'latest', 190 | }) 191 | ) 192 | ).once() 193 | }) 194 | 195 | it('doesnt call classic when nitro genesis is 0', async () => { 196 | const { l2Provider, l2ProviderMock } = await createProviderMock(421614) 197 | const fromBlock = 'earliest' 198 | const toBlock = 'latest' 199 | 200 | await ChildToParentMessage.getChildToParentEvents(l2Provider, { 201 | fromBlock: fromBlock, 202 | toBlock: toBlock, 203 | }) 204 | 205 | // we dont expect classic to be called ever on sepolia 206 | verify(l2ProviderMock.getLogs(anything())).once() 207 | verify( 208 | l2ProviderMock.getLogs( 209 | deepEqual({ 210 | address: arbSys, 211 | topics: [nitroTopic], 212 | fromBlock: 0, 213 | toBlock: toBlock, 214 | }) 215 | ) 216 | ).once() 217 | }) 218 | }) 219 | -------------------------------------------------------------------------------- /packages/sdk/tests/unit/messageDataParser.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict' 3 | 4 | import { expect } from 'chai' 5 | 6 | import { BigNumber } from 'ethers' 7 | import { parseEther } from 'ethers/lib/utils' 8 | import { SubmitRetryableMessageDataParser } from '../../src/lib/message/messageDataParser' 9 | 10 | describe('SubmitRetryableMessageDataParser', () => { 11 | it('does parse l1 to l2 message', async () => { 12 | const messageDataParser = new SubmitRetryableMessageDataParser() 13 | // taken from https://etherscan.io/tx/0x83636bc9e73b4065d1e5d69b52e43ec05a9430a0cb270c8f595ac22399fe3c20#eventlog 14 | const retryableData = 15 | '0x000000000000000000000000467194771DAE2967AEF3ECBEDD3BF9A310C76C650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030346F1C785E00000000000000000000000000000000000000000000000000000053280CF1490000000000000000000000007F869DC59A96E798E759030B3C39398BA584F0870000000000000000000000007F869DC59A96E798E759030B3C39398BA584F08700000000000000000000000000000000000000000000000000000000000210F100000000000000000000000000000000000000000000000000000000172C586500000000000000000000000000000000000000000000000000000000000001442E567B360000000000000000000000006B175474E89094C44DA98B954EEDEAC495271D0F0000000000000000000000007F869DC59A96E798E759030B3C39398BA584F0870000000000000000000000007F869DC59A96E798E759030B3C39398BA584F08700000000000000000000000000000000000000000000003871022F1082344C7700000000000000000000000000000000000000000000000000000000000000A000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' 16 | 17 | const res = messageDataParser.parse(retryableData) 18 | 19 | expect(res.callValueRefundAddress).to.eq( 20 | '0x7F869dC59A96e798e759030b3c39398ba584F087' 21 | ) 22 | expect(res.data).to.eq( 23 | '0x2E567B360000000000000000000000006B175474E89094C44DA98B954EEDEAC495271D0F0000000000000000000000007F869DC59A96E798E759030B3C39398BA584F0870000000000000000000000007F869DC59A96E798E759030B3C39398BA584F08700000000000000000000000000000000000000000000003871022F1082344C7700000000000000000000000000000000000000000000000000000000000000A000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' 24 | ) 25 | expect(res.destAddress).to.eq('0x467194771dAe2967Aef3ECbEDD3Bf9a310C76C65') 26 | expect(res.excessFeeRefundAddress).to.eq( 27 | '0x7F869dC59A96e798e759030b3c39398ba584F087' 28 | ) 29 | expect(res.gasLimit.eq(BigNumber.from('0x0210f1')), 'incorrect gas limit') 30 | .to.be.true 31 | expect( 32 | res.l1Value.eq(BigNumber.from('0x30346f1c785e')), 33 | 'incorrect l1 value' 34 | ).to.be.true 35 | expect(res.l2CallValue.eq(BigNumber.from(0)), 'incorrect l2 call value').to 36 | .be.true 37 | expect( 38 | res.maxFeePerGas.eq(BigNumber.from('0x172c5865')), 39 | 'incorrect max fee per gas' 40 | ).to.be.true 41 | expect( 42 | res.maxSubmissionFee.eq(BigNumber.from('0x53280cf149')), 43 | 'incorrect max submission fee' 44 | ).to.be.true 45 | }) 46 | 47 | // left a separate test here for eth deposits done via a retryable 48 | // we dont normally use this method for depositing eth - we have a separate message type for that 49 | // but depositing eth via retryables is still valid so I've left this test here 50 | it('does parse eth deposit in an l1 to l2 message', async () => { 51 | const messageDataParser = new SubmitRetryableMessageDataParser() 52 | // taken from https://etherscan.io/tx/0xfe54a8166c62cf65468234c728249c28997904d6988913625ca5c4e249d06058#eventlog 53 | const retryableData = 54 | '0x000000000000000000000000F71946496600E1E1D47B8A77EB2F109FD82DC86A0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001A078F0000D790000000000000000000000000000000000000000000000000000000000370E285A0C000000000000000000000000F71946496600E1E1D47B8A77EB2F109FD82DC86A000000000000000000000000F71946496600E1E1D47B8A77EB2F109FD82DC86A000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' 55 | 56 | const res = messageDataParser.parse(retryableData) 57 | 58 | expect(res.callValueRefundAddress).to.eq( 59 | '0xf71946496600e1e1d47b8A77EB2f109Fd82dc86a' 60 | ) 61 | expect(res.data).to.eq('0x') 62 | expect(res.destAddress).to.eq('0xf71946496600e1e1d47b8A77EB2f109Fd82dc86a') 63 | expect(res.excessFeeRefundAddress).to.eq( 64 | '0xf71946496600e1e1d47b8A77EB2f109Fd82dc86a' 65 | ) 66 | expect(res.gasLimit.eq(BigNumber.from(0)), 'incorrect gas limit').to.be.true 67 | expect(res.l1Value.eq(parseEther('30.01')), 'incorrect l1 value').to.be.true 68 | expect(res.l2CallValue.eq(BigNumber.from(0)), 'incorrect l2 call value').to 69 | .be.true 70 | expect(res.maxFeePerGas.eq(BigNumber.from(0)), 'incorrect max fee per gas') 71 | .to.be.true 72 | expect( 73 | res.maxSubmissionFee.eq(BigNumber.from('0x370e285a0c')), 74 | 'incorrect max submission fee' 75 | ).to.be.true 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /packages/sdk/tests/unit/multicall.test.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { 4 | getArbitrumNetwork, 5 | getNitroGenesisBlock, 6 | } from '../../src/lib/dataEntities/networks' 7 | import { providers } from 'ethers' 8 | import { mock, when, anything, instance, deepEqual } from 'ts-mockito' 9 | import { expect } from 'chai' 10 | 11 | import { MultiCaller } from '../../src' 12 | 13 | describe('Multicall', () => { 14 | const createProviderMock = async (networkChoiceOverride?: number) => { 15 | const l2Network = await getArbitrumNetwork(networkChoiceOverride || 42161) 16 | 17 | const l2ProviderMock = mock(providers.JsonRpcProvider) 18 | const latestBlock = getNitroGenesisBlock(l2Network) + 1000 19 | when(l2ProviderMock.getBlockNumber()).thenResolve(latestBlock) 20 | when(l2ProviderMock.getNetwork()).thenResolve({ 21 | chainId: l2Network.chainId, 22 | } as any) 23 | when(l2ProviderMock._isProvider).thenReturn(true) 24 | when(l2ProviderMock.getLogs(anything())).thenResolve([]) 25 | 26 | /* 27 | This test data is taken from an actual example of a mainnet multicall. To produce this data we do the following: 28 | 1. Pass mainnet args to the multicall class, instantiated with a mock provider 29 | 2. Capture the .call request that was made on the provider 30 | 3. In another script, execute the captured .call against mainnet and record the response 31 | 4. Mock the provider with the captured request, and the response as below 32 | */ 33 | // Maker 34 | when( 35 | l2ProviderMock.call( 36 | deepEqual({ 37 | data: '0xbce38bd7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000009f8f72aa9304c8b593d555f12ef6589cc3a579a20000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000406fdde03000000000000000000000000000000000000000000000000000000000000000000000000000000009f8f72aa9304c8b593d555f12ef6589cc3a579a20000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000495d89b4100000000000000000000000000000000000000000000000000000000', 38 | // L2 multicall address 39 | to: '0xA115146782b7143fAdB3065D86eACB54c169d092', 40 | }), 41 | undefined 42 | ) 43 | ).thenResolve( 44 | '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000204d616b65720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000204d4b520000000000000000000000000000000000000000000000000000000000' 45 | ) 46 | // Uniswap 47 | when( 48 | l2ProviderMock.call( 49 | deepEqual({ 50 | data: '0xbce38bd7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000001f9840a85d5af5bf1d1762f925bdaddc4201f9840000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000406fdde03000000000000000000000000000000000000000000000000000000000000000000000000000000001f9840a85d5af5bf1d1762f925bdaddc4201f9840000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000495d89b4100000000000000000000000000000000000000000000000000000000', 51 | to: '0xA115146782b7143fAdB3065D86eACB54c169d092', 52 | }), 53 | undefined 54 | ) 55 | ).thenResolve( 56 | '0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000007556e69737761700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003554e490000000000000000000000000000000000000000000000000000000000' 57 | ) 58 | const l2Provider = instance(l2ProviderMock) 59 | 60 | return { 61 | l2ProviderMock, 62 | l2Provider, 63 | l2Network, 64 | latestBlock, 65 | } 66 | } 67 | 68 | it('returns parsed data from bytes32', async function () { 69 | const { l2Provider } = await createProviderMock(421614) 70 | const multicaller = await MultiCaller.fromProvider(l2Provider) 71 | const [data] = await multicaller.getTokenData( 72 | // Maker mainnet address 73 | ['0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2'], 74 | { symbol: true, name: true } 75 | ) 76 | 77 | expect(data.name, 'Failed to get token name from bytes32').to.be.equal( 78 | 'Maker' 79 | ) 80 | expect(data.symbol, 'Failed to get token symbol from bytes32').to.be.equal( 81 | 'MKR' 82 | ) 83 | }) 84 | 85 | it('returns parsed data from byte string', async function () { 86 | const { l2Provider } = await createProviderMock(421614) 87 | const multicaller = await MultiCaller.fromProvider(l2Provider) 88 | const [data] = await multicaller.getTokenData( 89 | // Uniswap mainnet address 90 | ['0x1f9840a85d5af5bf1d1762f925bdaddc4201f984'], 91 | { symbol: true, name: true } 92 | ) 93 | 94 | expect(data.name, 'Failed to get token name from byte string').to.be.equal( 95 | 'Uniswap' 96 | ) 97 | expect( 98 | data.symbol, 99 | 'Failed to get token symbol from byte string' 100 | ).to.be.equal('UNI') 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /packages/sdk/tests/unit/nativeToken.test.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { expect } from 'chai' 4 | 5 | import { BigNumber } from 'ethers' 6 | import { parseEther } from 'ethers/lib/utils' 7 | import { 8 | scaleFromNativeTokenDecimalsTo18Decimals, 9 | scaleFrom18DecimalsToNativeTokenDecimals, 10 | } from '../../src/lib/utils/lib' 11 | 12 | const AMOUNT_TO_SCALE = parseEther('1.23456789') 13 | 14 | describe('Native token', () => { 15 | function decimalsToError(decimals: number) { 16 | return `incorrect scaling result for ${decimals} decimals` 17 | } 18 | 19 | it('scales to native token decimals', () => { 20 | expect( 21 | scaleFrom18DecimalsToNativeTokenDecimals({ 22 | amount: AMOUNT_TO_SCALE, 23 | decimals: 18, 24 | }).eq(BigNumber.from('1234567890000000000')), 25 | decimalsToError(18) 26 | ).to.be.true 27 | 28 | // Rounds up the last digit - in this case no decimals so rounds up 1 to 2 29 | expect( 30 | scaleFrom18DecimalsToNativeTokenDecimals({ 31 | amount: AMOUNT_TO_SCALE, 32 | decimals: 0, 33 | }).eq(BigNumber.from('2')), 34 | decimalsToError(0) 35 | ).to.be.true 36 | 37 | // Rounds up the last digit 38 | expect( 39 | scaleFrom18DecimalsToNativeTokenDecimals({ 40 | amount: AMOUNT_TO_SCALE, 41 | decimals: 1, 42 | }).eq(BigNumber.from('13')), 43 | decimalsToError(1) 44 | ).to.be.true 45 | 46 | // Rounds up the last digit 47 | expect( 48 | scaleFrom18DecimalsToNativeTokenDecimals({ 49 | amount: AMOUNT_TO_SCALE, 50 | decimals: 6, 51 | }).eq(BigNumber.from('1234568')), 52 | decimalsToError(6) 53 | ).to.be.true 54 | 55 | // Rounds up the last digit 56 | expect( 57 | scaleFrom18DecimalsToNativeTokenDecimals({ 58 | amount: AMOUNT_TO_SCALE, 59 | decimals: 7, 60 | }).eq(BigNumber.from('12345679')), 61 | decimalsToError(7) 62 | ).to.be.true 63 | 64 | // Does not round up the last digit because all original decimals are included 65 | expect( 66 | scaleFrom18DecimalsToNativeTokenDecimals({ 67 | amount: AMOUNT_TO_SCALE, 68 | decimals: 8, 69 | }).eq(BigNumber.from('123456789')), 70 | decimalsToError(8) 71 | ).to.be.true 72 | 73 | // Does not round up the last digit because all original decimals are included 74 | expect( 75 | scaleFrom18DecimalsToNativeTokenDecimals({ 76 | amount: AMOUNT_TO_SCALE, 77 | decimals: 9, 78 | }).eq(BigNumber.from('1234567890')), 79 | decimalsToError(9) 80 | ).to.be.true 81 | 82 | // Does not round up the last digit because all original decimals are included 83 | expect( 84 | scaleFrom18DecimalsToNativeTokenDecimals({ 85 | amount: AMOUNT_TO_SCALE, 86 | decimals: 24, 87 | }).eq(BigNumber.from('1234567890000000000000000')), 88 | decimalsToError(24) 89 | ).to.be.true 90 | }) 91 | 92 | it('scales native token decimals to 18 decimals', () => { 93 | expect( 94 | scaleFromNativeTokenDecimalsTo18Decimals({ 95 | amount: AMOUNT_TO_SCALE, 96 | decimals: 16, 97 | }).eq(BigNumber.from('123456789000000000000')), 98 | decimalsToError(16) 99 | ).to.be.true 100 | 101 | expect( 102 | scaleFromNativeTokenDecimalsTo18Decimals({ 103 | amount: AMOUNT_TO_SCALE, 104 | decimals: 18, 105 | }).eq(BigNumber.from('1234567890000000000')), 106 | decimalsToError(18) 107 | ).to.be.true 108 | 109 | expect( 110 | scaleFromNativeTokenDecimalsTo18Decimals({ 111 | amount: AMOUNT_TO_SCALE, 112 | decimals: 20, 113 | }).eq(BigNumber.from('12345678900000000')), 114 | decimalsToError(20) 115 | ).to.be.true 116 | }) 117 | }) 118 | -------------------------------------------------------------------------------- /packages/sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist" 6 | }, 7 | "include": ["src/**/*.ts", "src/**/*.d.ts"], 8 | "exclude": ["node_modules", "dist", "tests", "scripts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/sdk/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | // Input options 3 | "entryPoints": ["./src/lib"], 4 | "entryPointStrategy": "expand", 5 | "exclude": ["./src/lib/abi"], 6 | "excludeNotDocumented": true, 7 | "excludeInternal": true, 8 | "customCss": "./theme/style.css", 9 | 10 | "sidebarLinks": { 11 | "Arbitrum Docs": "https://docs.arbitrum.io" 12 | }, 13 | 14 | // Output options 15 | "out": "docs" 16 | } 17 | -------------------------------------------------------------------------------- /packages/sdk/typedoc_md.js: -------------------------------------------------------------------------------- 1 | // References: 2 | // - https://typedoc.org/guides/overview/ 3 | // - https://github.com/tgreyuk/typedoc-plugin-markdown 4 | 5 | /** @type {import('typedoc').TypeDocOptions} */ 6 | module.exports = { 7 | // Input options 8 | entryPoints: ['./src/lib'], 9 | entryPointStrategy: 'expand', 10 | exclude: ['./src/lib/abi'], 11 | excludeNotDocumented: true, 12 | excludeInternal: true, 13 | 14 | // Output options 15 | out: 'docs', 16 | 17 | // Plugins 18 | plugin: ['typedoc-plugin-markdown'], 19 | 20 | // typedoc-plugin-markdown options 21 | // entryDocument: 'modules.md', 22 | hideBreadcrumbs: true, 23 | hideInPageTOC: true, 24 | hideMembersSymbol: true, 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "strict": true, 7 | "allowJs": true, 8 | "noImplicitAny": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "resolveJsonModule": true, 14 | "esModuleInterop": true, 15 | "experimentalDecorators": true, 16 | "paths": { 17 | "@ethersproject/*": ["./node_modules/@ethersproject/*"] 18 | } 19 | } 20 | } 21 | --------------------------------------------------------------------------------