├── sdk ├── README.md ├── src │ ├── client │ │ ├── mod.rs │ │ └── eth │ │ │ └── mod.rs │ ├── l2 │ │ ├── mod.rs │ │ ├── constants.rs │ │ ├── deposit.rs │ │ ├── privileged_transaction_data.rs │ │ └── withdraw.rs │ ├── errors.rs │ ├── utils.rs │ ├── sign.rs │ ├── keystore.rs │ ├── create.rs │ └── sdk.rs ├── .gitignore ├── examples │ ├── keystore │ │ ├── contracts │ │ │ └── RecoverSigner.sol │ │ └── main.rs │ └── simple_usage.rs └── Cargo.toml ├── Makefile ├── cli ├── src │ ├── commands │ │ ├── mod.rs │ │ ├── autocomplete.rs │ │ └── l2.rs │ ├── lib.rs │ ├── main.rs │ ├── utils.rs │ ├── common.rs │ └── cli.rs ├── Cargo.toml ├── README.md └── tests │ └── tests.rs ├── rust-toolchain.toml ├── .github ├── CODEOWNERS ├── pull_request_template.md ├── actions │ ├── free-disk │ │ └── action.yml │ ├── setup-rust │ │ └── action.yml │ └── build-docker │ │ └── action.yml ├── scripts │ └── publish_report.sh ├── workflows │ ├── pr_lint_gha.yml │ ├── pr_main_cli.yml │ ├── pr_main_sdk.yml │ ├── main_docker_publish.yml │ ├── pr_lint_pr_title.yml │ ├── pr_main.yml │ ├── daily_reports.yml │ └── tag_release.yaml └── SECURITY.md ├── assets ├── chain_demo.gif └── operations_demo.gif ├── .gitignore ├── LICENSE ├── Dockerfile ├── Cargo.toml └── README.md /sdk/README.md: -------------------------------------------------------------------------------- 1 | # SDK 2 | 3 | TODO 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: cli 2 | 3 | cli: 4 | cargo install --path cli --locked 5 | -------------------------------------------------------------------------------- /sdk/src/client/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod eth; 2 | pub use ethrex_rpc::clients::auth; 3 | -------------------------------------------------------------------------------- /cli/src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod autocomplete; 2 | pub(crate) mod l2; 3 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.90.0" 3 | profile = "default" 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lambdaclass core team 2 | * @lambdaclass/lambda-execution-reviewers 3 | -------------------------------------------------------------------------------- /assets/chain_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdaclass/rex/HEAD/assets/chain_demo.gif -------------------------------------------------------------------------------- /sdk/.gitignore: -------------------------------------------------------------------------------- 1 | examples/keystore/contracts/lib 2 | examples/keystore/contracts/solc_out 3 | -------------------------------------------------------------------------------- /assets/operations_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdaclass/rex/HEAD/assets/operations_demo.gif -------------------------------------------------------------------------------- /sdk/src/l2/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod constants; 2 | pub mod deposit; 3 | pub mod privileged_transaction_data; 4 | pub mod withdraw; 5 | -------------------------------------------------------------------------------- /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 | let _ = cli::start().await.inspect_err(|err| { 6 | eprintln!("\x1b[31;1mError:\x1b[0m execution failed: {err:?}"); 7 | std::process::exit(1); 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Motivation** 2 | 3 | 4 | 5 | **Description** 6 | 7 | 8 | 9 | 10 | 11 | Closes #issue_number 12 | 13 | -------------------------------------------------------------------------------- /sdk/src/errors.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, thiserror::Error)] 2 | pub enum KeystoreError { 3 | #[error("Error creating default dir: {0}")] 4 | ErrorCreatingDefaultDir(String), 5 | #[error("Error creating keystore: {0}")] 6 | ErrorCreatingKeystore(String), 7 | #[error("Error creating secret key: {0}")] 8 | ErrorCreatingSecretKey(String), 9 | #[error("Error opening keystore: {0}")] 10 | ErrorOpeningKeystore(String), 11 | } 12 | -------------------------------------------------------------------------------- /.github/actions/free-disk/action.yml: -------------------------------------------------------------------------------- 1 | name: "Free disk space" 2 | description: "Remove unneeded dependencies to free disk space" 3 | 4 | runs: 5 | using: "composite" 6 | steps: 7 | - name: Remove .NET 8 | shell: bash 9 | run: sudo rm -rf /usr/share/dotnet/ 10 | 11 | - name: Remove Android SDK 12 | shell: bash 13 | run: sudo rm -rf /usr/local/lib/android 14 | 15 | - name: Remove Haskell 16 | shell: bash 17 | run: sudo rm -rf /opt/ghc /usr/local/.ghcup 18 | -------------------------------------------------------------------------------- /.github/scripts/publish_report.sh: -------------------------------------------------------------------------------- 1 | curl -X POST $1 \ 2 | -H 'Content-Type: application/json; charset=utf-8' \ 3 | --data @- < Result { 12 | let mut calldata = Vec::from(BALANCE_OF_SELECTOR); 13 | calldata.resize(16, 0); 14 | calldata.extend(address.to_fixed_bytes()); 15 | U256::from_str_radix( 16 | ð_client 17 | .call( 18 | token_address, 19 | calldata.into(), 20 | ethrex_rpc::clients::Overrides::default(), 21 | ) 22 | .await?, 23 | 16, 24 | ) 25 | .map_err(|_| { 26 | EthClientError::Custom(format!("Address {token_address} did not return a uint256")) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /.github/actions/setup-rust/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Setup Rust Environment' 2 | description: 'Sets up Rust and caches dependencies' 3 | inputs: 4 | components: 5 | description: "Rust components to install (e.g., rustfmt, clippy)" 6 | required: false 7 | type: string 8 | runs: 9 | using: "composite" 10 | steps: 11 | - name: Extract Rust version from rust-toolchain.toml 12 | id: rustver 13 | shell: bash 14 | run: | 15 | rust_version=$(grep -E '^[[:space:]]*channel[[:space:]]*=' rust-toolchain.toml \ 16 | | sed -E 's/.*"([^"]+)".*/\1/') 17 | echo "rust_version=${rust_version}" >>"$GITHUB_OUTPUT" 18 | echo "Rust version: ${rust_version}" 19 | 20 | - name: Install Rust 21 | uses: dtolnay/rust-toolchain@master 22 | with: 23 | toolchain: ${{ steps.rustver.outputs.rust_version }} 24 | components: ${{ inputs.components }} 25 | 26 | - name: Add Rust Cache 27 | uses: Swatinem/rust-cache@v2 -------------------------------------------------------------------------------- /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-common.workspace = true 10 | ethrex-blockchain.workspace = true 11 | ethrex-rlp.workspace = true 12 | ethrex-rpc.workspace = true 13 | ethrex-l2-rpc.workspace = true 14 | ethrex-l2-common.workspace = true 15 | ethrex-sdk.workspace = true 16 | 17 | # Runtime 18 | tokio = "1.43.0" 19 | 20 | # CLI 21 | clap = { version = "4.3", features = ["derive", "env"] } 22 | clap_complete = "4.5.17" 23 | eyre = "0.6" 24 | dialoguer = "0.11" 25 | colored = "3.0.0" 26 | spinoff = "0.8.0" 27 | strum = "0.27.1" 28 | 29 | # Crypto 30 | keccak-hash.workspace = true 31 | secp256k1.workspace = true 32 | 33 | # Utils 34 | hex.workspace = true 35 | itertools = "0.14.0" 36 | toml = "0.8.19" 37 | dirs = "6.0.0" 38 | rand = "0.9.1" 39 | rayon.workspace = true 40 | url = "2.5.7" 41 | 42 | # Serde 43 | serde = "1.0.218" 44 | serde_json = "1.0.139" 45 | -------------------------------------------------------------------------------- /sdk/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rex-sdk" 3 | version.workspace = true 4 | edition.workspace = true 5 | 6 | [dependencies] 7 | ethrex-common.workspace = true 8 | ethrex-blockchain.workspace = true 9 | ethrex-rlp.workspace = true 10 | ethrex-rpc.workspace = true 11 | ethrex-l2-rpc.workspace = true 12 | ethrex-sdk.workspace = true 13 | ethrex-l2-common.workspace = true 14 | 15 | # Runtime 16 | tokio = "1.43.0" 17 | 18 | # Clients 19 | reqwest = { version = "0.12.7", features = ["json"] } 20 | jsonwebtoken = "9.3.0" 21 | 22 | # Crypto 23 | keccak-hash.workspace = true 24 | secp256k1.workspace = true 25 | eth-keystore = "0.5" 26 | rand = "0.8.5" 27 | 28 | # Utils 29 | hex.workspace = true 30 | itertools = "0.14.0" 31 | toml = "0.8.19" 32 | dirs = "6.0.0" 33 | envy = "0.4.2" 34 | thiserror.workspace = true 35 | rayon.workspace = true 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.90 AS chef 2 | 3 | RUN apt-get update && apt-get install -y \ 4 | build-essential \ 5 | libclang-dev \ 6 | libc6 \ 7 | libssl-dev \ 8 | ca-certificates \ 9 | && rm -rf /var/lib/apt/lists/* 10 | RUN cargo install cargo-chef 11 | 12 | WORKDIR /rex 13 | 14 | # --- Planner Stage --- 15 | # Copy all source code to calculate the dependency recipe. 16 | # This layer is fast and will be invalidated on any source change. 17 | FROM chef AS planner 18 | COPY cli ./cli 19 | COPY sdk ./sdk 20 | COPY Cargo.* ./ 21 | RUN cargo chef prepare --recipe-path recipe.json 22 | 23 | # --- Builder Stage --- 24 | # Build the dependencies. This is the most time-consuming step. 25 | # This layer will be cached and only re-run if the recipe.json from the 26 | # previous stage has changed, which only happens when dependencies change. 27 | FROM chef AS builder 28 | COPY --from=planner /rex/recipe.json recipe.json 29 | RUN cargo chef cook --release --recipe-path recipe.json 30 | 31 | # --- Application Build Stage --- 32 | # Copy the full, up-to-date source code and build the application. 33 | # This uses the cached dependencies from the builder stage. 34 | 35 | # Optional build flags 36 | ARG BUILD_FLAGS="" 37 | COPY cli ./cli 38 | COPY sdk ./sdk 39 | COPY Cargo.* ./ 40 | RUN cargo build --release --bin rex $BUILD_FLAGS 41 | 42 | # --- Final Image --- 43 | FROM ubuntu:24.04 44 | WORKDIR /usr/local/bin 45 | 46 | COPY --from=builder /rex/target/release/rex . 47 | ENTRYPOINT [ "./rex" ] 48 | -------------------------------------------------------------------------------- /.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 --lib 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 --lib 58 | -------------------------------------------------------------------------------- /.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! -------------------------------------------------------------------------------- /sdk/src/utils.rs: -------------------------------------------------------------------------------- 1 | use ethrex_common::H256; 2 | use keccak_hash::keccak; 3 | use secp256k1::SecretKey; 4 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 5 | 6 | pub fn secret_key_deserializer<'de, D>(deserializer: D) -> Result 7 | where 8 | D: Deserializer<'de>, 9 | { 10 | let hex = H256::deserialize(deserializer)?; 11 | SecretKey::from_slice(hex.as_bytes()).map_err(serde::de::Error::custom) 12 | } 13 | 14 | pub fn secret_key_serializer(secret_key: &SecretKey, serializer: S) -> Result 15 | where 16 | S: Serializer, 17 | { 18 | let hex = H256::from_slice(&secret_key.secret_bytes()); 19 | hex.serialize(serializer) 20 | } 21 | 22 | /// EIP-55 Checksum Address. 23 | /// This is how addresses are actually displayed on ethereum apps 24 | /// Returns address as string without "0x" prefix 25 | pub fn to_checksum_address(address: &str) -> String { 26 | // Trim if necessary 27 | let addr = address.trim_start_matches("0x").to_lowercase(); 28 | 29 | // Hash the raw address using Keccak-256 30 | let hash = keccak(&addr); 31 | 32 | // Convert hash to hex string 33 | let hash_hex = hex::encode(hash); 34 | 35 | // Apply checksum by walking each nibble 36 | let mut checksummed = String::with_capacity(40); 37 | 38 | for (i, c) in addr.chars().enumerate() { 39 | let hash_char = hash_hex.chars().nth(i).unwrap(); 40 | let hash_value = hash_char.to_digit(16).unwrap(); 41 | 42 | if c.is_ascii_alphabetic() && hash_value >= 8 { 43 | checksummed.push(c.to_ascii_uppercase()); 44 | } else { 45 | checksummed.push(c); 46 | } 47 | } 48 | 49 | checksummed 50 | } 51 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["cli", "sdk"] 3 | default-members = ["cli"] 4 | resolver = "3" 5 | 6 | [workspace.package] 7 | version = "7.0.0" 8 | edition = "2024" 9 | 10 | [workspace.lints.rust] 11 | unsafe_code = "forbid" 12 | warnings = "warn" 13 | 14 | [workspace.lints.clippy] 15 | panic = "deny" 16 | unnecessary_cast = "warn" 17 | deref_by_slicing = "warn" 18 | indexing_slicing = "warn" 19 | manual_unwrap_or = "warn" 20 | manual_unwrap_or_default = "warn" 21 | as_conversions = "deny" 22 | unwrap_used = "deny" 23 | expect_used = "deny" 24 | arithmetic_side_effects = "deny" 25 | overflow_check_conditional = "warn" 26 | manual_saturating_arithmetic = "warn" 27 | 28 | [workspace.dependencies] 29 | rex-cli = { path = "cli" } 30 | rex-sdk = { path = "sdk" } 31 | 32 | ethrex-common = { git = "https://github.com/lambdaclass/ethrex", package = "ethrex-common", tag = "v7.0.0" } 33 | ethrex-blockchain = { git = "https://github.com/lambdaclass/ethrex", package = "ethrex-blockchain", tag = "v7.0.0" } 34 | ethrex-rlp = { git = "https://github.com/lambdaclass/ethrex", package = "ethrex-rlp", tag = "v7.0.0" } 35 | ethrex-rpc = { git = "https://github.com/lambdaclass/ethrex", package = "ethrex-rpc", tag = "v7.0.0" } 36 | ethrex-l2-rpc = { git = "https://github.com/lambdaclass/ethrex", package = "ethrex-l2-rpc", tag = "v7.0.0" } 37 | ethrex-sdk = { git = "https://github.com/lambdaclass/ethrex", package = "ethrex-sdk", tag = "v7.0.0" } 38 | ethrex-l2-common = { git = "https://github.com/lambdaclass/ethrex", package = "ethrex-l2-common", tag = "v7.0.0" } 39 | 40 | keccak-hash = "0.11.0" 41 | thiserror = "2.0.11" 42 | hex = "0.4.3" 43 | secp256k1 = { version = "0.30.0", default-features = false, features = [ 44 | "global-context", 45 | "recovery", 46 | "rand", 47 | ] } 48 | rayon = "1.10.0" 49 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /sdk/src/sign.rs: -------------------------------------------------------------------------------- 1 | use ethrex_common::{Address, Bytes, H256}; 2 | use keccak_hash::keccak; 3 | use secp256k1::Error; 4 | use secp256k1::SecretKey; 5 | 6 | /// This function receives a hash and a SecretKey and signs it using secp256k1. 7 | pub fn sign_hash(hash: H256, private_key: SecretKey) -> Vec { 8 | let signed_msg = secp256k1::SECP256K1.sign_ecdsa_recoverable( 9 | &secp256k1::Message::from_digest(*hash.as_fixed_bytes()), 10 | &private_key, 11 | ); 12 | let (msg_signature_recovery_id, msg_signature) = signed_msg.serialize_compact(); 13 | 14 | let msg_signature_recovery_id = i32::from(msg_signature_recovery_id) + 27; 15 | 16 | [&msg_signature[..], &[msg_signature_recovery_id as u8]].concat() 17 | } 18 | 19 | /// This function takes signatures that are computed as a 0x45 signature, as described in EIP-191 (https://eips.ethereum.org/EIPS/eip-191), 20 | /// then it has an extra byte concatenated at the end, which is a scalar value added to the signatures parity, 21 | /// 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). 22 | pub fn get_address_from_message_and_signature( 23 | message: Bytes, 24 | signature: Bytes, 25 | ) -> Result { 26 | let raw_recovery_id = if signature[64] >= 27 { 27 | signature[64] - 27 28 | } else { 29 | signature[64] 30 | }; 31 | 32 | let recovery_id = secp256k1::ecdsa::RecoveryId::try_from(raw_recovery_id as i32)?; 33 | 34 | let signature = 35 | secp256k1::ecdsa::RecoverableSignature::from_compact(&signature[..64], recovery_id)?; 36 | 37 | let payload = [ 38 | b"\x19Ethereum Signed Message:\n", 39 | message.len().to_string().as_bytes(), 40 | message.as_ref(), 41 | ] 42 | .concat(); 43 | 44 | let signer_public_key = signature.recover(&secp256k1::Message::from_digest( 45 | *keccak(payload).as_fixed_bytes(), 46 | ))?; 47 | 48 | Ok(Address::from_slice( 49 | &keccak(&signer_public_key.serialize_uncompressed()[1..])[12..], 50 | )) 51 | } 52 | -------------------------------------------------------------------------------- /.github/actions/build-docker/action.yml: -------------------------------------------------------------------------------- 1 | name: Build Docker 2 | description: Builds the rex docker image 3 | 4 | inputs: 5 | username: 6 | description: "Username for docker registry login" 7 | required: false 8 | password: 9 | description: "Password or token for docker registry login" 10 | required: false 11 | registry: 12 | description: "Docker registry to push the image to (if pushing)" 13 | required: false 14 | tags: 15 | description: "Comma-separated list of tags to apply to the built image" 16 | required: false 17 | default: "rex:ci" 18 | push: 19 | description: "Whether to push the built image to the registry" 20 | required: false 21 | default: "false" 22 | artifact_path: 23 | description: "The name of the artifact that is going to be pushed" 24 | required: false 25 | default: "rex_image.tar" 26 | build_args: 27 | description: "The arguments that are sent to the dockerfile to built. Format ARG=value" 28 | required: false 29 | default: "" 30 | 31 | outputs: 32 | artifact_path: 33 | description: "The path of the image tar inside the action runner" 34 | value: ${{ steps.vars.outputs.artifact_path }} 35 | 36 | runs: 37 | using: "composite" 38 | steps: 39 | - id: vars 40 | shell: bash 41 | run: | 42 | echo "artifact_path=/tmp/${{ inputs.artifact_path }}" >> $GITHUB_OUTPUT 43 | 44 | - name: Login to Docker registry 45 | if: inputs.username != '' && inputs.password != '' 46 | uses: docker/login-action@v3 47 | with: 48 | registry: ${{ inputs.registry }} 49 | username: ${{ inputs.username }} 50 | password: ${{ inputs.password }} 51 | 52 | - name: Set up Docker Buildx 53 | uses: docker/setup-buildx-action@v3 54 | 55 | - name: Build Docker image 56 | uses: docker/build-push-action@v6 57 | with: 58 | context: . 59 | file: ./Dockerfile 60 | push: ${{ inputs.push }} 61 | tags: ${{ inputs.tags }} 62 | outputs: ${{ inputs.push == 'false' && format('type=docker,dest={0}', steps.vars.outputs.artifact_path) || '' }} 63 | cache-from: type=gha 64 | cache-to: type=gha,mode=max 65 | build-args: ${{ inputs.build_args }} 66 | 67 | # Since we're exporting the image as a tar, we need to load it manually as well 68 | - name: Load image locally 69 | shell: bash 70 | if: inputs.push == 'false' 71 | run: | 72 | docker load -i ${{ steps.vars.outputs.artifact_path }} 73 | -------------------------------------------------------------------------------- /sdk/examples/simple_usage.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use ethrex_common::{Address, Bytes, U256}; 3 | use ethrex_l2_common::utils::get_address_from_secret_key; 4 | use ethrex_rpc::{ 5 | EthClient, 6 | clients::Overrides, 7 | types::block_identifier::{BlockIdentifier, BlockTag}, 8 | }; 9 | use hex::FromHexError; 10 | use reqwest::Url; 11 | use rex_sdk::{transfer, wait_for_transaction_receipt}; 12 | use secp256k1::SecretKey; 13 | use std::str::FromStr; 14 | 15 | #[derive(Parser)] 16 | struct SimpleUsageArgs { 17 | #[arg(long, value_parser = parse_private_key, env = "PRIVATE_KEY", help = "The private key to derive the address from.")] 18 | private_key: SecretKey, 19 | #[arg(long, default_value = "http://localhost:8545", env = "RPC_URL")] 20 | rpc_url: Url, 21 | } 22 | 23 | fn parse_private_key(s: &str) -> eyre::Result { 24 | Ok(SecretKey::from_slice(&parse_hex(s)?)?) 25 | } 26 | 27 | fn parse_hex(s: &str) -> eyre::Result { 28 | match s.strip_prefix("0x") { 29 | Some(s) => hex::decode(s).map(Into::into), 30 | None => hex::decode(s).map(Into::into), 31 | } 32 | } 33 | 34 | #[tokio::main] 35 | async fn main() { 36 | let args = SimpleUsageArgs::parse(); 37 | 38 | let account = get_address_from_secret_key(&args.private_key).unwrap(); 39 | 40 | let eth_client = EthClient::new(args.rpc_url).unwrap(); 41 | 42 | let account_balance = eth_client 43 | .get_balance(account, BlockIdentifier::Tag(BlockTag::Latest)) 44 | .await 45 | .unwrap(); 46 | 47 | let account_nonce = eth_client 48 | .get_nonce(account, BlockIdentifier::Tag(BlockTag::Latest)) 49 | .await 50 | .unwrap(); 51 | 52 | let chain_id = eth_client.get_chain_id().await.unwrap(); 53 | 54 | println!("Account balance: {account_balance}"); 55 | println!("Account nonce: {account_nonce}"); 56 | println!("Chain id: {chain_id}"); 57 | 58 | let amount = U256::from_dec_str("1000000000000000000").unwrap(); // 1 ETH in wei 59 | let from = account; 60 | let to = Address::from_str("0x4852f44fd706e34cb906b399b729798665f64a83").unwrap(); 61 | 62 | let tx_hash = transfer( 63 | amount, 64 | from, 65 | to, 66 | &args.private_key, 67 | ð_client, 68 | Overrides::default(), 69 | ) 70 | .await 71 | .unwrap(); 72 | 73 | // Wait for the transaction to be finalized 74 | wait_for_transaction_receipt(tx_hash, ð_client, 100, false) 75 | .await 76 | .unwrap(); 77 | 78 | let tx_receipt = eth_client.get_transaction_receipt(tx_hash).await.unwrap(); 79 | 80 | println!("transfer tx receipt: {tx_receipt:?}"); 81 | 82 | let tx_details = eth_client.get_transaction_by_hash(tx_hash).await.unwrap(); 83 | 84 | println!("transfer tx details: {tx_details:?}"); 85 | } 86 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /sdk/src/keystore.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::KeystoreError; 2 | use dirs::data_dir; 3 | use eth_keystore::{decrypt_key, new as eth_keystore_new}; 4 | use rand::rngs::OsRng; 5 | use secp256k1::SecretKey; 6 | use std::fs; 7 | use std::path::Path; 8 | 9 | fn get_keystore_default_path() -> String { 10 | data_dir() 11 | .expect("Failed to get base directories") 12 | .join("rex/keystores/") 13 | .to_str() 14 | .expect("Failed to convert path to string") 15 | .to_owned() 16 | } 17 | 18 | /// Creates a new keystore in the given path and name using the password. 19 | /// If no path is provided, uses keystore_default_path. 20 | /// If no name is provided, generates a random one. 21 | /// Returns the SecretKey and the UUID of the keystore file. 22 | pub fn create_new_keystore( 23 | path: Option<&str>, 24 | name: Option<&str>, 25 | password: S, 26 | ) -> Result<(SecretKey, String), KeystoreError> 27 | where 28 | S: AsRef<[u8]>, 29 | { 30 | let keystore_default_path = get_keystore_default_path(); 31 | let path = path.map_or(Path::new(keystore_default_path.as_str()), Path::new); 32 | 33 | if !path.exists() { 34 | fs::create_dir_all(path.as_os_str()) 35 | .map_err(|e| KeystoreError::ErrorCreatingDefaultDir(e.to_string()))?; 36 | } 37 | 38 | let mut rng = OsRng; 39 | let (key_vec, uuid) = eth_keystore_new(path, &mut rng, password, name) 40 | .map_err(|e| KeystoreError::ErrorCreatingKeystore(e.to_string()))?; 41 | 42 | let secret_key = SecretKey::from_slice(&key_vec) 43 | .map_err(|e| KeystoreError::ErrorCreatingSecretKey(e.to_string()))?; 44 | Ok((secret_key, uuid)) 45 | } 46 | 47 | /// Loads the SecretKey from a given Keystore. 48 | /// If path is not provided, uses KEYSTORE_DEFAULT_PATH. 49 | /// Returns the SecretKey loaded. 50 | pub fn load_keystore_from_path( 51 | path: Option<&str>, 52 | name: &str, 53 | password: S, 54 | ) -> Result 55 | where 56 | S: AsRef<[u8]>, 57 | { 58 | let keystore_default_path = get_keystore_default_path(); 59 | let path = path 60 | .map_or(Path::new(keystore_default_path.as_str()), Path::new) 61 | .join(name); 62 | 63 | let key_vec = decrypt_key(path, password) 64 | .map_err(|e| KeystoreError::ErrorOpeningKeystore(e.to_string()))?; 65 | let secret_key = SecretKey::from_slice(&key_vec) 66 | .map_err(|e| KeystoreError::ErrorCreatingSecretKey(e.to_string()))?; 67 | Ok(secret_key) 68 | } 69 | 70 | #[cfg(test)] 71 | mod tests { 72 | use super::*; 73 | 74 | #[test] 75 | pub fn test_create_and_load_keystore() { 76 | assert_eq!( 77 | create_new_keystore(None, Some("RexTest"), "LambdaClass") 78 | .unwrap() 79 | .0, 80 | load_keystore_from_path(None, "RexTest", "LambdaClass").unwrap() 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /cli/src/utils.rs: -------------------------------------------------------------------------------- 1 | use ethrex_common::{Address, Bytes, U256}; 2 | use ethrex_l2_common::calldata::Value; 3 | use ethrex_sdk::calldata::{encode_calldata, encode_tuple, parse_signature}; 4 | use hex::FromHexError; 5 | use secp256k1::SecretKey; 6 | use std::str::FromStr; 7 | 8 | pub fn parse_private_key(s: &str) -> eyre::Result { 9 | Ok(SecretKey::from_slice(&parse_hex(s)?)?) 10 | } 11 | 12 | pub fn parse_u256(s: &str) -> eyre::Result { 13 | let parsed = if s.starts_with("0x") { 14 | U256::from_str(s)? 15 | } else { 16 | U256::from_dec_str(s)? 17 | }; 18 | Ok(parsed) 19 | } 20 | 21 | pub fn parse_hex(s: &str) -> eyre::Result { 22 | match s.strip_prefix("0x") { 23 | Some(s) => hex::decode(s).map(Into::into), 24 | None => hex::decode(s).map(Into::into), 25 | } 26 | } 27 | 28 | /// Parses a hex string, stripping the "0x" prefix if present. 29 | /// Unlike `parse_hex`, the string doesn't need to be of even length. 30 | pub fn parse_hex_string(s: &str) -> eyre::Result { 31 | let s = s.strip_prefix("0x").unwrap_or(s); 32 | if s.chars().all(|c| c.is_ascii_hexdigit()) { 33 | Ok(s.to_string()) 34 | } else { 35 | Err(eyre::eyre!("Invalid hex string")) 36 | } 37 | } 38 | 39 | fn parse_call_args(args: Vec) -> eyre::Result)>> { 40 | let mut args_iter = args.iter(); 41 | let Some(signature) = args_iter.next() else { 42 | return Ok(None); 43 | }; 44 | let (_, params) = parse_signature(signature)?; 45 | let mut values = Vec::new(); 46 | for param in params { 47 | let val = args_iter 48 | .next() 49 | .ok_or(eyre::Error::msg("missing parameter for given signature"))?; 50 | values.push(match param.as_str() { 51 | "address" => Value::Address(Address::from_str(val)?), 52 | _ if param.starts_with("uint") => Value::Uint(U256::from_dec_str(val)?), 53 | _ if param.starts_with("int") => { 54 | if let Some(val) = val.strip_prefix("-") { 55 | let x = U256::from_str(val)?; 56 | if x.is_zero() { 57 | Value::Uint(x) 58 | } else { 59 | Value::Uint(U256::max_value() - x + 1) 60 | } 61 | } else { 62 | Value::Uint(U256::from_dec_str(val)?) 63 | } 64 | } 65 | "bool" => match val.as_str() { 66 | "true" => Value::Uint(U256::from(1)), 67 | "false" => Value::Uint(U256::from(0)), 68 | _ => Err(eyre::Error::msg("Invalid boolean"))?, 69 | }, 70 | "bytes" => Value::Bytes(hex::decode(val)?.into()), 71 | _ if param.starts_with("bytes") => Value::FixedBytes(hex::decode(val)?.into()), 72 | _ => todo!("type unsupported"), 73 | }); 74 | } 75 | Ok(Some((signature.to_string(), values))) 76 | } 77 | 78 | pub fn parse_func_call(args: Vec) -> eyre::Result { 79 | let Some((signature, values)) = parse_call_args(args)? else { 80 | return Ok(Bytes::new()); 81 | }; 82 | Ok(encode_calldata(&signature, &values)?.into()) 83 | } 84 | 85 | pub fn parse_contract_creation(args: Vec) -> eyre::Result { 86 | let Some((_signature, values)) = parse_call_args(args)? else { 87 | return Ok(Bytes::new()); 88 | }; 89 | Ok(encode_tuple(&values)?.into()) 90 | } 91 | -------------------------------------------------------------------------------- /sdk/src/l2/deposit.rs: -------------------------------------------------------------------------------- 1 | use crate::transfer; 2 | use ethrex_common::{Address, U256, types::TxType}; 3 | use ethrex_l2_common::{calldata::Value, utils::get_address_from_secret_key}; 4 | use ethrex_l2_rpc::signer::{LocalSigner, Signer}; 5 | use ethrex_rpc::{ 6 | EthClient, 7 | clients::{EthClientError, Overrides}, 8 | }; 9 | use ethrex_sdk::{build_generic_tx, calldata::encode_calldata, send_generic_transaction}; 10 | use keccak_hash::H256; 11 | use secp256k1::SecretKey; 12 | 13 | const DEPOSIT_SIGNATURE: &str = "deposit(address)"; 14 | const DEPOSIT_ERC20_SIGNATURE: &str = "depositERC20(address,address,address,uint256)"; 15 | 16 | pub async fn deposit_through_transfer( 17 | amount: U256, 18 | from: Address, 19 | from_pk: &SecretKey, 20 | bridge_address: Address, 21 | eth_client: &EthClient, 22 | ) -> Result { 23 | transfer( 24 | amount, 25 | from, 26 | bridge_address, 27 | from_pk, 28 | eth_client, 29 | Overrides::default(), 30 | ) 31 | .await 32 | } 33 | 34 | pub async fn deposit_through_contract_call( 35 | amount: U256, 36 | to: Address, 37 | depositor_private_key: &SecretKey, 38 | bridge_address: Address, 39 | eth_client: &EthClient, 40 | ) -> Result { 41 | let l1_from = 42 | get_address_from_secret_key(depositor_private_key).map_err(EthClientError::Custom)?; 43 | let calldata = encode_calldata(DEPOSIT_SIGNATURE, &[Value::Address(to)])?; 44 | let gas_price = eth_client 45 | .get_gas_price_with_extra(20) 46 | .await? 47 | .try_into() 48 | .map_err(|_| { 49 | EthClientError::InternalError("Failed to convert gas_price to a u64".to_owned()) 50 | })?; 51 | 52 | let deposit_tx = build_generic_tx( 53 | eth_client, 54 | TxType::EIP1559, 55 | bridge_address, 56 | l1_from, 57 | calldata.into(), 58 | Overrides { 59 | from: Some(l1_from), 60 | value: Some(amount), 61 | max_fee_per_gas: Some(gas_price), 62 | max_priority_fee_per_gas: Some(gas_price), 63 | ..Default::default() 64 | }, 65 | ) 66 | .await?; 67 | 68 | let signer = Signer::Local(LocalSigner::new(*depositor_private_key)); 69 | 70 | send_generic_transaction(eth_client, deposit_tx, &signer).await 71 | } 72 | 73 | pub async fn deposit_erc20( 74 | token_l1: Address, 75 | token_l2: Address, 76 | amount: U256, 77 | from: Address, 78 | from_pk: SecretKey, 79 | eth_client: &EthClient, 80 | bridge_address: Address, 81 | ) -> Result { 82 | println!( 83 | "Depositing {amount} from {from:#x} to token L2: {token_l2:#x} via L1 token: {token_l1:#x}" 84 | ); 85 | 86 | let calldata_values = vec![ 87 | Value::Address(token_l1), 88 | Value::Address(token_l2), 89 | Value::Address(from), 90 | Value::Uint(amount), 91 | ]; 92 | 93 | let deposit_data = encode_calldata(DEPOSIT_ERC20_SIGNATURE, &calldata_values)?; 94 | 95 | let deposit_tx = build_generic_tx( 96 | eth_client, 97 | TxType::EIP1559, 98 | bridge_address, 99 | from, 100 | deposit_data.into(), 101 | Overrides { 102 | from: Some(from), 103 | ..Default::default() 104 | }, 105 | ) 106 | .await?; 107 | 108 | let signer = Signer::Local(LocalSigner::new(from_pk)); 109 | 110 | send_generic_transaction(eth_client, deposit_tx, &signer).await 111 | } 112 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /sdk/src/create.rs: -------------------------------------------------------------------------------- 1 | use ethrex_common::Address; 2 | use ethrex_rlp::encode::RLPEncode; 3 | use keccak_hash::{H256, keccak}; 4 | use rand::RngCore; 5 | use rayon::prelude::*; 6 | use std::iter; 7 | 8 | use crate::utils::to_checksum_address; 9 | 10 | pub const DETERMINISTIC_DEPLOYER: &str = "0x4e59b44847b379578588920cA78FbF26c0B4956C"; 11 | 12 | /// address = keccak256(rlp([sender_address,sender_nonce]))[12:] 13 | pub fn compute_create_address(sender_address: Address, sender_nonce: u64) -> Address { 14 | let mut encoded = Vec::new(); 15 | (sender_address, sender_nonce).encode(&mut encoded); 16 | let keccak_bytes = keccak(encoded).0; 17 | Address::from_slice(&keccak_bytes[12..]) 18 | } 19 | 20 | /// address = keccak256(0xff || deployer_address || salt || keccak256(initialization_code))[12:] 21 | pub fn compute_create2_address( 22 | deployer_address: Address, 23 | init_code_hash: H256, 24 | salt: H256, 25 | ) -> Address { 26 | Address::from_slice( 27 | &keccak( 28 | [ 29 | &[0xff], 30 | deployer_address.as_bytes(), 31 | &salt.0, 32 | init_code_hash.as_bytes(), 33 | ] 34 | .concat(), 35 | ) 36 | .as_bytes()[12..], 37 | ) 38 | } 39 | 40 | /// Brute-force Create2 address generation 41 | /// This function generates random salts until it finds one that matches the specified criteria. 42 | /// `begins`, `ends`, and `contains` are optional filters for the generated address. 43 | /// If they are not provided, the function will not filter based on that criterion. 44 | /// Returns the salt and the generated address. 45 | pub fn brute_force_create2( 46 | deployer: Address, 47 | init_code_hash: H256, 48 | begins: Option, 49 | ends: Option, 50 | contains: Option, 51 | case_sensitive: bool, 52 | ) -> (H256, Address) { 53 | // If not case sensitive make the comparison with lowercase 54 | let begins = begins.map(|s| if case_sensitive { s } else { s.to_lowercase() }); 55 | let ends = ends.map(|s| if case_sensitive { s } else { s.to_lowercase() }); 56 | let contains = contains.map(|s| if case_sensitive { s } else { s.to_lowercase() }); 57 | 58 | let mut salt_bytes = [0u8; 32]; 59 | 60 | iter::repeat_with(|| { 61 | rand::thread_rng().fill_bytes(&mut salt_bytes); 62 | H256::from(salt_bytes) 63 | }) 64 | .par_bridge() // Convert into a parallel iterator 65 | .find_any(|salt| { 66 | // Find a salt that satisfies the criteria set by the user. 67 | let addr = compute_create2_address(deployer, init_code_hash, *salt); 68 | 69 | // Convert address to string, if it's not case sensitive leave it in lowercase. 70 | let addr_str = if !case_sensitive { 71 | format!("{addr:x}") // we could compare bytes directly but this produces cleaner code 72 | } else { 73 | to_checksum_address(&format!("{addr:x}")) 74 | }; 75 | 76 | let matches_begins = begins.as_deref().is_none_or(|b| addr_str.starts_with(b)); 77 | let matches_ends = ends.as_deref().is_none_or(|e| addr_str.ends_with(&e)); 78 | let matches_contains = contains.as_deref().is_none_or(|c| addr_str.contains(c)); 79 | 80 | matches_begins && matches_ends && matches_contains 81 | }) 82 | .map(|salt| { 83 | let addr = compute_create2_address(deployer, init_code_hash, salt); 84 | (salt, addr) 85 | }) 86 | .expect("should eventually find a match") 87 | } 88 | 89 | #[test] 90 | fn compute_address() { 91 | use std::str::FromStr; 92 | 93 | // Example Transaction: https://etherscan.io/tx/0x99b6e68fa690db1df9a969b838fb27e1254c0fc115428b3cc5695ab74ffe3943 94 | assert_eq!( 95 | Address::from_str("0x552b0c6688fcae5cf0164f27fd129b882a42fa05").unwrap(), 96 | compute_create_address( 97 | Address::from_str("0x899c284A89E113056a72dC9ade5b60E80DD3c94f").unwrap(), 98 | 1 99 | ) 100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /.github/workflows/pr_main.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | contents: read 3 | name: rex integration tests 4 | on: 5 | push: 6 | branches: ["main"] 7 | merge_group: 8 | pull_request: 9 | branches: ["**"] 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | sdk-integration-test: 17 | name: Integration Test - SDK 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Free Disk Space (Ubuntu) 22 | uses: jlumbroso/free-disk-space@v1.3.1 23 | with: 24 | tool-cache: false 25 | large-packages: false 26 | 27 | - name: Checkout sources 28 | uses: actions/checkout@v4 29 | 30 | - name: Setup Rust Environment 31 | uses: ./.github/actions/setup-rust 32 | 33 | - name: Install ethrex 34 | run: | 35 | curl -L https://github.com/lambdaclass/ethrex/releases/download/v7.0.0/ethrex-l2-linux-x86_64 -o /usr/local/bin/ethrex 36 | chmod +x /usr/local/bin/ethrex 37 | ethrex --version 38 | echo "ethrex installed successfully" 39 | 40 | - name: Run ethrex l2 --dev 41 | run: | 42 | ethrex l2 --dev --no-monitor & 43 | 44 | - name: Wait for ethrex l2 45 | run: | 46 | for i in {1..100}; do 47 | if nc -z localhost 3900; then 48 | echo "ProofCoordinator ready!" 49 | exit 0 50 | fi 51 | sleep 10 52 | done 53 | echo "ProofCoordinator not ready in time" 54 | exit 1 55 | 56 | - name: Start Prover 57 | run: | 58 | ethrex l2 prover --proof-coordinators http://localhost:3900 & 59 | 60 | - name: Run tests 61 | run: | 62 | cd /home/runner/work/rex/rex/sdk 63 | sleep 200 64 | PROPOSER_COINBASE_ADDRESS=0x0007a881CD95B1484fca47615B64803dad620C8d cargo test --package rex-sdk --test tests -- --nocapture --test-threads=1 65 | 66 | cli-integration-test: 67 | name: Integration Test - CLI 68 | runs-on: ubuntu-latest 69 | steps: 70 | - name: Free Disk Space (Ubuntu) 71 | uses: jlumbroso/free-disk-space@v1.3.1 72 | with: 73 | tool-cache: false 74 | large-packages: false 75 | - name: Checkout sources 76 | uses: actions/checkout@v4 77 | - name: Setup Rust Environment 78 | uses: ./.github/actions/setup-rust 79 | 80 | - name: Install ethrex 81 | run: | 82 | curl -L https://github.com/lambdaclass/ethrex/releases/download/v7.0.0/ethrex-l2-linux-x86_64 -o /usr/local/bin/ethrex 83 | chmod +x /usr/local/bin/ethrex 84 | ethrex --version 85 | echo "ethrex installed successfully" 86 | 87 | - name: Run ethrex l2 --dev 88 | run: | 89 | ethrex l2 --dev --no-monitor & 90 | 91 | - name: Wait for ethrex l2 92 | run: | 93 | for i in {1..100}; do 94 | if nc -z localhost 3900; then 95 | echo "Service ready!" 96 | exit 0 97 | fi 98 | sleep 10 99 | done 100 | echo "Service not ready in time" 101 | exit 1 102 | 103 | - name: Start Prover 104 | run: | 105 | ethrex l2 prover --proof-coordinators http://localhost:3900 & 106 | 107 | - name: Run tests 108 | run: | 109 | cd /home/runner/work/rex/rex 110 | make cli 111 | sleep 200 112 | cd cli 113 | PROPOSER_COINBASE_ADDRESS=0x0007a881CD95B1484fca47615B64803dad620C8d cargo test --package rex --test tests -- --nocapture --test-threads=1 114 | 115 | # The purpose of this job is to add it as a required check in GitHub so that we don't have to add every individual job as a required check 116 | all-tests: 117 | # "Integration Test" is a required check, don't change the name 118 | name: Integration Test 119 | runs-on: ubuntu-latest 120 | needs: [sdk-integration-test, cli-integration-test] 121 | # Make sure this job runs even if the previous jobs failed or were skipped 122 | if: ${{ always() && needs.sdk-integration-test.result != 'skipped' && needs.cli-integration-test.result != 'skipped' }} 123 | steps: 124 | - name: Check if any job failed 125 | run: | 126 | if [ "${{ needs.sdk-integration-test.result }}" != "success" ]; then 127 | echo "Job SDK integration test Check failed" 128 | exit 1 129 | fi 130 | 131 | if [ "${{ needs.cli-integration-test.result }}" != "success" ]; then 132 | echo "Job CLI integration test Check failed" 133 | exit 1 134 | fi 135 | -------------------------------------------------------------------------------- /.github/workflows/daily_reports.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | contents: read 3 | 4 | name: Daily Reports 5 | on: 6 | schedule: 7 | # Every day at UTC 03:00 8 | - cron: "0 3 * * 1,2,3,4,5" 9 | workflow_dispatch: 10 | 11 | jobs: 12 | sdk-integration-test: 13 | name: Integration Test - SDK 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Free Disk Space (Ubuntu) 17 | uses: jlumbroso/free-disk-space@v1.3.1 18 | with: 19 | tool-cache: false 20 | large-packages: false 21 | 22 | - name: Checkout sources 23 | uses: actions/checkout@v4 24 | - name: Setup Rust Environment 25 | uses: ./.github/actions/setup-rust 26 | 27 | - name: Install ethrex 28 | run: | 29 | curl -L https://github.com/lambdaclass/ethrex/releases/latest/download/ethrex-l2-linux-x86_64 -o /usr/local/bin/ethrex 30 | chmod +x /usr/local/bin/ethrex 31 | ethrex --version 32 | echo "ethrex installed successfully" 33 | - name: Run ethrex l2 --dev 34 | run: | 35 | ethrex l2 --dev --no-monitor & 36 | 37 | - name: Wait for ethrex l2 38 | run: | 39 | for i in {1..100}; do 40 | if nc -z localhost 3900; then 41 | echo "ProofCoordinator ready!" 42 | exit 0 43 | fi 44 | sleep 10 45 | done 46 | echo "ProofCoordinator not ready in time" 47 | exit 1 48 | 49 | - name: Start Prover 50 | run: | 51 | ethrex l2 prover --proof-coordinators http://localhost:3900 & 52 | 53 | - name: Run tests 54 | run: | 55 | cd /home/runner/work/rex/rex/sdk 56 | sleep 200 57 | PROPOSER_COINBASE_ADDRESS=0x0007a881CD95B1484fca47615B64803dad620C8d cargo test --package rex-sdk --test tests -- --nocapture --test-threads=1 58 | 59 | cli-integration-test: 60 | name: Integration Test - CLI 61 | runs-on: ubuntu-latest 62 | 63 | steps: 64 | - name: Free Disk Space (Ubuntu) 65 | uses: jlumbroso/free-disk-space@v1.3.1 66 | with: 67 | tool-cache: false 68 | large-packages: false 69 | 70 | - name: Checkout sources 71 | uses: actions/checkout@v4 72 | - name: Setup Rust Environment 73 | uses: ./.github/actions/setup-rust 74 | 75 | - name: Install ethrex 76 | run: | 77 | curl -L https://github.com/lambdaclass/ethrex/releases/latest/download/ethrex-l2-linux-x86_64 -o /usr/local/bin/ethrex 78 | chmod +x /usr/local/bin/ethrex 79 | ethrex --version 80 | echo "ethrex installed successfully" 81 | 82 | - name: Run ethrex l2 --dev 83 | run: | 84 | ethrex l2 --dev --no-monitor & 85 | 86 | - name: Wait for ethrex l2 87 | run: | 88 | for i in {1..100}; do 89 | if nc -z localhost 3900; then 90 | echo "ProofCoordinator ready!" 91 | exit 0 92 | fi 93 | sleep 10 94 | done 95 | echo "ProofCoordinator not ready in time" 96 | exit 1 97 | 98 | - name: Start Prover 99 | run: | 100 | ethrex l2 prover --proof-coordinators http://localhost:3900 & 101 | 102 | - name: Run tests 103 | run: | 104 | cd /home/runner/work/rex/rex 105 | make cli 106 | cd cli 107 | sleep 200 108 | PROPOSER_COINBASE_ADDRESS=0x0007a881CD95B1484fca47615B64803dad620C8d cargo test --package rex --test tests -- --nocapture --test-threads=1 109 | 110 | post-daily-report: 111 | name: Post report to slack 112 | runs-on: ubuntu-latest 113 | needs: [sdk-integration-test, cli-integration-test] 114 | if: ${{ always() && needs.sdk-integration-test.result != 'skipped' && needs.cli-integration-test.result != 'skipped' }} 115 | steps: 116 | - name: Checkout sources 117 | uses: actions/checkout@v4 118 | 119 | - name: Check if any job failed 120 | env: 121 | SLACK_WEBHOOK: > 122 | ${{ github.event_name == 'workflow_dispatch' 123 | && secrets.TEST_CHANNEL_SLACK 124 | || secrets.ETHREX_L2_SLACK_WEBHOOK 125 | }} 126 | run: | 127 | if [ "${{ needs.sdk-integration-test.result }}" != "success" ]; then 128 | sh .github/scripts/publish_report.sh "$SLACK_WEBHOOK" "Rex SDK is not working with the latest ethrex version." 129 | fi 130 | if [ "${{ needs.cli-integration-test.result }}" != "success" ]; then 131 | sh .github/scripts/publish_report.sh "$SLACK_WEBHOOK" "Rex CLI is not working with the latest ethrex version." 132 | fi 133 | echo "Sending Results" >> $GITHUB_STEP_SUMMARY 134 | -------------------------------------------------------------------------------- /sdk/src/l2/privileged_transaction_data.rs: -------------------------------------------------------------------------------- 1 | use ethrex_common::{ 2 | Address, Bytes, H160, H256, U256, 3 | types::{PrivilegedL2Transaction, TxType}, 4 | }; 5 | use ethrex_rpc::{ 6 | EthClient, 7 | clients::{EthClientError, Overrides}, 8 | types::receipt::RpcLogInfo, 9 | }; 10 | use ethrex_sdk::build_generic_tx; 11 | 12 | use crate::SdkError; 13 | 14 | // Duplicated from https://github.com/lambdaclass/ethrex/blob/c673d17568fdb044dce05513ecb17ec6db431e3f/crates/l2/sequencer/l1_watcher.rs#L303 15 | pub struct PrivilegedTransactionData { 16 | pub value: U256, 17 | pub to_address: H160, 18 | pub transaction_id: U256, 19 | pub from: H160, 20 | pub gas_limit: U256, 21 | pub calldata: Vec, 22 | } 23 | 24 | impl PrivilegedTransactionData { 25 | pub fn from_log(log: RpcLogInfo) -> Result { 26 | let from = 27 | H256::from_slice(log.data.get(0..32).ok_or(SdkError::FailedToDeserializeLog( 28 | "Failed to parse gas_limit from log: log.data[0..32] out of bounds".to_owned(), 29 | ))?); 30 | let from_address = hash_to_address(from); 31 | 32 | let to = H256::from_slice( 33 | log.data 34 | .get(32..64) 35 | .ok_or(SdkError::FailedToDeserializeLog( 36 | "Failed to parse gas_limit from log: log.data[32..64] out of bounds".to_owned(), 37 | ))?, 38 | ); 39 | let to_address = hash_to_address(to); 40 | 41 | let transaction_id = U256::from_big_endian(log.data.get(64..96).ok_or( 42 | SdkError::FailedToDeserializeLog( 43 | "Failed to parse gas_limit from log: log.data[64..96] out of bounds".to_owned(), 44 | ), 45 | )?); 46 | 47 | let value = U256::from_big_endian(log.data.get(96..128).ok_or( 48 | SdkError::FailedToDeserializeLog( 49 | "Failed to parse gas_limit from log: log.data[96..128] out of bounds".to_owned(), 50 | ), 51 | )?); 52 | 53 | let gas_limit = U256::from_big_endian(log.data.get(128..160).ok_or( 54 | SdkError::FailedToDeserializeLog( 55 | "Failed to parse gas_limit from log: log.data[128..160] out of bounds".to_owned(), 56 | ), 57 | )?); 58 | 59 | // 160..192 is taken by offset_data, which we do not need 60 | 61 | let calldata_len = U256::from_big_endian( 62 | log.data 63 | .get(192..224) 64 | .ok_or(SdkError::FailedToDeserializeLog( 65 | "Failed to parse calldata_len from log: log.data[192..224] out of bounds" 66 | .to_owned(), 67 | ))?, 68 | ); 69 | 70 | let calldata = log 71 | .data 72 | .get(224..224 + calldata_len.as_usize()) 73 | .ok_or(SdkError::FailedToDeserializeLog( 74 | "Failed to parse calldata from log: log.data[224..224 + calldata_len] out of bounds" 75 | .to_owned(), 76 | ))?; 77 | 78 | Ok(Self { 79 | value, 80 | to_address, 81 | transaction_id, 82 | from: from_address, 83 | gas_limit, 84 | calldata: calldata.to_vec(), 85 | }) 86 | } 87 | 88 | pub async fn into_tx( 89 | &self, 90 | eth_client: &EthClient, 91 | chain_id: u64, 92 | gas_price: u64, 93 | ) -> Result { 94 | let generic_tx = build_generic_tx( 95 | eth_client, 96 | TxType::Privileged, 97 | self.to_address, 98 | self.from, 99 | Bytes::copy_from_slice(&self.calldata), 100 | Overrides { 101 | chain_id: Some(chain_id), 102 | // Using the transaction_id as nonce. 103 | // If we make a transaction on the L2 with this address, we may break the 104 | // privileged transaction workflow. 105 | nonce: Some(self.transaction_id.as_u64()), 106 | value: Some(self.value), 107 | gas_limit: Some(self.gas_limit.as_u64()), 108 | // TODO(CHECK): Seems that when we start the L2, we need to set the gas. 109 | // Otherwise, the transaction is not included in the mempool. 110 | // We should override the blockchain to always include the transaction. 111 | max_fee_per_gas: Some(gas_price), 112 | max_priority_fee_per_gas: Some(gas_price), 113 | ..Default::default() 114 | }, 115 | ) 116 | .await?; 117 | Ok(generic_tx.try_into()?) 118 | } 119 | } 120 | 121 | pub fn hash_to_address(hash: H256) -> Address { 122 | Address::from_slice(&hash.as_fixed_bytes()[12..]) 123 | } 124 | -------------------------------------------------------------------------------- /.github/workflows/tag_release.yaml: -------------------------------------------------------------------------------- 1 | name: Rex Release 2 | 3 | on: 4 | push: 5 | # On tags, this generates a release and pushes docker images with the tag name 6 | tags: 7 | - "v*.*.*-*" 8 | # On pushes to main, this silently builds and pushes docker images tagged as 'main' 9 | branches: 10 | - main 11 | workflow_dispatch: 12 | 13 | permissions: 14 | contents: write 15 | packages: write 16 | actions: write 17 | 18 | env: 19 | REGISTRY: ghcr.io 20 | IMAGE_NAME: ${{ github.repository }} 21 | PROVER_REPRODUCIBLE_BUILD: true 22 | 23 | jobs: 24 | build-rex: 25 | strategy: 26 | matrix: 27 | platform: 28 | - ubuntu-22.04 29 | - ubuntu-22.04-arm 30 | - macos-latest 31 | include: 32 | - platform: ubuntu-22.04 33 | os: linux 34 | arch: x86_64 35 | - platform: ubuntu-22.04-arm 36 | os: linux 37 | arch: aarch64 38 | - platform: macos-latest 39 | os: macos 40 | arch: aarch64 41 | runs-on: ${{ matrix.platform }} 42 | steps: 43 | - name: Checkout code 44 | uses: actions/checkout@v4 45 | 46 | - name: Free Disk Space 47 | if: ${{ matrix.os == 'linux' }} 48 | uses: ./.github/actions/free-disk 49 | 50 | - name: Setup Rust Environment 51 | uses: ./.github/actions/setup-rust 52 | 53 | - name: Build rex 54 | run: | 55 | cargo build --release --bin rex 56 | chmod +x target/release/rex 57 | mv target/release/rex rex-${{ matrix.os }}-${{ matrix.arch }} 58 | 59 | - name: Upload artifact 60 | uses: actions/upload-artifact@v4 61 | with: 62 | name: rex-${{ matrix.os }}-${{ matrix.arch }} 63 | path: rex-${{ matrix.os }}-${{ matrix.arch }} 64 | 65 | build-docker: 66 | name: "Build and publish rex docker image" 67 | runs-on: ubuntu-latest 68 | 69 | # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. 70 | permissions: 71 | contents: read 72 | packages: write 73 | attestations: write 74 | id-token: write 75 | actions: write 76 | 77 | steps: 78 | - name: Checkout code 79 | uses: actions/checkout@v4 80 | 81 | - name: Free Disk Space 82 | uses: ./.github/actions/free-disk 83 | 84 | - name: Format name 85 | run: | 86 | # For branch builds (main) we want docker images tagged as 'main'. For tag pushes 87 | # use the tag name (stripped of a leading 'v'). 88 | if [[ "$GITHUB_REF" == "refs/heads/main" ]]; then 89 | echo "TAG_VERSION=main" >> $GITHUB_ENV 90 | else 91 | echo "TAG_VERSION=$(echo ${{ github.ref_name }} | tr -d v)" >> $GITHUB_ENV 92 | fi 93 | 94 | # Pushes to ghcr.io/lambdaclass/rex 95 | - name: Build and push L1 Docker image 96 | id: push_l1 97 | uses: ./.github/actions/build-docker 98 | with: 99 | registry: ${{ env.REGISTRY }} 100 | username: ${{ github.actor }} 101 | password: ${{ secrets.GITHUB_TOKEN }} 102 | push: ${{ github.event_name != 'workflow_dispatch'}} 103 | tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.TAG_VERSION }} 104 | 105 | # Creates a release on GitHub with the binaries 106 | finalize-release: 107 | if: github.ref_type == 'tag' && github.event_name != 'workflow_dispatch' 108 | needs: 109 | - build-rex 110 | - build-docker 111 | runs-on: ubuntu-latest 112 | steps: 113 | - name: Checkout Code 114 | uses: actions/checkout@v4 115 | with: 116 | fetch-depth: 0 117 | 118 | - name: Download artifacts 119 | uses: actions/download-artifact@v4 120 | with: 121 | path: ./bin 122 | pattern: "rex*" 123 | 124 | - name: Get previous tag 125 | run: | 126 | last_tag=$(git --no-pager tag --sort=creatordate | grep -v -E '^v[0-9]+\.[0-9]+\.[0-9]+-' | grep -v '${{ github.ref_name }}' | tail -1) 127 | echo "Last tag: $last_tag" 128 | common_parent=$(git merge-base ${{ github.ref_name }} $last_tag) 129 | echo "PREVIOUS_TAG: $common_parent" 130 | echo "PREVIOUS_TAG=$common_parent" >> $GITHUB_ENV 131 | 132 | - name: Update CHANGELOG 133 | id: changelog 134 | uses: requarks/changelog-action@v1 135 | with: 136 | token: ${{ secrets.GITHUB_TOKEN }} 137 | fromTag: ${{ github.ref_name }} 138 | toTag: ${{ env.PREVIOUS_TAG }} 139 | writeToFile: false 140 | 141 | - name: Finalize Release 142 | uses: softprops/action-gh-release@v2 143 | env: 144 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 145 | with: 146 | files: ./bin/**/* 147 | draft: false 148 | prerelease: true 149 | tag_name: ${{ github.ref_name }} 150 | name: "rex: ${{ github.ref_name }}" 151 | body: > 152 | Installation and running instructions can be found in [our docs](https://github.com/lambdaclass/rex/blob/main/README.md). 153 | 154 | ${{ steps.changelog.outputs.changes }} 155 | -------------------------------------------------------------------------------- /sdk/src/sdk.rs: -------------------------------------------------------------------------------- 1 | use ethrex_common::{ 2 | Address, Bytes, U256, 3 | types::{TxKind, TxType}, 4 | }; 5 | use ethrex_l2_rpc::signer::{LocalSigner, Signer}; 6 | use ethrex_rlp::encode::RLPEncode; 7 | 8 | use ethrex_rpc::{ 9 | EthClient, 10 | clients::{EthClientError, Overrides}, 11 | types::{ 12 | block_identifier::{BlockIdentifier, BlockTag}, 13 | receipt::RpcReceipt, 14 | }, 15 | }; 16 | use ethrex_sdk::{build_generic_tx, send_generic_transaction}; 17 | use keccak_hash::{H256, keccak}; 18 | use secp256k1::SecretKey; 19 | 20 | pub mod client; 21 | pub mod create; 22 | pub mod errors; 23 | pub mod keystore; 24 | pub mod sign; 25 | pub mod utils; 26 | 27 | pub mod l2; 28 | 29 | #[derive(Debug, thiserror::Error)] 30 | pub enum SdkError { 31 | #[error("Failed to parse address from hex")] 32 | FailedToParseAddressFromHex, 33 | #[error("Failed deserializing log: {0}")] 34 | FailedToDeserializeLog(String), 35 | } 36 | 37 | pub async fn transfer( 38 | amount: U256, 39 | from: Address, 40 | to: Address, 41 | private_key: &SecretKey, 42 | client: &EthClient, 43 | mut overrides: Overrides, 44 | ) -> Result { 45 | overrides.value = Some(amount); 46 | let tx = build_generic_tx( 47 | client, 48 | TxType::EIP1559, 49 | to, 50 | from, 51 | Default::default(), 52 | Overrides { 53 | value: Some(amount), 54 | ..Default::default() 55 | }, 56 | ) 57 | .await?; 58 | 59 | let signer = LocalSigner::new(*private_key).into(); 60 | send_generic_transaction(client, tx, &signer).await 61 | } 62 | 63 | pub async fn deploy( 64 | client: &EthClient, 65 | deployer: &Signer, 66 | init_code: Bytes, 67 | overrides: Overrides, 68 | silent: bool, 69 | ) -> Result<(H256, Address), EthClientError> { 70 | let mut deploy_overrides = overrides; 71 | deploy_overrides.to = Some(TxKind::Create); 72 | 73 | let deploy_tx = build_generic_tx( 74 | client, 75 | TxType::EIP1559, 76 | Address::zero(), 77 | deployer.address(), 78 | init_code, 79 | deploy_overrides, 80 | ) 81 | .await?; 82 | let deploy_tx_hash = send_generic_transaction(client, deploy_tx, deployer).await?; 83 | 84 | let nonce = client 85 | .get_nonce(deployer.address(), BlockIdentifier::Tag(BlockTag::Latest)) 86 | .await?; 87 | let mut encode = vec![]; 88 | (deployer.address(), nonce).encode(&mut encode); 89 | 90 | //Taking the last 20bytes so it matches an H160 == Address length 91 | let deployed_address = Address::from_slice(keccak(encode).as_fixed_bytes().get(12..).ok_or( 92 | EthClientError::Custom("Failed to get deployed_address".to_owned()), 93 | )?); 94 | 95 | wait_for_transaction_receipt(deploy_tx_hash, client, 1000, silent).await?; 96 | 97 | Ok((deploy_tx_hash, deployed_address)) 98 | } 99 | 100 | pub async fn wait_for_transaction_receipt( 101 | tx_hash: H256, 102 | client: &EthClient, 103 | max_retries: u64, 104 | silent: bool, 105 | ) -> Result { 106 | let mut receipt = client.get_transaction_receipt(tx_hash).await?; 107 | let mut r#try = 1; 108 | while receipt.is_none() { 109 | if !silent { 110 | println!("[{try}/{max_retries}] Retrying to get transaction receipt for {tx_hash:#x}"); 111 | } 112 | 113 | if max_retries == r#try { 114 | return Err(EthClientError::Custom(format!( 115 | "Transaction receipt for {tx_hash:#x} not found after {max_retries} retries" 116 | ))); 117 | } 118 | r#try += 1; 119 | 120 | tokio::time::sleep(std::time::Duration::from_secs(2)).await; 121 | 122 | receipt = client.get_transaction_receipt(tx_hash).await?; 123 | } 124 | receipt.ok_or(EthClientError::Custom( 125 | "Transaction receipt is None".to_owned(), 126 | )) 127 | } 128 | 129 | pub fn balance_in_eth(eth: bool, balance: U256) -> String { 130 | if eth { 131 | let mut balance = format!("{balance}"); 132 | let len = balance.len(); 133 | 134 | balance = match len { 135 | 18 => { 136 | let mut front = "0.".to_owned(); 137 | front.push_str(&balance); 138 | front 139 | } 140 | 0..=17 => { 141 | let mut front = "0.".to_owned(); 142 | let zeros = "0".repeat(18 - len); 143 | front.push_str(&zeros); 144 | front.push_str(&balance); 145 | front 146 | } 147 | 19.. => { 148 | balance.insert(len - 18, '.'); 149 | balance 150 | } 151 | }; 152 | balance 153 | } else { 154 | format!("{balance}") 155 | } 156 | } 157 | 158 | #[test] 159 | fn test_balance_in_ether() { 160 | // test more than 1 ether 161 | assert_eq!( 162 | "999999999.999003869993631450", 163 | balance_in_eth( 164 | true, 165 | U256::from_dec_str("999999999999003869993631450").unwrap() 166 | ) 167 | ); 168 | 169 | // test 0.5 170 | assert_eq!( 171 | "0.509003869993631450", 172 | balance_in_eth( 173 | true, 174 | U256::from_dec_str("000000000509003869993631450").unwrap() 175 | ) 176 | ); 177 | 178 | // test 0.005 179 | assert_eq!( 180 | "0.005090038699936314", 181 | balance_in_eth( 182 | true, 183 | U256::from_dec_str("000000000005090038699936314").unwrap() 184 | ) 185 | ); 186 | 187 | // test 0.0 188 | assert_eq!("0.000000000000000000", balance_in_eth(true, U256::zero())); 189 | } 190 | -------------------------------------------------------------------------------- /sdk/src/l2/withdraw.rs: -------------------------------------------------------------------------------- 1 | use ethrex_common::{Address, Bytes, U256, types::TxType}; 2 | use ethrex_l2_common::{ 3 | calldata::Value, l1_messages::L1MessageProof, utils::get_address_from_secret_key, 4 | }; 5 | use ethrex_l2_rpc::signer::{LocalSigner, Signer}; 6 | use ethrex_rpc::{ 7 | EthClient, 8 | clients::{EthClientError, Overrides}, 9 | }; 10 | use ethrex_sdk::{ 11 | COMMON_BRIDGE_L2_ADDRESS, build_generic_tx, calldata::encode_calldata, send_generic_transaction, 12 | }; 13 | use keccak_hash::H256; 14 | use secp256k1::SecretKey; 15 | 16 | use crate::l2::constants::{ 17 | CLAIM_WITHDRAWAL_ERC20_SIGNATURE, L2_WITHDRAW_SIGNATURE, L2_WITHDRAW_SIGNATURE_ERC20, 18 | }; 19 | 20 | pub async fn withdraw( 21 | amount: U256, 22 | from: Address, 23 | from_pk: SecretKey, 24 | proposer_client: &EthClient, 25 | nonce: Option, 26 | ) -> Result { 27 | let withdraw_transaction = build_generic_tx( 28 | proposer_client, 29 | TxType::EIP1559, 30 | COMMON_BRIDGE_L2_ADDRESS, 31 | from, 32 | Bytes::from( 33 | encode_calldata(L2_WITHDRAW_SIGNATURE, &[Value::Address(from)]) 34 | .expect("Failed to encode calldata"), 35 | ), 36 | Overrides { 37 | value: Some(amount), 38 | nonce, 39 | ..Default::default() 40 | }, 41 | ) 42 | .await?; 43 | 44 | let signer = Signer::Local(LocalSigner::new(from_pk)); 45 | 46 | send_generic_transaction(proposer_client, withdraw_transaction, &signer).await 47 | } 48 | 49 | pub async fn withdraw_erc20( 50 | amount: U256, 51 | from: Address, 52 | from_pk: SecretKey, 53 | token_l1: Address, 54 | token_l2: Address, 55 | l2_client: &EthClient, 56 | ) -> Result { 57 | let data = [ 58 | Value::Address(token_l1), 59 | Value::Address(token_l2), 60 | Value::Address(from), 61 | Value::Uint(amount), 62 | ]; 63 | let withdraw_data = encode_calldata(L2_WITHDRAW_SIGNATURE_ERC20, &data) 64 | .expect("Failed to encode calldata for withdraw ERC20"); 65 | let withdraw_transaction = build_generic_tx( 66 | l2_client, 67 | TxType::EIP1559, 68 | COMMON_BRIDGE_L2_ADDRESS, 69 | from, 70 | Bytes::from(withdraw_data), 71 | Default::default(), 72 | ) 73 | .await?; 74 | let signer = Signer::Local(LocalSigner::new(from_pk)); 75 | send_generic_transaction(l2_client, withdraw_transaction, &signer).await 76 | } 77 | 78 | pub async fn claim_withdraw( 79 | amount: U256, 80 | from: Address, 81 | from_pk: SecretKey, 82 | eth_client: &EthClient, 83 | message_proof: &L1MessageProof, 84 | bridge_address: Address, 85 | ) -> Result { 86 | println!("Claiming {amount} from bridge to {from:#x}"); 87 | 88 | const CLAIM_WITHDRAWAL_SIGNATURE: &str = "claimWithdrawal(uint256,uint256,uint256,bytes32[])"; 89 | 90 | let calldata_values = vec![ 91 | Value::Uint(amount), 92 | Value::Uint(message_proof.batch_number.into()), 93 | Value::Uint(message_proof.message_id), 94 | Value::Array( 95 | message_proof 96 | .merkle_proof 97 | .iter() 98 | .map(|hash| Value::FixedBytes(hash.as_fixed_bytes().to_vec().into())) 99 | .collect(), 100 | ), 101 | ]; 102 | 103 | let claim_withdrawal_data = encode_calldata(CLAIM_WITHDRAWAL_SIGNATURE, &calldata_values) 104 | .expect("Failed to encode calldata for claim withdrawal"); 105 | 106 | println!( 107 | "Claiming withdrawal with calldata: {}", 108 | hex::encode(&claim_withdrawal_data) 109 | ); 110 | 111 | let claim_tx = build_generic_tx( 112 | eth_client, 113 | TxType::EIP1559, 114 | bridge_address, 115 | from, 116 | claim_withdrawal_data.into(), 117 | Overrides { 118 | from: Some(from), 119 | ..Default::default() 120 | }, 121 | ) 122 | .await?; 123 | let signer = Signer::Local(LocalSigner::new(from_pk)); 124 | 125 | send_generic_transaction(eth_client, claim_tx, &signer).await 126 | } 127 | 128 | pub async fn claim_erc20withdraw( 129 | token_l1: Address, 130 | token_l2: Address, 131 | amount: U256, 132 | from_pk: SecretKey, 133 | eth_client: &EthClient, 134 | message_proof: &L1MessageProof, 135 | bridge_address: Address, 136 | ) -> Result { 137 | let from = get_address_from_secret_key(&from_pk).map_err(EthClientError::Custom)?; 138 | let calldata_values = vec![ 139 | Value::Address(token_l1), 140 | Value::Address(token_l2), 141 | Value::Uint(amount), 142 | Value::Uint(U256::from(message_proof.batch_number)), 143 | Value::Uint(message_proof.message_id), 144 | Value::Array( 145 | message_proof 146 | .merkle_proof 147 | .clone() 148 | .into_iter() 149 | .map(|v| Value::FixedBytes(Bytes::copy_from_slice(v.as_bytes()))) 150 | .collect(), 151 | ), 152 | ]; 153 | 154 | let claim_withdrawal_data = 155 | encode_calldata(CLAIM_WITHDRAWAL_ERC20_SIGNATURE, &calldata_values)?; 156 | 157 | println!( 158 | "Claiming withdrawal with calldata: {}", 159 | hex::encode(&claim_withdrawal_data) 160 | ); 161 | 162 | let claim_tx = build_generic_tx( 163 | eth_client, 164 | TxType::EIP1559, 165 | bridge_address, 166 | from, 167 | claim_withdrawal_data.into(), 168 | Overrides { 169 | from: Some(from), 170 | ..Default::default() 171 | }, 172 | ) 173 | .await?; 174 | 175 | let signer = Signer::Local(LocalSigner::new(from_pk)); 176 | 177 | send_generic_transaction(eth_client, claim_tx, &signer).await 178 | } 179 | -------------------------------------------------------------------------------- /cli/src/common.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use crate::utils::{parse_hex, parse_private_key, parse_u256}; 4 | use clap::Parser; 5 | use ethrex_common::{Address, Bytes, Secret, U256}; 6 | use secp256k1::SecretKey; 7 | 8 | #[derive(Parser)] 9 | pub struct BalanceArgs { 10 | pub account: Address, 11 | #[clap( 12 | long = "token", 13 | help = "ERC20 token address", 14 | long_help = "Specify the token address, the base token is used as default." 15 | )] 16 | pub token_address: Option
, 17 | #[arg( 18 | long = "eth", 19 | required = false, 20 | default_value_t = false, 21 | help = "Display the balance in ETH." 22 | )] 23 | pub eth: bool, 24 | } 25 | 26 | #[derive(Parser)] 27 | pub struct TransferArgs { 28 | #[clap(value_parser = parse_u256)] 29 | pub amount: U256, 30 | pub to: Address, 31 | #[clap(long = "token", required = false)] 32 | pub token_address: Option
, 33 | #[clap(long = "nonce")] 34 | pub nonce: Option, 35 | #[clap( 36 | long, 37 | short = 'c', 38 | required = false, 39 | help = "Send the request asynchronously." 40 | )] 41 | pub cast: bool, 42 | #[clap( 43 | long, 44 | short = 's', 45 | required = false, 46 | help = "Display only the tx hash." 47 | )] 48 | pub silent: bool, 49 | #[clap( 50 | long, 51 | required = false, 52 | help = "Display transaction URL in the explorer." 53 | )] 54 | pub explorer_url: bool, 55 | #[clap(value_parser = parse_private_key, env = "PRIVATE_KEY", required = false)] 56 | pub private_key: SecretKey, 57 | } 58 | 59 | #[derive(Parser)] 60 | pub struct SendArgs { 61 | pub to: Address, 62 | #[clap( 63 | long, 64 | value_parser = parse_u256, 65 | default_value = "0", 66 | required = false, 67 | help = "Value to send in wei" 68 | )] 69 | pub value: U256, 70 | #[clap(long = "calldata", value_parser = parse_hex, required = false, default_value = "")] 71 | pub calldata: Bytes, 72 | #[clap(long = "chain-id", required = false)] 73 | pub chain_id: Option, 74 | #[clap(long = "nonce", required = false)] 75 | pub nonce: Option, 76 | #[clap(long = "gas-limit", required = false)] 77 | pub gas_limit: Option, 78 | #[clap(long = "gas-price", required = false)] 79 | pub max_fee_per_gas: Option, 80 | #[clap(long = "priority-gas-price", required = false)] 81 | pub max_priority_fee_per_gas: Option, 82 | #[clap( 83 | long, 84 | short = 'c', 85 | required = false, 86 | help = "Send the request asynchronously." 87 | )] 88 | pub cast: bool, 89 | #[clap( 90 | long, 91 | short = 's', 92 | required = false, 93 | help = "Display only the tx hash." 94 | )] 95 | pub silent: bool, 96 | #[clap( 97 | long, 98 | required = false, 99 | help = "Display transaction URL in the explorer." 100 | )] 101 | pub explorer_url: bool, 102 | #[clap( 103 | long = "private-key", 104 | short = 'k', 105 | value_parser = parse_private_key, 106 | env = "PRIVATE_KEY", 107 | required = false 108 | )] 109 | pub private_key: SecretKey, 110 | #[clap(required = false)] 111 | pub _args: Vec, 112 | } 113 | 114 | #[derive(Parser)] 115 | pub struct CallArgs { 116 | pub to: Address, 117 | #[clap(long, value_parser = parse_hex, required = false, default_value = "")] 118 | pub calldata: Bytes, 119 | #[clap( 120 | long, 121 | value_parser = parse_u256, 122 | default_value = "0", 123 | required = false, 124 | help = "Value to send in wei" 125 | )] 126 | pub value: U256, 127 | #[clap(long, required = false)] 128 | pub from: Option
, 129 | #[clap(long, required = false)] 130 | pub gas_limit: Option, 131 | #[clap(long, required = false)] 132 | pub max_fee_per_gas: Option, 133 | #[clap( 134 | long, 135 | required = false, 136 | help = "Display transaction URL in the explorer." 137 | )] 138 | pub explorer_url: bool, 139 | #[clap(required = false)] 140 | pub _args: Vec, 141 | } 142 | 143 | #[derive(Parser, Clone)] 144 | #[clap(group = clap::ArgGroup::new("source").required(true))] 145 | pub struct DeployArgs { 146 | #[clap(long, group = "source", value_parser = parse_hex, required = false)] 147 | pub bytecode: Option, 148 | #[clap( 149 | value_parser = parse_u256, 150 | default_value = "0", 151 | required = false, 152 | help = "Value to send in wei" 153 | )] 154 | pub value: U256, 155 | #[clap(long = "chain-id", required = false)] 156 | pub chain_id: Option, 157 | #[clap(long = "nonce", required = false)] 158 | pub nonce: Option, 159 | #[clap(long = "gas-limit", required = false)] 160 | pub gas_limit: Option, 161 | #[clap(long = "gas-price", required = false)] 162 | pub max_fee_per_gas: Option, 163 | #[clap(long = "priority-gas-price", required = false)] 164 | pub max_priority_fee_per_gas: Option, 165 | #[clap(long, required = false)] 166 | pub print_address: bool, 167 | #[clap( 168 | long, 169 | short = 'c', 170 | required = false, 171 | help = "Send the request asynchronously." 172 | )] 173 | pub cast: bool, 174 | #[clap( 175 | long, 176 | short = 's', 177 | required = false, 178 | help = "Display only the tx hash." 179 | )] 180 | pub silent: bool, 181 | #[clap( 182 | long, 183 | required = false, 184 | help = "Display transaction URL in the explorer." 185 | )] 186 | pub explorer_url: bool, 187 | #[arg(value_parser = parse_private_key, env = "PRIVATE_KEY", required = false)] 188 | pub private_key: SecretKey, 189 | #[arg( 190 | long, 191 | help = "Path to the Solidity file to compile and deploy", 192 | group = "source" 193 | )] 194 | pub contract_path: Option, 195 | #[arg( 196 | long, 197 | required_unless_present = "bytecode", 198 | help = "Comma-separated remappings (e.g. '@openzeppelin/contracts=https://github.com/OpenZeppelin/openzeppelin-contracts.git,@custom=path/to/custom')" 199 | )] 200 | pub remappings: Option, 201 | #[arg( 202 | long, 203 | help = "Remove downloaded dependencies after compilation", 204 | default_value_t = false, 205 | required = false 206 | )] 207 | pub keep_deps: bool, 208 | #[arg( 209 | long, 210 | help = "Salt for deploying CREATE2 contracts. If it is provided, the contract will be deployed using CREATE2.", 211 | required = false 212 | )] 213 | pub salt: Option, 214 | #[arg(last = true, hide = true)] 215 | pub _args: Vec, 216 | } 217 | -------------------------------------------------------------------------------- /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 | address Get either the account's address from private key, the zero address, or a random address [aliases: addr, a] 41 | autocomplete Generate shell completion scripts. 42 | balance Get the account's balance info. [aliases: bal, b] 43 | block-number Get the current block_number. [aliases: bl] 44 | call Make a call to a contract 45 | chain-id Get the network's chain id. 46 | code Returns code at a given address 47 | create-address Compute contract address given the deployer address and nonce. 48 | create2-address Compute contract address with CREATE2 opcode. 49 | deploy Deploy a contract 50 | hash Get either the keccak for a given input, the zero hash, the empty string, or a random hash [aliases: h] 51 | l2 L2 specific commands. 52 | nonce Get the account's nonce. [aliases: n] 53 | receipt Get the transaction's receipt. [aliases: r] 54 | send Send a transaction 55 | sign Sign a message with a private key 56 | signer 57 | transaction Get the transaction's info. [aliases: tx, t] 58 | transfer Transfer funds to another wallet. 59 | verify-signature Verify if the signature of a message was made by an account 60 | help Print this message or the help of the given subcommand(s) 61 | 62 | Options: 63 | -h, --help Print help 64 | -V, --version Print version 65 | ``` 66 | 67 | ## Commands 68 | 69 | ### `rex address` 70 | 71 | ```Shell 72 | Get either the account's address from private key, the zero address, or a random address 73 | 74 | Usage: rex address [OPTIONS] 75 | 76 | Options: 77 | --from-private-key 78 | The private key to derive the address from. [env: PRIVATE_KEY=] 79 | -z, --zero 80 | The zero address. 81 | -r, --random 82 | A random address. 83 | -h, --help 84 | Print help 85 | ``` 86 | 87 | ### `rex hash` 88 | 89 | ```Shell 90 | Get either the keccak for a given input, the zero hash, the empty string, or a random hash 91 | 92 | Usage: rex hash [OPTIONS] 93 | 94 | Options: 95 | --input The input to hash. 96 | -z, --zero The zero hash. 97 | -r, --random A random hash. 98 | -s, --string Hash of empty string 99 | -h, --help Print help 100 | ``` 101 | 102 | ### `rex receipt` 103 | 104 | ```Shell 105 | Get the transaction's receipt. 106 | 107 | Usage: rex receipt [RPC_URL] 108 | 109 | Arguments: 110 | 111 | [RPC_URL] [env: RPC_URL=] [default: http://localhost:8545] 112 | 113 | Options: 114 | -h, --help Print help 115 | ``` 116 | 117 | ### `rex transaction` 118 | 119 | ```Shell 120 | Get the transaction's info. 121 | 122 | Usage: rex transaction [RPC_URL] 123 | 124 | Arguments: 125 | 126 | [RPC_URL] [env: RPC_URL=] [default: http://localhost:8545] 127 | 128 | Options: 129 | -h, --help Print help 130 | ``` 131 | 132 | ### `rex balance` 133 | 134 | ```Shell 135 | Get the account's balance info. 136 | 137 | Usage: rex balance [OPTIONS] [RPC_URL] 138 | 139 | Arguments: 140 | 141 | [RPC_URL] [env: RPC_URL=] [default: http://localhost:8545] 142 | 143 | Options: 144 | --token Specify the token address, the ETH is used as default. 145 | --eth Display the balance in ETH. 146 | -h, --help Print help 147 | ``` 148 | 149 | ### `rex nonce` 150 | 151 | ```Shell 152 | Get the account's nonce. 153 | 154 | Usage: rex nonce [RPC_URL] 155 | 156 | Arguments: 157 | 158 | [RPC_URL] [env: RPC_URL=] [default: http://localhost:8545] 159 | 160 | Options: 161 | -h, --help Print help 162 | ``` 163 | 164 | ### `rex block-number` 165 | 166 | ```Shell 167 | Get the current block_number. 168 | 169 | Usage: rex block-number [RPC_URL] 170 | 171 | Arguments: 172 | [RPC_URL] [env: RPC_URL=] [default: http://localhost:8545] 173 | 174 | Options: 175 | -h, --help Print help 176 | ``` 177 | 178 | ### `rex signer` 179 | 180 | ```Shell 181 | Usage: rex signer 182 | 183 | Arguments: 184 | 185 | 186 | 187 | Options: 188 | -h, --help Print help 189 | ``` 190 | 191 | ### `rex chain-id` 192 | 193 | ```Shell 194 | Get the network's chain id. 195 | 196 | Usage: rex chain-id [OPTIONS] [RPC_URL] 197 | 198 | Arguments: 199 | [RPC_URL] [env: RPC_URL=] [default: http://localhost:8545] 200 | 201 | Options: 202 | -h, --hex Display the chain id as a hex-string. 203 | -h, --help Print help 204 | ``` 205 | 206 | ### `rex transfer` 207 | 208 | ```Shell 209 | Transfer funds to another wallet. 210 | 211 | Usage: rex transfer [OPTIONS] [RPC_URL] 212 | 213 | Arguments: 214 | 215 | 216 | [env: PRIVATE_KEY=] 217 | [RPC_URL] [env: RPC_URL=] [default: http://localhost:8545] 218 | 219 | Options: 220 | --token 221 | --nonce 222 | -b Do not wait for the transaction receipt 223 | --explorer-url Display transaction URL in the explorer. 224 | -h, --help Print help 225 | ``` 226 | 227 | ### `rex send` 228 | 229 | ```Shell 230 | Send a transaction 231 | 232 | Usage: rex send [OPTIONS] [ARGS]... 233 | 234 | Arguments: 235 | 236 | [ARGS]... 237 | 238 | Options: 239 | --value 240 | Value to send in wei [default: 0] 241 | --calldata 242 | [default: ] 243 | --chain-id 244 | 245 | --nonce 246 | 247 | --gas-limit 248 | 249 | --gas-price 250 | 251 | --priority-gas-price 252 | 253 | -c, --cast 254 | Send the request asynchronously. 255 | -s, --silent 256 | Display only the tx hash. 257 | --explorer-url 258 | Display transaction URL in the explorer. 259 | -k, --private-key 260 | [env: PRIVATE_KEY=] 261 | --rpc-url 262 | [env: RPC_URL=] [default: http://localhost:8545] 263 | -h, --help 264 | Print help 265 | ``` 266 | 267 | ### `rex call` 268 | 269 | ```Shell 270 | Make a call to a contract 271 | 272 | Usage: rex call [OPTIONS] [ARGS]... 273 | 274 | Arguments: 275 | 276 | [ARGS]... 277 | 278 | Options: 279 | --calldata [default: ] 280 | --value Value to send in wei [default: 0] 281 | --from 282 | --gas-limit 283 | --max-fee-per-gas 284 | --explorer-url Display transaction URL in the explorer. 285 | --rpc-url [env: RPC_URL=] [default: http://localhost:8545] 286 | -h, --help Print help 287 | ``` 288 | 289 | ### `rex deploy` 290 | 291 | ```Shell 292 | Deploy a contract 293 | 294 | Usage: rex deploy [OPTIONS] [VALUE] -- [SIGNATURE [ARGS]] 295 | 296 | Arguments: 297 | 298 | [VALUE] Value to send in wei [default: 0] 299 | [env: PRIVATE_KEY=] 300 | 301 | 302 | Options: 303 | --chain-id 304 | --nonce 305 | --gas-limit 306 | --gas-price 307 | --priority-gas-price 308 | --print-address Only print the contract address 309 | --rpc-url [env: RPC_URL=] [default: http://localhost:8545] 310 | -b Do not wait for the transaction receipt 311 | --explorer-url Display transaction URL in the explorer. 312 | -h, --help Print help 313 | ``` 314 | 315 | ### `rex encode-calldata` 316 | 317 | ```Shell 318 | Encodes calldata 319 | 320 | Usage: rex encode-calldata [ARGS]... 321 | 322 | Arguments: 323 | 324 | [ARGS]... 325 | 326 | Options: 327 | -h, --help Print help 328 | ``` 329 | 330 | ### `rex decode-calldata` 331 | 332 | ```Shell 333 | Usage: rex decode-calldata 334 | 335 | Arguments: 336 | 337 | 338 | 339 | Options: 340 | -h, --help Print help 341 | ``` 342 | 343 | ## Examples 344 | 345 | A curated list of examples as GIFs. 346 | 347 | TODO 348 | -------------------------------------------------------------------------------- /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 | Or install without cloning: 31 | 32 | ```shell 33 | cargo install --git https://github.com/lambdaclass/rex --locked 34 | ``` 35 | 36 | ### Using the CLI 37 | 38 | After installing the CLI with `make cli`, run `rex` to display the help message and see the available commands. 39 | 40 | ```Shell 41 | ➜ ~ rex 42 | Usage: rex 43 | 44 | Commands: 45 | address Get either the account's address from private key, the zero address, or a random address [aliases: addr, a] 46 | autocomplete Generate shell completion scripts. 47 | balance Get the account's balance info. [aliases: bal, b] 48 | block-number Get the current block_number. [aliases: bl] 49 | call Make a call to a contract 50 | chain-id Get the network's chain id. 51 | code Returns code at a given address 52 | create-address Compute contract address given the deployer address and nonce. 53 | deploy Deploy a contract 54 | hash Get either the keccak for a given input, the zero hash, the empty string, or a random hash [aliases: h] 55 | l2 L2 specific commands. 56 | nonce Get the account's nonce. [aliases: n] 57 | receipt Get the transaction's receipt. [aliases: r] 58 | send Send a transaction 59 | sign Sign a message with a private key 60 | signer 61 | transaction Get the transaction's info. [aliases: tx, t] 62 | transfer Transfer funds to another wallet. 63 | verify-signature Verify if the signature of a message was made by an account 64 | help Print this message or the help of the given subcommand(s) 65 | 66 | Options: 67 | -h, --help Print help 68 | -V, --version Print version 69 | ``` 70 | 71 | #### Helpful operations 72 | 73 | ![ops](./assets/operations_demo.gif) 74 | 75 | #### Interacting with an Ethereum node 76 | 77 | > [!NOTE] 78 | > 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. 79 | 80 | ![eth](./assets/chain_demo.gif) 81 | 82 | #### Interacting with an ethrex L2 node 83 | 84 | TODO 85 | 86 | You can find the CLI documentation [here](cli/README.md). 87 | 88 | ## `rex` SDK 89 | 90 | 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. 91 | 92 | ### Getting Started with the SDK 93 | 94 | #### Adding the SDK to your project 95 | 96 | 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: 97 | 98 | ```toml 99 | [dependencies] 100 | rex-sdk = { git = "https://github.com/lambdaclass/rex", package = "rex-sdk", branch = "main" } 101 | ethrex-common = { git = "https://github.com/lambdaclass/ethrex", package = "ethrex-common", branch = "main" } 102 | ``` 103 | 104 | > [!TIP] 105 | > 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. 106 | 107 | 108 | 121 | 122 | #### First Steps 123 | 124 | 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). 125 | 126 | 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). 127 | 128 | *Importing the dependencies* 129 | 130 | ```Rust 131 | use ethrex_common::{Address, U256}; 132 | use rex_sdk::{ 133 | client::{EthClient, Overrides}, 134 | transfer, wait_for_transaction_receipt, 135 | }; 136 | use std::str::FromStr; 137 | ``` 138 | 139 | The following should be either part of a function or the main function. 140 | 141 | *Connecting to the node* 142 | 143 | ```Rust 144 | let rpc_url = "http://localhost:8545"; 145 | 146 | let eth_client = EthClient::new(rpc_url); 147 | ``` 148 | 149 | *Doing simple interactions (balance and nonce of an account and chain-id)* 150 | 151 | ```Rust 152 | let account_balance = eth_client.get_balance(account).await.unwrap(); 153 | 154 | let account_nonce = eth_client.get_nonce(account).await.unwrap(); 155 | 156 | let chain_id = eth_client.get_chain_id().await.unwrap(); 157 | 158 | println!("Account balance: {account_balance}"); 159 | println!("Account nonce: {account_nonce}"); 160 | println!("Chain id: {chain_id}"); 161 | ``` 162 | 163 | *Transferring funds* 164 | 165 | ```Rust 166 | let amount = U256::from_dec_str("1000000000000000000").unwrap(); // 1 ETH in wei 167 | let from = account; 168 | let to = Address::from_str("0x4852f44fd706e34cb906b399b729798665f64a83").unwrap(); 169 | 170 | let tx_hash = transfer( 171 | amount, 172 | from, 173 | to, 174 | from_private_key, 175 | ð_client, 176 | Overrides { 177 | value: Some(amount), 178 | ..Default::default() 179 | }, 180 | ) 181 | .await 182 | .unwrap(); 183 | 184 | // Wait for the transaction to be finalized 185 | wait_for_transaction_receipt(tx_hash, ð_client, 100) 186 | .await 187 | .unwrap(); 188 | ``` 189 | 190 | *Getting transfer tx hash details and receipt* 191 | 192 | ```Rust 193 | let tx_receipt = eth_client.get_transaction_receipt(tx_hash).await.unwrap(); 194 | 195 | println!("transfer tx receipt: {tx_receipt:?}"); 196 | 197 | let tx_details = eth_client.get_transaction_by_hash(tx_hash).await.unwrap(); 198 | 199 | println!("transfer tx details: {tx_details:?}"); 200 | ``` 201 | 202 | #### Full Example 203 | 204 | ```Rust 205 | use ethrex_common::{Address, U256}; 206 | use rex_sdk::{ 207 | client::{EthClient, Overrides}, 208 | transfer, wait_for_transaction_receipt, 209 | }; 210 | use std::str::FromStr; 211 | 212 | #[tokio::main] 213 | async fn main() { 214 | let rpc_url = "http://localhost:8545"; 215 | 216 | let eth_client = EthClient::new(&rpc_url); 217 | 218 | let account_balance = eth_client.get_balance(account).await.unwrap(); 219 | 220 | let account_nonce = eth_client.get_nonce(account).await.unwrap(); 221 | 222 | let chain_id = eth_client.get_chain_id().await.unwrap(); 223 | 224 | println!("Account balance: {account_balance}"); 225 | println!("Account nonce: {account_nonce}"); 226 | println!("Chain id: {chain_id}"); 227 | 228 | let amount = U256::from(1000000000000000000); // 1 ETH in wei 229 | let from = account; 230 | let to = Address::from_str("0x4852f44fd706e34cb906b399b729798665f64a83").unwrap(); 231 | 232 | let tx_hash = transfer( 233 | amount, 234 | from, 235 | to, 236 | from_private_key, 237 | ð_client, 238 | Overrides { 239 | value: Some(amount), 240 | ..Default::default() 241 | }, 242 | ) 243 | .await 244 | .unwrap(); 245 | 246 | // Wait for the transaction to be finalized 247 | wait_for_transaction_receipt(tx_hash, ð_client, 100) 248 | .await 249 | .unwrap(); 250 | 251 | let tx_receipt = eth_client.get_transaction_receipt(tx_hash).await.unwrap(); 252 | 253 | println!("transfer tx receipt: {tx_receipt:?}"); 254 | 255 | let tx_details = eth_client.get_transaction_by_hash(tx_hash).await.unwrap(); 256 | 257 | println!("transfer tx details: {tx_details:?}"); 258 | } 259 | ``` 260 | 261 | #### Running the examples 262 | 263 | > [!WARNING] 264 | > Before running the examples, 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. 265 | > The account associated to the private key must have some funds in the network you are connecting to. 266 | 267 | ```Shell 268 | cd sdk 269 | cargo run --release --example simple_usage -- --private-key --rpc-url 270 | cargo run --release --example keystore -- --private-key --rpc-url 271 | ``` 272 | 273 | > [!NOTE] 274 | > You can find the code for these examples in `sdk/examples/`. 275 | 276 | You can find the SDK documentation [here](sdk/README.md). 277 | 278 | 279 | # Security 280 | 281 | We take security seriously. If you discover a vulnerability in this project, please report it responsibly. 282 | 283 | - You can report vulnerabilities directly via the **[GitHub "Report a Vulnerability" feature](../../security/advisories/new)**. 284 | - Alternatively, send an email to **[security@lambdaclass.com](mailto:security@lambdaclass.com)**. 285 | 286 | For more details, please refer to our [Security Policy](./.github/SECURITY.md). 287 | 288 | 289 | -------------------------------------------------------------------------------- /sdk/examples/keystore/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use ethrex_common::types::TxType; 3 | use ethrex_common::{Bytes, H160, H256, U256}; 4 | use ethrex_l2_common::calldata::Value; 5 | use ethrex_l2_common::utils::get_address_from_secret_key; 6 | use ethrex_l2_rpc::signer::{LocalSigner, Signer}; 7 | use ethrex_rpc::EthClient; 8 | use ethrex_rpc::clients::Overrides; 9 | use ethrex_sdk::calldata::encode_calldata; 10 | use ethrex_sdk::{build_generic_tx, send_generic_transaction}; 11 | use keccak_hash::keccak; 12 | use reqwest::Url; 13 | use rex_sdk::deploy; 14 | use rex_sdk::{ 15 | keystore::{create_new_keystore, load_keystore_from_path}, 16 | sign::sign_hash, 17 | transfer, wait_for_transaction_receipt, 18 | }; 19 | use secp256k1::SecretKey; 20 | use std::fs::read_to_string; 21 | use std::path::PathBuf; 22 | use std::process::Command; 23 | use std::str::FromStr; 24 | 25 | #[derive(Parser)] 26 | struct ExampleArgs { 27 | #[arg( 28 | long, 29 | env = "PRIVATE_KEY", 30 | help = "The private key to derive the address from." 31 | )] 32 | private_key: String, 33 | #[arg(long, default_value = "http://localhost:8545", env = "RPC_URL")] 34 | rpc_url: Url, 35 | } 36 | 37 | #[tokio::main] 38 | async fn main() -> Result<(), Box> { 39 | let args = ExampleArgs::parse(); 40 | 41 | // 1. Download contract deps and compile contract. 42 | setup(); 43 | 44 | // 2. Create a new keystore named "RexTest" in the "ContractKeystores" directory. 45 | create_new_keystore(None, Some("RexTest"), "LambdaClass")?; 46 | 47 | // 3. Load the keystore with the password. 48 | let keystore_secret_key = load_keystore_from_path(None, "RexTest", "LambdaClass")?; 49 | let keystore_address = get_address_from_secret_key(&keystore_secret_key)?; 50 | 51 | println!("\nKeystore loaded successfully:"); 52 | println!( 53 | "\tPrivate Key: 0x{}", 54 | hex::encode(keystore_secret_key.secret_bytes()) 55 | ); 56 | println!("\tAddress: {keystore_address:#x}"); 57 | 58 | // Connect the client to a node 59 | let eth_client = EthClient::new(args.rpc_url)?; 60 | 61 | // 4. Fund the keystore account. 62 | let pk = &args 63 | .private_key 64 | .strip_prefix("0x") 65 | .unwrap_or(&args.private_key); 66 | let rich_wallet_pk = SecretKey::from_str(pk)?; 67 | let rich_wallet_address = get_address_from_secret_key(&rich_wallet_pk)?; 68 | let amount = U256::from_dec_str("1000000000000000000").expect("Failed to parse amount"); 69 | let transfer_tx_hash = transfer( 70 | amount, 71 | rich_wallet_address, 72 | keystore_address, 73 | &rich_wallet_pk, 74 | ð_client, 75 | Overrides::default(), 76 | ) 77 | .await?; 78 | 79 | let transfer_receipt = 80 | wait_for_transaction_receipt(transfer_tx_hash, ð_client, 10, true).await?; 81 | 82 | println!("\nFunds transferred successfully:"); 83 | println!("\tTransfer tx hash: {transfer_tx_hash:#x}"); 84 | println!("\tTransfer receipt: {transfer_receipt:?}"); 85 | 86 | // 5. Deploy the signer recovery example contract with the keystore account. 87 | let bytecode_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) 88 | .join("examples/keystore/contracts/solc_out") 89 | .join("RecoverSigner.bin"); 90 | let bytecode = hex::decode(read_to_string(bytecode_path)?)?; 91 | let signer = Signer::Local(LocalSigner::new(keystore_secret_key)); 92 | let (contract_tx_hash, deployed_address) = deploy( 93 | ð_client, 94 | &signer, 95 | Bytes::from(bytecode), 96 | Overrides::default(), 97 | true, 98 | ) 99 | .await?; 100 | 101 | let contract_deploy_receipt = 102 | wait_for_transaction_receipt(contract_tx_hash, ð_client, 10, true).await?; 103 | 104 | println!("\nContract deployed successfully:"); 105 | println!("\tContract deployment tx hash: {contract_tx_hash:#x}"); 106 | println!("\tContract deployment address: {deployed_address:#x}"); 107 | println!("\tContract deployment receipt: {contract_deploy_receipt:?}"); 108 | 109 | // Get the current block (for later). 110 | let from_block = eth_client.get_block_number().await?; 111 | 112 | // 6. Prepare the calldata to call the example contract. 113 | // i. Prepare a message. 114 | let message = H256::random(); 115 | let prefix = "\x19Ethereum Signed Message:\n32"; 116 | let mut hash_input = Vec::new(); 117 | hash_input.extend_from_slice(prefix.as_bytes()); 118 | hash_input.extend_from_slice(message.as_bytes()); 119 | let hash = keccak(&hash_input); 120 | 121 | // ii. Sign the hash of the message with the keystore private key. 122 | let signature = sign_hash(hash, keystore_secret_key); 123 | 124 | // iii. ABI-encode the parameters. 125 | let raw_function_signature = "recoverSigner(bytes32,bytes)"; 126 | let arguments = vec![ 127 | Value::FixedBytes(Bytes::from(message.to_fixed_bytes().to_vec())), 128 | Value::Bytes(Bytes::from(signature)), 129 | ]; 130 | let calldata = encode_calldata(raw_function_signature, &arguments).unwrap(); 131 | 132 | // 7. Prepare and send the transaction for calling the example contract. 133 | let tx = build_generic_tx( 134 | ð_client, 135 | TxType::EIP1559, 136 | deployed_address, 137 | keystore_address, 138 | calldata.into(), 139 | Overrides { 140 | value: Some(U256::from_dec_str("0")?), 141 | nonce: Some(1), 142 | chain_id: Some(9), 143 | gas_limit: Some(2000000), 144 | max_fee_per_gas: Some(2000000), 145 | max_priority_fee_per_gas: Some(20000), 146 | ..Default::default() 147 | }, 148 | ) 149 | .await?; 150 | 151 | let sent_tx_hash = send_generic_transaction(ð_client, tx, &signer).await?; 152 | 153 | let sent_tx_receipt = 154 | wait_for_transaction_receipt(sent_tx_hash, ð_client, 100, true).await?; 155 | 156 | println!("\nTx sent successfully:"); 157 | println!("\tTx hash: {sent_tx_hash:#x}"); 158 | println!("\tTx receipt: {sent_tx_receipt:?}"); 159 | 160 | // Get the new current block. 161 | let to_block = eth_client.get_block_number().await?; 162 | 163 | // 8. Get the log emitted by the contract call execution. 164 | let logs = eth_client 165 | .get_logs( 166 | from_block, 167 | to_block, 168 | deployed_address, 169 | vec![keccak("RecoveredSigner(address)")], 170 | ) 171 | .await?; 172 | 173 | println!("\tTx Logs: {:?}", logs); 174 | 175 | // 9. Compare it with the expected one. 176 | let address_bytes = &logs[0].log.data[logs[0].log.data.len() - 20..]; 177 | let recovered_address = H160::from_str(&hex::encode(address_bytes))?; 178 | assert_eq!(recovered_address, keystore_address); 179 | 180 | println!("\nAddress recovered successfully!"); 181 | println!("\tRecovered address: {recovered_address:#x}"); 182 | 183 | Ok(()) 184 | } 185 | 186 | fn setup() { 187 | download_contract_deps(); 188 | compile_contracts(); 189 | } 190 | 191 | fn download_contract_deps() { 192 | println!("Downloading contract dependencies"); 193 | 194 | let root_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 195 | 196 | let lib_path = root_path.join("examples/keystore/contracts/lib"); 197 | 198 | if !lib_path.exists() { 199 | std::fs::create_dir_all(&lib_path).expect("Failed to create lib directory"); 200 | } 201 | 202 | git_clone( 203 | "https://github.com/OpenZeppelin/openzeppelin-contracts.git", 204 | lib_path 205 | .join("openzeppelin-contracts") 206 | .to_str() 207 | .expect("Failed to get str from path"), 208 | None, 209 | true, 210 | ); 211 | 212 | println!("Contract dependencies downloaded"); 213 | } 214 | 215 | pub fn git_clone(repository_url: &str, outdir: &str, branch: Option<&str>, submodules: bool) { 216 | println!("Cloning repository: {repository_url} into {outdir}"); 217 | 218 | let mut git_cmd = Command::new("git"); 219 | 220 | let git_clone_cmd = git_cmd.arg("clone").arg(repository_url); 221 | 222 | if let Some(branch) = branch { 223 | git_clone_cmd.arg("--branch").arg(branch); 224 | } 225 | 226 | if submodules { 227 | git_clone_cmd.arg("--recurse-submodules"); 228 | } 229 | 230 | git_clone_cmd 231 | .arg(outdir) 232 | .spawn() 233 | .expect("Failed to spawn git clone command") 234 | .wait() 235 | .expect("Failed to wait for git clone command"); 236 | 237 | println!("Repository cloned successfully"); 238 | } 239 | 240 | fn compile_contracts() { 241 | println!("Compiling contracts"); 242 | 243 | let root_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 244 | 245 | let contracts_path = root_path.join("examples/keystore/contracts"); 246 | 247 | compile_contract(contracts_path, "RecoverSigner.sol", false); 248 | 249 | println!("Contracts compiled"); 250 | } 251 | 252 | pub fn compile_contract(general_contracts_path: PathBuf, contract_path: &str, runtime_bin: bool) { 253 | let bin_flag = if runtime_bin { 254 | "--bin-runtime" 255 | } else { 256 | "--bin" 257 | }; 258 | 259 | // Both the contract path and the output path are relative to where the Makefile is. 260 | if !Command::new("solc") 261 | .arg(bin_flag) 262 | .arg( 263 | "@openzeppelin/contracts=".to_string() 264 | + general_contracts_path 265 | .join("lib") 266 | .join("openzeppelin-contracts") 267 | .join("lib") 268 | .join("openzeppelin-contracts") 269 | .join("contracts") 270 | .to_str() 271 | .expect("Failed to get str from path"), 272 | ) 273 | .arg( 274 | "@openzeppelin/contracts=".to_string() 275 | + general_contracts_path 276 | .join("lib") 277 | .join("openzeppelin-contracts") 278 | .join("contracts") 279 | .to_str() 280 | .expect("Failed to get str from path"), 281 | ) 282 | .arg( 283 | general_contracts_path 284 | .join(contract_path) 285 | .to_str() 286 | .expect("Failed to get str from path"), 287 | ) 288 | .arg("--via-ir") 289 | .arg("-o") 290 | .arg( 291 | general_contracts_path 292 | .join("solc_out") 293 | .to_str() 294 | .expect("Failed to get str from path"), 295 | ) 296 | .arg("--overwrite") 297 | .arg("--allow-paths") 298 | .arg( 299 | general_contracts_path 300 | .to_str() 301 | .expect("Failed to get str from path"), 302 | ) 303 | .spawn() 304 | .expect("Failed to spawn solc command") 305 | .wait() 306 | .expect("Failed to wait for solc command") 307 | .success() 308 | { 309 | panic!("Failed to compile {contract_path}"); 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /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 ethrex_l2_common::utils::get_address_from_secret_key; 9 | use ethrex_rpc::EthClient; 10 | use ethrex_sdk::wait_for_message_proof; 11 | use rex_sdk::{ 12 | l2::{ 13 | deposit::{deposit_erc20, deposit_through_contract_call}, 14 | withdraw::{claim_erc20withdraw, claim_withdraw, withdraw, withdraw_erc20}, 15 | }, 16 | wait_for_transaction_receipt, 17 | }; 18 | use secp256k1::SecretKey; 19 | use url::Url; 20 | 21 | #[derive(Subcommand)] 22 | pub(crate) enum Command { 23 | #[clap(about = "Get the account's balance on L2.", visible_aliases = ["bal", "b"])] 24 | Balance { 25 | #[clap(flatten)] 26 | args: BalanceArgs, 27 | #[arg( 28 | default_value = "http://localhost:1729", 29 | env = "RPC_URL", 30 | help = "L2 RPC URL" 31 | )] 32 | rpc_url: Url, 33 | }, 34 | #[clap(about = "Get the current block_number.", visible_alias = "bl")] 35 | BlockNumber { 36 | #[arg( 37 | default_value = "http://localhost:1729", 38 | env = "RPC_URL", 39 | help = "L2 RPC URL" 40 | )] 41 | rpc_url: Url, 42 | }, 43 | #[clap(about = "Make a call to a contract")] 44 | Call { 45 | #[clap(flatten)] 46 | args: CallArgs, 47 | #[arg( 48 | default_value = "http://localhost:1729", 49 | env = "RPC_URL", 50 | help = "L2 RPC URL" 51 | )] 52 | rpc_url: Url, 53 | }, 54 | #[clap(about = "Get the network's chain id.")] 55 | ChainId { 56 | #[arg( 57 | short, 58 | long, 59 | default_value_t = false, 60 | help = "Display the chain id as a hex-string." 61 | )] 62 | hex: bool, 63 | #[arg(default_value = "http://localhost:1729", env = "RPC_URL")] 64 | rpc_url: Url, 65 | }, 66 | #[clap(about = "Finalize a pending withdrawal.")] 67 | ClaimWithdraw { 68 | #[clap(value_parser = parse_u256)] 69 | claimed_amount: U256, 70 | l2_withdrawal_tx_hash: H256, 71 | #[clap( 72 | long = "token-l1", 73 | help = "ERC20 token address on L1", 74 | long_help = "Specify the token address, the base token is used as default." 75 | )] 76 | token_l1: Option
, 77 | #[clap( 78 | long = "token-l2", 79 | help = "ERC20 token address on L2", 80 | long_help = "Specify the token address, it is required if you specify a token on L1.", 81 | requires("token-l1") 82 | )] 83 | token_l2: Option
, 84 | #[clap( 85 | long, 86 | short = 'c', 87 | required = false, 88 | help = "Send the request asynchronously." 89 | )] 90 | cast: bool, 91 | #[clap( 92 | long, 93 | short = 's', 94 | required = false, 95 | help = "Display only the tx hash." 96 | )] 97 | silent: bool, 98 | #[arg(value_parser = parse_private_key, env = "PRIVATE_KEY")] 99 | private_key: SecretKey, 100 | #[arg(env = "BRIDGE_ADDRESS")] 101 | bridge_address: Address, 102 | #[arg(env = "L1_RPC_URL", default_value = "http://localhost:8545")] 103 | l1_rpc_url: Url, 104 | #[arg(env = "RPC_URL", default_value = "http://localhost:1729")] 105 | rpc_url: Url, 106 | }, 107 | #[clap(about = "Deploy a contract")] 108 | Deploy { 109 | #[clap(flatten)] 110 | args: DeployArgs, 111 | #[arg( 112 | default_value = "http://localhost:1729", 113 | env = "RPC_URL", 114 | help = "L2 RPC URL" 115 | )] 116 | rpc_url: Url, 117 | }, 118 | #[clap(about = "Deposit funds into some wallet.")] 119 | Deposit { 120 | // TODO: Parse ether instead. 121 | #[clap(value_parser = parse_u256)] 122 | amount: U256, 123 | #[clap( 124 | long = "token-l1", 125 | help = "ERC20 token address on L1", 126 | long_help = "Specify the token address, the base token is used as default." 127 | )] 128 | token_l1: Option
, 129 | #[clap( 130 | long = "token-l2", 131 | help = "ERC20 token address on L2", 132 | long_help = "Specify the token address, it is required if you specify a token on L1.", 133 | requires("token-l1") 134 | )] 135 | token_l2: Option
, 136 | #[clap( 137 | long = "to", 138 | help = "Specify the wallet in which you want to deposit your funds." 139 | )] 140 | to: Option
, 141 | #[clap( 142 | long, 143 | short = 'c', 144 | required = false, 145 | help = "Send the request asynchronously." 146 | )] 147 | cast: bool, 148 | #[clap( 149 | long, 150 | short = 's', 151 | required = false, 152 | help = "Display only the tx hash." 153 | )] 154 | silent: bool, 155 | #[clap( 156 | long, 157 | short = 'e', 158 | required = false, 159 | help = "Display transaction URL in the explorer." 160 | )] 161 | explorer_url: bool, 162 | #[clap(value_parser = parse_private_key, env = "PRIVATE_KEY")] 163 | private_key: SecretKey, 164 | #[arg( 165 | env = "BRIDGE_ADDRESS", 166 | help = "Make sure you are using the correct bridge address before submitting your deposit." 167 | )] 168 | bridge_address: Address, 169 | #[arg(default_value = "http://localhost:8545", env = "L1_RPC_URL")] 170 | l1_rpc_url: Url, 171 | }, 172 | #[clap(about = "Get the account's nonce.", visible_aliases = ["n"])] 173 | Nonce { 174 | account: Address, 175 | #[arg(default_value = "http://localhost:1729", env = "RPC_URL")] 176 | rpc_url: Url, 177 | }, 178 | #[clap(about = "Get the transaction's receipt.", visible_alias = "r")] 179 | Receipt { 180 | tx_hash: H256, 181 | #[arg( 182 | default_value = "http://localhost:1729", 183 | env = "RPC_URL", 184 | help = "L2 RPC URL" 185 | )] 186 | rpc_url: Url, 187 | }, 188 | #[clap(about = "Send a transaction")] 189 | Send { 190 | #[clap(flatten)] 191 | args: SendArgs, 192 | #[arg( 193 | default_value = "http://localhost:1729", 194 | env = "RPC_URL", 195 | help = "L2 RPC URL" 196 | )] 197 | rpc_url: Url, 198 | }, 199 | #[clap(about = "Get the transaction's info.", visible_aliases = ["tx", "t"])] 200 | Transaction { 201 | tx_hash: H256, 202 | #[arg( 203 | default_value = "http://localhost:1729", 204 | env = "RPC_URL", 205 | help = "L2 RPC URL" 206 | )] 207 | rpc_url: Url, 208 | }, 209 | #[clap(about = "Transfer funds to another wallet.")] 210 | Transfer { 211 | #[clap(flatten)] 212 | args: TransferArgs, 213 | #[arg( 214 | default_value = "http://localhost:1729", 215 | env = "RPC_URL", 216 | help = "L2 RPC URL" 217 | )] 218 | rpc_url: Url, 219 | }, 220 | #[clap(about = "Withdraw funds from the wallet.")] 221 | Withdraw { 222 | // TODO: Parse ether instead. 223 | #[clap(value_parser = parse_u256)] 224 | amount: U256, 225 | #[clap(long = "nonce")] 226 | nonce: Option, 227 | #[clap( 228 | long = "token-l1", 229 | help = "ERC20 token address on L1", 230 | long_help = "Specify the token address, the base token is used as default." 231 | )] 232 | token_l1: Option
, 233 | #[clap( 234 | long = "token-l2", 235 | help = "ERC20 token address on L2", 236 | long_help = "Specify the token address, it is required if you specify a token on L1.", 237 | requires("token-l1") 238 | )] 239 | token_l2: Option
, 240 | #[clap( 241 | long, 242 | short = 'c', 243 | required = false, 244 | help = "Send the request asynchronously." 245 | )] 246 | cast: bool, 247 | #[clap( 248 | long, 249 | short = 's', 250 | required = false, 251 | help = "Display only the tx hash." 252 | )] 253 | silent: bool, 254 | #[clap( 255 | long, 256 | required = false, 257 | help = "Display transaction URL in the explorer." 258 | )] 259 | explorer_url: bool, 260 | #[arg(value_parser = parse_private_key, env = "PRIVATE_KEY")] 261 | private_key: SecretKey, 262 | #[arg( 263 | default_value = "http://localhost:1729", 264 | env = "RPC_URL", 265 | help = "L2 RPC URL" 266 | )] 267 | rpc_url: Url, 268 | }, 269 | #[clap(about = "Get the merkle proof of a L1MessageProof.")] 270 | MessageProof { 271 | message_tx_hash: H256, 272 | #[arg( 273 | default_value = "http://localhost:1729", 274 | env = "RPC_URL", 275 | help = "L2 RPC URL" 276 | )] 277 | rpc_url: Url, 278 | }, 279 | } 280 | 281 | impl Command { 282 | pub async fn run(self) -> eyre::Result<()> { 283 | match self { 284 | Command::Deposit { 285 | amount, 286 | token_l1, 287 | token_l2, 288 | to, 289 | cast, 290 | silent, 291 | explorer_url, 292 | private_key, 293 | l1_rpc_url, 294 | bridge_address, 295 | } => { 296 | let eth_client = EthClient::new(l1_rpc_url)?; 297 | let to = to.unwrap_or( 298 | get_address_from_secret_key(&private_key).map_err(|e| eyre::eyre!(e))?, 299 | ); 300 | if explorer_url { 301 | todo!("Display transaction URL in the explorer") 302 | } 303 | 304 | // Deposit through ERC20 token transfer 305 | let tx_hash = if let Some(token_l1) = token_l1 { 306 | let token_l2 = token_l2.expect( 307 | "Token address on L2 is required if token address on L1 is specified", 308 | ); 309 | let from = 310 | get_address_from_secret_key(&private_key).map_err(|e| eyre::eyre!(e))?; 311 | println!( 312 | "Depositing {amount} from {from:#x} to L2 token {token_l2:#x} using L1 token {token_l1:#x}" 313 | ); 314 | deposit_erc20( 315 | token_l1, 316 | token_l2, 317 | amount, 318 | from, 319 | private_key, 320 | ð_client, 321 | bridge_address, 322 | ) 323 | .await? 324 | } else { 325 | println!("Depositing {amount} from {to:#x} to bridge"); 326 | deposit_through_contract_call( 327 | amount, 328 | to, 329 | &private_key, 330 | bridge_address, 331 | ð_client, 332 | ) 333 | .await? 334 | }; 335 | 336 | println!("Deposit sent: {tx_hash:#x}"); 337 | 338 | if !cast { 339 | wait_for_transaction_receipt(tx_hash, ð_client, 100, silent).await?; 340 | } 341 | } 342 | Command::ClaimWithdraw { 343 | claimed_amount, 344 | l2_withdrawal_tx_hash, 345 | token_l1, 346 | token_l2, 347 | cast, 348 | silent, 349 | private_key, 350 | l1_rpc_url, 351 | rpc_url, 352 | bridge_address, 353 | } => { 354 | let from = get_address_from_secret_key(&private_key).map_err(|e| eyre::eyre!(e))?; 355 | 356 | let eth_client = EthClient::new(l1_rpc_url)?; 357 | 358 | let rollup_client = EthClient::new(rpc_url)?; 359 | 360 | let message_proof = 361 | wait_for_message_proof(&rollup_client, l2_withdrawal_tx_hash, 100).await?; 362 | 363 | let withdrawal_proof = message_proof.into_iter().next().ok_or(eyre::eyre!( 364 | "No withdrawal proof found for transaction {l2_withdrawal_tx_hash:#x}" 365 | ))?; 366 | 367 | let tx_hash = if let Some(token_l1) = token_l1 { 368 | let token_l2 = token_l2.expect( 369 | "Token address on L2 is required if token address on L1 is specified", 370 | ); 371 | claim_erc20withdraw( 372 | token_l1, 373 | token_l2, 374 | claimed_amount, 375 | private_key, 376 | ð_client, 377 | &withdrawal_proof, 378 | bridge_address, 379 | ) 380 | .await? 381 | } else { 382 | claim_withdraw( 383 | claimed_amount, 384 | from, 385 | private_key, 386 | ð_client, 387 | &withdrawal_proof, 388 | bridge_address, 389 | ) 390 | .await? 391 | }; 392 | 393 | println!("Withdrawal claim sent: {tx_hash:#x}"); 394 | 395 | if !cast { 396 | wait_for_transaction_receipt(tx_hash, ð_client, 100, silent).await?; 397 | } 398 | } 399 | Command::Withdraw { 400 | amount, 401 | nonce, 402 | token_l1, 403 | token_l2, 404 | cast, 405 | silent, 406 | explorer_url, 407 | private_key, 408 | rpc_url, 409 | } => { 410 | let from = get_address_from_secret_key(&private_key).map_err(|e| eyre::eyre!(e))?; 411 | 412 | let client = EthClient::new(rpc_url)?; 413 | 414 | if explorer_url { 415 | todo!("Display transaction URL in the explorer") 416 | } 417 | 418 | let tx_hash = if let Some(token_l1) = token_l1 { 419 | let token_l2 = token_l2.expect( 420 | "Token address on L2 is required if token address on L1 is specified", 421 | ); 422 | withdraw_erc20(amount, from, private_key, token_l1, token_l2, &client).await? 423 | } else { 424 | withdraw(amount, from, private_key, &client, nonce).await? 425 | }; 426 | 427 | println!("Withdrawal sent: {tx_hash:#x}"); 428 | 429 | if !cast { 430 | wait_for_transaction_receipt(tx_hash, &client, 100, silent).await?; 431 | } 432 | } 433 | Command::MessageProof { 434 | message_tx_hash, 435 | rpc_url, 436 | } => { 437 | let client = EthClient::new(rpc_url)?; 438 | 439 | let message_proof = wait_for_message_proof(&client, message_tx_hash, 100).await?; 440 | if message_proof.is_empty() { 441 | println!("No message proof found for transaction {message_tx_hash:#x}"); 442 | return Ok(()); 443 | }; 444 | 445 | let proof = message_proof.into_iter().next().expect("proof not found"); 446 | 447 | println!("{:?}", proof.merkle_proof); 448 | } 449 | Command::BlockNumber { rpc_url } => { 450 | Box::pin(async { EthCommand::BlockNumber { rpc_url }.run().await }).await? 451 | } 452 | Command::Transaction { tx_hash, rpc_url } => { 453 | Box::pin(async { EthCommand::Transaction { tx_hash, rpc_url }.run().await }).await? 454 | } 455 | Command::Receipt { tx_hash, rpc_url } => { 456 | Box::pin(async { EthCommand::Receipt { tx_hash, rpc_url }.run().await }).await? 457 | } 458 | Command::Balance { args, rpc_url } => { 459 | Box::pin(async { 460 | EthCommand::Balance { 461 | account: args.account, 462 | token_address: args.token_address, 463 | eth: args.eth, 464 | rpc_url, 465 | } 466 | .run() 467 | .await 468 | }) 469 | .await? 470 | } 471 | Command::Nonce { account, rpc_url } => { 472 | Box::pin(async { EthCommand::Nonce { account, rpc_url }.run().await }).await? 473 | } 474 | Command::Transfer { args, rpc_url } => { 475 | Box::pin(async { EthCommand::Transfer { args, rpc_url }.run().await }).await? 476 | } 477 | Command::Send { args, rpc_url } => { 478 | Box::pin(async { EthCommand::Send { args, rpc_url }.run().await }).await? 479 | } 480 | Command::Call { args, rpc_url } => { 481 | Box::pin(async { EthCommand::Call { args, rpc_url }.run().await }).await?; 482 | } 483 | Command::Deploy { args, rpc_url } => { 484 | Box::pin(async { EthCommand::Deploy { args, rpc_url }.run().await }).await? 485 | } 486 | Command::ChainId { hex, rpc_url } => { 487 | Box::pin(async { EthCommand::ChainId { hex, rpc_url }.run().await }).await? 488 | } 489 | }; 490 | Ok(()) 491 | } 492 | } 493 | -------------------------------------------------------------------------------- /cli/tests/tests.rs: -------------------------------------------------------------------------------- 1 | use ethrex_common::{Address, H160, H256, U256}; 2 | use ethrex_l2_common::utils::get_address_from_secret_key; 3 | use secp256k1::SecretKey; 4 | use std::process::Command; 5 | use std::str::FromStr; 6 | use std::{ 7 | fs::File, 8 | io::{BufRead, BufReader}, 9 | path::PathBuf, 10 | time::Duration, 11 | }; 12 | 13 | // 0x941e103320615d394a55708be13e45994c7d93b932b064dbcb2b511fe3254e2e 14 | const DEFAULT_L1_RICH_WALLET_PRIVATE_KEY: H256 = H256([ 15 | 0x94, 0x1e, 0x10, 0x33, 0x20, 0x61, 0x5d, 0x39, 0x4a, 0x55, 0x70, 0x8b, 0xe1, 0x3e, 0x45, 0x99, 16 | 0x4c, 0x7d, 0x93, 0xb9, 0x32, 0xb0, 0x64, 0xdb, 0xcb, 0x2b, 0x51, 0x1f, 0xe3, 0x25, 0x4e, 0x2e, 17 | ]); 18 | // 0xbcdf20249abf0ed6d944c0288fad489e33f66b3960d9e6229c1cd214ed3bbe31 19 | const DEFAULT_L2_RETURN_TRANSFER_PRIVATE_KEY: H256 = H256([ 20 | 0xbc, 0xdf, 0x20, 0x24, 0x9a, 0xbf, 0x0e, 0xd6, 0xd9, 0x44, 0xc0, 0x28, 0x8f, 0xad, 0x48, 0x9e, 21 | 0x33, 0xf6, 0x6b, 0x39, 0x60, 0xd9, 0xe6, 0x22, 0x9c, 0x1c, 0xd2, 0x14, 0xed, 0x3b, 0xbe, 0x31, 22 | ]); 23 | // 0x39b37222708e21491b9126e0969a043baa09d5a7 24 | const DEFAULT_BRIDGE_ADDRESS: Address = H160([ 25 | 0x39, 0xb3, 0x72, 0x22, 0x70, 0x8e, 0x21, 0x49, 0x1b, 0x91, 0x26, 0xe0, 0x96, 0x9a, 0x04, 0x3b, 26 | 0xaa, 0x09, 0xd5, 0xa7, 27 | ]); 28 | // 0x0007a881CD95B1484fca47615B64803dad620C8d 29 | const DEFAULT_PROPOSER_COINBASE_ADDRESS: Address = H160([ 30 | 0x00, 0x07, 0xa8, 0x81, 0xcd, 0x95, 0xb1, 0x48, 0x4f, 0xca, 0x47, 0x61, 0x5b, 0x64, 0x80, 0x3d, 31 | 0xad, 0x62, 0x0c, 0x8d, 32 | ]); 33 | 34 | const L2_GAS_COST_MAX_DELTA: U256 = U256([100_000_000_000_000, 0, 0, 0]); 35 | 36 | #[tokio::test] 37 | async fn cli_integration_test() -> Result<(), Box> { 38 | read_env_file_by_config(); 39 | 40 | let rich_wallet_private_key = l1_rich_wallet_private_key(); 41 | let transfer_return_private_key = l2_return_transfer_private_key(); 42 | let bridge_address = common_bridge_address(); 43 | 44 | test_deposit(&rich_wallet_private_key, bridge_address).await?; 45 | 46 | test_transfer(&rich_wallet_private_key, &transfer_return_private_key).await?; 47 | 48 | let withdrawals_count = std::env::var("INTEGRATION_TEST_WITHDRAW_COUNT") 49 | .map(|amount| amount.parse().expect("Invalid withdrawal amount value")) 50 | .unwrap_or(5); 51 | 52 | test_withdraws(&rich_wallet_private_key, bridge_address, withdrawals_count).await?; 53 | 54 | Ok(()) 55 | } 56 | 57 | pub fn read_env_file_by_config() { 58 | let env_file_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".env"); 59 | let Ok(env_file) = File::open(env_file_path) else { 60 | println!(".env file not found, skipping"); 61 | return; 62 | }; 63 | 64 | let reader = BufReader::new(env_file); 65 | 66 | for line in reader.lines() { 67 | let line = line.expect("Failed to read line"); 68 | if line.starts_with("#") { 69 | // Skip comments 70 | continue; 71 | }; 72 | match line.split_once('=') { 73 | Some((key, value)) => { 74 | if std::env::vars().any(|(k, _)| k == key) { 75 | continue; 76 | } 77 | unsafe { std::env::set_var(key, value) } 78 | } 79 | None => continue, 80 | }; 81 | } 82 | } 83 | 84 | fn l1_rich_wallet_private_key() -> SecretKey { 85 | let l1_rich_wallet_pk = std::env::var("INTEGRATION_TEST_L1_RICH_WALLET_PRIVATE_KEY") 86 | .map(|pk| pk.parse().expect("Invalid l1 rich wallet pk")) 87 | .unwrap_or(DEFAULT_L1_RICH_WALLET_PRIVATE_KEY); 88 | SecretKey::from_slice(l1_rich_wallet_pk.as_bytes()).unwrap() 89 | } 90 | 91 | fn l2_return_transfer_private_key() -> SecretKey { 92 | let l2_return_deposit_private_key = 93 | std::env::var("INTEGRATION_TEST_RETURN_TRANSFER_PRIVATE_KEY") 94 | .map(|pk| pk.parse().expect("Invalid l1 rich wallet pk")) 95 | .unwrap_or(DEFAULT_L2_RETURN_TRANSFER_PRIVATE_KEY); 96 | SecretKey::from_slice(l2_return_deposit_private_key.as_bytes()).unwrap() 97 | } 98 | 99 | fn common_bridge_address() -> Address { 100 | std::env::var("ETHREX_WATCHER_BRIDGE_ADDRESS") 101 | .unwrap_or(format!("{DEFAULT_BRIDGE_ADDRESS:#x}")) 102 | .parse() 103 | .expect("Invalid bridge address") 104 | } 105 | 106 | fn fees_vault() -> Address { 107 | std::env::var("INTEGRATION_TEST_PROPOSER_COINBASE_ADDRESS") 108 | .map(|address| address.parse().expect("Invalid proposer coinbase address")) 109 | .unwrap_or(DEFAULT_PROPOSER_COINBASE_ADDRESS) 110 | } 111 | 112 | async fn test_deposit( 113 | depositor_private_key: &SecretKey, 114 | bridge_address: Address, 115 | ) -> Result<(), Box> { 116 | let deposit_value = std::env::var("INTEGRATION_TEST_DEPOSIT_VALUE") 117 | .map(|value| U256::from_dec_str(&value).expect("Invalid deposit value")) 118 | .unwrap_or(U256::from(1000000000000000000000u128)); 119 | 120 | let depositor_address = get_address_from_secret_key(depositor_private_key)?; 121 | 122 | let depositor_l1_initial_balance = get_l1_balance(depositor_address)?; 123 | 124 | assert!( 125 | depositor_l1_initial_balance >= deposit_value, 126 | "L1 depositor doesn't have enough balance to deposit" 127 | ); 128 | 129 | let deposit_recipient_l2_initial_balance = get_l2_balance(depositor_address)?; 130 | 131 | let bridge_initial_balance = get_l1_balance(bridge_address)?; 132 | 133 | let fee_vault_balance_before_deposit = get_l2_balance(fees_vault())?; 134 | 135 | println!("Depositing funds from L1 to L2"); 136 | 137 | let deposit_tx_hash = deposit_l2(deposit_value, depositor_private_key, bridge_address)?; 138 | 139 | println!("Waiting for L1 deposit transaction receipt"); 140 | 141 | tokio::time::sleep(std::time::Duration::from_secs(24)).await; 142 | 143 | let deposit_tx_receipt = get_receipt(deposit_tx_hash)?; 144 | 145 | let gas_used = U256::from_str( 146 | deposit_tx_receipt 147 | .split("gas_used: ") 148 | .nth(1) 149 | .unwrap() 150 | .split(',') 151 | .next() 152 | .unwrap() 153 | .trim(), 154 | ) 155 | .unwrap(); 156 | 157 | let effective_gas_price = U256::from_str( 158 | deposit_tx_receipt 159 | .split("effective_gas_price: ") 160 | .nth(1) 161 | .unwrap() 162 | .split(',') 163 | .next() 164 | .unwrap() 165 | .trim(), 166 | ) 167 | .unwrap(); 168 | 169 | let depositor_l1_balance_after_deposit = get_l1_balance(depositor_address)?; 170 | 171 | assert_eq!( 172 | depositor_l1_balance_after_deposit, 173 | depositor_l1_initial_balance - deposit_value - gas_used * effective_gas_price, 174 | "Depositor L1 balance didn't decrease as expected after deposit" 175 | ); 176 | 177 | let bridge_balance_after_deposit = get_l1_balance(bridge_address)?; 178 | 179 | assert_eq!( 180 | bridge_balance_after_deposit, 181 | bridge_initial_balance + deposit_value, 182 | "Bridge balance didn't increase as expected after deposit" 183 | ); 184 | 185 | let deposit_recipient_l2_balance_after_deposit = get_l2_balance(depositor_address)?; 186 | 187 | assert_eq!( 188 | deposit_recipient_l2_balance_after_deposit, 189 | deposit_recipient_l2_initial_balance + deposit_value, 190 | "Deposit recipient L2 balance didn't increase as expected after deposit" 191 | ); 192 | 193 | let fee_vault_balance_after_deposit = get_l2_balance(fees_vault())?; 194 | 195 | assert_eq!( 196 | fee_vault_balance_after_deposit, fee_vault_balance_before_deposit, 197 | "Fee vault balance should not change after deposit" 198 | ); 199 | 200 | Ok(()) 201 | } 202 | 203 | async fn test_transfer( 204 | transferer_private_key: &SecretKey, 205 | returnerer_private_key: &SecretKey, 206 | ) -> Result<(), Box> { 207 | println!("Transferring funds on L2"); 208 | let transfer_value = std::env::var("INTEGRATION_TEST_TRANSFER_VALUE") 209 | .map(|value| U256::from_dec_str(&value).expect("Invalid transfer value")) 210 | .unwrap_or(U256::from(100000000000000000000u128)); 211 | let returner_address = get_address_from_secret_key(returnerer_private_key)?; 212 | let transferer_address = get_address_from_secret_key(transferer_private_key)?; 213 | 214 | perform_transfer(transferer_private_key, returner_address, transfer_value).await?; 215 | 216 | // Only return 99% of the transfer, other amount is for fees 217 | let return_amount = (transfer_value * 99) / 100; 218 | 219 | perform_transfer(returnerer_private_key, transferer_address, return_amount).await?; 220 | 221 | Ok(()) 222 | } 223 | 224 | async fn perform_transfer( 225 | transferer_private_key: &SecretKey, 226 | transfer_recipient_address: Address, 227 | transfer_value: U256, 228 | ) -> Result<(), Box> { 229 | let transferer_address = get_address_from_secret_key(transferer_private_key)?; 230 | 231 | let transferer_initial_l2_balance = get_l2_balance(transferer_address)?; 232 | 233 | assert!( 234 | transferer_initial_l2_balance >= transfer_value, 235 | "L2 transferer doesn't have enough balance to transfer" 236 | ); 237 | 238 | let transfer_recipient_initial_balance = get_l2_balance(transfer_recipient_address)?; 239 | 240 | let _ = transfer( 241 | transfer_value, 242 | transfer_recipient_address, 243 | transferer_private_key, 244 | )?; 245 | 246 | tokio::time::sleep(std::time::Duration::from_secs(12)).await; 247 | 248 | let recoverable_fees_vault_balance = get_l2_balance(fees_vault())?; 249 | 250 | println!("Recoverable Fees Balance: {recoverable_fees_vault_balance}",); 251 | 252 | println!("Checking balances on L2 after transfer"); 253 | 254 | let transferer_l2_balance_after_transfer = get_l2_balance(transferer_address)?; 255 | 256 | assert!( 257 | (transferer_initial_l2_balance - transfer_value) 258 | .abs_diff(transferer_l2_balance_after_transfer) 259 | < L2_GAS_COST_MAX_DELTA, 260 | "L2 transferer balance didn't decrease as expected after transfer. Gas costs were {}/{L2_GAS_COST_MAX_DELTA}", 261 | (transferer_initial_l2_balance - transfer_value) 262 | .abs_diff(transferer_l2_balance_after_transfer) 263 | ); 264 | 265 | let transfer_recipient_l2_balance_after_transfer = get_l2_balance(transfer_recipient_address)?; 266 | 267 | assert_eq!( 268 | transfer_recipient_l2_balance_after_transfer, 269 | transfer_recipient_initial_balance + transfer_value, 270 | "L2 transfer recipient balance didn't increase as expected after transfer" 271 | ); 272 | 273 | Ok(()) 274 | } 275 | 276 | fn get_l1_balance(address: Address) -> Result> { 277 | let output = Command::new("rex") 278 | .arg("balance") 279 | .arg(format!("{:#x}", address)) 280 | .output() 281 | .unwrap(); 282 | 283 | if !output.status.success() { 284 | let stderr = String::from_utf8_lossy(&output.stderr); 285 | panic!("Error getting balance: {stderr}"); 286 | } 287 | 288 | let str = String::from_utf8(output.stdout).unwrap(); 289 | Ok(U256::from_dec_str(str.trim()).unwrap()) 290 | } 291 | 292 | fn get_l2_balance(address: Address) -> Result> { 293 | let output = Command::new("rex") 294 | .arg("l2") 295 | .arg("balance") 296 | .arg(format!("{:#x}", address)) 297 | .output() 298 | .unwrap(); 299 | 300 | if !output.status.success() { 301 | let stderr = String::from_utf8_lossy(&output.stderr); 302 | panic!("Error getting balance: {stderr}"); 303 | } 304 | 305 | let str = String::from_utf8(output.stdout).unwrap(); 306 | Ok(U256::from_dec_str(str.trim()).unwrap()) 307 | } 308 | 309 | fn deposit_l2( 310 | amount: U256, 311 | depositor_private_key: &SecretKey, 312 | bridge_address: Address, 313 | ) -> Result> { 314 | let output = Command::new("rex") 315 | .arg("l2") 316 | .arg("deposit") 317 | .arg(format!("{}", amount)) 318 | .arg(depositor_private_key.display_secret().to_string()) 319 | .arg(format!("{:#x}", bridge_address)) 320 | .output() 321 | .unwrap(); 322 | 323 | if !output.status.success() { 324 | let stderr = String::from_utf8_lossy(&output.stderr); 325 | panic!("Error depositing to l2: {stderr}"); 326 | } 327 | 328 | let output = String::from_utf8(output.stdout).unwrap(); 329 | 330 | let hash_line = output 331 | .lines() 332 | .find(|line| line.contains("Deposit sent: ")) 333 | .unwrap(); 334 | 335 | let hash = hash_line.strip_prefix("Deposit sent: ").unwrap().trim(); 336 | 337 | Ok(H256::from_str(hash).unwrap()) 338 | } 339 | 340 | fn get_receipt(tx_hash: H256) -> Result> { 341 | let output = Command::new("rex") 342 | .arg("receipt") 343 | .arg(format!("{:#x}", tx_hash)) 344 | .output() 345 | .unwrap(); 346 | 347 | if !output.status.success() { 348 | let stderr = String::from_utf8_lossy(&output.stderr); 349 | println!("{}", String::from_utf8(output.stdout).unwrap()); 350 | panic!("Error getting receipt: {stderr}"); 351 | } 352 | 353 | Ok(String::from_utf8(output.stdout).unwrap()) 354 | } 355 | 356 | fn get_l2_receipt(tx_hash: H256) -> Result> { 357 | let output = Command::new("rex") 358 | .arg("l2") 359 | .arg("receipt") 360 | .arg(format!("{:#x}", tx_hash)) 361 | .output() 362 | .unwrap(); 363 | 364 | if !output.status.success() { 365 | let stderr = String::from_utf8_lossy(&output.stderr); 366 | println!("{}", String::from_utf8(output.stdout).unwrap()); 367 | panic!("Error getting receipt: {stderr}"); 368 | } 369 | 370 | Ok(String::from_utf8(output.stdout).unwrap()) 371 | } 372 | 373 | fn transfer( 374 | transfer_value: U256, 375 | transfer_recipient_address: Address, 376 | transferer_private_key: &SecretKey, 377 | ) -> Result> { 378 | let output = Command::new("rex") 379 | .arg("l2") 380 | .arg("transfer") 381 | .arg(format!("{}", transfer_value)) 382 | .arg(format!("{:#x}", transfer_recipient_address)) 383 | .arg(transferer_private_key.display_secret().to_string()) 384 | .output() 385 | .unwrap(); 386 | 387 | if !output.status.success() { 388 | let stderr = String::from_utf8_lossy(&output.stderr); 389 | panic!("Error depositing to l2: {stderr}"); 390 | } 391 | 392 | let str = String::from_utf8(output.stdout).unwrap(); 393 | 394 | let hash_line = str.lines().next().unwrap(); 395 | 396 | let hash = hash_line.trim(); 397 | 398 | Ok(H256::from_str(hash).unwrap()) 399 | } 400 | 401 | async fn test_withdraws( 402 | withdrawer_private_key: &SecretKey, 403 | bridge_address: Address, 404 | n: u64, 405 | ) -> Result<(), Box> { 406 | // Withdraw funds from L2 to L1 407 | let withdrawer_address = get_address_from_secret_key(withdrawer_private_key)?; 408 | let withdraw_value = std::env::var("INTEGRATION_TEST_WITHDRAW_VALUE") 409 | .map(|value| U256::from_dec_str(&value).expect("Invalid withdraw value")) 410 | .unwrap_or(U256::from(100000000000000000000u128)); 411 | 412 | println!("Checking balances on L1 and L2 before withdrawal"); 413 | 414 | let withdrawer_l2_balance_before_withdrawal = get_l2_balance(withdrawer_address)?; 415 | 416 | assert!( 417 | withdrawer_l2_balance_before_withdrawal >= withdraw_value, 418 | "L2 withdrawer doesn't have enough balance to withdraw" 419 | ); 420 | 421 | let bridge_balance_before_withdrawal = get_l1_balance(bridge_address)?; 422 | 423 | assert!( 424 | bridge_balance_before_withdrawal >= withdraw_value, 425 | "L1 bridge doesn't have enough balance to withdraw" 426 | ); 427 | 428 | let withdrawer_l1_balance_before_withdrawal = get_l1_balance(withdrawer_address)?; 429 | 430 | println!("Withdrawing funds from L2 to L1"); 431 | 432 | let mut withdraw_txs = vec![]; 433 | let mut receipts = vec![]; 434 | 435 | for x in 1..n + 1 { 436 | println!("Sending withdraw {x}/{n}"); 437 | let withdraw_tx = withdraw(withdraw_value, withdrawer_private_key)?; 438 | 439 | withdraw_txs.push(withdraw_tx); 440 | 441 | let withdraw_tx_receipt = get_l2_receipt(withdraw_tx)?; 442 | receipts.push(withdraw_tx_receipt); 443 | } 444 | 445 | println!("Checking balances on L1 and L2 after withdrawal"); 446 | 447 | let withdrawer_l2_balance_after_withdrawal = get_l2_balance(withdrawer_address)?; 448 | 449 | assert!( 450 | (withdrawer_l2_balance_before_withdrawal - withdraw_value * n) 451 | .abs_diff(withdrawer_l2_balance_after_withdrawal) 452 | < L2_GAS_COST_MAX_DELTA * n, 453 | "Withdrawer L2 balance didn't decrease as expected after withdrawal" 454 | ); 455 | 456 | let withdrawer_l1_balance_after_withdrawal = get_l1_balance(withdrawer_address)?; 457 | 458 | assert_eq!( 459 | withdrawer_l1_balance_after_withdrawal, withdrawer_l1_balance_before_withdrawal, 460 | "Withdrawer L1 balance should not change after withdrawal" 461 | ); 462 | 463 | println!("Waiting for the withdrawal to be included in some batch and verified"); 464 | 465 | // TODO: we should use `wait_for_verified_proof` instead 466 | tokio::time::sleep(Duration::from_secs(60)).await; 467 | 468 | // We need to wait for all the txs to be included in some batch 469 | let mut proofs = vec![]; 470 | for (i, tx) in withdraw_txs.clone().into_iter().enumerate() { 471 | println!("Getting withdrawal proof {}/{n}", i + 1); 472 | let message_proof = get_l2_message_proof(tx).expect("no l1 messages in withdrawal"); 473 | proofs.push(message_proof); 474 | } 475 | 476 | let mut withdraw_claim_txs_receipts = vec![]; 477 | 478 | for (i, tx) in withdraw_txs.clone().into_iter().enumerate() { 479 | println!("Claiming withdrawal on L1 {}/{n}", i + 1); 480 | 481 | let withdraw_claim_tx = 482 | claim_withdraw(withdraw_value, tx, withdrawer_private_key, bridge_address)?; 483 | let withdraw_claim_tx_receipt = get_receipt(withdraw_claim_tx)?; 484 | 485 | withdraw_claim_txs_receipts.push(withdraw_claim_tx_receipt); 486 | } 487 | 488 | println!("Checking balances on L1 and L2 after claim"); 489 | 490 | let withdrawer_l1_balance_after_claim = get_l1_balance(withdrawer_address)?; 491 | 492 | let mut gas_used_value = U256::zero(); 493 | for receipt in withdraw_claim_txs_receipts { 494 | let gas_used = U256::from_str( 495 | receipt 496 | .split("gas_used: ") 497 | .nth(1) 498 | .unwrap() 499 | .split(',') 500 | .next() 501 | .unwrap() 502 | .trim(), 503 | ) 504 | .unwrap(); 505 | 506 | let effective_gas_price = U256::from_str( 507 | receipt 508 | .split("effective_gas_price: ") 509 | .nth(1) 510 | .unwrap() 511 | .split(',') 512 | .next() 513 | .unwrap() 514 | .trim(), 515 | ) 516 | .unwrap(); 517 | 518 | gas_used_value += gas_used * effective_gas_price; 519 | } 520 | 521 | assert_eq!( 522 | withdrawer_l1_balance_after_claim, 523 | withdrawer_l1_balance_after_withdrawal + withdraw_value * n - gas_used_value, 524 | "Withdrawer L1 balance wasn't updated as expected after claim" 525 | ); 526 | 527 | let withdrawer_l2_balance_after_claim = get_l2_balance(withdrawer_address)?; 528 | 529 | assert_eq!( 530 | withdrawer_l2_balance_after_claim, withdrawer_l2_balance_after_withdrawal, 531 | "Withdrawer L2 balance should not change after claim" 532 | ); 533 | 534 | let bridge_balance_after_withdrawal = get_l1_balance(bridge_address)?; 535 | 536 | assert_eq!( 537 | bridge_balance_after_withdrawal, 538 | bridge_balance_before_withdrawal - withdraw_value * n, 539 | "Bridge balance didn't decrease as expected after withdrawal" 540 | ); 541 | 542 | Ok(()) 543 | } 544 | 545 | fn withdraw( 546 | withdraw_amount: U256, 547 | withdrawer_private_key: &SecretKey, 548 | ) -> Result> { 549 | let output = Command::new("rex") 550 | .arg("l2") 551 | .arg("withdraw") 552 | .arg(format!("{}", withdraw_amount)) 553 | .arg(withdrawer_private_key.display_secret().to_string()) 554 | .output() 555 | .unwrap(); 556 | 557 | if !output.status.success() { 558 | let stderr = String::from_utf8_lossy(&output.stderr); 559 | panic!("Error depositing to l2: {stderr}"); 560 | } 561 | 562 | let str = String::from_utf8(output.stdout).unwrap(); 563 | 564 | let hash_line = str 565 | .lines() 566 | .find(|line| line.contains("Withdrawal sent: ")) 567 | .unwrap(); 568 | 569 | let hash = hash_line.strip_prefix("Withdrawal sent: ").unwrap().trim(); 570 | 571 | Ok(H256::from_str(hash).unwrap()) 572 | } 573 | 574 | fn get_l2_message_proof(tx_hash: H256) -> Result, Box> { 575 | let output = Command::new("rex") 576 | .arg("l2") 577 | .arg("message-proof") 578 | .arg(format!("{:#x}", tx_hash)) 579 | .output() 580 | .unwrap(); 581 | 582 | if !output.status.success() { 583 | let stderr = String::from_utf8_lossy(&output.stderr); 584 | panic!("Error depositing to l2: {stderr}"); 585 | } 586 | let str = String::from_utf8(output.stdout).unwrap(); 587 | 588 | let hash = str 589 | .trim() 590 | .trim_start_matches('[') 591 | .trim_end_matches(']') 592 | .trim(); 593 | 594 | let Ok(path) = H256::from_str(hash) else { 595 | return Ok(vec![]); 596 | }; 597 | 598 | Ok(vec![path]) 599 | } 600 | 601 | fn claim_withdraw( 602 | amount: U256, 603 | tx_hash: H256, 604 | private_key: &SecretKey, 605 | bridge_address: Address, 606 | ) -> Result> { 607 | let output = Command::new("rex") 608 | .arg("l2") 609 | .arg("claim-withdraw") 610 | .arg(format!("{}", amount)) 611 | .arg(format!("{:#x}", tx_hash)) 612 | .arg(private_key.display_secret().to_string()) 613 | .arg(format!("{:#x}", bridge_address)) 614 | .output() 615 | .unwrap(); 616 | 617 | if !output.status.success() { 618 | let stderr = String::from_utf8_lossy(&output.stderr); 619 | panic!("Error depositing to l2: {stderr}"); 620 | } 621 | 622 | let str = String::from_utf8(output.stdout).unwrap(); 623 | 624 | let hash_line = str 625 | .lines() 626 | .find(|line| line.contains("Withdrawal claim sent: ")) 627 | .unwrap(); 628 | 629 | let hash = hash_line 630 | .strip_prefix("Withdrawal claim sent: ") 631 | .unwrap() 632 | .trim(); 633 | 634 | Ok(H256::from_str(hash).unwrap()) 635 | } 636 | -------------------------------------------------------------------------------- /cli/src/cli.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::l2; 2 | use crate::utils::{parse_contract_creation, parse_func_call, parse_hex, parse_hex_string}; 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::types::TxType; 10 | use ethrex_common::{Address, Bytes, H256, H520}; 11 | use ethrex_l2_common::calldata::Value; 12 | use ethrex_l2_common::utils::get_address_from_secret_key; 13 | use ethrex_l2_rpc::signer::{LocalSigner, Signer}; 14 | use ethrex_rpc::EthClient; 15 | use ethrex_rpc::clients::Overrides; 16 | use ethrex_rpc::types::block_identifier::{BlockIdentifier, BlockTag}; 17 | use ethrex_sdk::calldata::decode_calldata; 18 | use ethrex_sdk::{build_generic_tx, create2_deploy_from_bytecode, send_generic_transaction}; 19 | use ethrex_sdk::{compile_contract, git_clone}; 20 | use keccak_hash::keccak; 21 | use rex_sdk::client::eth::get_token_balance; 22 | use rex_sdk::create::{ 23 | DETERMINISTIC_DEPLOYER, brute_force_create2, compute_create_address, compute_create2_address, 24 | }; 25 | use rex_sdk::sign::{get_address_from_message_and_signature, sign_hash}; 26 | use rex_sdk::utils::to_checksum_address; 27 | use rex_sdk::{balance_in_eth, deploy, transfer, wait_for_transaction_receipt}; 28 | use secp256k1::SecretKey; 29 | use std::io::{self, Write}; 30 | use std::path::{Path, PathBuf}; 31 | use url::Url; 32 | 33 | pub const VERSION_STRING: &str = env!("CARGO_PKG_VERSION"); 34 | 35 | pub async fn start() -> eyre::Result<()> { 36 | let CLI { command } = CLI::parse(); 37 | command.run().await 38 | } 39 | 40 | #[allow(clippy::upper_case_acronyms)] 41 | #[derive(Parser)] 42 | #[command(name="rex", author, version=VERSION_STRING, about, long_about = None)] 43 | pub(crate) struct CLI { 44 | #[command(subcommand)] 45 | command: Command, 46 | } 47 | 48 | #[derive(Subcommand)] 49 | pub(crate) enum Command { 50 | #[clap( 51 | about = "Get either the account's address from private key, the zero address, or a random address", 52 | visible_aliases = ["addr", "a"] 53 | )] 54 | Address { 55 | #[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.")] 56 | from_private_key: Option, 57 | #[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.")] 58 | zero: bool, 59 | #[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.")] 60 | random: bool, 61 | }, 62 | #[clap(subcommand, about = "Generate shell completion scripts.")] 63 | Autocomplete(autocomplete::Command), 64 | #[clap(about = "Get the account's balance info.", visible_aliases = ["bal", "b"])] 65 | Balance { 66 | account: Address, 67 | #[clap( 68 | long = "token", 69 | conflicts_with = "eth", 70 | help = "Specify the token address, the ETH is used as default." 71 | )] 72 | token_address: Option
, 73 | #[arg( 74 | long = "eth", 75 | required = false, 76 | default_value_t = false, 77 | conflicts_with = "token_address", 78 | help = "Display the balance in ETH." 79 | )] 80 | eth: bool, 81 | #[arg(default_value = "http://localhost:8545", env = "RPC_URL")] 82 | rpc_url: Url, 83 | }, 84 | #[clap(about = "Get the current block_number.", visible_alias = "bl")] 85 | BlockNumber { 86 | #[arg(default_value = "http://localhost:8545", env = "RPC_URL")] 87 | rpc_url: Url, 88 | }, 89 | #[clap(about = "Make a call to a contract")] 90 | Call { 91 | #[clap(flatten)] 92 | args: CallArgs, 93 | #[arg(long, default_value = "http://localhost:8545", env = "RPC_URL")] 94 | rpc_url: Url, 95 | }, 96 | #[clap(about = "Get the network's chain id.")] 97 | ChainId { 98 | #[arg( 99 | long, 100 | default_value_t = false, 101 | help = "Display the chain id as a hex-string." 102 | )] 103 | hex: bool, 104 | #[arg(default_value = "http://localhost:8545", env = "RPC_URL")] 105 | rpc_url: Url, 106 | }, 107 | #[clap(about = "Returns code at a given address")] 108 | Code { 109 | address: Address, 110 | #[arg( 111 | short = 'B', 112 | long = "block", 113 | required = false, 114 | default_value_t = String::from("latest"), 115 | help = "defaultBlock parameter: can be integer block number, 'earliest', 'finalized', 'safe', 'latest' or 'pending'" 116 | )] 117 | block: String, 118 | #[arg(default_value = "http://localhost:8545", env = "RPC_URL")] 119 | rpc_url: Url, 120 | }, 121 | #[clap(about = "Compute contract address given the deployer address and nonce.")] 122 | CreateAddress { 123 | #[arg(help = "Deployer address.")] 124 | deployer: Address, 125 | #[arg(short = 'n', long, help = "Deployer Nonce. Latest by default.")] 126 | nonce: Option, 127 | #[arg(long, default_value = "http://localhost:8545", env = "RPC_URL")] 128 | rpc_url: Url, 129 | }, 130 | Create2Address { 131 | #[arg( 132 | short = 'd', 133 | long, 134 | help = "Deployer address. Default is Mainnet Deterministic Deployer", 135 | default_value = DETERMINISTIC_DEPLOYER 136 | )] 137 | deployer: Address, 138 | #[arg( 139 | short = 'i', 140 | long, 141 | help = "Initcode of the contract to deploy.", 142 | required_unless_present_any = ["init_code_hash"], 143 | conflicts_with_all = ["init_code_hash"] 144 | )] 145 | init_code: Option, 146 | #[arg( 147 | long, 148 | help = "Hash of the initcode (keccak256).", 149 | required_unless_present_any = ["init_code"], 150 | conflicts_with_all = ["init_code"] 151 | )] 152 | init_code_hash: Option, 153 | #[arg(short = 's', long, help = "Salt for CREATE2 opcode")] 154 | salt: Option, 155 | #[arg( 156 | long, 157 | required_unless_present_any = ["salt", "ends", "contains"], 158 | help = "Address must begin with this hex prefix.", 159 | value_parser = parse_hex_string, 160 | )] 161 | begins: Option, 162 | #[arg( 163 | long, 164 | required_unless_present_any = ["salt", "begins", "contains"], 165 | help = "Address must end with this hex suffix.", 166 | value_parser = parse_hex_string, 167 | )] 168 | ends: Option, 169 | #[arg( 170 | long, 171 | required_unless_present_any = ["salt", "begins", "ends"], 172 | help = "Address must contain this hex substring.", 173 | value_parser = parse_hex_string, 174 | )] 175 | contains: Option, 176 | #[arg( 177 | long, 178 | help = "Make the address search case sensitive when using begins, ends, or contains.", 179 | default_value_t = false, 180 | conflicts_with_all = ["salt"], 181 | )] 182 | case_sensitive: bool, 183 | #[arg( 184 | long, 185 | help = "Number of threads to use for brute-forcing. Defaults to the number of logical CPUs.", 186 | default_value_t = rayon::current_num_threads(), 187 | conflicts_with_all = ["salt"], 188 | )] 189 | threads: usize, 190 | }, 191 | #[clap(about = "Deploy a contract")] 192 | Deploy { 193 | #[clap(flatten)] 194 | args: DeployArgs, 195 | #[arg(long, default_value = "http://localhost:8545", env = "RPC_URL")] 196 | rpc_url: Url, 197 | }, 198 | #[clap( 199 | about = "Get either the keccak for a given input, the zero hash, the empty string, or a random hash", 200 | visible_alias = "h" 201 | )] 202 | Hash { 203 | #[arg(long, value_parser = parse_hex, conflicts_with_all = ["zero", "random", "string"], required_unless_present_any = ["zero", "random", "string"], help = "The input to hash.")] 204 | input: Option, 205 | #[arg(short, long, action = ArgAction::SetTrue, conflicts_with_all = ["input", "random", "string"], required_unless_present_any = ["input", "random", "string"], help = "The zero hash.")] 206 | zero: bool, 207 | #[arg(short, long, action = ArgAction::SetTrue, conflicts_with_all = ["input", "zero", "string"], required_unless_present_any = ["input", "zero", "string"], help = "A random hash.")] 208 | random: bool, 209 | #[arg(short, long, action = ArgAction::SetTrue, conflicts_with_all = ["input", "zero", "random"], required_unless_present_any = ["input", "zero", "random"], help = "Hash of empty string")] 210 | string: bool, 211 | }, 212 | #[clap(subcommand, about = "L2 specific commands.")] 213 | L2(l2::Command), 214 | #[clap(about = "Get the account's nonce.", visible_aliases = ["n"])] 215 | Nonce { 216 | account: Address, 217 | #[arg(default_value = "http://localhost:8545", env = "RPC_URL")] 218 | rpc_url: Url, 219 | }, 220 | #[clap(about = "Get the transaction's receipt.", visible_alias = "r")] 221 | Receipt { 222 | tx_hash: H256, 223 | #[arg(default_value = "http://localhost:8545", env = "RPC_URL")] 224 | rpc_url: Url, 225 | }, 226 | #[clap(about = "Send a transaction")] 227 | Send { 228 | #[clap(flatten)] 229 | args: SendArgs, 230 | #[arg(long, default_value = "http://localhost:8545", env = "RPC_URL")] 231 | rpc_url: Url, 232 | }, 233 | #[clap(about = "Sign a message with a private key")] 234 | Sign { 235 | #[arg(value_parser = parse_hex, help = "Message to be signed with the private key.")] 236 | msg: Bytes, 237 | #[arg(value_parser = parse_private_key, env = "PRIVATE_KEY", help = "The private key to sign the message.")] 238 | private_key: SecretKey, 239 | }, 240 | Signer { 241 | #[arg(value_parser = parse_hex)] 242 | message: Bytes, 243 | #[arg(value_parser = parse_hex)] 244 | signature: Bytes, 245 | }, 246 | #[clap(about = "Get the transaction's info.", visible_aliases = ["tx", "t"])] 247 | Transaction { 248 | tx_hash: H256, 249 | #[arg(default_value = "http://localhost:8545", env = "RPC_URL")] 250 | rpc_url: Url, 251 | }, 252 | #[clap(about = "Transfer funds to another wallet.")] 253 | Transfer { 254 | #[clap(flatten)] 255 | args: TransferArgs, 256 | #[arg(default_value = "http://localhost:8545", env = "RPC_URL")] 257 | rpc_url: Url, 258 | }, 259 | #[clap(about = "Verify if the signature of a message was made by an account")] 260 | VerifySignature { 261 | #[arg(value_parser = parse_hex)] 262 | message: Bytes, 263 | #[arg(value_parser = parse_hex)] 264 | signature: Bytes, 265 | address: Address, 266 | }, 267 | #[clap(about = "Encodes calldata")] 268 | EncodeCalldata { 269 | signature: String, 270 | #[clap(required = false)] 271 | args: Vec, 272 | }, 273 | #[clap(about = "Decodes calldata")] 274 | DecodeCalldata { 275 | signature: String, 276 | #[arg(value_parser = parse_hex)] 277 | data: Bytes, 278 | }, 279 | } 280 | 281 | impl Command { 282 | pub async fn run(self) -> eyre::Result<()> { 283 | match self { 284 | Command::L2(cmd) => cmd.run().await?, 285 | Command::Autocomplete(cmd) => cmd.run()?, 286 | Command::Balance { 287 | account, 288 | token_address, 289 | eth, 290 | rpc_url, 291 | } => { 292 | let eth_client = EthClient::new(rpc_url)?; 293 | let account_balance = if let Some(token_address) = token_address { 294 | get_token_balance(ð_client, account, token_address).await? 295 | } else { 296 | eth_client 297 | .get_balance(account, BlockIdentifier::Tag(BlockTag::Latest)) 298 | .await? 299 | }; 300 | 301 | println!("{}", balance_in_eth(eth, account_balance)); 302 | } 303 | Command::BlockNumber { rpc_url } => { 304 | let eth_client = EthClient::new(rpc_url)?; 305 | 306 | let block_number = eth_client.get_block_number().await?; 307 | 308 | println!("{block_number}"); 309 | } 310 | Command::CreateAddress { 311 | deployer, 312 | nonce, 313 | rpc_url, 314 | } => { 315 | let nonce = nonce.unwrap_or( 316 | EthClient::new(rpc_url)? 317 | .get_nonce(deployer, BlockIdentifier::Tag(BlockTag::Latest)) 318 | .await?, 319 | ); 320 | 321 | println!("Address: {:#x}", compute_create_address(deployer, nonce)) 322 | } 323 | Command::Create2Address { 324 | deployer, 325 | init_code, 326 | salt, 327 | init_code_hash, 328 | begins, 329 | ends, 330 | contains, 331 | case_sensitive, 332 | threads, 333 | } => { 334 | let init_code_hash = init_code_hash 335 | .or_else(|| init_code.as_ref().map(keccak)) 336 | .ok_or_else(|| eyre::eyre!("init_code_hash and init_code are both None"))?; 337 | 338 | let (salt, contract_address) = match salt { 339 | Some(salt) => { 340 | let contract_address = 341 | compute_create2_address(deployer, init_code_hash, salt); 342 | (salt, contract_address) 343 | } 344 | None => { 345 | // If salt is not provided, search for a salt that matches the criteria set by the user. 346 | println!("\nComputing Create2 Address with {threads} threads..."); 347 | io::stdout().flush().ok(); 348 | 349 | let start = std::time::Instant::now(); 350 | let (salt, contract_address) = brute_force_create2( 351 | deployer, 352 | init_code_hash, 353 | begins, 354 | ends, 355 | contains, 356 | case_sensitive, 357 | ); 358 | let duration = start.elapsed(); 359 | println!("Generated in: {duration:.2?}."); 360 | (salt, contract_address) 361 | } 362 | }; 363 | 364 | let contract_address = to_checksum_address(&format!("{contract_address:x}")); 365 | 366 | println!("\nSalt: {salt:#x}"); 367 | println!("\nAddress: 0x{contract_address}"); 368 | } 369 | Command::Transaction { tx_hash, rpc_url } => { 370 | let eth_client = EthClient::new(rpc_url)?; 371 | 372 | let tx = eth_client 373 | .get_transaction_by_hash(tx_hash) 374 | .await? 375 | .ok_or(eyre::Error::msg("Not found"))?; 376 | 377 | println!("{tx:?}"); 378 | } 379 | Command::Receipt { tx_hash, rpc_url } => { 380 | let eth_client = EthClient::new(rpc_url)?; 381 | 382 | let receipt = eth_client 383 | .get_transaction_receipt(tx_hash) 384 | .await? 385 | .ok_or(eyre::Error::msg("Not found"))?; 386 | 387 | println!("{:x?}", receipt.tx_info); 388 | } 389 | Command::Nonce { account, rpc_url } => { 390 | let eth_client = EthClient::new(rpc_url)?; 391 | 392 | let nonce = eth_client 393 | .get_nonce(account, BlockIdentifier::Tag(BlockTag::Latest)) 394 | .await?; 395 | 396 | println!("{nonce}"); 397 | } 398 | Command::Address { 399 | from_private_key, 400 | zero, 401 | random, 402 | } => { 403 | let address = if let Some(private_key) = from_private_key { 404 | get_address_from_secret_key(&private_key).map_err(|e| eyre::eyre!(e))? 405 | } else if zero { 406 | Address::zero() 407 | } else if random { 408 | Address::random() 409 | } else { 410 | return Err(eyre::Error::msg("No option provided")); 411 | }; 412 | 413 | println!("{address:#x}"); 414 | } 415 | Command::Hash { 416 | input, 417 | zero, 418 | random, 419 | string, 420 | } => { 421 | let hash = if let Some(input) = input { 422 | keccak(&input) 423 | } else if zero { 424 | H256::zero() 425 | } else if random { 426 | H256::random() 427 | } else if string { 428 | keccak(b"") 429 | } else { 430 | return Err(eyre::Error::msg("No option provided")); 431 | }; 432 | 433 | println!("{hash:#x}"); 434 | } 435 | Command::Signer { message, signature } => { 436 | let signer = get_address_from_message_and_signature(message, signature)?; 437 | 438 | println!("{signer:x?}"); 439 | } 440 | Command::Transfer { args, rpc_url } => { 441 | if args.token_address.is_some() { 442 | todo!("Handle ERC20 transfers") 443 | } 444 | 445 | if args.explorer_url { 446 | todo!("Display transaction URL in the explorer") 447 | } 448 | 449 | let from = 450 | get_address_from_secret_key(&args.private_key).map_err(|e| eyre::eyre!(e))?; 451 | 452 | let client = EthClient::new(rpc_url)?; 453 | 454 | let tx_hash = transfer( 455 | args.amount, 456 | from, 457 | args.to, 458 | &args.private_key, 459 | &client, 460 | Overrides::default(), 461 | ) 462 | .await?; 463 | 464 | println!("{tx_hash:#x}"); 465 | 466 | if !args.cast { 467 | wait_for_transaction_receipt(tx_hash, &client, 100, args.silent).await?; 468 | } 469 | } 470 | Command::Send { args, rpc_url } => { 471 | if args.explorer_url { 472 | todo!("Display transaction URL in the explorer") 473 | } 474 | 475 | let from = 476 | get_address_from_secret_key(&args.private_key).map_err(|e| eyre::eyre!(e))?; 477 | 478 | let client = EthClient::new(rpc_url)?; 479 | 480 | let calldata = if !args.calldata.is_empty() { 481 | args.calldata 482 | } else { 483 | parse_func_call(args._args)? 484 | }; 485 | 486 | let tx = build_generic_tx( 487 | &client, 488 | TxType::EIP1559, 489 | args.to, 490 | from, 491 | calldata, 492 | Overrides { 493 | value: Some(args.value), 494 | chain_id: args.chain_id, 495 | nonce: args.nonce, 496 | gas_limit: args.gas_limit, 497 | max_fee_per_gas: args.max_fee_per_gas, 498 | max_priority_fee_per_gas: args.max_priority_fee_per_gas, 499 | from: Some(from), 500 | ..Default::default() 501 | }, 502 | ) 503 | .await?; 504 | 505 | let signer = Signer::Local(LocalSigner::new(args.private_key)); 506 | 507 | let tx_hash = send_generic_transaction(&client, tx, &signer).await?; 508 | 509 | println!("{tx_hash:#x}"); 510 | 511 | if !args.cast { 512 | wait_for_transaction_receipt(tx_hash, &client, 100, args.silent).await?; 513 | } 514 | } 515 | Command::Call { args, rpc_url } => { 516 | if args.explorer_url { 517 | todo!("Display transaction URL in the explorer") 518 | } 519 | 520 | let client = EthClient::new(rpc_url)?; 521 | 522 | let calldata = if !args.calldata.is_empty() { 523 | args.calldata 524 | } else { 525 | parse_func_call(args._args)? 526 | }; 527 | 528 | let result = client 529 | .call( 530 | args.to, 531 | calldata, 532 | Overrides { 533 | from: args.from, 534 | value: args.value.into(), 535 | gas_limit: args.gas_limit, 536 | max_fee_per_gas: args.max_fee_per_gas, 537 | ..Default::default() 538 | }, 539 | ) 540 | .await?; 541 | 542 | println!("{result}"); 543 | } 544 | Command::Deploy { args, rpc_url } => { 545 | if args.explorer_url { 546 | todo!("Display transaction URL in the explorer") 547 | } 548 | 549 | let deployer = Signer::Local(LocalSigner::new(args.private_key)); 550 | let client = EthClient::new(rpc_url)?; 551 | 552 | let bytecode = if let Some(bytecode) = args.bytecode { 553 | bytecode 554 | } else { 555 | compile_contract_from_path(args.clone()).await? 556 | }; 557 | let init_args = if !args._args.is_empty() { 558 | parse_contract_creation(args._args)? 559 | } else { 560 | Bytes::new() 561 | }; 562 | 563 | let (tx_hash, deployed_contract_address) = if let Some(salt) = args.salt { 564 | create2_deploy_from_bytecode( 565 | &init_args, 566 | &bytecode, 567 | &deployer, 568 | salt.as_bytes(), 569 | &client, 570 | ) 571 | .await? 572 | } else { 573 | let init_code = [bytecode, init_args].concat().into(); 574 | deploy( 575 | &client, 576 | &deployer, 577 | init_code, 578 | Overrides { 579 | value: args.value.into(), 580 | nonce: args.nonce, 581 | chain_id: args.chain_id, 582 | gas_limit: args.gas_limit, 583 | max_fee_per_gas: args.max_fee_per_gas, 584 | max_priority_fee_per_gas: args.max_priority_fee_per_gas, 585 | ..Default::default() 586 | }, 587 | true, 588 | ) 589 | .await? 590 | }; 591 | 592 | if args.print_address { 593 | println!("{deployed_contract_address:#x}"); 594 | } else { 595 | println!("Contract deployed in tx: {tx_hash:#x}"); 596 | println!("Contract address: {deployed_contract_address:#x}"); 597 | } 598 | let silent = args.print_address; 599 | 600 | if !args.cast { 601 | wait_for_transaction_receipt(tx_hash, &client, 100, silent).await?; 602 | } 603 | } 604 | Command::ChainId { hex, rpc_url } => { 605 | let eth_client = EthClient::new(rpc_url)?; 606 | 607 | let chain_id = eth_client.get_chain_id().await?; 608 | 609 | if hex { 610 | println!("{chain_id:#x}"); 611 | } else { 612 | println!("{chain_id}"); 613 | } 614 | } 615 | Command::Code { 616 | address, 617 | block, 618 | rpc_url, 619 | } => { 620 | let eth_client = EthClient::new(rpc_url)?; 621 | 622 | let block_identifier = BlockIdentifier::parse(serde_json::Value::String(block), 0)?; 623 | 624 | let code = eth_client.get_code(address, block_identifier).await?; 625 | 626 | println!("0x{}", hex::encode(code)); 627 | } 628 | 629 | // Signature computed as a 0x45 signature, as described in EIP-191 (https://eips.ethereum.org/EIPS/eip-191), 630 | // then it has an extra byte concatenated at the end, which is a scalar value added to the signatures parity, 631 | // 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) 632 | Command::Sign { msg, private_key } => { 633 | let payload = [ 634 | b"\x19Ethereum Signed Message:\n", 635 | msg.len().to_string().as_bytes(), 636 | msg.as_ref(), 637 | ] 638 | .concat(); 639 | let encoded_signature = sign_hash(keccak(payload), private_key); 640 | println!("0x{:x}", H520::from_slice(&encoded_signature)); 641 | } 642 | Command::VerifySignature { 643 | message, 644 | signature, 645 | address, 646 | } => { 647 | println!( 648 | "{}", 649 | get_address_from_message_and_signature(message, signature)? == address 650 | ); 651 | } 652 | Command::EncodeCalldata { 653 | signature, 654 | mut args, 655 | } => { 656 | args.insert(0, signature); 657 | println!("0x{:x}", parse_func_call(args)?); 658 | } 659 | Command::DecodeCalldata { signature, data } => { 660 | for elem in decode_calldata(&signature, data)? { 661 | print_calldata(0, elem); 662 | } 663 | } 664 | }; 665 | Ok(()) 666 | } 667 | } 668 | 669 | fn print_calldata(depth: usize, data: Value) { 670 | print!("{}", " ".repeat(depth)); 671 | match data { 672 | Value::Address(addr) => println!("{addr:#x}"), 673 | Value::Array(inner) => { 674 | println!("["); 675 | for elem in inner { 676 | print_calldata(depth + 2, elem); 677 | } 678 | println!("]"); 679 | } 680 | Value::Bool(b) => println!("{b}"), 681 | Value::Bytes(bytes) => println!("0x{bytes:#x}"), 682 | Value::FixedArray(inner) => { 683 | println!("["); 684 | for elem in inner { 685 | print_calldata(depth + 2, elem); 686 | } 687 | println!("]"); 688 | } 689 | Value::FixedBytes(bytes) => println!("{bytes:#x}"), 690 | Value::Int(val) => println!("{val}"), 691 | Value::Uint(val) => println!("{val}"), 692 | Value::String(str) => println!("{str}"), 693 | Value::Tuple(inner) => { 694 | println!("("); 695 | for elem in inner { 696 | print_calldata(depth + 2, elem); 697 | } 698 | println!(")"); 699 | } 700 | } 701 | } 702 | 703 | async fn compile_contract_from_path(args: DeployArgs) -> eyre::Result { 704 | let contract_path = args 705 | .contract_path 706 | .as_ref() 707 | .ok_or_else(|| eyre::eyre!("Contract path is required when bytecode is not provided"))?; 708 | 709 | let clean = !args.keep_deps; 710 | let output_dir = Path::new("."); 711 | let deps_dir = Path::new("rex_deps"); 712 | let mut solc_remappings = Vec::new(); 713 | let mut cloned_dirs = Vec::new(); 714 | 715 | std::fs::create_dir_all(deps_dir).ok(); 716 | 717 | if let Some(remappings_str) = &args.remappings { 718 | let remappings = remappings_str 719 | .split(',') 720 | .filter_map(|mapping| { 721 | let mut parts = mapping.splitn(2, '='); 722 | match (parts.next(), parts.next()) { 723 | (Some(key), Some(val)) if !key.trim().is_empty() && !val.trim().is_empty() => { 724 | Some((key.trim().to_string(), val.trim().to_string())) 725 | } 726 | _ => None, 727 | } 728 | }) 729 | .collect::>(); 730 | 731 | for (remap, repo_or_path) in remappings { 732 | let existing_path = Path::new(&repo_or_path); 733 | let base_path = if existing_path.exists() { 734 | existing_path.to_path_buf() 735 | } else { 736 | let repo_name = repo_or_path 737 | .split('/') 738 | .next_back() 739 | .and_then(|s| s.strip_suffix(".git")) 740 | .unwrap_or("dep"); 741 | 742 | let local_path = deps_dir.join(repo_name); 743 | git_clone(&repo_or_path, local_path.to_str().unwrap(), None, true)?; 744 | cloned_dirs.push(local_path.clone()); 745 | local_path 746 | }; 747 | 748 | solc_remappings.push((remap, base_path)); 749 | } 750 | } 751 | 752 | let mut include_paths = vec![output_dir]; 753 | 754 | if let Some(parent) = contract_path.parent() { 755 | include_paths.push(parent); 756 | } 757 | 758 | include_paths.push(deps_dir); 759 | 760 | for (_, path) in &solc_remappings { 761 | include_paths.push(path); 762 | } 763 | 764 | let solc_remappings_ref: Vec<(&str, PathBuf)> = solc_remappings 765 | .iter() 766 | .map(|(k, v)| (k.as_str(), v.clone())) 767 | .collect(); 768 | 769 | compile_contract( 770 | output_dir, 771 | contract_path, 772 | false, 773 | Some(&solc_remappings_ref), 774 | &include_paths, 775 | ) 776 | .map_err(|e| eyre::eyre!("Failed to compile contract: {}", e))?; 777 | 778 | let bin_path = output_dir.join("solc_out").join(format!( 779 | "{}.bin", 780 | contract_path 781 | .file_stem() 782 | .unwrap_or_default() 783 | .to_string_lossy() 784 | )); 785 | 786 | let bin_content = std::fs::read_to_string(&bin_path).map_err(|e| { 787 | eyre::eyre!( 788 | "Failed to read compiled bytecode from {}: {}", 789 | bin_path.display(), 790 | e 791 | ) 792 | })?; 793 | let bytecode = hex::decode(bin_content) 794 | .map_err(|e| { 795 | eyre::eyre!( 796 | "Failed to decode bytecode from hex in {}: {}", 797 | bin_path.display(), 798 | e 799 | ) 800 | })? 801 | .into(); 802 | 803 | if clean { 804 | for dir in cloned_dirs { 805 | if dir.exists() { 806 | std::fs::remove_dir_all(&dir) 807 | .map_err(|e| eyre::eyre!("Failed to clean up {}: {}", dir.display(), e))?; 808 | } 809 | } 810 | std::fs::remove_dir_all("rex_deps").ok(); 811 | std::fs::remove_dir_all("solc_out").ok(); 812 | } 813 | 814 | Ok(bytecode) 815 | } 816 | --------------------------------------------------------------------------------