├── .github ├── CODEOWNERS ├── SECURITY.md ├── pull_request_template.md └── workflows │ ├── main_docker_publish.yml │ ├── pr_lint_gha.yml │ ├── pr_lint_pr_title.yml │ ├── pr_main_cli.yml │ └── pr_main_sdk.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── assets ├── chain_demo.gif └── operations_demo.gif ├── cli ├── Cargo.toml ├── README.md └── src │ ├── cli.rs │ ├── commands │ ├── autocomplete.rs │ ├── l2.rs │ └── mod.rs │ ├── common.rs │ ├── lib.rs │ ├── main.rs │ └── utils.rs └── sdk ├── Cargo.toml ├── README.md ├── examples └── simple_usage.rs └── src ├── calldata.rs ├── client ├── eth │ ├── errors.rs │ ├── eth_sender.rs │ └── mod.rs └── mod.rs ├── errors.rs ├── l2 ├── constants.rs ├── deposit.rs ├── merkle_tree.rs ├── mod.rs └── withdraw.rs ├── sdk.rs └── utils.rs /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lambdaclass core team 2 | * @lambdaclass/lambda-execution-reviewers 3 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | We take the security of our project seriously. If you discover a vulnerability, we encourage you to report it responsibly so we can address it promptly. 6 | 7 | ### How to Report 8 | 9 | 1. Navigate to the **Security** tab of this repository. 10 | 2. Click on **"Report a Vulnerability"** to open the GitHub Security Advisories form. 11 | 3. Fill out the form with as much detail as possible, including: 12 | - A clear description of the issue. 13 | - Steps to reproduce the vulnerability. 14 | - The affected versions or components. 15 | - Any potential impact or severity details. 16 | 17 | Alternatively, you can send an email to **[security@lambdaclass.com](mailto:security@lambdaclass.com)** with the same details. 18 | 19 | ### Guidelines for Reporting 20 | 21 | - **Do not publicly disclose vulnerabilities** until we have confirmed and fixed the issue. 22 | - Include any proof-of-concept code, if possible, to help us verify the vulnerability more efficiently. 23 | - If applicable, specify if the vulnerability is already being exploited. 24 | 25 | ### Our Response Process 26 | 27 | - We commit to handling reports with diligence. 28 | - We will investigate all reported vulnerabilities thoroughly and transparently. 29 | - Once the vulnerability has been fixed, we will disclose the details publicly to ensure awareness and understanding. 30 | 31 | 32 | ### Reward Program 33 | 34 | While we do not currently offer a formal bug bounty program, we value your contribution and will recognize your efforts in our changelog or release notes (if you consent). 35 | 36 | Thank you for helping us improve the security of our project! -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Motivation** 2 | 3 | 4 | 5 | **Description** 6 | 7 | 8 | 9 | 10 | 11 | Closes #issue_number 12 | 13 | -------------------------------------------------------------------------------- /.github/workflows/main_docker_publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | workflow_dispatch: 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | IMAGE_NAME: ${{ github.repository }} 11 | 12 | jobs: 13 | build-and-push-image: 14 | name: Build and push Docker image 15 | runs-on: ubuntu-latest 16 | 17 | # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. 18 | permissions: 19 | contents: read 20 | packages: write 21 | attestations: write 22 | id-token: write 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v4 27 | 28 | - name: Log in to the Container registry 29 | uses: docker/login-action@v3 30 | with: 31 | registry: ${{ env.REGISTRY }} 32 | username: ${{ github.actor }} 33 | password: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | # Generates the tags and labels based on the image name. The id allows using it in the next step. 36 | - name: Extract metadata (tags, labels) for Docker 37 | id: meta 38 | uses: docker/metadata-action@v5 39 | with: 40 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 41 | 42 | # Pushes to ghcr.io/ethrex 43 | - name: Build and push Docker image 44 | id: push 45 | uses: docker/build-push-action@v6 46 | with: 47 | context: . 48 | push: true 49 | tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest 50 | 51 | - name: Generate artifact attestation 52 | uses: actions/attest-build-provenance@v2 53 | with: 54 | subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 55 | subject-digest: ${{ steps.push.outputs.digest }} 56 | push-to-registry: true 57 | -------------------------------------------------------------------------------- /.github/workflows/pr_lint_gha.yml: -------------------------------------------------------------------------------- 1 | name: Github Actions 2 | on: 3 | pull_request: 4 | branches: ["**"] 5 | paths: 6 | - ".github/**.yaml" 7 | - ".github/*.yml" 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | lint: 15 | name: Lint 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout sources 19 | uses: actions/checkout@v4 20 | 21 | - name: actionlint 22 | uses: raven-actions/actionlint@v2 23 | with: 24 | flags: "-ignore SC2086 -ignore SC2006 -ignore SC2046" 25 | -------------------------------------------------------------------------------- /.github/workflows/pr_lint_pr_title.yml: -------------------------------------------------------------------------------- 1 | name: PR title lint 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | validate: 15 | name: Validate PR title 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: amannn/action-semantic-pull-request@v5 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | # Configure which types are allowed (newline-delimited). 23 | # Default: https://github.com/commitizen/conventional-commit-types 24 | # Customized based on sections defined in scripts/generate_changelog.mjs 25 | types: | 26 | feat 27 | fix 28 | perf 29 | refactor 30 | revert 31 | deps 32 | build 33 | ci 34 | test 35 | style 36 | chore 37 | docs 38 | 39 | # Configure which scopes are allowed (newline-delimited). 40 | # These are regex patterns auto-wrapped in `^ $`. 41 | scopes: | 42 | cli 43 | ci 44 | sdk 45 | 46 | # Configure that a scope must always be provided. 47 | requireScope: true 48 | 49 | # Configure which scopes are disallowed in PR titles (newline-delimited). 50 | # For instance by setting the value below, `chore(release): ...` (lowercase) 51 | # and `ci(e2e,release): ...` (unknown scope) will be rejected. 52 | # These are regex patterns auto-wrapped in `^ $`. 53 | #disallowScopes: | 54 | 55 | # Configure additional validation for the subject based on a regex. 56 | # This example ensures the subject doesn't start with an uppercase character. 57 | subjectPattern: ^(?![A-Z]).+$ 58 | 59 | # If `subjectPattern` is configured, you can use this property to override 60 | # the default error message that is shown when the pattern doesn't match. 61 | # The variables `subject` and `title` can be used within the message. 62 | subjectPatternError: | 63 | The subject "{subject}" found in the pull request title "{title}" 64 | didn't match the configured pattern. Please ensure that the subject 65 | doesn't start with an uppercase character. 66 | 67 | # If the PR contains one of these newline-delimited labels, the 68 | # validation is skipped. If you want to rerun the validation when 69 | # labels change, you might want to use the `labeled` and `unlabeled` 70 | # event triggers in your workflow. 71 | # ignoreLabels: | 72 | # bot 73 | # ignore-semantic-pull-request 74 | -------------------------------------------------------------------------------- /.github/workflows/pr_main_cli.yml: -------------------------------------------------------------------------------- 1 | name: rex cli 2 | on: 3 | push: 4 | branches: ["main"] 5 | merge_group: 6 | pull_request: 7 | branches: ["**"] 8 | paths-ignore: 9 | - "sdk/**" # We run this in a separate workflow 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | lint: 17 | name: Lint 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout sources 21 | uses: actions/checkout@v4 22 | 23 | - name: Rustup toolchain install 24 | uses: dtolnay/rust-toolchain@stable 25 | with: 26 | components: rustfmt, clippy 27 | 28 | - name: Add Rust Cache 29 | uses: Swatinem/rust-cache@v2 30 | 31 | - name: Run cargo check 32 | run: cargo check --manifest-path cli/Cargo.toml 33 | 34 | - name: Run cargo build 35 | run: cargo build --manifest-path cli/Cargo.toml 36 | 37 | - name: Run cargo clippy 38 | run: cargo clippy --manifest-path cli/Cargo.toml --all-targets --all-features -- -D warnings 39 | 40 | - name: Run cargo fmt 41 | run: cargo fmt --manifest-path cli/Cargo.toml --all -- --check 42 | 43 | test: 44 | name: Test 45 | runs-on: ubuntu-latest 46 | steps: 47 | - name: Checkout sources 48 | uses: actions/checkout@v4 49 | 50 | - name: Rustup toolchain install 51 | uses: dtolnay/rust-toolchain@stable 52 | 53 | - name: Caching 54 | uses: Swatinem/rust-cache@v2 55 | 56 | - name: Run tests 57 | run: cargo test --manifest-path cli/Cargo.toml 58 | -------------------------------------------------------------------------------- /.github/workflows/pr_main_sdk.yml: -------------------------------------------------------------------------------- 1 | name: rex sdk 2 | on: 3 | push: 4 | branches: ["main"] 5 | merge_group: 6 | pull_request: 7 | branches: ["**"] 8 | paths-ignore: 9 | - "cli/**" # We run this in a separate workflow 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | lint: 17 | name: Lint 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout sources 21 | uses: actions/checkout@v4 22 | 23 | - name: Rustup toolchain install 24 | uses: dtolnay/rust-toolchain@stable 25 | with: 26 | components: rustfmt, clippy 27 | 28 | - name: Add Rust Cache 29 | uses: Swatinem/rust-cache@v2 30 | 31 | - name: Run cargo check 32 | run: cargo check --manifest-path sdk/Cargo.toml 33 | 34 | - name: Run cargo build 35 | run: cargo build --manifest-path sdk/Cargo.toml 36 | 37 | - name: Run cargo clippy 38 | run: cargo clippy --manifest-path sdk/Cargo.toml --all-targets --all-features -- -D warnings 39 | 40 | - name: Run cargo fmt 41 | run: cargo fmt --manifest-path sdk/Cargo.toml --all -- --check 42 | 43 | test: 44 | name: Test 45 | runs-on: ubuntu-latest 46 | steps: 47 | - name: Checkout sources 48 | uses: actions/checkout@v4 49 | 50 | - name: Rustup toolchain install 51 | uses: dtolnay/rust-toolchain@stable 52 | 53 | - name: Caching 54 | uses: Swatinem/rust-cache@v2 55 | 56 | - name: Run tests 57 | run: cargo test --manifest-path sdk/Cargo.toml 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | 12 | # RustRover 13 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 14 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 15 | # and can be added to the global gitignore or merged into this file. For a more nuclear 16 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 17 | #.idea/ 18 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "cli", 4 | "sdk", 5 | ] 6 | default-members = ["cli"] 7 | resolver = "3" 8 | 9 | [workspace.package] 10 | version = "0.1.0" 11 | edition = "2024" 12 | 13 | [workspace.lints.rust] 14 | unsafe_code = "forbid" 15 | warnings = "warn" 16 | 17 | [workspace.lints.clippy] 18 | panic = "deny" 19 | unnecessary_cast = "warn" 20 | deref_by_slicing = "warn" 21 | indexing_slicing = "warn" 22 | manual_unwrap_or = "warn" 23 | manual_unwrap_or_default = "warn" 24 | as_conversions = "deny" 25 | unwrap_used = "deny" 26 | expect_used = "deny" 27 | arithmetic_side_effects = "deny" 28 | overflow_check_conditional = "warn" 29 | manual_saturating_arithmetic = "warn" 30 | 31 | [workspace.dependencies] 32 | rex-cli = { path = "cli" } 33 | rex-sdk = { path = "sdk" } 34 | 35 | ethrex-l2 = { git = "https://github.com/lambdaclass/ethrex", package = "ethrex-l2" } 36 | ethrex-common = { git = "https://github.com/lambdaclass/ethrex", package = "ethrex-common" } 37 | ethrex-blockchain = { git = "https://github.com/lambdaclass/ethrex", package = "ethrex-blockchain" } 38 | ethrex-rlp = { git = "https://github.com/lambdaclass/ethrex", package = "ethrex-rlp" } 39 | ethrex-rpc = { git = "https://github.com/lambdaclass/ethrex", package = "ethrex-rpc" } 40 | 41 | keccak-hash = "0.11.0" 42 | thiserror = "2.0.11" 43 | hex = "0.4.3" 44 | secp256k1 = { version = "0.29.1", default-features = false, features = [ 45 | "global-context", 46 | "recovery", 47 | "rand", 48 | ] } 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Lambdaclass 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: cli 2 | 3 | cli: 4 | cargo install --path cli 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rex - Developing on Ethereum powered by Ethrex 2 | 3 | Rex is a set of utilities for Ethereum development powered by [Ethrex](https://github.com/lambdaclass/ethrex). 4 | 5 | With **Rex** you can 6 | - Launch your own devnet using Ethrex 7 | - Interact with a running L1 network 8 | - Interact with a running Ethrex L2 network 9 | - Execute useful functions for Ethereum development. 10 | 11 | **Rex** can be used both as a **CLI tool** and via its **Rust SDK**, allowing seamless integration with any Rust script. 12 | 13 | Our **CLI** is built on top of the **SDK**, ensuring a consistent and powerful developer experience. 14 | 15 | Rex is currently a replacement for foundry's `cast` and `alloy`. 16 | 17 | ## `rex` CLI 18 | 19 | The `rex` CLI is a command line tool that provides a set of utilities for Ethereum development. 20 | 21 | ### Installing the CLI 22 | 23 | So far, **Rex** does not have a published release on [crates.io](https://crates.io). 24 | To install it, you need to clone the repository and run the following command to install the CLI as the binary `rex`: 25 | 26 | ```Shell 27 | make cli 28 | ``` 29 | 30 | ### Using the CLI 31 | 32 | After installing the CLI with `make cli`, run `rex` to display the help message and see the available commands. 33 | 34 | ```Shell 35 | ➜ ~ rex 36 | Usage: rex 37 | 38 | Commands: 39 | address Get either the account's address from private key, the zero address, or a random address [aliases: addr, a] 40 | autocomplete Generate shell completion scripts. 41 | balance Get the account's balance info. [aliases: bal, b] 42 | block-number Get the current block_number. [aliases: bl] 43 | call Make a call to a contract. 44 | chain-id Get the network's chain id. 45 | deploy Deploy a contract. 46 | code Returns code at a given address. 47 | hash Get either the keccak for a given input, the zero hash, the empty string, or a random hash. [aliases: h, h] 48 | l2 L2 specific commands. 49 | nonce Get the account's nonce. [aliases: n] 50 | receipt Get the transaction's receipt. [aliases: r] 51 | send Send a transaction. 52 | signer 53 | transaction Get the transaction's info. [aliases: tx, t] 54 | transfer Transfer funds to another wallet. 55 | sign Sign a message with a private key. 56 | verify Verify if a messages signature was made by an account. 57 | help Print this message or the help of the given subcommand(s) 58 | 59 | Options: 60 | -h, --help Print help 61 | -V, --version Print version 62 | ``` 63 | 64 | #### Helpful operations 65 | 66 | ![ops](./assets/operations_demo.gif) 67 | 68 | #### Interacting with an Ethereum node 69 | 70 | > [!NOTE] 71 | > Before running the following commands, make sure you have an Ethereum node running or override the default RPC URL with the `--rpc-url` flag to point to a public node. 72 | 73 | ![eth](./assets/chain_demo.gif) 74 | 75 | #### Interacting with an ethrex L2 node 76 | 77 | TODO 78 | 79 | You can find the CLI documentation [here](cli/README.md). 80 | 81 | ## `rex` SDK 82 | 83 | The `rex` SDK provides a set of utilities for Ethereum and ethrex L2 development. With it, you can write Rust scripts to interact with Ethereum and ethrex L2 networks as well as deploy and interact with smart contracts, transferring funds between accounts, and more. 84 | 85 | ### Getting Started with the SDK 86 | 87 | #### Adding the SDK to your project 88 | 89 | For the moment, `rex-sdk` is not yet published on crates.io. You can add the SDK to your project by adding the following to your `Cargo.toml` file: 90 | 91 | ```toml 92 | [dependencies] 93 | rex-sdk = { git = "https://github.com/lambdaclass/rex", package = "rex-sdk", branch = "main" } 94 | ethrex-common = { git = "https://github.com/lambdaclass/ethrex", package = "ethrex-common", branch = "main" } 95 | ``` 96 | 97 | > [!TIP] 98 | > Maybe consider adding tokio as dependency since we are using a lot of async/await functions. If this example is meant to be done in the main function the #[tokio::main] annotation is needed. 99 | 100 | 101 | 114 | 115 | #### First Steps 116 | 117 | In the following example we will show simple interactions with an Ethereum node similar to the CLI example but using the SDK (as a matter of fact, the CLI uses the SDK as backend). 118 | 119 | As pre-requisites for running this example you need to have an Ethereum node running locally or have access to a public node. And you need to have an account with some funds (these must be the values of `account` and `from_private_key` in the following example). 120 | 121 | *Importing the dependencies* 122 | 123 | ```Rust 124 | use ethrex_common::{Address, U256}; 125 | use rex_sdk::{ 126 | client::{EthClient, Overrides}, 127 | transfer, wait_for_transaction_receipt, 128 | }; 129 | use std::str::FromStr; 130 | ``` 131 | 132 | The following should be either part of a function or the main function. 133 | 134 | *Connecting to the node* 135 | 136 | ```Rust 137 | let rpc_url = "http://localhost:8545"; 138 | 139 | let eth_client = EthClient::new(rpc_url); 140 | ``` 141 | 142 | *Doing simple interactions (balance and nonce of an account and chain-id)* 143 | 144 | ```Rust 145 | let account_balance = eth_client.get_balance(account).await.unwrap(); 146 | 147 | let account_nonce = eth_client.get_nonce(account).await.unwrap(); 148 | 149 | let chain_id = eth_client.get_chain_id().await.unwrap(); 150 | 151 | println!("Account balance: {account_balance}"); 152 | println!("Account nonce: {account_nonce}"); 153 | println!("Chain id: {chain_id}"); 154 | ``` 155 | 156 | *Transferring funds* 157 | 158 | ```Rust 159 | let amount = U256::from_dec_str("1000000000000000000").unwrap(); // 1 ETH in wei 160 | let from = account; 161 | let to = Address::from_str("0x4852f44fd706e34cb906b399b729798665f64a83").unwrap(); 162 | 163 | let tx_hash = transfer( 164 | amount, 165 | from, 166 | to, 167 | from_private_key, 168 | ð_client, 169 | Overrides { 170 | value: Some(amount), 171 | ..Default::default() 172 | }, 173 | ) 174 | .await 175 | .unwrap(); 176 | 177 | // Wait for the transaction to be finalized 178 | wait_for_transaction_receipt(tx_hash, ð_client, 100) 179 | .await 180 | .unwrap(); 181 | ``` 182 | 183 | *Getting transfer tx hash details and receipt* 184 | 185 | ```Rust 186 | let tx_receipt = eth_client.get_transaction_receipt(tx_hash).await.unwrap(); 187 | 188 | println!("transfer tx receipt: {tx_receipt:?}"); 189 | 190 | let tx_details = eth_client.get_transaction_by_hash(tx_hash).await.unwrap(); 191 | 192 | println!("transfer tx details: {tx_details:?}"); 193 | ``` 194 | 195 | #### Full Example 196 | 197 | ```Rust 198 | use ethrex_common::{Address, U256}; 199 | use rex_sdk::{ 200 | client::{EthClient, Overrides}, 201 | transfer, wait_for_transaction_receipt, 202 | }; 203 | use std::str::FromStr; 204 | 205 | #[tokio::main] 206 | async fn main() { 207 | let rpc_url = "http://localhost:8545"; 208 | 209 | let eth_client = EthClient::new(&rpc_url); 210 | 211 | let account_balance = eth_client.get_balance(account).await.unwrap(); 212 | 213 | let account_nonce = eth_client.get_nonce(account).await.unwrap(); 214 | 215 | let chain_id = eth_client.get_chain_id().await.unwrap(); 216 | 217 | println!("Account balance: {account_balance}"); 218 | println!("Account nonce: {account_nonce}"); 219 | println!("Chain id: {chain_id}"); 220 | 221 | let amount = U256::from(1000000000000000000); // 1 ETH in wei 222 | let from = account; 223 | let to = Address::from_str("0x4852f44fd706e34cb906b399b729798665f64a83").unwrap(); 224 | 225 | let tx_hash = transfer( 226 | amount, 227 | from, 228 | to, 229 | from_private_key, 230 | ð_client, 231 | Overrides { 232 | value: Some(amount), 233 | ..Default::default() 234 | }, 235 | ) 236 | .await 237 | .unwrap(); 238 | 239 | // Wait for the transaction to be finalized 240 | wait_for_transaction_receipt(tx_hash, ð_client, 100) 241 | .await 242 | .unwrap(); 243 | 244 | let tx_receipt = eth_client.get_transaction_receipt(tx_hash).await.unwrap(); 245 | 246 | println!("transfer tx receipt: {tx_receipt:?}"); 247 | 248 | let tx_details = eth_client.get_transaction_by_hash(tx_hash).await.unwrap(); 249 | 250 | println!("transfer tx details: {tx_details:?}"); 251 | } 252 | ``` 253 | 254 | #### Running the example 255 | 256 | > [!WARNING] 257 | > Before running the example, make sure you have an Ethereum node running or override the default RPC URL with the `--rpc-url` flag to point to a public node. 258 | > The account associated to the private key must have some funds in the network you are connecting to. 259 | 260 | ```Shell 261 | cd sdk 262 | cargo run --release --example simple_usage -- --private-key --rpc-url 263 | ``` 264 | 265 | > [!NOTE] 266 | > You can find the code for this example in `sdk/examples/simple_usage.rs`. 267 | 268 | You can find the SDK documentation [here](sdk/README.md). 269 | 270 | 271 | # Security 272 | 273 | We take security seriously. If you discover a vulnerability in this project, please report it responsibly. 274 | 275 | - You can report vulnerabilities directly via the **[GitHub "Report a Vulnerability" feature](../../security/advisories/new)**. 276 | - Alternatively, send an email to **[security@lambdaclass.com](mailto:security@lambdaclass.com)**. 277 | 278 | For more details, please refer to our [Security Policy](./.github/SECURITY.md). 279 | 280 | 281 | -------------------------------------------------------------------------------- /assets/chain_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdaclass/rex/789727a656654faed767b1970676308535e4c8b5/assets/chain_demo.gif -------------------------------------------------------------------------------- /assets/operations_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdaclass/rex/789727a656654faed767b1970676308535e4c8b5/assets/operations_demo.gif -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rex" 3 | version.workspace = true 4 | edition.workspace = true 5 | 6 | [dependencies] 7 | rex-sdk.workspace = true 8 | 9 | ethrex-l2.workspace = true 10 | ethrex-common.workspace = true 11 | ethrex-blockchain.workspace = true 12 | ethrex-rlp.workspace = true 13 | ethrex-rpc.workspace = true 14 | 15 | # Runtime 16 | tokio = "1.43.0" 17 | 18 | # CLI 19 | clap = { version = "4.3", features = ["derive", "env"] } 20 | clap_complete = "4.5.17" 21 | eyre = "0.6" 22 | dialoguer = "0.11" 23 | colored = "3.0.0" 24 | spinoff = "0.8.0" 25 | strum = "0.27.1" 26 | 27 | # Crypto 28 | keccak-hash.workspace = true 29 | secp256k1.workspace = true 30 | 31 | # Utils 32 | hex.workspace = true 33 | itertools = "0.14.0" 34 | toml = "0.8.19" 35 | dirs = "6.0.0" 36 | 37 | # Logging 38 | log = "0.4" 39 | tracing = "0.1.41" 40 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } 41 | 42 | # Serde 43 | serde = "1.0.218" 44 | serde_json = "1.0.139" 45 | -------------------------------------------------------------------------------- /cli/README.md: -------------------------------------------------------------------------------- 1 | # CLI 2 | 3 | - [How to install](#how-to-install) 4 | - [How to run](#how-to-run) 5 | - [Commands](#commands) 6 | - [`rex address`](#rex-address) 7 | - [`rex hash`](#rex-hash) 8 | - [`rex receipt`](#rex-receipt) 9 | - [`rex transaction`](#rex-transaction) 10 | - [`rex balance`](#rex-balance) 11 | - [`rex nonce`](#rex-nonce) 12 | - [`rex block-number`](#rex-block-number) 13 | - [`rex signer`](#rex-signer) 14 | - [`rex chain-id`](#rex-chain-id) 15 | - [`rex transfer`](#rex-transfer) 16 | - [`rex send`](#rex-send) 17 | - [`rex call`](#rex-call) 18 | - [`rex deploy`](#rex-deploy) 19 | - [Examples](#examples) 20 | 21 | 22 | ## How to install 23 | 24 | Running the following command will install the CLI as the binary `rex`. 25 | 26 | ```Shell 27 | make cli 28 | ``` 29 | 30 | ## How to run 31 | 32 | After installing the CLI with `make cli`, run `rex` to display the help message. 33 | 34 | ```Shell 35 | > rex 36 | 37 | Usage: rex 38 | 39 | Commands: 40 | l2 L2 specific commands. 41 | autocomplete Generate shell completion scripts. 42 | block-number Get the current block_number. [aliases: bl] 43 | transaction Get the transaction's info. [aliases: tx, t] 44 | receipt Get the transaction's receipt. [aliases: r] 45 | balance Get the account's balance info. [aliases: bal, b] 46 | nonce Get the account's nonce. [aliases: n] 47 | address Get either the account's address from private key, the zero address, or a random address [aliases: addr, a] 48 | hash Get either the keccak for a given input, the zero hash, the empty string, or a random hash [aliases: h] 49 | signer 50 | transfer Transfer funds to another wallet. 51 | send Send a transaction 52 | call Make a call to a contract 53 | deploy Deploy a contract 54 | chain-id Get the network's chain id. 55 | help Print this message or the help of the given subcommand(s) 56 | 57 | Options: 58 | -h, --help Print help 59 | -V, --version Print version 60 | ``` 61 | 62 | ## Commands 63 | 64 | ### `rex address` 65 | 66 | ```Shell 67 | Get either the account's address from private key, the zero address, or a random address 68 | 69 | Usage: rex address [OPTIONS] 70 | 71 | Options: 72 | --from-private-key 73 | The private key to derive the address from. [env: PRIVATE_KEY=] 74 | -z, --zero 75 | The zero address. 76 | -r, --random 77 | A random address. 78 | -h, --help 79 | Print help 80 | ``` 81 | 82 | ### `rex hash` 83 | 84 | ```Shell 85 | Get either the keccak for a given input, the zero hash, the empty string, or a random hash 86 | 87 | Usage: rex hash [OPTIONS] 88 | 89 | Options: 90 | --input The input to hash. 91 | -z, --zero The zero hash. 92 | -r, --random A random hash. 93 | -s, --string Hash of empty string 94 | -h, --help Print help 95 | ``` 96 | 97 | ### `rex receipt` 98 | 99 | ```Shell 100 | Get the transaction's receipt. 101 | 102 | Usage: rex receipt [RPC_URL] 103 | 104 | Arguments: 105 | 106 | [RPC_URL] [env: RPC_URL=] [default: http://localhost:8545] 107 | 108 | Options: 109 | -h, --help Print help 110 | ``` 111 | 112 | ### `rex transaction` 113 | 114 | ```Shell 115 | Get the transaction's info. 116 | 117 | Usage: rex transaction [RPC_URL] 118 | 119 | Arguments: 120 | 121 | [RPC_URL] [env: RPC_URL=] [default: http://localhost:8545] 122 | 123 | Options: 124 | -h, --help Print help 125 | ``` 126 | 127 | ### `rex balance` 128 | 129 | ```Shell 130 | Get the account's balance info. 131 | 132 | Usage: rex balance [OPTIONS] [RPC_URL] 133 | 134 | Arguments: 135 | 136 | [RPC_URL] [env: RPC_URL=] [default: http://localhost:8545] 137 | 138 | Options: 139 | --token Specify the token address, the ETH is used as default. 140 | --eth Display the balance in ETH. 141 | -h, --help Print help 142 | ``` 143 | 144 | ### `rex nonce` 145 | 146 | ```Shell 147 | Get the account's nonce. 148 | 149 | Usage: rex nonce [RPC_URL] 150 | 151 | Arguments: 152 | 153 | [RPC_URL] [env: RPC_URL=] [default: http://localhost:8545] 154 | 155 | Options: 156 | -h, --help Print help 157 | ``` 158 | 159 | ### `rex block-number` 160 | 161 | ```Shell 162 | Get the current block_number. 163 | 164 | Usage: rex block-number [RPC_URL] 165 | 166 | Arguments: 167 | [RPC_URL] [env: RPC_URL=] [default: http://localhost:8545] 168 | 169 | Options: 170 | -h, --help Print help 171 | ``` 172 | 173 | ### `rex signer` 174 | 175 | ```Shell 176 | Usage: rex signer 177 | 178 | Arguments: 179 | 180 | 181 | 182 | Options: 183 | -h, --help Print help 184 | ``` 185 | 186 | ### `rex chain-id` 187 | 188 | ```Shell 189 | Get the network's chain id. 190 | 191 | Usage: rex chain-id [OPTIONS] [RPC_URL] 192 | 193 | Arguments: 194 | [RPC_URL] [env: RPC_URL=] [default: http://localhost:8545] 195 | 196 | Options: 197 | -h, --hex Display the chain id as a hex-string. 198 | -h, --help Print help 199 | ``` 200 | 201 | ### `rex transfer` 202 | 203 | ```Shell 204 | Transfer funds to another wallet. 205 | 206 | Usage: rex transfer [OPTIONS] [RPC_URL] 207 | 208 | Arguments: 209 | 210 | 211 | [env: PRIVATE_KEY=] 212 | [RPC_URL] [env: RPC_URL=] [default: http://localhost:8545] 213 | 214 | Options: 215 | --token 216 | --nonce 217 | -b Do not wait for the transaction receipt 218 | --explorer-url Display transaction URL in the explorer. 219 | -h, --help Print help 220 | ``` 221 | 222 | ### `rex send` 223 | 224 | ```Shell 225 | Send a transaction 226 | 227 | Usage: rex send [OPTIONS] [VALUE] -- [SIGNATURE [ARGS]] 228 | 229 | Arguments: 230 | 231 | [VALUE] Value to send in wei [default: 0] 232 | [env: PRIVATE_KEY=] 233 | 234 | Options: 235 | --calldata [default: ] 236 | --chain-id 237 | --nonce 238 | --gas-limit 239 | --gas-price 240 | --priority-gas-price 241 | --rpc-url [env: RPC_URL=] [default: http://localhost:8545] 242 | -b Do not wait for the transaction receipt 243 | --explorer-url Display transaction URL in the explorer. 244 | -h, --help Print help 245 | ``` 246 | 247 | ### `rex call` 248 | 249 | ```Shell 250 | Make a call to a contract 251 | 252 | Usage: rex call [OPTIONS] [CALLDATA] [VALUE] -- [SIGNATURE [ARGS]] 253 | 254 | Arguments: 255 | 256 | [CALLDATA] [default: ] 257 | [VALUE] Value to send in wei [default: 0] 258 | 259 | Options: 260 | --from 261 | --gas-limit 262 | --max-fee-per-gas 263 | --explorer-url Display transaction URL in the explorer. 264 | --rpc-url [env: RPC_URL=] [default: http://localhost:8545] 265 | -h, --help Print help 266 | ``` 267 | 268 | ### `rex deploy` 269 | 270 | ```Shell 271 | Deploy a contract 272 | 273 | Usage: rex deploy [OPTIONS] [VALUE] -- [SIGNATURE [ARGS]] 274 | 275 | Arguments: 276 | 277 | [VALUE] Value to send in wei [default: 0] 278 | [env: PRIVATE_KEY=] 279 | 280 | 281 | Options: 282 | --chain-id 283 | --nonce 284 | --gas-limit 285 | --gas-price 286 | --priority-gas-price 287 | --print-address Only print the contract address 288 | --rpc-url [env: RPC_URL=] [default: http://localhost:8545] 289 | -b Do not wait for the transaction receipt 290 | --explorer-url Display transaction URL in the explorer. 291 | -h, --help Print help 292 | ``` 293 | 294 | ## Examples 295 | 296 | A curated list of examples as GIFs. 297 | 298 | TODO 299 | -------------------------------------------------------------------------------- /cli/src/cli.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::l2; 2 | use crate::utils::{parse_contract_creation, parse_func_call, parse_hex}; 3 | use crate::{ 4 | commands::autocomplete, 5 | common::{CallArgs, DeployArgs, SendArgs, TransferArgs}, 6 | utils::parse_private_key, 7 | }; 8 | use clap::{ArgAction, Parser, Subcommand}; 9 | use ethrex_common::{Address, Bytes, H256, H520}; 10 | use keccak_hash::keccak; 11 | use rex_sdk::{ 12 | balance_in_eth, 13 | client::{ 14 | EthClient, Overrides, 15 | eth::{get_address_from_message_and_signature, get_address_from_secret_key}, 16 | }, 17 | transfer, wait_for_transaction_receipt, 18 | }; 19 | use secp256k1::{Message, SecretKey}; 20 | 21 | pub const VERSION_STRING: &str = env!("CARGO_PKG_VERSION"); 22 | 23 | pub async fn start() -> eyre::Result<()> { 24 | let CLI { command } = CLI::parse(); 25 | command.run().await 26 | } 27 | 28 | #[allow(clippy::upper_case_acronyms)] 29 | #[derive(Parser)] 30 | #[command(name="rex", author, version=VERSION_STRING, about, long_about = None)] 31 | pub(crate) struct CLI { 32 | #[command(subcommand)] 33 | command: Command, 34 | } 35 | 36 | #[derive(Subcommand)] 37 | pub(crate) enum Command { 38 | #[clap( 39 | about = "Get either the account's address from private key, the zero address, or a random address", 40 | visible_aliases = ["addr", "a"] 41 | )] 42 | Address { 43 | #[arg(long, value_parser = parse_private_key, conflicts_with_all = ["zero", "random"], required_unless_present_any = ["zero", "random"], env = "PRIVATE_KEY", help = "The private key to derive the address from.")] 44 | from_private_key: Option, 45 | #[arg(short, long, action = ArgAction::SetTrue, conflicts_with_all = ["from_private_key", "random"], required_unless_present_any = ["from_private_key", "random"], help = "The zero address.")] 46 | zero: bool, 47 | #[arg(short, long, action = ArgAction::SetTrue, conflicts_with_all = ["from_private_key", "zero"], required_unless_present_any = ["from_private_key", "zero"], help = "A random address.")] 48 | random: bool, 49 | }, 50 | #[clap(subcommand, about = "Generate shell completion scripts.")] 51 | Autocomplete(autocomplete::Command), 52 | #[clap(about = "Get the account's balance info.", visible_aliases = ["bal", "b"])] 53 | Balance { 54 | account: Address, 55 | #[clap( 56 | long = "token", 57 | conflicts_with = "eth", 58 | help = "Specify the token address, the ETH is used as default." 59 | )] 60 | token_address: Option
, 61 | #[arg( 62 | long = "eth", 63 | required = false, 64 | default_value_t = false, 65 | conflicts_with = "token_address", 66 | help = "Display the balance in ETH." 67 | )] 68 | eth: bool, 69 | #[arg(default_value = "http://localhost:8545", env = "RPC_URL")] 70 | rpc_url: String, 71 | }, 72 | #[clap(about = "Get the current block_number.", visible_alias = "bl")] 73 | BlockNumber { 74 | #[arg(default_value = "http://localhost:8545", env = "RPC_URL")] 75 | rpc_url: String, 76 | }, 77 | #[clap(about = "Make a call to a contract")] 78 | Call { 79 | #[clap(flatten)] 80 | args: CallArgs, 81 | #[arg(long, default_value = "http://localhost:8545", env = "RPC_URL")] 82 | rpc_url: String, 83 | }, 84 | #[clap(about = "Get the network's chain id.")] 85 | ChainId { 86 | #[arg( 87 | long, 88 | default_value_t = false, 89 | help = "Display the chain id as a hex-string." 90 | )] 91 | hex: bool, 92 | #[arg(default_value = "http://localhost:8545", env = "RPC_URL")] 93 | rpc_url: String, 94 | }, 95 | #[clap(about = "Returns code at a given address")] 96 | Code { 97 | address: Address, 98 | #[arg( 99 | short = 'B', 100 | long = "block", 101 | required = false, 102 | default_value_t = String::from("latest"), 103 | help = "defaultBlock parameter: can be integer block number, 'earliest', 'finalized', 'safe', 'latest' or 'pending'" 104 | )] 105 | block: String, 106 | #[arg(default_value = "http://localhost:8545", env = "RPC_URL")] 107 | rpc_url: String, 108 | }, 109 | #[clap(about = "Deploy a contract")] 110 | Deploy { 111 | #[clap(flatten)] 112 | args: DeployArgs, 113 | #[arg(long, default_value = "http://localhost:8545", env = "RPC_URL")] 114 | rpc_url: String, 115 | }, 116 | #[clap( 117 | about = "Get either the keccak for a given input, the zero hash, the empty string, or a random hash", 118 | visible_alias = "h" 119 | )] 120 | Hash { 121 | #[arg(long, value_parser = parse_hex, conflicts_with_all = ["zero", "random", "string"], required_unless_present_any = ["zero", "random", "string"], help = "The input to hash.")] 122 | input: Option, 123 | #[arg(short, long, action = ArgAction::SetTrue, conflicts_with_all = ["input", "random", "string"], required_unless_present_any = ["input", "random", "string"], help = "The zero hash.")] 124 | zero: bool, 125 | #[arg(short, long, action = ArgAction::SetTrue, conflicts_with_all = ["input", "zero", "string"], required_unless_present_any = ["input", "zero", "string"], help = "A random hash.")] 126 | random: bool, 127 | #[arg(short, long, action = ArgAction::SetTrue, conflicts_with_all = ["input", "zero", "random"], required_unless_present_any = ["input", "zero", "random"], help = "Hash of empty string")] 128 | string: bool, 129 | }, 130 | #[clap(subcommand, about = "L2 specific commands.")] 131 | L2(l2::Command), 132 | #[clap(about = "Get the account's nonce.", visible_aliases = ["n"])] 133 | Nonce { 134 | account: Address, 135 | #[arg(default_value = "http://localhost:8545", env = "RPC_URL")] 136 | rpc_url: String, 137 | }, 138 | #[clap(about = "Get the transaction's receipt.", visible_alias = "r")] 139 | Receipt { 140 | tx_hash: H256, 141 | #[arg(default_value = "http://localhost:8545", env = "RPC_URL")] 142 | rpc_url: String, 143 | }, 144 | #[clap(about = "Send a transaction")] 145 | Send { 146 | #[clap(flatten)] 147 | args: SendArgs, 148 | #[arg(long, default_value = "http://localhost:8545", env = "RPC_URL")] 149 | rpc_url: String, 150 | }, 151 | #[clap(about = "Sign a message with a private key")] 152 | Sign { 153 | #[arg(value_parser = parse_hex, help = "Message to be signed with the private key.")] 154 | msg: Bytes, 155 | #[arg(value_parser = parse_private_key, env = "PRIVATE_KEY", help = "The private key to sign the message.")] 156 | private_key: SecretKey, 157 | }, 158 | Signer { 159 | #[arg(value_parser = parse_hex)] 160 | message: Bytes, 161 | #[arg(value_parser = parse_hex)] 162 | signature: Bytes, 163 | }, 164 | #[clap(about = "Get the transaction's info.", visible_aliases = ["tx", "t"])] 165 | Transaction { 166 | tx_hash: H256, 167 | #[arg(default_value = "http://localhost:8545", env = "RPC_URL")] 168 | rpc_url: String, 169 | }, 170 | #[clap(about = "Transfer funds to another wallet.")] 171 | Transfer { 172 | #[clap(flatten)] 173 | args: TransferArgs, 174 | #[arg(default_value = "http://localhost:8545", env = "RPC_URL")] 175 | rpc_url: String, 176 | }, 177 | #[clap(about = "Verify if the signature of a message was made by an account")] 178 | VerifySignature { 179 | #[arg(value_parser = parse_hex)] 180 | message: Bytes, 181 | #[arg(value_parser = parse_hex)] 182 | signature: Bytes, 183 | address: Address, 184 | }, 185 | } 186 | 187 | impl Command { 188 | pub async fn run(self) -> eyre::Result<()> { 189 | match self { 190 | Command::L2(cmd) => cmd.run().await?, 191 | Command::Autocomplete(cmd) => cmd.run()?, 192 | Command::Balance { 193 | account, 194 | token_address, 195 | eth, 196 | rpc_url, 197 | } => { 198 | let eth_client = EthClient::new(&rpc_url); 199 | let account_balance = if let Some(token_address) = token_address { 200 | eth_client.get_token_balance(account, token_address).await? 201 | } else { 202 | eth_client.get_balance(account).await? 203 | }; 204 | 205 | println!("{}", balance_in_eth(eth, account_balance)); 206 | } 207 | Command::BlockNumber { rpc_url } => { 208 | let eth_client = EthClient::new(&rpc_url); 209 | 210 | let block_number = eth_client.get_block_number().await?; 211 | 212 | println!("{block_number}"); 213 | } 214 | Command::Transaction { tx_hash, rpc_url } => { 215 | let eth_client = EthClient::new(&rpc_url); 216 | 217 | let tx = eth_client 218 | .get_transaction_by_hash(tx_hash) 219 | .await? 220 | .ok_or(eyre::Error::msg("Not found"))?; 221 | 222 | println!("{tx}"); 223 | } 224 | Command::Receipt { tx_hash, rpc_url } => { 225 | let eth_client = EthClient::new(&rpc_url); 226 | 227 | let receipt = eth_client 228 | .get_transaction_receipt(tx_hash) 229 | .await? 230 | .ok_or(eyre::Error::msg("Not found"))?; 231 | 232 | println!("{:x?}", receipt.tx_info); 233 | } 234 | Command::Nonce { account, rpc_url } => { 235 | let eth_client = EthClient::new(&rpc_url); 236 | 237 | let nonce = eth_client.get_nonce(account).await?; 238 | 239 | println!("{nonce}"); 240 | } 241 | Command::Address { 242 | from_private_key, 243 | zero, 244 | random, 245 | } => { 246 | let address = if let Some(private_key) = from_private_key { 247 | get_address_from_secret_key(&private_key)? 248 | } else if zero { 249 | Address::zero() 250 | } else if random { 251 | Address::random() 252 | } else { 253 | return Err(eyre::Error::msg("No option provided")); 254 | }; 255 | 256 | println!("{address:#x}"); 257 | } 258 | Command::Hash { 259 | input, 260 | zero, 261 | random, 262 | string, 263 | } => { 264 | let hash = if let Some(input) = input { 265 | keccak(&input) 266 | } else if zero { 267 | H256::zero() 268 | } else if random { 269 | H256::random() 270 | } else if string { 271 | keccak(b"") 272 | } else { 273 | return Err(eyre::Error::msg("No option provided")); 274 | }; 275 | 276 | println!("{hash:#x}"); 277 | } 278 | Command::Signer { message, signature } => { 279 | let signer = get_address_from_message_and_signature(message, signature)?; 280 | 281 | println!("{signer:x?}"); 282 | } 283 | Command::Transfer { args, rpc_url } => { 284 | if args.token_address.is_some() { 285 | todo!("Handle ERC20 transfers") 286 | } 287 | 288 | if args.explorer_url { 289 | todo!("Display transaction URL in the explorer") 290 | } 291 | 292 | let from = get_address_from_secret_key(&args.private_key)?; 293 | 294 | let client = EthClient::new(&rpc_url); 295 | 296 | let tx_hash = transfer( 297 | args.amount, 298 | from, 299 | args.to, 300 | args.private_key, 301 | &client, 302 | Overrides { 303 | value: Some(args.amount), 304 | nonce: args.nonce, 305 | ..Default::default() 306 | }, 307 | ) 308 | .await?; 309 | 310 | println!("{tx_hash:#x}"); 311 | 312 | if !args.cast { 313 | wait_for_transaction_receipt(tx_hash, &client, 100, false).await?; 314 | } 315 | } 316 | Command::Send { args, rpc_url } => { 317 | if args.explorer_url { 318 | todo!("Display transaction URL in the explorer") 319 | } 320 | 321 | let from = get_address_from_secret_key(&args.private_key)?; 322 | 323 | let client = EthClient::new(&rpc_url); 324 | 325 | let calldata = if !args.calldata.is_empty() { 326 | args.calldata 327 | } else { 328 | parse_func_call(args._args)? 329 | }; 330 | 331 | let tx = client 332 | .build_eip1559_transaction( 333 | args.to, 334 | from, 335 | calldata, 336 | Overrides { 337 | value: Some(args.value), 338 | chain_id: args.chain_id, 339 | nonce: args.nonce, 340 | gas_limit: args.gas_limit, 341 | max_fee_per_gas: args.max_fee_per_gas, 342 | max_priority_fee_per_gas: args.max_priority_fee_per_gas, 343 | from: Some(from), 344 | ..Default::default() 345 | }, 346 | 10, 347 | ) 348 | .await?; 349 | 350 | let tx_hash = client 351 | .send_eip1559_transaction(&tx, &args.private_key) 352 | .await?; 353 | 354 | println!("{tx_hash:#x}",); 355 | 356 | if !args.cast { 357 | wait_for_transaction_receipt(tx_hash, &client, 100, false).await?; 358 | } 359 | } 360 | Command::Call { args, rpc_url } => { 361 | if args.explorer_url { 362 | todo!("Display transaction URL in the explorer") 363 | } 364 | 365 | let client = EthClient::new(&rpc_url); 366 | 367 | let calldata = if !args.calldata.is_empty() { 368 | args.calldata 369 | } else { 370 | parse_func_call(args._args)? 371 | }; 372 | 373 | let result = client 374 | .call( 375 | args.to, 376 | calldata, 377 | Overrides { 378 | from: args.from, 379 | value: args.value.into(), 380 | gas_limit: args.gas_limit, 381 | max_fee_per_gas: args.max_fee_per_gas, 382 | ..Default::default() 383 | }, 384 | ) 385 | .await?; 386 | 387 | println!("{result}"); 388 | } 389 | Command::Deploy { args, rpc_url } => { 390 | if args.explorer_url { 391 | todo!("Display transaction URL in the explorer") 392 | } 393 | 394 | let from = get_address_from_secret_key(&args.private_key)?; 395 | 396 | let client = EthClient::new(&rpc_url); 397 | 398 | let init_code = if args._args.len() > 0 { 399 | let init_args = parse_contract_creation(args._args)?; 400 | [args.bytecode, init_args].concat().into() 401 | } else { 402 | args.bytecode 403 | }; 404 | 405 | let (tx_hash, deployed_contract_address) = client 406 | .deploy( 407 | from, 408 | args.private_key, 409 | init_code, 410 | Overrides { 411 | value: args.value.into(), 412 | nonce: args.nonce, 413 | chain_id: args.chain_id, 414 | gas_limit: args.gas_limit, 415 | max_fee_per_gas: args.max_fee_per_gas, 416 | max_priority_fee_per_gas: args.max_priority_fee_per_gas, 417 | ..Default::default() 418 | }, 419 | ) 420 | .await?; 421 | 422 | if args.print_address { 423 | println!("{deployed_contract_address:#x}"); 424 | } else { 425 | println!("Contract deployed in tx: {tx_hash:#x}"); 426 | println!("Contract address: {deployed_contract_address:#x}"); 427 | } 428 | let silent = args.print_address; 429 | 430 | if !args.cast { 431 | wait_for_transaction_receipt(tx_hash, &client, 100, silent).await?; 432 | } 433 | } 434 | Command::ChainId { hex, rpc_url } => { 435 | let eth_client = EthClient::new(&rpc_url); 436 | 437 | let chain_id = eth_client.get_chain_id().await?; 438 | 439 | if hex { 440 | println!("{chain_id:#x}"); 441 | } else { 442 | println!("{chain_id}"); 443 | } 444 | } 445 | Command::Code { 446 | address, 447 | block, 448 | rpc_url, 449 | } => { 450 | let eth_client = EthClient::new(&rpc_url); 451 | 452 | let code = eth_client.get_code(address, block).await?; 453 | 454 | println!("{}", code); 455 | } 456 | 457 | // Signature computed as a 0x45 signature, as described in EIP-191 (https://eips.ethereum.org/EIPS/eip-191), 458 | // then it has an extra byte concatenated at the end, which is a scalar value added to the signatures parity, 459 | // as described in the Yellow Paper Section 4.2 in the specification of a transaction's w field. (https://ethereum.github.io/yellowpaper/paper.pdf) 460 | Command::Sign { msg, private_key } => { 461 | let payload = [ 462 | b"\x19Ethereum Signed Message:\n", 463 | msg.len().to_string().as_bytes(), 464 | msg.as_ref(), 465 | ] 466 | .concat(); 467 | 468 | let signed_msg = secp256k1::SECP256K1.sign_ecdsa_recoverable( 469 | &Message::from_digest(*keccak(&payload).as_fixed_bytes()), 470 | &private_key, 471 | ); 472 | 473 | let (msg_signature_recovery_id, msg_signature) = signed_msg.serialize_compact(); 474 | 475 | let msg_signature_recovery_id = msg_signature_recovery_id.to_i32() + 27; 476 | 477 | let encoded_signature = 478 | [&msg_signature[..], &[msg_signature_recovery_id as u8]].concat(); 479 | 480 | println!("0x{:x}", H520::from_slice(&encoded_signature)); 481 | } 482 | Command::VerifySignature { 483 | message, 484 | signature, 485 | address, 486 | } => { 487 | println!( 488 | "{}", 489 | get_address_from_message_and_signature(message, signature)? == address 490 | ); 491 | } 492 | }; 493 | Ok(()) 494 | } 495 | } 496 | -------------------------------------------------------------------------------- /cli/src/commands/autocomplete.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::CLI; 2 | use clap::{CommandFactory, Subcommand, ValueEnum}; 3 | use clap_complete::{aot::Shell, generate}; 4 | use std::fs::{File, OpenOptions}; 5 | use std::io::{self, BufRead, Write}; 6 | 7 | #[derive(Subcommand)] 8 | pub(crate) enum Command { 9 | #[clap(about = "Generate autocomplete shell script.")] 10 | Generate { 11 | #[clap(short = 's', long = "shell", help = "Default: $SHELL")] 12 | shell: Option, 13 | }, 14 | #[clap(about = "Generate and install autocomplete shell script.")] 15 | Install { 16 | #[clap(short = 's', long = "shell", help = "Default: $SHELL")] 17 | shell: Option, 18 | }, 19 | } 20 | 21 | impl Command { 22 | pub fn run(self) -> eyre::Result<()> { 23 | match self { 24 | Command::Generate { shell } => generate_bash_script(shell), 25 | Command::Install { shell } => install_bash_script(shell), 26 | } 27 | } 28 | } 29 | 30 | fn get_shellrc_path(shell: Shell) -> eyre::Result { 31 | match shell { 32 | Shell::Bash => Ok(".bashrc".to_owned()), 33 | Shell::Zsh => Ok(".zshrc".to_owned()), 34 | Shell::Fish => Ok(".config/fish/config.fish".to_owned()), 35 | Shell::Elvish => Ok(".elvish/rc.elv".to_owned()), 36 | Shell::PowerShell => Ok(".config/powershell/Microsoft.PowerShell_profile.ps1".to_owned()), 37 | _ => Err(eyre::eyre!( 38 | "Your shell is not supported. Supported shells are: {:?}", 39 | Shell::value_variants() 40 | )), 41 | } 42 | } 43 | 44 | fn get_shell(arg: Option) -> eyre::Result { 45 | if let Some(shell) = arg { 46 | Ok(shell) 47 | } else if let Some(env_shell) = Shell::from_env() { 48 | Ok(env_shell) 49 | } else { 50 | Err(eyre::eyre!( 51 | "Your shell is not supported. Supported shells are: {:?}", 52 | Shell::value_variants() 53 | )) 54 | } 55 | } 56 | 57 | fn generate_bash_script(shell_arg: Option) -> eyre::Result<()> { 58 | let shell = get_shell(shell_arg)?; 59 | generate(shell, &mut CLI::command(), "ethrex_l2", &mut io::stdout()); 60 | Ok(()) 61 | } 62 | 63 | fn shellrc_command_exists(shellrc_path: &std::path::Path, shell: Shell) -> eyre::Result { 64 | let expected_string = if shell == Shell::Elvish { 65 | "-source $HOME/.ethrex-l2-completion" 66 | } else { 67 | ". $HOME/.ethrex-l2-completion" 68 | }; 69 | 70 | let file = File::open(shellrc_path)?; 71 | let reader = io::BufReader::new(file); 72 | let lines = reader.lines(); 73 | for line in lines { 74 | let line = line?; 75 | if line == expected_string { 76 | return Ok(true); 77 | } 78 | } 79 | 80 | Ok(false) 81 | } 82 | 83 | fn install_bash_script(shell_arg: Option) -> eyre::Result<()> { 84 | let shell = get_shell(shell_arg)?; 85 | 86 | let file_path = dirs::home_dir() 87 | .ok_or(eyre::eyre!("Cannot find home directory."))? 88 | .join(".ethrex-l2-completion"); 89 | let mut file = File::create(file_path)?; 90 | generate(shell, &mut CLI::command(), "ethrex_l2", &mut file); 91 | file.flush()?; 92 | 93 | let shellrc_path = dirs::home_dir() 94 | .ok_or(eyre::eyre!("Cannot find home directory."))? 95 | .join(get_shellrc_path(shell)?); 96 | 97 | if !shellrc_command_exists(&shellrc_path, shell)? { 98 | let mut file = OpenOptions::new().append(true).open(shellrc_path)?; 99 | if shell == Shell::Elvish { 100 | file.write_all(b"\n-source $HOME/.ethrex-l2-completion\n")?; 101 | } else { 102 | file.write_all(b"\n. $HOME/.ethrex-l2-completion\n")?; 103 | } 104 | file.flush()?; 105 | } 106 | 107 | println!("Autocomplete script installed. To apply changes, restart your shell."); 108 | Ok(()) 109 | } 110 | -------------------------------------------------------------------------------- /cli/src/commands/l2.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | cli::Command as EthCommand, 3 | common::{BalanceArgs, CallArgs, DeployArgs, SendArgs, TransferArgs}, 4 | utils::{parse_private_key, parse_u256}, 5 | }; 6 | use clap::Subcommand; 7 | use ethrex_common::{Address, H256, U256}; 8 | use rex_sdk::{ 9 | client::{EthClient, Overrides, eth::get_address_from_secret_key}, 10 | l2::{ 11 | deposit::deposit, 12 | withdraw::{claim_withdraw, get_withdraw_merkle_proof, withdraw}, 13 | }, 14 | wait_for_transaction_receipt, 15 | }; 16 | use secp256k1::SecretKey; 17 | 18 | #[derive(Subcommand)] 19 | pub(crate) enum Command { 20 | #[clap(about = "Get the account's balance on L2.", visible_aliases = ["bal", "b"])] 21 | Balance { 22 | #[clap(flatten)] 23 | args: BalanceArgs, 24 | #[arg( 25 | default_value = "http://localhost:1729", 26 | env = "RPC_URL", 27 | help = "L2 RPC URL" 28 | )] 29 | rpc_url: String, 30 | }, 31 | #[clap(about = "Get the current block_number.", visible_alias = "bl")] 32 | BlockNumber { 33 | #[arg( 34 | default_value = "http://localhost:1729", 35 | env = "RPC_URL", 36 | help = "L2 RPC URL" 37 | )] 38 | rpc_url: String, 39 | }, 40 | #[clap(about = "Make a call to a contract")] 41 | Call { 42 | #[clap(flatten)] 43 | args: CallArgs, 44 | #[arg( 45 | default_value = "http://localhost:1729", 46 | env = "RPC_URL", 47 | help = "L2 RPC URL" 48 | )] 49 | rpc_url: String, 50 | }, 51 | #[clap(about = "Get the network's chain id.")] 52 | ChainId { 53 | #[arg( 54 | short, 55 | long, 56 | default_value_t = false, 57 | help = "Display the chain id as a hex-string." 58 | )] 59 | hex: bool, 60 | #[arg(default_value = "http://localhost:1729", env = "RPC_URL")] 61 | rpc_url: String, 62 | }, 63 | #[clap(about = "Finalize a pending withdrawal.")] 64 | ClaimWithdraw { 65 | l2_withdrawal_tx_hash: H256, 66 | #[clap( 67 | short = 'b', 68 | required = false, 69 | help = "Send the request asynchronously." 70 | )] 71 | cast: bool, 72 | #[arg(value_parser = parse_private_key, env = "PRIVATE_KEY")] 73 | private_key: SecretKey, 74 | #[arg(env = "BRIDGE_ADDRESS")] 75 | bridge_address: Address, 76 | #[arg(env = "L1_RPC_URL", default_value = "http://localhost:8545")] 77 | l1_rpc_url: String, 78 | #[arg(env = "RPC_URL", default_value = "http://localhost:1729")] 79 | rpc_url: String, 80 | }, 81 | #[clap(about = "Deploy a contract")] 82 | Deploy { 83 | #[clap(flatten)] 84 | args: DeployArgs, 85 | #[arg( 86 | default_value = "http://localhost:1729", 87 | env = "RPC_URL", 88 | help = "L2 RPC URL" 89 | )] 90 | rpc_url: String, 91 | }, 92 | #[clap(about = "Deposit funds into some wallet.")] 93 | Deposit { 94 | // TODO: Parse ether instead. 95 | #[clap(value_parser = parse_u256)] 96 | amount: U256, 97 | #[clap( 98 | long = "token", 99 | help = "ERC20 token address", 100 | long_help = "Specify the token address, the base token is used as default." 101 | )] 102 | token_address: Option
, 103 | #[clap( 104 | long = "to", 105 | help = "Specify the wallet in which you want to deposit your funds." 106 | )] 107 | to: Option
, 108 | #[clap( 109 | short = 'b', 110 | required = false, 111 | help = "Send the request asynchronously." 112 | )] 113 | cast: bool, 114 | #[clap( 115 | long, 116 | short = 'e', 117 | required = false, 118 | help = "Display transaction URL in the explorer." 119 | )] 120 | explorer_url: bool, 121 | #[clap(value_parser = parse_private_key, env = "PRIVATE_KEY")] 122 | private_key: SecretKey, 123 | #[arg(env = "BRIDGE_ADDRESS")] 124 | bridge_address: Address, 125 | #[arg(default_value = "http://localhost:8545", env = "L1_RPC_URL")] 126 | l1_rpc_url: String, 127 | }, 128 | #[clap(about = "Get the account's nonce.", visible_aliases = ["n"])] 129 | Nonce { 130 | account: Address, 131 | #[arg(default_value = "http://localhost:1729", env = "RPC_URL")] 132 | rpc_url: String, 133 | }, 134 | #[clap(about = "Get the transaction's receipt.", visible_alias = "r")] 135 | Receipt { 136 | tx_hash: H256, 137 | #[arg( 138 | default_value = "http://localhost:1729", 139 | env = "RPC_URL", 140 | help = "L2 RPC URL" 141 | )] 142 | rpc_url: String, 143 | }, 144 | #[clap(about = "Send a transaction")] 145 | Send { 146 | #[clap(flatten)] 147 | args: SendArgs, 148 | #[arg( 149 | default_value = "http://localhost:1729", 150 | env = "RPC_URL", 151 | help = "L2 RPC URL" 152 | )] 153 | rpc_url: String, 154 | }, 155 | #[clap(about = "Get the transaction's info.", visible_aliases = ["tx", "t"])] 156 | Transaction { 157 | tx_hash: H256, 158 | #[arg( 159 | default_value = "http://localhost:1729", 160 | env = "RPC_URL", 161 | help = "L2 RPC URL" 162 | )] 163 | rpc_url: String, 164 | }, 165 | #[clap(about = "Transfer funds to another wallet.")] 166 | Transfer { 167 | #[clap(flatten)] 168 | args: TransferArgs, 169 | #[arg( 170 | default_value = "http://localhost:1729", 171 | env = "RPC_URL", 172 | help = "L2 RPC URL" 173 | )] 174 | rpc_url: String, 175 | }, 176 | #[clap(about = "Withdraw funds from the wallet.")] 177 | Withdraw { 178 | // TODO: Parse ether instead. 179 | #[clap(value_parser = parse_u256)] 180 | amount: U256, 181 | #[clap(long = "nonce")] 182 | nonce: Option, 183 | #[clap( 184 | long = "token", 185 | help = "ERC20 token address", 186 | long_help = "Specify the token address, the base token is used as default." 187 | )] 188 | token_address: Option
, 189 | #[clap( 190 | short = 'b', 191 | required = false, 192 | help = "Send the request asynchronously." 193 | )] 194 | cast: bool, 195 | #[clap( 196 | long, 197 | required = false, 198 | help = "Display transaction URL in the explorer." 199 | )] 200 | explorer_url: bool, 201 | #[arg(value_parser = parse_private_key, env = "PRIVATE_KEY")] 202 | private_key: SecretKey, 203 | #[arg( 204 | default_value = "http://localhost:1729", 205 | env = "RPC_URL", 206 | help = "L2 RPC URL" 207 | )] 208 | rpc_url: String, 209 | }, 210 | #[clap(about = "Get the withdrawal merkle proof of a transaction.")] 211 | WithdrawalProof { 212 | l2_withdrawal_tx_hash: H256, 213 | #[arg( 214 | default_value = "http://localhost:1729", 215 | env = "RPC_URL", 216 | help = "L2 RPC URL" 217 | )] 218 | rpc_url: String, 219 | }, 220 | } 221 | 222 | impl Command { 223 | pub async fn run(self) -> eyre::Result<()> { 224 | match self { 225 | Command::Deposit { 226 | amount, 227 | token_address, 228 | to, 229 | cast, 230 | explorer_url, 231 | private_key, 232 | l1_rpc_url, 233 | bridge_address, 234 | } => { 235 | if explorer_url { 236 | todo!("Display transaction URL in the explorer") 237 | } 238 | 239 | if to.is_some() { 240 | // There are two ways of depositing funds into the L2: 241 | // 1. Directly transferring funds to the bridge. 242 | // 2. Depositing through a contract call to the deposit method of the bridge. 243 | // The second method is not handled in the CLI yet. 244 | todo!("Handle deposits through contract") 245 | } 246 | 247 | if token_address.is_some() { 248 | todo!("Handle ERC20 deposits") 249 | } 250 | 251 | let from = get_address_from_secret_key(&private_key)?; 252 | 253 | let eth_client = EthClient::new(&l1_rpc_url); 254 | 255 | let tx_hash = deposit( 256 | amount, 257 | from, 258 | private_key, 259 | ð_client, 260 | bridge_address, 261 | Overrides::default(), 262 | ) 263 | .await?; 264 | 265 | println!("Deposit sent: {tx_hash:#x}"); 266 | 267 | if !cast { 268 | wait_for_transaction_receipt(tx_hash, ð_client, 100, false).await?; 269 | } 270 | } 271 | Command::ClaimWithdraw { 272 | l2_withdrawal_tx_hash, 273 | cast, 274 | private_key, 275 | l1_rpc_url, 276 | rpc_url, 277 | bridge_address, 278 | } => { 279 | let from = get_address_from_secret_key(&private_key)?; 280 | 281 | let eth_client = EthClient::new(&l1_rpc_url); 282 | 283 | let client = EthClient::new(&rpc_url); 284 | 285 | let tx_hash = claim_withdraw( 286 | l2_withdrawal_tx_hash, 287 | U256::default(), // TODO: Fix this 288 | from, 289 | private_key, 290 | &client, 291 | ð_client, 292 | bridge_address, 293 | ) 294 | .await?; 295 | 296 | println!("Withdrawal claim sent: {tx_hash:#x}"); 297 | 298 | if !cast { 299 | wait_for_transaction_receipt(tx_hash, ð_client, 100, false).await?; 300 | } 301 | } 302 | Command::Withdraw { 303 | amount, 304 | nonce, 305 | token_address, 306 | cast, 307 | explorer_url, 308 | private_key, 309 | rpc_url, 310 | } => { 311 | if explorer_url { 312 | todo!("Display transaction URL in the explorer") 313 | } 314 | 315 | if token_address.is_some() { 316 | todo!("Handle ERC20 withdrawals") 317 | } 318 | 319 | let from = get_address_from_secret_key(&private_key)?; 320 | 321 | let client = EthClient::new(&rpc_url); 322 | 323 | let tx_hash = withdraw(amount, from, private_key, &client, nonce).await?; 324 | 325 | println!("Withdrawal sent: {tx_hash:#x}"); 326 | 327 | if !cast { 328 | wait_for_transaction_receipt(tx_hash, &client, 100, false).await?; 329 | } 330 | } 331 | Command::WithdrawalProof { 332 | l2_withdrawal_tx_hash, 333 | rpc_url, 334 | } => { 335 | let client = EthClient::new(&rpc_url); 336 | 337 | let (_index, path) = 338 | get_withdraw_merkle_proof(&client, l2_withdrawal_tx_hash).await?; 339 | 340 | println!("{path:?}"); 341 | } 342 | Command::BlockNumber { rpc_url } => { 343 | Box::pin(async { EthCommand::BlockNumber { rpc_url }.run().await }).await? 344 | } 345 | Command::Transaction { tx_hash, rpc_url } => { 346 | Box::pin(async { EthCommand::Transaction { tx_hash, rpc_url }.run().await }).await? 347 | } 348 | Command::Receipt { tx_hash, rpc_url } => { 349 | Box::pin(async { EthCommand::Receipt { tx_hash, rpc_url }.run().await }).await? 350 | } 351 | Command::Balance { args, rpc_url } => { 352 | Box::pin(async { 353 | EthCommand::Balance { 354 | account: args.account, 355 | token_address: args.token_address, 356 | eth: args.eth, 357 | rpc_url, 358 | } 359 | .run() 360 | .await 361 | }) 362 | .await? 363 | } 364 | Command::Nonce { account, rpc_url } => { 365 | Box::pin(async { EthCommand::Nonce { account, rpc_url }.run().await }).await? 366 | } 367 | Command::Transfer { args, rpc_url } => { 368 | Box::pin(async { EthCommand::Transfer { args, rpc_url }.run().await }).await? 369 | } 370 | Command::Send { args, rpc_url } => { 371 | Box::pin(async { EthCommand::Send { args, rpc_url }.run().await }).await? 372 | } 373 | Command::Call { args, rpc_url } => { 374 | Box::pin(async { EthCommand::Call { args, rpc_url }.run().await }).await?; 375 | } 376 | Command::Deploy { args, rpc_url } => { 377 | Box::pin(async { EthCommand::Deploy { args, rpc_url }.run().await }).await? 378 | } 379 | Command::ChainId { hex, rpc_url } => { 380 | Box::pin(async { EthCommand::ChainId { hex, rpc_url }.run().await }).await? 381 | } 382 | }; 383 | Ok(()) 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /cli/src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod autocomplete; 2 | pub(crate) mod l2; 3 | -------------------------------------------------------------------------------- /cli/src/common.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{parse_hex, parse_private_key, parse_u256}; 2 | use clap::Parser; 3 | use ethrex_common::{Address, Bytes, U256}; 4 | use secp256k1::SecretKey; 5 | 6 | #[derive(Parser)] 7 | pub struct BalanceArgs { 8 | pub account: Address, 9 | #[clap( 10 | long = "token", 11 | help = "ERC20 token address", 12 | long_help = "Specify the token address, the base token is used as default." 13 | )] 14 | pub token_address: Option
, 15 | #[arg( 16 | long = "eth", 17 | required = false, 18 | default_value_t = false, 19 | help = "Display the balance in ETH." 20 | )] 21 | pub eth: bool, 22 | } 23 | 24 | #[derive(Parser)] 25 | pub struct TransferArgs { 26 | #[clap(value_parser = parse_u256)] 27 | pub amount: U256, 28 | pub to: Address, 29 | #[clap(long = "token", required = false)] 30 | pub token_address: Option
, 31 | #[clap(long = "nonce")] 32 | pub nonce: Option, 33 | #[clap( 34 | short = 'b', 35 | required = false, 36 | help = "Send the request asynchronously." 37 | )] 38 | pub cast: bool, 39 | #[clap( 40 | long, 41 | required = false, 42 | help = "Display transaction URL in the explorer." 43 | )] 44 | pub explorer_url: bool, 45 | #[clap(value_parser = parse_private_key, env = "PRIVATE_KEY", required = false)] 46 | pub private_key: SecretKey, 47 | } 48 | 49 | #[derive(Parser)] 50 | pub struct SendArgs { 51 | pub to: Address, 52 | #[clap( 53 | value_parser = parse_u256, 54 | default_value = "0", 55 | required = false, 56 | help = "Value to send in wei" 57 | )] 58 | pub value: U256, 59 | #[clap(long = "calldata", value_parser = parse_hex, required = false, default_value = "")] 60 | pub calldata: Bytes, 61 | #[clap(long = "chain-id", required = false)] 62 | pub chain_id: Option, 63 | #[clap(long = "nonce", required = false)] 64 | pub nonce: Option, 65 | #[clap(long = "gas-limit", required = false)] 66 | pub gas_limit: Option, 67 | #[clap(long = "gas-price", required = false)] 68 | pub max_fee_per_gas: Option, 69 | #[clap(long = "priority-gas-price", required = false)] 70 | pub max_priority_fee_per_gas: Option, 71 | #[clap( 72 | short = 'b', 73 | required = false, 74 | help = "Send the request asynchronously." 75 | )] 76 | pub cast: bool, 77 | #[clap( 78 | long, 79 | required = false, 80 | help = "Display transaction URL in the explorer." 81 | )] 82 | pub explorer_url: bool, 83 | #[clap(value_parser = parse_private_key, env = "PRIVATE_KEY", required = false)] 84 | pub private_key: SecretKey, 85 | #[arg(last = true, hide = true)] 86 | pub _args: Vec, 87 | } 88 | 89 | #[derive(Parser)] 90 | pub struct CallArgs { 91 | pub to: Address, 92 | #[clap(long, value_parser = parse_hex, required = false, default_value = "")] 93 | pub calldata: Bytes, 94 | #[clap( 95 | value_parser = parse_u256, 96 | default_value = "0", 97 | required = false, 98 | help = "Value to send in wei" 99 | )] 100 | pub value: U256, 101 | #[clap(long, required = false)] 102 | pub from: Option
, 103 | #[clap(long, required = false)] 104 | pub gas_limit: Option, 105 | #[clap(long, required = false)] 106 | pub max_fee_per_gas: Option, 107 | #[clap( 108 | long, 109 | required = false, 110 | help = "Display transaction URL in the explorer." 111 | )] 112 | pub explorer_url: bool, 113 | #[arg(last = true, hide = true)] 114 | pub _args: Vec, 115 | } 116 | 117 | #[derive(Parser)] 118 | pub struct DeployArgs { 119 | #[clap(value_parser = parse_hex)] 120 | pub bytecode: Bytes, 121 | #[clap( 122 | value_parser = parse_u256, 123 | default_value = "0", 124 | required = false, 125 | help = "Value to send in wei" 126 | )] 127 | pub value: U256, 128 | #[clap(long = "chain-id", required = false)] 129 | pub chain_id: Option, 130 | #[clap(long = "nonce", required = false)] 131 | pub nonce: Option, 132 | #[clap(long = "gas-limit", required = false)] 133 | pub gas_limit: Option, 134 | #[clap(long = "gas-price", required = false)] 135 | pub max_fee_per_gas: Option, 136 | #[clap(long = "priority-gas-price", required = false)] 137 | pub max_priority_fee_per_gas: Option, 138 | #[clap(long, required = false)] 139 | pub print_address: bool, 140 | #[clap( 141 | short = 'b', 142 | required = false, 143 | help = "Send the request asynchronously." 144 | )] 145 | pub cast: bool, 146 | #[clap( 147 | long, 148 | required = false, 149 | help = "Display transaction URL in the explorer." 150 | )] 151 | pub explorer_url: bool, 152 | #[arg(value_parser = parse_private_key, env = "PRIVATE_KEY", required = false)] 153 | pub private_key: SecretKey, 154 | #[arg(last = true, hide = true)] 155 | pub _args: Vec, 156 | } 157 | -------------------------------------------------------------------------------- /cli/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(warnings, rust_2018_idioms)] 2 | #![forbid(unsafe_code)] 3 | #![recursion_limit = "256"] 4 | 5 | pub mod cli; 6 | mod commands; 7 | mod common; 8 | mod utils; 9 | -------------------------------------------------------------------------------- /cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use rex::cli; 2 | 3 | #[tokio::main] 4 | async fn main() { 5 | tracing_subscriber::fmt() 6 | .with_max_level(tracing::Level::ERROR) 7 | .init(); 8 | 9 | match cli::start().await { 10 | Ok(_) => {} 11 | Err(err) => { 12 | tracing::error!("{err:?}"); 13 | std::process::exit(1); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cli/src/utils.rs: -------------------------------------------------------------------------------- 1 | use ethrex_common::{Address, Bytes, U256}; 2 | use hex::FromHexError; 3 | use rex_sdk::calldata::{Value, encode_calldata, encode_tuple, parse_signature}; 4 | use secp256k1::SecretKey; 5 | use std::str::FromStr; 6 | 7 | pub fn parse_private_key(s: &str) -> eyre::Result { 8 | Ok(SecretKey::from_slice(&parse_hex(s)?)?) 9 | } 10 | 11 | pub fn parse_u256(s: &str) -> eyre::Result { 12 | let parsed = if s.starts_with("0x") { 13 | U256::from_str(s)? 14 | } else { 15 | U256::from_dec_str(s)? 16 | }; 17 | Ok(parsed) 18 | } 19 | 20 | pub fn parse_hex(s: &str) -> eyre::Result { 21 | match s.strip_prefix("0x") { 22 | Some(s) => hex::decode(s).map(Into::into), 23 | None => hex::decode(s).map(Into::into), 24 | } 25 | } 26 | 27 | fn parse_call_args(args: Vec) -> eyre::Result)>> { 28 | let mut args_iter = args.iter(); 29 | let Some(signature) = args_iter.next() else { 30 | return Ok(None); 31 | }; 32 | let (_, params) = parse_signature(signature)?; 33 | let mut values = Vec::new(); 34 | for param in params { 35 | let val = args_iter 36 | .next() 37 | .ok_or(eyre::Error::msg("missing parameter for given signature"))?; 38 | values.push(match param.as_str() { 39 | "address" => Value::Address(Address::from_str(val)?), 40 | _ if param.starts_with("uint") => Value::Uint(U256::from_dec_str(val)?), 41 | _ if param.starts_with("int") => { 42 | if let Some(val) = val.strip_prefix("-") { 43 | let x = U256::from_str(val)?; 44 | if x.is_zero() { 45 | Value::Uint(x) 46 | } else { 47 | Value::Uint(U256::max_value() - x + 1) 48 | } 49 | } else { 50 | Value::Uint(U256::from_dec_str(val)?) 51 | } 52 | } 53 | "bool" => match val.as_str() { 54 | "true" => Value::Uint(U256::from(1)), 55 | "false" => Value::Uint(U256::from(0)), 56 | _ => Err(eyre::Error::msg("Invalid boolean"))?, 57 | }, 58 | "bytes" => Value::Bytes(hex::decode(val)?.into()), 59 | _ if param.starts_with("bytes") => Value::FixedBytes(hex::decode(val)?.into()), 60 | _ => todo!("type unsupported"), 61 | }); 62 | } 63 | Ok(Some((signature.to_string(), values))) 64 | } 65 | 66 | pub fn parse_func_call(args: Vec) -> eyre::Result { 67 | let Some((signature, values)) = parse_call_args(args)? else { 68 | return Ok(Bytes::new()); 69 | }; 70 | Ok(encode_calldata(&signature, &values)?.into()) 71 | } 72 | 73 | pub fn parse_contract_creation(args: Vec) -> eyre::Result { 74 | let Some((_signature, values)) = parse_call_args(args)? else { 75 | return Ok(Bytes::new()); 76 | }; 77 | Ok(encode_tuple(&values)?.into()) 78 | } 79 | -------------------------------------------------------------------------------- /sdk/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rex-sdk" 3 | version.workspace = true 4 | edition.workspace = true 5 | 6 | [dependencies] 7 | ethrex-l2.workspace = true 8 | ethrex-common.workspace = true 9 | ethrex-blockchain.workspace = true 10 | ethrex-rlp.workspace = true 11 | ethrex-rpc.workspace = true 12 | 13 | # Runtime 14 | tokio = "1.43.0" 15 | 16 | # Clients 17 | reqwest = { version = "0.12.7", features = ["json"] } 18 | jsonwebtoken = "9.3.0" 19 | 20 | # Crypto 21 | keccak-hash.workspace = true 22 | secp256k1.workspace = true 23 | 24 | # Utils 25 | hex.workspace = true 26 | itertools = "0.14.0" 27 | toml = "0.8.19" 28 | dirs = "6.0.0" 29 | envy = "0.4.2" 30 | thiserror.workspace = true 31 | 32 | # Logging 33 | log = "0.4" 34 | tracing = "0.1.41" 35 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } 36 | 37 | # Serde 38 | serde = "1.0.218" 39 | serde_json = "1.0.139" 40 | 41 | # Examples deps 42 | clap = { version = "4.3", features = ["derive", "env"] } 43 | clap_complete = "4.5.17" 44 | eyre = "0.6" 45 | 46 | [lib] 47 | path = "./src/sdk.rs" 48 | 49 | [[example]] 50 | name = "simple_usage" 51 | path = "./examples/simple_usage.rs" 52 | -------------------------------------------------------------------------------- /sdk/README.md: -------------------------------------------------------------------------------- 1 | # SDK 2 | 3 | TODO 4 | -------------------------------------------------------------------------------- /sdk/examples/simple_usage.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use ethrex_common::{Address, Bytes, U256}; 3 | use hex::FromHexError; 4 | use rex_sdk::{ 5 | client::{EthClient, Overrides, eth::get_address_from_secret_key}, 6 | transfer, wait_for_transaction_receipt, 7 | }; 8 | use secp256k1::SecretKey; 9 | use std::str::FromStr; 10 | 11 | #[derive(Parser)] 12 | struct SimpleUsageArgs { 13 | #[arg(long, value_parser = parse_private_key, env = "PRIVATE_KEY", help = "The private key to derive the address from.")] 14 | private_key: SecretKey, 15 | #[arg(default_value = "http://localhost:8545", env = "RPC_URL")] 16 | rpc_url: String, 17 | } 18 | 19 | fn parse_private_key(s: &str) -> eyre::Result { 20 | Ok(SecretKey::from_slice(&parse_hex(s)?)?) 21 | } 22 | 23 | fn parse_hex(s: &str) -> eyre::Result { 24 | match s.strip_prefix("0x") { 25 | Some(s) => hex::decode(s).map(Into::into), 26 | None => hex::decode(s).map(Into::into), 27 | } 28 | } 29 | 30 | #[tokio::main] 31 | async fn main() { 32 | let args = SimpleUsageArgs::parse(); 33 | 34 | let account = get_address_from_secret_key(&args.private_key).unwrap(); 35 | 36 | let rpc_url = "http://localhost:8545"; 37 | 38 | let eth_client = EthClient::new(rpc_url); 39 | 40 | let account_balance = eth_client.get_balance(account).await.unwrap(); 41 | 42 | let account_nonce = eth_client.get_nonce(account).await.unwrap(); 43 | 44 | let chain_id = eth_client.get_chain_id().await.unwrap(); 45 | 46 | println!("Account balance: {account_balance}"); 47 | println!("Account nonce: {account_nonce}"); 48 | println!("Chain id: {chain_id}"); 49 | 50 | let amount = U256::from_dec_str("1000000000000000000").unwrap(); // 1 ETH in wei 51 | let from = account; 52 | let to = Address::from_str("0x4852f44fd706e34cb906b399b729798665f64a83").unwrap(); 53 | 54 | let tx_hash = transfer( 55 | amount, 56 | from, 57 | to, 58 | args.private_key, 59 | ð_client, 60 | Overrides { 61 | value: Some(amount), 62 | ..Default::default() 63 | }, 64 | ) 65 | .await 66 | .unwrap(); 67 | 68 | // Wait for the transaction to be finalized 69 | wait_for_transaction_receipt(tx_hash, ð_client, 100, false) 70 | .await 71 | .unwrap(); 72 | 73 | let tx_receipt = eth_client.get_transaction_receipt(tx_hash).await.unwrap(); 74 | 75 | println!("transfer tx receipt: {tx_receipt:?}"); 76 | 77 | let tx_details = eth_client.get_transaction_by_hash(tx_hash).await.unwrap(); 78 | 79 | println!("transfer tx details: {tx_details:?}"); 80 | } 81 | -------------------------------------------------------------------------------- /sdk/src/calldata.rs: -------------------------------------------------------------------------------- 1 | use ethrex_common::Bytes; 2 | use ethrex_common::{Address, H32, U256}; 3 | use ethrex_rpc::clients::eth::errors::CalldataEncodeError; 4 | use keccak_hash::keccak; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | /// Struct representing the possible solidity types for function arguments 8 | /// - `Uint` -> `uint256` 9 | /// - `Address` -> `address` 10 | /// - `Bool` -> `bool` 11 | /// - `Bytes` -> `bytes` 12 | /// - `String` -> `string` 13 | /// - `Array` -> `T[]` 14 | /// - `Tuple` -> `(X_1, ..., X_k)` 15 | /// - `FixedArray` -> `T[k]` 16 | /// - `FixedBytes` -> `bytesN` 17 | #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] 18 | pub enum Value { 19 | Address(Address), 20 | Uint(U256), 21 | Int(U256), 22 | Bool(bool), 23 | Bytes(Bytes), 24 | String(String), 25 | Array(Vec), 26 | Tuple(Vec), 27 | FixedArray(Vec), 28 | FixedBytes(Bytes), 29 | } 30 | 31 | pub fn parse_signature(signature: &str) -> Result<(String, Vec), CalldataEncodeError> { 32 | let sig = signature.trim().trim_start_matches("function "); 33 | let (name, params) = sig 34 | .split_once('(') 35 | .ok_or(CalldataEncodeError::ParseError(signature.to_owned()))?; 36 | let params = params.rsplit_once(')').map_or(params, |(left, _)| left); 37 | 38 | // We use this to only keep track of top level tuples 39 | // "address,(uint256,uint256)" -> "address" and "(uint256,uint256)" 40 | // "address,(unit256,(uint256,uint256))" -> "address" and "(unit256,(uint256,uint256))" 41 | let mut splitted_params = Vec::new(); 42 | let mut current_param = String::new(); 43 | let mut parenthesis_depth = 0; 44 | 45 | for ch in params.chars() { 46 | match ch { 47 | '(' => { 48 | parenthesis_depth += 1; 49 | current_param.push(ch); 50 | } 51 | ')' => { 52 | parenthesis_depth -= 1; 53 | current_param.push(ch); 54 | } 55 | ',' if parenthesis_depth == 0 => { 56 | if !current_param.is_empty() { 57 | splitted_params.push(current_param.trim().to_string()); 58 | current_param = String::new(); 59 | } 60 | } 61 | _ => current_param.push(ch), 62 | } 63 | } 64 | 65 | // push the last param if it exists 66 | if !current_param.is_empty() { 67 | splitted_params.push(current_param.trim().to_string()); 68 | } 69 | 70 | Ok((name.to_string(), splitted_params)) 71 | } 72 | 73 | fn compute_function_selector(name: &str, params: &[String]) -> Result { 74 | let normalized_signature = format!("{name}({})", params.join(",")); 75 | let hash = keccak(normalized_signature.as_bytes()); 76 | 77 | Ok(H32::from(&hash[..4].try_into().map_err(|_| { 78 | CalldataEncodeError::ParseError(name.to_owned()) 79 | })?)) 80 | } 81 | 82 | pub fn encode_calldata(signature: &str, values: &[Value]) -> Result, CalldataEncodeError> { 83 | let (name, params) = parse_signature(signature)?; 84 | 85 | // Checks if params = [""] 86 | // that case happen when we have a function selector as follows: function name() 87 | let mut params = params; 88 | if params.is_empty() { 89 | params = vec![]; 90 | } 91 | 92 | if params.len() != values.len() { 93 | return Err(CalldataEncodeError::WrongArgumentLength( 94 | signature.to_owned(), 95 | )); 96 | } 97 | 98 | let function_selector = compute_function_selector(&name, ¶ms)?; 99 | let calldata = encode_tuple(values)?; 100 | let mut with_selector = function_selector.as_bytes().to_vec(); 101 | 102 | with_selector.extend_from_slice(&calldata); 103 | 104 | Ok(with_selector) 105 | } 106 | 107 | // This is the main entrypoint for ABI encoding solidity function arguments, as the list of arguments themselves are 108 | // considered a tuple. Before going through this function, read the solidity ABI spec first 109 | // https://docs.soliditylang.org/en/develop/abi-spec.html. 110 | // The encoding of a tuple consists of two parts: a static and a dynamic one (what the spec calls the head and tail of the encoding). 111 | // The dynamic part always follows at the end of the static one. 112 | // Arguments are encoded in order. If the argument is static, it is encoded in place, i.e, there's no dynamic part. 113 | // If the argument is dynamic, only its offset to the dynamic part is recorded on the static sector. 114 | pub fn encode_tuple(values: &[Value]) -> Result, CalldataEncodeError> { 115 | let mut current_offset = 0; 116 | let mut current_dynamic_offset = 0; 117 | for value in values { 118 | current_dynamic_offset += static_offset_value(value); 119 | } 120 | 121 | let mut ret = vec![0; current_dynamic_offset]; 122 | 123 | for value in values { 124 | match value { 125 | Value::Address(h160) => { 126 | write_u256(&mut ret, address_to_word(*h160), current_offset)?; 127 | } 128 | Value::Uint(u256) => { 129 | write_u256(&mut ret, *u256, current_offset)?; 130 | } 131 | Value::Int(u256) => { 132 | write_u256(&mut ret, *u256, current_offset)?; 133 | } 134 | Value::Bool(boolean) => { 135 | write_u256(&mut ret, U256::from(u8::from(*boolean)), current_offset)?; 136 | } 137 | Value::Bytes(bytes) => { 138 | write_u256(&mut ret, U256::from(current_dynamic_offset), current_offset)?; 139 | 140 | let bytes_encoding = encode_bytes(bytes); 141 | ret.extend_from_slice(&bytes_encoding); 142 | current_dynamic_offset += bytes_encoding.len(); 143 | } 144 | Value::String(string_value) => { 145 | write_u256(&mut ret, U256::from(current_dynamic_offset), current_offset)?; 146 | 147 | let utf8_encoded = Bytes::copy_from_slice(string_value.as_bytes()); 148 | let bytes_encoding = encode_bytes(&utf8_encoded); 149 | ret.extend_from_slice(&bytes_encoding); 150 | current_dynamic_offset += bytes_encoding.len(); 151 | } 152 | Value::Array(array_values) => { 153 | write_u256(&mut ret, U256::from(current_dynamic_offset), current_offset)?; 154 | 155 | let array_encoding = encode_array(array_values)?; 156 | ret.extend_from_slice(&array_encoding); 157 | current_dynamic_offset += array_encoding.len(); 158 | } 159 | Value::Tuple(tuple_values) => { 160 | if !is_dynamic(value) { 161 | let tuple_encoding = encode_tuple(tuple_values)?; 162 | ret.extend_from_slice(&tuple_encoding); 163 | } else { 164 | write_u256(&mut ret, U256::from(current_dynamic_offset), current_offset)?; 165 | 166 | let tuple_encoding = encode_tuple(tuple_values)?; 167 | ret.extend_from_slice(&tuple_encoding); 168 | current_dynamic_offset += tuple_encoding.len(); 169 | } 170 | } 171 | Value::FixedArray(fixed_array_values) => { 172 | if !is_dynamic(value) { 173 | let fixed_array_encoding = encode_tuple(fixed_array_values)?; 174 | ret.extend_from_slice(&fixed_array_encoding); 175 | } else { 176 | write_u256(&mut ret, U256::from(current_dynamic_offset), current_offset)?; 177 | 178 | let tuple_encoding = encode_tuple(fixed_array_values)?; 179 | ret.extend_from_slice(&tuple_encoding); 180 | current_dynamic_offset += tuple_encoding.len(); 181 | } 182 | } 183 | Value::FixedBytes(bytes) => { 184 | let mut to_copy = [0; 32]; 185 | to_copy.copy_from_slice(bytes); 186 | copy_into(&mut ret, &to_copy, current_offset, 32)?; 187 | } 188 | } 189 | 190 | current_offset += static_offset_value(value); 191 | } 192 | 193 | Ok(ret) 194 | } 195 | 196 | fn write_u256(values: &mut [u8], number: U256, offset: usize) -> Result<(), CalldataEncodeError> { 197 | let to_copy = number.to_big_endian(); 198 | copy_into(values, &to_copy, offset, 32)?; 199 | 200 | Ok(()) 201 | } 202 | 203 | // Returns the size that the value occupies in the static sector of the abi encoding. 204 | // For dynamic types, this is always 32 (the offset to the dynamic sector). 205 | // For static types, it's 32 unless the value is a static tuple or a fixed array, in which case 206 | // it's the sum of the sizes of their elements. 207 | fn static_offset_value(value: &Value) -> usize { 208 | let mut ret = 0; 209 | 210 | match value { 211 | Value::Address(_) 212 | | Value::Uint(_) 213 | | Value::Int(_) 214 | | Value::Bool(_) 215 | | Value::Bytes(_) 216 | | Value::String(_) 217 | | Value::Array(_) 218 | | Value::FixedBytes(_) => ret += 32, 219 | Value::Tuple(vec) => { 220 | if is_dynamic(value) { 221 | ret += 32; 222 | } else { 223 | for element in vec { 224 | // Here every element is guaranteed to be static, otherwise we would not be 225 | // in the `else` branch of the `if` statement. 226 | ret += static_offset_value(element); 227 | } 228 | } 229 | } 230 | Value::FixedArray(vec) => { 231 | if is_dynamic(value) { 232 | ret += 32; 233 | } else { 234 | for element in vec { 235 | // Here every element is guaranteed to be static (and of the same type), otherwise we would not be 236 | // in the `else` branch of the `if` statement. 237 | ret += static_offset_value(element); 238 | } 239 | } 240 | } 241 | } 242 | 243 | ret 244 | } 245 | 246 | fn is_dynamic(value: &Value) -> bool { 247 | match value { 248 | Value::Bytes(_) | Value::String(_) | Value::Array(_) => true, 249 | Value::Tuple(vec) => vec.iter().any(is_dynamic), 250 | Value::FixedArray(vec) => { 251 | if let Some(first_elem) = vec.first() { 252 | is_dynamic(first_elem) 253 | } else { 254 | false 255 | } 256 | } 257 | _ => false, 258 | } 259 | } 260 | 261 | fn encode_array(values: &[Value]) -> Result, CalldataEncodeError> { 262 | let mut ret = vec![]; 263 | let to_copy = U256::from(values.len()).to_big_endian(); 264 | ret.extend_from_slice(&to_copy); 265 | 266 | let tuple_encoding = encode_tuple(values)?; 267 | ret.extend_from_slice(&tuple_encoding); 268 | 269 | Ok(ret) 270 | } 271 | 272 | fn encode_bytes(values: &Bytes) -> Vec { 273 | let mut ret = vec![]; 274 | 275 | // the bytes has to be padded to 32 bytes 276 | let padding = 32 - (values.len() % 32); 277 | let mut padded_bytes = values.to_vec(); 278 | if padding != 32 { 279 | padded_bytes.extend_from_slice(&vec![0; padding]); 280 | } 281 | 282 | let to_copy = U256::from(values.len()).to_big_endian(); // we write the length without padding 283 | 284 | ret.extend_from_slice(&to_copy); 285 | ret.extend_from_slice(&padded_bytes); 286 | 287 | ret 288 | } 289 | 290 | fn copy_into( 291 | values: &mut [u8], 292 | to_copy: &[u8], 293 | offset: usize, 294 | size: usize, 295 | ) -> Result<(), CalldataEncodeError> { 296 | let to_copy_slice = to_copy 297 | .get(..size) 298 | .ok_or(CalldataEncodeError::InternalError)?; 299 | 300 | values 301 | .get_mut(offset..(size + offset)) 302 | .ok_or(CalldataEncodeError::InternalError)? 303 | .copy_from_slice(to_copy_slice); 304 | 305 | Ok(()) 306 | } 307 | 308 | fn address_to_word(address: Address) -> U256 { 309 | let mut word = [0u8; 32]; 310 | for (word_byte, address_byte) in word.iter_mut().skip(12).zip(address.as_bytes().iter()) { 311 | *word_byte = *address_byte; 312 | } 313 | U256::from_big_endian(&word) 314 | } 315 | 316 | #[test] 317 | fn calldata_test() { 318 | let raw_function_signature = "blockWithdrawalsLogs(uint256,bytes)"; 319 | let mut bytes_calldata = vec![]; 320 | 321 | bytes_calldata.extend_from_slice(&U256::zero().to_big_endian()); 322 | bytes_calldata.extend_from_slice(&U256::one().to_big_endian()); 323 | 324 | let arguments = vec![ 325 | Value::Uint(U256::from(902)), 326 | Value::Bytes(bytes_calldata.into()), 327 | ]; 328 | 329 | let calldata = encode_calldata(raw_function_signature, &arguments).unwrap(); 330 | 331 | assert_eq!( 332 | calldata, 333 | vec![ 334 | 20, 108, 34, 199, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 335 | 0, 0, 0, 0, 0, 0, 0, 3, 134, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 336 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 337 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 338 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 339 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 340 | ] 341 | ); 342 | } 343 | 344 | #[test] 345 | fn raw_function_selector() { 346 | let raw_function_signature = "deposit((address,address,uint256,bytes))"; 347 | 348 | let (name, params) = parse_signature(raw_function_signature).unwrap(); 349 | let selector = compute_function_selector(&name, ¶ms).unwrap(); 350 | 351 | assert_eq!(selector, H32::from(&[0x02, 0xe8, 0x6b, 0xbe])); 352 | } 353 | 354 | #[test] 355 | fn encode_tuple_dynamic_offset() { 356 | let raw_function_signature = "deposit((address,address,uint256,bytes))"; 357 | let address = Address::from_low_u64_be(424242_u64); 358 | 359 | let tuple = Value::Tuple(vec![ 360 | Value::Address(address), 361 | Value::Address(address), 362 | Value::Uint(U256::from(21000 * 5)), 363 | Value::Bytes(Bytes::from_static(b"")), 364 | ]); 365 | let values = vec![tuple]; 366 | 367 | let calldata = encode_calldata(raw_function_signature, &values).unwrap(); 368 | 369 | assert_eq!(calldata, hex::decode("02e86bbe0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000006793200000000000000000000000000000000000000000000000000000000000679320000000000000000000000000000000000000000000000000000000000019a2800000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000").unwrap()); 370 | 371 | let mut encoding = vec![0x02, 0xe8, 0x6b, 0xbe]; // function selector 372 | encoding.extend_from_slice(&encode_tuple(&values).unwrap()); 373 | 374 | assert_eq!(calldata, encoding); 375 | } 376 | 377 | #[test] 378 | fn correct_tuple_parsing() { 379 | // the arguments are: 380 | // - uint256 381 | // - (uint256, address) 382 | // - ((address, address), (uint256, bytes)) 383 | // - ((address, address), uint256) 384 | // - (uint256, (address, address)) 385 | // - address 386 | let raw_function_signature = "my_function(uint256,(uin256,address),((address,address),(uint256,bytes)),((address,address),uint256),(uint256,(address,address)),address)"; 387 | 388 | let exepected_arguments: Vec = vec![ 389 | "uint256".to_string(), 390 | "(uin256,address)".to_string(), 391 | "((address,address),(uint256,bytes))".to_string(), 392 | "((address,address),uint256)".to_string(), 393 | "(uint256,(address,address))".to_string(), 394 | "address".to_string(), 395 | ]; 396 | let (name, params) = parse_signature(raw_function_signature).unwrap(); 397 | assert_eq!(name, "my_function"); 398 | assert_eq!(params, exepected_arguments); 399 | } 400 | 401 | #[test] 402 | fn empty_calldata() { 403 | let calldata = encode_calldata("number()", &[]).unwrap(); 404 | assert_eq!(calldata, hex::decode("8381f58a").unwrap()); 405 | } 406 | 407 | #[test] 408 | fn bytes_has_padding() { 409 | let raw_function_signature = "my_function(bytes)"; 410 | let bytes = Bytes::from_static(b"hello world"); 411 | let values = vec![Value::Bytes(bytes)]; 412 | 413 | let calldata = encode_calldata(raw_function_signature, &values).unwrap(); 414 | 415 | assert_eq!(calldata, hex::decode("f570899b0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b68656c6c6f20776f726c64000000000000000000000000000000000000000000").unwrap()); 416 | } 417 | -------------------------------------------------------------------------------- /sdk/src/client/eth/errors.rs: -------------------------------------------------------------------------------- 1 | use ethrex_rpc::utils::RpcRequest; 2 | 3 | #[derive(Debug, thiserror::Error)] 4 | pub enum EthClientError { 5 | #[error("Error sending request {0:?}")] 6 | RequestError(RpcRequest), 7 | #[error("reqwest error: {0}")] 8 | ReqwestError(#[from] reqwest::Error), 9 | #[error("eth_gasPrice request error: {0}")] 10 | GetGasPriceError(#[from] GetGasPriceError), 11 | #[error("eth_estimateGas request error: {0}")] 12 | EstimateGasPriceError(#[from] EstimateGasPriceError), 13 | #[error("eth_sendRawTransaction request error: {0}")] 14 | SendRawTransactionError(#[from] SendRawTransactionError), 15 | #[error("eth_call request error: {0}")] 16 | CallError(#[from] CallError), 17 | #[error("eth_getTransactionCount request error: {0}")] 18 | GetNonceError(#[from] GetNonceError), 19 | #[error("eth_blockNumber request error: {0}")] 20 | GetBlockNumberError(#[from] GetBlockNumberError), 21 | #[error("eth_getBlockByHash request error: {0}")] 22 | GetBlockByHashError(#[from] GetBlockByHashError), 23 | #[error("eth_getBlockByNumber request error: {0}")] 24 | GetBlockByNumberError(#[from] GetBlockByNumberError), 25 | #[error("eth_getLogs request error: {0}")] 26 | GetLogsError(#[from] GetLogsError), 27 | #[error("eth_getTransactionReceipt request error: {0}")] 28 | GetTransactionReceiptError(#[from] GetTransactionReceiptError), 29 | #[error("Failed to serialize request body: {0}")] 30 | FailedToSerializeRequestBody(String), 31 | #[error("Failed to deserialize response body: {0}")] 32 | GetBalanceError(#[from] GetBalanceError), 33 | #[error("eth_getTransactionByHash request error: {0}")] 34 | GetTransactionByHashError(#[from] GetTransactionByHashError), 35 | #[error("Unreachable nonce")] 36 | UnrecheableNonce, 37 | #[error("Error: {0}")] 38 | Custom(String), 39 | #[error("Failed to encode calldata: {0}")] 40 | CalldataEncodeError(#[from] CalldataEncodeError), 41 | } 42 | 43 | #[derive(Debug, thiserror::Error)] 44 | pub enum GetGasPriceError { 45 | #[error("{0}")] 46 | ReqwestError(#[from] reqwest::Error), 47 | #[error("{0}")] 48 | SerdeJSONError(#[from] serde_json::Error), 49 | #[error("{0}")] 50 | RPCError(String), 51 | #[error("{0}")] 52 | ParseIntError(#[from] std::num::ParseIntError), 53 | } 54 | 55 | #[derive(Debug, thiserror::Error)] 56 | pub enum EstimateGasPriceError { 57 | #[error("{0}")] 58 | ReqwestError(#[from] reqwest::Error), 59 | #[error("{0}")] 60 | SerdeJSONError(#[from] serde_json::Error), 61 | #[error("{0}")] 62 | RPCError(String), 63 | #[error("{0}")] 64 | ParseIntError(#[from] std::num::ParseIntError), 65 | #[error("{0}")] 66 | Custom(String), 67 | } 68 | 69 | #[derive(Debug, thiserror::Error)] 70 | pub enum SendRawTransactionError { 71 | #[error("{0}")] 72 | ReqwestError(#[from] reqwest::Error), 73 | #[error("{0}")] 74 | SerdeJSONError(#[from] serde_json::Error), 75 | #[error("{0}")] 76 | RPCError(String), 77 | #[error("{0}")] 78 | ParseIntError(#[from] std::num::ParseIntError), 79 | } 80 | 81 | #[derive(Debug, thiserror::Error)] 82 | pub enum CallError { 83 | #[error("{0}")] 84 | ReqwestError(#[from] reqwest::Error), 85 | #[error("{0}")] 86 | SerdeJSONError(#[from] serde_json::Error), 87 | #[error("{0}")] 88 | RPCError(String), 89 | #[error("{0}")] 90 | ParseIntError(#[from] std::num::ParseIntError), 91 | } 92 | 93 | #[derive(Debug, thiserror::Error)] 94 | pub enum GetNonceError { 95 | #[error("{0}")] 96 | ReqwestError(#[from] reqwest::Error), 97 | #[error("{0}")] 98 | SerdeJSONError(#[from] serde_json::Error), 99 | #[error("{0}")] 100 | RPCError(String), 101 | #[error("{0}")] 102 | ParseIntError(#[from] std::num::ParseIntError), 103 | } 104 | 105 | #[derive(Debug, thiserror::Error)] 106 | pub enum GetBlockNumberError { 107 | #[error("{0}")] 108 | ReqwestError(#[from] reqwest::Error), 109 | #[error("{0}")] 110 | SerdeJSONError(#[from] serde_json::Error), 111 | #[error("{0}")] 112 | RPCError(String), 113 | #[error("{0}")] 114 | ParseIntError(#[from] std::num::ParseIntError), 115 | } 116 | 117 | #[derive(Debug, thiserror::Error)] 118 | pub enum GetBlockByHashError { 119 | #[error("{0}")] 120 | ReqwestError(#[from] reqwest::Error), 121 | #[error("{0}")] 122 | SerdeJSONError(#[from] serde_json::Error), 123 | #[error("{0}")] 124 | RPCError(String), 125 | #[error("{0}")] 126 | ParseIntError(#[from] std::num::ParseIntError), 127 | } 128 | 129 | #[derive(Debug, thiserror::Error)] 130 | pub enum GetBlockByNumberError { 131 | #[error("{0}")] 132 | SerdeJSONError(#[from] serde_json::Error), 133 | #[error("{0}")] 134 | RPCError(String), 135 | } 136 | 137 | #[derive(Debug, thiserror::Error)] 138 | pub enum GetLogsError { 139 | #[error("{0}")] 140 | ReqwestError(#[from] reqwest::Error), 141 | #[error("{0}")] 142 | SerdeJSONError(#[from] serde_json::Error), 143 | #[error("{0}")] 144 | RPCError(String), 145 | #[error("{0}")] 146 | ParseIntError(#[from] std::num::ParseIntError), 147 | } 148 | 149 | #[derive(Debug, thiserror::Error)] 150 | pub enum GetTransactionReceiptError { 151 | #[error("{0}")] 152 | ReqwestError(#[from] reqwest::Error), 153 | #[error("{0}")] 154 | SerdeJSONError(#[from] serde_json::Error), 155 | #[error("{0}")] 156 | RPCError(String), 157 | #[error("{0}")] 158 | ParseIntError(#[from] std::num::ParseIntError), 159 | } 160 | 161 | #[derive(Debug, thiserror::Error)] 162 | pub enum GetBalanceError { 163 | #[error("{0}")] 164 | ReqwestError(#[from] reqwest::Error), 165 | #[error("{0}")] 166 | SerdeJSONError(#[from] serde_json::Error), 167 | #[error("{0}")] 168 | RPCError(String), 169 | #[error("{0}")] 170 | ParseIntError(#[from] std::num::ParseIntError), 171 | } 172 | 173 | #[derive(Debug, thiserror::Error)] 174 | pub enum GetTransactionByHashError { 175 | #[error("{0}")] 176 | ReqwestError(#[from] reqwest::Error), 177 | #[error("{0}")] 178 | SerdeJSONError(#[from] serde_json::Error), 179 | #[error("{0}")] 180 | RPCError(String), 181 | #[error("{0}")] 182 | ParseIntError(#[from] std::num::ParseIntError), 183 | } 184 | 185 | #[derive(Debug, thiserror::Error)] 186 | pub enum CalldataEncodeError { 187 | #[error("Failed to parse function signature: {0}")] 188 | ParseError(String), 189 | #[error("Wrong number of arguments provided for calldata: {0}")] 190 | WrongArgumentLength(String), 191 | #[error("Internal Calldata encoding error. This is most likely a bug")] 192 | InternalError, 193 | } 194 | -------------------------------------------------------------------------------- /sdk/src/client/eth/eth_sender.rs: -------------------------------------------------------------------------------- 1 | use crate::client::eth::{ 2 | EthClient, RpcResponse, 3 | errors::{CallError, EthClientError}, 4 | }; 5 | use ethrex_common::{ 6 | Address, Bytes, H256, U256, 7 | types::{GenericTransaction, TxKind}, 8 | }; 9 | use ethrex_rlp::encode::RLPEncode; 10 | use ethrex_rpc::utils::{RpcRequest, RpcRequestId}; 11 | use keccak_hash::keccak; 12 | use secp256k1::SecretKey; 13 | use serde_json::json; 14 | 15 | #[derive(Default, Clone, Debug)] 16 | pub struct Overrides { 17 | pub from: Option
, 18 | pub to: Option, 19 | pub value: Option, 20 | pub nonce: Option, 21 | pub chain_id: Option, 22 | pub gas_limit: Option, 23 | pub max_fee_per_gas: Option, 24 | pub max_priority_fee_per_gas: Option, 25 | pub access_list: Vec<(Address, Vec)>, 26 | pub gas_price_per_blob: Option, 27 | } 28 | 29 | impl EthClient { 30 | pub async fn call( 31 | &self, 32 | to: Address, 33 | calldata: Bytes, 34 | overrides: Overrides, 35 | ) -> Result { 36 | let tx = GenericTransaction { 37 | to: TxKind::Call(to), 38 | input: calldata, 39 | value: overrides.value.unwrap_or_default(), 40 | from: overrides.from.unwrap_or_default(), 41 | gas: overrides.gas_limit, 42 | gas_price: overrides 43 | .max_fee_per_gas 44 | .unwrap_or(self.get_gas_price().await?.as_u64()), 45 | ..Default::default() 46 | }; 47 | 48 | let request = RpcRequest { 49 | id: RpcRequestId::Number(1), 50 | jsonrpc: "2.0".to_string(), 51 | method: "eth_call".to_string(), 52 | params: Some(vec![ 53 | json!({ 54 | "to": match tx.to { 55 | TxKind::Call(addr) => format!("{addr:#x}"), 56 | TxKind::Create => format!("{:#x}", Address::zero()), 57 | }, 58 | "input": format!("0x{:#x}", tx.input), 59 | "value": format!("{:#x}", tx.value), 60 | "from": format!("{:#x}", tx.from), 61 | }), 62 | json!("latest"), 63 | ]), 64 | }; 65 | 66 | match self.send_request(request).await { 67 | Ok(RpcResponse::Success(result)) => serde_json::from_value(result.result) 68 | .map_err(CallError::SerdeJSONError) 69 | .map_err(EthClientError::from), 70 | Ok(RpcResponse::Error(error_response)) => { 71 | Err(CallError::RPCError(error_response.error.message).into()) 72 | } 73 | Err(error) => Err(error), 74 | } 75 | } 76 | 77 | pub async fn deploy( 78 | &self, 79 | deployer: Address, 80 | deployer_private_key: SecretKey, 81 | init_code: Bytes, 82 | overrides: Overrides, 83 | ) -> Result<(H256, Address), EthClientError> { 84 | let mut deploy_overrides = overrides; 85 | deploy_overrides.to = Some(TxKind::Create); 86 | let deploy_tx = self 87 | .build_eip1559_transaction(Address::zero(), deployer, init_code, deploy_overrides, 10) 88 | .await?; 89 | let deploy_tx_hash = self 90 | .send_eip1559_transaction(&deploy_tx, &deployer_private_key) 91 | .await?; 92 | 93 | let nonce = self.get_nonce(deployer).await?; 94 | let mut encode = vec![]; 95 | (deployer, nonce).encode(&mut encode); 96 | 97 | //Taking the last 20bytes so it matches an H160 == Address length 98 | let deployed_address = 99 | Address::from_slice(keccak(encode).as_fixed_bytes().get(12..).ok_or( 100 | EthClientError::Custom("Failed to get deployed_address".to_owned()), 101 | )?); 102 | 103 | Ok((deploy_tx_hash, deployed_address)) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /sdk/src/client/eth/mod.rs: -------------------------------------------------------------------------------- 1 | use errors::{ 2 | EstimateGasPriceError, EthClientError, GetBalanceError, GetBlockByHashError, 3 | GetBlockByNumberError, GetBlockNumberError, GetGasPriceError, GetLogsError, GetNonceError, 4 | GetTransactionByHashError, GetTransactionReceiptError, SendRawTransactionError, 5 | }; 6 | use eth_sender::Overrides; 7 | use ethrex_common::{ 8 | Address, Bytes, H160, H256, U256, 9 | types::{ 10 | BlobsBundle, EIP1559Transaction, EIP4844Transaction, GenericTransaction, 11 | PrivilegedL2Transaction, Signable, TxKind, TxType, WrappedEIP4844Transaction, 12 | }, 13 | }; 14 | use ethrex_rlp::encode::RLPEncode; 15 | use ethrex_rpc::{ 16 | types::{ 17 | block::RpcBlock, 18 | receipt::{RpcLog, RpcReceipt}, 19 | }, 20 | utils::{RpcErrorResponse, RpcRequest, RpcRequestId, RpcSuccessResponse}, 21 | }; 22 | use keccak_hash::keccak; 23 | use reqwest::Client; 24 | use secp256k1::{Error, SecretKey}; 25 | use serde::{Deserialize, Serialize}; 26 | use serde_json::{Value, json}; 27 | use std::ops::Div; 28 | use std::{fmt, time::Duration}; 29 | use tokio::time::{Instant, sleep}; 30 | use tracing::warn; 31 | 32 | pub mod errors; 33 | pub mod eth_sender; 34 | 35 | #[derive(Deserialize, Debug)] 36 | #[serde(untagged)] 37 | pub enum RpcResponse { 38 | Success(RpcSuccessResponse), 39 | Error(RpcErrorResponse), 40 | } 41 | 42 | #[derive(Debug, Clone)] 43 | pub struct EthClient { 44 | client: Client, 45 | pub url: String, 46 | } 47 | 48 | #[derive(Debug, Clone)] 49 | pub enum WrappedTransaction { 50 | EIP4844(WrappedEIP4844Transaction), 51 | EIP1559(EIP1559Transaction), 52 | L2(PrivilegedL2Transaction), 53 | } 54 | 55 | pub enum BlockByNumber { 56 | Number(u64), 57 | Latest, 58 | Earliest, 59 | Pending, 60 | } 61 | 62 | // 0x08c379a0 == Error(String) 63 | pub const ERROR_FUNCTION_SELECTOR: [u8; 4] = [0x08, 0xc3, 0x79, 0xa0]; 64 | // 0x70a08231 == balanceOf(address) 65 | pub const BALANCE_OF_SELECTOR: [u8; 4] = [0x70, 0xa0, 0x82, 0x31]; 66 | 67 | impl EthClient { 68 | pub fn new(url: &str) -> Self { 69 | Self { 70 | client: Client::new(), 71 | url: url.to_string(), 72 | } 73 | } 74 | 75 | async fn send_request(&self, request: RpcRequest) -> Result { 76 | self.client 77 | .post(&self.url) 78 | .header("content-type", "application/json") 79 | .body(serde_json::ser::to_string(&request).map_err(|error| { 80 | EthClientError::FailedToSerializeRequestBody(format!("{error}: {request:?}")) 81 | })?) 82 | .send() 83 | .await? 84 | .json::() 85 | .await 86 | .map_err(EthClientError::from) 87 | } 88 | 89 | pub async fn send_raw_transaction(&self, data: &[u8]) -> Result { 90 | let request = RpcRequest { 91 | id: RpcRequestId::Number(1), 92 | jsonrpc: "2.0".to_string(), 93 | method: "eth_sendRawTransaction".to_string(), 94 | params: Some(vec![json!("0x".to_string() + &hex::encode(data))]), 95 | }; 96 | 97 | match self.send_request(request).await { 98 | Ok(RpcResponse::Success(result)) => serde_json::from_value(result.result) 99 | .map_err(SendRawTransactionError::SerdeJSONError) 100 | .map_err(EthClientError::from), 101 | Ok(RpcResponse::Error(error_response)) => { 102 | Err(SendRawTransactionError::RPCError(error_response.error.message).into()) 103 | } 104 | Err(error) => Err(error), 105 | } 106 | } 107 | 108 | pub async fn send_eip1559_transaction( 109 | &self, 110 | tx: &EIP1559Transaction, 111 | private_key: &SecretKey, 112 | ) -> Result { 113 | let signed_tx = tx.sign(private_key); 114 | 115 | let mut encoded_tx = signed_tx.encode_to_vec(); 116 | encoded_tx.insert(0, TxType::EIP1559.into()); 117 | 118 | self.send_raw_transaction(encoded_tx.as_slice()).await 119 | } 120 | 121 | pub async fn send_eip4844_transaction( 122 | &self, 123 | wrapped_tx: &WrappedEIP4844Transaction, 124 | private_key: &SecretKey, 125 | ) -> Result { 126 | let mut wrapped_tx = wrapped_tx.clone(); 127 | wrapped_tx.tx.sign_inplace(private_key); 128 | 129 | let mut encoded_tx = wrapped_tx.encode_to_vec(); 130 | encoded_tx.insert(0, TxType::EIP4844.into()); 131 | 132 | self.send_raw_transaction(encoded_tx.as_slice()).await 133 | } 134 | 135 | /// Sends a [WrappedTransaction] with retries and gas bumping. 136 | /// 137 | /// The total wait time for each retry is determined by dividing the `max_seconds_to_wait` 138 | /// by the `retries` parameter. The transaction is sent again with a gas bump if the receipt 139 | /// is not confirmed within each retry period. 140 | /// 141 | /// seconds_per_retry = max_seconds_to_wait / retries; 142 | pub async fn send_wrapped_transaction_with_retry( 143 | &self, 144 | wrapped_tx: &WrappedTransaction, 145 | private_key: &SecretKey, 146 | max_seconds_to_wait: u64, 147 | retries: u64, 148 | ) -> Result { 149 | let tx_hash_res = match wrapped_tx { 150 | WrappedTransaction::EIP4844(wrapped_eip4844_transaction) => { 151 | self.send_eip4844_transaction(wrapped_eip4844_transaction, private_key) 152 | .await 153 | } 154 | WrappedTransaction::EIP1559(eip1559_transaction) => { 155 | self.send_eip1559_transaction(eip1559_transaction, private_key) 156 | .await 157 | } 158 | WrappedTransaction::L2(privileged_l2_transaction) => { 159 | self.send_privileged_l2_transaction(privileged_l2_transaction) 160 | .await 161 | } 162 | }; 163 | 164 | // Check if the tx is `already known`, bump gas and resend it. 165 | let mut tx_hash = match tx_hash_res { 166 | Ok(hash) => hash, 167 | Err(e) => { 168 | let error = format!("{e}"); 169 | if error.contains("already known") 170 | || error.contains("replacement transaction underpriced") 171 | { 172 | H256::zero() 173 | } else { 174 | return Err(e); 175 | } 176 | } 177 | }; 178 | 179 | let mut wrapped_tx = wrapped_tx.clone(); 180 | 181 | let seconds_per_retry = max_seconds_to_wait / retries; 182 | let timer_total = Instant::now(); 183 | 184 | for r in 0..retries { 185 | // Check if we are not waiting more than needed. 186 | if timer_total.elapsed().as_secs() > max_seconds_to_wait { 187 | return Err(EthClientError::Custom( 188 | "TimeOut: Failed to send_wrapped_transaction_with_retry".to_owned(), 189 | )); 190 | } 191 | 192 | // Wait for the receipt with some time between retries. 193 | let timer_per_retry = Instant::now(); 194 | while timer_per_retry.elapsed().as_secs() < seconds_per_retry { 195 | match self.get_transaction_receipt(tx_hash).await? { 196 | Some(_) => return Ok(tx_hash), 197 | None => sleep(Duration::from_secs(1)).await, 198 | } 199 | } 200 | 201 | // If receipt is not found after the time period, increase gas and resend the transaction. 202 | tx_hash = match &mut wrapped_tx { 203 | WrappedTransaction::EIP4844(wrapped_eip4844_transaction) => { 204 | warn!("Resending EIP4844Transaction, attempts [{r}/{retries}]"); 205 | self.bump_and_resend_eip4844(wrapped_eip4844_transaction, private_key) 206 | .await? 207 | } 208 | WrappedTransaction::EIP1559(eip1559_transaction) => { 209 | warn!("Resending EIP1559Transaction, attempts [{r}/{retries}]"); 210 | self.bump_and_resend_eip1559(eip1559_transaction, private_key) 211 | .await? 212 | } 213 | WrappedTransaction::L2(privileged_l2_transaction) => { 214 | warn!("Resending PrivilegedL2Transaction, attempts [{r}/{retries}]"); 215 | self.bump_and_resend_privileged_l2(privileged_l2_transaction, private_key) 216 | .await? 217 | } 218 | }; 219 | } 220 | 221 | // If the loop ends without success, return a timeout error 222 | Err(EthClientError::Custom( 223 | "Max retries exceeded while waiting for transaction receipt".to_owned(), 224 | )) 225 | } 226 | 227 | pub async fn bump_and_resend_eip1559( 228 | &self, 229 | tx: &mut EIP1559Transaction, 230 | private_key: &SecretKey, 231 | ) -> Result { 232 | let from = get_address_from_secret_key(private_key).map_err(|e| { 233 | EthClientError::Custom(format!("Failed to get_address_from_secret_key: {e}")) 234 | })?; 235 | // Sometimes the penalty is a 100% 236 | // Increase max fee per gas by 110% (set it to 210% of the original) 237 | self.bump_eip1559(tx, 110); 238 | let wrapped_tx = &mut WrappedTransaction::EIP1559(tx.clone()); 239 | self.estimate_gas_for_wrapped_tx(wrapped_tx, from).await?; 240 | 241 | if let WrappedTransaction::EIP1559(eip1559) = wrapped_tx { 242 | tx.max_fee_per_gas = eip1559.max_fee_per_gas; 243 | tx.max_priority_fee_per_gas = eip1559.max_fee_per_gas; 244 | tx.gas_limit = eip1559.gas_limit; 245 | } 246 | self.send_eip1559_transaction(tx, private_key).await 247 | } 248 | 249 | /// Increase max fee per gas by percentage% (set it to (100+percentage)% of the original) 250 | pub fn bump_eip1559(&self, tx: &mut EIP1559Transaction, percentage: u64) { 251 | tx.max_fee_per_gas = (tx.max_fee_per_gas * (100 + percentage)) / 100; 252 | tx.max_priority_fee_per_gas += (tx.max_priority_fee_per_gas * (100 + percentage)) / 100; 253 | } 254 | 255 | pub async fn bump_and_resend_eip4844( 256 | &self, 257 | wrapped_tx: &mut WrappedEIP4844Transaction, 258 | private_key: &SecretKey, 259 | ) -> Result { 260 | let from = get_address_from_secret_key(private_key).map_err(|e| { 261 | EthClientError::Custom(format!("Failed to get_address_from_secret_key: {e}")) 262 | })?; 263 | // Sometimes the penalty is a 100% 264 | // Increase max fee per gas by 110% (set it to 210% of the original) 265 | self.bump_eip4844(wrapped_tx, 110); 266 | let wrapped_eip4844 = &mut WrappedTransaction::EIP4844(wrapped_tx.clone()); 267 | self.estimate_gas_for_wrapped_tx(wrapped_eip4844, from) 268 | .await?; 269 | 270 | if let WrappedTransaction::EIP4844(eip4844) = wrapped_eip4844 { 271 | wrapped_tx.tx.max_fee_per_gas = eip4844.tx.max_fee_per_gas; 272 | wrapped_tx.tx.max_priority_fee_per_gas = eip4844.tx.max_fee_per_gas; 273 | wrapped_tx.tx.gas = eip4844.tx.gas; 274 | wrapped_tx.tx.max_fee_per_blob_gas = eip4844.tx.max_fee_per_blob_gas; 275 | } 276 | self.send_eip4844_transaction(wrapped_tx, private_key).await 277 | } 278 | 279 | /// Increase max fee per gas by percentage% (set it to (100+percentage)% of the original) 280 | pub fn bump_eip4844(&self, wrapped_tx: &mut WrappedEIP4844Transaction, percentage: u64) { 281 | wrapped_tx.tx.max_fee_per_gas = (wrapped_tx.tx.max_fee_per_gas * (100 + percentage)) / 100; 282 | wrapped_tx.tx.max_priority_fee_per_gas += 283 | (wrapped_tx.tx.max_priority_fee_per_gas * (100 + percentage)) / 100; 284 | let factor = 1 + (percentage / 100) * 10; 285 | wrapped_tx.tx.max_fee_per_blob_gas = wrapped_tx 286 | .tx 287 | .max_fee_per_blob_gas 288 | .saturating_mul(U256::from(factor)) 289 | .div(10); 290 | } 291 | 292 | pub async fn bump_and_resend_privileged_l2( 293 | &self, 294 | tx: &mut PrivilegedL2Transaction, 295 | private_key: &SecretKey, 296 | ) -> Result { 297 | let from = get_address_from_secret_key(private_key).map_err(|e| { 298 | EthClientError::Custom(format!("Failed to get_address_from_secret_key: {e}")) 299 | })?; 300 | // Sometimes the penalty is a 100% 301 | // Increase max fee per gas by 110% (set it to 210% of the original) 302 | self.bump_privileged_l2(tx, 110); 303 | let wrapped_tx = &mut WrappedTransaction::L2(tx.clone()); 304 | self.estimate_gas_for_wrapped_tx(wrapped_tx, from).await?; 305 | if let WrappedTransaction::L2(l2_tx) = wrapped_tx { 306 | tx.max_fee_per_gas = l2_tx.max_fee_per_gas; 307 | tx.max_priority_fee_per_gas = l2_tx.max_fee_per_gas; 308 | tx.gas_limit = l2_tx.gas_limit; 309 | } 310 | self.send_privileged_l2_transaction(tx).await 311 | } 312 | 313 | /// Increase max fee per gas by percentage% (set it to (100+percentage)% of the original) 314 | pub fn bump_privileged_l2(&self, tx: &mut PrivilegedL2Transaction, percentage: u64) { 315 | tx.max_fee_per_gas = (tx.max_fee_per_gas * (100 + percentage)) / 100; 316 | tx.max_priority_fee_per_gas += (tx.max_priority_fee_per_gas * (100 + percentage)) / 100; 317 | } 318 | 319 | pub async fn send_privileged_l2_transaction( 320 | &self, 321 | tx: &PrivilegedL2Transaction, 322 | ) -> Result { 323 | let mut encoded_tx = tx.encode_to_vec(); 324 | encoded_tx.insert(0, TxType::Privileged.into()); 325 | 326 | self.send_raw_transaction(encoded_tx.as_slice()).await 327 | } 328 | 329 | pub async fn estimate_gas( 330 | &self, 331 | transaction: GenericTransaction, 332 | ) -> Result { 333 | // If the transaction.to field matches TxKind::Create, we use an empty string. 334 | // In this way, when the blockchain receives the request, it deserializes the json into a GenericTransaction, 335 | // with the 'to' field set to TxKind::Create. 336 | // The TxKind has TxKind::Create as #[default] 337 | // https://github.com/lambdaclass/ethrex/blob/41b124c39030ad5d0b2674d7369be3599c7a3008/crates/common/types/transaction.rs#L267 338 | let to = match transaction.to { 339 | TxKind::Call(addr) => format!("{addr:#x}"), 340 | TxKind::Create => String::new(), 341 | }; 342 | let mut data = json!({ 343 | "to": to, 344 | "input": format!("0x{:#x}", transaction.input), 345 | "from": format!("{:#x}", transaction.from), 346 | "value": format!("{:#x}", transaction.value), 347 | }); 348 | 349 | // Add the nonce just if present, otherwise the RPC will use the latest nonce 350 | if let Some(nonce) = transaction.nonce { 351 | if let Value::Object(ref mut map) = data { 352 | map.insert("nonce".to_owned(), json!(format!("{nonce:#x}"))); 353 | } 354 | } 355 | 356 | let request = RpcRequest { 357 | id: RpcRequestId::Number(1), 358 | jsonrpc: "2.0".to_string(), 359 | method: "eth_estimateGas".to_string(), 360 | params: Some(vec![data, json!("latest")]), 361 | }; 362 | 363 | match self.send_request(request).await { 364 | Ok(RpcResponse::Success(result)) => { 365 | let res = serde_json::from_value::(result.result) 366 | .map_err(EstimateGasPriceError::SerdeJSONError)?; 367 | let res = res.get(2..).ok_or(EstimateGasPriceError::Custom( 368 | "Failed to slice index response in estimate_gas".to_owned(), 369 | ))?; 370 | u64::from_str_radix(res, 16) 371 | } 372 | .map_err(EstimateGasPriceError::ParseIntError) 373 | .map_err(EthClientError::from), 374 | Ok(RpcResponse::Error(error_response)) => { 375 | let error_data = if let Some(error_data) = error_response.error.data { 376 | if &error_data == "0x" { 377 | "unknown error".to_owned() 378 | } else { 379 | println!("{error_data}"); 380 | let abi_decoded_error_data = hex::decode( 381 | error_data.strip_prefix("0x").ok_or(EthClientError::Custom( 382 | "Failed to strip_prefix in estimate_gas".to_owned(), 383 | ))?, 384 | ) 385 | .map_err(|_| { 386 | EthClientError::Custom( 387 | "Failed to hex::decode in estimate_gas".to_owned(), 388 | ) 389 | })?; 390 | let string_length = U256::from_big_endian( 391 | abi_decoded_error_data 392 | .get(36..68) 393 | .ok_or(EthClientError::Custom( 394 | "Failed to slice index abi_decoded_error_data in estimate_gas" 395 | .to_owned(), 396 | ))?, 397 | ); 398 | 399 | let string_len = if string_length > usize::MAX.into() { 400 | return Err(EthClientError::Custom( 401 | "Failed to convert string_length to usize in estimate_gas" 402 | .to_owned(), 403 | )); 404 | } else { 405 | string_length.as_usize() 406 | }; 407 | let string_data = abi_decoded_error_data.get(68..68 + string_len).ok_or( 408 | EthClientError::Custom( 409 | "Failed to slice index abi_decoded_error_data in estimate_gas" 410 | .to_owned(), 411 | ), 412 | )?; 413 | String::from_utf8(string_data.to_vec()).map_err(|_| { 414 | EthClientError::Custom( 415 | "Failed to String::from_utf8 in estimate_gas".to_owned(), 416 | ) 417 | })? 418 | } 419 | } else { 420 | "unknown error".to_owned() 421 | }; 422 | Err(EstimateGasPriceError::RPCError(format!( 423 | "{}: {}", 424 | error_response.error.message, error_data 425 | )) 426 | .into()) 427 | } 428 | Err(error) => Err(error), 429 | } 430 | } 431 | 432 | pub async fn get_gas_price(&self) -> Result { 433 | let request = RpcRequest { 434 | id: RpcRequestId::Number(1), 435 | jsonrpc: "2.0".to_string(), 436 | method: "eth_gasPrice".to_string(), 437 | params: None, 438 | }; 439 | 440 | match self.send_request(request).await { 441 | Ok(RpcResponse::Success(result)) => serde_json::from_value(result.result) 442 | .map_err(GetGasPriceError::SerdeJSONError) 443 | .map_err(EthClientError::from), 444 | Ok(RpcResponse::Error(error_response)) => { 445 | Err(GetGasPriceError::RPCError(error_response.error.message).into()) 446 | } 447 | Err(error) => Err(error), 448 | } 449 | } 450 | 451 | pub async fn get_nonce(&self, address: Address) -> Result { 452 | let request = RpcRequest { 453 | id: RpcRequestId::Number(1), 454 | jsonrpc: "2.0".to_string(), 455 | method: "eth_getTransactionCount".to_string(), 456 | params: Some(vec![json!(format!("{address:#x}")), json!("latest")]), 457 | }; 458 | 459 | match self.send_request(request).await { 460 | Ok(RpcResponse::Success(result)) => u64::from_str_radix( 461 | serde_json::from_value::(result.result) 462 | .map_err(GetNonceError::SerdeJSONError)? 463 | .get(2..) 464 | .ok_or(EthClientError::Custom( 465 | "Failed to deserialize get_nonce request".to_owned(), 466 | ))?, 467 | 16, 468 | ) 469 | .map_err(GetNonceError::ParseIntError) 470 | .map_err(EthClientError::from), 471 | Ok(RpcResponse::Error(error_response)) => { 472 | Err(GetNonceError::RPCError(error_response.error.message).into()) 473 | } 474 | Err(error) => Err(error), 475 | } 476 | } 477 | 478 | pub async fn get_block_number(&self) -> Result { 479 | let request = RpcRequest { 480 | id: RpcRequestId::Number(1), 481 | jsonrpc: "2.0".to_string(), 482 | method: "eth_blockNumber".to_string(), 483 | params: None, 484 | }; 485 | 486 | match self.send_request(request).await { 487 | Ok(RpcResponse::Success(result)) => serde_json::from_value(result.result) 488 | .map_err(GetBlockNumberError::SerdeJSONError) 489 | .map_err(EthClientError::from), 490 | Ok(RpcResponse::Error(error_response)) => { 491 | Err(GetBlockNumberError::RPCError(error_response.error.message).into()) 492 | } 493 | Err(error) => Err(error), 494 | } 495 | } 496 | 497 | pub async fn get_block_by_hash(&self, block_hash: H256) -> Result { 498 | let request = RpcRequest { 499 | id: RpcRequestId::Number(1), 500 | jsonrpc: "2.0".to_string(), 501 | method: "eth_getBlockByHash".to_string(), 502 | params: Some(vec![json!(block_hash), json!(true)]), 503 | }; 504 | 505 | match self.send_request(request).await { 506 | Ok(RpcResponse::Success(result)) => serde_json::from_value(result.result) 507 | .map_err(GetBlockByHashError::SerdeJSONError) 508 | .map_err(EthClientError::from), 509 | Ok(RpcResponse::Error(error_response)) => { 510 | Err(GetBlockByHashError::RPCError(error_response.error.message).into()) 511 | } 512 | Err(error) => Err(error), 513 | } 514 | } 515 | 516 | pub async fn get_next_block_to_commit( 517 | eth_client: &EthClient, 518 | on_chain_proposer_address: Address, 519 | ) -> Result { 520 | Self::_call_block_variable( 521 | eth_client, 522 | b"nextBlockToCommit()", 523 | on_chain_proposer_address, 524 | ) 525 | .await 526 | } 527 | 528 | /// Fetches a block from the Ethereum blockchain by its number or the latest/earliest/pending block. 529 | /// If no `block_number` is provided, get the latest. 530 | pub async fn get_block_by_number( 531 | &self, 532 | block: BlockByNumber, 533 | ) -> Result { 534 | let r = match block { 535 | BlockByNumber::Number(n) => format!("{n:#x}"), 536 | BlockByNumber::Latest => "latest".to_owned(), 537 | BlockByNumber::Earliest => "earliest".to_owned(), 538 | BlockByNumber::Pending => "pending".to_owned(), 539 | }; 540 | let request = RpcRequest { 541 | id: RpcRequestId::Number(1), 542 | jsonrpc: "2.0".to_string(), 543 | method: "eth_getBlockByNumber".to_string(), 544 | // With false it just returns the hash of the transactions. 545 | params: Some(vec![json!(r), json!(false)]), 546 | }; 547 | 548 | match self.send_request(request).await { 549 | Ok(RpcResponse::Success(result)) => serde_json::from_value(result.result) 550 | .map_err(GetBlockByNumberError::SerdeJSONError) 551 | .map_err(EthClientError::from), 552 | Ok(RpcResponse::Error(error_response)) => { 553 | Err(GetBlockByNumberError::RPCError(error_response.error.message).into()) 554 | } 555 | Err(error) => Err(error), 556 | } 557 | } 558 | 559 | pub async fn get_logs( 560 | &self, 561 | from_block: U256, 562 | to_block: U256, 563 | address: Address, 564 | topic: H256, 565 | ) -> Result, EthClientError> { 566 | let request = RpcRequest { 567 | id: RpcRequestId::Number(1), 568 | jsonrpc: "2.0".to_string(), 569 | method: "eth_getLogs".to_string(), 570 | params: Some(vec![serde_json::json!( 571 | { 572 | "fromBlock": format!("{:#x}", from_block), 573 | "toBlock": format!("{:#x}", to_block), 574 | "address": format!("{:#x}", address), 575 | "topics": [format!("{:#x}", topic)] 576 | } 577 | )]), 578 | }; 579 | 580 | match self.send_request(request).await { 581 | Ok(RpcResponse::Success(result)) => serde_json::from_value(result.result) 582 | .map_err(GetLogsError::SerdeJSONError) 583 | .map_err(EthClientError::from), 584 | Ok(RpcResponse::Error(error_response)) => { 585 | Err(GetLogsError::RPCError(error_response.error.message).into()) 586 | } 587 | Err(error) => Err(error), 588 | } 589 | } 590 | 591 | pub async fn get_transaction_receipt( 592 | &self, 593 | tx_hash: H256, 594 | ) -> Result, EthClientError> { 595 | let request = RpcRequest { 596 | id: RpcRequestId::Number(1), 597 | jsonrpc: "2.0".to_string(), 598 | method: "eth_getTransactionReceipt".to_string(), 599 | params: Some(vec![json!(format!("{:#x}", tx_hash))]), 600 | }; 601 | 602 | match self.send_request(request).await { 603 | Ok(RpcResponse::Success(result)) => serde_json::from_value(result.result) 604 | .map_err(GetTransactionReceiptError::SerdeJSONError) 605 | .map_err(EthClientError::from), 606 | Ok(RpcResponse::Error(error_response)) => { 607 | Err(GetTransactionReceiptError::RPCError(error_response.error.message).into()) 608 | } 609 | Err(error) => Err(error), 610 | } 611 | } 612 | 613 | pub async fn get_balance(&self, address: Address) -> Result { 614 | let request = RpcRequest { 615 | id: RpcRequestId::Number(1), 616 | jsonrpc: "2.0".to_string(), 617 | method: "eth_getBalance".to_string(), 618 | params: Some(vec![json!(format!("{:#x}", address)), json!("latest")]), 619 | }; 620 | 621 | match self.send_request(request).await { 622 | Ok(RpcResponse::Success(result)) => serde_json::from_value(result.result) 623 | .map_err(GetBalanceError::SerdeJSONError) 624 | .map_err(EthClientError::from), 625 | Ok(RpcResponse::Error(error_response)) => { 626 | Err(GetBalanceError::RPCError(error_response.error.message).into()) 627 | } 628 | Err(error) => Err(error), 629 | } 630 | } 631 | 632 | pub async fn get_token_balance( 633 | &self, 634 | address: Address, 635 | token_address: Address, 636 | ) -> Result { 637 | let mut calldata = Vec::from(BALANCE_OF_SELECTOR); 638 | calldata.resize(16, 0); 639 | calldata.extend(address.to_fixed_bytes()); 640 | U256::from_str_radix( 641 | &self 642 | .call(token_address, calldata.into(), Overrides::default()) 643 | .await?, 644 | 16, 645 | ) 646 | .map_err(|_| { 647 | EthClientError::Custom(format!("Address {token_address} did not return a uint256")) 648 | }) 649 | } 650 | 651 | pub async fn get_chain_id(&self) -> Result { 652 | let request = RpcRequest { 653 | id: RpcRequestId::Number(1), 654 | jsonrpc: "2.0".to_string(), 655 | method: "eth_chainId".to_string(), 656 | params: None, 657 | }; 658 | 659 | match self.send_request(request).await { 660 | Ok(RpcResponse::Success(result)) => serde_json::from_value(result.result) 661 | .map_err(GetBalanceError::SerdeJSONError) 662 | .map_err(EthClientError::from), 663 | Ok(RpcResponse::Error(error_response)) => { 664 | Err(GetBalanceError::RPCError(error_response.error.message).into()) 665 | } 666 | Err(error) => Err(error), 667 | } 668 | } 669 | 670 | pub async fn get_transaction_by_hash( 671 | &self, 672 | tx_hash: H256, 673 | ) -> Result, EthClientError> { 674 | let request = RpcRequest { 675 | id: RpcRequestId::Number(1), 676 | jsonrpc: "2.0".to_string(), 677 | method: "eth_getTransactionByHash".to_string(), 678 | params: Some(vec![json!(format!("{tx_hash:#x}"))]), 679 | }; 680 | 681 | match self.send_request(request).await { 682 | Ok(RpcResponse::Success(result)) => serde_json::from_value(result.result) 683 | .map_err(GetTransactionByHashError::SerdeJSONError) 684 | .map_err(EthClientError::from), 685 | Ok(RpcResponse::Error(error_response)) => { 686 | Err(GetTransactionByHashError::RPCError(error_response.error.message).into()) 687 | } 688 | Err(error) => Err(error), 689 | } 690 | } 691 | 692 | pub async fn estimate_gas_for_wrapped_tx( 693 | &self, 694 | wrapped_tx: &mut WrappedTransaction, 695 | from: H160, 696 | ) -> Result { 697 | loop { 698 | let mut transaction = match wrapped_tx { 699 | WrappedTransaction::EIP4844(wrapped_eip4844_transaction) => { 700 | GenericTransaction::from(wrapped_eip4844_transaction.clone().tx) 701 | } 702 | WrappedTransaction::EIP1559(eip1559_transaction) => { 703 | GenericTransaction::from(eip1559_transaction.clone()) 704 | } 705 | WrappedTransaction::L2(privileged_l2_transaction) => { 706 | GenericTransaction::from(privileged_l2_transaction.clone()) 707 | } 708 | }; 709 | 710 | transaction.from = from; 711 | 712 | match self.estimate_gas(transaction).await { 713 | Ok(gas_limit) => return Ok(gas_limit), 714 | Err(e) => { 715 | let error = format!("{e}").to_owned(); 716 | if error.contains("transaction underpriced") { 717 | match wrapped_tx { 718 | WrappedTransaction::EIP4844(wrapped_eip4844_transaction) => { 719 | self.bump_eip4844(wrapped_eip4844_transaction, 110); 720 | } 721 | WrappedTransaction::EIP1559(eip1559_transaction) => { 722 | self.bump_eip1559(eip1559_transaction, 110); 723 | } 724 | WrappedTransaction::L2(privileged_l2_transaction) => { 725 | self.bump_privileged_l2(privileged_l2_transaction, 110); 726 | } 727 | }; 728 | continue; 729 | } 730 | return Err(e); 731 | } 732 | }; 733 | } 734 | } 735 | 736 | /// Build an EIP1559 transaction with the given parameters. 737 | /// Either `overrides.nonce` or `overrides.from` must be provided. 738 | /// If `overrides.gas_price`, `overrides.chain_id` or `overrides.gas_price` 739 | /// are not provided, the client will fetch them from the network. 740 | /// If `overrides.gas_limit` is not provided, the client will estimate the tx cost. 741 | pub async fn build_eip1559_transaction( 742 | &self, 743 | to: Address, 744 | from: Address, 745 | calldata: Bytes, 746 | overrides: Overrides, 747 | bump_retries: u64, 748 | ) -> Result { 749 | let mut get_gas_price = 1; 750 | let mut tx = EIP1559Transaction { 751 | to: overrides.to.clone().unwrap_or(TxKind::Call(to)), 752 | chain_id: if let Some(chain_id) = overrides.chain_id { 753 | chain_id 754 | } else { 755 | self.get_chain_id().await?.try_into().map_err(|_| { 756 | EthClientError::Custom("Failed at get_chain_id().try_into()".to_owned()) 757 | })? 758 | }, 759 | nonce: self 760 | .get_nonce_from_overrides_or_rpc(&overrides, from) 761 | .await?, 762 | max_fee_per_gas: if let Some(gas_price) = overrides.max_fee_per_gas { 763 | gas_price 764 | } else { 765 | get_gas_price = self.get_gas_price().await?.try_into().map_err(|_| { 766 | EthClientError::Custom("Failed at gas_price.try_into()".to_owned()) 767 | })?; 768 | 769 | get_gas_price 770 | }, 771 | max_priority_fee_per_gas: overrides.max_priority_fee_per_gas.unwrap_or(get_gas_price), 772 | value: overrides.value.unwrap_or_default(), 773 | data: calldata, 774 | access_list: overrides.access_list, 775 | ..Default::default() 776 | }; 777 | 778 | let mut wrapped_tx; 779 | 780 | if let Some(overrides_gas_limit) = overrides.gas_limit { 781 | tx.gas_limit = overrides_gas_limit; 782 | Ok(tx) 783 | } else { 784 | let mut retry = 0_u64; 785 | while retry < bump_retries { 786 | wrapped_tx = WrappedTransaction::EIP1559(tx.clone()); 787 | match self 788 | .estimate_gas_for_wrapped_tx(&mut wrapped_tx, from) 789 | .await 790 | { 791 | Ok(gas_limit) => { 792 | // Estimation succeeded. 793 | tx.gas_limit = gas_limit; 794 | return Ok(tx); 795 | } 796 | Err(e) => { 797 | let error = format!("{e}"); 798 | if error.contains("replacement transaction underpriced") { 799 | warn!("Bumping gas while building: already known"); 800 | retry += 1; 801 | self.bump_eip1559(&mut tx, 110); 802 | continue; 803 | } 804 | return Err(e); 805 | } 806 | } 807 | } 808 | Err(EthClientError::EstimateGasPriceError( 809 | EstimateGasPriceError::Custom( 810 | "Exceeded maximum retries while estimating gas.".to_string(), 811 | ), 812 | )) 813 | } 814 | } 815 | 816 | /// Build an EIP4844 transaction with the given parameters. 817 | /// Either `overrides.nonce` or `overrides.from` must be provided. 818 | /// If `overrides.gas_price`, `overrides.chain_id` or `overrides.gas_price` 819 | /// are not provided, the client will fetch them from the network. 820 | /// If `overrides.gas_limit` is not provided, the client will estimate the tx cost. 821 | pub async fn build_eip4844_transaction( 822 | &self, 823 | to: Address, 824 | from: Address, 825 | calldata: Bytes, 826 | overrides: Overrides, 827 | blobs_bundle: BlobsBundle, 828 | bump_retries: u64, 829 | ) -> Result { 830 | let blob_versioned_hashes = blobs_bundle.generate_versioned_hashes(); 831 | let mut get_gas_price = 1; 832 | let tx = EIP4844Transaction { 833 | to, 834 | chain_id: if let Some(chain_id) = overrides.chain_id { 835 | chain_id 836 | } else { 837 | self.get_chain_id().await?.try_into().map_err(|_| { 838 | EthClientError::Custom("Failed at get_chain_id().try_into()".to_owned()) 839 | })? 840 | }, 841 | nonce: self 842 | .get_nonce_from_overrides_or_rpc(&overrides, from) 843 | .await?, 844 | max_fee_per_gas: if let Some(gas_price) = overrides.max_fee_per_gas { 845 | gas_price 846 | } else { 847 | get_gas_price = self.get_gas_price().await?.try_into().map_err(|_| { 848 | EthClientError::Custom("Failed at gas_price.try_into()".to_owned()) 849 | })?; 850 | 851 | get_gas_price 852 | }, 853 | max_priority_fee_per_gas: overrides.max_priority_fee_per_gas.unwrap_or(get_gas_price), 854 | value: overrides.value.unwrap_or_default(), 855 | data: calldata, 856 | access_list: overrides.access_list, 857 | max_fee_per_blob_gas: overrides.gas_price_per_blob.unwrap_or_default(), 858 | blob_versioned_hashes, 859 | ..Default::default() 860 | }; 861 | 862 | let mut wrapped_eip4844 = WrappedEIP4844Transaction { tx, blobs_bundle }; 863 | let mut wrapped_tx; 864 | if let Some(overrides_gas_limit) = overrides.gas_limit { 865 | wrapped_eip4844.tx.gas = overrides_gas_limit; 866 | Ok(wrapped_eip4844) 867 | } else { 868 | let mut retry = 0_u64; 869 | while retry < bump_retries { 870 | wrapped_tx = WrappedTransaction::EIP4844(wrapped_eip4844.clone()); 871 | 872 | match self 873 | .estimate_gas_for_wrapped_tx(&mut wrapped_tx, from) 874 | .await 875 | { 876 | Ok(gas_limit) => { 877 | // Estimation succeeded. 878 | wrapped_eip4844.tx.gas = gas_limit; 879 | return Ok(wrapped_eip4844); 880 | } 881 | Err(e) => { 882 | let error = format!("{e}"); 883 | if error.contains("already known") { 884 | warn!("Bumping gas while building: already known"); 885 | retry += 1; 886 | self.bump_eip4844(&mut wrapped_eip4844, 110); 887 | continue; 888 | } 889 | return Err(e); 890 | } 891 | } 892 | } 893 | Err(EthClientError::EstimateGasPriceError( 894 | EstimateGasPriceError::Custom( 895 | "Exceeded maximum retries while estimating gas.".to_string(), 896 | ), 897 | )) 898 | } 899 | } 900 | 901 | /// Build a PrivilegedL2 transaction with the given parameters. 902 | /// Either `overrides.nonce` or `overrides.from` must be provided. 903 | /// If `overrides.gas_price`, `overrides.chain_id` or `overrides.gas_price` 904 | /// are not provided, the client will fetch them from the network. 905 | /// If `overrides.gas_limit` is not provided, the client will estimate the tx cost. 906 | pub async fn build_privileged_transaction( 907 | &self, 908 | to: Address, 909 | from: Address, 910 | calldata: Bytes, 911 | overrides: Overrides, 912 | bump_retries: u64, 913 | ) -> Result { 914 | let mut get_gas_price = 1; 915 | let mut tx = PrivilegedL2Transaction { 916 | to: TxKind::Call(to), 917 | chain_id: if let Some(chain_id) = overrides.chain_id { 918 | chain_id 919 | } else { 920 | self.get_chain_id().await?.try_into().map_err(|_| { 921 | EthClientError::Custom("Failed at get_chain_id().try_into()".to_owned()) 922 | })? 923 | }, 924 | nonce: self 925 | .get_nonce_from_overrides_or_rpc(&overrides, from) 926 | .await?, 927 | max_fee_per_gas: if let Some(gas_price) = overrides.max_fee_per_gas { 928 | gas_price 929 | } else { 930 | get_gas_price = self.get_gas_price().await?.try_into().map_err(|_| { 931 | EthClientError::Custom("Failed at gas_price.try_into()".to_owned()) 932 | })?; 933 | 934 | get_gas_price 935 | }, 936 | max_priority_fee_per_gas: overrides.max_priority_fee_per_gas.unwrap_or(get_gas_price), 937 | value: overrides.value.unwrap_or_default(), 938 | data: calldata, 939 | access_list: overrides.access_list, 940 | ..Default::default() 941 | }; 942 | 943 | let mut wrapped_tx; 944 | 945 | if let Some(overrides_gas_limit) = overrides.gas_limit { 946 | tx.gas_limit = overrides_gas_limit; 947 | Ok(tx) 948 | } else { 949 | let mut retry = 0_u64; 950 | while retry < bump_retries { 951 | wrapped_tx = WrappedTransaction::L2(tx.clone()); 952 | match self 953 | .estimate_gas_for_wrapped_tx(&mut wrapped_tx, from) 954 | .await 955 | { 956 | Ok(gas_limit) => { 957 | // Estimation succeeded. 958 | tx.gas_limit = gas_limit; 959 | return Ok(tx); 960 | } 961 | Err(e) => { 962 | let error = format!("{e}"); 963 | if error.contains("already known") { 964 | warn!("Bumping gas while building: already known"); 965 | retry += 1; 966 | self.bump_privileged_l2(&mut tx, 110); 967 | continue; 968 | } 969 | return Err(e); 970 | } 971 | } 972 | } 973 | Err(EthClientError::EstimateGasPriceError( 974 | EstimateGasPriceError::Custom( 975 | "Exceeded maximum retries while estimating gas.".to_string(), 976 | ), 977 | )) 978 | } 979 | } 980 | 981 | async fn get_nonce_from_overrides_or_rpc( 982 | &self, 983 | overrides: &Overrides, 984 | address: Address, 985 | ) -> Result { 986 | if let Some(nonce) = overrides.nonce { 987 | return Ok(nonce); 988 | } 989 | self.get_nonce(address).await 990 | } 991 | 992 | pub async fn get_last_committed_block( 993 | eth_client: &EthClient, 994 | on_chain_proposer_address: Address, 995 | ) -> Result { 996 | Self::_call_block_variable( 997 | eth_client, 998 | b"lastCommittedBlock()", 999 | on_chain_proposer_address, 1000 | ) 1001 | .await 1002 | } 1003 | 1004 | pub async fn get_last_verified_block( 1005 | eth_client: &EthClient, 1006 | on_chain_proposer_address: Address, 1007 | ) -> Result { 1008 | Self::_call_block_variable( 1009 | eth_client, 1010 | b"lastVerifiedBlock()", 1011 | on_chain_proposer_address, 1012 | ) 1013 | .await 1014 | } 1015 | 1016 | pub async fn get_last_fetched_l1_block( 1017 | eth_client: &EthClient, 1018 | common_bridge_address: Address, 1019 | ) -> Result { 1020 | Self::_call_block_variable(eth_client, b"lastFetchedL1Block()", common_bridge_address).await 1021 | } 1022 | 1023 | async fn _call_block_variable( 1024 | eth_client: &EthClient, 1025 | selector: &[u8], 1026 | on_chain_proposer_address: Address, 1027 | ) -> Result { 1028 | let selector = keccak(selector) 1029 | .as_bytes() 1030 | .get(..4) 1031 | .ok_or(EthClientError::Custom("Failed to get selector.".to_owned()))? 1032 | .to_vec(); 1033 | 1034 | let mut calldata = Vec::new(); 1035 | calldata.extend_from_slice(&selector); 1036 | 1037 | let leading_zeros = 32 - ((calldata.len() - 4) % 32); 1038 | calldata.extend(vec![0; leading_zeros]); 1039 | 1040 | let hex_str = eth_client 1041 | .call( 1042 | on_chain_proposer_address, 1043 | calldata.into(), 1044 | Overrides::default(), 1045 | ) 1046 | .await?; 1047 | 1048 | let value = from_hex_string_to_u256(&hex_str)?.try_into().map_err(|_| { 1049 | EthClientError::Custom("Failed to convert from_hex_string_to_u256()".to_owned()) 1050 | })?; 1051 | 1052 | Ok(value) 1053 | } 1054 | 1055 | pub async fn wait_for_transaction_receipt( 1056 | &self, 1057 | tx_hash: H256, 1058 | max_retries: u64, 1059 | ) -> Result { 1060 | let mut receipt = self.get_transaction_receipt(tx_hash).await?; 1061 | let mut r#try = 1; 1062 | while receipt.is_none() { 1063 | println!("[{try}/{max_retries}] Retrying to get transaction receipt for {tx_hash:#x}"); 1064 | 1065 | if max_retries == r#try { 1066 | return Err(EthClientError::Custom(format!( 1067 | "Transaction receipt for {tx_hash:#x} not found after {max_retries} retries" 1068 | ))); 1069 | } 1070 | r#try += 1; 1071 | 1072 | tokio::time::sleep(std::time::Duration::from_secs(2)).await; 1073 | 1074 | receipt = self.get_transaction_receipt(tx_hash).await?; 1075 | } 1076 | receipt.ok_or(EthClientError::Custom( 1077 | "Transaction receipt is None".to_owned(), 1078 | )) 1079 | } 1080 | 1081 | pub async fn get_code( 1082 | &self, 1083 | address: Address, 1084 | block: String, 1085 | ) -> Result { 1086 | let request = RpcRequest { 1087 | id: RpcRequestId::Number(1), 1088 | jsonrpc: "2.0".to_string(), 1089 | method: "eth_getCode".to_string(), 1090 | params: Some(vec![json!(address), json!(block)]), 1091 | }; 1092 | 1093 | match self.send_request(request).await { 1094 | Ok(RpcResponse::Success(result)) => serde_json::from_value(result.result) 1095 | .map_err(SendRawTransactionError::SerdeJSONError) 1096 | .map_err(EthClientError::from), 1097 | Ok(RpcResponse::Error(error_response)) => { 1098 | Err(SendRawTransactionError::RPCError(error_response.error.message).into()) 1099 | } 1100 | Err(error) => Err(error), 1101 | } 1102 | } 1103 | } 1104 | 1105 | pub fn from_hex_string_to_u256(hex_str: &str) -> Result { 1106 | let hex_string = hex_str.strip_prefix("0x").ok_or(EthClientError::Custom( 1107 | "Couldn't strip prefix from last_committed_block.".to_owned(), 1108 | ))?; 1109 | 1110 | if hex_string.is_empty() { 1111 | return Err(EthClientError::Custom( 1112 | "Failed to fetch last_committed_block. Manual intervention required.".to_owned(), 1113 | )); 1114 | } 1115 | 1116 | let value = U256::from_str_radix(hex_string, 16).map_err(|_| { 1117 | EthClientError::Custom( 1118 | "Failed to parse after call, U256::from_str_radix failed.".to_owned(), 1119 | ) 1120 | })?; 1121 | Ok(value) 1122 | } 1123 | 1124 | pub fn get_address_from_secret_key(secret_key: &SecretKey) -> Result { 1125 | let public_key = secret_key 1126 | .public_key(secp256k1::SECP256K1) 1127 | .serialize_uncompressed(); 1128 | let hash = keccak(&public_key[1..]); 1129 | 1130 | // Get the last 20 bytes of the hash 1131 | let address_bytes: [u8; 20] = hash 1132 | .as_ref() 1133 | .get(12..32) 1134 | .ok_or(EthClientError::Custom( 1135 | "Failed to get_address_from_secret_key: error slicing address_bytes".to_owned(), 1136 | ))? 1137 | .try_into() 1138 | .map_err(|err| { 1139 | EthClientError::Custom(format!("Failed to get_address_from_secret_key: {err}")) 1140 | })?; 1141 | 1142 | Ok(Address::from(address_bytes)) 1143 | } 1144 | 1145 | // This function takes signatures that are computed as a 0x45 signature, as described in EIP-191 (https://eips.ethereum.org/EIPS/eip-191), 1146 | // then it has an extra byte concatenated at the end, which is a scalar value added to the signatures parity, 1147 | // as described in the Yellow Paper Section 4.2 in the specification of a transaction's w field. (https://ethereum.github.io/yellowpaper/paper.pdf) 1148 | pub fn get_address_from_message_and_signature( 1149 | message: Bytes, 1150 | signature: Bytes, 1151 | ) -> Result { 1152 | let raw_recovery_id = if signature[64] >= 27 { 1153 | signature[64] - 27 1154 | } else { 1155 | signature[64] 1156 | }; 1157 | 1158 | let recovery_id = secp256k1::ecdsa::RecoveryId::from_i32(raw_recovery_id as i32)?; 1159 | 1160 | let signature = 1161 | secp256k1::ecdsa::RecoverableSignature::from_compact(&signature[..64], recovery_id)?; 1162 | 1163 | let payload = [ 1164 | b"\x19Ethereum Signed Message:\n", 1165 | message.len().to_string().as_bytes(), 1166 | message.as_ref(), 1167 | ] 1168 | .concat(); 1169 | 1170 | let signer_public_key = signature.recover(&secp256k1::Message::from_digest( 1171 | *keccak(payload).as_fixed_bytes(), 1172 | ))?; 1173 | 1174 | Ok(Address::from_slice( 1175 | &keccak(&signer_public_key.serialize_uncompressed()[1..])[12..], 1176 | )) 1177 | } 1178 | 1179 | #[derive(Serialize, Deserialize, Debug)] 1180 | #[serde(rename_all = "camelCase")] 1181 | pub struct GetTransactionByHashTransactionResponse { 1182 | #[serde(default, with = "ethrex_common::serde_utils::u64::hex_str")] 1183 | pub chain_id: u64, 1184 | #[serde(default, with = "ethrex_common::serde_utils::u64::hex_str")] 1185 | pub nonce: u64, 1186 | #[serde(default, with = "ethrex_common::serde_utils::u64::hex_str")] 1187 | pub max_priority_fee_per_gas: u64, 1188 | #[serde(default, with = "ethrex_common::serde_utils::u64::hex_str")] 1189 | pub max_fee_per_gas: u64, 1190 | #[serde(default, with = "ethrex_common::serde_utils::u64::hex_str")] 1191 | pub gas_limit: u64, 1192 | #[serde(default)] 1193 | pub to: Address, 1194 | #[serde(default)] 1195 | pub value: U256, 1196 | #[serde(default)] 1197 | pub data: Vec, 1198 | #[serde(default)] 1199 | pub access_list: Vec<(Address, Vec)>, 1200 | #[serde(default)] 1201 | pub r#type: TxType, 1202 | #[serde(default)] 1203 | pub signature_y_parity: bool, 1204 | #[serde(default, with = "ethrex_common::serde_utils::u64::hex_str")] 1205 | pub signature_r: u64, 1206 | #[serde(default, with = "ethrex_common::serde_utils::u64::hex_str")] 1207 | pub signature_s: u64, 1208 | #[serde(default)] 1209 | pub block_number: U256, 1210 | #[serde(default)] 1211 | pub block_hash: H256, 1212 | #[serde(default)] 1213 | pub from: Address, 1214 | #[serde(default)] 1215 | pub hash: H256, 1216 | #[serde(default, with = "ethrex_common::serde_utils::u64::hex_str")] 1217 | pub transaction_index: u64, 1218 | } 1219 | 1220 | impl fmt::Display for GetTransactionByHashTransactionResponse { 1221 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 1222 | write!( 1223 | f, 1224 | r#" 1225 | chain_id: {}, 1226 | nonce: {}, 1227 | max_priority_fee_per_gas: {}, 1228 | max_fee_per_gas: {}, 1229 | gas_limit: {}, 1230 | to: {:#x}, 1231 | value: {}, 1232 | data: {:#?}, 1233 | access_list: {:#?}, 1234 | type: {:?}, 1235 | signature_y_parity: {}, 1236 | signature_r: {:x}, 1237 | signature_s: {:x}, 1238 | block_number: {}, 1239 | block_hash: {:#x}, 1240 | from: {:#x}, 1241 | hash: {:#x}, 1242 | transaction_index: {} 1243 | "#, 1244 | self.chain_id, 1245 | self.nonce, 1246 | self.max_priority_fee_per_gas, 1247 | self.max_fee_per_gas, 1248 | self.gas_limit, 1249 | self.to, 1250 | self.value, 1251 | self.data, 1252 | self.access_list, 1253 | self.r#type, 1254 | self.signature_y_parity, 1255 | self.signature_r, 1256 | self.signature_s, 1257 | self.block_number, 1258 | self.block_hash, 1259 | self.from, 1260 | self.hash, 1261 | self.transaction_index 1262 | ) 1263 | } 1264 | } 1265 | -------------------------------------------------------------------------------- /sdk/src/client/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod eth; 2 | pub use eth::{EthClient, errors::EthClientError, eth_sender::Overrides}; 3 | 4 | pub use ethrex_rpc::clients::auth; 5 | -------------------------------------------------------------------------------- /sdk/src/errors.rs: -------------------------------------------------------------------------------- 1 | use crate::client::EthClientError; 2 | 3 | #[derive(Debug, thiserror::Error)] 4 | pub enum Error { 5 | #[error(transparent)] 6 | EthClientError(#[from] EthClientError), 7 | } 8 | -------------------------------------------------------------------------------- /sdk/src/l2/constants.rs: -------------------------------------------------------------------------------- 1 | use ethrex_common::{Address, H160}; 2 | 3 | // Contract Addresses 4 | 5 | pub const COMMON_BRIDGE_L2_ADDRESS: Address = H160([ 6 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 7 | 0x00, 0x00, 0xff, 0xff, 8 | ]); 9 | 10 | // Function Signatures 11 | 12 | pub const L2_WITHDRAW_SIGNATURE: &str = "withdraw(address)"; 13 | 14 | // Function Selectors 15 | -------------------------------------------------------------------------------- /sdk/src/l2/deposit.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | client::{EthClient, EthClientError, Overrides}, 3 | transfer, 4 | }; 5 | use ethrex_common::{Address, H256, U256}; 6 | use secp256k1::SecretKey; 7 | 8 | pub async fn deposit( 9 | amount: U256, 10 | from: Address, 11 | from_pk: SecretKey, 12 | eth_client: &EthClient, 13 | bridge_address: Address, 14 | mut overrides: Overrides, 15 | ) -> Result { 16 | overrides.value = Some(amount); 17 | transfer(amount, from, bridge_address, from_pk, eth_client, overrides).await 18 | } 19 | -------------------------------------------------------------------------------- /sdk/src/l2/merkle_tree.rs: -------------------------------------------------------------------------------- 1 | use ethrex_common::H256; 2 | use keccak_hash::keccak; 3 | use serde::{Deserialize, Serialize}; 4 | use tracing::info; 5 | 6 | #[derive(Debug, thiserror::Error, Clone, Serialize, Deserialize)] 7 | pub enum MerkleError { 8 | #[error("Left element is None")] 9 | LeftElementIsNone(), 10 | #[error("Data vector is empty")] 11 | DataVectorIsEmpty(), 12 | } 13 | 14 | pub fn merkelize(data: Vec) -> Result { 15 | info!("Merkelizing {:?}", data); 16 | let mut data = data; 17 | let mut first = true; 18 | while data.len() > 1 || first { 19 | first = false; 20 | data = data 21 | .chunks(2) 22 | .flat_map(|chunk| -> Result { 23 | let left = chunk.first().ok_or(MerkleError::LeftElementIsNone())?; 24 | let right = *chunk.get(1).unwrap_or(left); 25 | Ok(keccak([left.as_bytes(), right.as_bytes()].concat()) 26 | .as_fixed_bytes() 27 | .into()) 28 | }) 29 | .collect(); 30 | } 31 | data.first() 32 | .copied() 33 | .ok_or(MerkleError::DataVectorIsEmpty()) 34 | } 35 | 36 | pub fn merkle_proof(data: Vec, base_element: H256) -> Result>, MerkleError> { 37 | if !data.contains(&base_element) { 38 | return Ok(None); 39 | } 40 | 41 | let mut proof = vec![]; 42 | let mut data = data; 43 | 44 | let mut target_hash = base_element; 45 | let mut first = true; 46 | while data.len() > 1 || first { 47 | first = false; 48 | let current_target = target_hash; 49 | data = data 50 | .chunks(2) 51 | .flat_map(|chunk| -> Result { 52 | let left = chunk 53 | .first() 54 | .copied() 55 | .ok_or(MerkleError::LeftElementIsNone())?; 56 | let right = chunk.get(1).copied().unwrap_or(left); 57 | let result = keccak([left.as_bytes(), right.as_bytes()].concat()) 58 | .as_fixed_bytes() 59 | .into(); 60 | if left == current_target { 61 | proof.push(right); 62 | target_hash = result; 63 | } else if right == current_target { 64 | proof.push(left); 65 | target_hash = result; 66 | } 67 | Ok(result) 68 | }) 69 | .collect(); 70 | } 71 | 72 | Ok(Some(proof)) 73 | } 74 | -------------------------------------------------------------------------------- /sdk/src/l2/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod constants; 2 | pub mod deposit; 3 | pub mod merkle_tree; 4 | pub mod withdraw; 5 | -------------------------------------------------------------------------------- /sdk/src/l2/withdraw.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | calldata::{Value, encode_calldata}, 3 | client::{EthClient, EthClientError, Overrides, eth::errors::GetTransactionReceiptError}, 4 | l2::{ 5 | constants::{COMMON_BRIDGE_L2_ADDRESS, L2_WITHDRAW_SIGNATURE}, 6 | merkle_tree::merkle_proof, 7 | }, 8 | }; 9 | use ethrex_common::{ 10 | Address, Bytes, H256, U256, 11 | types::{Transaction, TxKind}, 12 | }; 13 | use ethrex_rpc::types::block::BlockBodyWrapper; 14 | use itertools::Itertools; 15 | use secp256k1::SecretKey; 16 | 17 | pub async fn withdraw( 18 | amount: U256, 19 | from: Address, 20 | from_pk: SecretKey, 21 | proposer_client: &EthClient, 22 | nonce: Option, 23 | ) -> Result { 24 | let withdraw_transaction = proposer_client 25 | .build_eip1559_transaction( 26 | COMMON_BRIDGE_L2_ADDRESS, 27 | from, 28 | Bytes::from(encode_calldata(L2_WITHDRAW_SIGNATURE, &[Value::Address(from)]).unwrap()), 29 | Overrides { 30 | value: Some(amount), 31 | nonce, 32 | // CHECK: If we don't set max_fee_per_gas and max_priority_fee_per_gas 33 | // The transaction is not included on the L2. 34 | // Also we have some mismatches at the end of the L2 integration test. 35 | max_fee_per_gas: Some(800000000), 36 | max_priority_fee_per_gas: Some(800000000), 37 | gas_limit: Some(21000 * 2), 38 | ..Default::default() 39 | }, 40 | 10, 41 | ) 42 | .await?; 43 | 44 | proposer_client 45 | .send_eip1559_transaction(&withdraw_transaction, &from_pk) 46 | .await 47 | } 48 | 49 | pub async fn claim_withdraw( 50 | l2_withdrawal_tx_hash: H256, 51 | amount: U256, 52 | from: Address, 53 | from_pk: SecretKey, 54 | proposer_client: &EthClient, 55 | eth_client: &EthClient, 56 | bridge_address: Address, 57 | ) -> Result { 58 | println!("Claiming {amount} from bridge to {from:#x}"); 59 | 60 | const CLAIM_WITHDRAWAL_SIGNATURE: &str = 61 | "claimWithdrawal(bytes32,uint256,uint256,uint256,bytes32[])"; 62 | 63 | let (withdrawal_l2_block_number, claimed_amount) = match proposer_client 64 | .get_transaction_by_hash(l2_withdrawal_tx_hash) 65 | .await? 66 | { 67 | Some(l2_withdrawal_tx) => (l2_withdrawal_tx.block_number, l2_withdrawal_tx.value), 68 | None => { 69 | println!("Withdrawal transaction not found in L2"); 70 | return Err(EthClientError::GetTransactionReceiptError( 71 | GetTransactionReceiptError::RPCError( 72 | "Withdrawal transaction not found in L2".to_owned(), 73 | ), 74 | )); 75 | } 76 | }; 77 | 78 | let (index, proof) = get_withdraw_merkle_proof(proposer_client, l2_withdrawal_tx_hash).await?; 79 | 80 | let calldata_values = vec![ 81 | Value::Uint(U256::from_big_endian( 82 | l2_withdrawal_tx_hash.as_fixed_bytes(), 83 | )), 84 | Value::Uint(claimed_amount), 85 | Value::Uint(withdrawal_l2_block_number), 86 | Value::Uint(U256::from(index)), 87 | Value::Array( 88 | proof 89 | .iter() 90 | .map(|hash| Value::FixedBytes(hash.as_fixed_bytes().to_vec().into())) 91 | .collect(), 92 | ), 93 | ]; 94 | 95 | let claim_withdrawal_data = 96 | encode_calldata(CLAIM_WITHDRAWAL_SIGNATURE, &calldata_values).unwrap(); 97 | 98 | println!( 99 | "Claiming withdrawal with calldata: {}", 100 | hex::encode(&claim_withdrawal_data) 101 | ); 102 | 103 | let claim_tx = eth_client 104 | .build_eip1559_transaction( 105 | bridge_address, 106 | from, 107 | claim_withdrawal_data.into(), 108 | Overrides { 109 | from: Some(from), 110 | ..Default::default() 111 | }, 112 | 10, 113 | ) 114 | .await?; 115 | 116 | eth_client 117 | .send_eip1559_transaction(&claim_tx, &from_pk) 118 | .await 119 | } 120 | 121 | /// Returns the formated hash of the withdrawal transaction, 122 | /// or None if the transaction is not a withdrawal. 123 | /// The hash is computed as keccak256(to || value || tx_hash) 124 | pub fn get_withdrawal_hash(tx: &Transaction) -> Option { 125 | let to_bytes: [u8; 20] = match tx.data().get(16..36)?.try_into() { 126 | Ok(value) => value, 127 | Err(_) => return None, 128 | }; 129 | let to = Address::from(to_bytes); 130 | 131 | let value = tx.value().to_big_endian(); 132 | 133 | Some(keccak_hash::keccak( 134 | [to.as_bytes(), &value, tx.compute_hash().as_bytes()].concat(), 135 | )) 136 | } 137 | 138 | pub async fn get_withdraw_merkle_proof( 139 | client: &EthClient, 140 | tx_hash: H256, 141 | ) -> Result<(u64, Vec), EthClientError> { 142 | let tx_receipt = 143 | client 144 | .get_transaction_receipt(tx_hash) 145 | .await? 146 | .ok_or(EthClientError::Custom( 147 | "Failed to get transaction receipt".to_string(), 148 | ))?; 149 | 150 | let block = client 151 | .get_block_by_hash(tx_receipt.block_info.block_hash) 152 | .await?; 153 | 154 | let transactions = match block.body { 155 | BlockBodyWrapper::Full(body) => body.transactions, 156 | BlockBodyWrapper::OnlyHashes(_) => unreachable!(), 157 | }; 158 | let Some(Some((index, tx_withdrawal_hash))) = transactions 159 | .iter() 160 | .filter(|tx| match &tx.tx.to() { 161 | ethrex_common::types::TxKind::Call(to) => *to == COMMON_BRIDGE_L2_ADDRESS, 162 | ethrex_common::types::TxKind::Create => false, 163 | }) 164 | .find_position(|tx| tx.hash == tx_hash) 165 | .map(|(i, tx)| get_withdrawal_hash(&tx.tx).map(|withdrawal_hash| (i, (withdrawal_hash)))) 166 | else { 167 | return Err(EthClientError::Custom( 168 | "Failed to get widthdrawal hash, transaction is not a withdrawal".to_string(), 169 | )); 170 | }; 171 | 172 | let path = merkle_proof( 173 | transactions 174 | .iter() 175 | .filter_map(|tx| match tx.tx.to() { 176 | TxKind::Call(to) if to == COMMON_BRIDGE_L2_ADDRESS => get_withdrawal_hash(&tx.tx), 177 | _ => None, 178 | }) 179 | .collect(), 180 | tx_withdrawal_hash, 181 | ) 182 | .map_err(|err| EthClientError::Custom(format!("Failed to generate merkle proof: {err}")))? 183 | .ok_or(EthClientError::Custom( 184 | "Failed to generate merkle proof, element is not on the tree".to_string(), 185 | ))?; 186 | 187 | Ok(( 188 | index 189 | .try_into() 190 | .map_err(|err| EthClientError::Custom(format!("index does not fit in u64: {}", err)))?, 191 | path, 192 | )) 193 | } 194 | -------------------------------------------------------------------------------- /sdk/src/sdk.rs: -------------------------------------------------------------------------------- 1 | use crate::client::{EthClient, EthClientError, Overrides}; 2 | use ethrex_common::{Address, H256, U256}; 3 | use ethrex_rpc::types::receipt::RpcReceipt; 4 | use secp256k1::SecretKey; 5 | 6 | pub mod calldata; 7 | pub mod client; 8 | pub mod errors; 9 | pub mod utils; 10 | 11 | pub mod l2; 12 | 13 | pub async fn transfer( 14 | amount: U256, 15 | from: Address, 16 | to: Address, 17 | private_key: SecretKey, 18 | client: &EthClient, 19 | overrides: Overrides, 20 | ) -> Result { 21 | println!( 22 | "Transferring {amount} from {from:#x} to {to:#x}", 23 | amount = amount, 24 | from = from, 25 | to = to 26 | ); 27 | let tx = client 28 | .build_eip1559_transaction(to, from, Default::default(), overrides, 10) 29 | .await?; 30 | client.send_eip1559_transaction(&tx, &private_key).await 31 | } 32 | 33 | pub async fn wait_for_transaction_receipt( 34 | tx_hash: H256, 35 | client: &EthClient, 36 | max_retries: u64, 37 | silent: bool, 38 | ) -> Result { 39 | let mut receipt = client.get_transaction_receipt(tx_hash).await?; 40 | let mut r#try = 1; 41 | while receipt.is_none() { 42 | if !silent { 43 | println!("[{try}/{max_retries}] Retrying to get transaction receipt for {tx_hash:#x}"); 44 | } 45 | 46 | if max_retries == r#try { 47 | return Err(EthClientError::Custom(format!( 48 | "Transaction receipt for {tx_hash:#x} not found after {max_retries} retries" 49 | ))); 50 | } 51 | r#try += 1; 52 | 53 | tokio::time::sleep(std::time::Duration::from_secs(2)).await; 54 | 55 | receipt = client.get_transaction_receipt(tx_hash).await?; 56 | } 57 | receipt.ok_or(EthClientError::Custom( 58 | "Transaction receipt is None".to_owned(), 59 | )) 60 | } 61 | 62 | pub fn balance_in_eth(eth: bool, balance: U256) -> String { 63 | if eth { 64 | let mut balance = format!("{balance}"); 65 | let len = balance.len(); 66 | 67 | balance = match len { 68 | 18 => { 69 | let mut front = "0.".to_owned(); 70 | front.push_str(&balance); 71 | front 72 | } 73 | 0..=17 => { 74 | let mut front = "0.".to_owned(); 75 | let zeros = "0".repeat(18 - len); 76 | front.push_str(&zeros); 77 | front.push_str(&balance); 78 | front 79 | } 80 | 19.. => { 81 | balance.insert(len - 18, '.'); 82 | balance 83 | } 84 | }; 85 | balance 86 | } else { 87 | format!("{balance}") 88 | } 89 | } 90 | 91 | #[test] 92 | fn test_balance_in_ether() { 93 | // test more than 1 ether 94 | assert_eq!( 95 | "999999999.999003869993631450", 96 | balance_in_eth( 97 | true, 98 | U256::from_dec_str("999999999999003869993631450").unwrap() 99 | ) 100 | ); 101 | 102 | // test 0.5 103 | assert_eq!( 104 | "0.509003869993631450", 105 | balance_in_eth( 106 | true, 107 | U256::from_dec_str("000000000509003869993631450").unwrap() 108 | ) 109 | ); 110 | 111 | // test 0.005 112 | assert_eq!( 113 | "0.005090038699936314", 114 | balance_in_eth( 115 | true, 116 | U256::from_dec_str("000000000005090038699936314").unwrap() 117 | ) 118 | ); 119 | 120 | // test 0.0 121 | assert_eq!("0.000000000000000000", balance_in_eth(true, U256::zero())); 122 | } 123 | -------------------------------------------------------------------------------- /sdk/src/utils.rs: -------------------------------------------------------------------------------- 1 | use ethrex_common::H256; 2 | use secp256k1::SecretKey; 3 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 4 | 5 | pub fn secret_key_deserializer<'de, D>(deserializer: D) -> Result 6 | where 7 | D: Deserializer<'de>, 8 | { 9 | let hex = H256::deserialize(deserializer)?; 10 | SecretKey::from_slice(hex.as_bytes()).map_err(serde::de::Error::custom) 11 | } 12 | 13 | pub fn secret_key_serializer(secret_key: &SecretKey, serializer: S) -> Result 14 | where 15 | S: Serializer, 16 | { 17 | let hex = H256::from_slice(&secret_key.secret_bytes()); 18 | hex.serialize(serializer) 19 | } 20 | --------------------------------------------------------------------------------