├── .dockerignore ├── .github ├── pull_request_template.md └── workflows │ ├── build-test.yml │ └── publish-evm-docker-images.yml ├── .gitignore ├── AUDIT_IGNORE ├── Cargo.toml ├── LICENSE ├── README.md ├── docker-compose.yml ├── just ├── build.just ├── code_check.just └── test.just ├── justfile ├── rustfmt.toml └── src ├── did ├── Cargo.toml ├── README.md ├── src │ ├── block.rs │ ├── build.rs │ ├── bytes.rs │ ├── certified.rs │ ├── codec.rs │ ├── constant.rs │ ├── error.rs │ ├── evm_state.rs │ ├── fees.rs │ ├── gas.rs │ ├── hash.rs │ ├── http.rs │ ├── ic.rs │ ├── init.rs │ ├── integer.rs │ ├── keccak.rs │ ├── lib.rs │ ├── logs.rs │ ├── permission.rs │ ├── revert_blocks.rs │ ├── rpc │ │ ├── error.rs │ │ ├── id.rs │ │ ├── mod.rs │ │ ├── params.rs │ │ ├── request.rs │ │ ├── response.rs │ │ └── version.rs │ ├── send_raw_transaction.rs │ ├── send_raw_transaction │ │ ├── signature.rs │ │ └── tx_kind.rs │ ├── state.rs │ ├── test_utils.rs │ ├── transaction.rs │ ├── unsafe_blocks.rs │ └── utils.rs └── tests │ ├── resources │ └── json │ │ ├── block │ │ ├── 0x207dc8087bbdbef42146c9c31f5df79266c1c61be209416abf7d5ed260a63a21.json │ │ ├── 0x81f6e266d34db0c21165d78e0c5e37dab36aee204d0a4422533200fcc8a37b93.json │ │ ├── 0x9ee9da5fafb45610f3c2ba78abe34bd46be01f4de29fc2704a81a76c8171038e.json │ │ ├── 0xb2f703a57637e49572b16088b344db8fb108246f8360027ca8831766443a9c02.json │ │ └── 0xecea9251184f99ea1b65927b665363dd22d5fcf08f350e4157063fd34175d111.json │ │ ├── get_blocks.sh │ │ ├── get_transactions.sh │ │ └── transaction │ │ ├── 0x1f336059dde3447fe37e3969a50857597515c753e8336b7e406792a4176bd60f.json │ │ ├── 0x20081e3012905d97961c2f1a18e1f3fe39f72a46b24e078df2fe446051366dca.json │ │ ├── 0x945ed16321825a4610de3ecc51b2920659f390c5ee96ac468f57ee56aab45ff9.json │ │ ├── 0xd5f627e3ad2e6e0f4a131c52142e2a8344cd9077965c2404fa4ec555113b4ca6.json │ │ ├── 0xdcede6a4ac8829a7f18d3994e9a0e30d913e7d5b4cdb1106aafd9b0118d405a3.json │ │ └── 0xe1ffa2abdc95ebfa92d3178698c4feea7615d3669d16bf5929924881893837ce.json │ └── transaction_it.rs ├── eth-signer ├── Cargo.toml ├── README.md ├── src │ ├── ic_sign.rs │ ├── lib.rs │ ├── sign_strategy.rs │ └── transaction.rs └── tests │ ├── it.rs │ ├── pocket_ic_tests │ ├── ic_sign_test_canister.rs │ ├── mod.rs │ └── wasm_utils.rs │ └── test_canister │ ├── Cargo.toml │ ├── README.md │ └── src │ ├── canister.rs │ └── main.rs ├── ethereum-json-rpc-client ├── Cargo.toml ├── src │ ├── canister_client.rs │ ├── error.rs │ ├── http_outcall.rs │ ├── lib.rs │ └── reqwest.rs └── tests │ ├── integration_tests.rs │ └── reqwest │ ├── mod.rs │ └── rpc_client.rs ├── evm-block-extractor ├── Cargo.toml ├── Dockerfile ├── README.md ├── src │ ├── config.rs │ ├── database │ │ ├── mod.rs │ │ └── postgres_db_client.rs │ ├── lib.rs │ ├── main.rs │ ├── rpc.rs │ ├── server.rs │ └── task │ │ ├── block_extractor.rs │ │ └── mod.rs ├── src_resources │ └── db │ │ └── postgres │ │ └── migrations │ │ ├── 00001_create_schema.sql │ │ ├── 00002_drop_receipts_table.sql │ │ ├── 00003_add_certified_blocks.sql │ │ └── 00004_add_deleted_blocks.sql └── tests │ ├── evm_block_extractor_it.rs │ └── tests │ ├── block_extractor_it.rs │ ├── database_client_it.rs │ ├── mod.rs │ └── server_it.rs ├── evm-canister-client ├── Cargo.toml ├── README.md └── src │ ├── client.rs │ ├── error.rs │ └── lib.rs ├── evm-log-extractor ├── Cargo.toml ├── Dockerfile ├── README.md ├── src │ ├── config.rs │ ├── job │ │ ├── logs.rs │ │ └── mod.rs │ ├── lib.rs │ └── main.rs └── tests │ └── evm_log_extractor_it.rs ├── icrc-client ├── Cargo.toml └── src │ ├── client.rs │ └── lib.rs └── signature-verification-canister-client ├── Cargo.toml ├── README.md └── src ├── client.rs └── lib.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | scripts/ 3 | .github 4 | .artifacts 5 | .gitignore 6 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Issue ticket 2 | 3 | Issue ticket link: <> 4 | 5 | ## Checklist before requesting a review 6 | 7 | ### Code conventions 8 | 9 | - [ ] I have performed a self-review of my code 10 | - [ ] Every new function is documented 11 | - [ ] Object names are auto explicative 12 | 13 | ### Security 14 | 15 | - [ ] The PR does not break APIs backward compatibility 16 | - [ ] The PR does not break the stable storage backward compatibility 17 | 18 | ### Testing 19 | 20 | - [ ] Every function is properly unit tested 21 | - [ ] I have added integration tests that prove my fix is effective or that my feature works 22 | - [ ] New and existing unit tests pass locally with my changes 23 | - [ ] IC endpoints are always tested through the `canister_call!` macro 24 | -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: "Build Test" 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths-ignore: 7 | - "**/README.md" 8 | push: 9 | branches: [main] 10 | tags: 11 | - "v*" 12 | paths-ignore: 13 | - "**/README.md" 14 | 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.ref }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | build-test: 21 | name: Build and Test 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: Install rust toolchain 27 | uses: dtolnay/rust-toolchain@1.85.0 28 | with: 29 | components: clippy, rustfmt 30 | targets: wasm32-unknown-unknown, i686-unknown-linux-gnu 31 | 32 | - name: Install Just command runner 33 | uses: extractions/setup-just@v1 34 | 35 | - name: install ic-wasm 36 | run: | 37 | wget https://github.com/dfinity/ic-wasm/releases/download/0.8.1/ic-wasm-linux64 -O /usr/local/bin/ic-wasm 38 | chmod +x /usr/local/bin/ic-wasm 39 | 40 | - name: setup environment 41 | run: | 42 | sudo apt update 43 | sudo apt install gcc-multilib 44 | 45 | - name: check rust code style 46 | run: | 47 | just check_code 48 | 49 | - name: build 50 | run: | 51 | just build 52 | 53 | - name: test 54 | run: | 55 | just test 56 | env: 57 | ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} 58 | 59 | - name: 32bits test 60 | run: | 61 | just test_i686 62 | env: 63 | ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} 64 | 65 | -------------------------------------------------------------------------------- /.github/workflows/publish-evm-docker-images.yml: -------------------------------------------------------------------------------- 1 | name: 'Deploy EVM Docker Images' 2 | 3 | on: 4 | workflow_dispatch: {} 5 | 6 | push: 7 | branches: [main] 8 | tags: 9 | - 'v*' 10 | 11 | # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. 12 | permissions: 13 | contents: read 14 | packages: write 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | deploy-to-github: 22 | strategy: 23 | matrix: 24 | image: ["evm-block-extractor", "evm-log-extractor"] 25 | 26 | runs-on: ubuntu-latest 27 | 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v3 31 | 32 | - name: Log in to GitHub Container Registry 33 | uses: docker/login-action@v3 34 | with: 35 | registry: ghcr.io 36 | username: ${{ github.actor }} 37 | password: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: Extract metadata (tags, labels) for Docker 40 | id: gh-meta 41 | uses: docker/metadata-action@v5 42 | with: 43 | images: ghcr.io/${{ github.repository_owner }}/${{ matrix.image }} 44 | 45 | - name: Build and push Docker image to GitHub Container Registry 46 | uses: docker/build-push-action@v5 47 | with: 48 | context: . 49 | file: ./src/${{ matrix.image }}/Dockerfile 50 | push: true 51 | tags: ${{ steps.gh-meta.outputs.tags }} 52 | 53 | deploy-to-gcp: 54 | env: 55 | IMAGE_NAME: evm-block-extractor 56 | GCP_REGISTRY: us-east4-docker.pkg.dev 57 | runs-on: ubuntu-latest 58 | needs: deploy-to-github 59 | steps: 60 | - name: Checkout repository 61 | uses: actions/checkout@v3 62 | 63 | - name: Log in to GCP Registry 64 | uses: docker/login-action@v3 65 | with: 66 | registry: ${{ env.GCP_REGISTRY }} 67 | username: _json_key 68 | password: ${{ secrets.EXTRACTOR_GCP_DOCKER_KEY }} 69 | 70 | - name: Extract metadata (tags, labels) for Docker 71 | id: gcp-meta 72 | uses: docker/metadata-action@v5 73 | with: 74 | images: ${{ env.GCP_REGISTRY }}/extractor-410310/block-extractor-repo/${{ env.IMAGE_NAME }} 75 | 76 | - name: Build and push Docker image to GCP Registry 77 | uses: docker/build-push-action@v5 78 | with: 79 | context: . 80 | file: ./src/evm-block-extractor/Dockerfile 81 | push: true 82 | tags: ${{ steps.gcp-meta.outputs.tags }} 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .dfx 3 | .is20 4 | 5 | *.did 6 | .DS_Store 7 | 8 | # Generated by Cargo 9 | # will have compiled files and executables 10 | debug/ 11 | target/ 12 | 13 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 14 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 15 | Cargo.lock 16 | 17 | # These are backup files generated by rustfmt 18 | **/*.rs.bk 19 | 20 | # MSVC Windows builds of rustc generate these, which store debugging information 21 | *.pdb 22 | .vscode 23 | .fleet 24 | 25 | # These files are generated by the `proptest` test framework 26 | # Ignore all folders named `proptest-regressions` and all files ending with `.proptest-regressions` 27 | **/proptest-regressions/ 28 | *.proptest-regressions 29 | 30 | # Solidity compiler files 31 | solidity/cache/ 32 | solidity/out/ 33 | 34 | # Ignores solidity development broadcast logs 35 | solidity/broadcast/ 36 | 37 | # Dotenv file 38 | .env 39 | 40 | # Wasm files 41 | *.wasm 42 | *.wasm.gz 43 | 44 | .cargo 45 | 46 | # IDE files 47 | .idea -------------------------------------------------------------------------------- /AUDIT_IGNORE: -------------------------------------------------------------------------------- 1 | # rsa crate 2 | 3 | # Marvin Attack: potential key recovery through timing sidechannels 4 | 5 | RUSTSEC-2023-0071 -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "src/icrc-client", 4 | "src/did", 5 | "src/eth-signer", 6 | "src/eth-signer/tests/test_canister", 7 | "src/ethereum-json-rpc-client", 8 | "src/evm-block-extractor", 9 | "src/evm-canister-client", 10 | "src/evm-log-extractor", 11 | "src/signature-verification-canister-client", 12 | ] 13 | resolver = "3" 14 | 15 | [workspace.package] 16 | authors = ["Bitfinity Network"] 17 | categories = ["cryptography::cryptocurrencies"] 18 | description = "EVM canister SDK" 19 | edition = "2024" 20 | rust-version = "1.85" 21 | homepage = "https://github.com/bitfinity-network/bitfinity-evm-sdk" 22 | include = ["src/**/*", "LICENSE", "README.md"] 23 | license = "MIT" 24 | repository = "https://github.com/bitfinity-network/bitfinity-evm-sdk" 25 | version = "0.52.0" 26 | 27 | [workspace.dependencies] 28 | did = { path = "src/did" } 29 | eth-signer = { path = "src/eth-signer" } 30 | icrc-client = { path = "src/icrc-client" } 31 | ethereum-json-rpc-client = { path = "src/ethereum-json-rpc-client" } 32 | evm-block-extractor = { path = "src/evm-block-extractor" } 33 | evm-canister-client = { path = "src/evm-canister-client" } 34 | evm-log-extractor = { path = "src/evm-log-extractor" } 35 | signature-verification-canister-client = { path = "src/signature-verification-canister-client" } 36 | 37 | alloy = { version = "1", default-features = false, features = [ 38 | "consensus", 39 | "k256", 40 | "eips", 41 | "rpc-types-eth", 42 | "rlp", 43 | "serde", 44 | ] } 45 | anyhow = "1.0" 46 | bincode = "1.3" 47 | bytes = "1" 48 | candid = "0.10" 49 | clap = { version = "4", features = ["derive", "env"] } 50 | chrono = { version = "0.4", default-features = false } 51 | derive_more = { version = "2", features = ["display", "from", "into"] } 52 | env_logger = { version = "0.11.4", default-features = false } 53 | futures = { version = "0.3", default-features = false } 54 | ic-canister = { git = "https://github.com/bitfinity-network/canister-sdk", package = "ic-canister", tag = "v0.24.x" } 55 | ic-canister-client = { git = "https://github.com/bitfinity-network/canister-sdk", package = "ic-canister-client", tag = "v0.24.x" } 56 | ic-exports = { git = "https://github.com/bitfinity-network/canister-sdk", package = "ic-exports", tag = "v0.24.x" } 57 | ic-log = { git = "https://github.com/bitfinity-network/canister-sdk", package = "ic-log", tag = "v0.24.x" } 58 | ic-stable-structures = { git = "https://github.com/bitfinity-network/canister-sdk", package = "ic-stable-structures", tag = "v0.24.x" } 59 | itertools = "0.14" 60 | jsonrpsee = { version = "0.25", features = ["server", "macros"] } 61 | lightspeed_scheduler = "0.64" 62 | log = "0.4" 63 | num = "0.4" 64 | port_check = "0.2" 65 | proptest = { version = "1.6.0", default-features = false, features = ["std"] } 66 | rand = { version = "0.8", features = ["std_rng", "small_rng"] } 67 | reqwest = { version = "0.12", default-features = false } 68 | serial_test = "3" 69 | serde = "1.0" 70 | serde_bytes = "0.11" 71 | serde_json = "1.0" 72 | serde_with = "3.3" 73 | sha2 = "0.10" 74 | sha3 = "0.10" 75 | sqlx = { version = "0.8.1", default-features = false, features = [ 76 | "macros", 77 | "migrate", 78 | "json", 79 | "runtime-tokio", 80 | ] } 81 | tempfile = "3" 82 | testcontainers = { package = "testcontainers-modules", version = "0.12", features = [ 83 | "postgres", 84 | ] } 85 | thiserror = "2.0" 86 | tokio = { version = "1.39", features = ["macros", "rt-multi-thread", "signal"] } 87 | url = "2.5" 88 | 89 | [profile.dev] 90 | debug = false 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Bitfinity Network 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bitfinity EVM SDK 2 | 3 | [![license-mit](https://img.shields.io/badge/License-MIT-teal.svg)](https://opensource.org/licenses/MIT) 4 | [![Build Test](https://github.com/bitfinity-network/bitfinity-evm-sdk/actions/workflows/build-test.yml/badge.svg)](https://github.com/bitfinity-network/bitfinity-evm-sdk/actions/workflows/build-test.yml) 5 | 6 | ![github](https://github.com/bitfinity-network/bitfinity-evm-sdk/assets/25309184/4775bc4b-1033-4528-ab4b-64ed05b6dcbf) 7 | 8 | ## Components 9 | 10 | - [did](./src/did): Data types for [evm-canister](https://github.com/bitfinity-network/evm-canister) 11 | - [eth-signer](./src/eth-signer/): A library which provides a trait for signing transactions and messages. 12 | - [evm-block-extractor](./src/evm-block-extractor/): It is made up of two components: 13 | - [evm-block-extractor](./src/evm-block-extractor/): A library for extracting blocks from the Bitfinity EVM and storing them in a PostgresSQL DB 14 | - [evm-block-extractor-server](./src/evm-block-extractor/bin/server): A JSON-RPC server for the EVM block extractor 15 | - [evm-canister-client](./src/evm-canister-client/): A library for interacting with the Bitfinity EVM 16 | - [register-evm-agent](./src/register-evm-agent/): A Cli tool for generating an ETH Wallet & reserving a canister to the Bitfinity EVM 17 | 18 | ## License 19 | 20 | bitfinity-evm-sdk is licensed under the MIT license. 21 | 22 | You can read the entire license [HERE](./LICENSE) 23 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | # 4 | # This docker-compose file is used to start the services for local testing. 5 | # It starts a evm-blockchain-extractor connected to a local postgres database. 6 | # 7 | 8 | services: 9 | db: 10 | image: 'postgres:11-alpine' 11 | ports: 12 | - '5432:5432' 13 | environment: 14 | POSTGRES_PASSWORD: postgres 15 | POSTGRES_USER: postgres 16 | 17 | evm-block-extractor: 18 | # image: ghcr.io/bitfinity-network/evm-block-extractor:main 19 | image: "evm-block-extractor:latest" 20 | build: 21 | dockerfile: ./src/evm-block-extractor/Dockerfile 22 | ports: 23 | - '8080:8080' 24 | command: --rpc-url https://testnet.bitfinity.network --postgres --username postgres --password postgres --database-name postgres --database-url db 25 | depends_on: 26 | - db 27 | 28 | evm-log-extractor: 29 | # image: ghcr.io/bitfinity-network/evm-log-extractor:main 30 | image: "evm-log-extractor:latest" 31 | build: 32 | dockerfile: ./src/evm-log-extractor/Dockerfile 33 | environment: 34 | # use local dfx replica 35 | - EVMC_PRINCIPAL=bkyz2-fmaaa-aaaaa-qaaaq-cai 36 | - EVMC_NETWORK_URL=http://host.docker.internal:40837 37 | # use testnet 38 | #- EVMC_PRINCIPAL=4fe7g-7iaaa-aaaak-aegcq-cai 39 | volumes: 40 | - ~/.config/dfx/identity/alice/identity.pem:/data/config/identity.pem:ro 41 | - ./target/logs:/data/logs 42 | extra_hosts: 43 | - "host.docker.internal:host-gateway" 44 | -------------------------------------------------------------------------------- /just/build.just: -------------------------------------------------------------------------------- 1 | 2 | # Cleans the build artifacts 3 | [group('build')] 4 | [confirm("Are you sure you want to clean the build artifacts?")] 5 | clean: 6 | rm -rf {{WASM_DIR}} 7 | cargo clean 8 | 9 | 10 | # Builds the test canister 11 | [group('build')] 12 | build: 13 | mkdir -p {{WASM_DIR}} 14 | echo "Building ic-sign-client test canisters" 15 | cargo build -p ic-sign-test-canister --target wasm32-unknown-unknown --release --features "export-api" 16 | ic-wasm target/wasm32-unknown-unknown/release/ic-sign-test-canister.wasm -o {{WASM_DIR}}/ic-sign-test-canister.wasm shrink 17 | gzip -k "{{WASM_DIR}}/ic-sign-test-canister.wasm" --force 18 | -------------------------------------------------------------------------------- /just/code_check.just: -------------------------------------------------------------------------------- 1 | # Formats the code 2 | [group('code_check')] 3 | fmt args="": 4 | cargo fmt --all {{args}} 5 | 6 | 7 | # Formats the code using the nightly version of rustfmt 8 | [group('code_check')] 9 | fmt_nightly: 10 | cargo +nightly fmt --all 11 | 12 | 13 | # Runs clippy on the code 14 | [group('code_check')] 15 | clippy args="": 16 | cargo clippy --all-features --all-targets {{args}} 17 | 18 | 19 | # Runs fmt and clippy on the code. It fails if any of the checks fail 20 | [group('code_check')] 21 | check_code: 22 | just fmt "--check" 23 | just clippy "-- -D warnings" -------------------------------------------------------------------------------- /just/test.just: -------------------------------------------------------------------------------- 1 | 2 | # Run all unit tests 3 | [group('test')] 4 | test test_name="": 5 | cargo test {{test_name}} 6 | cargo test {{test_name}} --all-features 7 | 8 | 9 | # Run all unit tests for the i686 target 10 | [group('test')] 11 | test_i686 test_name="": 12 | cargo test {{test_name}} --target i686-unknown-linux-gnu 13 | cargo test {{test_name}} --target i686-unknown-linux-gnu --all-features 14 | 15 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | 2 | import "./just/build.just" 3 | import "./just/code_check.just" 4 | import "./just/test.just" 5 | 6 | export RUST_BACKTRACE := "full" 7 | WASM_DIR := env("WASM_DIR", "./target/artifact") 8 | 9 | 10 | # Lists all the available commands 11 | default: 12 | @just --list 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Module" 2 | group_imports = "StdExternalCrate" 3 | -------------------------------------------------------------------------------- /src/did/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | categories = ["cryptography::cryptocurrencies"] 3 | description = "API types definition for EVM canister" 4 | include = ["src/**/*", "../../LICENSE", "../../README.md"] 5 | name = "did" 6 | 7 | authors.workspace = true 8 | homepage.workspace = true 9 | version.workspace = true 10 | rust-version.workspace = true 11 | edition.workspace = true 12 | license.workspace = true 13 | repository.workspace = true 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [dependencies] 18 | alloy = { workspace = true } 19 | bincode = { workspace = true } 20 | bytes = { workspace = true } 21 | candid = { workspace = true } 22 | derive_more = { workspace = true } 23 | ic-log = { workspace = true } 24 | ic-stable-structures = { workspace = true } 25 | log = { workspace = true } 26 | num = { workspace = true } 27 | serde = { workspace = true } 28 | serde_bytes = { workspace = true } 29 | serde_json = { workspace = true } 30 | serde_with = { workspace = true } 31 | sha2 = { workspace = true } 32 | sha3 = { workspace = true } 33 | thiserror = { workspace = true } 34 | 35 | [dev-dependencies] 36 | alloy = { workspace = true, features = ["rand"] } 37 | eth-signer = { workspace = true } 38 | proptest = { workspace = true } 39 | rand = { workspace = true } 40 | tokio = { workspace = true } 41 | -------------------------------------------------------------------------------- /src/did/README.md: -------------------------------------------------------------------------------- 1 | # EVM Canister DID 2 | -------------------------------------------------------------------------------- /src/did/src/build.rs: -------------------------------------------------------------------------------- 1 | use candid::CandidType; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// Contains the build data. 5 | #[derive(Debug, Clone, Serialize, Deserialize, CandidType)] 6 | pub struct BuildData { 7 | pub cargo_target_triple: String, 8 | pub cargo_features: String, 9 | pub pkg_name: String, 10 | pub pkg_version: String, 11 | pub rustc_semver: String, 12 | pub build_timestamp: String, 13 | pub cargo_debug: String, 14 | pub git_branch: String, 15 | pub git_sha: String, 16 | pub git_commit_timestamp: String, 17 | } 18 | -------------------------------------------------------------------------------- /src/did/src/bytes.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::rc::Rc; 3 | 4 | use candid::CandidType; 5 | use candid::types::*; 6 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 7 | 8 | #[derive(Debug, Default, Clone, Eq, PartialEq)] 9 | pub struct Bytes(pub bytes::Bytes); 10 | 11 | impl Bytes { 12 | pub fn from_hex_str(mut s: &str) -> Result { 13 | if s.starts_with("0x") || s.starts_with("0X") { 14 | s = &s[2..] 15 | } 16 | let bytes = alloy::hex::decode(s)?; 17 | Ok(Self(bytes::Bytes::from(bytes))) 18 | } 19 | 20 | pub fn to_hex_str(&self) -> String { 21 | format!("0x{self:x}") 22 | } 23 | } 24 | 25 | impl alloy::rlp::Encodable for Bytes { 26 | fn encode(&self, out: &mut dyn bytes::BufMut) { 27 | self.0.encode(out); 28 | } 29 | } 30 | 31 | impl alloy::rlp::Decodable for Bytes { 32 | fn decode(buf: &mut &[u8]) -> alloy::rlp::Result { 33 | Ok(Self(bytes::Bytes::decode(buf)?)) 34 | } 35 | } 36 | 37 | impl From for bytes::Bytes { 38 | fn from(value: Bytes) -> Self { 39 | value.0 40 | } 41 | } 42 | 43 | impl From for Bytes { 44 | fn from(value: bytes::Bytes) -> Self { 45 | Bytes(value) 46 | } 47 | } 48 | 49 | impl From> for Bytes { 50 | fn from(value: Vec) -> Self { 51 | Bytes(value.into()) 52 | } 53 | } 54 | 55 | impl From for Vec { 56 | fn from(value: Bytes) -> Self { 57 | value.0.into() 58 | } 59 | } 60 | 61 | impl From for alloy::primitives::Bytes { 62 | fn from(value: Bytes) -> Self { 63 | value.0.into() 64 | } 65 | } 66 | 67 | impl From for Bytes { 68 | fn from(value: alloy::primitives::Bytes) -> Self { 69 | Bytes(value.0) 70 | } 71 | } 72 | 73 | impl fmt::LowerHex for Bytes { 74 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 75 | self.0.fmt(f) 76 | } 77 | } 78 | 79 | impl CandidType for Bytes { 80 | fn _ty() -> candid::types::Type { 81 | Type(Rc::new(TypeInner::Text)) 82 | } 83 | 84 | fn idl_serialize(&self, serializer: S) -> Result<(), S::Error> 85 | where 86 | S: candid::types::Serializer, 87 | { 88 | serializer.serialize_text(&self.to_hex_str()) 89 | } 90 | } 91 | 92 | impl Serialize for Bytes { 93 | fn serialize(&self, serializer: S) -> Result 94 | where 95 | S: Serializer, 96 | { 97 | serializer.serialize_str(&self.to_hex_str()) 98 | } 99 | } 100 | 101 | impl<'de> Deserialize<'de> for Bytes { 102 | fn deserialize(deserializer: D) -> Result 103 | where 104 | D: Deserializer<'de>, 105 | { 106 | let value = String::deserialize(deserializer)?; 107 | Bytes::from_hex_str(&value).map_err(|e| serde::de::Error::custom(e.to_string())) 108 | } 109 | } 110 | 111 | #[cfg(test)] 112 | mod tests { 113 | 114 | use candid::{Decode, Encode}; 115 | 116 | use super::*; 117 | 118 | #[test] 119 | fn test_candid_type_bytes() { 120 | let value = Bytes(bytes::Bytes::from(vec![ 121 | rand::random::(), 122 | rand::random::(), 123 | rand::random::(), 124 | ])); 125 | 126 | let encoded = Encode!(&value).unwrap(); 127 | let decoded = Decode!(&encoded, Bytes).unwrap(); 128 | 129 | assert_eq!(value, decoded); 130 | } 131 | 132 | #[test] 133 | fn test_bytes_fmt_lower_hex() { 134 | let value = Bytes(bytes::Bytes::from(vec![ 135 | rand::random::(), 136 | rand::random::(), 137 | rand::random::(), 138 | ])); 139 | let lower_hex = value.to_hex_str(); 140 | assert!(lower_hex.starts_with("0x")); 141 | assert_eq!(value, Bytes::from_hex_str(&lower_hex).unwrap()); 142 | } 143 | 144 | #[test] 145 | fn test_bytes_serde_serialization() { 146 | let value = Bytes(bytes::Bytes::from(vec![ 147 | rand::random::(), 148 | rand::random::(), 149 | rand::random::(), 150 | ])); 151 | 152 | let encoded_value = serde_json::json!(&value); 153 | let decoded_value: Bytes = serde_json::from_value(encoded_value).unwrap(); 154 | 155 | assert_eq!(value, decoded_value); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/did/src/certified.rs: -------------------------------------------------------------------------------- 1 | use candid::CandidType; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize, CandidType, Clone, PartialEq, Eq, Debug)] 5 | pub struct CertifiedResult { 6 | pub data: T, 7 | pub witness: Vec, 8 | pub certificate: Vec, 9 | } 10 | -------------------------------------------------------------------------------- /src/did/src/codec.rs: -------------------------------------------------------------------------------- 1 | use candid::{CandidType, Decode, Deserialize, Encode}; 2 | 3 | pub fn encode(item: &T) -> Vec { 4 | Encode!(item).expect("failed to encode item to candid") 5 | } 6 | 7 | pub fn decode<'a, T: CandidType + Deserialize<'a>>(bytes: &'a [u8]) -> T { 8 | Decode!(bytes, T).expect("failed to decode item from candid") 9 | } 10 | 11 | pub fn bincode_encode(item: &T) -> Vec { 12 | bincode::serialize(item).expect("failed to serialize item with bincode") 13 | } 14 | 15 | pub fn bincode_decode<'a, T: serde::Deserialize<'a>>(bytes: &'a [u8]) -> T { 16 | bincode::deserialize(bytes).expect("failed to deserialize item with bincode") 17 | } 18 | 19 | /// A reader for byte data 20 | pub struct ByteChunkReader<'a> { 21 | position: usize, 22 | data: &'a [u8], 23 | } 24 | 25 | impl<'a> ByteChunkReader<'a> { 26 | pub fn new(data: &'a [u8]) -> Self { 27 | Self { data, position: 0 } 28 | } 29 | 30 | /// Reads a chunk of data and update the reader internal pointer 31 | /// to the beginning of the next chunk. 32 | /// It panics if not enough data is present 33 | pub fn read(&mut self, chunk_size: usize) -> &'a [u8] { 34 | let res = &self.data[self.position..self.position + chunk_size]; 35 | self.position += chunk_size; 36 | res 37 | } 38 | 39 | /// Reads a chunk of data and update the reader internal pointer 40 | /// to the beginning of the next chunk. 41 | /// It panics if not enough data is present. 42 | pub fn read_slice(&mut self) -> &'a [u8; N] { 43 | self.read(N) 44 | .try_into() 45 | .expect("Should read the exact size of bytes") 46 | } 47 | 48 | /// Reads the remaining data and update the reader internal pointer. 49 | /// It panics if not enough data is present 50 | pub fn read_to_end(mut self) -> &'a [u8] { 51 | self.read(self.data.len() - self.position) 52 | } 53 | } 54 | 55 | #[cfg(test)] 56 | mod tests { 57 | use super::*; 58 | 59 | #[test] 60 | fn test_chunck_reader() { 61 | let data = [0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]; 62 | let mut reader = ByteChunkReader::new(&data); 63 | 64 | assert_eq!(&[0u8], reader.read(1)); 65 | assert_eq!(&[1u8, 2], reader.read(2)); 66 | assert_eq!(&[3u8, 4u8, 5u8, 6u8], reader.read_slice::<4>()); 67 | assert_eq!(&[7u8], reader.read(1)); 68 | assert_eq!(&[8u8, 9, 10, 11, 12, 13], reader.read_to_end()); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/did/src/constant.rs: -------------------------------------------------------------------------------- 1 | pub const EIP1559_INITIAL_BASE_FEE: u128 = 1000000000; 2 | pub const EIP1559_ELASTICITY_MULTIPLIER: u128 = 2; 3 | pub const EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR: u128 = 8; 4 | 5 | /// Identifier for Legacy Transaction 6 | pub const TRANSACTION_TYPE_LEGACY: u64 = 0; 7 | /// Identifier for Eip2930 Transaction 8 | pub const TRANSACTION_TYPE_EIP2930: u64 = 1; 9 | /// Identifier for Eip1559 Transaction 10 | pub const TRANSACTION_TYPE_EIP1559: u64 = 2; 11 | 12 | /// The methods will be upgraded when doing http outcalls 13 | pub const UPGRADE_HTTP_METHODS: &[&str] = &[ 14 | JSON_RPC_METHOD_ETH_SEND_RAW_TRANSACTION_NAME, 15 | JSON_RPC_METHOD_IC_MINT_NATIVE_TOKEN_NAME, 16 | IC_SEND_CONFIRM_BLOCK, 17 | ]; 18 | 19 | pub const JSON_RPC_METHOD_ETH_SEND_RAW_TRANSACTION_NAME: &str = "eth_sendRawTransaction"; 20 | 21 | /// This endpoint is used for minting tokens, on the testnet 22 | /// 23 | /// NB: This endpoint is only enabled with the testnet feature 24 | pub const JSON_RPC_METHOD_IC_MINT_NATIVE_TOKEN_NAME: &str = "ic_mintNativeToken"; 25 | 26 | pub const IC_SEND_CONFIRM_BLOCK: &str = "ic_sendConfirmBlock"; 27 | -------------------------------------------------------------------------------- /src/did/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use alloy::eips::eip2718::Eip2718Error; 4 | use alloy::primitives::SignatureError; 5 | use alloy::rlp::Error as DecoderError; 6 | use candid::{CandidType, Deserialize}; 7 | use serde::Serialize; 8 | use thiserror::Error; 9 | 10 | use crate::rpc::error::ErrorCode; 11 | use crate::transaction::BlockId; 12 | use crate::{BlockNumber, U256, rpc}; 13 | 14 | pub type Result = std::result::Result; 15 | 16 | #[derive(Debug, Error, Deserialize, CandidType, Eq, PartialEq, Serialize, Clone)] 17 | pub enum EvmError { 18 | #[error("internal error: {0}")] 19 | Internal(String), 20 | 21 | #[error("insufficient balance: actual: {actual}, expected {expected}")] 22 | InsufficientBalance { 23 | actual: crate::U256, 24 | expected: crate::U256, 25 | }, 26 | 27 | #[error("evm transaction failed due to {0:?}")] 28 | NotProcessableTransactionError(HaltError), 29 | 30 | #[error("evm transaction failed due to {0:?}")] 31 | FatalEvmExecutorError(ExitFatal), 32 | 33 | #[error("gas price should be >= {0}")] 34 | InvalidGasPrice(crate::U256), 35 | 36 | #[error("the user has no permission to call this method")] 37 | NotAuthorized, 38 | 39 | #[error("reservation failed: {0}")] 40 | ReservationFailed(String), 41 | 42 | #[error("Stable Storage error: {0}")] 43 | StableStorageError(String), 44 | 45 | #[error("transaction pool error {0}")] 46 | TransactionPool(TransactionPoolError), 47 | 48 | #[error("no history state data for block {0}")] 49 | NoHistoryDataForBlock(BlockNumber), 50 | 51 | #[error("block doesn't exist: {0}")] 52 | BlockDoesNotExist(BlockId), 53 | 54 | #[error("Transaction Signature error: {0}")] 55 | TransactionSignature(String), 56 | 57 | #[error("gas is too low, minimum required: {minimum}")] 58 | GasTooLow { minimum: U256 }, 59 | 60 | #[error("anonymous caller is not allowed")] 61 | AnonymousPrincipal, 62 | 63 | #[error("The request is not valid: {0}")] 64 | BadRequest(String), 65 | 66 | #[error("The transaction has been reverted: {0}")] 67 | TransactionReverted(String), 68 | 69 | #[error("Transaction type error: {0}")] 70 | TransactionTypeError(String), 71 | 72 | #[error("Signature Parity is invalid: {0}")] 73 | InvalidSignatureParity(String), 74 | 75 | #[error("Signature error: {0}")] 76 | SignatureError(String), 77 | 78 | #[error("Rlp error: {0}")] 79 | RlpError(String), 80 | } 81 | 82 | /// Variant of `TransactionPool` error 83 | #[derive(Debug, Deserialize, Error, CandidType, PartialEq, Eq, Serialize, Clone)] 84 | pub enum TransactionPoolError { 85 | #[error("transaction already exists in the pool")] 86 | TransactionAlreadyExists, 87 | 88 | #[error("invalid transaction nonce, expected {expected}, actual {actual}")] 89 | InvalidNonce { expected: U256, actual: U256 }, 90 | 91 | #[error("the maximum amount of transactions per sender has been reached")] 92 | TooManyTransactions, 93 | 94 | #[error("transaction gas price is too low to replace an existing transaction")] 95 | TxReplacementUnderpriced, 96 | } 97 | 98 | impl EvmError { 99 | pub fn unsupported_method_error() -> Self { 100 | Self::Internal("method is not supported".to_string()) 101 | } 102 | } 103 | 104 | impl From for EvmError { 105 | fn from(msg: String) -> Self { 106 | Self::Internal(msg) 107 | } 108 | } 109 | 110 | impl From for EvmError { 111 | fn from(decode_error: DecoderError) -> Self { 112 | Self::RlpError(format!("rlp err: {decode_error}")) 113 | } 114 | } 115 | 116 | impl From for EvmError { 117 | fn from(eip2718_error: Eip2718Error) -> Self { 118 | Self::RlpError(format!("EIP-2718 rlp error: {eip2718_error}")) 119 | } 120 | } 121 | 122 | impl From for EvmError { 123 | fn from(err: serde_json::Error) -> Self { 124 | Self::Internal(format!("JSON encoding error: {err}")) 125 | } 126 | } 127 | 128 | /// https://docs.alchemy.com/reference/error-reference#kovan-error-codes 129 | impl From for rpc::error::Error { 130 | fn from(err: EvmError) -> Self { 131 | let code = match &err { 132 | EvmError::InsufficientBalance { 133 | actual: _, 134 | expected: _, 135 | } => -32010, // TRANSACTION_ERROR 136 | EvmError::InvalidGasPrice(_) => -32016, // ACCOUNT_ERROR 137 | EvmError::NotAuthorized => -32002, // NO_AUTHOR 138 | _ => -32015, // EXECUTION_ERROR 139 | }; 140 | 141 | let data = match &err { 142 | EvmError::TransactionReverted(msg) => Some(msg), 143 | _ => None, 144 | }; 145 | 146 | rpc::error::Error { 147 | code: ErrorCode::ServerError(code), 148 | message: err.to_string(), 149 | data: data.map(|s| s.as_str().into()), 150 | } 151 | } 152 | } 153 | 154 | #[derive( 155 | Debug, Clone, Serialize, Deserialize, CandidType, Eq, PartialEq, PartialOrd, Ord, Hash, 156 | )] 157 | pub enum HaltError { 158 | /// Trying to pop from an empty stack. 159 | StackUnderflow, 160 | /// Trying to push into a stack over stack limit. 161 | StackOverflow, 162 | /// Jump destination is invalid. 163 | InvalidJump, 164 | /// An opcode accesses memory region, but the region is invalid. 165 | InvalidRange, 166 | /// Encountered the designated invalid opcode. 167 | DesignatedInvalid, 168 | /// Call stack is too deep (runtime). 169 | CallTooDeep, 170 | /// Create opcode encountered collision (runtime). 171 | CreateCollision, 172 | /// Create init code exceeds limit (runtime). 173 | CreateContractLimit, 174 | /// Starting byte must not begin with 0xef. See [EIP-3541](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-3541.md). 175 | InvalidCode(u8), 176 | 177 | /// An opcode accesses external information, but the request is off offset 178 | /// limit (runtime). 179 | OutOfOffset, 180 | /// Execution runs out of gas (runtime). 181 | OutOfGas, 182 | /// Not enough fund to start the execution (runtime). 183 | OutOfFund, 184 | 185 | /// PC underflowed (unused). 186 | PCUnderflow, 187 | 188 | /// Attempt to create an empty account (runtime, unused). 189 | CreateEmpty, 190 | 191 | /// Other normal errors. 192 | Other(Cow<'static, str>), 193 | OpcodeNotFound, 194 | CallNotAllowedInsideStatic, 195 | InvalidFEOpcode, 196 | NotActivated, 197 | FatalExternalError, 198 | GasPriceLessThanBasefee, 199 | CallerGasLimitMoreThanBlock, 200 | RejectCallerWithCode, 201 | LackOfFundForMaxFee { 202 | fee: U256, 203 | balance: U256, 204 | }, 205 | OverflowPayment, 206 | PrecompileError, 207 | NonceOverflow, 208 | CreateContractWithEF, 209 | PrevrandaoNotSet, 210 | Continue, 211 | Revert(Option), 212 | PriorityFeeGreaterThanMaxFee, 213 | CallGasCostMoreThanGasLimit { 214 | initial_gas: u64, 215 | gas_limit: u64, 216 | }, 217 | NonceTooHigh { 218 | tx: u64, 219 | state: u64, 220 | }, 221 | NonceTooLow { 222 | tx: u64, 223 | state: u64, 224 | }, 225 | CreateInitcodeSizeLimit, 226 | InvalidChainId, 227 | StateChangeDuringStaticCall, 228 | InvalidEXTCALLTarget, 229 | SubRoutineStackOverflow, 230 | 231 | /// Aux data overflow, new aux data is larger tha u16 max size. 232 | EofAuxDataOverflow, 233 | /// Aux data is smaller then already present data size. 234 | EofAuxDataTooSmall, 235 | /// EOF Subroutine stack overflow 236 | EOFFunctionStackOverflow, 237 | } 238 | 239 | #[derive( 240 | Debug, Clone, Deserialize, CandidType, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, 241 | )] 242 | pub enum ExitFatal { 243 | /// The operation is not supported. 244 | NotSupported, 245 | /// The trap (interrupt) is unhandled. 246 | UnhandledInterrupt, 247 | /// The environment explicitly set call errors as fatal error. 248 | CallErrorAsFatal(HaltError), 249 | 250 | /// Other fatal errors. 251 | Other(Cow<'static, str>), 252 | } 253 | 254 | impl From for EvmError { 255 | fn from(exit_err: HaltError) -> Self { 256 | Self::NotProcessableTransactionError(exit_err) 257 | } 258 | } 259 | 260 | #[derive(Error, Debug, CandidType, Deserialize, PartialEq, Eq, PartialOrd, Ord, Serialize)] 261 | pub enum SignatureVerificationError { 262 | #[error("signature error: {0}")] 263 | SignatureError(String), 264 | #[error("failed to verify signature: {0}")] 265 | InternalError(String), 266 | #[error("unauthorized principal")] 267 | Unauthorized, 268 | } 269 | 270 | impl From for SignatureVerificationError { 271 | fn from(value: SignatureError) -> Self { 272 | SignatureVerificationError::SignatureError(format!("{:?}", value)) 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/did/src/evm_state.rs: -------------------------------------------------------------------------------- 1 | use candid::CandidType; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::{AccountInfoMap, Block, H256}; 5 | 6 | /// Describes the state of the EVM reset process. 7 | #[derive(Debug, Serialize, Deserialize, CandidType)] 8 | pub enum EvmResetState { 9 | /// Start of the reset process. 10 | /// It deletes all the accounts, storage, Transactions and everything else. 11 | Start, 12 | /// Add accounts to the state. 13 | AddAccounts(AccountInfoMap), 14 | /// End of the reset process. 15 | /// It sets the state to the given block. 16 | /// If the block state hash is not equal to the current state hash, it will fail. 17 | End(Block), 18 | } 19 | 20 | /// The EVM global state 21 | #[derive(Debug, Default, Deserialize, CandidType, Clone, PartialEq, Eq, Serialize)] 22 | pub enum EvmGlobalState { 23 | /// The EVM is enabled. 24 | /// All functions are available. 25 | #[default] 26 | Enabled, 27 | /// The EVM is disabled. 28 | /// Blocks are not processed and transactions are not executed. 29 | Disabled, 30 | /// The EVM is in staging mode. 31 | /// All functions are available, but the state is under testing and could be reset at any time. 32 | /// External users should not rely on the state. 33 | Staging { 34 | /// The maximum block number that the state can reach while in staging mode. 35 | max_block_number: Option, 36 | }, 37 | } 38 | 39 | impl EvmGlobalState { 40 | /// Returns true if the EVM is enabled. 41 | pub fn is_enabled(&self) -> bool { 42 | matches!(self, EvmGlobalState::Enabled) 43 | } 44 | 45 | /// Returns true if the EVM is disabled. 46 | pub fn is_disabled(&self) -> bool { 47 | matches!(self, EvmGlobalState::Disabled) 48 | } 49 | 50 | /// Returns true if the EVM is in staging mode. 51 | pub fn is_staging(&self) -> bool { 52 | matches!(self, EvmGlobalState::Staging { .. }) 53 | } 54 | } 55 | 56 | #[cfg(test)] 57 | mod test { 58 | 59 | use super::*; 60 | 61 | #[test] 62 | fn test_evm_global_state() { 63 | let enabled = EvmGlobalState::Enabled; 64 | assert!(enabled.is_enabled()); 65 | assert!(!enabled.is_disabled()); 66 | assert!(!enabled.is_staging()); 67 | 68 | let disabled = EvmGlobalState::Disabled; 69 | assert!(!disabled.is_enabled()); 70 | assert!(disabled.is_disabled()); 71 | assert!(!disabled.is_staging()); 72 | 73 | let staging = EvmGlobalState::Staging { 74 | max_block_number: None, 75 | }; 76 | assert!(!staging.is_enabled()); 77 | assert!(!staging.is_disabled()); 78 | assert!(staging.is_staging()); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/did/src/fees.rs: -------------------------------------------------------------------------------- 1 | use candid::CandidType; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::constant::{ 5 | TRANSACTION_TYPE_EIP1559, TRANSACTION_TYPE_EIP2930, TRANSACTION_TYPE_LEGACY, 6 | }; 7 | use crate::transaction::StorableExecutionResult; 8 | use crate::{Transaction, U64, U256}; 9 | 10 | #[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize, CandidType)] 11 | #[serde(rename_all = "camelCase")] 12 | pub struct FeeHistory { 13 | /// An array of block base fees per gas. 14 | pub base_fee_per_gas: Vec, 15 | /// An array of block gas used ratios. 16 | /// These are calculated as the ratio of `gas_used` and `gas_limit`. 17 | #[serde(skip_serializing_if = "Vec::is_empty")] 18 | #[serde(default)] 19 | pub gas_used_ratio: Vec, 20 | /// Lowest number block of the returned range. 21 | pub oldest_block: U256, 22 | /// An (optional) array of effective priority fee per gas data points from a single 23 | /// block. All zeroes are returned if the block is empty. 24 | #[serde(default)] 25 | pub reward: Option>>, 26 | } 27 | 28 | /// A trait which contains helper methods for the calculation 29 | /// of the fees of the transaction 30 | pub trait FeeCalculation { 31 | /// Transaction type 32 | fn transaction_type(&self) -> Option; 33 | fn gas_price(&self) -> Option; 34 | fn max_fee_per_gas(&self) -> Option; 35 | fn max_priority_fee_per_gas(&self) -> Option; 36 | 37 | /// Returns the effective miner gas tip for the given base fee. 38 | /// This is used in the calculation of the fee history. 39 | /// 40 | /// see: 41 | /// https://github.com/ethereum/go-ethereum/blob/ 42 | /// 5b9cbe30f8ca2487c8991e50e9c939d5e6ec3cc2/core/types/transaction.go#L347 43 | fn effective_gas_tip(&self, base_fee: Option) -> Option { 44 | if let Some(base_fee) = base_fee { 45 | let max_fee_per_gas = self.gas_cost(); 46 | 47 | if max_fee_per_gas < base_fee { 48 | None 49 | } else { 50 | let effective_max_fee = max_fee_per_gas - base_fee; 51 | 52 | Some(effective_max_fee.min(self.max_priority_fee_or_gas_price())) 53 | } 54 | } else { 55 | Some(self.max_priority_fee_or_gas_price()) 56 | } 57 | } 58 | 59 | /// Gas cost of the transaction 60 | fn gas_cost(&self) -> U256 { 61 | match self.transaction_type().map(u64::from) { 62 | Some(TRANSACTION_TYPE_EIP1559) => self.max_fee_per_gas().unwrap_or_default(), 63 | Some(TRANSACTION_TYPE_EIP2930) | Some(TRANSACTION_TYPE_LEGACY) | None => { 64 | self.gas_price().unwrap_or_default() 65 | } 66 | tx_type => panic!("invalid transaction type: {tx_type:?}"), 67 | } 68 | } 69 | 70 | /// Returns the priority fee or gas price of the transaction 71 | fn max_priority_fee_or_gas_price(&self) -> U256 { 72 | match self.transaction_type().map(u64::from) { 73 | Some(TRANSACTION_TYPE_EIP1559) => self.max_priority_fee_per_gas().unwrap_or_default(), 74 | Some(TRANSACTION_TYPE_EIP2930) | Some(TRANSACTION_TYPE_LEGACY) | None => { 75 | self.gas_price().unwrap_or_default() 76 | } 77 | tx_type => panic!("invalid transaction type: {tx_type:?}"), 78 | } 79 | } 80 | } 81 | 82 | impl FeeCalculation for Transaction { 83 | fn transaction_type(&self) -> Option { 84 | self.transaction_type 85 | } 86 | 87 | fn gas_price(&self) -> Option { 88 | self.gas_price.clone() 89 | } 90 | 91 | fn max_fee_per_gas(&self) -> Option { 92 | self.max_fee_per_gas.clone() 93 | } 94 | 95 | fn max_priority_fee_per_gas(&self) -> Option { 96 | self.max_priority_fee_per_gas.clone() 97 | } 98 | } 99 | 100 | impl FeeCalculation for StorableExecutionResult { 101 | fn transaction_type(&self) -> Option { 102 | self.transaction_type 103 | } 104 | 105 | fn gas_price(&self) -> Option { 106 | self.gas_price.clone() 107 | } 108 | 109 | fn max_fee_per_gas(&self) -> Option { 110 | self.max_fee_per_gas.clone() 111 | } 112 | 113 | fn max_priority_fee_per_gas(&self) -> Option { 114 | self.max_priority_fee_per_gas.clone() 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/did/src/gas.rs: -------------------------------------------------------------------------------- 1 | use candid::CandidType; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::transaction::AccessList; 5 | use crate::{Bytes, H160, U256}; 6 | 7 | #[derive(Debug, Clone, Default, Eq, PartialEq, CandidType, Deserialize, Serialize)] 8 | /// The `estimate_gas` method parameters 9 | pub struct EstimateGasRequest { 10 | pub from: Option, 11 | pub to: Option, 12 | #[serde(rename = "gasPrice", default, skip_serializing_if = "Option::is_none")] 13 | pub gas_price: Option, 14 | /// EIP-1559 Max base fee the caller is willing to pay 15 | #[serde( 16 | rename = "maxFeePerGas", 17 | default, 18 | skip_serializing_if = "Option::is_none" 19 | )] 20 | pub max_fee_per_gas: Option, 21 | /// EIP-1559 Priority fee the caller is paying to the block author 22 | #[serde( 23 | rename = "maxPriorityFeePerGas", 24 | default, 25 | skip_serializing_if = "Option::is_none" 26 | )] 27 | pub max_priority_fee_per_gas: Option, 28 | pub gas: Option, 29 | pub value: Option, 30 | #[serde(default, alias = "data", skip_serializing_if = "Option::is_none")] 31 | pub input: Option, 32 | pub nonce: Option, 33 | #[serde(rename = "chainId", default, skip_serializing_if = "Option::is_none")] 34 | pub chain_id: Option, 35 | #[serde( 36 | rename = "accessList", 37 | default, 38 | skip_serializing_if = "Option::is_none" 39 | )] 40 | pub access_list: Option, 41 | } 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | use super::*; 46 | use crate::test_utils::{test_candid_roundtrip, test_json_roundtrip}; 47 | 48 | #[test] 49 | fn test_serde_roundtrip_with_data_field() { 50 | let json = r#"{ 51 | "from": "0x1234567890123456789012345678901234567890", 52 | "to": "0x0987654321098765432109876543210987654321", 53 | "gasPrice": "0x1234", 54 | "value": "0x5678", 55 | "data": "0xabcdef", 56 | "nonce": "0x9", 57 | "chainId": "0x1" 58 | }"#; 59 | 60 | let request: EstimateGasRequest = serde_json::from_str(json).unwrap(); 61 | test_json_roundtrip(&request); 62 | } 63 | 64 | #[test] 65 | fn test_serde_roundtrip_with_input_field() { 66 | let json = r#"{ 67 | "from": "0x1234567890123456789012345678901234567890", 68 | "to": "0x0987654321098765432109876543210987654321", 69 | "gasPrice": "0x1234", 70 | "value": "0x5678", 71 | "input": "0xabcdef", 72 | "nonce": "0x9", 73 | "chainId": "0x1" 74 | }"#; 75 | 76 | let request: EstimateGasRequest = serde_json::from_str(json).unwrap(); 77 | test_json_roundtrip(&request); 78 | } 79 | 80 | #[test] 81 | fn test_candid_roundtrip_with_data_field() { 82 | let json = r#"{ 83 | "from": "0x1234567890123456789012345678901234567890", 84 | "to": "0x0987654321098765432109876543210987654321", 85 | "gasPrice": "0x1234", 86 | "value": "0x5678", 87 | "data": "0xabcdef", 88 | "nonce": "0x9", 89 | "chainId": "0x1" 90 | }"#; 91 | 92 | let request: EstimateGasRequest = serde_json::from_str(json).unwrap(); 93 | test_candid_roundtrip(&request); 94 | } 95 | 96 | #[test] 97 | fn test_candid_roundtrip_with_input_field() { 98 | let json = r#"{ 99 | "from": "0x1234567890123456789012345678901234567890", 100 | "to": "0x0987654321098765432109876543210987654321", 101 | "gasPrice": "0x1234", 102 | "value": "0x5678", 103 | "input": "0xabcdef", 104 | "nonce": "0x9", 105 | "chainId": "0x1" 106 | }"#; 107 | 108 | let request: EstimateGasRequest = serde_json::from_str(json).unwrap(); 109 | test_candid_roundtrip(&request); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/did/src/http.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::collections::HashMap; 3 | 4 | use candid::CandidType; 5 | use serde::de::DeserializeOwned; 6 | use serde::{Deserialize, Serialize}; 7 | use serde_bytes::ByteBuf; 8 | 9 | use crate::rpc::error::Error; 10 | use crate::rpc::id::Id; 11 | use crate::rpc::response::Failure; 12 | use crate::rpc::version::Version; 13 | 14 | // A HTTP response. 15 | #[derive(Clone, Debug, CandidType, Deserialize)] 16 | pub struct HttpResponse { 17 | /// The HTTP status code. 18 | pub status_code: u16, 19 | /// The response header map. 20 | pub headers: HashMap, Cow<'static, str>>, 21 | /// The response body. 22 | pub body: ByteBuf, 23 | /// Whether the query call should be upgraded to an update call. 24 | pub upgrade: Option, 25 | } 26 | 27 | impl HttpResponse { 28 | pub fn new( 29 | status_code: u16, 30 | headers: HashMap, Cow<'static, str>>, 31 | body: ByteBuf, 32 | upgrade: Option, 33 | ) -> Self { 34 | Self { 35 | status_code, 36 | headers, 37 | body, 38 | upgrade, 39 | } 40 | } 41 | 42 | pub fn new_failure( 43 | jsonrpc: Option, 44 | id: Id, 45 | error: Error, 46 | status_code: HttpStatusCode, 47 | ) -> Self { 48 | let failure = Failure { jsonrpc, error, id }; 49 | let body = match serde_json::to_vec(&failure) { 50 | Ok(bytes) => ByteBuf::from(&bytes[..]), 51 | Err(e) => ByteBuf::from(e.to_string().as_bytes()), 52 | }; 53 | 54 | Self::new( 55 | status_code as u16, 56 | HashMap::from([("content-type".into(), "application/json".into())]), 57 | body, 58 | None, 59 | ) 60 | } 61 | 62 | /// Returns a new `HttpResponse` intended to be used for internal errors. 63 | pub fn internal_error(e: String) -> Self { 64 | let body = match serde_json::to_vec(&e) { 65 | Ok(bytes) => ByteBuf::from(&bytes[..]), 66 | Err(e) => ByteBuf::from(e.to_string().as_bytes()), 67 | }; 68 | 69 | Self { 70 | status_code: 500, 71 | headers: HashMap::from([("content-type".into(), "application/json".into())]), 72 | body, 73 | upgrade: None, 74 | } 75 | } 76 | 77 | /// Returns an OK response with the given body. 78 | pub fn ok(body: ByteBuf) -> Self { 79 | Self::new( 80 | HttpStatusCode::Ok as u16, 81 | HashMap::from([("content-type".into(), "application/json".into())]), 82 | body, 83 | None, 84 | ) 85 | } 86 | 87 | /// Upgrade response to update call. 88 | pub fn upgrade_response() -> Self { 89 | Self::new(204, HashMap::default(), ByteBuf::default(), Some(true)) 90 | } 91 | } 92 | 93 | /// The important components of an HTTP request. 94 | #[derive(Clone, Debug, CandidType, Deserialize)] 95 | pub struct HttpRequest { 96 | /// The HTTP method string. 97 | pub method: Cow<'static, str>, 98 | /// The URL that was visited. 99 | pub url: Cow<'static, str>, 100 | /// The request headers. 101 | pub headers: HashMap, Cow<'static, str>>, 102 | /// The request body. 103 | pub body: ByteBuf, 104 | } 105 | 106 | impl HttpRequest { 107 | pub fn new(data: &T) -> Self { 108 | let mut headers = HashMap::new(); 109 | headers.insert("content-type".into(), "application/json".into()); 110 | Self { 111 | method: "POST".into(), 112 | url: "".into(), 113 | headers, 114 | body: ByteBuf::from(serde_json::to_vec(&data).unwrap()), 115 | } 116 | } 117 | 118 | pub fn decode_body(&self) -> Result> 119 | where 120 | T: DeserializeOwned, 121 | { 122 | serde_json::from_slice::(&self.body).map_err(|_| { 123 | Box::new(HttpResponse::new_failure( 124 | Some(Version::V2), 125 | Id::Null, 126 | Error::parse_error(), 127 | HttpStatusCode::BadRequest, 128 | )) 129 | }) 130 | } 131 | 132 | /// Returns an header value by matching the key with a case-insensitive comparison. 133 | /// As IC HTTP headers are lowercased, this method cost is usually O(1) for matching lowercase inputs, and O(n) in any other case. 134 | pub fn get_header_ignore_case(&self, header_name: &str) -> Option<&Cow<'static, str>> { 135 | match self.headers.get(header_name) { 136 | Some(ip) => Some(ip), 137 | None => self 138 | .headers 139 | .iter() 140 | .find(|(k, _)| k.eq_ignore_ascii_case(header_name)) 141 | .map(|(_, v)| v), 142 | } 143 | } 144 | } 145 | 146 | #[repr(u16)] 147 | pub enum HttpStatusCode { 148 | Ok = 200, 149 | BadRequest = 400, 150 | InternalServerError = 500, 151 | } 152 | -------------------------------------------------------------------------------- /src/did/src/ic.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::collections::BTreeMap; 3 | 4 | use candid::CandidType; 5 | use ic_stable_structures::Storable; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::{Bytes, H160, H256, U256, codec}; 9 | 10 | /// Account full data 11 | #[derive(Debug, candid::CandidType, PartialEq, Eq, Clone, Serialize, Deserialize)] 12 | pub struct RawAccountInfo { 13 | /// Account nonce. 14 | pub nonce: U256, 15 | /// Account balance. 16 | pub balance: U256, 17 | /// Account bytecode. 18 | pub bytecode: Option, 19 | /// Storage value for the account. 20 | pub storage: Vec<(U256, U256)>, 21 | } 22 | 23 | impl RawAccountInfo { 24 | /// Estimate the byte size of the account info. 25 | pub fn estimate_byte_size(&self) -> usize { 26 | const NONCE_SIZE: usize = U256::BYTE_SIZE; 27 | const BALANCE_SIZE: usize = U256::BYTE_SIZE; 28 | let bytecode_size = self.bytecode.as_ref().map(|b| b.0.len()).unwrap_or(0); 29 | let storage_size = U256::BYTE_SIZE * 2 * self.storage.len(); 30 | 31 | NONCE_SIZE + BALANCE_SIZE + bytecode_size + storage_size 32 | } 33 | } 34 | 35 | /// A Map from account address to account info. 36 | #[derive(Debug, Default, candid::CandidType, PartialEq, Eq, Clone, Serialize, Deserialize)] 37 | pub struct AccountInfoMap { 38 | pub data: BTreeMap, 39 | } 40 | 41 | impl AccountInfoMap { 42 | /// Create a new account info map. 43 | pub fn new() -> Self { 44 | Self::default() 45 | } 46 | 47 | /// Estimate the byte size of the account info map. 48 | pub fn estimate_byte_size(&self) -> usize { 49 | const KEY_SIZE: usize = H160::BYTE_SIZE; 50 | let mut total_size = KEY_SIZE * self.data.len(); 51 | 52 | for account in self.data.values() { 53 | total_size += account.estimate_byte_size(); 54 | } 55 | 56 | total_size 57 | } 58 | } 59 | 60 | impl>> From for AccountInfoMap { 61 | fn from(data: D) -> Self { 62 | Self { data: data.into() } 63 | } 64 | } 65 | 66 | /// Contains the stats for the evm 67 | #[derive(Debug, Clone, CandidType, Eq, PartialEq, Deserialize)] 68 | pub struct EvmStats { 69 | /// This is the number of the pending transaction count 70 | pub pending_transactions_count: usize, 71 | /// Returns a vec of the transactions in the pool 72 | pub pending_transactions: Vec, 73 | /// Latest Block number 74 | pub block_number: u64, 75 | /// The CHAIN_ID for the evm 76 | pub chain_id: u64, 77 | /// This is the hash of all account balances, contract storage etc 78 | pub state_root: H256, 79 | /// Amount of Cycles that the canister has 80 | pub cycles: u128, 81 | /// The gas limit for the block 82 | pub block_gas_limit: u64, 83 | /// The total number of blocks in the history 84 | pub blocks_history_count: u64, 85 | /// The total number of receipts in the history 86 | pub receipts_history_count: u64, 87 | /// The total number of transactions in the history 88 | pub transactions_history_count: u64, 89 | /// The oldest version in the trie 90 | pub oldest_block_in_trie_history: u64, 91 | } 92 | 93 | /// The limits for the blockchain storage 94 | #[derive(Debug, Copy, Clone, CandidType, Serialize, Deserialize, PartialEq, Eq)] 95 | pub struct BlockchainStorageLimits { 96 | /// The maximum number of the blocks in the storage 97 | pub blocks_max_history_size: u64, 98 | /// The maximum number of the transactions and receipts in the storage 99 | pub transactions_and_receipts_max_history_size: u64, 100 | } 101 | 102 | /// Information about the blockchain 103 | #[derive(Debug, Clone, CandidType, Serialize, Deserialize, PartialEq, Eq)] 104 | pub struct BlockchainBlockInfo { 105 | /// The number of the first block in the blockchain 106 | pub earliest_block_number: u64, 107 | /// The number of the latest block in the blockchain 108 | pub latest_block_number: u64, 109 | /// The number of the safe block in the blockchain 110 | pub safe_block_number: u64, 111 | /// The number of the finalized block in the blockchain 112 | pub finalized_block_number: u64, 113 | /// The number of the pending block in the blockchain 114 | pub pending_block_number: u64, 115 | } 116 | 117 | /// Strategy for confirming a block. 118 | /// When a block is confirmed, it becomes `safe`. 119 | #[derive(Debug, Default, Clone, CandidType, Serialize, Deserialize, PartialEq, Eq)] 120 | pub enum BlockConfirmationStrategy { 121 | /// The block does not require any particular confirmation, 122 | /// it is always considered safe. 123 | #[default] 124 | None, 125 | 126 | /// The block requires a proof of work to be considered safe. 127 | /// The block is dropped if the proof of work is not provided in time. 128 | HashDropOnTimeout { 129 | /// The number of seconds to wait before dropping the block. 130 | /// If the block is not confirmed by then, it is dropped. 131 | timeout_secs: u64, 132 | }, 133 | } 134 | 135 | impl Storable for BlockConfirmationStrategy { 136 | const BOUND: ic_stable_structures::Bound = ic_stable_structures::Bound::Unbounded; 137 | 138 | fn to_bytes(&self) -> std::borrow::Cow<[u8]> { 139 | codec::encode(self).into() 140 | } 141 | 142 | fn from_bytes(bytes: Cow<[u8]>) -> Self { 143 | codec::decode(&bytes) 144 | } 145 | } 146 | 147 | /// Data required to confirm a block and mark it `safe`. 148 | #[derive(Default, Debug, Clone, CandidType, Serialize, Deserialize, PartialEq, Eq)] 149 | pub struct BlockConfirmationData { 150 | /// the block number 151 | pub block_number: u64, 152 | /// Hash of the block 153 | pub hash: H256, 154 | /// State root of the block 155 | pub state_root: H256, 156 | /// Transactions root of the block 157 | pub transactions_root: H256, 158 | /// Receipts root of the block 159 | pub receipts_root: H256, 160 | /// Proof of work of the block provided by the validator 161 | pub proof_of_work: H256, 162 | } 163 | 164 | /// Result of confirming a block. 165 | #[derive(Debug, Clone, CandidType, Serialize, Deserialize, PartialEq, Eq)] 166 | pub enum BlockConfirmationResult { 167 | /// The block is confirmed and is now safe. 168 | Confirmed, 169 | /// The block is not confirmed and is not safe. 170 | NotConfirmed, 171 | /// The block is already confirmed and is safe. 172 | AlreadyConfirmed, 173 | } 174 | 175 | #[cfg(test)] 176 | mod tests { 177 | 178 | use candid::{Decode, Encode}; 179 | 180 | use super::*; 181 | 182 | #[test] 183 | fn test_candid_encoding_raw_account() { 184 | let account_info = RawAccountInfo { 185 | nonce: U256::from(1u64), 186 | balance: U256::from(2u64), 187 | bytecode: Some(Bytes::from(vec![1, 2, 3])), 188 | storage: vec![ 189 | (U256::from(1u64), U256::from(2u64)), 190 | (U256::from(3u64), U256::from(4u64)), 191 | ], 192 | }; 193 | 194 | let bytes = Encode!(&account_info).unwrap(); 195 | let decoded = Decode!(bytes.as_slice(), RawAccountInfo).unwrap(); 196 | assert_eq!(account_info, decoded); 197 | } 198 | 199 | #[test] 200 | fn test_account_info_map_roundtrip() { 201 | let account_info_map = AccountInfoMap { 202 | data: [( 203 | H160::from([1; 20]), 204 | RawAccountInfo { 205 | nonce: U256::from(1u64), 206 | balance: U256::from(2u64), 207 | bytecode: Some(Bytes::from(vec![1, 2, 3])), 208 | storage: vec![ 209 | (U256::from(1u64), U256::from(2u64)), 210 | (U256::from(3u64), U256::from(4u64)), 211 | ], 212 | }, 213 | )] 214 | .into(), 215 | }; 216 | 217 | let bytes = Encode!(&account_info_map).unwrap(); 218 | let decoded = Decode!(bytes.as_slice(), AccountInfoMap).unwrap(); 219 | assert_eq!(account_info_map, decoded); 220 | } 221 | 222 | #[test] 223 | fn test_estimate_byte_size() { 224 | let account_info = RawAccountInfo { 225 | nonce: U256::from(1u64), 226 | balance: U256::from(2u64), 227 | bytecode: Some(Bytes::from(vec![1, 2, 3])), 228 | storage: vec![ 229 | (U256::from(1u64), U256::from(2u64)), 230 | (U256::from(3u64), U256::from(4u64)), 231 | ], 232 | }; 233 | 234 | let account_info_map = AccountInfoMap { 235 | data: [ 236 | (H160::from([1; 20]), account_info.clone()), 237 | (H160::from([2; 20]), account_info.clone()), 238 | (H160::from([3; 20]), account_info.clone()), 239 | ] 240 | .into(), 241 | }; 242 | 243 | let account_info_size = account_info.estimate_byte_size(); 244 | let account_info_map_size = account_info_map.estimate_byte_size(); 245 | 246 | assert_eq!(account_info_size, 32 + 32 + 3 + (32 * 4)); 247 | assert_eq!(account_info_map_size, 3 * (account_info_size + 20)); 248 | } 249 | 250 | #[test] 251 | fn test_storable_block_confirmation_strategy() { 252 | let strategy = BlockConfirmationStrategy::HashDropOnTimeout { timeout_secs: 10 }; 253 | 254 | let serialized = strategy.to_bytes(); 255 | let deserialized = BlockConfirmationStrategy::from_bytes(serialized); 256 | 257 | assert_eq!(strategy, deserialized); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/did/src/init.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use candid::{CandidType, Nat, Principal}; 4 | use ic_log::LogSettings; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::permission::Permission; 8 | use crate::{BlockConfirmationStrategy, H160, U256}; 9 | 10 | /// These are the arguments which are taken by the evm canister init fn 11 | #[derive(Debug, Clone, CandidType, Deserialize)] 12 | pub struct EvmCanisterInitData { 13 | pub signature_verification_principal: Principal, 14 | pub min_gas_price: Nat, 15 | pub chain_id: u64, 16 | #[serde(default)] 17 | pub log_settings: Option, 18 | #[serde(default)] 19 | pub permissions: Option)>>, 20 | #[serde(default)] 21 | pub transaction_processing_interval: Option, 22 | #[serde(default)] 23 | pub reserve_memory_pages: Option, 24 | /// Owner of the EVM Canister 25 | pub owner: Principal, 26 | /// Genesis accounts 27 | pub genesis_accounts: Vec<(H160, Option)>, 28 | /// Coinbase address 29 | pub coinbase: H160, 30 | /// Block confirmation strategy 31 | pub block_confirmation_strategy: BlockConfirmationStrategy, 32 | } 33 | 34 | impl Default for EvmCanisterInitData { 35 | fn default() -> Self { 36 | Self { 37 | signature_verification_principal: Principal::anonymous(), 38 | min_gas_price: Default::default(), 39 | chain_id: Default::default(), 40 | log_settings: Default::default(), 41 | permissions: Default::default(), 42 | reserve_memory_pages: Default::default(), 43 | transaction_processing_interval: Default::default(), 44 | owner: Principal::management_canister(), 45 | genesis_accounts: vec![], 46 | coinbase: Default::default(), 47 | block_confirmation_strategy: BlockConfirmationStrategy::default(), 48 | } 49 | } 50 | } 51 | 52 | /// These are the arguments which are taken by the signature verification canister init fn 53 | #[derive(Debug, Clone, Serialize, CandidType, Deserialize)] 54 | pub struct SignatureVerificationCanisterInitData { 55 | /// Access list of principals that are allowed to send transactions to the EVM canisters 56 | pub access_list: Vec, 57 | /// EVM canister Principal 58 | pub evm_canister: Principal, 59 | } 60 | -------------------------------------------------------------------------------- /src/did/src/keccak.rs: -------------------------------------------------------------------------------- 1 | use alloy::rlp::Encodable; 2 | 3 | use crate::H256; 4 | use crate::hash::Hash; 5 | 6 | /// The KECCAK of the RLP encoding of empty data, used in empty trie node, genesis block. 7 | /// https://docs.rs/keccak-hash/latest/keccak_hash/constant.KECCAK_NULL_RLP.html 8 | pub const KECCAK_NULL_RLP: H256 = Hash::(alloy::primitives::B256::new([ 9 | 0x56, 0xe8, 0x1f, 0x17, 0x1b, 0xcc, 0x55, 0xa6, 0xff, 0x83, 0x45, 0xe6, 0x92, 0xc0, 0xf8, 0x6e, 10 | 0x5b, 0x48, 0xe0, 0x1b, 0x99, 0x6c, 0xad, 0xc0, 0x01, 0x62, 0x2f, 0xb5, 0xe3, 0x63, 0xb4, 0x21, 11 | ])); 12 | 13 | /// The KECCAK of the empty byte slice 14 | pub const KECCAK_EMPTY: H256 = Hash::(alloy::primitives::B256::new([ 15 | 0xc5, 0xd2, 0x46, 0x01, 0x86, 0xf7, 0x23, 0x3c, 0x92, 0x7e, 0x7d, 0xb2, 0xdc, 0xc7, 0x03, 0xc0, 16 | 0xe5, 0x00, 0xb6, 0x53, 0xca, 0x82, 0x27, 0x3b, 0x7b, 0xfa, 0xd8, 0x04, 0x5d, 0x85, 0xa4, 0x70, 17 | ])); 18 | 19 | /// The KECCAK of the RLP encoding of empty list, used in genesis block. 20 | /// https://docs.rs/keccak-hash/latest/keccak_hash/constant.KECCAK_EMPTY_LIST_RLP.html 21 | pub const KECCAK_EMPTY_LIST_RLP: H256 = 22 | Hash::(alloy::primitives::B256::new([ 23 | 0x1d, 0xcc, 0x4d, 0xe8, 0xde, 0xc7, 0x5d, 0x7a, 0xab, 0x85, 0xb5, 0x67, 0xb6, 0xcc, 0xd4, 24 | 0x1a, 0xd3, 0x12, 0x45, 0x1b, 0x94, 0x8a, 0x74, 0x13, 0xf0, 0xa1, 0x42, 0xfd, 0x40, 0xd4, 25 | 0x93, 0x47, 26 | ])); 27 | 28 | /// Calculate the Keccak hash of an encoded rlp stream 29 | #[inline] 30 | pub fn keccak_hash_rlp(data: &E) -> H256 { 31 | keccak_hash(&alloy::rlp::encode(data)) 32 | } 33 | 34 | /// Calculate the Keccak hash 35 | #[inline] 36 | pub fn keccak_hash(data: &[u8]) -> H256 { 37 | alloy::primitives::keccak256(data).into() 38 | } 39 | -------------------------------------------------------------------------------- /src/did/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate contains our implementation of some of the `alloy_primitives` type. 2 | //! We have derived `candid::CandidType` for all of the types required, and implemented `From` and `Into` for all for easy conversion between the two. 3 | //! This is required because of `ic` Canisters required all types that are used in `update` and `query` methods to have `candid::CandidType` derived. 4 | //! This module contains submodules for each of the types that we have implemented. 5 | 6 | pub mod block; 7 | pub mod build; 8 | pub mod bytes; 9 | pub mod certified; 10 | pub mod codec; 11 | pub mod constant; 12 | pub mod error; 13 | pub mod evm_state; 14 | pub mod gas; 15 | pub mod hash; 16 | pub mod ic; 17 | pub mod init; 18 | pub mod integer; 19 | pub mod keccak; 20 | pub mod logs; 21 | pub mod permission; 22 | pub mod revert_blocks; 23 | pub mod send_raw_transaction; 24 | pub mod state; 25 | pub mod transaction; 26 | pub mod unsafe_blocks; 27 | 28 | pub mod fees; 29 | pub mod http; 30 | pub mod rpc; 31 | #[cfg(test)] 32 | mod test_utils; 33 | pub mod utils; 34 | 35 | pub use block::Block; 36 | pub use error::{ExitFatal, HaltError}; 37 | pub use fees::FeeHistory; 38 | pub use gas::*; 39 | pub use hash::{H64, H160, H256}; 40 | pub use integer::{U64, U256}; 41 | pub use transaction::{BlockId, BlockNumber, Transaction, TransactionReceipt}; 42 | 43 | pub use crate::bytes::Bytes; 44 | pub use crate::ic::*; 45 | -------------------------------------------------------------------------------- /src/did/src/logs.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::Value; 3 | use serde_with::formats::PreferOne; 4 | use serde_with::{OneOrMany, serde_as}; 5 | 6 | use crate::{BlockNumber, Bytes, H160, H256, U64, U256}; 7 | 8 | #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] 9 | #[serde(untagged)] 10 | pub enum BlockFilter { 11 | #[serde(rename_all = "camelCase")] 12 | Exact { block_hash: H256 }, 13 | #[serde(rename_all = "camelCase")] 14 | Bounded { 15 | from_block: Option, 16 | to_block: Option, 17 | }, 18 | } 19 | 20 | #[serde_as] 21 | #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] 22 | #[serde(transparent)] 23 | pub struct LogAddressFilter(#[serde_as(deserialize_as = "OneOrMany<_, PreferOne>")] pub Vec); 24 | 25 | #[serde_as] 26 | #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] 27 | #[serde(transparent)] 28 | pub struct LogTopicFilter(#[serde_as(deserialize_as = "OneOrMany<_, PreferOne>")] pub Vec); 29 | 30 | #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Default)] 31 | #[serde(rename_all = "camelCase")] 32 | pub struct LogFilter { 33 | #[serde(flatten)] 34 | pub block_filter: Option, 35 | pub address: Option, 36 | pub topics: Option>>, 37 | } 38 | 39 | impl TryFrom for LogFilter { 40 | type Error = crate::rpc::error::Error; 41 | 42 | fn try_from(value: Value) -> Result { 43 | if let Value::Object(ref map) = value { 44 | // According to documentation if `blockHash` property is specified then `fromBlock` and `toBlock` shouldn't be specified 45 | if map.contains_key("blockHash") 46 | && (map.contains_key("fromBlock") || map.contains_key("toBlock")) 47 | { 48 | Err(Self::Error::invalid_params( 49 | "'blockHash' property cannot be used with 'fromBlock' or 'toBlock'", 50 | )) 51 | } else { 52 | let mut filter: LogFilter = 53 | serde_json::from_value(value).map_err(|_| Self::Error::parse_error())?; 54 | 55 | // Empty block filter can be serialized as `block_filter: BlockFilter::Bounded(from_block: None, to_block:None)` 56 | // That could be OK for us because it is equivalent to `block_filter: None`, but it's better to disambiguate things 57 | if let Some(BlockFilter::Bounded { 58 | from_block: None, 59 | to_block: None, 60 | }) = filter.block_filter 61 | { 62 | filter.block_filter = None; 63 | } 64 | 65 | Ok(filter) 66 | } 67 | } else { 68 | Err(Self::Error::invalid_params("invalid json value")) 69 | } 70 | } 71 | } 72 | 73 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 74 | #[serde(rename_all = "camelCase")] 75 | /// Transaction's log entry. 76 | pub struct TransactionLog { 77 | /// Log's index within transaction. 78 | pub log_index: U256, 79 | /// Transaction's index within block. 80 | pub transaction_index: U64, 81 | /// Transaction's hash. 82 | pub transaction_hash: H256, 83 | /// Block's hash, transaction is included in. 84 | pub block_hash: H256, 85 | /// Block number, transaction is included in. 86 | pub block_number: U64, 87 | /// Log's address. 88 | pub address: H160, 89 | /// Log's data. 90 | pub data: Bytes, 91 | /// Log's Topics. 92 | pub topics: Vec, 93 | } 94 | 95 | #[cfg(test)] 96 | mod tests { 97 | use serde_json::json; 98 | 99 | use super::*; 100 | 101 | const BLOCK_HASH_1: &str = "f43869e67c02c57d1f9a07bb897b54bec1cfa1feb704d91a2ee087566de5df2c"; 102 | const TOPIC_1: &str = "cc6a069bf885d8cf2fb456ca33db48ab7d5e3df1e6504a18e7899a16d604f5c6"; 103 | const TOPIC_2: &str = "e4058f2da8dda0b1ffb454bb7d121c1498dcfc4446a3d86b7c03e27b34e29345"; 104 | const ADDRESS: &str = "7fafd954cbcfd683304cd9be0a85848cbbb1c13d"; 105 | 106 | fn get_block_hash_1_str() -> String { 107 | format!("0x{BLOCK_HASH_1}") 108 | } 109 | 110 | fn get_block_hash_1() -> H256 { 111 | H256::from_hex_str(BLOCK_HASH_1).unwrap() 112 | } 113 | 114 | fn get_topic_1() -> H256 { 115 | H256::from_hex_str(TOPIC_1).unwrap() 116 | } 117 | 118 | fn get_topic_1_str() -> String { 119 | format!("0x{TOPIC_1}") 120 | } 121 | 122 | fn get_topic_2() -> H256 { 123 | H256::from_hex_str(TOPIC_2).unwrap() 124 | } 125 | 126 | fn get_topic_2_str() -> String { 127 | format!("0x{TOPIC_2}") 128 | } 129 | 130 | fn get_address_str() -> String { 131 | format!("0x{ADDRESS}") 132 | } 133 | 134 | fn get_address_1() -> H160 { 135 | H160::from_hex_str(ADDRESS).unwrap() 136 | } 137 | 138 | #[test] 139 | fn test_log_filter_deserialization_fail() { 140 | assert!(LogFilter::try_from(json!([])).is_err()); 141 | assert!(LogFilter::try_from(json!("str")).is_err()); 142 | assert!(LogFilter::try_from(json!(42)).is_err()); 143 | assert!( 144 | LogFilter::try_from( 145 | json!({"blockHash": get_block_hash_1_str(), "fromBlock": "earliest"}) 146 | ) 147 | .is_err() 148 | ); 149 | assert!( 150 | LogFilter::try_from(json!({"blockHash": get_block_hash_1_str(), "toBlock": "0x01"})) 151 | .is_err() 152 | ); 153 | } 154 | 155 | #[test] 156 | fn test_log_filter_deserialization_empty() { 157 | let filter = LogFilter::try_from(json!({})).unwrap(); 158 | let expected_filter = Default::default(); 159 | assert_eq!(filter, expected_filter); 160 | } 161 | 162 | #[test] 163 | fn test_log_filter_deserialization_block_filter() { 164 | let filter = 165 | LogFilter::try_from(json!({"fromBlock": "earliest", "toBlock": "0x01"})).unwrap(); 166 | 167 | let expected_filter = LogFilter { 168 | block_filter: Some(BlockFilter::Bounded { 169 | from_block: Some(BlockNumber::Earliest), 170 | to_block: Some(BlockNumber::Number(U64::from(1u64))), 171 | }), 172 | ..Default::default() 173 | }; 174 | assert_eq!(filter, expected_filter); 175 | 176 | let filter = LogFilter::try_from(json!({ "blockHash": get_block_hash_1_str() })).unwrap(); 177 | let expected_filter = LogFilter { 178 | block_filter: Some(BlockFilter::Exact { 179 | block_hash: get_block_hash_1(), 180 | }), 181 | ..Default::default() 182 | }; 183 | assert_eq!(filter, expected_filter); 184 | } 185 | 186 | #[test] 187 | fn test_log_filter_deserialization_address() { 188 | let filter = LogFilter::try_from(json!({ 189 | "address": [], 190 | })) 191 | .unwrap(); 192 | let expected_filter = LogFilter { 193 | address: Some(LogAddressFilter(vec![])), 194 | ..Default::default() 195 | }; 196 | assert_eq!(filter, expected_filter); 197 | 198 | let filter = LogFilter::try_from(json!({ 199 | "address": [get_address_str()], 200 | })) 201 | .unwrap(); 202 | let expected_filter = LogFilter { 203 | address: Some(LogAddressFilter(vec![get_address_1()])), 204 | ..Default::default() 205 | }; 206 | assert_eq!(filter, expected_filter); 207 | 208 | let filter = LogFilter::try_from(json!({ 209 | "address": [get_address_str(), get_address_str()], 210 | })) 211 | .unwrap(); 212 | let expected_filter = LogFilter { 213 | address: Some(LogAddressFilter(vec![get_address_1(), get_address_1()])), 214 | ..Default::default() 215 | }; 216 | assert_eq!(filter, expected_filter); 217 | } 218 | 219 | #[test] 220 | fn test_log_filter_deserialization_topics() { 221 | let filter = LogFilter::try_from(json!({ 222 | "topics": [], 223 | })) 224 | .unwrap(); 225 | let expected_filter = LogFilter { 226 | topics: Some(vec![]), 227 | ..Default::default() 228 | }; 229 | assert_eq!(filter, expected_filter); 230 | 231 | let filter = LogFilter::try_from(json!({ 232 | "topics": [null], 233 | })) 234 | .unwrap(); 235 | let expected_filter = LogFilter { 236 | topics: Some(vec![None]), 237 | ..Default::default() 238 | }; 239 | assert_eq!(filter, expected_filter); 240 | 241 | let filter = LogFilter::try_from(json!({ 242 | "topics": [[get_topic_1_str()]], 243 | })) 244 | .unwrap(); 245 | let expected_filter = LogFilter { 246 | topics: Some(vec![Some(LogTopicFilter(vec![get_topic_1()]))]), 247 | ..Default::default() 248 | }; 249 | assert_eq!(filter, expected_filter); 250 | 251 | let filter = LogFilter::try_from(json!({ 252 | "topics": [[get_topic_1_str()], null], 253 | })) 254 | .unwrap(); 255 | let expected_filter = LogFilter { 256 | topics: Some(vec![Some(LogTopicFilter(vec![get_topic_1()])), None]), 257 | ..Default::default() 258 | }; 259 | assert_eq!(filter, expected_filter); 260 | 261 | let filter = LogFilter::try_from(json!({ 262 | "topics": [[get_topic_1_str()], null, [get_topic_1_str(), get_topic_2_str()]], 263 | })) 264 | .unwrap(); 265 | let expected_filter = LogFilter { 266 | topics: Some(vec![ 267 | Some(LogTopicFilter(vec![get_topic_1()])), 268 | None, 269 | Some(LogTopicFilter(vec![get_topic_1(), get_topic_2()])), 270 | ]), 271 | ..Default::default() 272 | }; 273 | assert_eq!(filter, expected_filter); 274 | } 275 | 276 | #[test] 277 | fn test_log_filter_deserialization_combine() { 278 | let filter = LogFilter::try_from(json!({ 279 | "blockHash": get_block_hash_1_str(), 280 | "address": [get_address_str()], 281 | "topics": [null, [get_topic_1_str()], [get_topic_1_str(), get_topic_2_str()]], 282 | })) 283 | .unwrap(); 284 | let expected_filter = LogFilter { 285 | block_filter: Some(BlockFilter::Exact { 286 | block_hash: get_block_hash_1(), 287 | }), 288 | address: Some(LogAddressFilter(vec![get_address_1()])), 289 | topics: Some(vec![ 290 | None, 291 | Some(LogTopicFilter(vec![get_topic_1()])), 292 | Some(LogTopicFilter(vec![get_topic_1(), get_topic_2()])), 293 | ]), 294 | }; 295 | assert_eq!(filter, expected_filter); 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /src/did/src/permission.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use candid::{CandidType, Deserialize}; 4 | use ic_stable_structures::Storable; 5 | 6 | use crate::codec; 7 | 8 | /// Principal specific permission 9 | #[derive( 10 | Debug, Clone, CandidType, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, 11 | )] 12 | pub enum Permission { 13 | /// Gives administrator permissions 14 | Admin, 15 | /// Allows calling the endpoints to read the logs and get runtime statistics 16 | ReadLogs, 17 | /// Allows caller to reset the EVM state 18 | ResetEvmState, 19 | /// Allows calling the endpoints to set the logs configuration 20 | UpdateLogsConfiguration, 21 | /// Allows calling the endpoints to set validate unsafe blocks 22 | ValidateUnsafeBlocks, 23 | /// Allows the signature verification canister to send transaction to 24 | /// the EVM Canister 25 | PrivilegedSendTransaction, 26 | } 27 | 28 | #[derive(Debug, Clone, Default, CandidType, Deserialize, PartialEq, Eq, serde::Serialize)] 29 | pub struct PermissionList { 30 | pub permissions: HashSet, 31 | } 32 | 33 | impl Storable for PermissionList { 34 | fn to_bytes(&self) -> std::borrow::Cow<[u8]> { 35 | codec::encode(self).into() 36 | } 37 | 38 | fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { 39 | codec::decode(&bytes) 40 | } 41 | 42 | const BOUND: ic_stable_structures::Bound = ic_stable_structures::Bound::Unbounded; 43 | } 44 | 45 | #[cfg(test)] 46 | mod test { 47 | 48 | use candid::{Decode, Encode}; 49 | 50 | use super::*; 51 | 52 | #[test] 53 | fn test_candid_permission_list() { 54 | let permission_list = PermissionList { 55 | permissions: HashSet::from_iter(vec![Permission::Admin, Permission::ReadLogs]), 56 | }; 57 | 58 | let serialized = Encode!(&permission_list).unwrap(); 59 | let deserialized = Decode!(serialized.as_slice(), PermissionList).unwrap(); 60 | 61 | assert_eq!(permission_list, deserialized); 62 | } 63 | 64 | #[test] 65 | fn test_storable_permission_list() { 66 | let permission_list = PermissionList { 67 | permissions: HashSet::from_iter(vec![Permission::Admin, Permission::ReadLogs]), 68 | }; 69 | 70 | let serialized = permission_list.to_bytes(); 71 | let deserialized = PermissionList::from_bytes(serialized); 72 | 73 | assert_eq!(permission_list, deserialized); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/did/src/revert_blocks.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use candid::CandidType; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::H256; 7 | 8 | /// Arguments for `revert_to_block` method of EVM canister 9 | /// 10 | /// The target block is specified by the `to_block_number` field and the other fields are used to 11 | /// verify that the caller actually knows what they are doing. 12 | #[derive(Debug, Serialize, Deserialize, CandidType, PartialEq, Eq, Clone)] 13 | pub struct RevertToBlockArgs { 14 | /// Current latest block number. 15 | pub from_block_number: u64, 16 | 17 | /// Hash of the latest block. 18 | pub from_block_hash: H256, 19 | 20 | /// Block number to revert to. 21 | pub to_block_number: u64, 22 | 23 | /// Hash of the block to revert to. 24 | pub to_block_hash: H256, 25 | } 26 | 27 | impl Display for RevertToBlockArgs { 28 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 29 | write!( 30 | f, 31 | "{{from_block_number: {}, from_block_hash: {}, to_block_number: {}, to_block_hash: {}}}", 32 | self.from_block_number, self.from_block_hash, self.to_block_number, self.to_block_hash 33 | ) 34 | } 35 | } 36 | 37 | #[cfg(test)] 38 | mod test { 39 | 40 | use super::*; 41 | 42 | #[test] 43 | fn test_revert_to_block_args_println() { 44 | let args = RevertToBlockArgs { 45 | from_block_number: 1, 46 | from_block_hash: H256::from([1; 32]), 47 | to_block_number: 2, 48 | to_block_hash: H256::from([2; 32]), 49 | }; 50 | 51 | assert_eq!( 52 | "{from_block_number: 1, from_block_hash: 0x0101010101010101010101010101010101010101010101010101010101010101, to_block_number: 2, to_block_hash: 0x0202020202020202020202020202020202020202020202020202020202020202}", 53 | format!("{}", args) 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/did/src/rpc/error.rs: -------------------------------------------------------------------------------- 1 | //! jsonrpc errors 2 | 3 | use std::fmt; 4 | 5 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 6 | use serde_json::Value; 7 | 8 | /// JSONRPC error code 9 | #[derive(Debug, thiserror::Error, PartialEq, Clone)] 10 | pub enum ErrorCode { 11 | /// Invalid JSON was received by the server. 12 | /// An error occurred on the server while parsing the JSON text. 13 | #[error("Parse error")] 14 | ParseError, 15 | /// The JSON sent is not a valid Request object. 16 | #[error("Invalid request")] 17 | InvalidRequest, 18 | /// The method does not exist / is not available. 19 | #[error("Method not found")] 20 | MethodNotFound, 21 | /// Invalid method parameter(s). 22 | #[error("Invalid params")] 23 | InvalidParams, 24 | /// Internal JSON-RPC error. 25 | #[error("Internal error")] 26 | InternalError, 27 | /// Reserved for implementation-defined server-errors. 28 | #[error("Server error: {0}")] 29 | ServerError(i64), 30 | } 31 | 32 | impl ErrorCode { 33 | /// Returns integer code value 34 | pub fn code(&self) -> i64 { 35 | match *self { 36 | ErrorCode::ParseError => -32700, 37 | ErrorCode::InvalidRequest => -32600, 38 | ErrorCode::MethodNotFound => -32601, 39 | ErrorCode::InvalidParams => -32602, 40 | ErrorCode::InternalError => -32603, 41 | ErrorCode::ServerError(code) => code, 42 | } 43 | } 44 | 45 | /// Returns human-readable description 46 | pub fn description(&self) -> String { 47 | self.to_string() 48 | } 49 | } 50 | 51 | impl From for ErrorCode { 52 | fn from(code: i64) -> Self { 53 | match code { 54 | -32700 => ErrorCode::ParseError, 55 | -32600 => ErrorCode::InvalidRequest, 56 | -32601 => ErrorCode::MethodNotFound, 57 | -32602 => ErrorCode::InvalidParams, 58 | -32603 => ErrorCode::InternalError, 59 | code => ErrorCode::ServerError(code), 60 | } 61 | } 62 | } 63 | 64 | impl<'a> Deserialize<'a> for ErrorCode { 65 | fn deserialize(deserializer: D) -> Result 66 | where 67 | D: Deserializer<'a>, 68 | { 69 | let code: i64 = Deserialize::deserialize(deserializer)?; 70 | Ok(ErrorCode::from(code)) 71 | } 72 | } 73 | 74 | impl Serialize for ErrorCode { 75 | fn serialize(&self, serializer: S) -> Result 76 | where 77 | S: Serializer, 78 | { 79 | serializer.serialize_i64(self.code()) 80 | } 81 | } 82 | 83 | /// Error object as defined in Spec 84 | #[derive(Debug, thiserror::Error, PartialEq, Clone, Serialize, Deserialize)] 85 | #[serde(deny_unknown_fields)] 86 | #[error("{code}: {message}")] 87 | pub struct Error { 88 | /// Code 89 | pub code: ErrorCode, 90 | /// Message 91 | pub message: String, 92 | /// Optional data 93 | #[serde(skip_serializing_if = "Option::is_none")] 94 | pub data: Option, 95 | } 96 | 97 | impl Error { 98 | /// Wraps given `ErrorCode` 99 | pub fn new(code: ErrorCode) -> Self { 100 | Error { 101 | message: code.description(), 102 | code, 103 | data: None, 104 | } 105 | } 106 | 107 | /// Creates new `ParseError` 108 | pub fn parse_error() -> Self { 109 | Self::new(ErrorCode::ParseError) 110 | } 111 | 112 | /// Creates new `InvalidRequest` 113 | pub fn invalid_request() -> Self { 114 | Self::new(ErrorCode::InvalidRequest) 115 | } 116 | 117 | /// Creates new `MethodNotFound` 118 | pub fn method_not_found() -> Self { 119 | Self::new(ErrorCode::MethodNotFound) 120 | } 121 | 122 | /// Creates new `InvalidParams` 123 | pub fn invalid_params(message: M) -> Self 124 | where 125 | M: Into, 126 | { 127 | Error { 128 | code: ErrorCode::InvalidParams, 129 | message: message.into(), 130 | data: None, 131 | } 132 | } 133 | 134 | /// Creates `InvalidParams` for given parameter, with details. 135 | pub fn invalid_params_with_details(message: M, details: T) -> Error 136 | where 137 | M: Into, 138 | T: fmt::Debug, 139 | { 140 | Error { 141 | code: ErrorCode::InvalidParams, 142 | message: format!("Invalid parameters: {}", message.into()), 143 | data: Some(Value::String(format!("{:?}", details))), 144 | } 145 | } 146 | 147 | /// Creates new `InternalError` 148 | pub fn internal_error() -> Self { 149 | Self::new(ErrorCode::InternalError) 150 | } 151 | 152 | /// Creates new `InvalidRequest` with invalid version description 153 | pub fn invalid_version() -> Self { 154 | Error { 155 | code: ErrorCode::InvalidRequest, 156 | message: "Unsupported JSON-RPC protocol version".to_owned(), 157 | data: None, 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/did/src/rpc/id.rs: -------------------------------------------------------------------------------- 1 | //! jsonrpc id field 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// Request Id 6 | #[derive(Debug, PartialEq, Clone, Hash, Eq, Deserialize, Serialize)] 7 | #[serde(deny_unknown_fields)] 8 | #[serde(untagged)] 9 | pub enum Id { 10 | /// No id (notification) 11 | Null, 12 | /// Numeric id 13 | Number(u64), 14 | /// String id 15 | String(String), 16 | } 17 | 18 | #[cfg(test)] 19 | mod tests { 20 | use serde_json; 21 | 22 | use super::*; 23 | 24 | #[test] 25 | fn id_deserialization() { 26 | let s = r#""2""#; 27 | let deserialized: Id = serde_json::from_str(s).unwrap(); 28 | assert_eq!(deserialized, Id::String("2".into())); 29 | 30 | let s = r#"2"#; 31 | let deserialized: Id = serde_json::from_str(s).unwrap(); 32 | assert_eq!(deserialized, Id::Number(2)); 33 | 34 | let s = r#""2x""#; 35 | let deserialized: Id = serde_json::from_str(s).unwrap(); 36 | assert_eq!(deserialized, Id::String("2x".to_owned())); 37 | 38 | let s = r#"[null, 0, 2, "3"]"#; 39 | let deserialized: Vec = serde_json::from_str(s).unwrap(); 40 | assert_eq!( 41 | deserialized, 42 | vec![ 43 | Id::Null, 44 | Id::Number(0), 45 | Id::Number(2), 46 | Id::String("3".into()) 47 | ] 48 | ); 49 | } 50 | 51 | #[test] 52 | fn id_serialization() { 53 | let d = vec![ 54 | Id::Null, 55 | Id::Number(0), 56 | Id::Number(2), 57 | Id::Number(3), 58 | Id::String("3".to_owned()), 59 | Id::String("test".to_owned()), 60 | ]; 61 | let serialized = serde_json::to_string(&d).unwrap(); 62 | assert_eq!(serialized, r#"[null,0,2,3,"3","test"]"#); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/did/src/rpc/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | pub mod id; 3 | pub mod params; 4 | pub mod request; 5 | pub mod response; 6 | pub mod version; 7 | -------------------------------------------------------------------------------- /src/did/src/rpc/version.rs: -------------------------------------------------------------------------------- 1 | //! jsonrpc version field 2 | use std::fmt; 3 | 4 | use serde::de::{self, Visitor}; 5 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 6 | 7 | /// Protocol Version 8 | #[derive(Debug, PartialEq, Clone, Copy, Hash, Eq)] 9 | pub enum Version { 10 | /// JSONRPC 2.0 11 | V2, 12 | } 13 | 14 | impl Serialize for Version { 15 | fn serialize(&self, serializer: S) -> Result 16 | where 17 | S: Serializer, 18 | { 19 | match *self { 20 | Version::V2 => serializer.serialize_str("2.0"), 21 | } 22 | } 23 | } 24 | 25 | impl<'a> Deserialize<'a> for Version { 26 | fn deserialize(deserializer: D) -> Result 27 | where 28 | D: Deserializer<'a>, 29 | { 30 | deserializer.deserialize_identifier(VersionVisitor) 31 | } 32 | } 33 | 34 | struct VersionVisitor; 35 | 36 | impl Visitor<'_> for VersionVisitor { 37 | type Value = Version; 38 | 39 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 40 | formatter.write_str("a string") 41 | } 42 | 43 | fn visit_str(self, value: &str) -> Result 44 | where 45 | E: de::Error, 46 | { 47 | match value { 48 | "2.0" => Ok(Version::V2), 49 | _ => Err(de::Error::custom("invalid version")), 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/did/src/send_raw_transaction/signature.rs: -------------------------------------------------------------------------------- 1 | use candid::CandidType; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::{U64, U256}; 5 | 6 | /// A signature is a pair of integers (r, s) that are used to sign transactions. 7 | /// The signature also contains a recovery id v that is used to recover the public key from the signature. 8 | /// The public key is then used to verify the signature. 9 | #[derive(Debug, Clone, PartialEq, Eq, CandidType, Serialize, Deserialize)] 10 | pub struct Signature { 11 | pub v: U64, 12 | pub r: U256, 13 | pub s: U256, 14 | } 15 | -------------------------------------------------------------------------------- /src/did/src/send_raw_transaction/tx_kind.rs: -------------------------------------------------------------------------------- 1 | use candid::CandidType; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::H160; 5 | 6 | /// The `to` field of a transaction. Either a target address, or empty for a contract creation 7 | #[derive(Debug, Clone, PartialEq, Eq, CandidType, Serialize, Deserialize, Default)] 8 | pub enum TxKind { 9 | #[default] 10 | Create, 11 | Call(H160), 12 | } 13 | 14 | impl From> for TxKind { 15 | fn from(value: Option) -> Self { 16 | match value { 17 | Some(address) => TxKind::Call(address), 18 | None => TxKind::Create, 19 | } 20 | } 21 | } 22 | 23 | impl TxKind { 24 | /// Returns the address of the contract that will be called or will receive the transfer 25 | pub fn to(&self) -> Option<&H160> { 26 | match self { 27 | TxKind::Create => None, 28 | TxKind::Call(to) => Some(to), 29 | } 30 | } 31 | } 32 | 33 | #[cfg(test)] 34 | mod test { 35 | 36 | use super::*; 37 | 38 | #[test] 39 | fn test_should_convert_from_h160() { 40 | let address = H160::from([0u8; 20]); 41 | let tx_kind: TxKind = Some(address.clone()).into(); 42 | assert_eq!(tx_kind, TxKind::Call(address)); 43 | 44 | let tx_kind: TxKind = None.into(); 45 | assert_eq!(tx_kind, TxKind::Create); 46 | } 47 | 48 | #[test] 49 | fn test_should_return_to_address() { 50 | let address = H160::from([0u8; 20]); 51 | let tx_kind = TxKind::Call(address.clone()); 52 | assert_eq!(tx_kind.to(), Some(&address)); 53 | 54 | let tx_kind = TxKind::Create; 55 | assert_eq!(tx_kind.to(), None); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/did/src/state.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use candid::CandidType; 4 | use ic_stable_structures::{Bound, Storable}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::{U256, codec}; 8 | 9 | /// Describes basic state of an EVM account. 10 | #[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, CandidType)] 11 | pub struct BasicAccount { 12 | /// Account balance. 13 | pub balance: U256, 14 | /// Account nonce. 15 | pub nonce: U256, 16 | } 17 | 18 | /// Full information about entry 19 | #[derive(Clone, Debug, CandidType, Deserialize, Serialize, PartialEq, Eq)] 20 | pub struct StorageValue { 21 | /// Data 22 | pub data: Vec, 23 | /// Number of inserts subtracted by number of removals. 24 | /// May be zero for the values which were removed in past before the moment they are cleaned. 25 | pub rc: u32, 26 | } 27 | 28 | impl Storable for StorageValue { 29 | const BOUND: Bound = Bound::Unbounded; 30 | 31 | fn to_bytes(&self) -> std::borrow::Cow<[u8]> { 32 | codec::bincode_encode(self).into() 33 | } 34 | 35 | fn from_bytes(bytes: Cow<[u8]>) -> Self { 36 | codec::bincode_decode(&bytes) 37 | } 38 | } 39 | 40 | #[cfg(test)] 41 | mod test { 42 | 43 | use candid::{Decode, Encode}; 44 | 45 | use super::*; 46 | 47 | #[test] 48 | fn test_candid_basic_account() { 49 | let account = BasicAccount { 50 | balance: U256::from(1u64), 51 | nonce: U256::from(2u64), 52 | }; 53 | 54 | let serialized = Encode!(&account).unwrap(); 55 | let deserialized = Decode!(serialized.as_slice(), BasicAccount).unwrap(); 56 | 57 | assert_eq!(account, deserialized); 58 | } 59 | 60 | #[test] 61 | fn test_storable_storage_value() { 62 | let storage_value = StorageValue { 63 | data: vec![1, 2, 3], 64 | rc: 4, 65 | }; 66 | 67 | let serialized = storage_value.to_bytes(); 68 | let deserialized = StorageValue::from_bytes(serialized); 69 | 70 | assert_eq!(storage_value, deserialized); 71 | } 72 | 73 | #[test] 74 | fn test_candid_storage_value() { 75 | let storage_value = StorageValue { 76 | data: vec![1, 2, 3], 77 | rc: 4, 78 | }; 79 | 80 | let serialized = Encode!(&storage_value).unwrap(); 81 | let deserialized = Decode!(serialized.as_slice(), StorageValue).unwrap(); 82 | 83 | assert_eq!(storage_value, deserialized); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/did/src/test_utils.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt::Debug; 3 | 4 | use candid::CandidType; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_json::Value; 7 | 8 | /// Reads all the files in a directory and parses them into serde_json::Value 9 | pub fn read_all_files_to_json(path: &str) -> HashMap { 10 | let mut jsons = HashMap::new(); 11 | for file in std::fs::read_dir(path).unwrap() { 12 | let file = file.unwrap(); 13 | let filename = file 14 | .file_name() 15 | .to_str() 16 | .unwrap() 17 | .trim_end_matches(".json") 18 | .to_owned(); 19 | let value: Value = 20 | serde_json::from_str(&std::fs::read_to_string(file.path()).unwrap()).unwrap(); 21 | jsons.insert(filename, value); 22 | } 23 | jsons 24 | } 25 | 26 | pub fn test_json_roundtrip(val: &T) 27 | where 28 | for<'de> T: Serialize + Deserialize<'de> + PartialEq + Debug, 29 | { 30 | let serialized = serde_json::to_string(val).unwrap(); 31 | let restored: T = serde_json::from_str(&serialized).unwrap(); 32 | 33 | assert_eq!(val, &restored); 34 | } 35 | 36 | pub fn test_candid_roundtrip(val: &T) 37 | where 38 | for<'de> T: CandidType + Deserialize<'de> + PartialEq + Debug, 39 | { 40 | let serialized = candid::encode_one(val).unwrap(); 41 | let restored: T = candid::decode_one(&serialized).unwrap(); 42 | 43 | assert_eq!(val, &restored); 44 | } 45 | -------------------------------------------------------------------------------- /src/did/src/unsafe_blocks.rs: -------------------------------------------------------------------------------- 1 | use candid::{CandidType, Deserialize}; 2 | use serde::Serialize; 3 | 4 | use crate::H256; 5 | 6 | #[derive(CandidType, Serialize, Deserialize, Debug)] 7 | /// Arguments to `validate_unsafe_blocks` function. 8 | pub struct ValidateUnsafeBlockArgs { 9 | pub block_number: u64, 10 | pub block_hash: H256, 11 | pub state_root: H256, 12 | pub transactions_root: H256, 13 | pub receipts_root: H256, 14 | } 15 | -------------------------------------------------------------------------------- /src/did/src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::{H160, Transaction, U256}; 2 | 3 | /// Creates an ephemeral transaction to calculate the Proof of Work (PoW) 4 | /// required by the EVM block confirmation endpoint. 5 | /// This transaction is not meant to be used in a real blockchain. 6 | /// 7 | pub fn block_confirmation_pow_transaction( 8 | from: H160, 9 | base_fee: Option, 10 | nonce: Option, 11 | gas_price: Option, 12 | ) -> Transaction { 13 | let nonce = nonce.unwrap_or(U256::from(0_u64)); 14 | let gas_price = gas_price.or_else(|| Some(U256::from(1_u64) + base_fee.unwrap_or_default())); 15 | 16 | Transaction { 17 | from, 18 | to: Some(H160::zero()), 19 | value: U256::from(1_u64), 20 | gas: U256::from(23000_u64), 21 | gas_price, 22 | nonce, 23 | ..Default::default() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/did/tests/resources/json/get_blocks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## exit if something fails 4 | set -e 5 | 6 | ## 7 | ## This script downloads from the Ethereum network the blocks in the hash_list 8 | ## and saves each of them in a json file named TRANSACTION_HASH.json 9 | ## 10 | 11 | declare -a hash_list=( 12 | "0xb2f703a57637e49572b16088b344db8fb108246f8360027ca8831766443a9c02" 13 | "0x81f6e266d34db0c21165d78e0c5e37dab36aee204d0a4422533200fcc8a37b93" 14 | "0x9ee9da5fafb45610f3c2ba78abe34bd46be01f4de29fc2704a81a76c8171038e" 15 | "0x207dc8087bbdbef42146c9c31f5df79266c1c61be209416abf7d5ed260a63a21" 16 | "0xecea9251184f99ea1b65927b665363dd22d5fcf08f350e4157063fd34175d111" 17 | ) 18 | 19 | for i in "${hash_list[@]}" 20 | do 21 | LINE_SEPARATOR='--------------------------------------------------------' 22 | 23 | echo $LINE_SEPARATOR 24 | echo 'Fetching block [' $i '] from Ethereum' 25 | 26 | JSON_DATA="{\"jsonrpc\":\"2.0\",\"method\":\"eth_getBlockByHash\",\"params\":[\"$i\",false],\"id\":1}" 27 | 28 | # [optional] validate the string is valid json 29 | echo $JSON_DATA | jq 30 | 31 | curl -X POST --data $JSON_DATA https://cloudflare-eth.com/ -v | jq . > ./block/$i.json 32 | 33 | echo 'Done' 34 | echo $LINE_SEPARATOR 35 | 36 | done 37 | -------------------------------------------------------------------------------- /src/did/tests/resources/json/get_transactions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## exit if something fails 4 | set -e 5 | 6 | ## 7 | ## This script downloads from the Ethereum network the transactions in the hash_list 8 | ## and saves each of them in a json file named TRANSACTION_HASH.json 9 | ## 10 | 11 | declare -a hash_list=( 12 | # Type 0x0 13 | "0xe1ffa2abdc95ebfa92d3178698c4feea7615d3669d16bf5929924881893837ce" 14 | "0x20081e3012905d97961c2f1a18e1f3fe39f72a46b24e078df2fe446051366dca" 15 | # Type 0x2 16 | "0xdcede6a4ac8829a7f18d3994e9a0e30d913e7d5b4cdb1106aafd9b0118d405a3" 17 | "0x1f336059dde3447fe37e3969a50857597515c753e8336b7e406792a4176bd60f" 18 | "0xd5f627e3ad2e6e0f4a131c52142e2a8344cd9077965c2404fa4ec555113b4ca6" 19 | "0x945ed16321825a4610de3ecc51b2920659f390c5ee96ac468f57ee56aab45ff9" 20 | ) 21 | 22 | for i in "${hash_list[@]}" 23 | do 24 | LINE_SEPARATOR='--------------------------------------------------------' 25 | 26 | echo $LINE_SEPARATOR 27 | echo 'Fetching transaction [' $i '] from Ethereum' 28 | 29 | JSON_DATA="{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionByHash\",\"params\":[\"$i\"],\"id\":1}" 30 | 31 | # [optional] validate the string is valid json 32 | echo $JSON_DATA | jq 33 | 34 | curl -X POST --data $JSON_DATA https://cloudflare-eth.com/ -v | jq . > ./transaction/$i.json 35 | 36 | echo 'Done' 37 | echo $LINE_SEPARATOR 38 | 39 | done 40 | 41 | -------------------------------------------------------------------------------- /src/did/tests/resources/json/transaction/0x1f336059dde3447fe37e3969a50857597515c753e8336b7e406792a4176bd60f.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonrpc": "2.0", 3 | "result": { 4 | "type": "0x2", 5 | "blockHash": "0x9ee9da5fafb45610f3c2ba78abe34bd46be01f4de29fc2704a81a76c8171038e", 6 | "blockNumber": "0xffee90", 7 | "from": "0xcb1096ca99ac4a9eb6517c35d4a3164158366192", 8 | "gas": "0x40d84", 9 | "hash": "0x1f336059dde3447fe37e3969a50857597515c753e8336b7e406792a4176bd60f", 10 | "input": "0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000640686b300000000000000000000000000000000000000000000000000000000000000020b080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000001f161421c8e00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000000000000000000000000000000223bd397e9daba00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000bec3312bec8866726f1b079ceb18546854ea1523", 11 | "nonce": "0xf4", 12 | "to": "0xef1c6e67703c7bd7107eed8303fbe6ec2554bf6b", 13 | "transactionIndex": "0x82", 14 | "value": "0x1f161421c8e0000", 15 | "v": "0x1", 16 | "r": "0x178bd604590ecef243fc309b60a054252e461b1441e40d634b4f6f15461fef94", 17 | "s": "0x5e5eea949bc7e2f4b02234d6579016fb7a47a511b591aafe118c272138426c31", 18 | "gasPrice": "0x8f2892137", 19 | "maxFeePerGas": "0xc4593ebc0", 20 | "maxPriorityFeePerGas": "0x2a9feba4", 21 | "chainId": "0x1", 22 | "accessList": [] 23 | }, 24 | "id": 1 25 | } 26 | -------------------------------------------------------------------------------- /src/did/tests/resources/json/transaction/0x20081e3012905d97961c2f1a18e1f3fe39f72a46b24e078df2fe446051366dca.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonrpc": "2.0", 3 | "result": { 4 | "blockHash": "0xbd4c1f27df055d4aa7e1540808f1e63a6126e178ecb5324062d8df2525137ad7", 5 | "blockNumber": "0x4aa1ab", 6 | "from": "0x2b5634c42055806a59e9107ed44d43c426e58258", 7 | "gas": "0xc528", 8 | "gasPrice": "0x184eb99400", 9 | "hash": "0x20081e3012905d97961c2f1a18e1f3fe39f72a46b24e078df2fe446051366dca", 10 | "input": "0xa9059cbb0000000000000000000000007bf925893f7713e00493a67ef0f0127855ad36be00000000000000000000000000000000000000000000000cf4ca91b9465c0000", 11 | "nonce": "0x27746", 12 | "to": "0x1063ce524265d5a3a624f4914acd573dd89ce988", 13 | "transactionIndex": "0x97", 14 | "value": "0x0", 15 | "type": "0x0", 16 | "chainId": "0x1", 17 | "v": "0x25", 18 | "r": "0x1d0d932a9bdde1aeba41b5ef431d82b99c325901402bf4033abed28518ed8fec", 19 | "s": "0x15e6df986efaeffaaa809a6cffa5998e9329afd5ba97f49a3aaa6b2f1c2cb4d3" 20 | }, 21 | "id": 1 22 | } 23 | -------------------------------------------------------------------------------- /src/did/tests/resources/json/transaction/0x945ed16321825a4610de3ecc51b2920659f390c5ee96ac468f57ee56aab45ff9.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonrpc": "2.0", 3 | "result": { 4 | "type": "0x2", 5 | "blockHash": "0x207dc8087bbdbef42146c9c31f5df79266c1c61be209416abf7d5ed260a63a21", 6 | "blockNumber": "0xff5eed", 7 | "from": "0xb0390f8e5fdcb801276c965ee740b936eec80e10", 8 | "gas": "0x3f66d", 9 | "hash": "0x945ed16321825a4610de3ecc51b2920659f390c5ee96ac468f57ee56aab45ff9", 10 | "input": "0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000063ffb4b300000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000001b1ae4d6e2ef500000000000000000000000000000000000000000000000000000000000009cfc779200000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000042b23d80f5fefcddaa212212f028021b41ded428cf002710c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000", 11 | "nonce": "0x5f7", 12 | "to": "0xef1c6e67703c7bd7107eed8303fbe6ec2554bf6b", 13 | "transactionIndex": "0x98", 14 | "value": "0x0", 15 | "v": "0x0", 16 | "r": "0xe737660248848a1db4f88c2620a3c939aac58f59f819fbee737456ec9d00be18", 17 | "s": "0xac36505101a19642359368e5f4d83a9a6cc3836e47df1dea13899ee58872d44", 18 | "gasPrice": "0x87a610d51", 19 | "maxFeePerGas": "0xbb1bf10b3", 20 | "maxPriorityFeePerGas": "0x1dcd6500", 21 | "chainId": "0x1", 22 | "accessList": [] 23 | }, 24 | "id": 1 25 | } 26 | -------------------------------------------------------------------------------- /src/did/tests/resources/json/transaction/0xd5f627e3ad2e6e0f4a131c52142e2a8344cd9077965c2404fa4ec555113b4ca6.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonrpc": "2.0", 3 | "result": { 4 | "type": "0x2", 5 | "blockHash": "0xb2f703a57637e49572b16088b344db8fb108246f8360027ca8831766443a9c02", 6 | "blockNumber": "0xfffa4f", 7 | "from": "0xbc07ed106b6c5bfae569aced9ed97fdc11e2981f", 8 | "gas": "0xb71b0", 9 | "hash": "0xd5f627e3ad2e6e0f4a131c52142e2a8344cd9077965c2404fa4ec555113b4ca6", 10 | "input": "0x16b01148000000000000000000000000000000000000000000000000000000000000f7fa000000000000000000000000a1213d41d5dc401e77ef7907f1f7211d1e5a7fa6", 11 | "nonce": "0x4b9", 12 | "to": "0x73eaecc8021527a3acafca7edaf69fba4f311ac8", 13 | "transactionIndex": "0x9d", 14 | "value": "0xd529ae9e860000", 15 | "v": "0x1", 16 | "r": "0x66747bb7d9597f10b297f1d724f397d13d3bb42c7838e570719c2f534843c01d", 17 | "s": "0x7cbec591bf0586f5ac2a4b6b00d3c1926fcc9055c1242d75a01dfd21b549105", 18 | "gasPrice": "0x4e1cc8380", 19 | "maxFeePerGas": "0x6be2e2a14", 20 | "maxPriorityFeePerGas": "0x1dcd6500", 21 | "chainId": "0x1", 22 | "accessList": [] 23 | }, 24 | "id": 1 25 | } 26 | -------------------------------------------------------------------------------- /src/did/tests/resources/json/transaction/0xdcede6a4ac8829a7f18d3994e9a0e30d913e7d5b4cdb1106aafd9b0118d405a3.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonrpc": "2.0", 3 | "result": { 4 | "type": "0x2", 5 | "blockHash": "0x9ee9da5fafb45610f3c2ba78abe34bd46be01f4de29fc2704a81a76c8171038e", 6 | "blockNumber": "0xffee90", 7 | "from": "0xdafea492d9c6733ae3d56b7ed1adb60692c98bc5", 8 | "gas": "0x565f", 9 | "hash": "0xdcede6a4ac8829a7f18d3994e9a0e30d913e7d5b4cdb1106aafd9b0118d405a3", 10 | "input": "0x", 11 | "nonce": "0x3ed68", 12 | "to": "0x388c818ca8b9251b393131c08a736a67ccb19297", 13 | "transactionIndex": "0x8f", 14 | "value": "0x9fb7e6b58043cf", 15 | "v": "0x1", 16 | "r": "0x31f6d4e934fafad08175da0a62cfa7eaa8f044a53c7a36a823c0a4ae95066083", 17 | "s": "0x278b928f7d5764612f1b41dd0dfe25a9c6fcd6c4304835db7b509ea2b936433a", 18 | "gasPrice": "0x8c7e93593", 19 | "maxFeePerGas": "0x8c7e93593", 20 | "maxPriorityFeePerGas": "0x0", 21 | "chainId": "0x1", 22 | "accessList": [] 23 | }, 24 | "id": 1 25 | } 26 | -------------------------------------------------------------------------------- /src/did/tests/resources/json/transaction/0xe1ffa2abdc95ebfa92d3178698c4feea7615d3669d16bf5929924881893837ce.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonrpc": "2.0", 3 | "result": { 4 | "blockHash": "0xecea9251184f99ea1b65927b665363dd22d5fcf08f350e4157063fd34175d111", 5 | "blockNumber": "0xc1a869", 6 | "from": "0xf8cd371ae43e1a6a9bafbb4fd48707607d24ae43", 7 | "gas": "0x1a2c9", 8 | "gasPrice": "0x3f5476a00", 9 | "hash": "0xe1ffa2abdc95ebfa92d3178698c4feea7615d3669d16bf5929924881893837ce", 10 | "input": "0x7c025200000000000000000000000000fd3dfb524b2da40c8a6d703c62be36b5d854062600000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000180000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000fd3dfb524b2da40c8a6d703c62be36b5d8540626000000000000000000000000f8cd371ae43e1a6a9bafbb4fd48707607d24ae43000000000000000000000000000000000000000000000000340aad21b3b700000000000000000000000000000000000000000000000000003385731490a3400000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000242e1a7d4d000000000000000000000000000000000000000000000000340aad21b3b700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f99000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000f8cd371ae43e1a6a9bafbb4fd48707607d24ae43000000000000000000000000000000000000000000000000340aad21b3b7000000000000000000000000000000000000000000000000000000000000", 11 | "nonce": "0x42", 12 | "to": "0x11111112542d85b3ef69ae05771c2dccff4faa26", 13 | "transactionIndex": "0xae", 14 | "value": "0x0", 15 | "type": "0x0", 16 | "chainId": "0x1", 17 | "v": "0x26", 18 | "r": "0x69d84d87306e33500f677a69b61021807257540893f1abad3446ea4832a84dc", 19 | "s": "0xb2d320705568f9988ae4fea0168eb07980d91725ce68d34530bb086cac3877f" 20 | }, 21 | "id": 1 22 | } 23 | -------------------------------------------------------------------------------- /src/did/tests/transaction_it.rs: -------------------------------------------------------------------------------- 1 | use did::fees::FeeCalculation; 2 | use did::{H160, Transaction, U64, U256}; 3 | use eth_signer::transaction::{SigningMethod, TransactionBuilder}; 4 | 5 | fn build_transaction( 6 | tx_type: Option, 7 | gas_price: Option, 8 | max_priority_fee_per_gas: Option, 9 | max_fee_per_gas: Option, 10 | ) -> Transaction { 11 | let mut tx = TransactionBuilder { 12 | from: &H160::from_slice(&[2u8; 20]), 13 | to: None, 14 | nonce: U256::zero(), 15 | value: U256::zero(), 16 | gas: 10_000u64.into(), 17 | gas_price: 10u64.into(), 18 | input: Vec::new(), 19 | signature: SigningMethod::None, 20 | chain_id: 31540, 21 | } 22 | .calculate_hash_and_build() 23 | .unwrap(); 24 | 25 | match tx_type { 26 | Some(1) => { 27 | tx.transaction_type = Some(U64::from(1u64)); 28 | tx.gas_price = gas_price; 29 | } 30 | Some(2) => { 31 | tx.transaction_type = Some(U64::from(2u64)); 32 | tx.max_priority_fee_per_gas = max_priority_fee_per_gas; 33 | tx.max_fee_per_gas = max_fee_per_gas; 34 | } 35 | Some(_) => panic!("Invalid transaction type"), 36 | None => tx.gas_price = gas_price, 37 | } 38 | 39 | tx 40 | } 41 | 42 | #[test] 43 | fn test_gas_cost_for_different_transaction_types() { 44 | let txns = vec![ 45 | ( 46 | build_transaction(Some(1), Some(20_000u64.into()), None, None), 47 | 20_000u64.into(), 48 | ), 49 | ( 50 | build_transaction( 51 | Some(2), 52 | None, 53 | Some(20_000u64.into()), 54 | Some(30_000u64.into()), 55 | ), 56 | 30_000u64.into(), 57 | ), 58 | ( 59 | build_transaction(None, Some(20_000u64.into()), None, None), 60 | 20_000u64.into(), 61 | ), 62 | ( 63 | build_transaction(None, Some(20_000u64.into()), None, None), 64 | 20_000u64.into(), 65 | ), 66 | ]; 67 | 68 | for (tx, expected_gas_cost) in txns { 69 | assert_eq!(tx.gas_cost(), expected_gas_cost); 70 | } 71 | } 72 | 73 | #[test] 74 | fn test_max_priority_fee_or_gas_price_for_different_transaction_types() { 75 | let txns = vec![ 76 | ( 77 | build_transaction(Some(1), Some(20_000u64.into()), None, None), 78 | 20_000u64.into(), 79 | ), 80 | ( 81 | build_transaction( 82 | Some(2), 83 | None, 84 | Some(20_000u64.into()), 85 | Some(30_000u64.into()), 86 | ), 87 | 20_000u64.into(), 88 | ), 89 | ( 90 | build_transaction(None, Some(20_000u64.into()), None, None), 91 | 20_000u64.into(), 92 | ), 93 | ( 94 | build_transaction(None, Some(20_000u64.into()), None, None), 95 | 20_000u64.into(), 96 | ), 97 | ]; 98 | 99 | for (tx, expected_max_priority_fee_or_gas_price) in txns { 100 | assert_eq!( 101 | tx.max_priority_fee_or_gas_price(), 102 | expected_max_priority_fee_or_gas_price 103 | ); 104 | } 105 | } 106 | 107 | #[test] 108 | fn test_effective_gas_tip_for_different_transaction_types() { 109 | let base_per_gas: U256 = 20_000u64.into(); 110 | 111 | let tx = build_transaction(Some(1), Some(30_000u64.into()), None, None); 112 | 113 | assert_eq!( 114 | tx.effective_gas_tip(Some(base_per_gas.clone())).unwrap(), 115 | 10_000u64.into() 116 | ); 117 | 118 | let tx = build_transaction( 119 | Some(2), 120 | None, 121 | Some(30_000u64.into()), 122 | Some(40_000u64.into()), 123 | ); 124 | 125 | assert_eq!( 126 | tx.effective_gas_tip(Some(base_per_gas)).unwrap(), 127 | 20_000u64.into() 128 | ); 129 | 130 | let tx = build_transaction(None, Some(30_000u64.into()), None, None); 131 | 132 | assert_eq!( 133 | tx.effective_gas_tip(None).unwrap(), 134 | tx.max_priority_fee_or_gas_price() 135 | ) 136 | } 137 | -------------------------------------------------------------------------------- /src/eth-signer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | categories = ["cryptography::cryptocurrencies"] 3 | description = "API types definition for EVM canister" 4 | include = ["src/**/*", "../../LICENSE", "../../README.md"] 5 | name = "eth-signer" 6 | 7 | authors.workspace = true 8 | homepage.workspace = true 9 | version.workspace = true 10 | rust-version.workspace = true 11 | edition.workspace = true 12 | license.workspace = true 13 | repository.workspace = true 14 | 15 | [features] 16 | ic_sign = ["ic-canister", "ic-exports"] 17 | 18 | [dependencies] 19 | alloy = { workspace = true, features = ["network", "signer-local"]} 20 | candid = { workspace = true } 21 | did = { workspace = true } 22 | ic-stable-structures = { workspace = true } 23 | serde = { workspace = true } 24 | serde_json = { workspace = true } 25 | sha2 = { workspace = true } 26 | thiserror = { workspace = true } 27 | 28 | # Dependencies for ic-siginig 29 | ic-exports = { workspace = true, optional = true } 30 | ic-canister = { workspace = true, optional = true } 31 | 32 | [dev-dependencies] 33 | ic-exports = { workspace = true, features = ["pocket-ic-tests"]} 34 | rand = { workspace = true } 35 | tokio = { workspace = true } 36 | -------------------------------------------------------------------------------- /src/eth-signer/README.md: -------------------------------------------------------------------------------- 1 | # Eth Signer 2 | -------------------------------------------------------------------------------- /src/eth-signer/src/lib.rs: -------------------------------------------------------------------------------- 1 | use alloy::signers::local::PrivateKeySigner; 2 | 3 | #[cfg(feature = "ic_sign")] 4 | pub mod ic_sign; 5 | pub mod sign_strategy; 6 | pub mod transaction; 7 | 8 | /// A wallet instantiated with a locally stored private key 9 | pub type LocalWallet = PrivateKeySigner; 10 | pub type SignerError = alloy::signers::Error; 11 | -------------------------------------------------------------------------------- /src/eth-signer/tests/it.rs: -------------------------------------------------------------------------------- 1 | mod pocket_ic_tests; 2 | -------------------------------------------------------------------------------- /src/eth-signer/tests/pocket_ic_tests/ic_sign_test_canister.rs: -------------------------------------------------------------------------------- 1 | use candid::{Encode, Principal}; 2 | use ic_exports::pocket_ic::{self, PocketIc}; 3 | 4 | use crate::pocket_ic_tests::wasm_utils::get_test_canister_bytecode; 5 | 6 | #[tokio::test] 7 | async fn test_canister_sign_and_check() { 8 | let env = pocket_ic::init_pocket_ic().await.build_async().await; 9 | let canister = deploy_canister(&env).await; 10 | 11 | let result = env 12 | .update_call( 13 | canister, 14 | Principal::anonymous(), 15 | "sign_and_check", 16 | Encode!(&()).unwrap(), 17 | ) 18 | .await; 19 | 20 | assert!(result.is_ok()); 21 | } 22 | 23 | async fn deploy_canister(env: &PocketIc) -> Principal { 24 | let dummy_wasm = get_test_canister_bytecode(); 25 | let args = Encode!(&()).unwrap(); 26 | let canister = env.create_canister().await; 27 | env.add_cycles(canister, 10_u128.pow(12)).await; 28 | env.install_canister(canister, dummy_wasm.to_vec(), args, None) 29 | .await; 30 | canister 31 | } 32 | -------------------------------------------------------------------------------- /src/eth-signer/tests/pocket_ic_tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod ic_sign_test_canister; 2 | mod wasm_utils; 3 | -------------------------------------------------------------------------------- /src/eth-signer/tests/pocket_ic_tests/wasm_utils.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::Read; 3 | use std::path::{Path, PathBuf}; 4 | use std::sync::OnceLock; 5 | 6 | /// Returns the bytecode of the canister 7 | pub fn get_test_canister_bytecode() -> Vec { 8 | static CANISTER_BYTECODE: OnceLock> = OnceLock::new(); 9 | CANISTER_BYTECODE 10 | .get_or_init(|| load_wasm_bytecode_or_panic("ic-sign-test-canister.wasm.gz")) 11 | .to_owned() 12 | } 13 | 14 | fn load_wasm_bytecode_or_panic(wasm_name: &str) -> Vec { 15 | let path = get_path_to_wasm(wasm_name); 16 | 17 | let mut f = File::open(path).expect("File does not exists"); 18 | 19 | let mut buffer = Vec::new(); 20 | f.read_to_end(&mut buffer) 21 | .expect("Could not read file content"); 22 | 23 | buffer 24 | } 25 | 26 | fn get_path_to_wasm(wasm_name: &str) -> PathBuf { 27 | const ARTIFACT_PATH: &str = "../../target/artifact/"; 28 | // Get to the root of the project 29 | let wasm_path = format!("{}{}", ARTIFACT_PATH, wasm_name); 30 | println!("path: {wasm_path:?}"); 31 | if Path::new(&wasm_path).exists() { 32 | wasm_path.into() 33 | } else { 34 | panic!("File {wasm_name} was not found in {ARTIFACT_PATH}"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/eth-signer/tests/test_canister/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ic-sign-test-canister" 3 | 4 | authors.workspace = true 5 | homepage.workspace = true 6 | version.workspace = true 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | 11 | [features] 12 | default = [] 13 | export-api = [] 14 | 15 | 16 | [dependencies] 17 | alloy = { workspace = true } 18 | candid = { workspace = true } 19 | did = { workspace = true } 20 | eth-signer = { workspace = true, features = ["ic_sign"] } 21 | ic-exports = { workspace = true } 22 | ic-canister = { workspace = true } 23 | -------------------------------------------------------------------------------- /src/eth-signer/tests/test_canister/README.md: -------------------------------------------------------------------------------- 1 | # Canister to test management canister ECDSA signing. 2 | 3 | ## Test 4 | The `sign_and_check` test creates a transaction, signs it using management canister and recovers `from` address from signature. 5 | 6 | ## Running 7 | To perform the test, use `/scripts/dfx_test.sh` script. 8 | 9 | **NOTE: `dfx` should be started** 10 | 11 | -------------------------------------------------------------------------------- /src/eth-signer/tests/test_canister/src/canister.rs: -------------------------------------------------------------------------------- 1 | use alloy::consensus::SignableTransaction; 2 | use alloy::network::TransactionBuilder; 3 | use alloy::primitives::{Address, U256}; 4 | use alloy::rpc::types::TransactionRequest; 5 | use candid::Principal; 6 | use eth_signer::ic_sign::{DerivationPath, IcSigner, SigningKeyId}; 7 | use ic_canister::{Canister, Idl, PreUpdate, generate_idl, update}; 8 | 9 | #[derive(Canister)] 10 | pub struct TestCanister { 11 | #[id] 12 | id: Principal, 13 | } 14 | 15 | impl PreUpdate for TestCanister {} 16 | 17 | impl TestCanister { 18 | /// Signs and recovers two different transactions and two different digests. 19 | #[update] 20 | pub async fn sign_and_check(&self) { 21 | let pubkey = IcSigner 22 | .public_key(SigningKeyId::PocketIc, DerivationPath::default()) 23 | .await 24 | .unwrap(); 25 | let from = IcSigner.pubkey_to_address(&pubkey).unwrap(); 26 | 27 | let mut tx = TransactionRequest::default() 28 | .with_from(from) 29 | .with_to(Address::ZERO) 30 | .with_value(U256::from(10u64)) 31 | .with_chain_id(355113) 32 | .with_nonce(0) 33 | .with_gas_price(10) 34 | .with_gas_limit(53000) 35 | .build_typed_tx() 36 | .unwrap() 37 | .legacy() 38 | .cloned() 39 | .unwrap(); 40 | 41 | let signature = IcSigner 42 | .sign_transaction( 43 | &mut tx, 44 | &pubkey, 45 | SigningKeyId::PocketIc, 46 | DerivationPath::default(), 47 | ) 48 | .await 49 | .unwrap(); 50 | 51 | let tx = tx.into_signed(signature.into()); 52 | let recovered_from = tx.recover_signer().unwrap(); 53 | assert_eq!(recovered_from, from); 54 | 55 | let mut tx = TransactionRequest::default() 56 | .with_from(from) 57 | .with_to(Address::ZERO) 58 | .with_value(U256::from(10)) 59 | .with_chain_id(355113) 60 | .with_nonce(1) 61 | .with_gas_price(10) 62 | .with_gas_limit(53000) 63 | .build_typed_tx() 64 | .unwrap() 65 | .legacy() 66 | .cloned() 67 | .unwrap(); 68 | 69 | let signature = IcSigner 70 | .sign_transaction( 71 | &mut tx, 72 | &pubkey, 73 | SigningKeyId::PocketIc, 74 | DerivationPath::default(), 75 | ) 76 | .await 77 | .unwrap(); 78 | 79 | let recovered_from = signature.recover_from(&tx.signature_hash().into()).unwrap(); 80 | assert_eq!(recovered_from.0, from); 81 | 82 | let digest = [42u8; 32]; 83 | let signature = IcSigner 84 | .sign_digest( 85 | digest, 86 | &pubkey, 87 | SigningKeyId::PocketIc, 88 | DerivationPath::default(), 89 | ) 90 | .await 91 | .unwrap(); 92 | 93 | let recovered_from = signature.recover_from(&digest.into()).unwrap(); 94 | assert_eq!(recovered_from.0, from); 95 | 96 | let digest = [43u8; 32]; 97 | let signature = IcSigner 98 | .sign_digest( 99 | digest, 100 | &pubkey, 101 | SigningKeyId::PocketIc, 102 | DerivationPath::default(), 103 | ) 104 | .await 105 | .unwrap(); 106 | 107 | let recovered_from = signature.recover_from(&digest.into()).unwrap(); 108 | assert_eq!(recovered_from.0, from); 109 | } 110 | 111 | /// Important: This function must be added to the canister to provide the idl. 112 | pub fn idl() -> Idl { 113 | generate_idl!() 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/eth-signer/tests/test_canister/src/main.rs: -------------------------------------------------------------------------------- 1 | use crate::canister::TestCanister; 2 | 3 | pub mod canister; 4 | 5 | fn main() { 6 | let canister_e_idl = TestCanister::idl(); 7 | let idl = candid::pretty::candid::compile(&canister_e_idl.env.env, &Some(canister_e_idl.actor)); 8 | 9 | println!("{}", idl); 10 | } 11 | -------------------------------------------------------------------------------- /src/ethereum-json-rpc-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ethereum-json-rpc-client" 3 | 4 | authors.workspace = true 5 | homepage.workspace = true 6 | version.workspace = true 7 | edition.workspace = true 8 | rust-version.workspace = true 9 | license.workspace = true 10 | repository.workspace = true 11 | 12 | [features] 13 | ic-canister-client = ["dep:ic-canister-client"] 14 | pocket-ic-tests-client = [ 15 | "ic-canister-client", 16 | "ic-canister-client/pocket-ic-client", 17 | ] 18 | reqwest = ["dep:reqwest"] 19 | http-outcall = ["dep:url"] 20 | # Adds an API method `sanitize_http_response` to the canister and `HttpOutcallClient::new_sanitized` method to use it. 21 | # We feature-gate it because it changes the API of the canister which is not always necessary. 22 | sanitize-http-outcall = [] 23 | 24 | [dependencies] 25 | alloy = { workspace = true } 26 | candid = { workspace = true } 27 | did = { workspace = true } 28 | ic-canister-client = { workspace = true, optional = true } 29 | ic-exports = { workspace = true } 30 | itertools = { workspace = true } 31 | log = { workspace = true } 32 | reqwest = { workspace = true, optional = true, features = [ 33 | "gzip", 34 | "json", 35 | "rustls-tls", 36 | "trust-dns", 37 | ] } 38 | serde = { workspace = true } 39 | serde_bytes = { workspace = true } 40 | serde_json = { workspace = true } 41 | thiserror = { workspace = true } 42 | url = { workspace = true, optional = true } 43 | 44 | [dev-dependencies] 45 | alloy = { workspace = true, features = ["dyn-abi", "json-abi"] } 46 | env_logger = { workspace = true } 47 | rand = { workspace = true } 48 | serial_test = { workspace = true } 49 | tokio = { workspace = true } 50 | -------------------------------------------------------------------------------- /src/ethereum-json-rpc-client/src/canister_client.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::future::Future; 3 | use std::pin::Pin; 4 | 5 | use candid::{CandidType, Deserialize}; 6 | use did::constant::UPGRADE_HTTP_METHODS; 7 | use did::rpc::request::RpcRequest; 8 | use did::rpc::response::RpcResponse; 9 | use ic_canister_client::CanisterClient; 10 | use serde::Serialize; 11 | use serde_bytes::ByteBuf; 12 | 13 | use crate::{Client, JsonRpcError, JsonRpcResult}; 14 | 15 | impl Client for T { 16 | fn send_rpc_request( 17 | &self, 18 | request: RpcRequest, 19 | ) -> Pin> + Send>> { 20 | let client = self.clone(); 21 | 22 | Box::pin(async move { 23 | log::trace!("CanisterClient - sending 'http_request'. request: {request:?}"); 24 | 25 | let is_update_call = match &request { 26 | RpcRequest::Single(request) => is_update_call(&request.method), 27 | RpcRequest::Batch(calls) => calls.iter().any(|call| is_update_call(&call.method)), 28 | }; 29 | 30 | let args = HttpRequest::new(&request)?; 31 | 32 | let http_response: HttpResponse = if is_update_call { 33 | client.update("http_request_update", (args,)).await 34 | } else { 35 | client.query("http_request", (args,)).await 36 | } 37 | .map_err(|e| { 38 | log::warn!("failed to send RPC request: {e}"); 39 | JsonRpcError::CanisterClient(e) 40 | })?; 41 | 42 | let response = serde_json::from_slice(&http_response.body)?; 43 | 44 | log::trace!("response: {:?}", response); 45 | 46 | Ok(response) 47 | }) 48 | } 49 | } 50 | 51 | /// The important components of an HTTP request. 52 | #[derive(Clone, Debug, CandidType)] 53 | struct HttpRequest { 54 | /// The HTTP method string. 55 | pub method: &'static str, 56 | /// The URL method string. 57 | pub url: &'static str, 58 | /// The request headers. 59 | pub headers: HashMap<&'static str, &'static str>, 60 | /// The request body. 61 | pub body: ByteBuf, 62 | } 63 | 64 | impl HttpRequest { 65 | pub fn new(data: &T) -> JsonRpcResult { 66 | let mut headers = HashMap::new(); 67 | headers.insert("content-type", "application/json"); 68 | Ok(Self { 69 | method: "POST", 70 | headers, 71 | url: "", 72 | body: ByteBuf::from(serde_json::to_vec(data)?), 73 | }) 74 | } 75 | } 76 | 77 | #[derive(Clone, Debug, CandidType, Deserialize)] 78 | pub struct HttpResponse { 79 | /// The HTTP status code. 80 | pub status_code: u16, 81 | /// The response header map. 82 | pub headers: HashMap, 83 | /// The response body. 84 | pub body: ByteBuf, 85 | } 86 | 87 | #[inline] 88 | fn is_update_call(method: &str) -> bool { 89 | UPGRADE_HTTP_METHODS.contains(&method) 90 | } 91 | 92 | #[cfg(test)] 93 | mod test { 94 | 95 | use super::*; 96 | use crate::{ETH_CHAIN_ID_METHOD, ETH_SEND_RAW_TRANSACTION_METHOD, IC_SEND_CONFIRM_BLOCK}; 97 | 98 | #[test] 99 | fn test_is_update_call() { 100 | assert!(is_update_call(ETH_SEND_RAW_TRANSACTION_METHOD)); 101 | assert!(is_update_call(IC_SEND_CONFIRM_BLOCK)); 102 | assert!(!is_update_call(ETH_CHAIN_ID_METHOD)); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/ethereum-json-rpc-client/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types for the Ethereum JSON-RPC client. 2 | 3 | use did::H256; 4 | use did::rpc::response::Failure; 5 | use ic_exports::ic_kit::RejectionCode; 6 | use thiserror::Error; 7 | 8 | /// Result type for the Ethereum JSON-RPC client. 9 | pub type JsonRpcResult = std::result::Result; 10 | 11 | /// Error type for the Ethereum JSON-RPC client. 12 | #[derive(Error, Debug)] 13 | pub enum JsonRpcError { 14 | /// Canister client error [`ic_canister_client::CanisterClientError`] 15 | #[cfg(feature = "ic-canister-client")] 16 | #[error("Canister client error: {0}")] 17 | CanisterClient(#[from] ic_canister_client::CanisterClientError), 18 | #[error("Canister call failed with code: {rejection_code:?}: {message}")] 19 | CanisterCall { 20 | /// Canister [`RejectionCode`]. 21 | rejection_code: RejectionCode, 22 | /// Canister rejection message. 23 | message: String, 24 | }, 25 | /// Error while parsing the JSON response. 26 | #[error("Invalid JSON response: {0}")] 27 | Json(#[from] serde_json::Error), 28 | /// EVM failed to process the request. See [`Failure`] for details, 29 | /// and [`did::rpc::error::Error`] in particular to get the message and the code. 30 | #[error("EVM error: {0}")] 31 | Evm(Failure), 32 | /// HTTP error. 33 | #[cfg(feature = "reqwest")] 34 | #[error("HTTP error {code}: {text}")] 35 | Http { 36 | /// HTTP status code. 37 | code: reqwest::StatusCode, 38 | /// HTTP response text. 39 | text: String, 40 | }, 41 | /// There were not enough cycles to send the request. 42 | #[error("Insufficient cycles: available {available}, required {cost}")] 43 | InsufficientCycles { 44 | /// The amount of cycles that are available. 45 | available: u128, 46 | /// The amount of cycles that are required. 47 | cost: u128, 48 | }, 49 | /// Reqwest error. 50 | #[cfg(feature = "reqwest")] 51 | #[error("Reqwest error: {0}")] 52 | Reqwest(#[from] reqwest::Error), 53 | /// Fetched transaction information for a transaction that hasn't been found. 54 | #[error("transaction {0} not found")] 55 | TransactionNotFound(H256), 56 | /// A single request was sent, but a batch response was received. 57 | #[error("unexpected batch response: expected single but got batch")] 58 | UnexpectedBatch, 59 | /// A batch request was sent, but the number of responses is not equal to the number of requests. 60 | #[error("unexpected response: expected {expected} but got {actual}")] 61 | UnexpectedResultsAmount { expected: usize, actual: usize }, 62 | /// The URL provided is invalid, because it is missing the host name. 63 | #[cfg(feature = "http-outcall")] 64 | #[error("provided host is missing the host name: {0}")] 65 | UrlMissingHost(url::Url), 66 | /// Error while parsing the URL. 67 | #[cfg(feature = "http-outcall")] 68 | #[error("Invalid URL: {0}")] 69 | UrlParser(#[from] url::ParseError), 70 | } 71 | 72 | impl From for JsonRpcError { 73 | fn from(err: Failure) -> Self { 74 | JsonRpcError::Evm(err) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/ethereum-json-rpc-client/src/http_outcall.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | 4 | use did::rpc::request::RpcRequest; 5 | use did::rpc::response::RpcResponse; 6 | use ic_cdk::api::management_canister::http_request::{ 7 | self, CanisterHttpRequestArgument, HttpHeader, HttpMethod, TransformContext, 8 | }; 9 | #[cfg(feature = "sanitize-http-outcall")] 10 | use ic_cdk::api::management_canister::http_request::{HttpResponse, TransformArgs}; 11 | use ic_exports::ic_cdk; 12 | 13 | use crate::{Client, JsonRpcError, JsonRpcResult}; 14 | 15 | /// EVM client that uses HTTPS Outcalls to communicate with EVM. 16 | /// 17 | /// This client can be used to connect to external EVM RPC services, but due to IC HTTPS Outcall 18 | /// protocol interface, there are a few limitations that these services must comply with: 19 | /// * Identical requests must produce identical response bodies. 20 | /// * If some of the response headers may vary for different requests, response transformation must 21 | /// be used (see [HttpOutcallClient::new_with_transform]). 22 | /// * The service must not use redirects or any other form of indirection. All valid RPC requests 23 | /// must be answered with `200 OK` status code and a valid RPC response. 24 | /// * The service must support batched RPC requests. 25 | /// 26 | /// Note that when a canister is run in replicated mode (e.g. on IC mainneet), every call to 27 | /// [`HttpClient::send_rpc_request`] will result in multiple concurrent identical HTTP POST requests 28 | /// to the target EVM. 29 | /// 30 | /// For more information about how IC HTTPS Outcalls work see [IC documentation](https://internetcomputer.org/docs/current/tutorials/developer-journey/level-3/3.2-https-outcalls/) 31 | #[derive(Debug, Clone)] 32 | pub struct HttpOutcallClient { 33 | url: String, 34 | max_response_bytes: Option, 35 | transform_context: Option, 36 | } 37 | 38 | impl HttpOutcallClient { 39 | /// Creates a new client. 40 | /// 41 | /// # Arguments 42 | /// * `url` - the url of the RPC service to connect to. 43 | pub fn new(url: String) -> Self { 44 | Self { 45 | url, 46 | max_response_bytes: None, 47 | transform_context: None, 48 | } 49 | } 50 | 51 | /// Sets transform context for the client. 52 | /// 53 | /// Transform context is used to sanitize HTTP responses before checking for consensus. 54 | /// 55 | /// You can use [`sanitized`] method to set up default transform context. (Available with 56 | /// Cargo feature `sanitize-http-outcall`) 57 | /// 58 | /// # Arguments 59 | /// * `transform_context` - method to use to sanitize HTTP response 60 | pub fn with_transform(mut self, transform_context: TransformContext) -> Self { 61 | self.transform_context = Some(transform_context); 62 | self 63 | } 64 | 65 | /// Sets default transform context for the client. 66 | /// 67 | /// The default sanitize drops most of HTTP headers that may prevent consensus on the response. 68 | /// 69 | /// Only available with Cargo feature `sanitize-http-outcall`. 70 | #[cfg(feature = "sanitize-http-outcall")] 71 | pub fn sanitized(mut self) -> Self { 72 | self.transform_context = Some(TransformContext::from_name( 73 | "sanitize_http_response".into(), 74 | vec![], 75 | )); 76 | self 77 | } 78 | 79 | /// The maximal size of the response in bytes. If None, 2MiB will be the 80 | /// limit. 81 | /// This value affects the cost of the http request and it is highly 82 | /// recommended to set it as low as possible to avoid unnecessary extra 83 | /// costs. 84 | /// 85 | /// # Arguments 86 | /// * `max_response_bytes` - The max response bytes. 87 | pub fn set_max_response_bytes(&mut self, max_response_bytes: Option) { 88 | self.max_response_bytes = max_response_bytes; 89 | } 90 | } 91 | 92 | #[cfg(feature = "sanitize-http-outcall")] 93 | #[ic_cdk::query] 94 | fn sanitize_http_response(raw_response: TransformArgs) -> HttpResponse { 95 | const USE_HEADERS: &[&str] = &["content-encoding", "content-length", "content-type", "host"]; 96 | let TransformArgs { mut response, .. } = raw_response; 97 | response 98 | .headers 99 | .retain(|header| USE_HEADERS.iter().any(|v| v == &header.name.to_lowercase())); 100 | 101 | response 102 | } 103 | 104 | impl Client for HttpOutcallClient { 105 | fn send_rpc_request( 106 | &self, 107 | request: RpcRequest, 108 | ) -> Pin> + Send>> { 109 | let url = self.url.clone(); 110 | let max_response_bytes = self.max_response_bytes; 111 | let body = serde_json::to_vec(&request).expect("failed to serialize body"); 112 | 113 | let transform = self.transform_context.clone(); 114 | Box::pin(async move { 115 | log::trace!("CanisterClient - sending 'http_outcall'. url: {url}"); 116 | 117 | let parsed_url = url::Url::parse(&url)?; 118 | 119 | let host = parsed_url 120 | .host_str() 121 | .ok_or_else(|| JsonRpcError::UrlMissingHost(parsed_url.clone()))?; 122 | 123 | let headers = vec![ 124 | HttpHeader { 125 | name: "Host".to_string(), 126 | value: host.to_string(), 127 | }, 128 | HttpHeader { 129 | name: "Content-Type".to_string(), 130 | value: "application/json".to_string(), 131 | }, 132 | ]; 133 | log::trace!("Making http request to {url} with headers: {headers:?}"); 134 | log::trace!("Request body is: {}", String::from_utf8_lossy(&body)); 135 | 136 | let request = CanisterHttpRequestArgument { 137 | url, 138 | max_response_bytes, 139 | method: HttpMethod::POST, 140 | headers, 141 | body: Some(body), 142 | transform, 143 | }; 144 | 145 | let cost = http_request_required_cycles(&request); 146 | 147 | let cycles_available = ic_exports::ic_cdk::api::canister_balance128(); 148 | if cycles_available < cost { 149 | return Err(JsonRpcError::InsufficientCycles { 150 | available: cycles_available, 151 | cost, 152 | }); 153 | } 154 | 155 | let http_response = http_request::http_request(request, cost) 156 | .await 157 | .map(|(res,)| res) 158 | .map_err(|(r, m)| JsonRpcError::CanisterCall { 159 | rejection_code: r, 160 | message: m, 161 | })?; 162 | 163 | log::trace!( 164 | "CanisterClient - Response from http_outcall'. Response: {} {:?}. Body: {}", 165 | http_response.status, 166 | http_response.headers, 167 | String::from_utf8_lossy(&http_response.body) 168 | ); 169 | 170 | let response = serde_json::from_slice(&http_response.body)?; 171 | 172 | log::trace!("CanisterClient - Deserialized response: {response:?}"); 173 | 174 | Ok(response) 175 | }) 176 | } 177 | } 178 | 179 | // Calculate cycles for http_request 180 | // NOTE: 181 | // https://github.com/dfinity/cdk-rs/blob/710a6cdcc3eb03d2392df1dfd5f047dff9deee80/examples/management_canister/src/caller/lib.rs#L7-L19 182 | pub fn http_request_required_cycles(arg: &CanisterHttpRequestArgument) -> u128 { 183 | let max_response_bytes = match arg.max_response_bytes { 184 | Some(ref n) => *n as u128, 185 | None => 2 * 1024 * 1024u128, // default 2MiB 186 | }; 187 | let arg_raw = candid::utils::encode_args((arg,)).expect("Failed to encode arguments."); 188 | // The fee is for a 13-node subnet to demonstrate a typical usage. 189 | (3_000_000u128 190 | + 60_000u128 * 13 191 | + (arg_raw.len() as u128 + "http_request".len() as u128) * 400 192 | + max_response_bytes * 800) 193 | * 13 194 | } 195 | 196 | #[cfg(test)] 197 | #[cfg(feature = "sanitize-http-outcall")] 198 | mod tests { 199 | use candid::Nat; 200 | 201 | use super::*; 202 | 203 | #[test] 204 | fn sanitize_http_response_removes_extra_headers() { 205 | let transform_args = TransformArgs { 206 | response: HttpResponse { 207 | status: 200u128.into(), 208 | headers: vec![ 209 | HttpHeader { 210 | name: "content-type".to_string(), 211 | value: "application/json".to_string(), 212 | }, 213 | HttpHeader { 214 | name: "content-length".to_string(), 215 | value: "42".to_string(), 216 | }, 217 | HttpHeader { 218 | name: "content-encoding".to_string(), 219 | value: "gzip".to_string(), 220 | }, 221 | HttpHeader { 222 | name: "date".to_string(), 223 | value: "Fri, 11 Oct 2024 10:25:08 GMT".to_string(), 224 | }, 225 | ], 226 | body: vec![], 227 | }, 228 | context: vec![], 229 | }; 230 | 231 | let sanitized: HttpResponse = sanitize_http_response(transform_args); 232 | assert_eq!(sanitized.headers.len(), 3); 233 | assert_eq!(sanitized.status, Nat::from(200u128)); 234 | assert!( 235 | sanitized 236 | .headers 237 | .iter() 238 | .any(|header| header.name == "content-type") 239 | ); 240 | assert!(!sanitized.headers.iter().any(|header| header.name == "date")); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/ethereum-json-rpc-client/src/reqwest.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | 4 | use did::rpc::request::RpcRequest; 5 | use did::rpc::response::RpcResponse; 6 | pub use reqwest; 7 | 8 | use crate::{Client, JsonRpcError, JsonRpcResult}; 9 | 10 | /// Reqwest client implementation. 11 | #[derive(Clone)] 12 | pub struct ReqwestClient { 13 | client: reqwest::Client, 14 | endpoint_url: String, 15 | } 16 | 17 | impl ReqwestClient { 18 | /// Creates a new client. 19 | pub fn new(endpoint_url: String) -> Self { 20 | Self::new_with_client(endpoint_url, Default::default()) 21 | } 22 | 23 | /// Creates a new client with a custom reqwest client. 24 | pub fn new_with_client(endpoint_url: String, client: reqwest::Client) -> Self { 25 | Self { 26 | endpoint_url, 27 | client, 28 | } 29 | } 30 | } 31 | 32 | impl Client for ReqwestClient { 33 | fn send_rpc_request( 34 | &self, 35 | request: RpcRequest, 36 | ) -> Pin> + Send>> { 37 | log::trace!("ReqwestClient - sending request {request:?}"); 38 | 39 | let request_builder = self.client.post(&self.endpoint_url).json(&request); 40 | 41 | Box::pin(async move { 42 | let response = request_builder.send().await?; 43 | 44 | if !response.status().is_success() { 45 | let status = response.status(); 46 | let text = response.text().await.unwrap_or_default(); 47 | return Err(JsonRpcError::Http { code: status, text }); 48 | } 49 | 50 | let json_response = response.json::().await?; 51 | 52 | log::trace!("response: {:?}", json_response); 53 | Ok(json_response) 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/ethereum-json-rpc-client/tests/integration_tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "reqwest")] 2 | mod reqwest; 3 | -------------------------------------------------------------------------------- /src/ethereum-json-rpc-client/tests/reqwest/rpc_client.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use did::rpc::request::RpcRequest; 4 | use did::rpc::response::{Response, RpcResponse}; 5 | use ethereum_json_rpc_client::reqwest::ReqwestClient; 6 | use ethereum_json_rpc_client::{Client, JsonRpcError, JsonRpcResult}; 7 | use rand::SeedableRng as _; 8 | 9 | /// Public ethereum endpoints which can be used to send RPC requests. 10 | const PUBLIC_ETHEREUM_JSON_API_ENDPOINTS: &[&str] = &[ 11 | "https://cloudflare-eth.com/", 12 | "https://ethereum.publicnode.com", 13 | "https://rpc.ankr.com/eth", 14 | "https://nodes.mewapi.io/rpc/eth", 15 | "https://eth-mainnet.gateway.pokt.network/v1/5f3453978e354ab992c4da79", 16 | "https://eth-mainnet.nodereal.io/v1/1659dfb40aa24bbb8153a677b98064d7", 17 | "https://eth.llamarpc.com", 18 | "https://eth-mainnet.public.blastapi.io", 19 | ]; 20 | 21 | #[derive(Clone)] 22 | pub enum RpcReqwestClient { 23 | Public(PublicRpcReqwestClient), 24 | Alchemy(AlchemyRpcReqwestClient), 25 | } 26 | 27 | impl RpcReqwestClient { 28 | pub fn alchemy(apikey: String) -> Self { 29 | RpcReqwestClient::Alchemy(AlchemyRpcReqwestClient { apikey }) 30 | } 31 | 32 | pub fn public() -> Self { 33 | RpcReqwestClient::Public(PublicRpcReqwestClient) 34 | } 35 | } 36 | 37 | impl Client for RpcReqwestClient { 38 | fn send_rpc_request( 39 | &self, 40 | request: RpcRequest, 41 | ) -> std::pin::Pin> + Send>> 42 | { 43 | match self { 44 | RpcReqwestClient::Public(client) => client.send_rpc_request(request), 45 | RpcReqwestClient::Alchemy(client) => client.send_rpc_request(request), 46 | } 47 | } 48 | } 49 | 50 | /// This client randomly shuffle RPC providers and tries to send the request to each one of them 51 | /// until it gets a successful response. 52 | /// This was necessary because some RPC providers have rate limits and running the CI was more like a nightmare. 53 | #[derive(Clone)] 54 | pub struct PublicRpcReqwestClient; 55 | 56 | impl Client for PublicRpcReqwestClient { 57 | fn send_rpc_request( 58 | &self, 59 | request: RpcRequest, 60 | ) -> std::pin::Pin> + Send>> 61 | { 62 | Box::pin(async move { 63 | let mut rng = rand::rngs::StdRng::from_entropy(); 64 | 65 | use rand::seq::SliceRandom; 66 | let mut err = None; 67 | let mut endpoints = PUBLIC_ETHEREUM_JSON_API_ENDPOINTS.to_vec(); 68 | endpoints.shuffle(&mut rng); 69 | for rpc_endpoint in endpoints { 70 | let client = ReqwestClient::new_with_client( 71 | rpc_endpoint.to_string(), 72 | reqwest::ClientBuilder::new() 73 | .timeout(Duration::from_secs(10)) 74 | .build() 75 | .unwrap(), 76 | ); 77 | let result = client.send_rpc_request(request.clone()).await; 78 | 79 | match result { 80 | Ok(RpcResponse::Single(Response::Success(_))) => return result, 81 | Ok(RpcResponse::Batch(batch)) 82 | if batch 83 | .iter() 84 | .all(|output| matches!(output, Response::Success(_))) => 85 | { 86 | return Ok(RpcResponse::Batch(batch)); 87 | } 88 | Ok(RpcResponse::Single(Response::Failure(failure))) => { 89 | err = Some(JsonRpcError::Evm(failure)); 90 | } 91 | Ok(RpcResponse::Batch(batch)) => { 92 | let failure = batch 93 | .iter() 94 | .find(|resp| matches!(resp, Response::Failure(_))) 95 | .map(|resp| { 96 | if let Response::Failure(failure) = resp { 97 | JsonRpcError::Evm(failure.clone()) 98 | } else { 99 | unreachable!() 100 | } 101 | }) 102 | .unwrap(); 103 | err = Some(failure); 104 | } 105 | Err(e) => { 106 | err = Some(e); 107 | } 108 | } 109 | } 110 | 111 | Err(err.unwrap()) 112 | }) 113 | } 114 | } 115 | 116 | /// Rpc reqwest client which uses the Alchemy API which is very reliable and has a high rate limit. 117 | /// Always use this in CI!!! 118 | #[derive(Clone)] 119 | pub struct AlchemyRpcReqwestClient { 120 | apikey: String, 121 | } 122 | 123 | impl AlchemyRpcReqwestClient { 124 | /// Get endpoint for Alchemy API 125 | #[inline] 126 | fn endpoint(&self) -> String { 127 | format!("https://eth-mainnet.alchemyapi.io/v2/{}", self.apikey) 128 | } 129 | } 130 | 131 | impl Client for AlchemyRpcReqwestClient { 132 | fn send_rpc_request( 133 | &self, 134 | request: RpcRequest, 135 | ) -> std::pin::Pin> + Send>> 136 | { 137 | let endpoint = self.endpoint(); 138 | 139 | Box::pin(async move { 140 | let client = ReqwestClient::new_with_client( 141 | endpoint, 142 | reqwest::ClientBuilder::new() 143 | .timeout(Duration::from_secs(10)) 144 | .build() 145 | .unwrap(), 146 | ); 147 | let result = client.send_rpc_request(request.clone()).await; 148 | 149 | match result { 150 | Ok(RpcResponse::Single(Response::Success(_))) => result, 151 | Ok(RpcResponse::Batch(batch)) 152 | if batch 153 | .iter() 154 | .all(|output| matches!(output, Response::Success(_))) => 155 | { 156 | Ok(RpcResponse::Batch(batch)) 157 | } 158 | Ok(RpcResponse::Single(Response::Failure(failure))) => { 159 | Err(JsonRpcError::Evm(failure)) 160 | } 161 | Ok(RpcResponse::Batch(batch)) => { 162 | let failure = batch 163 | .iter() 164 | .find(|resp| matches!(resp, Response::Failure(_))) 165 | .map(|resp| { 166 | if let Response::Failure(failure) = resp { 167 | JsonRpcError::Evm(failure.clone()) 168 | } else { 169 | unreachable!() 170 | } 171 | }) 172 | .unwrap(); 173 | Err(failure) 174 | } 175 | Err(e) => Err(e), 176 | } 177 | }) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/evm-block-extractor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "evm-block-extractor" 3 | 4 | authors.workspace = true 5 | homepage.workspace = true 6 | version.workspace = true 7 | rust-version.workspace = true 8 | edition.workspace = true 9 | license.workspace = true 10 | repository.workspace = true 11 | 12 | [dependencies] 13 | alloy = { workspace = true } 14 | anyhow = { workspace = true } 15 | chrono = { workspace = true } 16 | clap = { workspace = true } 17 | did = { workspace = true } 18 | env_logger = { workspace = true } 19 | ethereum-json-rpc-client = { workspace = true, features = ["reqwest"] } 20 | futures = { workspace = true } 21 | jsonrpsee = { workspace = true } 22 | lightspeed_scheduler = { workspace = true } 23 | log = { workspace = true } 24 | serde = { workspace = true } 25 | serde_json = { workspace = true } 26 | sqlx = { workspace = true, features = ["postgres", "tls-rustls", "chrono"] } 27 | thiserror = { workspace = true } 28 | tokio = { workspace = true } 29 | 30 | 31 | [dev-dependencies] 32 | alloy = { workspace = true, features = ["rand"] } 33 | port_check = { workspace = true } 34 | rand = { workspace = true } 35 | tempfile = { workspace = true } 36 | testcontainers = { workspace = true } 37 | -------------------------------------------------------------------------------- /src/evm-block-extractor/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # This dockerfile has to be called from the root folder of the project 3 | # > docker build -f src/evm-block-extractor/Dockerfile -t evm-block-extractor . 4 | # 5 | FROM rust:slim-bookworm AS builder 6 | WORKDIR /app 7 | 8 | ADD . . 9 | 10 | RUN cargo build --release --bin evm-block-extractor 11 | 12 | FROM ubuntu:22.04 AS runtime 13 | 14 | WORKDIR /app 15 | 16 | RUN apt-get update -y \ 17 | && apt-get install -y --no-install-recommends ca-certificates \ 18 | && update-ca-certificates \ 19 | # Clean up 20 | && apt-get autoremove -y \ 21 | && apt-get clean -y \ 22 | && rm -rf /var/lib/apt/lists/* 23 | 24 | COPY --from=builder /app/target/release/evm-block-extractor /app/evm-block-extractor 25 | 26 | EXPOSE 8080 27 | 28 | # Set stop signal to ctrl+c 29 | STOPSIGNAL SIGINT 30 | 31 | ENTRYPOINT ["./evm-block-extractor"] 32 | 33 | -------------------------------------------------------------------------------- /src/evm-block-extractor/README.md: -------------------------------------------------------------------------------- 1 | # EVM Block Extractor 2 | 3 | ## Introduction 4 | 5 | The EVM block extractor is an advanced tool used to collect EVM blocks and transactions, and send them to a specified data storage. 6 | This version is enhanced to handle parallel requests efficiently and integrates with Postgres DB. 7 | 8 | The block extractor tool extracts blocks only if the evm-canister global state is `Enabled`. 9 | 10 | ## Configuration 11 | 12 | ### Usage with Postgres 13 | 14 | ```sh 15 | evm-block-extractor 16 | --server-address 17 | --rpc-url 18 | --max-number-of-requests 19 | --rpc-batch-size 20 | --postgres 21 | --username 22 | --password 23 | --database_name 24 | --database_url 25 | --database_port 26 | --require_ssl 27 | ``` 28 | 29 | Where: 30 | 31 | - **username**: Username for the database connection 32 | - **password**: Password for the database connection 33 | - **database_name**: database name 34 | - **database_url**: database IP or URL 35 | - **database_port**: database port 36 | - **require_ssl**: whether to use ssl (true/false) 37 | 38 | 39 | ## Endpoints 40 | 41 | The evm-block-extractor is also a minimal version of the Ethereum JSON-RPC server which supports the following endpoints: 42 | 43 | - **eth_blockNumber**: Returns the number of most recent block. 44 | - **eth_getBlockByNumber**: Returns information about a block by block number. 45 | - **eth_getTransactionReceipt**: Returns the receipt of a transaction by transaction hash. 46 | - **ic_getBlocksRLP**: Returns a list of blocks in RLP format. 47 | 48 | ### Example 49 | 50 | ```sh 51 | curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://127.0.0.1:8080 52 | ``` 53 | 54 | ## Docker image 55 | 56 | The evm-block-extractor docker image is an ubuntu:22.04 based image that allows for simple installation of the service. 57 | The docker image accepts the same configuration arguments of the plain executor. 58 | E.g.: 59 | ```sh 60 | docker run ghcr.io/bitfinity-network/evm-block-extractor:main --rpc-url https://testnet.bitfinity.network --postgres --username postgres --password postgres --database-name postgres --database-url 127.0.0.1:5432 61 | ``` 62 | 63 | -------------------------------------------------------------------------------- /src/evm-block-extractor/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use clap::{Parser, Subcommand}; 4 | use sqlx::PgPool; 5 | use sqlx::postgres::{PgConnectOptions, PgSslMode}; 6 | 7 | use crate::database::postgres_db_client::PostgresDbClient; 8 | 9 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 10 | 11 | /// Simple CLI parser for the EVM block extractor 12 | #[derive(Parser, Debug, Clone)] 13 | #[clap( 14 | version = VERSION, 15 | about = "A tool to extract EVM blocks and transactions and serve them through JSON RPC endpoints" 16 | )] 17 | pub struct ExtractorArgs { 18 | /// The server address to bind to serve JSON RPC requests 19 | #[arg(long = "server-address", short('s'), default_value = "0.0.0.0:8080")] 20 | pub server_address: String, 21 | 22 | /// The JSON-RPC URL of the remote EVMC instance from which to extract blocks. 23 | /// If missing or empty the block extracting task won't start. 24 | #[arg(long = "rpc-url", short('u'))] 25 | pub remote_rpc_url: String, 26 | 27 | /// Time in seconds to wait for a response from the EVMC 28 | #[arg(long, default_value = "60")] 29 | pub request_time_out_secs: u64, 30 | 31 | #[arg(long, default_value = "10")] 32 | pub rpc_batch_size: usize, 33 | 34 | /// Sets the logger [`EnvFilter`]. 35 | /// Valid values: trace, debug, info, warn, error 36 | /// Example of a valid filter: "warn,my_crate=info,my_crate::my_mod=debug,[my_span]=trace". 37 | #[arg(long, default_value = "info")] 38 | pub log_filter: String, 39 | 40 | #[command(subcommand)] 41 | pub command: Database, 42 | 43 | /// Whether to reset the database when the blockchain state changes. 44 | /// This is useful for testing environments, but should not be used in production. 45 | #[arg(long, default_value = "false")] 46 | pub reset_db_on_state_change: bool, 47 | 48 | /// The interval in seconds at which the block extractor job should run 49 | #[arg(long, default_value = "120")] 50 | pub block_extractor_job_interval_seconds: u64, 51 | } 52 | 53 | #[derive(Subcommand, Debug, Clone)] 54 | pub enum Database { 55 | #[command(name = "--postgres")] 56 | Postgres { 57 | /// The username of the Postgres database 58 | #[arg(long)] 59 | username: String, 60 | /// The password of the Postgres database 61 | #[arg(long)] 62 | password: String, 63 | /// The name of the Postgres database 64 | #[arg(long)] 65 | database_name: String, 66 | /// The host of the Postgres database 67 | #[arg(long)] 68 | database_url: String, 69 | /// The port of the Postgres database 70 | #[arg(long, default_value = "5432")] 71 | database_port: u16, 72 | /// Demand SSL connection 73 | #[arg(long, default_value = "false")] 74 | require_ssl: bool, 75 | }, 76 | } 77 | 78 | impl Database { 79 | /// Build a database client based on the database type 80 | pub async fn build_client(self) -> anyhow::Result> { 81 | match self { 82 | Database::Postgres { 83 | username, 84 | password, 85 | database_name: database, 86 | database_url: host, 87 | database_port: port, 88 | require_ssl, 89 | } => { 90 | log::info!("Use Postgres database"); 91 | log::info!("- username: {}", username); 92 | log::info!("- database: {}", database); 93 | log::info!("- host: {}", host); 94 | log::info!("- port: {}", port); 95 | log::info!("- require-ssl: {}", require_ssl); 96 | 97 | let ssl_mode = if require_ssl { 98 | PgSslMode::Require 99 | } else { 100 | PgSslMode::Prefer 101 | }; 102 | 103 | let options = PgConnectOptions::new() 104 | .username(&username) 105 | .password(&password) 106 | .database(&database) 107 | .host(&host) 108 | .port(port) 109 | .ssl_mode(ssl_mode); 110 | 111 | let pool = PgPool::connect_with(options).await?; 112 | Ok(Arc::new(PostgresDbClient::new(pool))) 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/evm-block-extractor/src/database/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod postgres_db_client; 2 | 3 | use chrono::{DateTime, Utc}; 4 | use did::certified::CertifiedResult; 5 | use did::{Block, BlockchainBlockInfo, H160, H256, Transaction, U256}; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | /// Account balance 9 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 10 | pub struct AccountBalance { 11 | pub address: H160, 12 | pub balance: U256, 13 | } 14 | 15 | /// Generic data container 16 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 17 | pub struct DataContainer { 18 | pub data: D, 19 | } 20 | 21 | impl DataContainer { 22 | pub fn new(data: D) -> Self { 23 | Self { data } 24 | } 25 | } 26 | 27 | /// The genesis balances key in the key value store 28 | const GENESIS_BALANCES_KEY: &str = "genesis_balances"; 29 | /// The chain id key in the key value store 30 | const CHAIN_ID_KEY: &str = "chain_id"; 31 | /// The blockchain block info key in the key value store 32 | const BLOCKCHAIN_BLOCK_INFO_KEY: &str = "blockchain_block_info"; 33 | 34 | /// Certified block data 35 | pub type CertifiedBlock = CertifiedResult>; 36 | 37 | /// A trait for interacting with a blockchain database 38 | pub trait DatabaseClient: Send + Sync { 39 | /// Initialize the database 40 | fn init( 41 | &self, 42 | block: Option>, 43 | reset_database: bool, 44 | ) -> impl Future> + Send; 45 | 46 | /// Delete/clear the tables 47 | fn clear(&self) -> impl Future> + Send; 48 | 49 | /// Returns whether the block hash corresponds to the one in the db 50 | fn check_if_same_block_hash( 51 | &self, 52 | block: &Block, 53 | ) -> impl Future> + Send { 54 | async { 55 | let block_number = block.number.0.to(); 56 | let block_in_db = self.get_block_by_number(block_number).await?; 57 | Ok(block.hash == block_in_db.hash) 58 | } 59 | } 60 | 61 | /// Get a block from the database 62 | fn get_block_by_number( 63 | &self, 64 | block_number: u64, 65 | ) -> impl Future>> + Send; 66 | 67 | /// Get a block from the database 68 | fn get_full_block_by_number( 69 | &self, 70 | block_number: u64, 71 | ) -> impl Future>> + Send; 72 | 73 | /// Insert block data; this includes transactions and the blocks 74 | fn insert_block_data( 75 | &self, 76 | blocks: &[Block], 77 | transactions: &[Transaction], 78 | ) -> impl Future> + Send; 79 | 80 | /// Insert certified block data 81 | fn insert_certified_block_data( 82 | &self, 83 | response: CertifiedBlock, 84 | ) -> impl Future> + Send; 85 | 86 | /// Returns certified response for the last block 87 | fn get_last_certified_block_data( 88 | &self, 89 | ) -> impl Future> + Send; 90 | 91 | /// Get genesis balances 92 | fn get_genesis_balances( 93 | &self, 94 | ) -> impl Future>>> + Send; 95 | 96 | /// Insert genesis balances 97 | fn insert_genesis_balances( 98 | &self, 99 | genesis_balances: &[AccountBalance], 100 | ) -> impl Future> + Send; 101 | 102 | /// Get chain id 103 | fn get_chain_id(&self) -> impl Future>> + Send; 104 | 105 | /// Insert chain_id 106 | fn insert_chain_id(&self, chain_id: u64) -> impl Future> + Send; 107 | 108 | /// Get a transaction from the database 109 | fn get_transaction( 110 | &self, 111 | tx_hash: H256, 112 | ) -> impl Future> + Send; 113 | 114 | /// Get the latest block number 115 | fn get_latest_block_number(&self) -> impl Future>> + Send; 116 | 117 | /// Get earliest block number 118 | fn get_earliest_block_number(&self) -> impl Future> + Send; 119 | 120 | /// Delete latest blocks starting with `start_from`, and related transactions. 121 | /// Deleted blocks and transactions will be preserved in 'discarded' table with 122 | /// the given 'reason' and timestamp. 123 | fn discard_blocks_from( 124 | &self, 125 | start_from: u64, 126 | reason: &str, 127 | ) -> impl Future> + Send; 128 | 129 | /// Returns a discarded block by its hash. 130 | fn get_discarded_block_by_hash( 131 | &self, 132 | block_hash: H256, 133 | ) -> impl Future> + Send; 134 | 135 | /// Returns block info from storage. 136 | /// 137 | /// # Warning 138 | /// Do not use this info fields as indexes for blocks in storage. 139 | /// The following numbers are about block numbers in the source blockchain 140 | /// and can exceed the latest block number in the database. 141 | /// - latest_block_number: u64, 142 | /// - safe_block_number: u64, 143 | /// - finalized_block_number: u64, 144 | /// - pending_block_number: u64, 145 | fn get_block_info( 146 | &self, 147 | ) -> impl Future>> + Send; 148 | 149 | /// Stores blockchain block info. 150 | fn set_block_info( 151 | &self, 152 | info: BlockchainBlockInfo, 153 | ) -> impl Future> + Send; 154 | } 155 | 156 | /// Discarded block with metadata. 157 | #[derive(Debug)] 158 | pub struct DiscardedBlock { 159 | pub block: Block, 160 | pub reason: String, 161 | pub timestamp: DateTime, 162 | } 163 | -------------------------------------------------------------------------------- /src/evm-block-extractor/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod database; 3 | pub mod rpc; 4 | pub mod server; 5 | pub mod task; 6 | -------------------------------------------------------------------------------- /src/evm-block-extractor/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::sync::Arc; 3 | use std::time::Duration; 4 | 5 | use clap::Parser; 6 | use env_logger::Builder; 7 | use ethereum_json_rpc_client::EthJsonRpcClient; 8 | use ethereum_json_rpc_client::reqwest::ReqwestClient; 9 | use evm_block_extractor::config::ExtractorArgs; 10 | use evm_block_extractor::server::{server_start, server_stop}; 11 | use evm_block_extractor::task::block_extractor::start_extractor; 12 | use lightspeed_scheduler::JobExecutor; 13 | use lightspeed_scheduler::job::Job; 14 | use lightspeed_scheduler::scheduler::Scheduler; 15 | use log::*; 16 | 17 | #[tokio::main] 18 | async fn main() -> Result<(), Box> { 19 | let config = ExtractorArgs::parse(); 20 | 21 | // Initialize logger 22 | init_logger(&config.log_filter)?; 23 | 24 | info!("Emvc Block Extractor"); 25 | info!("----------------------"); 26 | info!("- server_address: {}", config.server_address); 27 | info!("- remote_rpc_url: {:?}", config.remote_rpc_url); 28 | info!("- rpc_batch_size: {}", config.rpc_batch_size); 29 | info!("- request_time_out_secs: {}", config.request_time_out_secs); 30 | info!( 31 | "- reset_db_on_state_change: {}", 32 | config.reset_db_on_state_change 33 | ); 34 | info!("----------------------"); 35 | 36 | let db_client = config.command.clone().build_client().await?; 37 | 38 | let job_executor = JobExecutor::new_with_local_tz(); 39 | let evm_client = Arc::new(EthJsonRpcClient::new(ReqwestClient::new( 40 | config.remote_rpc_url.clone(), 41 | ))); 42 | 43 | // Configure and start the block extractor task 44 | { 45 | let config = config.clone(); 46 | let evm_client = evm_client.clone(); 47 | let db_client = db_client.clone(); 48 | 49 | job_executor 50 | .add_job_with_scheduler( 51 | Scheduler::Interval { 52 | interval_duration: Duration::from_secs( 53 | config.block_extractor_job_interval_seconds, 54 | ), 55 | execute_at_startup: true, 56 | }, 57 | Job::new("evm_block_extractor", "extract_blocks", None, move || { 58 | let config = config.clone(); 59 | let evm_client = evm_client.clone(); 60 | let db_client = db_client.clone(); 61 | Box::pin(async move { 62 | start_extractor(config, db_client, evm_client).await?; 63 | Ok(()) 64 | }) 65 | }), 66 | ) 67 | .await; 68 | } 69 | 70 | // Start the job executor 71 | let _job_executor_handle = job_executor.run().await?; 72 | 73 | // Start JSON RPC server 74 | let server_handle = server_start(&config.server_address, db_client, evm_client).await?; 75 | 76 | // Subscribe to the termination signals 77 | match tokio::signal::ctrl_c().await { 78 | Ok(_) => { 79 | info!("Received shutdown signal"); 80 | } 81 | Err(err) => error!("Failed to listen for shutdown signal: {err}"), 82 | } 83 | 84 | // Stop the world 85 | { 86 | let stop_gracefully = true; 87 | job_executor 88 | .stop(stop_gracefully) 89 | .await 90 | .expect("The job executor should stop!"); 91 | 92 | server_stop(server_handle).await?; 93 | } 94 | 95 | Ok(()) 96 | } 97 | 98 | /// Initialize the logger 99 | fn init_logger(logger_filter: &str) -> Result<(), SetLoggerError> { 100 | Builder::new().parse_filters(logger_filter).try_init() 101 | } 102 | -------------------------------------------------------------------------------- /src/evm-block-extractor/src/rpc.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use alloy::eips::BlockNumberOrTag; 4 | use alloy::primitives::{Address, U64, U256}; 5 | use did::evm_state::EvmGlobalState; 6 | use did::{BlockConfirmationData, BlockConfirmationResult, BlockchainBlockInfo}; 7 | use ethereum_json_rpc_client::{Client, EthJsonRpcClient}; 8 | use jsonrpsee::core::RpcResult; 9 | use jsonrpsee::proc_macros::rpc; 10 | use jsonrpsee::types::{ErrorCode, ErrorObject}; 11 | 12 | use crate::database::{CertifiedBlock, DatabaseClient}; 13 | 14 | pub struct EthImpl 15 | where 16 | DB: DatabaseClient, 17 | C: Client + Send + Sync + 'static, 18 | { 19 | pub blockchain: Arc, 20 | pub evm_client: Arc>, 21 | } 22 | 23 | impl Clone for EthImpl 24 | where 25 | DB: DatabaseClient, 26 | C: Client + Send + Sync + 'static, 27 | { 28 | fn clone(&self) -> Self { 29 | Self { 30 | blockchain: self.blockchain.clone(), 31 | evm_client: self.evm_client.clone(), 32 | } 33 | } 34 | } 35 | 36 | impl EthImpl 37 | where 38 | DB: DatabaseClient, 39 | C: Client + Send + Sync + 'static, 40 | { 41 | pub fn new(db: Arc, evm_client: Arc>) -> Self { 42 | Self { 43 | blockchain: db, 44 | evm_client, 45 | } 46 | } 47 | } 48 | 49 | /// eth_* RPC methods 50 | #[rpc(server, namespace = "eth")] 51 | pub trait Eth { 52 | #[method(name = "getBlockByNumber")] 53 | /// Get a block by number 54 | async fn get_block_by_number( 55 | &self, 56 | block: BlockNumberOrTag, 57 | full_transactions: bool, 58 | ) -> RpcResult; 59 | 60 | #[method(name = "blockNumber")] 61 | /// Get the latest block number 62 | async fn block_number(&self) -> RpcResult; 63 | 64 | #[method(name = "chainId")] 65 | /// Get the chain id 66 | async fn get_chain_id(&self) -> RpcResult; 67 | } 68 | 69 | /// ic_* RPC methods 70 | #[rpc(server, namespace = "ic")] 71 | pub trait IC { 72 | #[method(name = "getGenesisBalances")] 73 | async fn get_genesis_balances(&self) -> RpcResult>; 74 | 75 | #[method(name = "getLastCertifiedBlock")] 76 | async fn get_last_block_certified_data(&self) -> RpcResult; 77 | 78 | #[method(name = "getEvmGlobalState")] 79 | async fn get_evm_global_state(&self) -> RpcResult; 80 | 81 | #[method(name = "sendConfirmBlock")] 82 | async fn send_confirm_block( 83 | &self, 84 | data: BlockConfirmationData, 85 | ) -> RpcResult; 86 | } 87 | 88 | #[jsonrpsee::core::async_trait] 89 | impl ICServer for EthImpl 90 | where 91 | DB: DatabaseClient + Send + Sync + 'static, 92 | C: Client + Send + Sync + 'static, 93 | { 94 | async fn get_genesis_balances(&self) -> RpcResult> { 95 | let tx = self.blockchain.get_genesis_balances().await.map_err(|e| { 96 | log::error!("Error getting genesis balances: {:?}", e); 97 | ErrorCode::InternalError 98 | })?; 99 | 100 | Ok(tx 101 | .unwrap_or_default() 102 | .into_iter() 103 | .map(|account| (account.address.into(), account.balance.into())) 104 | .collect()) 105 | } 106 | 107 | async fn get_last_block_certified_data(&self) -> RpcResult { 108 | let certified_data = self 109 | .blockchain 110 | .get_last_certified_block_data() 111 | .await 112 | .map_err(|e| { 113 | log::error!("Error getting last block certified data: {:?}", e); 114 | ErrorCode::InternalError 115 | })?; 116 | 117 | Ok(certified_data) 118 | } 119 | 120 | async fn get_evm_global_state(&self) -> RpcResult { 121 | self.evm_client.get_evm_global_state().await.map_err(|e| { 122 | log::error!("Error getting EVM global state: {:?}", e); 123 | ErrorObject::from(ErrorCode::InternalError) 124 | }) 125 | } 126 | 127 | async fn send_confirm_block( 128 | &self, 129 | data: BlockConfirmationData, 130 | ) -> RpcResult { 131 | let block_info = self.blockchain.get_block_info().await.map_err(|e| { 132 | log::warn!("failed to get block info from database: {e}"); 133 | ErrorCode::InternalError 134 | })?; 135 | 136 | let should_forward = match block_info { 137 | Some(info) if info.safe_block_number < data.block_number => true, 138 | None => true, 139 | _ => false, 140 | }; 141 | 142 | let confirmation_result = if should_forward { 143 | self.evm_client 144 | .send_confirm_block(data) 145 | .await 146 | .map_err(|e| { 147 | log::warn!("failed to send block confirmation to evm: {e}"); 148 | ErrorCode::InternalError 149 | })? 150 | } else { 151 | BlockConfirmationResult::AlreadyConfirmed 152 | }; 153 | 154 | Ok(confirmation_result) 155 | } 156 | } 157 | 158 | #[jsonrpsee::core::async_trait] 159 | impl EthServer for EthImpl 160 | where 161 | DB: DatabaseClient + Send + Sync + 'static, 162 | C: Client + Send + Sync + 'static, 163 | { 164 | async fn get_block_by_number( 165 | &self, 166 | block: BlockNumberOrTag, 167 | include_transactions: bool, 168 | ) -> RpcResult { 169 | let db = &self.blockchain; 170 | 171 | let Some(latest_block_in_db) = 172 | self.blockchain 173 | .get_latest_block_number() 174 | .await 175 | .map_err(|e| { 176 | log::warn!("Error getting earliest block number: {:?}", e); 177 | ErrorCode::InternalError 178 | })? 179 | else { 180 | return Ok(serde_json::Value::Null); 181 | }; 182 | 183 | let block_info_future = async { 184 | match db.get_block_info().await { 185 | Ok(Some(info)) => info, 186 | Ok(None) => { 187 | log::warn!("No block info set, can't select {block} block."); 188 | // We can't get the block info if the evm-canister version is too old. 189 | // Once all the canisters are updated, we can remove this logic and return instead of proceed. 190 | // TODO: Remove this logic in EPROD-1123 191 | // Err(ErrorCode::InternalError) 192 | BlockchainBlockInfo { 193 | earliest_block_number: 0, 194 | latest_block_number: latest_block_in_db, 195 | safe_block_number: latest_block_in_db, 196 | finalized_block_number: latest_block_in_db, 197 | pending_block_number: latest_block_in_db + 1, 198 | } 199 | } 200 | Err(e) => { 201 | log::warn!("Error getting blockchain block info: {:?}", e); 202 | // We can't get the block info if the evm-canister version is too old. 203 | // Once all the canisters are updated, we can remove this logic and return instead of proceed. 204 | // TODO: Remove this logic in EPROD-1123 205 | // Err(ErrorCode::InternalError) 206 | BlockchainBlockInfo { 207 | earliest_block_number: 0, 208 | latest_block_number: latest_block_in_db, 209 | safe_block_number: latest_block_in_db, 210 | finalized_block_number: latest_block_in_db, 211 | pending_block_number: latest_block_in_db + 1, 212 | } 213 | } 214 | } 215 | }; 216 | 217 | let block_number = match block { 218 | BlockNumberOrTag::Finalized => { 219 | let block_info = block_info_future.await; 220 | block_info.finalized_block_number.min(latest_block_in_db) 221 | } 222 | BlockNumberOrTag::Safe => { 223 | let block_info = block_info_future.await; 224 | block_info.safe_block_number.min(latest_block_in_db) 225 | } 226 | BlockNumberOrTag::Latest => latest_block_in_db, 227 | BlockNumberOrTag::Earliest => db.get_earliest_block_number().await.map_err(|e| { 228 | log::error!("Error getting earliest block number: {:?}", e); 229 | ErrorCode::InternalError 230 | })?, 231 | BlockNumberOrTag::Number(num) => num, 232 | BlockNumberOrTag::Pending => return Ok(serde_json::Value::Null), 233 | }; 234 | 235 | if include_transactions { 236 | let block = self 237 | .blockchain 238 | .get_full_block_by_number(block_number) 239 | .await 240 | .map_err(|e| { 241 | log::error!("Error getting block: {:?}", e); 242 | ErrorCode::InternalError 243 | })?; 244 | 245 | let block = serde_json::to_value(&block).map_err(|e| { 246 | log::error!("Error serializing block: {:?}", e); 247 | ErrorCode::InternalError 248 | })?; 249 | 250 | Ok(block) 251 | } else { 252 | let block = self 253 | .blockchain 254 | .get_block_by_number(block_number) 255 | .await 256 | .map_err(|e| { 257 | log::error!("Error getting block: {:?}", e); 258 | ErrorCode::InternalError 259 | })?; 260 | 261 | let block = serde_json::to_value(&block).map_err(|e| { 262 | log::error!("Error serializing block: {:?}", e); 263 | ErrorCode::InternalError 264 | })?; 265 | 266 | Ok(block) 267 | } 268 | } 269 | 270 | async fn block_number(&self) -> RpcResult { 271 | let block_number = self 272 | .blockchain 273 | .get_latest_block_number() 274 | .await 275 | .map_err(|e| { 276 | log::error!("Error getting block number: {:?}", e); 277 | ErrorCode::InternalError 278 | })? 279 | .unwrap_or(0); 280 | 281 | Ok(U256::from(block_number)) 282 | } 283 | 284 | async fn get_chain_id(&self) -> RpcResult { 285 | let chain_id = self 286 | .blockchain 287 | .get_chain_id() 288 | .await 289 | .map_err(|e| { 290 | log::error!("Error getting chain id: {:?}", e); 291 | ErrorCode::InternalError 292 | })? 293 | .ok_or(ErrorCode::InternalError)?; 294 | 295 | Ok(U64::from(chain_id)) 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /src/evm-block-extractor/src/server.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use ethereum_json_rpc_client::{Client, EthJsonRpcClient}; 4 | use jsonrpsee::RpcModule; 5 | use jsonrpsee::server::{Server, ServerHandle}; 6 | use log::*; 7 | 8 | use crate::database::DatabaseClient; 9 | use crate::rpc::{EthImpl, EthServer, ICServer}; 10 | 11 | /// Start the RPC server 12 | pub async fn server_start( 13 | server_address: &str, 14 | db_client: Arc, 15 | evm_client: Arc>, 16 | ) -> anyhow::Result { 17 | info!("Start server"); 18 | 19 | let server = Server::builder().build(server_address).await?; 20 | 21 | let eth = EthImpl::new(db_client, evm_client); 22 | 23 | let mut module = RpcModule::new(()); 24 | 25 | module.merge(EthServer::into_rpc(eth.clone()))?; 26 | module.merge(ICServer::into_rpc(eth))?; 27 | 28 | info!("Server started on {}", server.local_addr()?); 29 | 30 | Ok(server.start(module)) 31 | } 32 | 33 | /// Stop the RPC server 34 | pub async fn server_stop(server: ServerHandle) -> anyhow::Result<()> { 35 | info!("Stopping server"); 36 | server.stop()?; 37 | server.stopped().await; 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /src/evm-block-extractor/src/task/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod block_extractor; 2 | -------------------------------------------------------------------------------- /src/evm-block-extractor/src_resources/db/postgres/migrations/00001_create_schema.sql: -------------------------------------------------------------------------------- 1 | 2 | ----------------------------- 3 | -- Begin - EVM_BLOCK - 4 | ----------------------------- 5 | 6 | create table EVM_BLOCK ( 7 | ID bigint primary key, 8 | DATA JSONB 9 | ); 10 | 11 | -- End - EVM_BLOCK - 12 | 13 | ----------------------------------------- 14 | -- Begin - EVM_TRANSACTION_EXE_RESULT - 15 | ----------------------------------------- 16 | 17 | create table EVM_TRANSACTION_EXE_RESULT ( 18 | ID char(66) primary key, -- 64 is the length of a H256 in hex, plus 0x 19 | DATA JSONB 20 | ); 21 | 22 | -- End - EVM_TRANSACTION_EXE_RESULT - 23 | 24 | 25 | ----------------------------------------- 26 | -- Begin - EVM_TRANSACTION - 27 | ----------------------------------------- 28 | 29 | create table EVM_TRANSACTION ( 30 | ID char(66) primary key, -- 64 is the length of a H256 in hex, plus 0x 31 | DATA JSONB, 32 | BLOCK_NUMBER bigint 33 | ); 34 | 35 | CREATE INDEX EVM_TRANSACTION_INDEX_BLOCK_NUMBER ON EVM_TRANSACTION( BLOCK_NUMBER ); 36 | 37 | -- End - EVM_TRANSACTION - 38 | 39 | 40 | ----------------------------------------- 41 | -- Begin - EVM_KEY_VALUE_DATA - 42 | ----------------------------------------- 43 | 44 | create table EVM_KEY_VALUE_DATA ( 45 | KEY TEXT primary key, 46 | DATA JSONB 47 | ); 48 | 49 | CREATE INDEX EVM_KEY_VALUE_DATA_INDEX_KEY ON EVM_KEY_VALUE_DATA( KEY ); 50 | 51 | -- End - EVM_KEY_VALUE_DATA - 52 | -------------------------------------------------------------------------------- /src/evm-block-extractor/src_resources/db/postgres/migrations/00002_drop_receipts_table.sql: -------------------------------------------------------------------------------- 1 | ----------------------------------------- 2 | -- Begin - drop EVM_TRANSACTION_EXE_RESULT - 3 | ----------------------------------------- 4 | 5 | drop table EVM_TRANSACTION_EXE_RESULT cascade; 6 | 7 | -- End - drop EVM_TRANSACTION_EXE_RESULT - -------------------------------------------------------------------------------- /src/evm-block-extractor/src_resources/db/postgres/migrations/00003_add_certified_blocks.sql: -------------------------------------------------------------------------------- 1 | ----------------------------- 2 | -- Begin - CERTIFIED_EVM_BLOCK - 3 | ----------------------------- 4 | 5 | create table CERTIFIED_EVM_BLOCK ( 6 | ID bigint primary key, 7 | CERTIFIED_RESPONSE JSONB 8 | ); 9 | 10 | -- End - CERTIFIED_EVM_BLOCK - 11 | -------------------------------------------------------------------------------- /src/evm-block-extractor/src_resources/db/postgres/migrations/00004_add_deleted_blocks.sql: -------------------------------------------------------------------------------- 1 | 2 | ----------------------------- 3 | -- Begin - DISCARDED_EVM_BLOCK - 4 | ----------------------------- 5 | 6 | create table DISCARDED_EVM_BLOCK ( 7 | ID char(66) primary key, -- 64 is the length of a H256 in hex, plus 0x 8 | DATA JSONB, 9 | REASON TEXT, 10 | DISCARDED_AT TIMESTAMPTZ default (now() AT TIME ZONE 'utc') 11 | ); 12 | 13 | -- End - DISCARDED_EVM_BLOCK - 14 | -------------------------------------------------------------------------------- /src/evm-block-extractor/tests/evm_block_extractor_it.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use evm_block_extractor::config::Database; 4 | use evm_block_extractor::database::postgres_db_client::PostgresDbClient; 5 | use testcontainers::testcontainers::ContainerAsync; 6 | use testcontainers::testcontainers::runners::AsyncRunner; 7 | 8 | mod tests; 9 | 10 | async fn test_with_clients) -> ()>(test: T) { 11 | let _ = env_logger::Builder::new().parse_filters("info").try_init(); 12 | println!("----------------------------------"); 13 | println!("Running test with PostgresDbClient"); 14 | println!("----------------------------------"); 15 | let (postgres_client, _node) = new_postgres_db_client().await; 16 | test(postgres_client).await; 17 | } 18 | 19 | async fn new_postgres_db_client() -> ( 20 | Arc, 21 | ContainerAsync, 22 | ) { 23 | let node = testcontainers::postgres::Postgres::default() 24 | .start() 25 | .await 26 | .unwrap(); 27 | 28 | let db = Database::Postgres { 29 | username: "postgres".to_string(), 30 | password: "postgres".to_string(), 31 | database_name: "postgres".to_string(), 32 | database_url: "127.0.0.1".to_owned(), 33 | database_port: node.get_host_port_ipv4(5432).await.unwrap(), 34 | require_ssl: false, 35 | }; 36 | 37 | (db.build_client().await.unwrap(), node) 38 | } 39 | -------------------------------------------------------------------------------- /src/evm-block-extractor/tests/tests/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod block_extractor_it; 2 | pub mod database_client_it; 3 | pub mod server_it; 4 | -------------------------------------------------------------------------------- /src/evm-canister-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "evm-canister-client" 3 | categories = ["cryptography::cryptocurrencies"] 4 | description = "Client for interacting with the Evm Canister" 5 | include = ["src/**/*", "../../LICENSE", "../../README.md"] 6 | 7 | authors.workspace = true 8 | homepage.workspace = true 9 | version.workspace = true 10 | edition.workspace = true 11 | rust-version.workspace = true 12 | license.workspace = true 13 | repository.workspace = true 14 | 15 | 16 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 17 | [features] 18 | default = [] 19 | ic-agent-client = ["ic-canister-client/ic-agent-client"] 20 | 21 | [dependencies] 22 | candid = { workspace = true } 23 | did = { workspace = true } 24 | ic-canister-client = { workspace = true } 25 | ic-log = { workspace = true } 26 | -------------------------------------------------------------------------------- /src/evm-canister-client/README.md: -------------------------------------------------------------------------------- 1 | # evm-canister-client 2 | -------------------------------------------------------------------------------- /src/evm-canister-client/src/error.rs: -------------------------------------------------------------------------------- 1 | use did::error::EvmError; 2 | 3 | /// This is the result type for all EVM calls. 4 | pub type EvmResult = Result; 5 | -------------------------------------------------------------------------------- /src/evm-canister-client/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod error; 3 | 4 | pub use client::EvmCanisterClient; 5 | pub use error::EvmResult; 6 | pub use ic_canister_client::*; 7 | -------------------------------------------------------------------------------- /src/evm-log-extractor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "evm-log-extractor" 3 | 4 | authors.workspace = true 5 | homepage.workspace = true 6 | version.workspace = true 7 | rust-version.workspace = true 8 | edition.workspace = true 9 | license.workspace = true 10 | repository.workspace = true 11 | 12 | [dependencies] 13 | anyhow = { workspace = true } 14 | clap = { workspace = true } 15 | chrono = { workspace = true } 16 | did = { workspace = true } 17 | env_logger = { workspace = true } 18 | evm-canister-client = { workspace = true, features = ["ic-agent-client"] } 19 | lightspeed_scheduler = { workspace = true } 20 | log = { workspace = true } 21 | tokio = { workspace = true } 22 | 23 | [dev-dependencies] 24 | candid = { workspace = true } 25 | rand = { workspace = true } 26 | serde = { workspace = true } 27 | serde_json = { workspace = true } 28 | tempfile = { workspace = true } 29 | -------------------------------------------------------------------------------- /src/evm-log-extractor/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # This dockerfile has to be called from the root folder of the project 3 | # > docker build -f src/evm-log-extractor/Dockerfile -t evm-log-extractor . 4 | # 5 | FROM rust:slim-bookworm AS builder 6 | WORKDIR /app 7 | 8 | ADD . . 9 | 10 | RUN cargo build --release --bin evm-log-extractor 11 | 12 | FROM ubuntu:22.04 AS runtime 13 | 14 | WORKDIR /app 15 | 16 | COPY --from=builder /app/target/release/evm-log-extractor /app/evm-log-extractor 17 | 18 | ENV LOGGER_FILTER=info 19 | ENV EVMC_PRINCIPAL= 20 | ENV IDENTITY=/data/config/identity.pem 21 | ENV EVMC_NETWORK_URL=https://icp0.io 22 | ENV LOGS_DIR=/data/logs 23 | 24 | # Set stop signal to ctrl+c 25 | STOPSIGNAL SIGINT 26 | 27 | CMD exec ./evm-log-extractor --evmc-principal=${EVMC_PRINCIPAL} --identity=${IDENTITY} --logs-directory=${LOGS_DIR} --evmc-network-url=${EVMC_NETWORK_URL} --logger-filter=${LOGGER_FILTER} 28 | 29 | -------------------------------------------------------------------------------- /src/evm-log-extractor/README.md: -------------------------------------------------------------------------------- 1 | # EVM Block Extractor 2 | 3 | ## Introduction 4 | 5 | The EVM log extractor is a tool used to collect logs from the EVM canister. 6 | 7 | ## Configuration 8 | 9 | To run the log extractor use CLI command: 10 | ```bash 11 | evm-log-extractor [OPTIONS] 12 | ``` 13 | 14 | ### Requirements 15 | 16 | The principal utilizing this tool must have the `ReadLogs` permission configured for the EVMC canister instance from which logs are to be downloaded. This permission should be granted by an administrator using the following command: 17 | 18 | ```bash 19 | dfx canister call admin_ic_permissions_add '(principal "", vec {variant { ReadLogs }})' --network ic 20 | ``` 21 | 22 | ### CLI options 23 | 24 | - `--logger-filter ` 25 | 26 | Sets the logger `EnvFilter`. Valid values: `trace`, `debug`, `info`, `warn`, `error`. Example of a valid filter: `warn,my_crate=info,my_crate::my_mod=debug,[my_span]=trace`. Default: `info`. 27 | 28 | - `--evmc-network-url ` 29 | 30 | URL of the EVMC network. 31 | Default: http://127.0.0.1:8000 32 | 33 | - `--identity ` 34 | 35 | Path to your identity pem file. 36 | 37 | - `--evmc-principal ` 38 | 39 | Evmc canister Principal. 40 | 41 | - `--logs-synchronization-job-interval-seconds ` 42 | 43 | Logs synchronization job interval in seconds. 44 | This job executes is executed every seconds and download the 45 | evmc logs to a file on the local filesystem. The job is enables only if both `identity` and `evmc_principal` are provided. 46 | Default is 10 seconds. 47 | 48 | - `--logs-synchronization-job-max-logs-per-call ` 49 | 50 | The max number of logs to be downloaded on each log synchronization job loop. 51 | Default is 5_000. 52 | 53 | - `--logs-directory ` 54 | 55 | Path to the directory where the EVM downloaded logs are written into. 56 | 57 | 58 | ### Examples 59 | 60 | Example of how to run the log extractor from source code targeting an evmc canister running in a local dfx replica: 61 | 62 | `$ cargo run -p evm-log-extractor -- --evmc-principal=bkyz2-fmaaa-aaaaa-qaaaq-cai --identity ~/.config/dfx/identity/alice/identity.pem --logs-directory ./target/logs --evmc-network-url "http://127.0.0.1:38985"` 63 | 64 | 65 | ## Docker image 66 | 67 | The evm-log-extractor docker image is an ubuntu:22.04 based image that allows for simple installation of the service. 68 | 69 | The docker image accepts the following configuration variables: 70 | 71 | - `LOGGER_FILTER`: (Optional) the level of the logger. Default is `info` 72 | - `EVMC_PRINCIPAL`: (Mandatory) the canister ID of the evmc canister 73 | - `EVMC_NETWORK_URL`: (Optional) the URL of the IC network. Default is `https://icp0.io` 74 | 75 | It is also required to configure these volumes: 76 | - `/data/config/identity.pem`: mount point for the identity pem file to be used for calling the evmc canister 77 | - `/data/logs`: mount point where the extracted logs will be persisted 78 | 79 | E.g.: 80 | ```sh 81 | docker run ghcr.io/bitfinity-network/evm-log-extractor:main \ 82 | -e EVMC_PRINCIPAL=bkyz2-fmaaa-aaaaa-qaaaq-cai \ 83 | -v ~/.config/dfx/identity/alice/identity.pem:/data/config/identity.pem:ro \ 84 | -v ./target/logs:/data/logs 85 | ``` 86 | -------------------------------------------------------------------------------- /src/evm-log-extractor/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::Parser; 4 | use evm_canister_client::ic_agent::export::Principal; 5 | 6 | /// A tool that synchronizes blocks from the remote Ethereum JSON RPC endpoint 7 | /// and re-executes it locally. 8 | #[derive(Debug, Clone, Parser)] 9 | pub struct LogExtractorConfig { 10 | /// Sets the logger [`EnvFilter`]. 11 | /// Valid values: trace, debug, info, warn, error 12 | /// Example of a valid filter: "warn,my_crate=info,my_crate::my_mod=debug,[my_span]=trace". 13 | #[clap(long, default_value = "info")] 14 | pub logger_filter: String, 15 | 16 | /// URL of the EVMC network. 17 | #[clap(long, default_value = "http://127.0.0.1:8000")] 18 | pub evmc_network_url: String, 19 | 20 | /// Path to your identity pem file. 21 | #[arg(long)] 22 | pub identity: PathBuf, 23 | 24 | /// evmc canister Principal. 25 | #[arg(long)] 26 | pub evmc_principal: Principal, 27 | 28 | /// Logs synchronization job interval schedule. 29 | #[clap(long, default_value = "10")] 30 | pub logs_synchronization_job_interval_seconds: u64, 31 | 32 | /// Logs synchronization job max logs to download per call. 33 | #[clap(long, default_value = "5000")] 34 | pub logs_synchronization_job_max_logs_per_call: usize, 35 | 36 | /// Path to the directory where to put the EVM downloaded logs. 37 | #[clap(long, default_value = "./")] 38 | pub logs_directory: String, 39 | } 40 | -------------------------------------------------------------------------------- /src/evm-log-extractor/src/job/logs.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::sync::Arc; 3 | 4 | use chrono::{DateTime, Datelike, Utc}; 5 | use evm_canister_client::client::Log; 6 | use evm_canister_client::{CanisterClient, EvmCanisterClient}; 7 | use tokio::io::AsyncWriteExt; 8 | use tokio::sync::Mutex; 9 | 10 | /// Logs Jobs settings 11 | #[derive(Clone)] 12 | pub struct LogsJobSettings { 13 | /// Path where to put log files 14 | pub path: String, 15 | pub max_logs_per_call: usize, 16 | pub start_from_offset: Arc>, 17 | } 18 | 19 | /// Download the Logs from an evmc instance and saves them to a file. 20 | pub async fn run_logs_job( 21 | client: EvmCanisterClient, 22 | settings: LogsJobSettings, 23 | ) -> anyhow::Result<()> { 24 | let offset = *settings.start_from_offset.lock().await; 25 | let logs = client.ic_logs(settings.max_logs_per_call, offset).await??; 26 | 27 | let current_date = chrono::Utc::now(); 28 | let filename = filename(¤t_date); 29 | write_logs(&logs.logs, &settings.path, &filename).await?; 30 | 31 | *settings.start_from_offset.lock().await = logs 32 | .logs 33 | .last() 34 | .map(|log| log.offset + 1) 35 | .unwrap_or_else(|| logs.all_logs_count); 36 | 37 | Ok(()) 38 | } 39 | 40 | fn filename(date: &DateTime) -> String { 41 | format!( 42 | "{}_{:02}_{:02}_logs.log", 43 | date.year(), 44 | date.month(), 45 | date.day() 46 | ) 47 | } 48 | 49 | async fn write_logs(logs: &[Log], path: &str, filename: &str) -> anyhow::Result<()> { 50 | tokio::fs::create_dir_all(Path::new(path)).await?; 51 | 52 | let mut file = tokio::fs::OpenOptions::new() 53 | .create(true) 54 | .append(true) 55 | .open(Path::new(path).join(filename)) 56 | .await?; 57 | 58 | for log in logs { 59 | file.write_all(log.log.trim().as_bytes()).await?; 60 | file.write_all(b"\n").await?; 61 | } 62 | file.flush().await?; 63 | 64 | Ok(()) 65 | } 66 | 67 | #[cfg(test)] 68 | mod tests { 69 | 70 | use chrono::NaiveDate; 71 | use tempfile::tempdir; 72 | use tokio::fs::File; 73 | use tokio::io::{AsyncBufReadExt, BufReader}; 74 | 75 | use super::*; 76 | 77 | #[tokio::test] 78 | async fn test_job_should_create_file() { 79 | // Arrange 80 | let temp_dir = tempdir().unwrap(); 81 | let logs_path = temp_dir.path().join("some").join("else"); 82 | let logs_file = "file.log"; 83 | assert!(!logs_path.exists()); 84 | 85 | let logs = vec![ 86 | Log { 87 | log: "line 0".to_string(), 88 | offset: 0, 89 | }, 90 | Log { 91 | log: "line 1".to_string(), 92 | offset: 0, 93 | }, 94 | ]; 95 | 96 | // Act 97 | write_logs(&logs, logs_path.to_str().unwrap(), logs_file) 98 | .await 99 | .unwrap(); 100 | 101 | // Assert 102 | let log_file = logs_path.join(logs_file); 103 | assert!(log_file.exists()); 104 | 105 | let file = File::open(log_file).await.unwrap(); 106 | let reader = BufReader::new(file); 107 | 108 | let mut lines = reader.lines(); 109 | assert_eq!(Some("line 0".to_owned()), lines.next_line().await.unwrap()); 110 | assert_eq!(Some("line 1".to_owned()), lines.next_line().await.unwrap()); 111 | assert_eq!(None, lines.next_line().await.unwrap()); 112 | } 113 | 114 | #[tokio::test] 115 | async fn test_job_should_append_to_file() { 116 | // Arrange 117 | let temp_dir = tempdir().unwrap(); 118 | let logs_path = temp_dir.path(); 119 | let logs_file = "file.log"; 120 | 121 | let logs = vec![ 122 | Log { 123 | log: "line 0".to_string(), 124 | offset: 0, 125 | }, 126 | Log { 127 | log: "line 1".to_string(), 128 | offset: 0, 129 | }, 130 | ]; 131 | write_logs(&logs, logs_path.to_str().unwrap(), logs_file) 132 | .await 133 | .unwrap(); 134 | 135 | assert!(logs_path.join(logs_file).exists()); 136 | 137 | // Act 138 | let new_logs = vec![ 139 | Log { 140 | log: "line 2".to_string(), 141 | offset: 0, 142 | }, 143 | Log { 144 | log: "line 3".to_string(), 145 | offset: 0, 146 | }, 147 | ]; 148 | write_logs(&new_logs, logs_path.to_str().unwrap(), logs_file) 149 | .await 150 | .unwrap(); 151 | 152 | // Assert 153 | let log_file = logs_path.join(logs_file); 154 | assert!(log_file.exists()); 155 | 156 | let file = File::open(log_file).await.unwrap(); 157 | let reader = BufReader::new(file); 158 | 159 | let mut lines = reader.lines(); 160 | assert_eq!(Some("line 0".to_owned()), lines.next_line().await.unwrap()); 161 | assert_eq!(Some("line 1".to_owned()), lines.next_line().await.unwrap()); 162 | assert_eq!(Some("line 2".to_owned()), lines.next_line().await.unwrap()); 163 | assert_eq!(Some("line 3".to_owned()), lines.next_line().await.unwrap()); 164 | assert_eq!(None, lines.next_line().await.unwrap()); 165 | } 166 | 167 | #[test] 168 | fn test_filename() { 169 | // Arrange 170 | let date_2014_01_01 = NaiveDate::from_ymd_opt(2014, 1, 1) 171 | .unwrap() 172 | .and_hms_opt(1, 2, 3) 173 | .unwrap() 174 | .and_utc(); 175 | let date_2021_12_01 = NaiveDate::from_ymd_opt(2021, 12, 1) 176 | .unwrap() 177 | .and_hms_opt(1, 2, 3) 178 | .unwrap() 179 | .and_utc(); 180 | let date_2014_03_17 = NaiveDate::from_ymd_opt(2014, 3, 17) 181 | .unwrap() 182 | .and_hms_opt(1, 2, 3) 183 | .unwrap() 184 | .and_utc(); 185 | 186 | // Act & Assert 187 | assert_eq!("2014_01_01_logs.log", filename(&date_2014_01_01)); 188 | assert_eq!("2021_12_01_logs.log", filename(&date_2021_12_01)); 189 | assert_eq!("2014_03_17_logs.log", filename(&date_2014_03_17)); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/evm-log-extractor/src/job/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod logs; 2 | -------------------------------------------------------------------------------- /src/evm-log-extractor/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod job; 3 | -------------------------------------------------------------------------------- /src/evm-log-extractor/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::time::Duration; 3 | 4 | use clap::Parser; 5 | use env_logger::Builder; 6 | use evm_canister_client::{EvmCanisterClient, IcAgentClient}; 7 | use evm_log_extractor::config::LogExtractorConfig; 8 | use evm_log_extractor::job::logs::{LogsJobSettings, run_logs_job}; 9 | use lightspeed_scheduler::JobExecutor; 10 | use lightspeed_scheduler::job::Job; 11 | use lightspeed_scheduler::scheduler::Scheduler; 12 | use log::{SetLoggerError, info}; 13 | 14 | #[tokio::main] 15 | async fn main() -> Result<(), Box> { 16 | let config = LogExtractorConfig::parse(); 17 | init_logger(&config.logger_filter)?; 18 | 19 | let job_executor = JobExecutor::new_with_local_tz(); 20 | 21 | let evmc_client = build_evmc_client(&config) 22 | .await 23 | .expect("failed to build evmc client"); 24 | 25 | configure_logs_job(evmc_client, &config, &job_executor).await; 26 | 27 | // Start the job executor 28 | let _job_executor_handle = job_executor.run().await?; 29 | 30 | // Wait for a signal to stop the job executor 31 | tokio::signal::ctrl_c().await.unwrap(); 32 | 33 | info!("Shutdown message received"); 34 | 35 | // Stop the job executor 36 | let stop_gracefully = true; 37 | job_executor 38 | .stop(stop_gracefully) 39 | .await 40 | .expect("The job executor should stop!"); 41 | 42 | Ok(()) 43 | } 44 | 45 | /// Initializes the logger 46 | fn init_logger(logger_filter: &str) -> Result<(), SetLoggerError> { 47 | // Initialize logger 48 | Builder::new().parse_filters(logger_filter).try_init() 49 | } 50 | 51 | /// Builds the EVMC client if the config is provided 52 | async fn build_evmc_client( 53 | config: &LogExtractorConfig, 54 | ) -> anyhow::Result> { 55 | let agent = IcAgentClient::with_identity( 56 | config.evmc_principal, 57 | config.identity.clone(), 58 | &config.evmc_network_url, 59 | None, 60 | ) 61 | .await?; 62 | Ok(EvmCanisterClient::new(agent)) 63 | } 64 | 65 | /// Configures and starts the logs synchronization job 66 | async fn configure_logs_job( 67 | evmc_client: EvmCanisterClient, 68 | config: &LogExtractorConfig, 69 | job_executor: &JobExecutor, 70 | ) { 71 | // Configure and start the logs job 72 | let settings = LogsJobSettings { 73 | path: config.logs_directory.clone(), 74 | max_logs_per_call: config.logs_synchronization_job_max_logs_per_call, 75 | start_from_offset: Default::default(), 76 | }; 77 | let evmc_client = evmc_client.clone(); 78 | job_executor 79 | .add_job_with_scheduler( 80 | Scheduler::Interval { 81 | interval_duration: Duration::from_secs( 82 | config.logs_synchronization_job_interval_seconds, 83 | ), 84 | execute_at_startup: true, 85 | }, 86 | Job::new("evm_log_extractor", "logs", None, move || { 87 | let evmc_client = evmc_client.clone(); 88 | let settings = settings.clone(); 89 | Box::pin(async move { 90 | run_logs_job(evmc_client, settings).await?; 91 | Ok(()) 92 | }) 93 | }), 94 | ) 95 | .await; 96 | } 97 | -------------------------------------------------------------------------------- /src/evm-log-extractor/tests/evm_log_extractor_it.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use candid::utils::ArgumentEncoder; 4 | use candid::{CandidType, decode_args, encode_args}; 5 | use evm_canister_client::client::{Log, Logs}; 6 | use evm_canister_client::{CanisterClient, CanisterClientResult, EvmCanisterClient}; 7 | use evm_log_extractor::job::logs::{LogsJobSettings, run_logs_job}; 8 | use serde::de::DeserializeOwned; 9 | use tokio::fs::{File, read_dir}; 10 | use tokio::io::{AsyncBufReadExt, BufReader}; 11 | 12 | #[derive(Clone)] 13 | struct MockCanisterClient { 14 | max_logs: usize, 15 | } 16 | 17 | impl CanisterClient for MockCanisterClient { 18 | async fn update(&self, _method: &str, _args: T) -> CanisterClientResult 19 | where 20 | T: ArgumentEncoder + Send + Sync, 21 | R: DeserializeOwned + CandidType, 22 | { 23 | panic!("should never call update") 24 | } 25 | 26 | async fn query(&self, method: &str, args: T) -> CanisterClientResult 27 | where 28 | T: ArgumentEncoder + Send + Sync, 29 | R: DeserializeOwned + CandidType, 30 | { 31 | if method == "ic_logs" { 32 | let (limit, offset): (usize, usize) = decode_args(&encode_args(args).unwrap()).unwrap(); 33 | 34 | let last_log = self.max_logs.min(offset + limit); 35 | let first_log = offset.min(last_log); 36 | 37 | let mut logs = vec![]; 38 | for i in first_log..last_log { 39 | logs.push(Log { 40 | log: format!("{i}"), 41 | offset: i, 42 | }); 43 | } 44 | 45 | let logs = serde_json::to_value(Ok::(Logs { 46 | logs, 47 | all_logs_count: self.max_logs, 48 | })) 49 | .unwrap(); 50 | 51 | Ok(serde_json::from_value(logs).unwrap()) 52 | } else { 53 | panic!("should never call query with method {}", method); 54 | } 55 | } 56 | } 57 | 58 | #[tokio::test] 59 | async fn test_extract_logs() { 60 | // Arrange 61 | let max_logs_in_canister = 1500; 62 | let max_logs_per_call = 1000; 63 | let mock_client = MockCanisterClient { 64 | max_logs: max_logs_in_canister, 65 | }; 66 | let evm_client = EvmCanisterClient::new(mock_client); 67 | 68 | let temp_dir = tempfile::tempdir().unwrap(); 69 | let logs_path = temp_dir.path().join("logs"); 70 | assert!(!logs_path.exists()); 71 | 72 | let logs_settings = LogsJobSettings { 73 | path: logs_path.to_str().unwrap().to_string(), 74 | max_logs_per_call, 75 | start_from_offset: Default::default(), 76 | }; 77 | 78 | // Act 1 - get all logs 79 | let log_file = { 80 | run_logs_job(evm_client.clone(), logs_settings.clone()) 81 | .await 82 | .unwrap(); 83 | 84 | // Assert the logs file is created 85 | assert!(logs_path.exists()); 86 | let log_file = read_dir(logs_path) 87 | .await 88 | .unwrap() 89 | .next_entry() 90 | .await 91 | .unwrap() 92 | .unwrap(); 93 | assert!(log_file.file_name().to_str().unwrap().ends_with(".log")); 94 | 95 | // Assert there is at least one log in the file (there should be at least the one from `admin_ic_permissions_add`) 96 | let previous_logs_from_file = file_to_vec(log_file.path()).await; 97 | assert_eq!(max_logs_per_call, previous_logs_from_file.len()); 98 | 99 | // Assert the next start offset is updated 100 | { 101 | let start_from_offset = *logs_settings.start_from_offset.lock().await; 102 | assert_eq!(max_logs_per_call, start_from_offset); 103 | } 104 | 105 | log_file 106 | }; 107 | 108 | // Act 2 - create a new log and get all logs 109 | { 110 | run_logs_job(evm_client.clone(), logs_settings.clone()) 111 | .await 112 | .unwrap(); 113 | 114 | // Assert the new logs are appended to the file 115 | let logs_from_file = file_to_vec(log_file.path()).await; 116 | assert_eq!(max_logs_in_canister, logs_from_file.len()); 117 | 118 | // Assert the next start offset is updated 119 | { 120 | let start_from_offset = *logs_settings.start_from_offset.lock().await; 121 | assert_eq!(max_logs_in_canister, start_from_offset); 122 | } 123 | 124 | // Assert the logs are not duplicated 125 | let mut unique_logs = logs_from_file.clone(); 126 | unique_logs.dedup(); 127 | assert_eq!(logs_from_file, unique_logs); 128 | } 129 | 130 | // Act 3 - there are no more logs in the canister 131 | { 132 | run_logs_job(evm_client.clone(), logs_settings.clone()) 133 | .await 134 | .unwrap(); 135 | 136 | // Assert the new logs are appended to the file 137 | let logs_from_file = file_to_vec(log_file.path()).await; 138 | assert_eq!(max_logs_in_canister, logs_from_file.len()); 139 | 140 | // Assert the next start offset is updated 141 | { 142 | let start_from_offset = *logs_settings.start_from_offset.lock().await; 143 | assert_eq!(max_logs_in_canister, start_from_offset); 144 | } 145 | } 146 | } 147 | 148 | async fn file_to_vec(file_path: PathBuf) -> Vec { 149 | let file = File::open(file_path).await.unwrap(); 150 | let reader = BufReader::new(file); 151 | let mut lines = reader.lines(); 152 | 153 | let mut logs_from_file = vec![]; 154 | while let Some(line) = lines.next_line().await.unwrap() { 155 | logs_from_file.push(line); 156 | } 157 | 158 | logs_from_file 159 | } 160 | -------------------------------------------------------------------------------- /src/icrc-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "icrc-client" 3 | 4 | authors.workspace = true 5 | categories.workspace = true 6 | description.workspace = true 7 | edition.workspace = true 8 | rust-version.workspace = true 9 | homepage.workspace = true 10 | include.workspace = true 11 | license.workspace = true 12 | repository.workspace = true 13 | version.workspace = true 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [dependencies] 18 | ic-canister-client = { workspace = true } 19 | candid.workspace = true 20 | ic-exports = { workspace = true, features = ["icrc", "ledger"] } 21 | thiserror.workspace = true 22 | serde.workspace = true 23 | -------------------------------------------------------------------------------- /src/icrc-client/src/client.rs: -------------------------------------------------------------------------------- 1 | use candid::{CandidType, Nat}; 2 | use ic_canister_client::{CanisterClient, CanisterClientResult}; 3 | use serde::Deserialize; 4 | 5 | use crate::Value; 6 | use crate::account::Account; 7 | use crate::allowance::{Allowance, AllowanceArgs}; 8 | use crate::approve::{ApproveArgs, ApproveError}; 9 | use crate::transfer::{TransferArg, TransferError}; 10 | use crate::transfer_from::{TransferFromArgs, TransferFromError}; 11 | 12 | #[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] 13 | pub struct StandardRecord { 14 | pub name: String, 15 | pub url: String, 16 | } 17 | 18 | /// ICRC-1/ICRC-2 client 19 | #[derive(Debug, Clone)] 20 | pub struct IcrcCanisterClient { 21 | client: C, 22 | } 23 | 24 | /// Implements the ICRC-1 and ICRC-2 client interfaces. 25 | /// 26 | /// The `IcrcCanisterClient` provides a set of methods to interact with an ICRC-1 and ICRC-2 compatible canister. 27 | /// It allows querying metadata, balances, and performing token transfers. 28 | impl IcrcCanisterClient { 29 | /// Create a ICRC Client 30 | /// 31 | /// # Arguments 32 | /// * `client` - The canister client. 33 | pub fn new(client: C) -> Self { 34 | Self { client } 35 | } 36 | 37 | // ============================== ICRC-1 ============================== 38 | 39 | pub async fn icrc1_metadata(&self) -> CanisterClientResult> { 40 | self.client.query("icrc1_metadata", ()).await 41 | } 42 | 43 | pub async fn icrc1_name(&self) -> CanisterClientResult { 44 | self.client.query("icrc1_name", ()).await 45 | } 46 | 47 | pub async fn icrc1_symbol(&self) -> CanisterClientResult { 48 | self.client.query("icrc1_symbol", ()).await 49 | } 50 | 51 | pub async fn icrc1_decimals(&self) -> CanisterClientResult { 52 | self.client.query("icrc1_decimals", ()).await 53 | } 54 | 55 | pub async fn icrc1_total_supply(&self) -> CanisterClientResult { 56 | self.client.query("icrc1_total_supply", ()).await 57 | } 58 | 59 | pub async fn icrc1_fee(&self) -> CanisterClientResult { 60 | self.client.query("icrc1_fee", ()).await 61 | } 62 | 63 | pub async fn icrc1_supported_standards(&self) -> CanisterClientResult> { 64 | self.client.query("icrc1_supported_standards", ()).await 65 | } 66 | 67 | pub async fn icrc1_minting_account(&self) -> CanisterClientResult> { 68 | self.client.query("icrc1_minting_account", ()).await 69 | } 70 | 71 | pub async fn icrc1_balance_of(&self, account: Account) -> CanisterClientResult { 72 | self.client.query("icrc1_balance_of", (account,)).await 73 | } 74 | 75 | /// Transfers the specified `amount` of tokens from the current subaccount to the 76 | /// `to` account. 77 | /// 78 | /// # Arguments 79 | /// 80 | /// - `to`: The account to transfer the tokens to. 81 | /// - `amount`: The amount of tokens to transfer. 82 | /// - `from_subaccount`: The optional subaccount to transfer the tokens from. 83 | /// 84 | /// # Returns 85 | /// 86 | /// A result containing the new balance of the `from_subaccount` after the 87 | /// transfer, or an error if the transfer failed. 88 | pub async fn icrc1_transfer( 89 | &self, 90 | transfer_args: TransferArg, 91 | ) -> CanisterClientResult> { 92 | self.client.update("icrc1_transfer", (transfer_args,)).await 93 | } 94 | 95 | // ============================== ICRC-2 ============================== 96 | 97 | /// Returns the current allowance for the specified `owner` and `spender`. 98 | /// The allowance is the amount of tokens that the `spender` is allowed to 99 | /// spend on behalf of the `owner`. 100 | pub async fn icrc2_allowance(&self, args: AllowanceArgs) -> CanisterClientResult { 101 | self.client.query("icrc2_allowance", (args,)).await 102 | } 103 | 104 | /// Approves the specified `spender` to spend up to `amount` on behalf of 105 | /// the `from_subaccount`. 106 | /// Returns the new allowance amount. 107 | pub async fn icrc2_approve( 108 | &self, 109 | approve: ApproveArgs, 110 | ) -> CanisterClientResult> { 111 | self.client.update("icrc2_approve", (approve,)).await 112 | } 113 | 114 | /// Transfers the specified `amount` of tokens from the `from` account 115 | /// to `to` account. 116 | pub async fn icrc2_transfer_from( 117 | &self, 118 | transfer_args: TransferFromArgs, 119 | ) -> CanisterClientResult> { 120 | self.client 121 | .update("icrc2_transfer_from", (transfer_args,)) 122 | .await 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/icrc-client/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod client; 2 | 3 | pub use client::{IcrcCanisterClient, StandardRecord}; 4 | pub use ic_exports::icrc_types::icrc::generic_value::Value; 5 | pub use ic_exports::icrc_types::icrc1::*; 6 | pub use ic_exports::icrc_types::icrc2::*; 7 | -------------------------------------------------------------------------------- /src/signature-verification-canister-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "signature-verification-canister-client" 3 | categories = ["cryptography::cryptocurrencies"] 4 | description = "Client for interacting with the signature Verification Canister" 5 | include = ["src/**/*", "../../LICENSE", "../../README.md"] 6 | 7 | authors.workspace = true 8 | homepage.workspace = true 9 | version.workspace = true 10 | rust-version.workspace = true 11 | edition.workspace = true 12 | license.workspace = true 13 | repository.workspace = true 14 | 15 | 16 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 17 | [features] 18 | default = [] 19 | ic-agent-client = ["ic-canister-client/ic-agent-client"] 20 | 21 | [dependencies] 22 | candid = { workspace = true } 23 | did = { workspace = true } 24 | ic-canister-client = { workspace = true } 25 | -------------------------------------------------------------------------------- /src/signature-verification-canister-client/README.md: -------------------------------------------------------------------------------- 1 | # signature-verification-canister-client 2 | -------------------------------------------------------------------------------- /src/signature-verification-canister-client/src/client.rs: -------------------------------------------------------------------------------- 1 | use candid::Principal; 2 | use did::build::BuildData; 3 | use did::error::SignatureVerificationError; 4 | use did::{H160, Transaction}; 5 | use ic_canister_client::{CanisterClient, CanisterClientResult}; 6 | 7 | /// This is the result type for all SignatureVerification canister calls. 8 | pub type SignatureVerificationResult = Result; 9 | 10 | /// A Signature Verification canister client. 11 | #[derive(Debug)] 12 | pub struct SignatureVerificationCanisterClient 13 | where 14 | C: CanisterClient, 15 | { 16 | /// The canister client. 17 | client: C, 18 | } 19 | 20 | impl SignatureVerificationCanisterClient { 21 | /// Create a new canister client. 22 | /// 23 | /// # Arguments 24 | /// * `client` - The canister client. 25 | pub fn new(client: C) -> Self { 26 | Self { client } 27 | } 28 | 29 | /// Verifies a transaction signature and returns the signing address 30 | pub async fn verify_signature( 31 | &self, 32 | transaction: &Transaction, 33 | ) -> CanisterClientResult> { 34 | self.client.query("verify_signature", (transaction,)).await 35 | } 36 | 37 | /// Add principal to the access control list 38 | pub async fn admin_add_principal_to_access_list( 39 | &self, 40 | principal: Principal, 41 | ) -> CanisterClientResult> { 42 | self.client 43 | .update("admin_add_principal_to_access_list", (principal,)) 44 | .await 45 | } 46 | 47 | /// Remove principal from the access control list 48 | pub async fn admin_remove_principal_from_access_list( 49 | &self, 50 | principal: Principal, 51 | ) -> CanisterClientResult> { 52 | self.client 53 | .update("admin_remove_principal_from_access_list", (principal,)) 54 | .await 55 | } 56 | 57 | /// Get the owner of the canister 58 | pub async fn get_owner(&self) -> CanisterClientResult { 59 | self.client.query("get_owner", ()).await 60 | } 61 | 62 | /// Set the owner of the canister 63 | pub async fn admin_set_owner( 64 | &self, 65 | principal: Principal, 66 | ) -> CanisterClientResult> { 67 | self.client.update("admin_set_owner", (principal,)).await 68 | } 69 | 70 | /// Get the access control list 71 | pub async fn get_access_list(&self) -> CanisterClientResult> { 72 | self.client.query("get_access_list", ()).await 73 | } 74 | 75 | /// Returns the build data of the canister. 76 | pub async fn get_canister_build_data(&self) -> CanisterClientResult { 77 | self.client.query("get_canister_build_data", ()).await 78 | } 79 | 80 | /// Get the evm canister for the transaction forwarding 81 | pub async fn get_evm_canister( 82 | &self, 83 | ) -> CanisterClientResult> { 84 | self.client.query("get_evm_canister", ()).await 85 | } 86 | 87 | /// Sets the evm canister for the transaction forwarding 88 | pub async fn admin_set_evm_canister( 89 | &self, 90 | principal: Principal, 91 | ) -> CanisterClientResult> { 92 | self.client 93 | .update("admin_set_evm_canister", (principal,)) 94 | .await 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/signature-verification-canister-client/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | 3 | pub use client::{SignatureVerificationCanisterClient, SignatureVerificationResult}; 4 | pub use ic_canister_client::*; 5 | --------------------------------------------------------------------------------