diff --git a/.env.example b/.env.example index 32a88a729..e6a4ca0dc 100644 --- a/.env.example +++ b/.env.example @@ -12,9 +12,6 @@ STARKNET_NETWORK= ## Katana specific configurations KATANA_ACCOUNT_ADDRESS=0xb3ff441a68610b30fd5e2abbf3a1548eb6ba6f3559f2862bf2dc757e5828ca KATANA_PRIVATE_KEY=0x2bbf4f9fd0bbb2e60b0316c1fe0b76cf7a4d0198bd493ced9b8df2a3a24d68a -## Madara account address and private key -MADARA_ACCOUNT_ADDRESS=0x3 -MADARA_PRIVATE_KEY=0x00c1cf1490de1352865301bb8705143f3ef938f97fdf892f1090dcb5ac7bcd1d # Kakarot Environment KAKAROT_RPC_URL=127.0.0.1:3030 diff --git a/.github/workflows/deno_test.yml b/.github/workflows/deno_test.yml new file mode 100644 index 000000000..3ab4e3c85 --- /dev/null +++ b/.github/workflows/deno_test.yml @@ -0,0 +1,68 @@ +--- +name: deno test + +on: + workflow_call: + +jobs: + formatting: + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: indexer + - uses: denoland/setup-deno@v1 + with: + deno-version: vx.x.x + - name: Run formatting + run: deno fmt --check indexer/ + + test: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: denoland/setup-deno@v1 + with: + deno-version: v1.x + # Checkout target branch and run tests + - name: Run Tests with Coverage on target branch + run: KAKAROT_ADDRESS=0x1 deno test --allow-env --coverage=cov_profile + - name: Generate Coverage Report on target branch + run: deno coverage cov_profile --html + - name: Install bc + run: | + sudo apt-get update + sudo apt-get install -y bc + - name: Extract coverage percentage from HTML for the target branch + run: | + cat cov_profile/html/index.html + coverage_percentage=$(grep -A 3 "
" cov_profile/html/index.html | grep "" | awk -F'[<>]' '{gsub("%","",$3); print $3}') + echo $coverage_percentage > curr_coverage_percentage.txt + # Checkout base branch and run tests + - uses: actions/checkout@v4 + with: + ref: ${{ github.base_ref }} + clean: false + - name: Run Tests with Coverage on base branch + run: KAKAROT_ADDRESS=0x1 deno test --allow-env --coverage=cov_profile_main + - name: Generate HTML report from for the base branch + run: deno coverage cov_profile_main --html + - name: Extract coverage percentage from HTML for the base branch + run: | + coverage_percentage_main=$(grep -A 3 "
" cov_profile_main/html/index.html | grep "" | awk -F'[<>]' '{gsub("%","",$3); print $3}') + echo $coverage_percentage_main > coverage_percentage_main.txt + - name: Compare coverage percentage + run: | + previous_coverage=$(awk '{print $2}' coverage_percentage_main.txt) + echo "Previous coverage percentage was $previous_coverage%" + current_coverage=$(awk '{print $2}' curr_coverage_percentage.txt) + echo "Current coverage percentage is $current_coverage%" + change=$(echo "$previous_coverage - $current_coverage" | bc) + echo "Coverage change is $change%" + if (( $(echo "$change > 5.0" | bc -l) )); then + echo "Coverage dropped by more than 5%!" + exit 1 + fi diff --git a/.github/workflows/indexer_formatting.yml b/.github/workflows/indexer_formatting.yml deleted file mode 100644 index c211590e8..000000000 --- a/.github/workflows/indexer_formatting.yml +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Format indexer - -on: - workflow_call: - -jobs: - deno-formatting: - runs-on: ubuntu-latest - timeout-minutes: 5 - - steps: - - uses: actions/checkout@v4 - with: - sparse-checkout: indexer - - uses: denoland/setup-deno@v1 - with: - deno-version: vx.x.x - - name: Run formatting - run: deno fmt --check indexer/ diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 8a8bb42ea..3fcd873aa 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -12,10 +12,10 @@ jobs: name: Spell check uses: ./.github/workflows/spell_check.yml - tests: + rust-tests: name: Rust tests - uses: ./.github/workflows/test.yml + uses: ./.github/workflows/rust_test.yml - indexer_formatting: - name: Deno formatting - uses: ./.github/workflows/indexer_formatting.yml + deno-tests: + name: Deno tests + uses: ./.github/workflows/deno_test.yml diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 4df371931..f45a890ee 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -14,6 +14,6 @@ jobs: name: Spell check uses: ./.github/workflows/spell_check.yml - indexer_formatting: - name: Deno formatting - uses: ./.github/workflows/indexer_formatting.yml + deno-tests: + name: Deno tests + uses: ./.github/workflows/deno_test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/rust_test.yml similarity index 94% rename from .github/workflows/test.yml rename to .github/workflows/rust_test.yml index 1b8b99a65..5a0a7d1f6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/rust_test.yml @@ -1,30 +1,19 @@ --- -name: test +name: rust test on: workflow_call: jobs: - test-deno: - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - uses: denoland/setup-deno@v1 - with: - deno-version: v1.x - - run: KAKAROT_ADDRESS=0x1 deno test --allow-env - test-rust: + test: runs-on: ubuntu-latest-16-cores timeout-minutes: 30 steps: - uses: actions/checkout@v4 - name: Setup rust env - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@stable with: - profile: minimal - components: llvm-tools-preview, rustfmt, clippy - override: true + components: rustfmt, clippy toolchain: 1.76.0 - name: Retrieve cached dependencies uses: Swatinem/rust-cache@v2 diff --git a/.gitignore b/.gitignore index 1971a548f..59c3cb74b 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ vscode broadcast .idea +.DS_Store # ignore genesis.json .hive diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 22830cebe..f47f5abce 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -22,7 +22,7 @@ lint: - git-diff-check - hadolint@2.12.0 - markdownlint@0.41.0 - - osv-scanner@1.7.4 + - osv-scanner@1.8.1 - oxipng@9.1.1 - prettier@3.3.2 - rustfmt@1.65.0 @@ -31,7 +31,7 @@ lint: - taplo@0.8.1 - terrascan@1.19.1 - trivy@0.52.2 - - trufflehog@3.78.1 + - trufflehog@3.79.0 - yamllint@1.35.1 - deno@1.44.4 ignore: diff --git a/Cargo.lock b/Cargo.lock index d1855c57d..d039581bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -368,7 +368,6 @@ dependencies = [ "alloy-rpc-types-eth", "alloy-rpc-types-trace", "alloy-transport", - "alloy-transport-http", "async-stream", "async-trait", "auto_impl", @@ -377,12 +376,10 @@ dependencies = [ "futures-utils-wasm", "lru 0.12.2", "pin-project", - "reqwest 0.12.3", "serde", "serde_json", "tokio", "tracing", - "url", ] [[package]] @@ -417,14 +414,12 @@ dependencies = [ "alloy-transport-http", "futures", "pin-project", - "reqwest 0.12.3", "serde", "serde_json", "tokio", "tokio-stream", "tower", "tracing", - "url", ] [[package]] @@ -571,7 +566,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd9899da7d011b4fe4c406a524ed3e3f963797dbc93b45479d60341d3a27b252" dependencies = [ - "alloy-json-abi 0.7.6", "alloy-sol-macro-input", "const-hex", "heck 0.5.0", @@ -590,13 +584,11 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d32d595768fdc61331a132b6f65db41afae41b9b97d36c21eb1b955c422a7e60" dependencies = [ - "alloy-json-abi 0.7.6", "const-hex", "dunce", "heck 0.5.0", "proc-macro2", "quote", - "serde_json", "syn 2.0.58", "syn-solidity", ] @@ -625,7 +617,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a49042c6d3b66a9fe6b2b5a8bf0d39fc2ae1ee0310a2a26ffedd79fb097878dd" dependencies = [ - "alloy-json-abi 0.7.6", "alloy-primitives 0.7.6", "alloy-sol-macro", "const-hex", @@ -654,12 +645,7 @@ name = "alloy-transport-http" version = "0.1.0" source = "git+https://github.com/alloy-rs/alloy?rev=00d81d7#00d81d7882a0bee4720d6d6a1db4c8f164ebb9d0" dependencies = [ - "alloy-json-rpc", "alloy-transport", - "reqwest 0.12.3", - "serde_json", - "tower", - "tracing", "url", ] @@ -2891,7 +2877,7 @@ version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12d0939f42d40fb1d975cae073d7d4f82d83de4ba2149293115525245425f909" dependencies = [ - "env_logger 0.10.2", + "env_logger", "hashbrown 0.14.3", "indexmap 2.2.6", "itertools 0.11.0", @@ -4439,15 +4425,6 @@ dependencies = [ "syn 2.0.58", ] -[[package]] -name = "env_filter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" -dependencies = [ - "log", -] - [[package]] name = "env_logger" version = "0.10.2" @@ -4461,16 +4438,6 @@ dependencies = [ "termcolor", ] -[[package]] -name = "env_logger" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" -dependencies = [ - "env_filter", - "log", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -6024,23 +5991,6 @@ dependencies = [ "minilp", ] -[[package]] -name = "governor" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b" -dependencies = [ - "cfg-if", - "futures", - "futures-timer", - "no-std-compat", - "nonzero_ext", - "parking_lot 0.12.1", - "portable-atomic", - "smallvec", - "spinning_top", -] - [[package]] name = "group" version = "0.12.1" @@ -6436,22 +6386,6 @@ dependencies = [ "tokio-native-tls", ] -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper 1.3.1", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.3" @@ -7415,7 +7349,6 @@ dependencies = [ "alloy-dyn-abi", "alloy-json-abi 0.7.6", "alloy-primitives 0.7.6", - "alloy-provider", "alloy-rlp", "alloy-signer-wallet", "alloy-sol-types", @@ -7425,22 +7358,18 @@ dependencies = [ "auto_impl", "bytes", "cainome 0.2.3 (git+https://github.com/cartridge-gg/cainome.git?tag=v0.2.6)", - "cairo-lang-starknet 2.5.4 (registry+https://github.com/rust-lang/crates.io-index)", "dojo-test-utils", "dotenvy", "ef-testing", - "env_logger 0.11.3", "eyre", "foundry-config", "futures", - "governor", "hex", "http-body-util", "hyper 1.3.1", "hyper-util", "itertools 0.12.1", "jsonrpsee 0.21.0", - "katana-core", "katana-primitives", "lazy_static", "log", @@ -7486,15 +7415,7 @@ name = "katana-core" version = "0.7.0-alpha.0" source = "git+https://github.com/dojoengine/dojo?tag=v0.7.0-alpha.0#2b2dc1bf35e4568bda7341f17267a931b49148b0" dependencies = [ - "alloy-contract", - "alloy-network", "alloy-primitives 0.7.6", - "alloy-provider", - "alloy-rpc-types", - "alloy-signer", - "alloy-signer-wallet", - "alloy-sol-types", - "alloy-transport", "anyhow", "async-trait", "cairo-lang-casm 2.5.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -8525,12 +8446,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "no-std-compat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" - [[package]] name = "nom" version = "7.1.3" @@ -8541,12 +8456,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nonzero_ext" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" - [[package]] name = "ntapi" version = "0.4.1" @@ -9861,7 +9770,7 @@ dependencies = [ "http-body 0.4.6", "hyper 0.14.28", "hyper-rustls 0.24.2", - "hyper-tls 0.5.0", + "hyper-tls", "ipnet", "js-sys", "log", @@ -9906,23 +9815,19 @@ dependencies = [ "http-body 1.0.0", "http-body-util", "hyper 1.3.1", - "hyper-tls 0.6.0", "hyper-util", "ipnet", "js-sys", "log", "mime", - "native-tls", "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile 2.1.2", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-native-tls", "tower-service", "url", "wasm-bindgen", @@ -12738,15 +12643,6 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -[[package]] -name = "spinning_top" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" -dependencies = [ - "lock_api", -] - [[package]] name = "spki" version = "0.6.0" diff --git a/Cargo.toml b/Cargo.toml index 32923aff9..462ec75fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,19 +51,13 @@ no_effect_underscore_binding = "allow" cainome = { git = "https://github.com/cartridge-gg/cainome.git", tag = "v0.2.6", default-features = false, features = [ "abigen-rs", ] } -cairo-lang-starknet = { version = "2.5.4", default-features = false } starknet = { version = "0.9.0", default-features = false } starknet-crypto = { version = "0.6.1", default-features = false } -starknet_api = { version = "0.7.0-dev.0", default-features = false } # Ethereum dependencies -alloy-primitives = { version = "0.7.2", default-features = false } alloy-rlp = { version = "0.3.4", default-features = false } alloy-sol-types = { version = "0.7.2", default-features = false } alloy-contract = { git = "https://github.com/alloy-rs/alloy", rev = "00d81d7" } -alloy-provider = { git = "https://github.com/alloy-rs/alloy", rev = "00d81d7" } -alloy-json-abi = { version = "0.7.6", default-features = false } -alloy-dyn-abi = { version = "0.7.6", default-features = false } jsonrpsee = { version = "0.21.0", features = ["macros", "server"] } reth-primitives = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.0.0-rc.1", default-features = false, features = [ "alloy-compat", @@ -80,40 +74,31 @@ revm-inspectors = { git = "https://github.com/paradigmxyz/evm-inspectors", rev = # Serde serde = { version = "1.0.198", default-features = false, features = ["derive"] } serde_json = { version = "1.0.115", default-features = false } -serde_with = { version = "2.3.1", default-features = false } # Others -anyhow = { version = "1.0.82", default-features = false } async-trait = { version = "0.1.80", default-features = false } auto_impl = { version = "1.1.0", default-features = false } bytes = { version = "1.6.0", default-features = false } dotenvy = { version = "0.15.7", default-features = false } -env_logger = { version = "0.11.3", default-features = false } eyre = { version = "0.6.12", default-features = false } -foundry-config = { git = "https://github.com/foundry-rs/foundry", branch = "master" } futures = { version = "0.3.30", default-features = false } -hex = { version = "0.4.3", default-features = false } itertools = { version = "0.12.1", default-features = false } lazy_static = { version = "1.4.0", default-features = false } log = { version = "0.4.21", default-features = false } mongodb = { version = "2.8.2", default-features = false, features = [ "tokio-runtime", ] } -reqwest = { version = "0.12.3", default-features = false } -rstest = { version = "0.19.0", default-features = false } thiserror = { version = "1.0.58", default-features = false } tokio = { version = "1.37.0", features = ["macros"] } tower = { version = "0.4.13", default-features = false } -tower-http = { version = "0.4.4", default-features = false } +tower-http = { version = "0.4.4", features = ["cors"] } tracing = { version = "0.1.40", default-features = false } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } url = { version = "2.5.0", default-features = false } -walkdir = { version = "2.5.0", default-features = false } # Prometheus -governor = { version = "0.6.0", default-features = false, features = ["std"] } prometheus = { version = "0.13.0", default-features = false } hyper = { version = "1.3.1", default-features = false } hyper-util = { version = "0.1.3", default-features = false, features = [ @@ -123,12 +108,21 @@ http-body-util = { version = "0.1.1", default-features = false } pin-project-lite = { version = "0.2", default-features = false } # Testing crates -arbitrary = { version = "1", features = ["derive"] } +alloy-dyn-abi = { version = "0.7.6", default-features = false, optional = true } +alloy-json-abi = { version = "0.7.6", default-features = false, optional = true } +alloy-primitives = { version = "0.7.2", default-features = false, optional = true } +alloy-signer-wallet = { version = "0.1.0", default-features = false, optional = true } +anyhow = { version = "1.0.82", default-features = false, optional = true } +arbitrary = { version = "1", features = ["derive"], optional = true } ef-testing = { git = "https://github.com/kkrt-labs/ef-tests.git", rev = "ccd9042", default-features = false, features = [ "v0", ], optional = true } +foundry-config = { git = "https://github.com/foundry-rs/foundry", branch = "master", optional = true } rand = { version = "0.8.5", default-features = false, optional = true } rayon = { version = "1.10.0", default-features = false, optional = true } +rstest = { version = "0.19.0", default-features = false, optional = true } +serde_with = { version = "2.3.1", default-features = false, optional = true } +starknet_api = { version = "0.7.0-dev.0", default-features = false, optional = true } strum = { version = "0.26.2", default-features = false, optional = true } strum_macros = { version = "0.26.2", default-features = false, optional = true } testcontainers = { version = "0.15.0", default-features = false, optional = true } @@ -136,21 +130,18 @@ tokio-util = { version = "0.7.10", features = [ "codec", ], default-features = false, optional = true } tokio-stream = { version = "0.1.15", default-features = false, optional = true } +walkdir = { version = "2.5.0", default-features = false, optional = true } # In order to use dojo-test-utils, we need to explicitly declare the same patches as them in our Cargo.toml # Otherwise, underlying dependencies of dojo will not be patched and we will get a compilation error # see https://github.com/dojoengine/dojo/issues/563 # When making changes to the rev, please also update to make file to the same rev in the `install-katana` rule. -dojo-test-utils = { git = 'https://github.com/dojoengine/dojo', tag = "v0.7.0-alpha.0", default-features = false } -katana-core = { git = 'https://github.com/dojoengine/dojo', tag = "v0.7.0-alpha.0", features = [ - "messaging", -] } +dojo-test-utils = { git = 'https://github.com/dojoengine/dojo', tag = "v0.7.0-alpha.0", default-features = false, optional = true } katana-primitives = { git = 'https://github.com/dojoengine/dojo', tag = "v0.7.0-alpha.0", default-features = false, features = [ "serde", -] } +], optional = true } -alloy-signer-wallet = { version = "0.1.0", default-features = false } [patch."https://github.com/starkware-libs/blockifier"] blockifier = { git = "https://github.com/dojoengine/blockifier", rev = "d38b979" } @@ -177,23 +168,37 @@ revm-precompile = { git = "https://github.com/bluealloy/revm", rev = "a28a543" } revm-primitives = { git = "https://github.com/bluealloy/revm", rev = "a28a543" } [dev-dependencies] -rstest = { version = "0.19.0", default-features = false } -toml = { version = "0.8.12", default-features = false } +hex = { version = "0.4.3", default-features = false } proptest = { version = "1.4.0", default-features = false } +reqwest = { version = "0.12.3", default-features = false } +toml = { version = "0.8.12", default-features = false } [features] testing = [ - "testcontainers", - "rayon", + "alloy-dyn-abi", + "alloy-json-abi", + "alloy-primitives", + "alloy-signer-wallet", + "anyhow", + "dep:arbitrary", + "dojo-test-utils", "ef-testing", - "tokio-util", - "tokio-stream", + "foundry-config", + "katana-primitives", "rand", + "rayon", + "rstest", + "serde_with", + "starknet_api", "strum", "strum_macros", + "testcontainers", + "tokio-stream", + "tokio-util", + "walkdir", ] hive = [] -arbitrary = ["rand"] +arbitrary = ["rand", "dep:arbitrary"] [[bin]] name = "katana_genesis" diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml index 69dee1ab5..9c39beeaf 100644 --- a/docker-compose.prod.yaml +++ b/docker-compose.prod.yaml @@ -63,7 +63,7 @@ services: - starknet-explorer_prod:/var/lib/postgresql/data/ kakarot-rpc: - image: ghcr.io/kkrt-labs/kakarot-rpc/node:v0.6.17 + image: ghcr.io/kkrt-labs/kakarot-rpc/node:v0.6.19 pull_policy: always ports: - 3030:3030 diff --git a/docker-compose.yaml b/docker-compose.yaml index 20007ecb6..5233d6bc9 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -23,7 +23,7 @@ services: restart: on-failure kakarot-deployer: - image: ghcr.io/kkrt-labs/kakarot/deployer:v0.8.1 + image: ghcr.io/kkrt-labs/kakarot/deployer:v0.8.4 # Always pull the latest image, until we use release tags pull_policy: always environment: @@ -64,7 +64,7 @@ services: condition: service_completed_successfully kakarot-rpc: - image: ghcr.io/kkrt-labs/kakarot-rpc/node:v0.6.18 + image: ghcr.io/kkrt-labs/kakarot-rpc/node:v0.6.19 ports: - 3030:3030 environment: diff --git a/docker/hive/Dockerfile b/docker/hive/Dockerfile index ed46224e4..dd072bb90 100644 --- a/docker/hive/Dockerfile +++ b/docker/hive/Dockerfile @@ -142,7 +142,7 @@ ENV ALLOW_NET="" ### Port 8545: https://github.com/ethereum/hive/blob/master/simulators/ethereum/rpc/helper.go#L50 ENV KAKAROT_RPC_URL=0.0.0.0:8545 ENV STARKNET_NETWORK=http://localhost:5050 -ENV RUST_LOG=kakarot_rpc=info +ENV RUST_LOG=info ENV MAX_FELTS_IN_CALLDATA=30000 ENV TRANSACTION_MAX_RETRIES=10 ENV MAX_LOGS=10000 diff --git a/docker/hive/start.sh b/docker/hive/start.sh index fff753f3b..6685cca6f 100644 --- a/docker/hive/start.sh +++ b/docker/hive/start.sh @@ -47,14 +47,14 @@ echo "Launching mongo..." mongod --bind_ip 0.0.0.0 --noauth & ## DNA echo "Launching DNA..." -starknet start --rpc=http://localhost:5050 --wait-for-rpc --head-refresh-interval-ms=500 --data=/data & +starknet start --rpc=http://localhost:5050 --wait-for-rpc --head-refresh-interval-ms=300 --data=/data & # ## Indexer echo "Launching indexer..." sink-mongo run /usr/src/app/code/indexer/src/main.ts & ### 3.5. Await the Indexer to be healthy echo "Waiting for the indexer to start..." -sleep 8 +sleep 9 # 4. Start the Kakarot RPC service echo "Launching Kakarot RPC..." diff --git a/scripts/staging.sh b/scripts/staging.sh index c1bc3054c..6d88ca3a8 100644 --- a/scripts/staging.sh +++ b/scripts/staging.sh @@ -10,4 +10,4 @@ export KAKAROT_ADDRESS="${KAKAROT_ADDRESS}" export UNINITIALIZED_ACCOUNT_CLASS_HASH="${UNINITIALIZED_ACCOUNT_CLASS_HASH}" export ACCOUNT_CONTRACT_CLASS_HASH="${ACCOUNT_CONTRACT_CLASS_HASH}" -poetry run pytest -s tests/end_to_end +poetry run pytest -s tests/end_to_end --ignore tests/end_to_end/L1L2Messaging diff --git a/src/bin/hive_chain.rs b/src/bin/hive_chain.rs index d8967a6ec..99f935394 100644 --- a/src/bin/hive_chain.rs +++ b/src/bin/hive_chain.rs @@ -61,7 +61,7 @@ async fn main() -> eyre::Result<()> { for transaction in body.transactions { let signer = transaction.recover_signer().ok_or_eyre("failed to recover signer")?; - let starknet_tx = to_starknet_transaction(&transaction, signer, u64::MAX, 0)?; + let starknet_tx = to_starknet_transaction(&transaction, signer, 0)?; let nonce = match &starknet_tx { BroadcastedInvokeTransaction::V1(starknet_tx) => starknet_tx.nonce, diff --git a/src/config.rs b/src/config.rs index b02b448e6..9454df0b1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,5 @@ use eyre::eyre; use starknet::core::types::FieldElement; -use starknet::providers::jsonrpc::{HttpTransport, JsonRpcTransport}; -use starknet::providers::{JsonRpcClient, SequencerGatewayProvider}; use std::env::var; use url::Url; @@ -11,60 +9,11 @@ fn env_var_to_field_element(var_name: &str) -> Result Ok(FieldElement::from_hex_be(&env_var)?) } -#[derive(Default, Clone, Debug)] -pub enum Network { - #[default] - Katana, - Madara, - Sharingan, - MainnetGateway, - Goerli1Gateway, - Goerli2Gateway, - JsonRpcProvider(Url), -} - -impl From for Network { - fn from(s: String) -> Self { - match s.to_lowercase().as_str() { - "katana" => Self::Katana, - "madara" => Self::Madara, - "sharingan" => Self::Sharingan, - "mainnet" => Self::MainnetGateway, - "goerli1" | "testnet" => Self::Goerli1Gateway, - "goerli2" => Self::Goerli2Gateway, - network_url => Url::parse(network_url).map_or(Self::Katana, Self::JsonRpcProvider), - } - } -} - -impl Network { - pub fn gateway_url(&self) -> Result { - match self { - Self::MainnetGateway => Ok(Url::parse("https://alpha-mainnet.starknet.io/feeder_gateway/")?), - Self::Goerli1Gateway => Ok(Url::parse("https://alpha4.starknet.io/feeder_gateway/")?), - Self::Goerli2Gateway => Ok(Url::parse("https://alpha4-2.starknet.io/feeder_gateway/")?), - _ => Err(eyre!("Network {:?} is not supported for gateway url", self)), - } - } - - pub fn provider_url(&self) -> Result { - match self { - Self::Katana => Ok(Url::parse("http://0.0.0.0:5050")?), - Self::Madara => Ok(Url::parse("http://127.0.0.1:9944")?), - Self::Sharingan => Ok(Url::parse( - var("SHARINGAN_RPC_URL").map_err(|_| eyre!("Missing env var SHARINGAN_RPC_URL".to_string()))?.as_str(), - )?), - Self::JsonRpcProvider(url) => Ok(url.clone()), - _ => Err(eyre!("Network {:?} is not supported for provider url", self)), - } - } -} - -#[derive(Default, Clone, Debug)] +#[derive(Clone, Debug)] /// Configuration for the Starknet RPC client. pub struct KakarotRpcConfig { /// Starknet network. - pub network: Network, + pub network_url: Url, /// Kakarot contract address. pub kakarot_address: FieldElement, /// Uninitialized account class hash. @@ -74,91 +23,14 @@ pub struct KakarotRpcConfig { } impl KakarotRpcConfig { - pub const fn new( - network: Network, - kakarot_address: FieldElement, - uninitialized_account_class_hash: FieldElement, - account_contract_class_hash: FieldElement, - ) -> Self { - Self { network, kakarot_address, uninitialized_account_class_hash, account_contract_class_hash } - } - - /// Create a new `StarknetConfig` from environment variables. - /// When using non-standard providers (i.e. not "katana", "madara", "mainnet"), the /// `STARKNET_NETWORK` environment variable should be set the URL of a `JsonRpc` /// starknet provider, e.g. . - pub fn from_env() -> Result { + pub fn from_env() -> eyre::Result { Ok(Self { - network: var("STARKNET_NETWORK")?.into(), + network_url: Url::parse(&var("STARKNET_NETWORK")?)?, kakarot_address: env_var_to_field_element("KAKAROT_ADDRESS")?, uninitialized_account_class_hash: env_var_to_field_element("UNINITIALIZED_ACCOUNT_CLASS_HASH")?, account_contract_class_hash: env_var_to_field_element("ACCOUNT_CONTRACT_CLASS_HASH")?, }) } } - -/// A builder for a `JsonRpcClient`. -#[derive(Debug)] -pub struct JsonRpcClientBuilder(JsonRpcClient); - -impl JsonRpcClientBuilder { - /// Create a new `JsonRpcClientBuilder`. - pub fn new(transport: T) -> Self { - Self(JsonRpcClient::new(transport)) - } - - /// Build the `JsonRpcClient`. - pub fn build(self) -> JsonRpcClient { - self.0 - } -} - -impl JsonRpcClientBuilder { - /// Returns a new `JsonRpcClientBuilder` with a `HttpTransport`. - /// Currently only supports Katana and Madara networks or manual Starknet provider URL. - /// # Example - /// - /// ```rust - /// use kakarot_rpc::config::{JsonRpcClientBuilder, KakarotRpcConfig, Network}; - /// use starknet::core::types::FieldElement; - /// use starknet::providers::jsonrpc::HttpTransport; - /// use starknet::providers::JsonRpcClient; - /// use url::Url; - /// - /// let url = "http://0.0.0.0:1234/rpc"; - /// let config = KakarotRpcConfig::new( - /// Network::JsonRpcProvider(Url::parse(url).unwrap()), - /// FieldElement::default(), - /// FieldElement::default(), - /// FieldElement::default(), - /// ); - /// let starknet_provider: JsonRpcClient = - /// JsonRpcClientBuilder::with_http(&config).unwrap().build(); - /// ``` - pub fn with_http(config: &KakarotRpcConfig) -> Result { - let url = config.network.provider_url()?; - let transport = HttpTransport::new(url); - Ok(Self::new(transport)) - } -} - -/// A builder for a `SequencerGatewayProvider`. -#[derive(Debug)] -pub struct SequencerGatewayProviderBuilder(SequencerGatewayProvider); - -impl SequencerGatewayProviderBuilder { - /// Create a new `SequencerGatewayProviderBuilder`. - pub fn new(network: &Network) -> Self { - match network { - Network::MainnetGateway => Self(SequencerGatewayProvider::starknet_alpha_mainnet()), - Network::Goerli1Gateway => Self(SequencerGatewayProvider::starknet_alpha_goerli()), - Network::Goerli2Gateway => Self(SequencerGatewayProvider::starknet_alpha_goerli_2()), - _ => panic!("Unsupported network for SequencerGatewayProvider"), - } - } - - /// Build the `SequencerGatewayProvider`. - pub fn build(self) -> SequencerGatewayProvider { - self.0 - } -} diff --git a/src/eth_provider/constant.rs b/src/eth_provider/constant.rs index 5ac618b25..9d85dc8cd 100644 --- a/src/eth_provider/constant.rs +++ b/src/eth_provider/constant.rs @@ -54,7 +54,7 @@ lazy_static! { static ref RPC_CONFIG: KakarotRpcConfig = KakarotRpcConfig::from_env().expect("Failed to load Kakarot RPC config"); pub static ref DEPLOY_WALLET: SingleOwnerAccount, LocalWallet> = SingleOwnerAccount::new( - JsonRpcClient::new(HttpTransport::new(RPC_CONFIG.network.provider_url().expect("Incorrect provider URL"))), + JsonRpcClient::new(HttpTransport::new(RPC_CONFIG.network_url.clone())), LocalWallet::from_signing_key(SigningKey::from_secret_scalar( FieldElement::from_str(&var("KATANA_PRIVATE_KEY").expect("Missing deployer private key")) .expect("Failed to parse deployer private key") diff --git a/src/eth_provider/contracts/erc20.rs b/src/eth_provider/contracts/erc20.rs index 4e8675cc9..1d01c4124 100644 --- a/src/eth_provider/contracts/erc20.rs +++ b/src/eth_provider/contracts/erc20.rs @@ -4,7 +4,7 @@ use reth_primitives::{BlockId, TxKind, U256}; use reth_rpc_types::request::TransactionInput; use reth_rpc_types::TransactionRequest; -use crate::eth_provider::error::{ExecutionError, KakarotError}; +use crate::eth_provider::error::ExecutionError; use crate::eth_provider::provider::EthProviderResult; use crate::eth_provider::provider::EthereumProvider; @@ -44,7 +44,7 @@ impl EthereumErc20

{ let ret = self.provider.call(request, Some(block_id)).await?; let balance = U256::try_from_be_slice(&ret) - .ok_or_else(|| KakarotError::from(ExecutionError::Other("failed to deserialize balance".to_string())))?; + .ok_or_else(|| ExecutionError::Other("failed to deserialize balance".to_string()))?; Ok(balance) } diff --git a/src/eth_provider/database/ethereum.rs b/src/eth_provider/database/ethereum.rs index cc4ada8d6..287fdd0ef 100644 --- a/src/eth_provider/database/ethereum.rs +++ b/src/eth_provider/database/ethereum.rs @@ -1,10 +1,14 @@ +use alloy_rlp::Encodable; use async_trait::async_trait; -use reth_primitives::B256; -use reth_rpc_types::Transaction; +use mongodb::bson::doc; +use reth_primitives::constants::EMPTY_ROOT_HASH; +use reth_primitives::{TransactionSigned, B256, U256}; +use reth_rpc_types::{Block, BlockHashOrNumber, BlockTransactions, Header, RichBlock, Transaction}; -use crate::eth_provider::error::EthApiError; +use crate::eth_provider::error::{EthApiError, EthereumDataFormatError}; -use super::filter; +use super::types::header::StoredHeader; +use super::{filter, FindOpts}; use super::{ filter::EthDatabaseFilterBuilder, types::transaction::{StoredPendingTransaction, StoredTransaction}, @@ -12,12 +16,18 @@ use super::{ }; /// Trait for interacting with a database that stores Ethereum typed -/// data. +/// transaction data. #[async_trait] -pub trait EthereumDatabase { - /// Returns the transaction with the given hash. +pub trait EthereumTransactionStore { + /// Returns the transaction with the given hash. Returns None if the + /// transaction is not found. async fn transaction(&self, hash: &B256) -> Result, EthApiError>; - /// Returns the pending transaction with the given hash. + /// Returns all transactions for the given block hash or number. + async fn transactions(&self, block_hash_or_number: BlockHashOrNumber) -> Result, EthApiError>; + /// Returns all transactions hashes for the given block hash or number. + async fn transaction_hashes(&self, block_hash_or_number: BlockHashOrNumber) -> Result, EthApiError>; + /// Returns the pending transaction with the given hash. Returns None if the + /// transaction is not found. async fn pending_transaction(&self, hash: &B256) -> Result, EthApiError>; /// Returns the pending transaction's retries with the given hash. /// Returns 0 if the transaction is not found. @@ -29,12 +39,33 @@ pub trait EthereumDatabase { } #[async_trait] -impl EthereumDatabase for Database { +impl EthereumTransactionStore for Database { async fn transaction(&self, hash: &B256) -> Result, EthApiError> { let filter = EthDatabaseFilterBuilder::::default().with_tx_hash(hash).build(); Ok(self.get_one::(filter, None).await?.map(Into::into)) } + async fn transactions(&self, block_hash_or_number: BlockHashOrNumber) -> Result, EthApiError> { + let filter = EthDatabaseFilterBuilder::::default() + .with_block_hash_or_number(block_hash_or_number) + .build(); + + Ok(self.get::(filter, None).await?.into_iter().map(Into::into).collect()) + } + + async fn transaction_hashes(&self, block_hash_or_number: BlockHashOrNumber) -> Result, EthApiError> { + let filter = EthDatabaseFilterBuilder::::default() + .with_block_hash_or_number(block_hash_or_number) + .build(); + + Ok(self + .get::(filter, FindOpts::default().with_projection(doc! {"tx.hash": 1})) + .await? + .into_iter() + .map(|tx| tx.tx.hash) + .collect()) + } + async fn pending_transaction(&self, hash: &B256) -> Result, EthApiError> { let filter = EthDatabaseFilterBuilder::::default().with_tx_hash(hash).build(); Ok(self.get_one::(filter, None).await?.map(Into::into)) @@ -64,3 +95,107 @@ impl EthereumDatabase for Database { Ok(self.update_one(StoredPendingTransaction::new(transaction, retries), filter, true).await?) } } + +/// Trait for interacting with a database that stores Ethereum typed +/// blocks. +#[async_trait] +pub trait EthereumBlockStore { + /// Returns the header for the given hash or number. Returns None if the + /// header is not found. + async fn header(&self, block_hash_or_number: BlockHashOrNumber) -> Result, EthApiError>; + /// Returns the block for the given hash or number. Returns None if the + /// block is not found. + async fn block( + &self, + block_hash_or_number: BlockHashOrNumber, + full: bool, + ) -> Result, EthApiError>; + /// Returns true if the block with the given hash or number exists. + async fn block_exists(&self, block_hash_or_number: BlockHashOrNumber) -> Result { + self.header(block_hash_or_number).await.map(|header| header.is_some()) + } + /// Returns the transaction count for the given block hash or number. Returns None if the + /// block is not found. + async fn transaction_count(&self, block_hash_or_number: BlockHashOrNumber) -> Result, EthApiError>; +} + +#[async_trait] +impl EthereumBlockStore for Database { + async fn header(&self, block_hash_or_number: BlockHashOrNumber) -> Result, EthApiError> { + let filter = EthDatabaseFilterBuilder::::default() + .with_block_hash_or_number(block_hash_or_number) + .build(); + Ok(self + .get_one::(filter, None) + .await + .inspect_err(|err| tracing::error!("internal error: {:?}", err)) + .map_err(|_| EthApiError::UnknownBlock(block_hash_or_number))? + .map(|sh| sh.header)) + } + + async fn block( + &self, + block_hash_or_number: BlockHashOrNumber, + full: bool, + ) -> Result, EthApiError> { + let maybe_header = self.header(block_hash_or_number).await?; + if maybe_header.is_none() { + return Ok(None); + } + let header = maybe_header.unwrap(); + + // The withdrawals are not supported, hence the withdrawals_root should always be empty. + if let Some(withdrawals_root) = header.withdrawals_root { + if withdrawals_root != EMPTY_ROOT_HASH { + return Err(EthApiError::Unsupported("withdrawals")); + } + } + + let transactions = self.transactions(block_hash_or_number).await?; + let block_transactions = if full { + BlockTransactions::Full(transactions.clone()) + } else { + BlockTransactions::Hashes(transactions.iter().map(|tx| tx.hash).collect()) + }; + + let signed_transactions = transactions + .into_iter() + .map(|tx| TransactionSigned::try_from(tx).map_err(|_| EthereumDataFormatError::TransactionConversion)) + .collect::, _>>()?; + + let block = reth_primitives::Block { + body: signed_transactions, + header: reth_primitives::Header::try_from(header.clone()) + .map_err(|_| EthereumDataFormatError::Primitive)?, + withdrawals: Some(Default::default()), + ..Default::default() + }; + + // This is how Reth computes the block size. + // `https://github.com/paradigmxyz/reth/blob/v0.2.0-beta.5/crates/rpc/rpc-types-compat/src/block.rs#L66` + let size = block.length(); + + Ok(Some( + Block { + header, + transactions: block_transactions, + size: Some(U256::from(size)), + withdrawals: Some(Default::default()), + ..Default::default() + } + .into(), + )) + } + + async fn transaction_count(&self, block_hash_or_number: BlockHashOrNumber) -> Result, EthApiError> { + if !self.block_exists(block_hash_or_number).await? { + return Ok(None); + } + + let filter = EthDatabaseFilterBuilder::::default() + .with_block_hash_or_number(block_hash_or_number) + .build(); + let count = self.count::(filter).await?; + Ok(Some(U256::from(count))) + } +} diff --git a/src/eth_provider/database/filter.rs b/src/eth_provider/database/filter.rs index 4276ca877..e062a0470 100644 --- a/src/eth_provider/database/filter.rs +++ b/src/eth_provider/database/filter.rs @@ -2,7 +2,7 @@ use std::fmt::{Display, LowerHex}; use mongodb::bson::{doc, Document}; use reth_primitives::{Address, B256}; -use reth_rpc_types::{Index, Topic}; +use reth_rpc_types::{BlockHashOrNumber, Index, Topic}; use crate::eth_provider::constant::{ ADDRESS_HEX_STRING_LEN, BLOCK_NUMBER_HEX_STRING_LEN, HASH_HEX_STRING_LEN, LOGS_TOPICS_HEX_STRING_LEN, @@ -165,6 +165,15 @@ impl EthDatabaseFilterBuilder { self.filter.insert(key, format_hex(number, BLOCK_NUMBER_HEX_STRING_LEN)); self } + + /// Adds a filter on the block hash or number. + #[must_use] + pub fn with_block_hash_or_number(self, block_hash_or_number: BlockHashOrNumber) -> Self { + match block_hash_or_number { + BlockHashOrNumber::Hash(hash) => self.with_block_hash(&hash), + BlockHashOrNumber::Number(number) => self.with_block_number(number), + } + } } impl EthDatabaseFilterBuilder { diff --git a/src/eth_provider/database/mod.rs b/src/eth_provider/database/mod.rs index 1d6e27a8f..248b7f383 100644 --- a/src/eth_provider/database/mod.rs +++ b/src/eth_provider/database/mod.rs @@ -76,11 +76,12 @@ impl Database { pub async fn get( &self, filter: impl Into>, - find_options: Option, + find_options: impl Into>, ) -> DatabaseResult> where T: DeserializeOwned + CollectionName, { + let find_options = find_options.into(); Ok(self.collection::().find(filter, find_options.unwrap_or_default().build()).await?.try_collect().await?) } diff --git a/src/eth_provider/error.rs b/src/eth_provider/error.rs index 19d61db01..351e33874 100644 --- a/src/eth_provider/error.rs +++ b/src/eth_provider/error.rs @@ -1,4 +1,4 @@ -use alloy_sol_types::SolType; +use alloy_sol_types::decode_revert_reason; use jsonrpsee::types::ErrorObject; use reth_primitives::{Bytes, B256}; use reth_rpc_types::BlockHashOrNumber; @@ -26,8 +26,8 @@ pub enum EthRpcErrorCode { JsonRpcVersionUnsupported = -32006, } -impl From for EthRpcErrorCode { - fn from(error: EthApiError) -> Self { +impl From<&EthApiError> for EthRpcErrorCode { + fn from(error: &EthApiError) -> Self { match error { EthApiError::UnknownBlock(_) | EthApiError::UnknownBlockNumber(_) | EthApiError::TransactionNotFound(_) => { Self::ResourceNotFound @@ -36,52 +36,56 @@ impl From for EthRpcErrorCode { | EthApiError::EthereumDataFormat(_) | EthApiError::CalldataExceededLimit(_, _) => Self::InvalidParams, EthApiError::Transaction(err) => err.into(), - EthApiError::Unsupported(_) => Self::InternalError, - EthApiError::Kakarot(err) => err.into(), + EthApiError::Unsupported(_) | EthApiError::Kakarot(_) => Self::InternalError, + EthApiError::Execution(_) => Self::ExecutionError, } } } /// Error that can occur when interacting with the ETH Api. -#[derive(Error)] +#[derive(Debug, Error)] pub enum EthApiError { /// When a block is not found - #[error("unknown block {0}")] UnknownBlock(BlockHashOrNumber), /// When an unknown block number is encountered - #[error("unknown block number {0:?}")] UnknownBlockNumber(Option), /// When a transaction is not found - #[error("transaction not found {0}")] TransactionNotFound(B256), /// Error related to transaction - #[error("transaction error: {0}")] Transaction(#[from] TransactionError), /// Error related to signing - #[error("signature error: {0}")] Signature(#[from] SignatureError), /// Unsupported feature - #[error("unsupported: {0}")] Unsupported(&'static str), /// Ethereum data format error - #[error("ethereum data format error: {0}")] EthereumDataFormat(#[from] EthereumDataFormatError), - /// Other Kakarot error - #[error("kakarot error: {0}")] + /// Execution error + Execution(#[from] ExecutionError), + /// Kakarot related error (database, ...) Kakarot(KakarotError), /// Error related to transaction calldata being too large. - #[error("calldata exceeded limit of {0}: {1}")] CalldataExceededLimit(usize, usize), } -impl std::fmt::Debug for EthApiError { +impl std::fmt::Display for EthApiError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + Self::UnknownBlock(block) => write!(f, "unknown block {block}"), + Self::UnknownBlockNumber(block) => write!(f, "unknown block number {block:?}"), + Self::TransactionNotFound(tx) => write!(f, "transaction not found {tx}"), + Self::Transaction(err) => write!(f, "{err}"), + Self::Signature(err) => write!(f, "{err}"), + Self::Unsupported(feature) => write!(f, "unsupported: {feature}"), + Self::EthereumDataFormat(err) => write!(f, "ethereum data format error: {err}"), + Self::Execution(err) => write!(f, "{err}"), Self::Kakarot(KakarotError::Provider(err)) => { + // We use Debug here otherwise we risk losing some information on contract error write!(f, "starknet provider error: {err:?}") } - Self::Kakarot(KakarotError::Execution(err)) => write!(f, "execution reverted: {err:?}"), - _ => write!(f, "{self}"), + Self::Kakarot(err) => write!(f, "kakarot error: {err}"), + Self::CalldataExceededLimit(limit, actual) => { + write!(f, "calldata exceeded limit of {limit}: {actual}") + } } } } @@ -89,8 +93,13 @@ impl std::fmt::Debug for EthApiError { /// Constructs a JSON-RPC error object, consisting of `code` and `message`. impl From for ErrorObject<'static> { fn from(value: EthApiError) -> Self { - let msg = format!("{value:?}"); - ErrorObject::owned(EthRpcErrorCode::from(value) as i32, msg, None::<()>) + let msg = format!("{value}"); + let code = EthRpcErrorCode::from(&value); + let data = match value { + EthApiError::Execution(ExecutionError::Evm(EvmError::Other(ref b))) => Some(b), + _ => None, + }; + ErrorObject::owned(code as i32, msg, data) } } @@ -108,19 +117,6 @@ pub enum KakarotError { /// Error related to the database deserialization. #[error(transparent)] DatabaseDeserialization(#[from] mongodb::bson::de::Error), - /// Error related to execution. - #[error(transparent)] - Execution(#[from] ExecutionError), -} - -impl From for KakarotError { - fn from(error: cainome::cairo_serde::Error) -> Self { - let error = error.to_string(); - if error.contains("RunResources has no remaining steps.") { - return ExecutionError::from(CairoError::VmOutOfResources).into(); - } - ExecutionError::Other(error).into() - } } impl From for EthApiError { @@ -129,28 +125,46 @@ impl From for EthApiError { } } -impl From for EthRpcErrorCode { - fn from(value: KakarotError) -> Self { - match value { - KakarotError::Execution(_) => Self::ExecutionError, - _ => Self::InternalError, - } - } -} - /// Error related to execution errors, by the EVM or Cairo vm. #[derive(Debug, Error)] pub enum ExecutionError { /// Error related to the EVM execution failures. - #[error(transparent)] Evm(#[from] EvmError), /// Error related to the Cairo vm execution failures. - #[error(transparent)] CairoVm(#[from] CairoError), - #[error("{0}")] + /// Other execution error. Other(String), } +impl From for ExecutionError { + fn from(error: cainome::cairo_serde::Error) -> Self { + let error = error.to_string(); + if error.contains("RunResources has no remaining steps.") { + return Self::CairoVm(CairoError::VmOutOfResources); + } + Self::Other(error) + } +} + +impl std::fmt::Display for ExecutionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("execution reverted")?; + match self { + Self::Evm(err) => match err { + EvmError::Other(b) => { + if let Some(reason) = decode_revert_reason(b.as_ref()) { + write!(f, ": {reason}")?; + } + Ok(()) + } + _ => write!(f, ": {err}"), + }, + Self::CairoVm(err) => write!(f, ": {err}"), + Self::Other(err) => write!(f, ": {err}"), + } + } +} + /// Error related to the Cairo vm execution failures. #[derive(Debug, Error)] pub enum CairoError { @@ -162,17 +176,17 @@ pub enum CairoError { #[derive(Debug, Error)] pub enum EvmError { #[error("validation failed")] - ValidationError, + Validation, #[error("state modification error")] - StateModificationError, + StateModification, #[error("unknown opcode")] UnknownOpcode, #[error("invalid jump dest")] InvalidJumpDest, - #[error("invalid caller")] + #[error("caller is not a Kakarot EOA")] NotKakarotEoaCaller, - #[error("view function error")] - ViewFunctionError, + #[error("function limited to view call")] + ViewFunction, #[error("stack overflow")] StackOverflow, #[error("stack underflow")] @@ -187,24 +201,18 @@ pub enum EvmError { NotImplementedPrecompile(String), #[error("invalid cairo selector")] InvalidCairoSelector, - #[error("precompile input error")] - PrecompileInputError, + #[error("precompile wrong input length")] + PrecompileInputLength, #[error("precompile flag error")] - PrecompileFlagError, - #[error("balance error")] - BalanceError, + PrecompileFlag, + #[error("transfer amount exceeds balance")] + Balance, #[error("address collision")] AddressCollision, #[error("out of gas")] OutOfGas, #[error("{0}")] - Other(String), -} - -impl From for KakarotError { - fn from(value: EvmError) -> Self { - Self::Execution(ExecutionError::Evm(value)) - } + Other(Bytes), } impl From> for EvmError { @@ -212,18 +220,18 @@ impl From> for EvmError { let bytes = value.into_iter().filter_map(|x| u8::try_from(x).ok()).collect::>(); let maybe_revert_reason = String::from_utf8(bytes.clone()); if maybe_revert_reason.is_err() { - return Self::Other(decode_err(&bytes)); + return Self::Other(bytes.into()); } let revert_reason = maybe_revert_reason.unwrap(); // safe unwrap let trimmed = revert_reason.trim_start_matches("Kakarot: ").trim_start_matches("Precompile: "); match trimmed { - "eth validation failed" => Self::ValidationError, - "StateModificationError" => Self::StateModificationError, + "eth validation failed" => Self::Validation, + "StateModificationError" => Self::StateModification, "UnknownOpcode" => Self::UnknownOpcode, "invalidJumpDestError" => Self::InvalidJumpDest, "caller contract is not a Kakarot account" => Self::NotKakarotEoaCaller, - "entrypoint should only be called in view mode" => Self::ViewFunctionError, + "entrypoint should only be called in view mode" => Self::ViewFunction, "StackOverflow" => Self::StackOverflow, "StackUnderflow" => Self::StackUnderflow, "OutOfBoundsRead" => Self::OutOfBoundsRead, @@ -235,23 +243,16 @@ impl From> for EvmError { Self::NotImplementedPrecompile(s.trim_start_matches("NotImplementedPrecompile ").to_string()) } "invalidCairoSelector" => Self::InvalidCairoSelector, - "wrong input_len" => Self::PrecompileInputError, - "flag error" => Self::PrecompileFlagError, - "transfer amount exceeds balance" => Self::BalanceError, + "wrong input_length" => Self::PrecompileInputLength, + "flag error" => Self::PrecompileFlag, + "transfer amount exceeds balance" => Self::Balance, "addressCollision" => Self::AddressCollision, s if s.contains("outOfGas") => Self::OutOfGas, - _ => Self::Other(decode_err(&bytes)), + _ => Self::Other(bytes.into()), } } } -fn decode_err(bytes: &[u8]) -> String { - // Skip the first 4 bytes which is the function selector - let msg = &bytes.get(4..); - let maybe_decoded_msg = msg.and_then(|msg| alloy_sol_types::sol_data::String::abi_decode(msg, true).ok()); - maybe_decoded_msg.map_or_else(|| format!("{}", bytes.iter().collect::()), |s| s) -} - /// Error related to a transaction. #[derive(Debug, Error)] pub enum TransactionError { @@ -273,8 +274,8 @@ pub enum TransactionError { Tracing(Box), } -impl From for EthRpcErrorCode { - fn from(error: TransactionError) -> Self { +impl From<&TransactionError> for EthRpcErrorCode { + fn from(error: &TransactionError) -> Self { match error { TransactionError::InvalidChainId | TransactionError::InvalidTransactionType => Self::InvalidInput, TransactionError::GasOverflow => Self::TransactionRejected, @@ -288,10 +289,10 @@ impl From for EthRpcErrorCode { pub enum SignatureError { /// Thrown when signer recovery fails. #[error("could not recover signer")] - RecoveryError, + Recovery, /// Thrown when signing fails. - #[error("failed to sign")] - SignError, + #[error("failed to sign transaction")] + SigningFailure, /// Thrown when signature is missing. #[error("missing signature")] MissingSignature, @@ -305,16 +306,16 @@ pub enum SignatureError { pub enum EthereumDataFormatError { /// Error related to conversion in header. #[error("header conversion error")] - HeaderConversionError, + HeaderConversion, /// Error related to conversion in receipt. #[error("header conversion error")] - ReceiptConversionError, + ReceiptConversion, /// Error related to conversion in transaction. #[error("transaction conversion error")] - TransactionConversionError, + TransactionConversion, /// Error related to starknet to eth conversion or vice versa. #[error("primitive conversion error")] - PrimitiveError, + Primitive, } #[cfg(test)] @@ -339,41 +340,81 @@ mod tests { } #[test] - fn test_decode_evm_error() { + fn test_decode_revert_message() { // Given - let bytes: Vec<_> = vec![ + let b: Vec<_> = vec![ 0x08u8, 0xc3, 0x79, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0x46, 0x61, 0x75, 0x63, 0x65, 0x74, 0x3a, 0x20, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x20, 0x74, 0x6f, 0x6f, 0x20, 0x73, 0x6f, 0x6f, 0x6e, 0x2e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + let bytes = b.clone().into_iter().map(FieldElement::from).collect::>(); + + // When + let evm_err: EvmError = bytes.into(); + let json_rpsee_error: ErrorObject<'static> = EthApiError::Execution(ExecutionError::Evm(evm_err)).into(); + + // Then + assert_eq!(json_rpsee_error.message(), "execution reverted: revert: Faucet: Claim too soon."); + assert_eq!(json_rpsee_error.code(), 3); + assert_eq!(format!("{}", json_rpsee_error.data().unwrap()), format!("\"{}\"", Bytes::from(b))); + } + + #[test] + fn test_decode_undecodable_message() { + // Given + let b = vec![ + 0x6cu8, 0xa7, 0xb8, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x71, + 0x52, 0xe0, 0x85, 0x5b, 0xab, 0x82, 0xb8, 0xe1, 0x0b, 0x86, 0x92, 0xe5, 0x84, 0xad, 0x03, 0x4b, 0xd2, 0x29, + 0x12, + ]; + let bytes = b.clone().into_iter().map(FieldElement::from).collect::>(); + + // When + let evm_err: EvmError = bytes.into(); + let json_rpsee_error: ErrorObject<'static> = EthApiError::Execution(ExecutionError::Evm(evm_err)).into(); + + // Then + assert_eq!(json_rpsee_error.message(), "execution reverted"); + assert_eq!(json_rpsee_error.code(), 3); + assert_eq!(format!("{}", json_rpsee_error.data().unwrap()), format!("\"{}\"", Bytes::from(b))); + } + + #[test] + fn test_decode_kakarot_evm_error() { + // Given + let bytes = vec![ + 0x4bu8, 0x61, 0x6b, 0x61, 0x72, 0x6f, 0x74, 0x3a, 0x20, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x70, 0x6f, 0x69, + 0x6e, 0x74, 0x20, 0x73, 0x68, 0x6f, 0x75, 0x6c, 0x64, 0x20, 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x62, 0x65, 0x20, + 0x63, 0x61, 0x6c, 0x6c, 0x65, 0x64, 0x20, 0x69, 0x6e, 0x20, 0x76, 0x69, 0x65, 0x77, 0x20, 0x6d, 0x6f, 0x64, + 0x65, ] .into_iter() .map(FieldElement::from) - .collect(); + .collect::>(); // When let evm_err: EvmError = bytes.into(); + let json_rpsee_error: ErrorObject<'static> = EthApiError::Execution(ExecutionError::Evm(evm_err)).into(); // Then - if let EvmError::Other(err) = evm_err { - assert_eq!(err, "Faucet: Claim too soon."); - } else { - panic!("Expected EvmError::Other, got {evm_err:?}"); - } + assert_eq!(json_rpsee_error.message(), "execution reverted: function limited to view call"); + assert_eq!(json_rpsee_error.code(), 3); + assert!(json_rpsee_error.data().is_none()); } #[test] fn test_display_execution_error() { // Given - let err = EthApiError::Kakarot(KakarotError::Execution(ExecutionError::Evm(EvmError::BalanceError))); + let err = EthApiError::Execution(ExecutionError::Evm(EvmError::Balance)); // When - let display = format!("{err:?}"); + let display = format!("{err}"); // Then - assert_eq!(display, "execution reverted: Evm(BalanceError)"); + assert_eq!(display, "execution reverted: transfer amount exceeds balance"); } #[test] @@ -418,10 +459,10 @@ mod tests { )); // When - let eth_err: KakarotError = err.into(); - let display = format!("{eth_err:?}"); + let eth_err: ExecutionError = err.into(); + let display = format!("{eth_err}"); // Then - assert_eq!(display, "Execution(CairoVm(VmOutOfResources))"); + assert_eq!(display, "execution reverted: cairo vm out of resources"); } } diff --git a/src/eth_provider/provider.rs b/src/eth_provider/provider.rs index 10c4bb834..2a01f7587 100644 --- a/src/eth_provider/provider.rs +++ b/src/eth_provider/provider.rs @@ -1,22 +1,21 @@ use crate::eth_provider::database::filter::format_hex; use crate::eth_provider::database::FindOpts; -use crate::eth_provider::database::{ethereum::EthereumDatabase, filter}; -use alloy_rlp::{Decodable, Encodable}; +use crate::eth_provider::database::{ethereum::EthereumTransactionStore, filter}; +use alloy_rlp::Decodable; use async_trait::async_trait; use auto_impl::auto_impl; use cainome::cairo_serde::CairoArrayLegacy; use eyre::{eyre, Result}; use itertools::Itertools; use mongodb::bson::doc; -use reth_primitives::constants::EMPTY_ROOT_HASH; use reth_primitives::{ Address, BlockId, BlockNumberOrTag, Bytes, TransactionSigned, TransactionSignedEcRecovered, TxKind, B256, U256, U64, }; use reth_rpc_types::serde_helpers::JsonStorageKey; use reth_rpc_types::txpool::TxpoolContent; use reth_rpc_types::{ - Block, BlockHashOrNumber, BlockTransactions, FeeHistory, Filter, FilterChanges, Header, Index, RichBlock, - Transaction, TransactionReceipt, TransactionRequest, + BlockHashOrNumber, FeeHistory, Filter, FilterChanges, Header, Index, RichBlock, Transaction, TransactionReceipt, + TransactionRequest, }; use reth_rpc_types::{SyncInfo, SyncStatus}; use reth_rpc_types_compat::transaction::from_recovered; @@ -27,13 +26,16 @@ use starknet_crypto::FieldElement; use super::constant::{ BLOCK_NUMBER_HEX_STRING_LEN, CALL_REQUEST_GAS_LIMIT, HASH_HEX_STRING_LEN, MAX_LOGS, TRANSACTION_MAX_RETRIES, }; +use super::database::ethereum::EthereumBlockStore; use super::database::filter::EthDatabaseFilterBuilder; use super::database::types::{ header::StoredHeader, log::StoredLog, receipt::StoredTransactionReceipt, transaction::StoredPendingTransaction, - transaction::StoredTransaction, transaction::StoredTransactionHash, + transaction::StoredTransaction, }; use super::database::{CollectionName, Database}; -use super::error::{EthApiError, EthereumDataFormatError, EvmError, KakarotError, SignatureError, TransactionError}; +use super::error::{ + EthApiError, EthereumDataFormatError, EvmError, ExecutionError, KakarotError, SignatureError, TransactionError, +}; use super::starknet::kakarot_core::{ self, account_contract::AccountContractReader, @@ -162,12 +164,8 @@ where SP: starknet::providers::Provider + Send + Sync, { async fn header(&self, block_id: &BlockId) -> EthProviderResult> { - let block = match block_id { - BlockId::Hash(hash) => BlockHashOrNumber::Hash((*hash).into()), - BlockId::Number(number_or_tag) => self.tag_into_block_number(*number_or_tag).await?.to::().into(), - }; - - Ok(self.header(block).await?.map(|h| h.header)) + let block_hash_or_number = self.block_id_into_block_number_or_hash(*block_id).await?; + Ok(self.database.header(block_hash_or_number).await?) } async fn block_number(&self) -> EthProviderResult { @@ -201,7 +199,7 @@ where } async fn block_by_hash(&self, hash: B256, full: bool) -> EthProviderResult> { - Ok(self.block(hash.into(), full).await?) + Ok(self.database.block(hash.into(), full).await?) } async fn block_by_number( @@ -210,16 +208,11 @@ where full: bool, ) -> EthProviderResult> { let block_number = self.tag_into_block_number(number_or_tag).await?; - Ok(self.block(block_number.into(), full).await?) + Ok(self.database.block(block_number.into(), full).await?) } async fn block_transaction_count_by_hash(&self, hash: B256) -> EthProviderResult> { - Ok(if self.block_exists(hash.into()).await? { - let filter = EthDatabaseFilterBuilder::::default().with_block_hash(&hash).build(); - Some(U256::from(self.database.count::(filter).await?)) - } else { - None - }) + self.database.transaction_count(hash.into()).await } async fn block_transaction_count_by_number( @@ -227,15 +220,7 @@ where number_or_tag: BlockNumberOrTag, ) -> EthProviderResult> { let block_number = self.tag_into_block_number(number_or_tag).await?; - let block_exists = self.block_exists(block_number.into()).await?; - if !block_exists { - return Ok(None); - } - - let filter = - EthDatabaseFilterBuilder::::default().with_block_number(block_number.to()).build(); - let count = self.database.count::(filter).await?; - Ok(Some(U256::from(count))) + self.database.transaction_count(block_number.into()).await } async fn transaction_by_hash(&self, hash: B256) -> EthProviderResult> { @@ -289,7 +274,7 @@ where number_or_tag: BlockNumberOrTag, index: Index, ) -> EthProviderResult> { - let block_number = self.tag_into_block_number(number_or_tag).await?.to(); + let block_number = self.tag_into_block_number(number_or_tag).await?; let filter = EthDatabaseFilterBuilder::::default() .with_block_number(block_number) .with_tx_index(&index) @@ -318,8 +303,8 @@ where if contract_not_found(&res) { return Ok(Default::default()); } - // Otherwise, extract the balance from the result, converting any errors to KakarotError - let balance = res.map_err(KakarotError::from)?.balance; + // Otherwise, extract the balance from the result, converting any errors to ExecutionError + let balance = res.map_err(ExecutionError::from)?.balance; // Convert the low and high parts of the balance to U256 let low: U256 = into_via_wrapper!(balance.low); @@ -349,7 +334,7 @@ where return Ok(U256::ZERO.into()); } - let storage = maybe_storage.map_err(KakarotError::from)?.value; + let storage = maybe_storage.map_err(ExecutionError::from)?.value; let low: U256 = into_via_wrapper!(storage.low); let high: U256 = into_via_wrapper!(storage.high); let storage: U256 = low + (high << 128); @@ -367,7 +352,7 @@ where if contract_not_found(&maybe_nonce) || entrypoint_not_found(&maybe_nonce) { return Ok(U256::ZERO); } - let nonce = maybe_nonce.map_err(KakarotError::from)?.nonce; + let nonce = maybe_nonce.map_err(ExecutionError::from)?.nonce; // Get the protocol nonce as well, in edge cases where the protocol nonce is higher than the account nonce. // This can happen when an underlying Starknet transaction reverts => Account storage changes are reverted, @@ -389,7 +374,7 @@ where return Ok(Bytes::default()); } - let bytecode = bytecode.map_err(KakarotError::from)?.bytecode.0; + let bytecode = bytecode.map_err(ExecutionError::from)?.bytecode.0; Ok(Bytes::from(bytecode.into_iter().filter_map(|x| x.try_into().ok()).collect::>())) } @@ -470,7 +455,6 @@ where } let end_block = self.tag_into_block_number(newest_block).await?; - let end_block = end_block.to::(); let end_block_plus_one = end_block.saturating_add(1); // 0 <= start_block <= end_block @@ -516,7 +500,7 @@ where async fn send_raw_transaction(&self, transaction: Bytes) -> EthProviderResult { // Decode the transaction data let transaction_signed = TransactionSigned::decode(&mut transaction.0.as_ref()) - .map_err(|_| EthApiError::EthereumDataFormat(EthereumDataFormatError::TransactionConversionError))?; + .map_err(|_| EthApiError::EthereumDataFormat(EthereumDataFormatError::TransactionConversion))?; let chain_id: u64 = self.chain_id().await?.unwrap_or_default().try_into().map_err(|_| TransactionError::InvalidChainId)?; @@ -525,7 +509,7 @@ where validate_transaction(&transaction_signed, chain_id)?; // Recover the signer from the transaction - let signer = transaction_signed.recover_signer().ok_or(SignatureError::RecoveryError)?; + let signer = transaction_signed.recover_signer().ok_or(SignatureError::Recovery)?; // Get the number of retries for the transaction let retries = self.database.pending_transaction_retries(&transaction_signed.hash).await?; @@ -535,13 +519,8 @@ where from_recovered(TransactionSignedEcRecovered::from_signed_transaction(transaction_signed.clone(), signer)); self.database.upsert_pending_transaction(transaction, retries).await?; - // The max fee is always set to 0. This means that no fee is perceived by the - // Starknet sequencer, which is the intended behavior has fee perception is - // handled by the Kakarot execution layer through EVM gas accounting. - let max_fee = 0; - // Convert the Ethereum transaction to a Starknet transaction - let starknet_transaction = to_starknet_transaction(&transaction_signed, signer, max_fee, retries)?; + let starknet_transaction = to_starknet_transaction(&transaction_signed, signer, retries)?; // Deploy EVM transaction signer if Hive feature is enabled #[cfg(feature = "hive")] @@ -567,25 +546,25 @@ where async fn gas_price(&self) -> EthProviderResult { let kakarot_contract = KakarotCoreReader::new(*KAKAROT_ADDRESS, &self.starknet_provider); - let gas_price = kakarot_contract.get_base_fee().call().await.map_err(KakarotError::from)?.base_fee; + let gas_price = kakarot_contract.get_base_fee().call().await.map_err(ExecutionError::from)?.base_fee; Ok(into_via_wrapper!(gas_price)) } async fn block_receipts(&self, block_id: Option) -> EthProviderResult>> { match block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest)) { - BlockId::Number(maybe_number) => { - let block_number = self.tag_into_block_number(maybe_number).await?; - if !self.block_exists(block_number.into()).await? { + BlockId::Number(number_or_tag) => { + let block_number = self.tag_into_block_number(number_or_tag).await?; + if !self.database.block_exists(block_number.into()).await? { return Ok(None); } let filter = - EthDatabaseFilterBuilder::::default().with_block_number(block_number.to()).build(); + EthDatabaseFilterBuilder::::default().with_block_number(block_number).build(); let tx: Vec = self.database.get(filter, None).await?; Ok(Some(tx.into_iter().map(Into::into).collect())) } BlockId::Hash(hash) => { - if !self.block_exists(hash.block_hash.into()).await? { + if !self.database.block_exists(hash.block_hash.into()).await? { return Ok(None); } let filter = @@ -599,18 +578,14 @@ where &self, block_id: Option, ) -> EthProviderResult>> { - let block_id = match block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest)) { - BlockId::Number(maybe_number) => self.tag_into_block_number(maybe_number).await?.to::().into(), - BlockId::Hash(hash) => hash.block_hash.into(), - }; - if !self.block_exists(block_id).await? { + let block_hash_or_number = self + .block_id_into_block_number_or_hash(block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest))) + .await?; + if !self.database.block_exists(block_hash_or_number).await? { return Ok(None); } - match self.transactions(block_id, true).await? { - BlockTransactions::Full(transactions) => Ok(Some(transactions)), - _ => Err(TransactionError::ExpectedFullTransactions.into()), - } + Ok(Some(self.database.transactions(block_hash_or_number).await?)) } async fn txpool_transactions(&self) -> EthProviderResult> { @@ -720,11 +695,11 @@ where .block_id(starknet_block_id) .call() .await - .map_err(KakarotError::from)?; + .map_err(ExecutionError::from)?; let return_data = call_output.return_data; if call_output.success == FieldElement::ZERO { - return Err(KakarotError::from(EvmError::from(return_data.0)).into()); + return Err(ExecutionError::from(EvmError::from(return_data.0)).into()); } Ok(return_data) } @@ -755,97 +730,16 @@ where .block_id(starknet_block_id) .call() .await - .map_err(KakarotError::from)?; + .map_err(ExecutionError::from)?; let return_data = estimate_gas_output.return_data; if estimate_gas_output.success == FieldElement::ZERO { - return Err(KakarotError::from(EvmError::from(return_data.0)).into()); + return Err(ExecutionError::from(EvmError::from(return_data.0)).into()); } let required_gas = estimate_gas_output.required_gas.try_into().map_err(|_| TransactionError::GasOverflow)?; Ok(required_gas) } - /// Check if a block exists in the database. - async fn block_exists(&self, block_id: BlockHashOrNumber) -> EthProviderResult { - Ok(self.header(block_id).await?.is_some()) - } - - /// Get a header from the database based on the filter. - async fn header(&self, id: BlockHashOrNumber) -> EthProviderResult> { - let builder = EthDatabaseFilterBuilder::::default(); - let filter = match id { - BlockHashOrNumber::Hash(hash) => builder.with_block_hash(&hash), - BlockHashOrNumber::Number(number) => builder.with_block_number(number), - } - .build(); - self.database - .get_one(filter, None) - .await - .inspect_err(|err| tracing::error!("internal error: {:?}", err)) - .map_err(|_| EthApiError::UnknownBlock(id)) - } - - /// Return the transactions given a block id. - pub(crate) async fn transactions( - &self, - block_id: BlockHashOrNumber, - full: bool, - ) -> EthProviderResult { - let builder = EthDatabaseFilterBuilder::::default(); - let filter = match block_id { - BlockHashOrNumber::Hash(hash) => builder.with_block_hash(&hash), - BlockHashOrNumber::Number(number) => builder.with_block_number(number), - } - .build(); - let block_transactions = if full { - BlockTransactions::Full(self.database.get_and_map_to::<_, StoredTransaction>(filter, None).await?) - } else { - BlockTransactions::Hashes( - self.database - .get_and_map_to::<_, StoredTransactionHash>( - filter, - Some(FindOpts::default().with_projection(doc! {"tx.hash": 1})), - ) - .await?, - ) - }; - - Ok(block_transactions) - } - - /// Get a block from the database based on a block hash or number. - /// If full is true, the block will contain the full transactions, otherwise just the hashes - async fn block(&self, block_id: BlockHashOrNumber, full: bool) -> EthProviderResult> { - let header = match self.header(block_id).await? { - Some(h) => h.header, - None => return Ok(None), - }; - - // The withdrawals are not supported, hence the withdrawals_root should always be empty. - if let Some(withdrawals_root) = header.withdrawals_root { - if withdrawals_root != EMPTY_ROOT_HASH { - return Err(EthApiError::Unsupported("withdrawals")); - } - } - - // This is how reth computes the block size. - // `https://github.com/paradigmxyz/reth/blob/v0.2.0-beta.5/crates/rpc/rpc-types-compat/src/block.rs#L66` - let size = reth_primitives::Header::try_from(header.clone()) - .map_err(|_| EthereumDataFormatError::PrimitiveError)? - .length(); - Ok(Some( - Block { - header, - transactions: self.transactions(block_id, full).await?, - uncles: Default::default(), - size: Some(U256::from(size)), - withdrawals: Some(Default::default()), - other: Default::default(), - } - .into(), - )) - } - /// Convert the given block id into a Starknet block id pub async fn to_starknet_block_id( &self, @@ -863,10 +757,13 @@ where // 3. The block number is not found, then we return an error match number_or_tag { BlockNumberOrTag::Number(number) => { - let header = - self.header(number.into()).await?.ok_or(EthApiError::UnknownBlockNumber(Some(number)))?; + let header = self + .database + .header(number.into()) + .await? + .ok_or(EthApiError::UnknownBlockNumber(Some(number)))?; // If the block hash is zero, then the block corresponds to a Starknet pending block - if header.header.hash.ok_or(EthApiError::UnknownBlock(number.into()))?.is_zero() { + if header.hash.ok_or(EthApiError::UnknownBlock(number.into()))?.is_zero() { Ok(starknet::core::types::BlockId::Tag(starknet::core::types::BlockTag::Pending)) } else { Ok(starknet::core::types::BlockId::Number(number)) @@ -880,18 +777,26 @@ where } /// Converts the given [`BlockNumberOrTag`] into a block number. - async fn tag_into_block_number(&self, tag: BlockNumberOrTag) -> EthProviderResult { + async fn tag_into_block_number(&self, tag: BlockNumberOrTag) -> EthProviderResult { match tag { // Converts the tag representing the earliest block into block number 0. - BlockNumberOrTag::Earliest => Ok(U64::ZERO), + BlockNumberOrTag::Earliest => Ok(0), // Converts the tag containing a specific block number into a `U64`. - BlockNumberOrTag::Number(number) => Ok(U64::from(number)), + BlockNumberOrTag::Number(number) => Ok(number), // Returns `self.block_number()` which is the block number of the latest finalized block. BlockNumberOrTag::Latest | BlockNumberOrTag::Finalized | BlockNumberOrTag::Safe => { - self.block_number().await + self.block_number().await.map(|x| x.to()) } // Adds 1 to the block number of the latest finalized block. - BlockNumberOrTag::Pending => Ok(self.block_number().await?.saturating_add(U64::from(1))), + BlockNumberOrTag::Pending => Ok(self.block_number().await?.to::().saturating_add(1)), + } + } + + /// Converts the given [`BlockId`] into a [`BlockHashOrNumber`]. + async fn block_id_into_block_number_or_hash(&self, block_id: BlockId) -> EthProviderResult { + match block_id { + BlockId::Hash(hash) => Ok(BlockHashOrNumber::Hash(hash.into())), + BlockId::Number(number_or_tag) => Ok(self.tag_into_block_number(number_or_tag).await?.into()), } } } @@ -934,10 +839,10 @@ where .nonce(current_nonce) .max_fee(u64::MAX.into()) .prepared() - .map_err(|_| EthApiError::EthereumDataFormat(EthereumDataFormatError::TransactionConversionError))? + .map_err(|_| EthApiError::EthereumDataFormat(EthereumDataFormatError::TransactionConversion))? .get_invoke_request(false) .await - .map_err(|_| SignatureError::SignError)?; + .map_err(|_| SignatureError::SigningFailure)?; self.starknet_provider.add_invoke_transaction(tx).await.map_err(KakarotError::from)?; *nonce += 1u8.into(); diff --git a/src/eth_provider/starknet/kakarot_core.rs b/src/eth_provider/starknet/kakarot_core.rs index bd1e4274a..2703fea4c 100644 --- a/src/eth_provider/starknet/kakarot_core.rs +++ b/src/eth_provider/starknet/kakarot_core.rs @@ -93,7 +93,6 @@ pub fn starknet_address(address: Address) -> FieldElement { pub fn to_starknet_transaction( transaction: &TransactionSigned, signer: Address, - max_fee: u64, retries: u8, ) -> EthProviderResult { let sender_address = starknet_address(signer); @@ -104,8 +103,11 @@ pub fn to_starknet_transaction( // Transform the transaction's data to Starknet calldata let calldata = transaction_data_to_starknet_calldata(transaction, retries)?; + // The max fee is always set to 0. This means that no fee is perceived by the + // Starknet sequencer, which is the intended behavior as fee perception is + // handled by the Kakarot execution layer through EVM gas accounting. Ok(BroadcastedInvokeTransaction::V1(BroadcastedInvokeTransactionV1 { - max_fee: max_fee.into(), + max_fee: FieldElement::ZERO, signature, nonce: transaction.nonce().into(), sender_address, @@ -131,7 +133,6 @@ mod tests { &transaction, Address::from_str("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266").unwrap(), 0, - 0, ) .unwrap() { @@ -194,7 +195,7 @@ mod tests { } #[test] - #[should_panic(expected = "calldata exceeded limit of 22500: 30008")] + #[should_panic(expected = "CalldataExceededLimit(22500, 30008)")] fn to_starknet_transaction_too_large_calldata_test() { // Test that an example create transaction from goerli decodes properly let tx_bytes = hex!("b901f202f901ee05228459682f008459682f11830209bf8080b90195608060405234801561001057600080fd5b50610175806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80630c49c36c14610030575b600080fd5b61003861004e565b604051610045919061011d565b60405180910390f35b60606020600052600f6020527f68656c6c6f2073746174656d696e64000000000000000000000000000000000060405260406000f35b600081519050919050565b600082825260208201905092915050565b60005b838110156100be5780820151818401526020810190506100a3565b838111156100cd576000848401525b50505050565b6000601f19601f8301169050919050565b60006100ef82610084565b6100f9818561008f565b93506101098185602086016100a0565b610112816100d3565b840191505092915050565b6000602082019050818103600083015261013781846100e4565b90509291505056fea264697066735822122051449585839a4ea5ac23cae4552ef8a96b64ff59d0668f76bfac3796b2bdbb3664736f6c63430008090033c080a0136ebffaa8fc8b9fda9124de9ccb0b1f64e90fbd44251b4c4ac2501e60b104f9a07eb2999eec6d185ef57e91ed099afb0a926c5b536f0155dd67e537c7476e1471"); @@ -212,6 +213,6 @@ mod tests { transaction.transaction.set_input(vec![0; 30000 * 31].into()); // Attempt to convert the transaction into a Starknet transaction - to_starknet_transaction(&transaction, transaction.recover_signer().unwrap(), 100_000_000, 0).unwrap(); + to_starknet_transaction(&transaction, transaction.recover_signer().unwrap(), 0).unwrap(); } } diff --git a/src/eth_rpc/servers/alchemy_rpc.rs b/src/eth_rpc/servers/alchemy_rpc.rs index 0f1e33849..a3855989e 100644 --- a/src/eth_rpc/servers/alchemy_rpc.rs +++ b/src/eth_rpc/servers/alchemy_rpc.rs @@ -1,10 +1,11 @@ use futures::future::join_all; -use jsonrpsee::core::{async_trait, RpcResult as Result}; +use jsonrpsee::core::{async_trait, RpcResult}; use reth_primitives::{Address, BlockId, BlockNumberOrTag}; use crate::eth_provider::contracts::erc20::EthereumErc20; +use crate::eth_provider::error::EthApiError; use crate::eth_rpc::api::alchemy_api::AlchemyApiServer; -use crate::models::balance::FutureTokenBalance; +use crate::models::balance::TokenBalanceFuture; use crate::{eth_provider::provider::EthereumProvider, models::balance::TokenBalances}; /// The RPC module for the Ethereum protocol required by Kakarot. @@ -22,16 +23,18 @@ impl AlchemyRpc

{ #[async_trait] impl AlchemyApiServer for AlchemyRpc

{ #[tracing::instrument(skip_all, ret, fields(address = %address, token_addresses = ?token_addresses))] - async fn token_balances(&self, address: Address, token_addresses: Vec

) -> Result { + async fn token_balances(&self, address: Address, token_addresses: Vec
) -> RpcResult { + tracing::info!("Serving alchemy_getTokenBalances"); + let block_id = BlockId::Number(BlockNumberOrTag::Latest); let handles = token_addresses.into_iter().map(|token_addr| { let token = EthereumErc20::new(token_addr, &self.eth_provider); let balance = token.balance_of(address, block_id); - FutureTokenBalance::new(Box::pin(balance), token_addr) + TokenBalanceFuture::new(Box::pin(balance), token_addr) }); - let token_balances = join_all(handles).await; + let token_balances = join_all(handles).await.into_iter().collect::, EthApiError>>()?; Ok(TokenBalances { address, token_balances }) } diff --git a/src/eth_rpc/servers/debug_rpc.rs b/src/eth_rpc/servers/debug_rpc.rs index 970f33bd7..b154e1897 100644 --- a/src/eth_rpc/servers/debug_rpc.rs +++ b/src/eth_rpc/servers/debug_rpc.rs @@ -29,6 +29,8 @@ impl DebugApiServer for DebugRpc

/// Returns an RLP-encoded header. #[tracing::instrument(skip(self), err, fields(block_id = ?block_id))] async fn raw_header(&self, block_id: BlockId) -> Result { + tracing::info!("Serving debug_getRawHeader"); + let mut res = Vec::new(); if let Some(header) = self .eth_provider @@ -36,7 +38,7 @@ impl DebugApiServer for DebugRpc

.await? .map(Header::try_from) .transpose() - .map_err(|_| EthApiError::EthereumDataFormat(EthereumDataFormatError::HeaderConversionError))? + .map_err(|_| EthApiError::EthereumDataFormat(EthereumDataFormatError::HeaderConversion))? { header.encode(&mut res); } @@ -47,6 +49,8 @@ impl DebugApiServer for DebugRpc

/// Returns an RLP-encoded block. #[tracing::instrument(skip(self), err, fields(block_id = ?block_id))] async fn raw_block(&self, block_id: BlockId) -> Result { + tracing::info!("Serving debug_getRawBlock"); + let block = match block_id { BlockId::Hash(hash) => self.eth_provider.block_by_hash(hash.into(), true).await?, BlockId::Number(number) => self.eth_provider.block_by_number(number, true).await?, @@ -54,7 +58,7 @@ impl DebugApiServer for DebugRpc

let mut raw_block = Vec::new(); if let Some(block) = block { let block = - Block::try_from(block.inner).map_err(|_| EthApiError::from(EthereumDataFormatError::PrimitiveError))?; + Block::try_from(block.inner).map_err(|_| EthApiError::from(EthereumDataFormatError::Primitive))?; block.encode(&mut raw_block); } Ok(raw_block.into()) @@ -65,12 +69,13 @@ impl DebugApiServer for DebugRpc

/// If this is a pooled EIP-4844 transaction, the blob sidecar is included. #[tracing::instrument(skip(self), err, fields(hash = ?hash))] async fn raw_transaction(&self, hash: B256) -> Result> { + tracing::info!("Serving debug_getRawTransaction"); + let transaction = self.eth_provider.transaction_by_hash(hash).await?; if let Some(tx) = transaction { let signature = tx.signature.ok_or_else(|| EthApiError::from(SignatureError::MissingSignature))?; - let tx = - tx.try_into().map_err(|_| EthApiError::EthereumDataFormat(EthereumDataFormatError::PrimitiveError))?; + let tx = tx.try_into().map_err(|_| EthApiError::EthereumDataFormat(EthereumDataFormatError::Primitive))?; let bytes = TransactionSigned::from_transaction_and_signature( tx, reth_primitives::Signature { @@ -89,13 +94,14 @@ impl DebugApiServer for DebugRpc

/// Returns an array of EIP-2718 binary-encoded transactions for the given [BlockId]. #[tracing::instrument(skip(self), err, fields(block_id = ?block_id))] async fn raw_transactions(&self, block_id: BlockId) -> Result> { + tracing::info!("Serving debug_getRawTransactions"); + let transactions = self.eth_provider.block_transactions(Some(block_id)).await?.unwrap_or_default(); let mut raw_transactions = Vec::with_capacity(transactions.len()); for t in transactions { let signature = t.signature.ok_or_else(|| EthApiError::from(SignatureError::MissingSignature))?; - let tx = - t.try_into().map_err(|_| EthApiError::EthereumDataFormat(EthereumDataFormatError::PrimitiveError))?; + let tx = t.try_into().map_err(|_| EthApiError::EthereumDataFormat(EthereumDataFormatError::Primitive))?; let bytes = TransactionSigned::from_transaction_and_signature( tx, reth_primitives::Signature { @@ -114,6 +120,8 @@ impl DebugApiServer for DebugRpc

/// Returns an array of EIP-2718 binary-encoded receipts. #[tracing::instrument(skip(self), err, fields(block_id = ?block_id))] async fn raw_receipts(&self, block_id: BlockId) -> Result> { + tracing::info!("Serving debug_getRawReceipts"); + let receipts = self.eth_provider.block_receipts(Some(block_id)).await?.unwrap_or_default(); // Initializes an empty vector to store the raw receipts @@ -124,11 +132,11 @@ impl DebugApiServer for DebugRpc

// Converts the transaction type to a u8 and then tries to convert it into TxType let tx_type = Into::::into(receipt.transaction_type()) .try_into() - .map_err(|_| EthApiError::EthereumDataFormat(EthereumDataFormatError::ReceiptConversionError))?; + .map_err(|_| EthApiError::EthereumDataFormat(EthereumDataFormatError::ReceiptConversion))?; // Tries to convert the cumulative gas used to u64 let cumulative_gas_used = TryInto::::try_into(receipt.inner.cumulative_gas_used()) - .map_err(|_| EthApiError::EthereumDataFormat(EthereumDataFormatError::ReceiptConversionError))?; + .map_err(|_| EthApiError::EthereumDataFormat(EthereumDataFormatError::ReceiptConversion))?; // Creates a ReceiptWithBloom from the receipt data raw_receipts.push( @@ -161,6 +169,8 @@ impl DebugApiServer for DebugRpc

block_number: BlockNumberOrTag, opts: Option, ) -> Result> { + tracing::info!("Serving debug_traceBlockByNumber"); + let provider = Arc::new(&self.eth_provider); let tracer = TracerBuilder::new(provider) .await? @@ -179,6 +189,8 @@ impl DebugApiServer for DebugRpc

block_hash: B256, opts: Option, ) -> Result> { + tracing::info!("Serving debug_traceBlockByHash"); + let tracer = TracerBuilder::new(Arc::new(&self.eth_provider)) .await? .with_block_id(BlockId::Hash(block_hash.into())) @@ -196,6 +208,8 @@ impl DebugApiServer for DebugRpc

transaction_hash: B256, opts: Option, ) -> Result { + tracing::info!("Serving debug_traceTransaction"); + let tracer = TracerBuilder::new(Arc::new(&self.eth_provider)) .await? .with_transaction_hash(transaction_hash) diff --git a/src/eth_rpc/servers/eth_rpc.rs b/src/eth_rpc/servers/eth_rpc.rs index 5e2d36d94..c5dcc61f0 100644 --- a/src/eth_rpc/servers/eth_rpc.rs +++ b/src/eth_rpc/servers/eth_rpc.rs @@ -39,11 +39,13 @@ where { #[tracing::instrument(skip_all, ret, err)] async fn block_number(&self) -> Result { + tracing::info!("Serving eth_blockNumber"); Ok(self.eth_provider.block_number().await?) } #[tracing::instrument(skip_all, ret, err)] async fn syncing(&self) -> Result { + tracing::info!("Serving eth_syncing"); Ok(self.eth_provider.syncing().await?) } @@ -53,31 +55,37 @@ where #[tracing::instrument(skip_all, ret, err)] async fn accounts(&self) -> Result> { + tracing::info!("Serving eth_accounts"); Ok(Vec::new()) } #[tracing::instrument(skip_all, ret, err)] async fn chain_id(&self) -> Result> { + tracing::info!("Serving eth_chainId"); Ok(self.eth_provider.chain_id().await?) } #[tracing::instrument(skip(self), ret, err, fields(hash = %hash))] async fn block_by_hash(&self, hash: B256, full: bool) -> Result> { + tracing::info!("Serving eth_getBlockByHash"); Ok(self.eth_provider.block_by_hash(hash, full).await?) } #[tracing::instrument(skip(self), err, fields(number = %number, full = full))] async fn block_by_number(&self, number: BlockNumberOrTag, full: bool) -> Result> { + tracing::info!("Serving eth_getBlockByNumber"); Ok(self.eth_provider.block_by_number(number, full).await?) } #[tracing::instrument(skip(self), ret, err, fields(hash = %hash))] async fn block_transaction_count_by_hash(&self, hash: B256) -> Result> { + tracing::info!("Serving eth_getBlockTransactionCountByHash"); Ok(self.eth_provider.block_transaction_count_by_hash(hash).await?) } #[tracing::instrument(skip(self), ret, err, fields(number = %number))] async fn block_transaction_count_by_number(&self, number: BlockNumberOrTag) -> Result> { + tracing::info!("Serving eth_getBlockTransactionCountByNumber"); Ok(self.eth_provider.block_transaction_count_by_number(number).await?) } @@ -111,11 +119,13 @@ where #[tracing::instrument(skip(self), ret, err, fields(hash = %hash))] async fn transaction_by_hash(&self, hash: B256) -> Result> { + tracing::info!("Serving eth_getTransactionByHash"); Ok(self.eth_provider.transaction_by_hash(hash).await?) } #[tracing::instrument(skip(self), ret, err, fields(hash = %hash, index = ?index))] async fn transaction_by_block_hash_and_index(&self, hash: B256, index: Index) -> Result> { + tracing::info!("Serving eth_getTransactionByBlockHashAndIndex"); Ok(self.eth_provider.transaction_by_block_hash_and_index(hash, index).await?) } @@ -125,41 +135,49 @@ where number: BlockNumberOrTag, index: Index, ) -> Result> { + tracing::info!("Serving eth_getTransactionByBlockNumberAndIndex"); Ok(self.eth_provider.transaction_by_block_number_and_index(number, index).await?) } #[tracing::instrument(skip(self), ret, err, fields(hash = %hash))] async fn transaction_receipt(&self, hash: B256) -> Result> { + tracing::info!("Serving eth_getTransactionReceipt"); Ok(self.eth_provider.transaction_receipt(hash).await?) } #[tracing::instrument(skip(self), ret, err, fields(address = %address, block_id = ?block_id))] async fn balance(&self, address: Address, block_id: Option) -> Result { + tracing::info!("Serving eth_getBalance"); Ok(self.eth_provider.balance(address, block_id).await?) } #[tracing::instrument(skip(self), ret, err, fields(address = %address, index = ?index, block_id = ?block_id))] async fn storage_at(&self, address: Address, index: JsonStorageKey, block_id: Option) -> Result { + tracing::info!("Serving eth_getStorageAt"); Ok(self.eth_provider.storage_at(address, index, block_id).await?) } #[tracing::instrument(skip(self), ret, err, fields(address = %address, block_id = ?block_id))] async fn transaction_count(&self, address: Address, block_id: Option) -> Result { + tracing::info!("Serving eth_getTransactionCount"); Ok(self.eth_provider.transaction_count(address, block_id).await?) } #[tracing::instrument(skip(self), err, fields(address = %address, block_id = ?block_id))] async fn get_code(&self, address: Address, block_id: Option) -> Result { + tracing::info!("Serving eth_getCode"); Ok(self.eth_provider.get_code(address, block_id).await?) } #[tracing::instrument(skip(self), err, fields(filter = ?filter))] async fn get_logs(&self, filter: Filter) -> Result { + tracing::info!("Serving eth_getLogs"); Ok(self.eth_provider.get_logs(filter).await?) } #[tracing::instrument(skip(self, request), err, fields(block_id = ?block_id, gas_limit = request.gas))] async fn call(&self, request: TransactionRequest, block_id: Option) -> Result { + tracing::info!("Serving eth_call"); Ok(self.eth_provider.call(request, block_id).await?) } @@ -173,11 +191,13 @@ where #[tracing::instrument(skip(self, request), err, fields(block_id = ?block_id, gas_limit = request.gas))] async fn estimate_gas(&self, request: TransactionRequest, block_id: Option) -> Result { + tracing::info!("Serving eth_estimateGas"); Ok(self.eth_provider.estimate_gas(request, block_id).await?) } #[tracing::instrument(skip_all, ret, err)] async fn gas_price(&self) -> Result { + tracing::info!("Serving eth_gasPrice"); Ok(self.eth_provider.gas_price().await?) } @@ -188,11 +208,13 @@ where newest_block: BlockNumberOrTag, reward_percentiles: Option>, ) -> Result { + tracing::info!("Serving eth_feeHistory"); Ok(self.eth_provider.fee_history(block_count, newest_block, reward_percentiles).await?) } #[tracing::instrument(skip_all, ret, err)] async fn max_priority_fee_per_gas(&self) -> Result { + tracing::info!("Serving eth_maxPriorityFeePerGas"); Ok(U256::from(*MAX_PRIORITY_FEE_PER_GAS)) } @@ -229,6 +251,7 @@ where #[tracing::instrument(skip_all, ret, err)] async fn send_raw_transaction(&self, bytes: Bytes) -> Result { + tracing::info!("Serving eth_sendRawTransaction"); Ok(self.eth_provider.send_raw_transaction(bytes).await?) } diff --git a/src/eth_rpc/servers/trace_rpc.rs b/src/eth_rpc/servers/trace_rpc.rs index f38fa95e4..7747c952c 100644 --- a/src/eth_rpc/servers/trace_rpc.rs +++ b/src/eth_rpc/servers/trace_rpc.rs @@ -26,6 +26,7 @@ impl TraceApiServer for TraceRpc

#[allow(clippy::blocks_in_conditions)] #[tracing::instrument(skip(self), err, fields(block_id = ?block_id))] async fn trace_block(&self, block_id: BlockId) -> Result>> { + tracing::info!("Serving debug_traceBlock"); let tracer = TracerBuilder::new(Arc::new(&self.eth_provider)) .await? .with_block_id(block_id) diff --git a/src/lib.rs b/src/lib.rs index 21e840077..320955de1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,6 @@ +#![cfg_attr(not(any(test, feature = "testing")), warn(unused_crate_dependencies))] +use tracing_subscriber as _; + pub mod config; pub mod eth_provider; pub mod eth_rpc; diff --git a/src/main.rs b/src/main.rs index 46d7e5795..3816a6515 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use dotenvy::dotenv; use eyre::Result; -use kakarot_rpc::config::{JsonRpcClientBuilder, KakarotRpcConfig, Network, SequencerGatewayProviderBuilder}; +use kakarot_rpc::config::KakarotRpcConfig; use kakarot_rpc::eth_provider::database::Database; use kakarot_rpc::eth_provider::pending_pool::start_retry_service; use kakarot_rpc::eth_provider::provider::EthDataProvider; @@ -12,38 +12,25 @@ use kakarot_rpc::eth_rpc::rpc::KakarotRpcModuleBuilder; use kakarot_rpc::eth_rpc::run_server; use mongodb::options::{DatabaseOptions, ReadConcern, WriteConcern}; use starknet::providers::jsonrpc::HttpTransport; -use starknet::providers::{JsonRpcClient, SequencerGatewayProvider}; +use starknet::providers::JsonRpcClient; use tracing_subscriber::{filter, util::SubscriberInitExt}; -enum StarknetProvider { - JsonRpcClient(JsonRpcClient), - SequencerGatewayProvider(SequencerGatewayProvider), -} - #[tokio::main] async fn main() -> Result<()> { - dotenv().ok(); // Environment variables are safe to use after this + dotenv().ok(); + let filter = format!("kakarot_rpc={}", std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string())); let filter = filter::EnvFilter::new(filter); tracing_subscriber::FmtSubscriber::builder().with_env_filter(filter).finish().try_init()?; + // Load the configuration let starknet_config = KakarotRpcConfig::from_env()?; - let rpc_config = RPCConfig::from_env()?; - let starknet_provider = match &starknet_config.network { - Network::Madara | Network::Katana | Network::Sharingan => { - StarknetProvider::JsonRpcClient(JsonRpcClientBuilder::with_http(&starknet_config).unwrap().build()) - } - Network::JsonRpcProvider(url) => { - StarknetProvider::JsonRpcClient(JsonRpcClientBuilder::new(HttpTransport::new(url.clone())).build()) - } - _ => StarknetProvider::SequencerGatewayProvider( - SequencerGatewayProviderBuilder::new(&starknet_config.network).build(), - ), - }; + let starknet_provider = JsonRpcClient::new(HttpTransport::new(starknet_config.network_url)); + // Setup the database let db_client = mongodb::Client::with_uri_str(var("MONGO_CONNECTION_STRING").expect("Missing MONGO_CONNECTION_STRING .env")) .await?; @@ -52,42 +39,16 @@ async fn main() -> Result<()> { DatabaseOptions::builder().read_concern(ReadConcern::MAJORITY).write_concern(WriteConcern::MAJORITY).build(), )); - // Get the deployer nonce and set the value in the DEPLOY_WALLET_NONCE + // Setup hive #[cfg(feature = "hive")] - { - use kakarot_rpc::eth_provider::constant::{CHAIN_ID, DEPLOY_WALLET, DEPLOY_WALLET_NONCE}; - use starknet::accounts::ConnectedAccount; - use starknet::providers::Provider as _; - use starknet_crypto::FieldElement; - - let provider = JsonRpcClient::new(HttpTransport::new( - starknet_config.network.provider_url().expect("Incorrect provider URL"), - )); - let chain_id = provider.chain_id().await?; - let chain_id: u64 = (FieldElement::from(u64::MAX) & chain_id).try_into()?; - - CHAIN_ID.set(chain_id.into()).expect("Failed to set chain id"); - - let deployer_nonce = DEPLOY_WALLET.get_nonce().await?; - let mut nonce = DEPLOY_WALLET_NONCE.lock().await; - *nonce = deployer_nonce; - } - - let kakarot_rpc_module = match starknet_provider { - StarknetProvider::JsonRpcClient(starknet_provider) => { - let starknet_provider = Arc::new(starknet_provider); - let eth_provider = EthDataProvider::new(db.clone(), starknet_provider).await?; - tokio::spawn(start_retry_service(eth_provider.clone())); - KakarotRpcModuleBuilder::new(eth_provider).rpc_module()? - } - StarknetProvider::SequencerGatewayProvider(starknet_provider) => { - let starknet_provider = Arc::new(starknet_provider); - let eth_provider = EthDataProvider::new(db.clone(), starknet_provider).await?; - tokio::spawn(start_retry_service(eth_provider.clone())); - KakarotRpcModuleBuilder::new(eth_provider).rpc_module()? - } - }; + setup_hive(&starknet_provider).await?; + + // Setup the retry service + let eth_provider = EthDataProvider::new(db, Arc::new(starknet_provider)).await?; + tokio::spawn(start_retry_service(eth_provider.clone())); + let kakarot_rpc_module = KakarotRpcModuleBuilder::new(eth_provider).rpc_module()?; + // Start the RPC server let (socket_addr, server_handle) = run_server(kakarot_rpc_module, rpc_config).await?; let url = format!("http://{socket_addr}"); @@ -98,3 +59,23 @@ async fn main() -> Result<()> { Ok(()) } + +#[allow(clippy::significant_drop_tightening)] +#[cfg(feature = "hive")] +async fn setup_hive(starknet_provider: &JsonRpcClient) -> Result<()> { + use kakarot_rpc::eth_provider::constant::{CHAIN_ID, DEPLOY_WALLET, DEPLOY_WALLET_NONCE}; + use starknet::accounts::ConnectedAccount; + use starknet::providers::Provider as _; + use starknet_crypto::FieldElement; + + let chain_id = starknet_provider.chain_id().await?; + let chain_id: u64 = (FieldElement::from(u64::MAX) & chain_id).try_into()?; + + CHAIN_ID.set(chain_id.into()).expect("Failed to set chain id"); + + let deployer_nonce = DEPLOY_WALLET.get_nonce().await?; + let mut nonce = DEPLOY_WALLET_NONCE.lock().await; + *nonce = deployer_nonce; + + Ok(()) +} diff --git a/src/models/balance.rs b/src/models/balance.rs index 5aecfeadd..5f97552b8 100644 --- a/src/models/balance.rs +++ b/src/models/balance.rs @@ -1,17 +1,17 @@ +use std::fmt; use std::pin::Pin; use std::task::Poll; -use futures::{Future, FutureExt}; +use futures::{future::BoxFuture, Future, FutureExt}; use reth_primitives::{Address, U256}; use serde::{Deserialize, Serialize}; -use crate::eth_provider::error::EthApiError; +use crate::eth_provider::provider::EthProviderResult; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct TokenBalance { pub token_address: Address, - pub token_balance: Option, - pub error: Option, + pub token_balance: U256, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -20,22 +20,31 @@ pub struct TokenBalances { pub token_balances: Vec, } -type BalanceOfResult = Result; - -#[derive(Debug)] -pub struct FutureTokenBalance> { - pub balance: F, +pub struct TokenBalanceFuture<'a> { + pub balance: BoxFuture<'a, EthProviderResult>, pub token_address: Address, } -impl> FutureTokenBalance { - pub const fn new(balance: F, token_address: Address) -> Self { - Self { balance, token_address } +impl<'a> fmt::Debug for TokenBalanceFuture<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TokenBalanceFuture") + .field("balance", &"...") + .field("token_address", &self.token_address) + .finish() + } +} + +impl<'a> TokenBalanceFuture<'a> { + pub fn new(balance: F, token_address: Address) -> Self + where + F: Future> + Send + 'a, + { + Self { balance: Box::pin(balance), token_address } } } -impl + Unpin> Future for FutureTokenBalance { - type Output = TokenBalance; +impl<'a> Future for TokenBalanceFuture<'a> { + type Output = EthProviderResult; fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll { let balance = self.balance.poll_unpin(cx); @@ -43,10 +52,8 @@ impl + Unpin> Future for FutureTokenBalance< match balance { Poll::Ready(output) => match output { - Ok(balance) => Poll::Ready(TokenBalance { token_address, token_balance: Some(balance), error: None }), - Err(error) => { - Poll::Ready(TokenBalance { token_address, token_balance: None, error: Some(error.to_string()) }) - } + Ok(token_balance) => Poll::Ready(Ok(TokenBalance { token_address, token_balance })), + Err(err) => Poll::Ready(Err(err)), }, Poll::Pending => Poll::Pending, } diff --git a/src/models/felt.rs b/src/models/felt.rs index 992a19779..721f7e099 100644 --- a/src/models/felt.rs +++ b/src/models/felt.rs @@ -51,7 +51,7 @@ impl TryFrom for Address { fn try_from(felt: Felt252Wrapper) -> Result { EthAddress::from_felt(&felt) .map(|eth_address| Self::from_slice(eth_address.as_bytes())) - .map_err(|_| EthereumDataFormatError::PrimitiveError) + .map_err(|_| EthereumDataFormatError::Primitive) } } @@ -59,7 +59,7 @@ impl TryFrom for Felt252Wrapper { type Error = EthereumDataFormatError; fn try_from(value: B256) -> Result { - Ok(Self(FieldElement::from_bytes_be(value.as_ref()).map_err(|_| EthereumDataFormatError::PrimitiveError)?)) + Ok(Self(FieldElement::from_bytes_be(value.as_ref()).map_err(|_| EthereumDataFormatError::Primitive)?)) } } @@ -67,7 +67,7 @@ impl TryFrom for Felt252Wrapper { type Error = EthereumDataFormatError; fn try_from(u256: U256) -> Result { - Ok(Self(FieldElement::from_bytes_be(&u256.to_be_bytes()).map_err(|_| EthereumDataFormatError::PrimitiveError)?)) + Ok(Self(FieldElement::from_bytes_be(&u256.to_be_bytes()).map_err(|_| EthereumDataFormatError::Primitive)?)) } } @@ -110,7 +110,7 @@ macro_rules! into_via_try_wrapper { ($val: expr) => {{ let intermediate: Result<_, $crate::eth_provider::error::EthereumDataFormatError> = TryInto::<$crate::models::felt::Felt252Wrapper>::try_into($val) - .map_err(|_| $crate::eth_provider::error::EthereumDataFormatError::PrimitiveError) + .map_err(|_| $crate::eth_provider::error::EthereumDataFormatError::Primitive) .map(Into::into); intermediate }}; @@ -146,7 +146,7 @@ mod tests { } #[test] - #[should_panic(expected = "PrimitiveError")] + #[should_panic(expected = "Primitive")] fn test_address_try_from_felt_should_fail() { // Given let address: Felt252Wrapper = FieldElement::from_hex_be(OVERFLOW_ADDRESS).unwrap().into(); @@ -169,7 +169,7 @@ mod tests { } #[test] - #[should_panic(expected = "PrimitiveError")] + #[should_panic(expected = "Primitive")] fn test_felt_try_from_b256_should_fail() { // Given let hash = B256::from_str(OVERFLOW_FELT).unwrap(); @@ -192,7 +192,7 @@ mod tests { } #[test] - #[should_panic(expected = "PrimitiveError")] + #[should_panic(expected = "Primitive")] fn test_felt_try_from_u256_should_fail() { // Given let hash = U256::from_str_radix(OVERFLOW_FELT, 16).unwrap(); diff --git a/src/models/transaction.rs b/src/models/transaction.rs index 31dd40019..a9e8fb74a 100644 --- a/src/models/transaction.rs +++ b/src/models/transaction.rs @@ -28,7 +28,7 @@ pub(crate) fn validate_transaction(transaction_signed: &TransactionSigned, chain } // Recover the signer from the transaction - let _ = transaction_signed.recover_signer().ok_or(SignatureError::RecoveryError)?; + let _ = transaction_signed.recover_signer().ok_or(SignatureError::Recovery)?; // Assert the chain is correct let maybe_chain_id = transaction_signed.chain_id(); diff --git a/src/test_utils/katana/mod.rs b/src/test_utils/katana/mod.rs index 4e6da749b..6c9b5e8d7 100644 --- a/src/test_utils/katana/mod.rs +++ b/src/test_utils/katana/mod.rs @@ -17,7 +17,7 @@ use reth_rpc_types::Log; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::JsonRpcClient; -use crate::eth_provider::database::ethereum::EthereumDatabase; +use crate::eth_provider::database::ethereum::EthereumTransactionStore; use crate::eth_provider::database::filter; use crate::eth_provider::database::filter::format_hex; use crate::eth_provider::database::filter::EthDatabaseFilterBuilder; @@ -64,7 +64,7 @@ pub fn katana_config() -> StarknetConfig { /// Returns a `TestSequencer` configured for Kakarot. #[cfg(any(test, feature = "arbitrary", feature = "testing"))] async fn katana_sequencer() -> TestSequencer { - TestSequencer::start(SequencerConfig { no_mining: false, block_time: None, messaging: None }, katana_config()).await + TestSequencer::start(SequencerConfig { no_mining: false, block_time: None }, katana_config()).await } /// Represents the Katana test environment. diff --git a/src/tracing/database.rs b/src/tracing/database.rs index c53b340f7..7faf54ee0 100644 --- a/src/tracing/database.rs +++ b/src/tracing/database.rs @@ -95,7 +95,7 @@ impl Database for EthDatabaseSnapshot

{ return Ok(*hash); } - let block_number = number.try_into().map_err(|_| EthereumDataFormatError::PrimitiveError)?; + let block_number = number.try_into().map_err(|_| EthereumDataFormatError::Primitive)?; let hash = Handle::current().block_on(async { let hash = cache .db diff --git a/src/tracing/mod.rs b/src/tracing/mod.rs index 6caea7778..bebc8882b 100644 --- a/src/tracing/mod.rs +++ b/src/tracing/mod.rs @@ -271,7 +271,7 @@ impl Tracer

{ /// Returns the environment with the transaction env updated to the given transaction. fn env_with_tx(env: &EnvWithHandlerCfg, tx: reth_rpc_types::Transaction) -> TracerResult { // Convert the transaction to an ec recovered transaction and update the env with it. - let tx_ec_recovered = tx.try_into().map_err(|_| EthereumDataFormatError::TransactionConversionError)?; + let tx_ec_recovered = tx.try_into().map_err(|_| EthereumDataFormatError::TransactionConversion)?; let tx_env = tx_env_with_recovered(&tx_ec_recovered); Ok(EnvWithHandlerCfg { diff --git a/tests/tests/alchemy_api.rs b/tests/tests/alchemy_api.rs index 7d01cd9e7..cdac4baed 100644 --- a/tests/tests/alchemy_api.rs +++ b/tests/tests/alchemy_api.rs @@ -54,7 +54,7 @@ async fn test_token_balances(#[future] erc20: (Katana, KakarotEvmContract), _set let raw: Value = serde_json::from_str(&response).expect("Failed to deserialize response body"); let balances: TokenBalances = serde_json::from_value(raw.get("result").cloned().unwrap()).expect("Failed to deserialize response body"); - let erc20_balance = balances.token_balances[0].token_balance.expect("Failed to get ERC20 balance"); + let erc20_balance = balances.token_balances[0].token_balance; assert_eq!(amount, erc20_balance); drop(server_handle); diff --git a/tests/tests/eth_provider.rs b/tests/tests/eth_provider.rs index 6d0f41053..2df400e45 100644 --- a/tests/tests/eth_provider.rs +++ b/tests/tests/eth_provider.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use std::sync::Arc; use kakarot_rpc::eth_provider::constant::{MAX_LOGS, STARKNET_MODULUS, TRANSACTION_MAX_RETRIES}; -use kakarot_rpc::eth_provider::database::ethereum::EthereumDatabase; +use kakarot_rpc::eth_provider::database::ethereum::EthereumTransactionStore; use kakarot_rpc::eth_provider::database::types::transaction::StoredPendingTransaction; use kakarot_rpc::eth_provider::provider::EthereumProvider; use kakarot_rpc::models::felt::Felt252Wrapper;