├── rustfmt.toml ├── .cargo └── config ├── .gitignore ├── canister_ids.json ├── examples ├── contracts │ ├── SimpleEvent.sol │ └── SimpleStorage.sol ├── res │ ├── SimpleEvent.abi │ ├── SimpleStorage.abi │ ├── SimpleStorage.bin │ ├── SimpleEvent.bin │ └── contract_token.code ├── endpoint.did ├── ic_web3.did └── endpoint.rs ├── .editorconfig ├── src ├── types │ ├── bytes_array.rs │ ├── transaction_id.rs │ ├── example-traces-str.rs │ ├── proof.rs │ ├── fee_history.rs │ ├── work.rs │ ├── bytes.rs │ ├── mod.rs │ ├── parity_peers.rs │ ├── parity_pending_transaction.rs │ ├── signed.rs │ ├── uint.rs │ ├── traces.rs │ ├── recovery.rs │ └── sync_state.rs ├── contract │ ├── ens │ │ ├── DefaultReverseResolver.json │ │ ├── mod.rs │ │ ├── reverse_resolver.rs │ │ ├── ENSRegistry.json │ │ ├── registry.rs │ │ └── PublicResolver.json │ ├── res │ │ ├── MyLibrary.json │ │ ├── Main.json │ │ └── token.json │ └── error.rs ├── transports │ ├── mod.rs │ ├── test.rs │ ├── batch.rs │ ├── either.rs │ ├── ic_http_client.rs │ ├── ic_http.rs │ └── http.rs ├── api │ ├── web3.rs │ ├── net.rs │ ├── eth_subscribe.rs │ ├── mod.rs │ ├── parity_accounts.rs │ ├── parity.rs │ └── personal.rs ├── error.rs ├── ic.rs ├── lib.rs └── helpers.rs ├── .github ├── dependabot.yml └── workflows │ ├── deny.yml │ ├── lint.yml │ └── rust.yml ├── dfx.json ├── LICENSE ├── Cargo.toml ├── README.md └── deny.toml /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | -------------------------------------------------------------------------------- /.cargo/config: -------------------------------------------------------------------------------- 1 | [target.wasm32-unknown-unknown] 2 | runner = 'wasm-bindgen-test-runner' 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | *.swp 4 | .idea/ 5 | .dfx 6 | *.old.did 7 | .env 8 | -------------------------------------------------------------------------------- /canister_ids.json: -------------------------------------------------------------------------------- 1 | { 2 | "endpoint": { 3 | "ic": "3ondx-siaaa-aaaam-abf3q-cai" 4 | }, 5 | "eth": { 6 | "ic": "kq5hz-ziaaa-aaaam-abnda-cai" 7 | } 8 | } -------------------------------------------------------------------------------- /examples/contracts/SimpleEvent.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.0; 2 | 3 | contract SimpleEvent { 4 | event Hello(address sender); 5 | 6 | function hello() public { 7 | emit Hello(msg.sender); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/res/SimpleEvent.abi: -------------------------------------------------------------------------------- 1 | [{"constant":false,"inputs":[],"name":"hello","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"sender","type":"address"}],"name":"Hello","type":"event"}] -------------------------------------------------------------------------------- /examples/res/SimpleStorage.abi: -------------------------------------------------------------------------------- 1 | [{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"}] -------------------------------------------------------------------------------- /examples/contracts/SimpleStorage.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.0; 2 | 3 | contract SimpleStorage { 4 | uint storedData; 5 | 6 | function set(uint x) { 7 | storedData = x; 8 | } 9 | 10 | function get() constant returns (uint) { 11 | return storedData; 12 | } 13 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | indent_style=space 4 | indent_size=4 5 | tab_width=4 6 | end_of_line=lf 7 | charset=utf-8 8 | trim_trailing_whitespace=true 9 | max_line_length=120 10 | insert_final_newline=true 11 | 12 | [.travis.yml] 13 | indent_style=space 14 | indent_size=2 15 | tab_width=8 16 | end_of_line=lf 17 | -------------------------------------------------------------------------------- /src/types/bytes_array.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// A wrapper type for array of bytes. 4 | /// 5 | /// Implements `Tokenizable` so can be used to retrieve data from `Solidity` contracts returning `byte8[]`. 6 | #[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Hash, Serialize)] 7 | pub struct BytesArray(pub Vec); 8 | -------------------------------------------------------------------------------- /src/types/transaction_id.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{BlockId, Index, H256}; 2 | 3 | /// Transaction Identifier 4 | #[derive(Clone, Debug, PartialEq)] 5 | pub enum TransactionId { 6 | /// By hash 7 | Hash(H256), 8 | /// By block and index 9 | Block(BlockId, Index), 10 | } 11 | 12 | impl From for TransactionId { 13 | fn from(hash: H256) -> Self { 14 | TransactionId::Hash(hash) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/res/SimpleStorage.bin: -------------------------------------------------------------------------------- 1 | 6060604052341561000c57fe5b5b60c68061001b6000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b11460445780636d4ce63c146061575bfe5b3415604b57fe5b605f60048080359060200190919050506084565b005b3415606857fe5b606e608f565b6040518082815260200191505060405180910390f35b806000819055505b50565b600060005490505b905600a165627a7a72305820616d9257b411248095799f7dd90840e3b07ae5c3b6083c0d78d14d826122d3c40029 -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: arrayvec 10 | versions: 11 | - 0.6.0 12 | - dependency-name: tokio 13 | versions: 14 | - 1.1.0 15 | - 1.2.0 16 | - 1.3.0 17 | - 1.4.0 18 | - dependency-name: hyper-proxy 19 | versions: 20 | - 0.9.0 21 | - dependency-name: ethereum-types 22 | versions: 23 | - 0.11.0 24 | -------------------------------------------------------------------------------- /examples/res/SimpleEvent.bin: -------------------------------------------------------------------------------- 1 | 6080604052348015600f57600080fd5b5060e98061001e6000396000f300608060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806319ff1d21146044575b600080fd5b348015604f57600080fd5b5060566058565b005b7fd282f389399565f3671145f5916e51652b60eee8e5c759293a2f5771b8ddfd2e33604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390a15600a165627a7a7230582055392010c17991d1f1d7b60a934d6038103da718d9fccc7ec270634df8352ad70029 -------------------------------------------------------------------------------- /examples/endpoint.did: -------------------------------------------------------------------------------- 1 | type HttpHeader = record { value : text; name : text }; 2 | type HttpResponse = record { 3 | status : nat; 4 | body : vec nat8; 5 | headers : vec HttpHeader; 6 | }; 7 | type Registered = record { chain_id : nat64; api_provider : text }; 8 | type Result = variant { Ok : text; Err : text }; 9 | type RpcTarget = variant { url_with_api_key : text; registered : Registered }; 10 | type TransformArgs = record { context : vec nat8; response : HttpResponse }; 11 | service : () -> { 12 | add_controller : (principal) -> (); 13 | json_rpc : (text, RpcTarget, opt nat64) -> (Result); 14 | register_api_key : (nat64, text, text) -> (); 15 | registrations : () -> (vec Registered) query; 16 | transform : (TransformArgs) -> (HttpResponse) query; 17 | } -------------------------------------------------------------------------------- /src/contract/ens/DefaultReverseResolver.json: -------------------------------------------------------------------------------- 1 | [{"inputs":[{"internalType":"contract ENS","name":"ensAddr","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"constant":true,"inputs":[],"name":"ens","outputs":[{"internalType":"contract ENS","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"string","name":"_name","type":"string"}],"name":"setName","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}] -------------------------------------------------------------------------------- /src/contract/ens/mod.rs: -------------------------------------------------------------------------------- 1 | //! Ethereum Name Service Interface 2 | //! 3 | //! This interface provides most functions implemented in ENS. 4 | //! With it you can resolve ethereum addresses to domain names, domain name to blockchain adresses and more! 5 | //! 6 | //! # Example 7 | //! ```no_run 8 | //! ##[tokio::main] 9 | //! async fn main() -> web3::Result<()> { 10 | //! use crate::web3::api::Namespace; 11 | //! 12 | //! let transport = web3::transports::Http::new("http://localhost:8545")?; 13 | //! 14 | //! let ens = web3::contract::ens::Ens::new(transport); 15 | //! 16 | //! let address = ens.eth_address("vitalik.eth").await.unwrap(); 17 | //! 18 | //! println!("Address: {:?}", address); 19 | //! 20 | //! Ok(()) 21 | //! } 22 | //! ``` 23 | 24 | mod eth_ens; 25 | pub mod public_resolver; 26 | pub mod registry; 27 | pub mod reverse_resolver; 28 | 29 | pub use eth_ens::Ens; 30 | -------------------------------------------------------------------------------- /src/contract/res/MyLibrary.json: -------------------------------------------------------------------------------- 1 | { 2 | "contractName": "MyLibrary", 3 | "abi": [ 4 | { 5 | "constant": true, 6 | "inputs": [], 7 | "name": "test", 8 | "outputs": [ 9 | { 10 | "name": "", 11 | "type": "uint256" 12 | } 13 | ], 14 | "payable": false, 15 | "stateMutability": "pure", 16 | "type": "function", 17 | "signature": "0xf8a8fd6d" 18 | } 19 | ], 20 | "bytecode": "0x60ad61002f600b82828239805160001a6073146000811461001f57610021565bfe5b5030600052607381538281f3fe73000000000000000000000000000000000000000030146080604052600436106056576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063f8a8fd6d14605b575b600080fd5b60616077565b6040518082815260200191505060405180910390f35b600061010090509056fea165627a7a72305820b50091adcb7ef9987dd8daa665cec572801bf8243530d70d52631f9d5ddb943e0029" 21 | } -------------------------------------------------------------------------------- /examples/ic_web3.did: -------------------------------------------------------------------------------- 1 | type HttpHeader = record { value : text; name : text }; 2 | type HttpResponse = record { 3 | status : nat; 4 | body : vec nat8; 5 | headers : vec HttpHeader; 6 | }; 7 | type Result = variant { Ok : text; Err : text }; 8 | type Result_1 = variant { Ok : nat64; Err : text }; 9 | type TransformArgs = record { context : vec nat8; response : HttpResponse }; 10 | service : { 11 | batch_request : () -> (Result); 12 | get_block : (nat64) -> (Result); 13 | get_canister_addr : () -> (Result); 14 | get_eth_balance : (text) -> (Result); 15 | get_eth_gas_price : () -> (Result); 16 | get_tx_count : (text) -> (Result_1); 17 | rpc_call : (text) -> (Result); 18 | send_eth : (text, nat64, opt nat64) -> (Result); 19 | send_token : (text, text, nat64, opt nat64) -> (Result); 20 | token_balance : (text, text) -> (Result); 21 | transform : (TransformArgs) -> (HttpResponse) query; 22 | } -------------------------------------------------------------------------------- /.github/workflows/deny.yml: -------------------------------------------------------------------------------- 1 | name: Cargo deny 2 | 3 | on: 4 | pull_request: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | push: 8 | branches: 9 | - master 10 | tags: 11 | - v* 12 | paths-ignore: 13 | - 'README.md' 14 | jobs: 15 | cargo-deny: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Cancel Previous Runs 19 | uses: styfle/cancel-workflow-action@0.4.1 20 | with: 21 | access_token: ${{ github.token }} 22 | - name: Checkout sources & submodules 23 | uses: actions/checkout@master 24 | with: 25 | fetch-depth: 5 26 | submodules: recursive 27 | - name: Cargo deny 28 | uses: EmbarkStudios/cargo-deny-action@v1 29 | with: 30 | command: "check --hide-inclusion-graph" 31 | -------------------------------------------------------------------------------- /dfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "canisters": { 3 | "eth": { 4 | "candid": "examples/ic_web3.did", 5 | "type": "custom", 6 | "wasm": "target/wasm32-unknown-unknown/release/examples/example_opt.wasm", 7 | "build": [ 8 | "cargo build --target wasm32-unknown-unknown --example example --release", 9 | "ic-cdk-optimizer target/wasm32-unknown-unknown/release/examples/example.wasm -o target/wasm32-unknown-unknown/release/examples/example_opt.wasm" 10 | ] 11 | }, 12 | "endpoint": { 13 | "candid": "examples/endpoint.did", 14 | "type": "custom", 15 | "wasm": "target/wasm32-unknown-unknown/release/examples/endpoint_opt.wasm", 16 | "build": [ 17 | "cargo build --target wasm32-unknown-unknown --example endpoint --release", 18 | "ic-cdk-optimizer target/wasm32-unknown-unknown/release/examples/endpoint.wasm -o target/wasm32-unknown-unknown/release/examples/endpoint_opt.wasm" 19 | ] 20 | } 21 | }, 22 | "networks": { 23 | "local": { 24 | "bind": "127.0.0.1:8000", 25 | "type": "ephemeral" 26 | } 27 | }, 28 | "version": 1 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Check style 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | tags: 9 | - v* 10 | paths-ignore: 11 | - 'README.md' 12 | jobs: 13 | ## Check stage 14 | check-fmt: 15 | name: Check RustFmt 16 | runs-on: ubuntu-latest 17 | env: 18 | RUST_BACKTRACE: full 19 | steps: 20 | - name: Cancel Previous Runs 21 | uses: styfle/cancel-workflow-action@0.4.1 22 | with: 23 | access_token: ${{ github.token }} 24 | - name: Checkout sources & submodules 25 | uses: actions/checkout@master 26 | with: 27 | fetch-depth: 5 28 | submodules: recursive 29 | - name: Add rustfmt 30 | run: rustup component add rustfmt 31 | - name: rust-fmt check 32 | uses: actions-rs/cargo@master 33 | with: 34 | command: fmt 35 | args: --all -- --check --config merge_imports=true 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tomasz Drwięga 4 | Copyright (c) 2022 Rocklabs Team 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/contract/res/Main.json: -------------------------------------------------------------------------------- 1 | { 2 | "contractName": "Main", 3 | "abi": [ 4 | { 5 | "inputs": [], 6 | "payable": false, 7 | "stateMutability": "nonpayable", 8 | "type": "constructor", 9 | "signature": "constructor" 10 | }, 11 | { 12 | "constant": false, 13 | "inputs": [], 14 | "name": "test", 15 | "outputs": [ 16 | { 17 | "name": "", 18 | "type": "uint256" 19 | } 20 | ], 21 | "payable": false, 22 | "stateMutability": "nonpayable", 23 | "type": "function", 24 | "signature": "0xf8a8fd6d" 25 | } 26 | ], 27 | "bytecode": "0x608060405234801561001057600080fd5b5061013f806100206000396000f3fe608060405260043610610041576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063f8a8fd6d14610046575b600080fd5b34801561005257600080fd5b5061005b610071565b6040518082815260200191505060405180910390f35b600073__MyLibrary_____________________________63f8a8fd6d6040518163ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040160206040518083038186803b1580156100d357600080fd5b505af41580156100e7573d6000803e3d6000fd5b505050506040513d60208110156100fd57600080fd5b810190808051906020019092919050505090509056fea165627a7a72305820580d3776b3d132142f431e141a2e20bd4dd4907fa304feea7b604e8f39ed59520029" 28 | } -------------------------------------------------------------------------------- /src/types/example-traces-str.rs: -------------------------------------------------------------------------------- 1 | r#"[{ 2 | "output": "0x", 3 | "stateDiff": { 4 | "0x5df9b87991262f6ba471f09758cde1c0fc1de734": { 5 | "balance": { 6 | "+": "0x7a69" 7 | }, 8 | "code": { 9 | "+": "0x" 10 | }, 11 | "nonce": { 12 | "+": "0x0" 13 | }, 14 | "storage": {} 15 | }, 16 | "0xa1e4380a3b1f749673e270229993ee55f35663b4": { 17 | "balance": { 18 | "*": { 19 | "from": "0x6c6b935b8bbd400000", 20 | "to": "0x6c5d01021be7168597" 21 | } 22 | }, 23 | "code": "=", 24 | "nonce": { 25 | "*": { 26 | "from": "0x0", 27 | "to": "0x1" 28 | } 29 | }, 30 | "storage": {} 31 | }, 32 | "0xe6a7a1d47ff21b6321162aea7c6cb457d5476bca": { 33 | "balance": { 34 | "*": { 35 | "from": "0xf3426785a8ab466000", 36 | "to": "0xf350f9df18816f6000" 37 | } 38 | }, 39 | "code": "=", 40 | "nonce": "=", 41 | "storage": {} 42 | } 43 | }, 44 | "trace": [ 45 | { 46 | "action": { 47 | "callType": "call", 48 | "from": "0xa1e4380a3b1f749673e270229993ee55f35663b4", 49 | "gas": "0x0", 50 | "input": "0x", 51 | "to": "0x5df9b87991262f6ba471f09758cde1c0fc1de734", 52 | "value": "0x7a69" 53 | }, 54 | "result": { 55 | "gasUsed": "0x0", 56 | "output": "0x" 57 | }, 58 | "subtraces": 0, 59 | "traceAddress": [], 60 | "type": "call" 61 | } 62 | ], 63 | "transactionHash": "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060", 64 | "vmTrace": { 65 | "code": "0x", 66 | "ops": [] 67 | } 68 | }]"# 69 | -------------------------------------------------------------------------------- /src/transports/mod.rs: -------------------------------------------------------------------------------- 1 | //! Supported Ethereum JSON-RPC transports. 2 | 3 | pub mod batch; 4 | 5 | pub use self::batch::Batch; 6 | pub mod either; 7 | pub use self::either::Either; 8 | 9 | #[cfg(any(feature = "http", feature = "http-rustls"))] 10 | pub mod http; 11 | #[cfg(any(feature = "http", feature = "http-rustls"))] 12 | pub use self::http::Http; 13 | 14 | pub mod ic_http_client; 15 | pub use self::ic_http_client::ICHttpClient; 16 | pub mod ic_http; 17 | pub use self::ic_http::ICHttp; 18 | 19 | #[cfg(any(feature = "ws-tokio", feature = "ws-async-std"))] 20 | pub mod ws; 21 | #[cfg(any(feature = "ws-tokio", feature = "ws-async-std"))] 22 | pub use self::ws::WebSocket; 23 | 24 | #[cfg(feature = "ipc-tokio")] 25 | pub mod ipc; 26 | #[cfg(feature = "ipc-tokio")] 27 | pub use self::ipc::Ipc; 28 | 29 | #[cfg(any(feature = "test", test))] 30 | pub mod test; 31 | 32 | #[cfg(feature = "url")] 33 | impl From for crate::Error { 34 | fn from(err: url::ParseError) -> Self { 35 | use crate::error::TransportError; 36 | crate::Error::Transport(TransportError::Message(format!("failed to parse url: {}", err))) 37 | } 38 | } 39 | 40 | #[cfg(feature = "async-native-tls")] 41 | impl From for crate::Error { 42 | fn from(err: async_native_tls::Error) -> Self { 43 | use crate::error::TransportError; 44 | crate::Error::Transport(TransportError::Message(format!("{:?}", err))) 45 | } 46 | } 47 | 48 | #[cfg(feature = "eip-1193")] 49 | pub mod eip_1193; 50 | -------------------------------------------------------------------------------- /src/types/proof.rs: -------------------------------------------------------------------------------- 1 | use crate::types::Bytes; 2 | use ethereum_types::{H256, U256}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | ///Proof struct returned by eth_getProof method 6 | /// 7 | /// https://eips.ethereum.org/EIPS/eip-1186 8 | #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] 9 | pub struct Proof { 10 | /// the balance of the account. See eth_getBalance 11 | pub balance: U256, 12 | /// hash of the code of the account 13 | #[serde(rename = "codeHash")] 14 | pub code_hash: H256, 15 | /// nonce of the account. See eth_getTransactionCount 16 | pub nonce: U256, 17 | /// SHA3 of the StorageRoot. 18 | #[serde(rename = "storageHash")] 19 | pub storage_hash: H256, 20 | /// Array of rlp-serialized MerkleTree-Nodes, starting with the stateRoot-Node, following the path of the SHA3 (address) as key. 21 | #[serde(rename = "accountProof")] 22 | pub account_proof: Vec, 23 | /// Array of storage-entries as requested 24 | #[serde(rename = "storageProof")] 25 | pub storage_proof: Vec, 26 | } 27 | 28 | /// A key-value pair and it's state proof. 29 | #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] 30 | pub struct StorageProof { 31 | /// the requested storage key 32 | pub key: U256, 33 | /// the storage value 34 | pub value: U256, 35 | /// Array of rlp-serialized MerkleTree-Nodes, starting with the storageHash-Node, following the path of the SHA3 (key) as path. 36 | pub proof: Vec, 37 | } 38 | -------------------------------------------------------------------------------- /src/api/web3.rs: -------------------------------------------------------------------------------- 1 | //! `Web3` namespace 2 | 3 | use crate::{ 4 | api::Namespace, 5 | helpers::{self, CallFuture}, 6 | types::{Bytes, H256}, 7 | Transport, 8 | }; 9 | 10 | /// `Web3` namespace 11 | #[derive(Debug, Clone)] 12 | pub struct Web3 { 13 | transport: T, 14 | } 15 | 16 | impl Namespace for Web3 { 17 | fn new(transport: T) -> Self 18 | where 19 | Self: Sized, 20 | { 21 | Web3 { transport } 22 | } 23 | 24 | fn transport(&self) -> &T { 25 | &self.transport 26 | } 27 | } 28 | 29 | impl Web3 { 30 | /// Returns client version 31 | pub fn client_version(&self) -> CallFuture { 32 | CallFuture::new(self.transport.execute("web3_clientVersion", vec![])) 33 | } 34 | 35 | /// Returns sha3 of the given data 36 | pub fn sha3(&self, bytes: Bytes) -> CallFuture { 37 | let bytes = helpers::serialize(&bytes); 38 | CallFuture::new(self.transport.execute("web3_sha3", vec![bytes])) 39 | } 40 | } 41 | 42 | #[cfg(test)] 43 | mod tests { 44 | use super::Web3; 45 | use crate::{api::Namespace, rpc::Value, types::H256}; 46 | use hex_literal::hex; 47 | 48 | rpc_test! ( 49 | Web3:client_version => "web3_clientVersion"; 50 | Value::String("Test123".into()) => "Test123" 51 | ); 52 | 53 | rpc_test! ( 54 | Web3:sha3, hex!("01020304") 55 | => 56 | "web3_sha3", vec![r#""0x01020304""#]; 57 | Value::String("0x0000000000000000000000000000000000000000000000000000000000000123".into()) => H256::from_low_u64_be(0x123) 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/api/net.rs: -------------------------------------------------------------------------------- 1 | //! `Net` namespace 2 | 3 | use crate::{api::Namespace, helpers::CallFuture, types::U256, Transport}; 4 | 5 | /// `Net` namespace 6 | #[derive(Debug, Clone)] 7 | pub struct Net { 8 | transport: T, 9 | } 10 | 11 | impl Namespace for Net { 12 | fn new(transport: T) -> Self 13 | where 14 | Self: Sized, 15 | { 16 | Net { transport } 17 | } 18 | 19 | fn transport(&self) -> &T { 20 | &self.transport 21 | } 22 | } 23 | 24 | impl Net { 25 | /// Returns the network id. 26 | pub fn version(&self) -> CallFuture { 27 | CallFuture::new(self.transport.execute("net_version", vec![])) 28 | } 29 | 30 | /// Returns number of peers connected to node. 31 | pub fn peer_count(&self) -> CallFuture { 32 | CallFuture::new(self.transport.execute("net_peerCount", vec![])) 33 | } 34 | 35 | /// Whether the node is listening for network connections 36 | pub fn is_listening(&self) -> CallFuture { 37 | CallFuture::new(self.transport.execute("net_listening", vec![])) 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::Net; 44 | use crate::{api::Namespace, rpc::Value, types::U256}; 45 | 46 | rpc_test! ( 47 | Net:version => "net_version"; 48 | Value::String("Test123".into()) => "Test123" 49 | ); 50 | 51 | rpc_test! ( 52 | Net:peer_count => "net_peerCount"; 53 | Value::String("0x123".into()) => U256::from(0x123) 54 | ); 55 | 56 | rpc_test! ( 57 | Net:is_listening => "net_listening"; 58 | Value::Bool(true) => true 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /src/types/fee_history.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{BlockNumber, U256}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// The fee history type returned from `eth_feeHistory` call. 5 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct FeeHistory { 8 | /// Lowest number block of the returned range. 9 | pub oldest_block: BlockNumber, 10 | /// A vector of block base fees per gas. This includes the next block after the newest of the returned range, because this value can be derived from the newest block. Zeroes are returned for pre-EIP-1559 blocks. 11 | pub base_fee_per_gas: Vec, 12 | /// A vector of block gas used ratios. These are calculated as the ratio of gas used and gas limit. 13 | pub gas_used_ratio: Vec, 14 | /// A vector of effective priority fee per gas data points from a single block. All zeroes are returned if the block is empty. Returned only if requested. 15 | pub reward: Option>>, 16 | } 17 | 18 | #[cfg(test)] 19 | mod tests { 20 | use super::*; 21 | 22 | #[test] 23 | fn fee_history() { 24 | let fee_history = FeeHistory { 25 | oldest_block: BlockNumber::Number(123456.into()), 26 | base_fee_per_gas: vec![100.into(), 110.into()], 27 | gas_used_ratio: vec![1.0, 2.0, 3.0], 28 | reward: None, 29 | }; 30 | 31 | let serialized = serde_json::to_value(fee_history.clone()).unwrap(); 32 | assert_eq!(serialized.to_string(), "{\"baseFeePerGas\":[\"0x64\",\"0x6e\"],\"gasUsedRatio\":[1.0,2.0,3.0],\"oldestBlock\":\"0x1e240\",\"reward\":null}"); 33 | 34 | let deserialized = serde_json::from_value(serialized).unwrap(); 35 | assert_eq!(fee_history, deserialized); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/types/work.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{H256, U256}; 2 | use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; 3 | use serde_json::{self, Value}; 4 | 5 | /// Miner's work package 6 | #[derive(Debug, PartialEq, Eq)] 7 | pub struct Work { 8 | /// The proof-of-work hash. 9 | pub pow_hash: H256, 10 | /// The seed hash. 11 | pub seed_hash: H256, 12 | /// The target. 13 | pub target: H256, 14 | /// The block number: this isn't always stored. 15 | pub number: Option, 16 | } 17 | 18 | impl<'a> Deserialize<'a> for Work { 19 | fn deserialize(deserializer: D) -> Result 20 | where 21 | D: Deserializer<'a>, 22 | { 23 | let v: Value = Deserialize::deserialize(deserializer)?; 24 | 25 | let (pow_hash, seed_hash, target, number) = serde_json::from_value::<(H256, H256, H256, u64)>(v.clone()) 26 | .map(|(pow_hash, seed_hash, target, number)| (pow_hash, seed_hash, target, Some(number))) 27 | .or_else(|_| { 28 | serde_json::from_value::<(H256, H256, H256)>(v) 29 | .map(|(pow_hash, seed_hash, target)| (pow_hash, seed_hash, target, None)) 30 | }) 31 | .map_err(|e| D::Error::custom(format!("Cannot deserialize Work: {:?}", e)))?; 32 | 33 | Ok(Work { 34 | pow_hash, 35 | seed_hash, 36 | target, 37 | number, 38 | }) 39 | } 40 | } 41 | 42 | impl Serialize for Work { 43 | fn serialize(&self, s: S) -> Result 44 | where 45 | S: Serializer, 46 | { 47 | match self.number.as_ref() { 48 | Some(num) => (&self.pow_hash, &self.seed_hash, &self.target, U256::from(*num)).serialize(s), 49 | None => (&self.pow_hash, &self.seed_hash, &self.target).serialize(s), 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/types/bytes.rs: -------------------------------------------------------------------------------- 1 | use serde::{ 2 | de::{Error, Unexpected, Visitor}, 3 | Deserialize, Deserializer, Serialize, Serializer, 4 | }; 5 | use std::fmt; 6 | 7 | /// Raw bytes wrapper 8 | #[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] 9 | pub struct Bytes(pub Vec); 10 | 11 | impl>> From for Bytes { 12 | fn from(data: T) -> Self { 13 | Bytes(data.into()) 14 | } 15 | } 16 | 17 | impl Serialize for Bytes { 18 | fn serialize(&self, serializer: S) -> Result 19 | where 20 | S: Serializer, 21 | { 22 | let mut serialized = "0x".to_owned(); 23 | serialized.push_str(&hex::encode(&self.0)); 24 | serializer.serialize_str(serialized.as_ref()) 25 | } 26 | } 27 | 28 | impl<'a> Deserialize<'a> for Bytes { 29 | fn deserialize(deserializer: D) -> Result 30 | where 31 | D: Deserializer<'a>, 32 | { 33 | deserializer.deserialize_identifier(BytesVisitor) 34 | } 35 | } 36 | 37 | struct BytesVisitor; 38 | 39 | impl<'a> Visitor<'a> for BytesVisitor { 40 | type Value = Bytes; 41 | 42 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 43 | write!(formatter, "a 0x-prefixed hex-encoded vector of bytes") 44 | } 45 | 46 | fn visit_str(self, value: &str) -> Result 47 | where 48 | E: Error, 49 | { 50 | if value.len() >= 2 && &value[0..2] == "0x" { 51 | let bytes = hex::decode(&value[2..]).map_err(|e| Error::custom(format!("Invalid hex: {}", e)))?; 52 | Ok(Bytes(bytes)) 53 | } else { 54 | Err(Error::invalid_value(Unexpected::Str(value), &"0x prefix")) 55 | } 56 | } 57 | 58 | fn visit_string(self, value: String) -> Result 59 | where 60 | E: Error, 61 | { 62 | self.visit_str(value.as_ref()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/types/mod.rs: -------------------------------------------------------------------------------- 1 | //! Web3 Types 2 | 3 | mod block; 4 | mod bytes; 5 | mod bytes_array; 6 | mod fee_history; 7 | mod log; 8 | mod parity_peers; 9 | mod parity_pending_transaction; 10 | mod proof; 11 | mod recovery; 12 | mod signed; 13 | mod sync_state; 14 | mod trace_filtering; 15 | mod traces; 16 | mod transaction; 17 | mod transaction_id; 18 | mod transaction_request; 19 | mod txpool; 20 | mod uint; 21 | mod work; 22 | 23 | pub use self::{ 24 | block::{Block, BlockHeader, BlockId, BlockNumber}, 25 | bytes::Bytes, 26 | bytes_array::BytesArray, 27 | fee_history::FeeHistory, 28 | log::{Filter, FilterBuilder, Log}, 29 | parity_peers::{ 30 | EthProtocolInfo, ParityPeerInfo, ParityPeerType, PeerNetworkInfo, PeerProtocolsInfo, PipProtocolInfo, 31 | }, 32 | parity_pending_transaction::{ 33 | FilterCondition, ParityPendingTransactionFilter, ParityPendingTransactionFilterBuilder, ToFilter, 34 | }, 35 | proof::Proof, 36 | recovery::{ParseSignatureError, Recovery, RecoveryMessage}, 37 | signed::{SignedData, SignedTransaction, TransactionParameters}, 38 | sync_state::{SyncInfo, SyncState}, 39 | trace_filtering::{ 40 | Action, ActionType, Call, CallResult, CallType, Create, CreateResult, Res, Reward, RewardType, Suicide, Trace, 41 | TraceFilter, TraceFilterBuilder, 42 | }, 43 | traces::{ 44 | AccountDiff, BlockTrace, ChangedType, Diff, MemoryDiff, StateDiff, StorageDiff, TraceType, TransactionTrace, 45 | VMExecutedOperation, VMOperation, VMTrace, 46 | }, 47 | transaction::{AccessList, AccessListItem, RawTransaction, Receipt as TransactionReceipt, Transaction}, 48 | transaction_id::TransactionId, 49 | transaction_request::{CallRequest, TransactionCondition, TransactionRequest}, 50 | txpool::{TxpoolContentInfo, TxpoolInspectInfo, TxpoolStatus}, 51 | uint::{H128, H160, H2048, H256, H512, H520, H64, U128, U256, U64}, 52 | work::Work, 53 | }; 54 | 55 | /// Address 56 | pub type Address = H160; 57 | /// Index in block 58 | pub type Index = U64; 59 | -------------------------------------------------------------------------------- /src/contract/error.rs: -------------------------------------------------------------------------------- 1 | //! Contract call/query error. 2 | 3 | use crate::error::Error as ApiError; 4 | use derive_more::{Display, From}; 5 | use ethabi::Error as EthError; 6 | 7 | /// Contract error. 8 | #[derive(Debug, Display, From)] 9 | pub enum Error { 10 | /// invalid output type requested by the caller 11 | #[display(fmt = "Invalid output type: {}", _0)] 12 | InvalidOutputType(String), 13 | /// eth abi error 14 | #[display(fmt = "Abi error: {}", _0)] 15 | Abi(EthError), 16 | /// Rpc error 17 | #[display(fmt = "Api error: {}", _0)] 18 | Api(ApiError), 19 | /// An error during deployment. 20 | #[display(fmt = "Deployment error: {}", _0)] 21 | Deployment(crate::contract::deploy::Error), 22 | /// Contract does not support this interface. 23 | InterfaceUnsupported, 24 | } 25 | 26 | impl std::error::Error for Error { 27 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 28 | match *self { 29 | Error::InvalidOutputType(_) => None, 30 | Error::Abi(ref e) => Some(e), 31 | Error::Api(ref e) => Some(e), 32 | Error::Deployment(ref e) => Some(e), 33 | Error::InterfaceUnsupported => None, 34 | } 35 | } 36 | } 37 | 38 | pub mod deploy { 39 | use crate::{error::Error as ApiError, types::H256}; 40 | use derive_more::{Display, From}; 41 | 42 | /// Contract deployment error. 43 | #[derive(Debug, Display, From)] 44 | pub enum Error { 45 | /// eth abi error 46 | #[display(fmt = "Abi error: {}", _0)] 47 | Abi(ethabi::Error), 48 | /// Rpc error 49 | #[display(fmt = "Api error: {}", _0)] 50 | Api(ApiError), 51 | /// Contract deployment failed 52 | #[display(fmt = "Failure during deployment.Tx hash: {:?}", _0)] 53 | ContractDeploymentFailure(H256), 54 | } 55 | 56 | impl std::error::Error for Error { 57 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 58 | match *self { 59 | Error::Abi(ref e) => Some(e), 60 | Error::Api(ref e) => Some(e), 61 | Error::ContractDeploymentFailure(_) => None, 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/types/parity_peers.rs: -------------------------------------------------------------------------------- 1 | //! Types for getting peer information 2 | use ethereum_types::U256; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// Stores active peer count, connected count, max connected peers 6 | /// and a list of peers for parity node 7 | #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] 8 | pub struct ParityPeerType { 9 | /// number of active peers 10 | pub active: usize, 11 | /// number of connected peers 12 | pub connected: usize, 13 | /// maximum number of peers that can connect 14 | pub max: u32, 15 | /// list of all peers with details 16 | pub peers: Vec, 17 | } 18 | 19 | /// details of a peer 20 | #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] 21 | pub struct ParityPeerInfo { 22 | /// id of peer 23 | pub id: Option, 24 | /// name of peer if set by user 25 | pub name: String, 26 | /// sync logic for protocol messaging 27 | pub caps: Vec, 28 | /// remote address and local address 29 | pub network: PeerNetworkInfo, 30 | /// protocol version of peer 31 | pub protocols: PeerProtocolsInfo, 32 | } 33 | 34 | /// ip address of both local and remote 35 | #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] 36 | #[serde(rename_all = "camelCase")] 37 | pub struct PeerNetworkInfo { 38 | /// remote peer address 39 | pub remote_address: String, 40 | /// local peer address 41 | pub local_address: String, 42 | } 43 | 44 | /// chain protocol info 45 | #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] 46 | pub struct PeerProtocolsInfo { 47 | /// chain info 48 | pub eth: Option, 49 | /// chain info 50 | pub pip: Option, 51 | } 52 | 53 | /// eth chain version, difficulty, and head of chain 54 | /// which soft fork? Olympic, Frontier, Homestead, Metropolis, Serenity, etc. 55 | #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] 56 | pub struct EthProtocolInfo { 57 | /// version 58 | pub version: u32, 59 | /// difficulty 60 | pub difficulty: Option, 61 | /// head of chain 62 | pub head: String, 63 | } 64 | 65 | /// pip version, difficulty, and head 66 | #[derive(Serialize, PartialEq, Clone, Deserialize, Debug)] 67 | pub struct PipProtocolInfo { 68 | /// version 69 | pub version: u32, 70 | /// difficulty 71 | pub difficulty: U256, 72 | /// head of chain 73 | pub head: String, 74 | } 75 | -------------------------------------------------------------------------------- /src/transports/test.rs: -------------------------------------------------------------------------------- 1 | //! Test Transport 2 | 3 | use crate::{ 4 | error::{self, Error}, 5 | helpers, rpc, RequestId, Transport, 6 | }; 7 | use futures::future::{self, BoxFuture, FutureExt}; 8 | use std::{cell::RefCell, collections::VecDeque, rc::Rc}; 9 | 10 | type Result = BoxFuture<'static, error::Result>; 11 | 12 | /// Test Transport 13 | #[derive(Debug, Default, Clone)] 14 | pub struct TestTransport { 15 | asserted: usize, 16 | requests: Rc)>>>, 17 | responses: Rc>>, 18 | } 19 | 20 | impl Transport for TestTransport { 21 | type Out = Result; 22 | 23 | fn prepare(&self, method: &str, params: Vec) -> (RequestId, rpc::Call) { 24 | let request = helpers::build_request(1, method, params.clone()); 25 | self.requests.borrow_mut().push((method.into(), params)); 26 | (self.requests.borrow().len(), request) 27 | } 28 | 29 | fn send(&self, id: RequestId, request: rpc::Call) -> Result { 30 | future::ready(match self.responses.borrow_mut().pop_front() { 31 | Some(response) => Ok(response), 32 | None => { 33 | println!("Unexpected request (id: {:?}): {:?}", id, request); 34 | Err(Error::Unreachable) 35 | } 36 | }) 37 | .boxed() 38 | } 39 | } 40 | 41 | impl TestTransport { 42 | /// Set response 43 | pub fn set_response(&mut self, value: rpc::Value) { 44 | *self.responses.borrow_mut() = vec![value].into(); 45 | } 46 | 47 | /// Add response 48 | pub fn add_response(&mut self, value: rpc::Value) { 49 | self.responses.borrow_mut().push_back(value); 50 | } 51 | 52 | /// Assert request 53 | pub fn assert_request(&mut self, method: &str, params: &[String]) { 54 | let idx = self.asserted; 55 | self.asserted += 1; 56 | 57 | let (m, p) = self.requests.borrow().get(idx).expect("Expected result.").clone(); 58 | assert_eq!(&m, method); 59 | let p: Vec = p.into_iter().map(|p| serde_json::to_string(&p).unwrap()).collect(); 60 | assert_eq!(p, params); 61 | } 62 | 63 | /// Assert no more requests 64 | pub fn assert_no_more_requests(&self) { 65 | let requests = self.requests.borrow(); 66 | assert_eq!( 67 | self.asserted, 68 | requests.len(), 69 | "Expected no more requests, got: {:?}", 70 | &requests[self.asserted..] 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/contract/ens/reverse_resolver.rs: -------------------------------------------------------------------------------- 1 | //! Reverse Resolver ENS contract interface. 2 | 3 | use crate::{ 4 | api::Eth, 5 | contract::{Contract, Options}, 6 | signing::NameHash, 7 | types::{Address, TransactionId}, 8 | Transport, 9 | }; 10 | 11 | type ContractError = crate::contract::Error; 12 | 13 | /// Reverse resolution in ENS - the process of mapping from an Ethereum address (eg, 0x1234...) to an ENS name - is handled using a special namespace, *.addr.reverse*. 14 | /// A special-purpose registrar controls this namespace and allocates subdomains to any caller based on their address. 15 | /// 16 | /// For example, the account *0x314159265dd8dbb310642f98f50c066173c1259b* can claim *314159265dd8dbb310642f98f50c066173c1259b.addr.reverse*. After doing so, it can configure a resolver and expose metadata, such as a canonical ENS name for this address. 17 | /// 18 | /// The reverse registrar provides functions to claim a reverse record, as well as a convenience function to configure the record as it's most commonly used, as a way of specifying a canonical name for an address. 19 | /// 20 | /// The reverse registrar is specified in [EIP 181](https://eips.ethereum.org/EIPS/eip-181). 21 | /// 22 | /// [Source](https://github.com/ensdomains/ens/blob/master/contracts/ReverseRegistrar.sol) 23 | #[derive(Debug, Clone)] 24 | pub struct ReverseResolver { 25 | contract: Contract, 26 | } 27 | 28 | impl ReverseResolver { 29 | /// Creates new instance of [`ReverseResolver`] given contract address. 30 | pub fn new(eth: Eth, resolver_addr: Address) -> Self { 31 | // See https://github.com/ensdomains/ens-contracts for up to date contracts. 32 | let bytes = include_bytes!("DefaultReverseResolver.json"); 33 | 34 | let contract = Contract::from_json(eth, resolver_addr, bytes).expect("Contract Creation Failed"); 35 | 36 | Self { contract } 37 | } 38 | } 39 | 40 | impl ReverseResolver { 41 | /// Returns the canonical ENS name associated with the provided node. 42 | pub async fn canonical_name(&self, node: NameHash) -> Result { 43 | let options = Options::default(); 44 | 45 | self.contract.query("name", node, None, options, None).await 46 | } 47 | 48 | /// Sets the canonical ENS name for the provided node to name. 49 | pub async fn set_canonical_name( 50 | &self, 51 | from: Address, 52 | node: NameHash, 53 | name: String, 54 | ) -> Result { 55 | let options = Options::default(); 56 | 57 | let id = self.contract.call("setName", (node, name), from, options).await?; 58 | 59 | Ok(TransactionId::Hash(id)) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/transports/batch.rs: -------------------------------------------------------------------------------- 1 | //! Batching Transport 2 | 3 | use crate::{ 4 | error::{self, Error}, 5 | rpc, BatchTransport, RequestId, Transport, 6 | }; 7 | use futures::{ 8 | channel::oneshot, 9 | task::{Context, Poll}, 10 | Future, FutureExt, 11 | }; 12 | use parking_lot::Mutex; 13 | use std::{collections::BTreeMap, pin::Pin, sync::Arc}; 14 | 15 | type Pending = oneshot::Sender>; 16 | type PendingRequests = Arc>>; 17 | 18 | /// Transport allowing to batch queries together. 19 | #[derive(Debug, Clone)] 20 | pub struct Batch { 21 | transport: T, 22 | pending: PendingRequests, 23 | batch: Arc>>, 24 | } 25 | 26 | impl Batch 27 | where 28 | T: BatchTransport, 29 | { 30 | /// Creates new Batch transport given existing transport supporing batch requests. 31 | pub fn new(transport: T) -> Self { 32 | Batch { 33 | transport, 34 | pending: Default::default(), 35 | batch: Default::default(), 36 | } 37 | } 38 | 39 | /// Sends all requests as a batch. 40 | pub fn submit_batch(&self) -> impl Future>>> { 41 | let batch = std::mem::take(&mut *self.batch.lock()); 42 | let ids = batch.iter().map(|&(id, _)| id).collect::>(); 43 | 44 | let batch = self.transport.send_batch(batch); 45 | let pending = self.pending.clone(); 46 | 47 | async move { 48 | let res = batch.await; 49 | let mut pending = pending.lock(); 50 | for (idx, request_id) in ids.into_iter().enumerate() { 51 | if let Some(rx) = pending.remove(&request_id) { 52 | // Ignore sending error 53 | let _ = match res { 54 | Ok(ref results) if results.len() > idx => rx.send(results[idx].clone()), 55 | Err(ref err) => rx.send(Err(err.clone())), 56 | _ => rx.send(Err(Error::Internal)), 57 | }; 58 | } 59 | } 60 | res 61 | } 62 | } 63 | } 64 | 65 | impl Transport for Batch 66 | where 67 | T: BatchTransport, 68 | { 69 | type Out = SingleResult; 70 | 71 | fn prepare(&self, method: &str, params: Vec) -> (RequestId, rpc::Call) { 72 | self.transport.prepare(method, params) 73 | } 74 | 75 | fn send(&self, id: RequestId, request: rpc::Call) -> Self::Out { 76 | let (tx, rx) = oneshot::channel(); 77 | self.pending.lock().insert(id, tx); 78 | self.batch.lock().push((id, request)); 79 | 80 | SingleResult(rx) 81 | } 82 | 83 | fn set_max_response_bytes(&mut self, v: u64) { 84 | self.transport.set_max_response_bytes(v); 85 | } 86 | } 87 | 88 | /// Result of calling a single method that will be part of the batch. 89 | /// Converts `oneshot::Receiver` error into `Error::Internal` 90 | pub struct SingleResult(oneshot::Receiver>); 91 | 92 | impl Future for SingleResult { 93 | type Output = error::Result; 94 | 95 | fn poll(mut self: Pin<&mut Self>, ctx: &mut Context) -> Poll { 96 | Poll::Ready(ready!(self.0.poll_unpin(ctx)).map_err(|_| Error::Internal)?) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Web3 Error 2 | use crate::rpc::error::Error as RPCError; 3 | use derive_more::{Display, From}; 4 | use serde_json::Error as SerdeError; 5 | use std::io::Error as IoError; 6 | 7 | /// Web3 `Result` type. 8 | pub type Result = std::result::Result; 9 | 10 | /// Transport-depended error. 11 | #[derive(Display, Debug, Clone, PartialEq)] 12 | pub enum TransportError { 13 | /// Transport-specific error code. 14 | #[display(fmt = "code {}", _0)] 15 | Code(u16), 16 | /// Arbitrary, developer-readable description of the occurred error. 17 | #[display(fmt = "{}", _0)] 18 | Message(String), 19 | } 20 | 21 | /// Errors which can occur when attempting to generate resource uri. 22 | #[derive(Debug, Display, From)] 23 | pub enum Error { 24 | /// server is unreachable 25 | #[display(fmt = "Server is unreachable")] 26 | Unreachable, 27 | /// decoder error 28 | #[display(fmt = "Decoder error: {}", _0)] 29 | Decoder(String), 30 | /// invalid response 31 | #[display(fmt = "Got invalid response: {}", _0)] 32 | #[from(ignore)] 33 | InvalidResponse(String), 34 | /// transport error 35 | #[display(fmt = "Transport error: {}" _0)] 36 | #[from(ignore)] 37 | Transport(TransportError), 38 | /// rpc error 39 | #[display(fmt = "RPC error: {:?}", _0)] 40 | Rpc(RPCError), 41 | /// io error 42 | #[display(fmt = "IO error: {}", _0)] 43 | Io(IoError), 44 | /// recovery error 45 | #[display(fmt = "Recovery error: {}", _0)] 46 | Recovery(crate::signing::RecoveryError), 47 | /// web3 internal error 48 | #[display(fmt = "Internal Web3 error")] 49 | Internal, 50 | } 51 | 52 | impl std::error::Error for Error { 53 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 54 | use self::Error::*; 55 | match *self { 56 | Unreachable | Decoder(_) | InvalidResponse(_) | Transport { .. } | Internal => None, 57 | Rpc(ref e) => Some(e), 58 | Io(ref e) => Some(e), 59 | Recovery(ref e) => Some(e), 60 | } 61 | } 62 | } 63 | 64 | impl From for Error { 65 | fn from(err: SerdeError) -> Self { 66 | Error::Decoder(format!("{:?}", err)) 67 | } 68 | } 69 | 70 | impl Clone for Error { 71 | fn clone(&self) -> Self { 72 | use self::Error::*; 73 | match self { 74 | Unreachable => Unreachable, 75 | Decoder(s) => Decoder(s.clone()), 76 | InvalidResponse(s) => InvalidResponse(s.clone()), 77 | Transport(s) => Transport(s.clone()), 78 | Rpc(e) => Rpc(e.clone()), 79 | Io(e) => Io(IoError::from(e.kind())), 80 | Recovery(e) => Recovery(e.clone()), 81 | Internal => Internal, 82 | } 83 | } 84 | } 85 | 86 | #[cfg(test)] 87 | impl PartialEq for Error { 88 | fn eq(&self, other: &Self) -> bool { 89 | use self::Error::*; 90 | match (self, other) { 91 | (Unreachable, Unreachable) | (Internal, Internal) => true, 92 | (Decoder(a), Decoder(b)) | (InvalidResponse(a), InvalidResponse(b)) => a == b, 93 | (Transport(a), Transport(b)) => a == b, 94 | (Rpc(a), Rpc(b)) => a == b, 95 | (Io(a), Io(b)) => a.kind() == b.kind(), 96 | (Recovery(a), Recovery(b)) => a == b, 97 | _ => false, 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/transports/either.rs: -------------------------------------------------------------------------------- 1 | //! A strongly-typed transport alternative. 2 | 3 | use crate::{api, error, rpc, BatchTransport, DuplexTransport, RequestId, Transport}; 4 | use futures::{ 5 | future::{BoxFuture, FutureExt}, 6 | stream::{BoxStream, StreamExt}, 7 | }; 8 | 9 | /// A wrapper over two possible transports. 10 | /// 11 | /// This type can be used to write semi-generic 12 | /// code without the hassle of making all functions generic. 13 | /// 14 | /// See the `examples` folder for an example how to use it. 15 | #[derive(Debug, Clone)] 16 | pub enum Either { 17 | /// First possible transport. 18 | Left(A), 19 | /// Second possible transport. 20 | Right(B), 21 | } 22 | 23 | impl Transport for Either 24 | where 25 | A: Transport, 26 | B: Transport, 27 | AOut: futures::Future> + 'static + Send, 28 | BOut: futures::Future> + 'static + Send, 29 | { 30 | type Out = BoxFuture<'static, error::Result>; 31 | 32 | fn prepare(&self, method: &str, params: Vec) -> (RequestId, rpc::Call) { 33 | match *self { 34 | Self::Left(ref a) => a.prepare(method, params), 35 | Self::Right(ref b) => b.prepare(method, params), 36 | } 37 | } 38 | 39 | fn send(&self, id: RequestId, request: rpc::Call) -> Self::Out { 40 | match *self { 41 | Self::Left(ref a) => a.send(id, request).boxed(), 42 | Self::Right(ref b) => b.send(id, request).boxed(), 43 | } 44 | } 45 | 46 | fn set_max_response_bytes(&mut self, v: u64) { 47 | match *self { 48 | Self::Left(ref mut a) => a.set_max_response_bytes(v), 49 | Self::Right(ref mut b) => b.set_max_response_bytes(v), 50 | } 51 | } 52 | } 53 | 54 | impl BatchTransport for Either 55 | where 56 | A: BatchTransport, 57 | B: BatchTransport, 58 | A::Out: 'static + Send, 59 | B::Out: 'static + Send, 60 | ABatch: futures::Future>>> + 'static + Send, 61 | BBatch: futures::Future>>> + 'static + Send, 62 | { 63 | type Batch = BoxFuture<'static, error::Result>>>; 64 | 65 | fn send_batch(&self, requests: T) -> Self::Batch 66 | where 67 | T: IntoIterator, 68 | { 69 | match *self { 70 | Self::Left(ref a) => a.send_batch(requests).boxed(), 71 | Self::Right(ref b) => b.send_batch(requests).boxed(), 72 | } 73 | } 74 | } 75 | 76 | impl DuplexTransport for Either 77 | where 78 | A: DuplexTransport, 79 | B: DuplexTransport, 80 | A::Out: 'static + Send, 81 | B::Out: 'static + Send, 82 | AStream: futures::Stream + 'static + Send, 83 | BStream: futures::Stream + 'static + Send, 84 | { 85 | type NotificationStream = BoxStream<'static, rpc::Value>; 86 | 87 | fn subscribe(&self, id: api::SubscriptionId) -> error::Result { 88 | Ok(match *self { 89 | Self::Left(ref a) => a.subscribe(id)?.boxed(), 90 | Self::Right(ref b) => b.subscribe(id)?.boxed(), 91 | }) 92 | } 93 | 94 | fn unsubscribe(&self, id: api::SubscriptionId) -> error::Result { 95 | match *self { 96 | Self::Left(ref a) => a.unsubscribe(id), 97 | Self::Right(ref b) => b.unsubscribe(id), 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/transports/ic_http_client.rs: -------------------------------------------------------------------------------- 1 | //! IC http client 2 | 3 | use serde::{self, Deserialize, Serialize}; 4 | use candid::CandidType; 5 | use jsonrpc_core::Request; 6 | use candid::{Principal, candid_method}; 7 | use ic_cdk::api::management_canister::http_request::{ 8 | CanisterHttpRequestArgument, HttpHeader, HttpMethod, 9 | HttpResponse, http_request, 10 | TransformFunc, TransformContext, 11 | }; 12 | 13 | // #[derive(CandidType, Deserialize, Debug)] 14 | // pub struct CanisterHttpRequestArgs { 15 | // pub url: String, 16 | // pub max_response_bytes: Option, 17 | // pub headers: Vec, 18 | // pub body: Option>, 19 | // pub http_method: HttpMethod, 20 | // pub transform_method_name: Option, 21 | // } 22 | 23 | #[derive(Clone, Debug)] 24 | pub struct ICHttpClient { 25 | pub max_response_bytes: u64, 26 | } 27 | 28 | impl ICHttpClient { 29 | pub fn new(max_resp: Option) -> Self { 30 | ICHttpClient { 31 | max_response_bytes: if let Some(v) = max_resp { v } else { 500_000 }, 32 | } 33 | } 34 | 35 | pub fn set_max_response_bytes(&mut self, v: u64) { 36 | self.max_response_bytes = v; 37 | } 38 | 39 | async fn request( 40 | &self, 41 | url: String, 42 | req_type: HttpMethod, 43 | req_headers: Vec, 44 | payload: &Request, 45 | max_resp: Option, 46 | cycles: Option 47 | ) -> Result, String> { 48 | let request = CanisterHttpRequestArgument { 49 | url: url.clone(), 50 | max_response_bytes: if let Some(v) = max_resp { Some(v) } else { Some(self.max_response_bytes) }, 51 | method: req_type, 52 | headers: req_headers, 53 | body: Some(serde_json::to_vec(&payload).unwrap()), 54 | // transform: Some(TransformType::Function(TransformFunc(candid::Func { 55 | // principal: ic_cdk::api::id(), 56 | // method: "transform".to_string(), 57 | // }))), 58 | transform: Some(TransformContext { 59 | function: TransformFunc(candid::Func { 60 | principal: ic_cdk::api::id(), 61 | method: "transform".to_string(), 62 | }), 63 | context: vec![], 64 | }), 65 | }; 66 | 67 | match http_request(request).await { 68 | Ok((result, )) => { 69 | Ok(result.body) 70 | } 71 | Err((r, m)) => { 72 | let message = 73 | format!("The http_request resulted into error. RejectionCode: {r:?}, Error: {m}"); 74 | ic_cdk::api::print(message.clone()); 75 | Err(message) 76 | } 77 | } 78 | } 79 | 80 | pub async fn get(&self, url: String, payload: &Request, max_resp: Option, cycles: Option) -> Result, String> { 81 | let request_headers = vec![ 82 | HttpHeader { 83 | name: "Content-Type".to_string(), 84 | value: "application/json".to_string(), 85 | }, 86 | ]; 87 | 88 | self.request(url, HttpMethod::GET, request_headers, payload, max_resp, cycles).await 89 | } 90 | 91 | pub async fn post(&self, url: String, payload: &Request, max_resp: Option, cycles: Option) -> Result, String> { 92 | let request_headers = vec![ 93 | HttpHeader { 94 | name: "Content-Type".to_string(), 95 | value: "application/json".to_string(), 96 | }, 97 | ]; 98 | 99 | self.request(url, HttpMethod::POST, request_headers, payload, max_resp, cycles).await 100 | } 101 | } 102 | 103 | -------------------------------------------------------------------------------- /src/contract/res/token.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "function", 4 | "name": "name", 5 | "constant": true, 6 | "inputs": [], 7 | "outputs": [ 8 | { 9 | "name": "", 10 | "type": "string" 11 | } 12 | ] 13 | }, 14 | { 15 | "type": "function", 16 | "name": "decimals", 17 | "constant": true, 18 | "inputs": [], 19 | "outputs": [ 20 | { 21 | "name": "", 22 | "type": "uint8" 23 | } 24 | ] 25 | }, 26 | { 27 | "type": "function", 28 | "name": "balanceOf", 29 | "constant": true, 30 | "inputs": [ 31 | { 32 | "name": "", 33 | "type": "address" 34 | } 35 | ], 36 | "outputs": [ 37 | { 38 | "name": "", 39 | "type": "uint256" 40 | } 41 | ] 42 | }, 43 | { 44 | "type": "function", 45 | "name": "symbol", 46 | "constant": true, 47 | "inputs": [], 48 | "outputs": [ 49 | { 50 | "name": "", 51 | "type": "string" 52 | } 53 | ] 54 | }, 55 | { 56 | "type": "function", 57 | "name": "transfer", 58 | "constant": false, 59 | "inputs": [ 60 | { 61 | "name": "_to", 62 | "type": "address" 63 | }, 64 | { 65 | "name": "_value", 66 | "type": "uint256" 67 | } 68 | ], 69 | "outputs": [] 70 | }, 71 | { 72 | "type": "constructor", 73 | "inputs": [ 74 | { 75 | "name": "_supply", 76 | "type": "uint256" 77 | }, 78 | { 79 | "name": "_name", 80 | "type": "string" 81 | }, 82 | { 83 | "name": "_decimals", 84 | "type": "uint8" 85 | }, 86 | { 87 | "name": "_symbol", 88 | "type": "string" 89 | } 90 | ] 91 | }, 92 | { 93 | "name": "Transfer", 94 | "type": "event", 95 | "anonymous": false, 96 | "inputs": [ 97 | { 98 | "indexed": true, 99 | "name": "from", 100 | "type": "address" 101 | }, 102 | { 103 | "indexed": true, 104 | "name": "to", 105 | "type": "address" 106 | }, 107 | { 108 | "indexed": false, 109 | "name": "value", 110 | "type": "uint256" 111 | } 112 | ] 113 | }, 114 | { 115 | "constant":false, 116 | "inputs":[ 117 | { 118 | "name":"_spender", 119 | "type":"address" 120 | }, 121 | { 122 | "name":"_value", 123 | "type":"uint256" 124 | } 125 | ], 126 | "name":"approve", 127 | "outputs":[ 128 | { 129 | "name":"success", 130 | "type":"bool" 131 | } 132 | ], 133 | "type":"function" 134 | }, 135 | { 136 | "constant":true, 137 | "inputs":[ 138 | { 139 | "name":"", 140 | "type":"address" 141 | }, 142 | { 143 | "name":"", 144 | "type":"address" 145 | } 146 | ], 147 | "name":"allowance", 148 | "outputs":[ 149 | { 150 | "name":"", 151 | "type":"uint256" 152 | } 153 | ], 154 | "type":"function" 155 | } 156 | ] 157 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ic-web3" 3 | version = "0.1.7" 4 | description = "Ethereum JSON-RPC client for IC canisters." 5 | homepage = "https://github.com/rocklabs-io/ic-web3" 6 | repository = "https://github.com/rocklabs-io/ic-web3" 7 | documentation = "https://docs.rs/ic-web3" 8 | license = "MIT" 9 | keywords = ["dfinity", "icp", "web3", "ethereum", "rpc"] 10 | authors = ["Tomasz Drwięga ", "Rocklabs "] 11 | readme = "README.md" 12 | edition = "2018" 13 | 14 | [dependencies] 15 | arrayvec = "0.7.1" 16 | derive_more = "0.99.1" 17 | ethabi = "17.0.0" 18 | ethereum-types = "0.13.0" 19 | libsecp256k1 = { version = "0.7.1", features = ["lazy-static-context"] } 20 | futures = "0.3.5" 21 | futures-timer = "3.0.2" 22 | hex = "0.4" 23 | #idna = "0.2" 24 | jsonrpc-core = "18.0.0" 25 | #log = "0.4.6" 26 | parking_lot = "0.12.0" 27 | rlp = "0.5" 28 | serde = { version = "1.0.90", features = ["derive"] } 29 | serde_json = "1.0.39" 30 | tiny-keccak = { version = "2.0.1", features = ["keccak"] } 31 | pin-project = "1.0" 32 | # ic related 33 | ic-cdk = "0.7.3" 34 | ic-cdk-macros = "0.6" 35 | candid = "0.8.0" 36 | 37 | 38 | # Optional deps 39 | # secp256k1 = { version = "0.21", features = ["recovery"], optional = true } 40 | # once_cell = { version = "1.8.0", optional = true } 41 | 42 | ## HTTP 43 | #base64 = { version = "0.13", optional = true } 44 | #bytes = { version = "1.0", optional = true } 45 | #reqwest = { version = "0.11", optional = true, default-features = false, features = ["json"] } 46 | headers = { version = "0.3", optional = true } 47 | ## WS 48 | # async-native-tls = { git = "https://github.com/async-email/async-native-tls.git", rev = "b5b5562d6cea77f913d4cbe448058c031833bf17", optional = true, default-features = false } 49 | # Temporarily use forked version released to crates.io 50 | async-native-tls = { package = "web3-async-native-tls", version = "0.4", optional = true, default-features = false } 51 | async-std = { version = "1.6", optional = true } 52 | tokio = { version = "1.0", optional = true, features = ["full"] } 53 | tokio-stream = { version = "0.1", optional = true } 54 | tokio-util = { version = "0.7", optional = true, features = ["compat", "io"] } 55 | soketto = { version = "0.7.0", optional = true } 56 | ## Shared (WS, HTTP) 57 | url = { version = "2.1", optional = true } 58 | ## EIP-1193 59 | js-sys = { version = "0.3.45", optional = true } 60 | ### This is a transitive dependency, only here so we can turn on its wasm_bindgen feature 61 | rand = { version = "0.8.1", optional = true } 62 | getrandom = { version = "0.2", features = ["custom"] } 63 | wasm-bindgen = { version = "0.2.68", optional = true, features = ["serde-serialize"] } 64 | wasm-bindgen-futures = { version = "0.4.18", optional = true } 65 | 66 | [dev-dependencies] 67 | # For examples 68 | env_logger = "0.9" 69 | hex-literal = "0.3" 70 | wasm-bindgen-test = "0.3.19" 71 | 72 | #[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] 73 | #hyper = { version = "0.14", default-features = false, features = ["server"] } 74 | #tokio = { version = "1.0", features = ["full"] } 75 | #tokio-stream = { version = "0.1", features = ["net"] } 76 | 77 | [features] 78 | default = [] 79 | wasm = ["js-sys", "wasm-bindgen", "wasm-bindgen-futures", "futures-timer/wasm-bindgen"] 80 | eip-1193 = ["wasm"] 81 | #_http_base = ["reqwest", "bytes", "url", "base64", "headers"] 82 | #http = ["_http_base"] 83 | #http-tls = ["http", "reqwest/default-tls"] 84 | #http-native-tls = ["http", "reqwest/native-tls"] 85 | #http-rustls-tls = ["http", "reqwest/rustls-tls"] 86 | #ws-tokio = ["soketto", "url", "tokio", "tokio-util", "headers"] 87 | #ws-async-std = ["soketto", "url", "async-std", "headers"] 88 | #ws-tls-tokio = ["async-native-tls", "async-native-tls/runtime-tokio", "ws-tokio"] 89 | #ws-tls-async-std = ["async-native-tls", "async-native-tls/runtime-async-std", "ws-async-std"] 90 | #ipc-tokio = ["tokio", "tokio-stream", "tokio-util"] 91 | #arbitrary_precision = ["serde_json/arbitrary_precision", "jsonrpc-core/arbitrary_precision"] 92 | #test = [] 93 | 94 | [workspace] 95 | -------------------------------------------------------------------------------- /src/types/parity_pending_transaction.rs: -------------------------------------------------------------------------------- 1 | use serde::{ 2 | ser::{SerializeMap, Serializer}, 3 | Serialize, 4 | }; 5 | 6 | use super::{Address, U256, U64}; 7 | 8 | /// Condition to filter pending transactions 9 | #[derive(Clone, Serialize)] 10 | pub enum FilterCondition { 11 | /// Lower Than 12 | #[serde(rename(serialize = "lt"))] 13 | LowerThan(T), 14 | /// Equal 15 | #[serde(rename(serialize = "eq"))] 16 | Equal(T), 17 | /// Greater Than 18 | #[serde(rename(serialize = "gt"))] 19 | GreaterThan(T), 20 | } 21 | 22 | impl From for FilterCondition { 23 | fn from(t: T) -> Self { 24 | FilterCondition::Equal(t) 25 | } 26 | } 27 | 28 | /// To Filter 29 | #[derive(Clone)] 30 | pub enum ToFilter { 31 | /// Address 32 | Address(Address), 33 | /// Action (i.e. contract creation) 34 | Action, 35 | } 36 | 37 | /// Filter for pending transactions (only openethereum/Parity) 38 | #[derive(Clone, Default, Serialize)] 39 | pub struct ParityPendingTransactionFilter { 40 | /// From address 41 | #[serde(skip_serializing_if = "Option::is_none")] 42 | pub from: Option>, 43 | /// To address or action, i.e. contract creation 44 | #[serde(skip_serializing_if = "Option::is_none")] 45 | pub to: Option, 46 | /// Gas 47 | #[serde(skip_serializing_if = "Option::is_none")] 48 | pub gas: Option>, 49 | /// Gas Price 50 | #[serde(skip_serializing_if = "Option::is_none")] 51 | pub gas_price: Option>, 52 | /// Value 53 | #[serde(skip_serializing_if = "Option::is_none")] 54 | pub value: Option>, 55 | /// Nonce 56 | #[serde(skip_serializing_if = "Option::is_none")] 57 | pub nonce: Option>, 58 | } 59 | 60 | impl Serialize for ToFilter { 61 | fn serialize(&self, serializer: S) -> Result 62 | where 63 | S: Serializer, 64 | { 65 | let mut map = serializer.serialize_map(Some(1))?; 66 | 67 | match self { 68 | Self::Address(a) => map.serialize_entry("eq", a)?, 69 | Self::Action => map.serialize_entry("action", "contract_creation")?, 70 | } 71 | map.end() 72 | } 73 | } 74 | 75 | impl ParityPendingTransactionFilter { 76 | /// Returns a filter builder 77 | pub fn builder() -> ParityPendingTransactionFilterBuilder { 78 | Default::default() 79 | } 80 | } 81 | 82 | /// Filter Builder 83 | #[derive(Default, Clone)] 84 | pub struct ParityPendingTransactionFilterBuilder { 85 | filter: ParityPendingTransactionFilter, 86 | } 87 | 88 | impl ParityPendingTransactionFilterBuilder { 89 | /// Sets `from` 90 | pub fn from(mut self, from: Address) -> Self { 91 | self.filter.from = Some(FilterCondition::Equal(from)); 92 | self 93 | } 94 | 95 | /// Sets `to` 96 | pub fn to(mut self, to_or_action: ToFilter) -> Self { 97 | self.filter.to = Some(to_or_action); 98 | self 99 | } 100 | 101 | /// Sets `gas` 102 | pub fn gas(mut self, gas: impl Into>) -> Self { 103 | self.filter.gas = Some(gas.into()); 104 | self 105 | } 106 | 107 | /// Sets `gas_price` 108 | pub fn gas_price(mut self, gas_price: impl Into>) -> Self { 109 | self.filter.gas_price = Some(gas_price.into()); 110 | self 111 | } 112 | 113 | /// Sets `value` 114 | pub fn value(mut self, value: impl Into>) -> Self { 115 | self.filter.value = Some(value.into()); 116 | self 117 | } 118 | 119 | /// Sets `nonce` 120 | pub fn nonce(mut self, nonce: impl Into>) -> Self { 121 | self.filter.nonce = Some(nonce.into()); 122 | self 123 | } 124 | 125 | /// Returns filter 126 | pub fn build(&self) -> ParityPendingTransactionFilter { 127 | self.filter.clone() 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/contract/ens/ENSRegistry.json: -------------------------------------------------------------------------------- 1 | [{"inputs":[{"internalType":"contract ENS","name":"_old","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"label","type":"bytes32"},{"indexed":false,"internalType":"address","name":"owner","type":"address"}],"name":"NewOwner","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"address","name":"resolver","type":"address"}],"name":"NewResolver","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"uint64","name":"ttl","type":"uint64"}],"name":"NewTTL","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"address","name":"owner","type":"address"}],"name":"Transfer","type":"event"},{"constant":true,"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"old","outputs":[{"internalType":"contract ENS","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"recordExists","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"resolver","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"owner","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"resolver","type":"address"},{"internalType":"uint64","name":"ttl","type":"uint64"}],"name":"setRecord","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"resolver","type":"address"}],"name":"setResolver","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes32","name":"label","type":"bytes32"},{"internalType":"address","name":"owner","type":"address"}],"name":"setSubnodeOwner","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes32","name":"label","type":"bytes32"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"resolver","type":"address"},{"internalType":"uint64","name":"ttl","type":"uint64"}],"name":"setSubnodeRecord","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"uint64","name":"ttl","type":"uint64"}],"name":"setTTL","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"ttl","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"payable":false,"stateMutability":"view","type":"function"}] -------------------------------------------------------------------------------- /examples/res/contract_token.code: -------------------------------------------------------------------------------- 1 | 606060405260408051908101604052600481527f48302e31000000000000000000000000000000000000000000000000000000006020820152600690805161004b9291602001906100e7565b50341561005757600080fd5b6040516109f83803806109f8833981016040528080519190602001805182019190602001805191906020018051600160a060020a0333166000908152600160205260408120879055869055909101905060038380516100ba9291602001906100e7565b506004805460ff191660ff841617905560058180516100dd9291602001906100e7565b5050505050610182565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061012857805160ff1916838001178555610155565b82800160010185558215610155579182015b8281111561015557825182559160200191906001019061013a565b50610161929150610165565b5090565b61017f91905b80821115610161576000815560010161016b565b90565b610867806101916000396000f300606060405236156100935763ffffffff60e060020a60003504166306fdde0381146100a3578063095ea7b31461012d57806318160ddd1461016357806323b872dd14610188578063313ce567146101b057806354fd4d50146101d957806370a08231146101ec57806395d89b411461020b578063a9059cbb1461021e578063cae9ca5114610240578063dd62ed3e146102a5575b341561009e57600080fd5b600080fd5b34156100ae57600080fd5b6100b66102ca565b60405160208082528190810183818151815260200191508051906020019080838360005b838110156100f25780820151838201526020016100da565b50505050905090810190601f16801561011f5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561013857600080fd5b61014f600160a060020a0360043516602435610368565b604051901515815260200160405180910390f35b341561016e57600080fd5b6101766103d5565b60405190815260200160405180910390f35b341561019357600080fd5b61014f600160a060020a03600435811690602435166044356103db565b34156101bb57600080fd5b6101c36104d3565b60405160ff909116815260200160405180910390f35b34156101e457600080fd5b6100b66104dc565b34156101f757600080fd5b610176600160a060020a0360043516610547565b341561021657600080fd5b6100b6610562565b341561022957600080fd5b61014f600160a060020a03600435166024356105cd565b341561024b57600080fd5b61014f60048035600160a060020a03169060248035919060649060443590810190830135806020601f8201819004810201604051908101604052818152929190602084018383808284375094965061067095505050505050565b34156102b057600080fd5b610176600160a060020a0360043581169060243516610810565b60038054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156103605780601f1061033557610100808354040283529160200191610360565b820191906000526020600020905b81548152906001019060200180831161034357829003601f168201915b505050505081565b600160a060020a03338116600081815260026020908152604080832094871680845294909152808220859055909291907f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259085905190815260200160405180910390a35060015b92915050565b60005481565b600160a060020a03831660009081526001602052604081205482901080159061042b5750600160a060020a0380851660009081526002602090815260408083203390941683529290522054829010155b80156104375750600082115b156104c857600160a060020a03808416600081815260016020908152604080832080548801905588851680845281842080548990039055600283528184203390961684529490915290819020805486900390559091907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9085905190815260200160405180910390a35060016104cc565b5060005b9392505050565b60045460ff1681565b60068054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156103605780601f1061033557610100808354040283529160200191610360565b600160a060020a031660009081526001602052604090205490565b60058054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156103605780601f1061033557610100808354040283529160200191610360565b600160a060020a0333166000908152600160205260408120548290108015906105f65750600082115b1561066857600160a060020a033381166000818152600160205260408082208054879003905592861680825290839020805486019055917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9085905190815260200160405180910390a35060016103cf565b5060006103cf565b600160a060020a03338116600081815260026020908152604080832094881680845294909152808220869055909291907f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259086905190815260200160405180910390a383600160a060020a03166040517f72656365697665417070726f76616c28616464726573732c75696e743235362c81527f616464726573732c6279746573290000000000000000000000000000000000006020820152602e01604051809103902060e060020a9004338530866040518563ffffffff1660e060020a0281526004018085600160a060020a0316600160a060020a0316815260200184815260200183600160a060020a0316600160a060020a03168152602001828051906020019080838360005b838110156107b1578082015183820152602001610799565b50505050905090810190601f1680156107de5780820380516001836020036101000a031916815260200191505b5094505050505060006040518083038160008761646e5a03f192505050151561080657600080fd5b5060019392505050565b600160a060020a039182166000908152600260209081526040808320939094168252919091522054905600a165627a7a723058204d5eeb1cfd7573cd14412d3639887ef3085966b5b1c42ab4d98ff3396dbb1ada0029 2 | -------------------------------------------------------------------------------- /src/ic.rs: -------------------------------------------------------------------------------- 1 | //! IC's threshold ECDSA related functions 2 | 3 | use ic_cdk::export::{ 4 | candid::CandidType, 5 | serde::{Deserialize, Serialize}, 6 | Principal, 7 | }; 8 | use std::str::FromStr; 9 | use crate::types::{Address, Recovery}; 10 | use crate::signing; 11 | use libsecp256k1::{PublicKey, PublicKeyFormat, Message, Signature, RecoveryId, recover}; 12 | 13 | const ECDSA_SIGN_CYCLES : u64 = 10_000_000_000; 14 | // pub type Address = [u8; 20]; 15 | 16 | // #[derive(CandidType, Serialize, Debug, Clone)] 17 | // pub enum EcdsaCurve { 18 | // #[serde(rename = "secp256k1")] 19 | // secp256k1, 20 | // } 21 | 22 | use ic_cdk::api::management_canister::ecdsa::*; 23 | 24 | #[derive(CandidType, Serialize, Debug, Clone)] 25 | pub struct KeyInfo { 26 | pub derivation_path: Vec>, 27 | pub key_name: String, 28 | pub ecdsa_sign_cycles: Option, 29 | } 30 | 31 | /// get public key from ic, 32 | /// derivation_path: 4-byte big-endian encoding of an unsigned integer less than 2^31 33 | pub async fn get_public_key( 34 | canister_id: Option, 35 | derivation_path: Vec>, 36 | key_name: String 37 | ) -> Result, String> { 38 | let key_id = EcdsaKeyId { 39 | curve: EcdsaCurve::Secp256k1, 40 | name: key_name, 41 | }; 42 | let ic_canister_id = "aaaaa-aa"; 43 | let ic = Principal::from_str(&ic_canister_id).unwrap(); 44 | 45 | 46 | let request = EcdsaPublicKeyArgument { 47 | canister_id: canister_id, 48 | derivation_path: derivation_path, 49 | key_id: key_id.clone(), 50 | }; 51 | let (res,): (EcdsaPublicKeyResponse,) = ic_cdk::call(ic, "ecdsa_public_key", (request,)) 52 | .await 53 | .map_err(|e| format!("Failed to call ecdsa_public_key {}", e.1))?; 54 | 55 | Ok(res.public_key) 56 | } 57 | 58 | /// convert compressed public key to ethereum address 59 | pub fn pubkey_to_address(pubkey: &[u8]) -> Result { 60 | let uncompressed_pubkey = match PublicKey::parse_slice(pubkey, Some(PublicKeyFormat::Compressed)) { 61 | Ok(key) => { key.serialize() }, 62 | Err(_) => { return Err("uncompress public key failed: ".to_string()); }, 63 | }; 64 | let hash = signing::keccak256(&uncompressed_pubkey[1..65]); 65 | let mut result = [0u8; 20]; 66 | result.copy_from_slice(&hash[12..]); 67 | Ok(Address::from(result)) 68 | } 69 | 70 | /// get canister's eth address 71 | pub async fn get_eth_addr( 72 | canister_id: Option, 73 | derivation_path: Option>>, 74 | name: String 75 | ) -> Result { 76 | let path = if let Some(v) = derivation_path { v } else { vec![ic_cdk::id().as_slice().to_vec()] }; 77 | match get_public_key(canister_id, path, name).await { 78 | Ok(pubkey) => { return pubkey_to_address(&pubkey); }, 79 | Err(e) => { return Err(e); }, 80 | }; 81 | } 82 | 83 | /// use ic's threshold ecdsa to sign a message 84 | pub async fn ic_raw_sign( 85 | message: Vec, 86 | key_info: KeyInfo, 87 | ) -> Result, String> { 88 | assert!(message.len() == 32); 89 | 90 | let key_id = EcdsaKeyId { 91 | curve: EcdsaCurve::Secp256k1, 92 | name: key_info.key_name, 93 | }; 94 | let ic = Principal::management_canister(); 95 | 96 | let request = SignWithEcdsaArgument { 97 | message_hash: message.clone(), 98 | derivation_path: key_info.derivation_path, 99 | key_id, 100 | }; 101 | 102 | let ecdsa_sign_cycles = key_info.ecdsa_sign_cycles.unwrap_or(ECDSA_SIGN_CYCLES); 103 | 104 | let (res,): (SignWithEcdsaResponse,) = 105 | ic_cdk::api::call::call_with_payment(ic, "sign_with_ecdsa", (request,), ecdsa_sign_cycles) 106 | .await 107 | .map_err(|e| format!("Failed to call sign_with_ecdsa {}", e.1))?; 108 | 109 | Ok(res.signature) 110 | } 111 | 112 | 113 | // recover address from signature 114 | // rec_id < 4 115 | pub fn recover_address(msg: Vec, sig: Vec, rec_id: u8) -> String { 116 | let message = Message::parse_slice(&msg).unwrap(); 117 | let signature = Signature::parse_overflowing_slice(&sig).unwrap(); 118 | let recovery_id = RecoveryId::parse(rec_id).unwrap(); 119 | 120 | match recover(&message, &signature, &recovery_id) { 121 | Ok(pubkey) => { 122 | let uncompressed_pubkey = pubkey.serialize(); 123 | // let hash = keccak256_hash(&uncompressed_pubkey[1..65]); 124 | let hash = signing::keccak256(&uncompressed_pubkey[1..65]); 125 | let mut result = [0u8; 20]; 126 | result.copy_from_slice(&hash[12..]); 127 | hex::encode(result) 128 | }, 129 | Err(_) => { "".into() } 130 | } 131 | } 132 | 133 | pub fn verify(addr: String, message: Vec, signature: Vec) -> bool { 134 | let (sig, rec_id) = Recovery::from_raw_signature(message.clone(), signature) 135 | .unwrap() 136 | .as_signature() 137 | .unwrap(); 138 | let rec_addr = recover_address(message, sig.to_vec(), rec_id as u8); 139 | return rec_addr == addr; 140 | } 141 | -------------------------------------------------------------------------------- /src/api/eth_subscribe.rs: -------------------------------------------------------------------------------- 1 | //! `Eth` namespace, subscriptions 2 | 3 | use crate::{ 4 | api::Namespace, 5 | error, helpers, 6 | types::{BlockHeader, Filter, Log, SyncState, H256}, 7 | DuplexTransport, 8 | }; 9 | use futures::{ 10 | task::{Context, Poll}, 11 | Stream, 12 | }; 13 | use pin_project::{pin_project, pinned_drop}; 14 | use std::{marker::PhantomData, pin::Pin}; 15 | 16 | /// `Eth` namespace, subscriptions 17 | #[derive(Debug, Clone)] 18 | pub struct EthSubscribe { 19 | transport: T, 20 | } 21 | 22 | impl Namespace for EthSubscribe { 23 | fn new(transport: T) -> Self 24 | where 25 | Self: Sized, 26 | { 27 | EthSubscribe { transport } 28 | } 29 | 30 | fn transport(&self) -> &T { 31 | &self.transport 32 | } 33 | } 34 | 35 | /// ID of subscription returned from `eth_subscribe` 36 | #[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd)] 37 | pub struct SubscriptionId(String); 38 | 39 | impl From for SubscriptionId { 40 | fn from(s: String) -> Self { 41 | SubscriptionId(s) 42 | } 43 | } 44 | 45 | /// Stream of notifications from a subscription 46 | /// Given a type deserializable from rpc::Value and a subscription id, yields items of that type as 47 | /// notifications are delivered. 48 | #[pin_project(PinnedDrop)] 49 | #[derive(Debug)] 50 | pub struct SubscriptionStream { 51 | transport: T, 52 | id: SubscriptionId, 53 | #[pin] 54 | rx: T::NotificationStream, 55 | _marker: PhantomData, 56 | } 57 | 58 | impl SubscriptionStream { 59 | fn new(transport: T, id: SubscriptionId) -> error::Result { 60 | let rx = transport.subscribe(id.clone())?; 61 | Ok(SubscriptionStream { 62 | transport, 63 | id, 64 | rx, 65 | _marker: PhantomData, 66 | }) 67 | } 68 | 69 | /// Return the ID of this subscription 70 | pub fn id(&self) -> &SubscriptionId { 71 | &self.id 72 | } 73 | 74 | /// Unsubscribe from the event represented by this stream 75 | pub async fn unsubscribe(self) -> error::Result { 76 | let &SubscriptionId(ref id) = &self.id; 77 | let id = helpers::serialize(&id); 78 | let response = self.transport.execute("eth_unsubscribe", vec![id]).await?; 79 | helpers::decode(response) 80 | } 81 | } 82 | 83 | impl Stream for SubscriptionStream 84 | where 85 | T: DuplexTransport, 86 | I: serde::de::DeserializeOwned, 87 | { 88 | type Item = error::Result; 89 | 90 | fn poll_next(self: Pin<&mut Self>, ctx: &mut Context) -> Poll> { 91 | let this = self.project(); 92 | let x = ready!(this.rx.poll_next(ctx)); 93 | Poll::Ready(x.map(|result| serde_json::from_value(result).map_err(Into::into))) 94 | } 95 | } 96 | 97 | #[pinned_drop] 98 | impl PinnedDrop for SubscriptionStream 99 | where 100 | T: DuplexTransport, 101 | { 102 | fn drop(self: Pin<&mut Self>) { 103 | let _ = self.transport.unsubscribe(self.id().clone()); 104 | } 105 | } 106 | 107 | impl EthSubscribe { 108 | /// Create a new heads subscription 109 | pub async fn subscribe_new_heads(&self) -> error::Result> { 110 | let subscription = helpers::serialize(&&"newHeads"); 111 | let response = self.transport.execute("eth_subscribe", vec![subscription]).await?; 112 | let id: String = helpers::decode(response)?; 113 | SubscriptionStream::new(self.transport.clone(), SubscriptionId(id)) 114 | } 115 | 116 | /// Create a logs subscription 117 | pub async fn subscribe_logs(&self, filter: Filter) -> error::Result> { 118 | let subscription = helpers::serialize(&&"logs"); 119 | let filter = helpers::serialize(&filter); 120 | let response = self 121 | .transport 122 | .execute("eth_subscribe", vec![subscription, filter]) 123 | .await?; 124 | let id: String = helpers::decode(response)?; 125 | SubscriptionStream::new(self.transport.clone(), SubscriptionId(id)) 126 | } 127 | 128 | /// Create a pending transactions subscription 129 | pub async fn subscribe_new_pending_transactions(&self) -> error::Result> { 130 | let subscription = helpers::serialize(&&"newPendingTransactions"); 131 | let response = self.transport.execute("eth_subscribe", vec![subscription]).await?; 132 | let id: String = helpers::decode(response)?; 133 | SubscriptionStream::new(self.transport.clone(), SubscriptionId(id)) 134 | } 135 | 136 | /// Create a sync status subscription 137 | pub async fn subscribe_syncing(&self) -> error::Result> { 138 | let subscription = helpers::serialize(&&"syncing"); 139 | let response = self.transport.execute("eth_subscribe", vec![subscription]).await?; 140 | let id: String = helpers::decode(response)?; 141 | SubscriptionStream::new(self.transport.clone(), SubscriptionId(id)) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ic-web3 2 | RPC client for canisters on the Internet Computer to access Ethereum networks, powered by the Internet Computer's threshold ECDSA signature and outbound http call features. 3 | 4 | [![Crates.io](https://img.shields.io/crates/v/ic-web3)](https://crates.io/crates/ic-web3) 5 | 6 | ### Features 7 | 8 | * Perform RPC calls to Ethereum networks within canisters 9 | * Sign messages with IC's threshold ECDSA 10 | * Send transactions to Ethereum networks within canisters 11 | * Query/call Ethereum contracts within canisters 12 | 13 | ### Usage 14 | 15 | Add the following to your `Cargo.toml`: 16 | 17 | ``` 18 | [dependencies] 19 | ic-web3 = { git = "https://github.com/rocklabs-io/ic-web3" } 20 | ``` 21 | 22 | **Note:** It requires ipv6 compatibility for the RPC provider. For ipv6 compitable providers, you can refer to this [list]( https://forum.dfinity.org/t/long-term-r-d-integration-with-the-ethereum-network/9382/81). 23 | 24 | 25 | ### Examples 26 | 27 | Note: you should have dfx 0.11.2 or above. 28 | 29 | Please refer to [example](./examples/example.rs) for the complete example. 30 | 31 | ```rust 32 | use candid::candid_method; 33 | use ic_cdk_macros::{self, update}; 34 | use std::str::FromStr; 35 | 36 | use ic_web3::transports::ICHttp; 37 | use ic_web3::Web3; 38 | use ic_web3::ic::{get_eth_addr, KeyInfo}; 39 | use ic_web3::{ 40 | contract::{Contract, Options}, 41 | ethabi::ethereum_types::{U64, U256}, 42 | types::{Address, TransactionParameters, BlockId, BlockNumber, Block}, 43 | }; 44 | 45 | const URL: &str = ""; / https://ethereum.publicnode.com 46 | const CHAIN_ID: u64 = 1; 47 | const KEY_NAME: &str = "dfx_test_key"; 48 | const TOKEN_ABI: &[u8] = include_bytes!("../src/contract/res/token.json"); 49 | 50 | type Result = std::result::Result; 51 | 52 | #[update(name = "get_eth_gas_price")] 53 | #[candid_method(update, rename = "get_eth_gas_price")] 54 | async fn get_eth_gas_price() -> Result { 55 | let w3 = match ICHttp::new(URL, None) { 56 | Ok(v) => { Web3::new(v) }, 57 | Err(e) => { return Err(e.to_string()) }, 58 | }; 59 | let gas_price = w3.eth().gas_price().await.map_err(|e| format!("get gas price failed: {}", e))?; 60 | ic_cdk::println!("gas price: {}", gas_price); 61 | Ok(format!("{}", gas_price)) 62 | } 63 | 64 | // get canister's ethereum address 65 | #[update(name = "get_canister_addr")] 66 | #[candid_method(update, rename = "get_canister_addr")] 67 | async fn get_canister_addr() -> Result { 68 | match get_eth_addr(None, None, KEY_NAME.to_string()).await { 69 | Ok(addr) => { Ok(hex::encode(addr)) }, 70 | Err(e) => { Err(e) }, 71 | } 72 | } 73 | 74 | // send tx to eth 75 | #[update(name = "send_eth")] 76 | #[candid_method(update, rename = "send_eth")] 77 | async fn send_eth(to: String, value: u64) -> Result { 78 | // ecdsa key info 79 | let derivation_path = vec![ic_cdk::id().as_slice().to_vec()]; 80 | let key_info = KeyInfo{ derivation_path: derivation_path, key_name: KEY_NAME.to_string() }; 81 | 82 | // get canister eth address 83 | let from_addr = get_eth_addr(None, None, KEY_NAME.to_string()) 84 | .await 85 | .map_err(|e| format!("get canister eth addr failed: {}", e))?; 86 | // get canister the address tx count 87 | let w3 = match ICHttp::new(URL, None) { 88 | Ok(v) => { Web3::new(v) }, 89 | Err(e) => { return Err(e.to_string()) }, 90 | }; 91 | let tx_count = w3.eth() 92 | .transaction_count(from_addr, None) 93 | .await 94 | .map_err(|e| format!("get tx count error: {}", e))?; 95 | 96 | ic_cdk::println!("canister eth address {} tx count: {}", hex::encode(from_addr), tx_count); 97 | // construct a transaction 98 | let to = Address::from_str(&to).unwrap(); 99 | let tx = TransactionParameters { 100 | to: Some(to), 101 | nonce: Some(tx_count), // remember to fetch nonce first 102 | value: U256::from(value), 103 | gas_price: Some(U256::exp10(10)), // 10 gwei 104 | gas: U256::from(21000), 105 | ..Default::default() 106 | }; 107 | // sign the transaction and get serialized transaction + signature 108 | let signed_tx = w3.accounts() 109 | .sign_transaction(tx, key_info, CHAIN_ID) 110 | .await 111 | .map_err(|e| format!("sign tx error: {}", e))?; 112 | match w3.eth().send_raw_transaction(signed_tx.raw_transaction).await { 113 | Ok(txhash) => { 114 | ic_cdk::println!("txhash: {}", hex::encode(txhash.0)); 115 | Ok(format!("{}", hex::encode(txhash.0))) 116 | }, 117 | Err(e) => { Err(e.to_string()) }, 118 | } 119 | } 120 | ``` 121 | 122 | Start a local replica: 123 | 124 | ``` 125 | dfx start --background --clean --enable-canister-http 126 | ``` 127 | 128 | Deploy the example canister: 129 | 130 | ``` 131 | dfx deploy 132 | ``` 133 | 134 | ### Endpoint Canister 135 | 136 | The public endpoint canister is deployed at: `3ondx-siaaa-aaaam-abf3q-cai`, [code](./examples/endpoint.rs). You can access Ethereum Mainnet data by passing RPC calls to the endpoint canister. 137 | 138 | ### Acknowledgment 139 | 140 | This repo is modified from the [rust-web3](https://github.com/tomusdrw/rust-web3) project. 141 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Ethereum JSON-RPC client (Web3). 2 | 3 | #![allow( 4 | clippy::type_complexity, 5 | clippy::wrong_self_convention, 6 | clippy::single_match, 7 | clippy::let_unit_value, 8 | clippy::match_wild_err_arm 9 | )] 10 | // #![warn(missing_docs)] 11 | #![allow(non_camel_case_types)] 12 | #![allow(unused_variables)] 13 | #![allow(dead_code)] 14 | #![allow(unused_imports)] 15 | 16 | // select! in WS transport 17 | #![recursion_limit = "256"] 18 | 19 | use jsonrpc_core as rpc; 20 | 21 | /// Re-export of the `futures` crate. 22 | #[macro_use] 23 | pub extern crate futures; 24 | pub use futures::executor::{block_on, block_on_stream}; 25 | 26 | pub use ethabi; 27 | 28 | // it needs to be before other modules 29 | // otherwise the macro for tests is not available. 30 | #[macro_use] 31 | pub mod helpers; 32 | 33 | pub mod api; 34 | pub mod confirm; 35 | pub mod contract; 36 | pub mod error; 37 | pub mod signing; 38 | pub mod transports; 39 | pub mod types; 40 | pub mod ic; 41 | // pub mod tx_helpers; 42 | 43 | pub use crate::{ 44 | api::Web3, 45 | error::{Error, Result}, 46 | }; 47 | 48 | /// Assigned RequestId 49 | pub type RequestId = usize; 50 | 51 | // TODO [ToDr] The transport most likely don't need to be thread-safe. 52 | // (though it has to be Send) 53 | /// Transport implementation 54 | pub trait Transport: std::fmt::Debug + Clone { 55 | /// The type of future this transport returns when a call is made. 56 | type Out: futures::Future>; 57 | 58 | /// Prepare serializable RPC call for given method with parameters. 59 | fn prepare(&self, method: &str, params: Vec) -> (RequestId, rpc::Call); 60 | 61 | /// Execute prepared RPC call. 62 | fn send(&self, id: RequestId, request: rpc::Call) -> Self::Out; 63 | 64 | /// Execute remote method with given parameters. 65 | fn execute(&self, method: &str, params: Vec) -> Self::Out { 66 | let (id, request) = self.prepare(method, params); 67 | self.send(id, request) 68 | } 69 | 70 | /// set the max response bytes, do nothing by default 71 | fn set_max_response_bytes(&mut self, bytes: u64) {} 72 | } 73 | 74 | /// A transport implementation supporting batch requests. 75 | pub trait BatchTransport: Transport { 76 | /// The type of future this transport returns when a call is made. 77 | type Batch: futures::Future>>>; 78 | 79 | /// Sends a batch of prepared RPC calls. 80 | fn send_batch(&self, requests: T) -> Self::Batch 81 | where 82 | T: IntoIterator; 83 | } 84 | 85 | /// A transport implementation supporting pub sub subscriptions. 86 | pub trait DuplexTransport: Transport { 87 | /// The type of stream this transport returns 88 | type NotificationStream: futures::Stream; 89 | 90 | /// Add a subscription to this transport 91 | fn subscribe(&self, id: api::SubscriptionId) -> error::Result; 92 | 93 | /// Remove a subscription from this transport 94 | fn unsubscribe(&self, id: api::SubscriptionId) -> error::Result<()>; 95 | } 96 | 97 | impl Transport for X 98 | where 99 | T: Transport + ?Sized, 100 | X: std::ops::Deref, 101 | X: std::fmt::Debug, 102 | X: Clone, 103 | { 104 | type Out = T::Out; 105 | 106 | fn prepare(&self, method: &str, params: Vec) -> (RequestId, rpc::Call) { 107 | (**self).prepare(method, params) 108 | } 109 | 110 | fn send(&self, id: RequestId, request: rpc::Call) -> Self::Out { 111 | (**self).send(id, request) 112 | } 113 | } 114 | 115 | impl BatchTransport for X 116 | where 117 | T: BatchTransport + ?Sized, 118 | X: std::ops::Deref, 119 | X: std::fmt::Debug, 120 | X: Clone, 121 | { 122 | type Batch = T::Batch; 123 | 124 | fn send_batch(&self, requests: I) -> Self::Batch 125 | where 126 | I: IntoIterator, 127 | { 128 | (**self).send_batch(requests) 129 | } 130 | } 131 | 132 | impl DuplexTransport for X 133 | where 134 | T: DuplexTransport + ?Sized, 135 | X: std::ops::Deref, 136 | X: std::fmt::Debug, 137 | X: Clone, 138 | { 139 | type NotificationStream = T::NotificationStream; 140 | 141 | fn subscribe(&self, id: api::SubscriptionId) -> error::Result { 142 | (**self).subscribe(id) 143 | } 144 | 145 | fn unsubscribe(&self, id: api::SubscriptionId) -> error::Result<()> { 146 | (**self).unsubscribe(id) 147 | } 148 | } 149 | 150 | #[cfg(test)] 151 | mod tests { 152 | use super::{error, rpc, RequestId, Transport}; 153 | 154 | use crate::api::Web3; 155 | use futures::future::BoxFuture; 156 | use std::sync::Arc; 157 | 158 | #[derive(Debug, Clone)] 159 | struct FakeTransport; 160 | 161 | impl Transport for FakeTransport { 162 | type Out = BoxFuture<'static, error::Result>; 163 | 164 | fn prepare(&self, _method: &str, _params: Vec) -> (RequestId, rpc::Call) { 165 | unimplemented!() 166 | } 167 | 168 | fn send(&self, _id: RequestId, _request: rpc::Call) -> Self::Out { 169 | unimplemented!() 170 | } 171 | } 172 | 173 | #[test] 174 | fn should_allow_to_use_arc_as_transport() { 175 | let transport = Arc::new(FakeTransport); 176 | let transport2 = transport.clone(); 177 | 178 | let _web3_1 = Web3::new(transport); 179 | let _web3_2 = Web3::new(transport2); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/api/mod.rs: -------------------------------------------------------------------------------- 1 | //! `Web3` implementation 2 | 3 | mod accounts; 4 | mod eth; 5 | mod eth_filter; 6 | mod eth_subscribe; 7 | mod net; 8 | mod parity; 9 | mod parity_accounts; 10 | mod parity_set; 11 | mod personal; 12 | mod traces; 13 | mod txpool; 14 | mod web3; 15 | 16 | pub use self::{ 17 | accounts::Accounts, 18 | eth::Eth, 19 | eth_filter::{BaseFilter, EthFilter}, 20 | eth_subscribe::{EthSubscribe, SubscriptionId, SubscriptionStream}, 21 | net::Net, 22 | parity::Parity, 23 | parity_accounts::ParityAccounts, 24 | parity_set::ParitySet, 25 | personal::Personal, 26 | traces::Traces, 27 | txpool::Txpool, 28 | web3::Web3 as Web3Api, 29 | }; 30 | 31 | use crate::{ 32 | confirm, error, 33 | types::{Bytes, TransactionReceipt, TransactionRequest, U64}, 34 | DuplexTransport, Transport, RequestId, Error, 35 | }; 36 | use futures::Future; 37 | use std::time::Duration; 38 | use jsonrpc_core::types::Call; 39 | 40 | /// Common API for all namespaces 41 | pub trait Namespace: Clone { 42 | /// Creates new API namespace 43 | fn new(transport: T) -> Self; 44 | 45 | /// Borrows a transport. 46 | fn transport(&self) -> &T; 47 | } 48 | 49 | /// `Web3` wrapper for all namespaces 50 | #[derive(Debug, Clone)] 51 | pub struct Web3 { 52 | transport: T, 53 | } 54 | 55 | impl Web3 { 56 | /// Create new `Web3` with given transport 57 | pub fn new(transport: T) -> Self { 58 | Web3 { transport } 59 | } 60 | 61 | /// Borrows a transport. 62 | pub fn transport(&self) -> &T { 63 | &self.transport 64 | } 65 | 66 | /// set the max response bytes 67 | pub fn set_max_response_bytes(&mut self, bytes: u64) { 68 | self.transport.set_max_response_bytes(bytes) 69 | } 70 | 71 | /// Access methods from custom namespace 72 | pub fn api>(&self) -> A { 73 | A::new(self.transport.clone()) 74 | } 75 | 76 | /// Access methods from `accounts` namespace 77 | pub fn accounts(&self) -> accounts::Accounts { 78 | self.api() 79 | } 80 | 81 | /// Access methods from `eth` namespace 82 | pub fn eth(&self) -> eth::Eth { 83 | self.api() 84 | } 85 | 86 | /// Access methods from `net` namespace 87 | pub fn net(&self) -> net::Net { 88 | self.api() 89 | } 90 | 91 | /// Access methods from `web3` namespace 92 | pub fn web3(&self) -> web3::Web3 { 93 | self.api() 94 | } 95 | 96 | /// Access filter methods from `eth` namespace 97 | pub fn eth_filter(&self) -> eth_filter::EthFilter { 98 | self.api() 99 | } 100 | 101 | /// Access methods from `parity` namespace 102 | pub fn parity(&self) -> parity::Parity { 103 | self.api() 104 | } 105 | 106 | /// Access methods from `parity_accounts` namespace 107 | pub fn parity_accounts(&self) -> parity_accounts::ParityAccounts { 108 | self.api() 109 | } 110 | 111 | /// Access methods from `parity_set` namespace 112 | pub fn parity_set(&self) -> parity_set::ParitySet { 113 | self.api() 114 | } 115 | 116 | /// Access methods from `personal` namespace 117 | pub fn personal(&self) -> personal::Personal { 118 | self.api() 119 | } 120 | 121 | /// Access methods from `trace` namespace 122 | pub fn trace(&self) -> traces::Traces { 123 | self.api() 124 | } 125 | 126 | /// Access methods from `txpool` namespace 127 | pub fn txpool(&self) -> txpool::Txpool { 128 | self.api() 129 | } 130 | 131 | /// Should be used to wait for confirmations 132 | pub async fn wait_for_confirmations( 133 | &self, 134 | poll_interval: Duration, 135 | confirmations: usize, 136 | check: V, 137 | ) -> error::Result<()> 138 | where 139 | F: Future>>, 140 | V: confirm::ConfirmationCheck, 141 | { 142 | confirm::wait_for_confirmations(self.eth(), self.eth_filter(), poll_interval, confirmations, check).await 143 | } 144 | 145 | /// Sends transaction and returns future resolved after transaction is confirmed 146 | pub async fn send_transaction_with_confirmation( 147 | &self, 148 | tx: TransactionRequest, 149 | poll_interval: Duration, 150 | confirmations: usize, 151 | ) -> error::Result { 152 | confirm::send_transaction_with_confirmation(self.transport.clone(), tx, poll_interval, confirmations).await 153 | } 154 | 155 | /// Sends raw transaction and returns future resolved after transaction is confirmed 156 | pub async fn send_raw_transaction_with_confirmation( 157 | &self, 158 | tx: Bytes, 159 | poll_interval: Duration, 160 | confirmations: usize, 161 | ) -> error::Result { 162 | confirm::send_raw_transaction_with_confirmation(self.transport.clone(), tx, poll_interval, confirmations).await 163 | } 164 | 165 | /// Call json rpc directly 166 | pub async fn json_rpc_call(&self, body: &str) -> error::Result { 167 | let request: Call = serde_json::from_str(body).map_err(|_| Error::Decoder(body.to_string()))?; 168 | // currently, the request id is not used 169 | self.transport 170 | .send(RequestId::default(), request) 171 | .await 172 | .map(|v| format!("{}", v)) 173 | } 174 | } 175 | 176 | impl Web3 { 177 | /// Access subscribe methods from `eth` namespace 178 | pub fn eth_subscribe(&self) -> eth_subscribe::EthSubscribe { 179 | self.api() 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/api/parity_accounts.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | api::Namespace, 3 | helpers::{self, CallFuture}, 4 | types::{Address, H256}, 5 | Transport, 6 | }; 7 | 8 | /// `Parity_Accounts` namespace 9 | #[derive(Debug, Clone)] 10 | pub struct ParityAccounts { 11 | transport: T, 12 | } 13 | 14 | impl Namespace for ParityAccounts { 15 | fn new(transport: T) -> Self 16 | where 17 | Self: Sized, 18 | { 19 | ParityAccounts { transport } 20 | } 21 | 22 | fn transport(&self) -> &T { 23 | &self.transport 24 | } 25 | } 26 | 27 | impl ParityAccounts { 28 | /// Given an address of an account and its password deletes the account from the parity node 29 | pub fn parity_kill_account(&self, address: &Address, pwd: &str) -> CallFuture { 30 | let address = helpers::serialize(&address); 31 | let pwd = helpers::serialize(&pwd); 32 | CallFuture::new(self.transport.execute("parity_killAccount", vec![address, pwd])) 33 | } 34 | /// Imports an account from a given seed/phrase 35 | /// Retunrs the address of the corresponding seed vinculated account 36 | pub fn parity_new_account_from_phrase(&self, seed: &str, pwd: &str) -> CallFuture { 37 | let seed = helpers::serialize(&seed); 38 | let pwd = helpers::serialize(&pwd); 39 | CallFuture::new(self.transport.execute("parity_newAccountFromPhrase", vec![seed, pwd])) 40 | } 41 | /// Imports an account from a given secret key. 42 | /// Returns the address of the corresponding Sk vinculated account. 43 | pub fn new_account_from_secret(&self, secret: &H256, pwd: &str) -> CallFuture { 44 | let secret = helpers::serialize(&secret); 45 | let pwd = helpers::serialize(&pwd); 46 | CallFuture::new(self.transport.execute("parity_newAccountFromSecret", vec![secret, pwd])) 47 | } 48 | /// Imports an account from a JSON encoded Wallet file. 49 | /// Returns the address of the corresponding wallet. 50 | pub fn parity_new_account_from_wallet(&self, wallet: &str, pwd: &str) -> CallFuture { 51 | let wallet = helpers::serialize(&wallet); 52 | let pwd = helpers::serialize(&pwd); 53 | CallFuture::new(self.transport.execute("parity_newAccountFromWallet", vec![wallet, pwd])) 54 | } 55 | /// Removes the address of the Parity node addressbook. 56 | /// Returns true if the operation suceeded. 57 | pub fn parity_remove_address(&self, address: &Address) -> CallFuture { 58 | let address = helpers::serialize(&address); 59 | CallFuture::new(self.transport.execute("parity_removeAddress", vec![address])) 60 | } 61 | } 62 | 63 | #[cfg(test)] 64 | mod tests { 65 | use super::ParityAccounts; 66 | use crate::{api::Namespace, rpc::Value}; 67 | use ethereum_types::{Address, H256}; 68 | 69 | rpc_test! ( 70 | ParityAccounts : parity_kill_account, &"9b776baeaf3896657a9ba0db5564623b3e0173e0".parse::
().unwrap(), "123456789" 71 | => "parity_killAccount", vec![r#""0x9b776baeaf3896657a9ba0db5564623b3e0173e0""#, r#""123456789""#]; 72 | Value::Bool(true) => true 73 | ); 74 | 75 | rpc_test! ( 76 | ParityAccounts : parity_new_account_from_phrase, "member funny cloth wrist ugly water tuition always fall recycle maze long", "123456789" 77 | => "parity_newAccountFromPhrase", vec![r#""member funny cloth wrist ugly water tuition always fall recycle maze long""#, r#""123456789""#]; 78 | Value::String("0xE43eD16390bd419d48B09d6E2aa20203D1eF93E1".into()) => "E43eD16390bd419d48B09d6E2aa20203D1eF93E1".parse::
().unwrap() 79 | ); 80 | 81 | rpc_test! ( 82 | ParityAccounts : new_account_from_secret, &"c6592108cc3577f6a2d6178bc6947b43db39057195802caa0120f26e39af4945".parse::().unwrap(), "123456789" 83 | => "parity_newAccountFromSecret", vec![r#""0xc6592108cc3577f6a2d6178bc6947b43db39057195802caa0120f26e39af4945""#, r#""123456789""#]; 84 | Value::String("0x9b776Baeaf3896657A9ba0db5564623B3E0173e0".into()) => "9b776Baeaf3896657A9ba0db5564623B3E0173e0".parse::
().unwrap() 85 | ); 86 | 87 | rpc_test! ( 88 | ParityAccounts : parity_new_account_from_wallet, r#"{"version":3,"id":"3b330c3b-b0b3-4e39-b62e-c2041a98d673","address":"4c8ab9d3e938285776d6717d7319f6a9b1d809dd","Crypto":{"ciphertext":"bb3a6dbf21f0bf2b5eb0b43426590f16650acee9462ab710cca18781691a5739","cipherparams":{"iv":"6a533f77fc5cb8a752a16ec6a3200da1"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"a58609853dec53c81feb165e346c700e714285771825bb4cbf87c4ea1996b682","n":8192,"r":8,"p":1},"mac":"a71edeb659ed628db13579ce9f75c80c9d386c1239b280548d9a0e58ad20d6c7"}}"#, "123456789" 89 | => "parity_newAccountFromWallet", vec![r#""{\"version\":3,\"id\":\"3b330c3b-b0b3-4e39-b62e-c2041a98d673\",\"address\":\"4c8ab9d3e938285776d6717d7319f6a9b1d809dd\",\"Crypto\":{\"ciphertext\":\"bb3a6dbf21f0bf2b5eb0b43426590f16650acee9462ab710cca18781691a5739\",\"cipherparams\":{\"iv\":\"6a533f77fc5cb8a752a16ec6a3200da1\"},\"cipher\":\"aes-128-ctr\",\"kdf\":\"scrypt\",\"kdfparams\":{\"dklen\":32,\"salt\":\"a58609853dec53c81feb165e346c700e714285771825bb4cbf87c4ea1996b682\",\"n\":8192,\"r\":8,\"p\":1},\"mac\":\"a71edeb659ed628db13579ce9f75c80c9d386c1239b280548d9a0e58ad20d6c7\"}}""#, r#""123456789""#]; 90 | Value::String("0x4C8aB9d3e938285776d6717d7319F6a9B1d809DD".into()) => "4C8aB9d3e938285776d6717d7319F6a9B1d809DD".parse::
().unwrap() 91 | ); 92 | 93 | rpc_test! ( 94 | ParityAccounts : parity_remove_address, &"9b776baeaf3896657a9ba0db5564623b3e0173e0".parse::
().unwrap() 95 | => "parity_removeAddress", vec![r#""0x9b776baeaf3896657a9ba0db5564623b3e0173e0""#]; 96 | Value::Bool(true) => true 97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /src/types/signed.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{AccessList, Address, Bytes, CallRequest, H256, U256, U64}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// Struct representing signed data returned from `Accounts::sign` method. 5 | #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] 6 | pub struct SignedData { 7 | /// The original message that was signed. 8 | pub message: Vec, 9 | /// The keccak256 hash of the signed data. 10 | #[serde(rename = "messageHash")] 11 | pub message_hash: H256, 12 | /// V value in 'Electrum' notation. 13 | pub v: u8, 14 | /// R value. 15 | pub r: H256, 16 | /// S value. 17 | pub s: H256, 18 | /// The signature bytes. 19 | pub signature: Bytes, 20 | } 21 | 22 | /// Transaction data for signing. 23 | /// 24 | /// The `Accounts::sign_transaction` method will fill optional fields with sane 25 | /// defaults when they are omitted. Specifically the signing account's current 26 | /// transaction count will be used for the `nonce`, the estimated recommended 27 | /// gas price will be used for `gas_price`, and the current network ID will be 28 | /// used for the `chain_id`. 29 | /// 30 | /// It is worth noting that the chain ID is not equivalent to the network ID. 31 | /// They happen to be the same much of the time but it is recommended to set 32 | /// this for signing transactions. 33 | /// 34 | /// `TransactionParameters` implements `Default` and uses `100_000` as the 35 | /// default `gas` to use for the transaction. This is more than enough for 36 | /// simple transactions sending ETH between accounts but may not be enough when 37 | /// interacting with complex contracts. It is recommended when interacting 38 | /// with contracts to use `Eth::estimate_gas` to estimate the required gas for 39 | /// the transaction. 40 | #[derive(Clone, Debug, PartialEq)] 41 | pub struct TransactionParameters { 42 | /// Transaction nonce (None for account transaction count) 43 | pub nonce: Option, 44 | /// To address 45 | pub to: Option
, 46 | /// Supplied gas 47 | pub gas: U256, 48 | /// Gas price (None for estimated gas price) 49 | pub gas_price: Option, 50 | /// Transferred value 51 | pub value: U256, 52 | /// Data 53 | pub data: Bytes, 54 | /// The chain ID (None for network ID) 55 | pub chain_id: Option, 56 | /// Transaction type, Some(1) for AccessList transaction, None for Legacy 57 | pub transaction_type: Option, 58 | /// Access list 59 | pub access_list: Option, 60 | /// Max fee per gas 61 | pub max_fee_per_gas: Option, 62 | /// miner bribe 63 | pub max_priority_fee_per_gas: Option, 64 | } 65 | 66 | /// The default fas for transactions. 67 | /// 68 | /// Unfortunately there is no way to construct `U256`s with const functions for 69 | /// constants so we just build it from it's `u64` words. Note that there is a 70 | /// unit test to verify that it is constructed correctly and holds the expected 71 | /// value of 100_000. 72 | const TRANSACTION_DEFAULT_GAS: U256 = U256([100_000, 0, 0, 0]); 73 | 74 | impl Default for TransactionParameters { 75 | fn default() -> Self { 76 | TransactionParameters { 77 | nonce: None, 78 | to: None, 79 | gas: TRANSACTION_DEFAULT_GAS, 80 | gas_price: None, 81 | value: U256::zero(), 82 | data: Bytes::default(), 83 | chain_id: None, 84 | transaction_type: None, 85 | access_list: None, 86 | max_fee_per_gas: None, 87 | max_priority_fee_per_gas: None, 88 | } 89 | } 90 | } 91 | 92 | impl From for TransactionParameters { 93 | fn from(call: CallRequest) -> Self { 94 | TransactionParameters { 95 | nonce: None, 96 | to: call.to, 97 | gas: call.gas.unwrap_or(TRANSACTION_DEFAULT_GAS), 98 | gas_price: call.gas_price, 99 | value: call.value.unwrap_or_default(), 100 | data: call.data.unwrap_or_default(), 101 | chain_id: None, 102 | transaction_type: call.transaction_type, 103 | access_list: call.access_list, 104 | max_fee_per_gas: call.max_fee_per_gas, 105 | max_priority_fee_per_gas: call.max_priority_fee_per_gas, 106 | } 107 | } 108 | } 109 | 110 | impl From for CallRequest { 111 | fn from(val: TransactionParameters) -> Self { 112 | CallRequest { 113 | from: None, 114 | to: val.to, 115 | gas: Some(val.gas), 116 | gas_price: val.gas_price, 117 | value: Some(val.value), 118 | data: Some(val.data), 119 | transaction_type: val.transaction_type, 120 | access_list: val.access_list, 121 | max_fee_per_gas: val.max_fee_per_gas, 122 | max_priority_fee_per_gas: val.max_priority_fee_per_gas, 123 | } 124 | } 125 | } 126 | 127 | /// Data for offline signed transaction 128 | #[derive(Clone, Debug, PartialEq)] 129 | pub struct SignedTransaction { 130 | /// The given message hash 131 | pub message_hash: H256, 132 | /// V value with chain replay protection. 133 | pub v: u64, 134 | /// R value. 135 | pub r: H256, 136 | /// S value. 137 | pub s: H256, 138 | /// The raw signed transaction ready to be sent with `send_raw_transaction` 139 | pub raw_transaction: Bytes, 140 | /// The transaction hash for the RLP encoded transaction. 141 | pub transaction_hash: H256, 142 | } 143 | 144 | #[cfg(test)] 145 | mod tests { 146 | use super::*; 147 | 148 | #[test] 149 | fn verify_transaction_default_gas() { 150 | assert_eq!(TRANSACTION_DEFAULT_GAS, U256::from(100_000)); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/api/parity.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | api::Namespace, 3 | helpers::{self, CallFuture}, 4 | rpc::Value, 5 | types::{Bytes, CallRequest, ParityPendingTransactionFilter, Transaction}, 6 | Transport, 7 | }; 8 | 9 | /// `Parity` namespace 10 | #[derive(Debug, Clone)] 11 | pub struct Parity { 12 | transport: T, 13 | } 14 | 15 | impl Namespace for Parity { 16 | fn new(transport: T) -> Self 17 | where 18 | Self: Sized, 19 | { 20 | Parity { transport } 21 | } 22 | 23 | fn transport(&self) -> &T { 24 | &self.transport 25 | } 26 | } 27 | 28 | impl Parity { 29 | /// Sequentially call multiple contract methods in one request without changing the state of the blockchain. 30 | pub fn call(&self, reqs: Vec) -> CallFuture, T::Out> { 31 | let reqs = helpers::serialize(&reqs); 32 | 33 | CallFuture::new(self.transport.execute("parity_call", vec![reqs])) 34 | } 35 | 36 | /// Get pending transactions 37 | /// Blocked by https://github.com/openethereum/openethereum/issues/159 38 | pub fn pending_transactions( 39 | &self, 40 | limit: Option, 41 | filter: Option, 42 | ) -> CallFuture, T::Out> { 43 | let limit = limit.map(Value::from); 44 | let filter = filter.as_ref().map(helpers::serialize); 45 | let params = match (limit, filter) { 46 | (l, Some(f)) => vec![l.unwrap_or(Value::Null), f], 47 | (Some(l), None) => vec![l], 48 | _ => vec![], 49 | }; 50 | 51 | CallFuture::new(self.transport.execute("parity_pendingTransactions", params)) 52 | } 53 | } 54 | 55 | #[cfg(test)] 56 | mod tests { 57 | use super::Parity; 58 | use crate::{ 59 | api::Namespace, 60 | rpc::Value, 61 | types::{Address, CallRequest, FilterCondition, ParityPendingTransactionFilter, Transaction, U64}, 62 | }; 63 | use hex_literal::hex; 64 | 65 | const EXAMPLE_PENDING_TX: &str = r#"{ 66 | "blockHash": null, 67 | "blockNumber": null, 68 | "creates": null, 69 | "from": "0xee3ea02840129123d5397f91be0391283a25bc7d", 70 | "gas": "0x23b58", 71 | "gasPrice": "0xba43b7400", 72 | "hash": "0x160b3c30ab1cf5871083f97ee1cee3901cfba3b0a2258eb337dd20a7e816b36e", 73 | "input": "0x095ea7b3000000000000000000000000bf4ed7b27f1d666546e30d74d50d173d20bca75400000000000000000000000000002643c948210b4bd99244ccd64d5555555555", 74 | "condition": { 75 | "block": 1 76 | }, 77 | "chainId": 1, 78 | "nonce": "0x5", 79 | "publicKey": "0x96157302dade55a1178581333e57d60ffe6fdf5a99607890456a578b4e6b60e335037d61ed58aa4180f9fd747dc50d44a7924aa026acbfb988b5062b629d6c36", 80 | "r": "0x92e8beb19af2bad0511d516a86e77fa73004c0811b2173657a55797bdf8558e1", 81 | "raw": "0xf8aa05850ba43b740083023b5894bb9bc244d798123fde783fcc1c72d3bb8c18941380b844095ea7b3000000000000000000000000bf4ed7b27f1d666546e30d74d50d173d20bca75400000000000000000000000000002643c948210b4bd99244ccd64d555555555526a092e8beb19af2bad0511d516a86e77fa73004c0811b2173657a55797bdf8558e1a062b4d4d125bbcb9c162453bc36ca156537543bb4414d59d1805d37fb63b351b8", 82 | "s": "0x62b4d4d125bbcb9c162453bc36ca156537543bb4414d59d1805d37fb63b351b8", 83 | "standardV": "0x1", 84 | "to": "0xbb9bc244d798123fde783fcc1c72d3bb8c189413", 85 | "transactionIndex": null, 86 | "v": "0x26", 87 | "value": "0x0" 88 | }"#; 89 | 90 | rpc_test!( 91 | Parity:call, 92 | vec![ 93 | CallRequest { 94 | from: None, 95 | to: Some(Address::from_low_u64_be(0x123)), 96 | gas: None, 97 | gas_price: None, 98 | value: Some(0x1.into()), 99 | data: None, 100 | transaction_type: None, 101 | access_list: None, 102 | max_fee_per_gas: None, 103 | max_priority_fee_per_gas: None, 104 | }, 105 | CallRequest { 106 | from: Some(Address::from_low_u64_be(0x321)), 107 | to: Some(Address::from_low_u64_be(0x123)), 108 | gas: None, 109 | gas_price: None, 110 | value: None, 111 | data: Some(hex!("0493").into()), 112 | transaction_type: None, 113 | access_list: None, 114 | max_fee_per_gas: None, 115 | max_priority_fee_per_gas: None, 116 | }, 117 | CallRequest { 118 | from: None, 119 | to: Some(Address::from_low_u64_be(0x765)), 120 | gas: None, 121 | gas_price: None, 122 | value: Some(0x5.into()), 123 | data: Some(hex!("0723").into()), 124 | transaction_type: None, 125 | access_list: None, 126 | max_fee_per_gas: None, 127 | max_priority_fee_per_gas: None, 128 | } 129 | ] => "parity_call", vec![ 130 | r#"[{"to":"0x0000000000000000000000000000000000000123","value":"0x1"},{"data":"0x0493","from":"0x0000000000000000000000000000000000000321","to":"0x0000000000000000000000000000000000000123"},{"data":"0x0723","to":"0x0000000000000000000000000000000000000765","value":"0x5"}]"# 131 | ]; 132 | Value::Array(vec![Value::String("0x010203".into()), Value::String("0x7198ab".into()), Value::String("0xde763f".into())]) => vec![hex!("010203").into(), hex!("7198ab").into(), hex!("de763f").into()] 133 | ); 134 | 135 | rpc_test!( 136 | Parity:pending_transactions, 137 | 1, 138 | ParityPendingTransactionFilter::builder() 139 | .from(Address::from_low_u64_be(0x32)) 140 | .gas(U64::from(100_000)) 141 | .gas_price(FilterCondition::GreaterThan(U64::from(100_000_000_000_u64))) 142 | .build() 143 | => "parity_pendingTransactions", 144 | vec![r#"1"#, r#"{"from":{"eq":"0x0000000000000000000000000000000000000032"},"gas":{"eq":"0x186a0"},"gas_price":{"gt":"0x174876e800"}}"#] 145 | ; 146 | Value::Array(vec![::serde_json::from_str(EXAMPLE_PENDING_TX).unwrap()]) 147 | => vec![::serde_json::from_str::(EXAMPLE_PENDING_TX).unwrap()] 148 | ); 149 | } 150 | -------------------------------------------------------------------------------- /src/types/uint.rs: -------------------------------------------------------------------------------- 1 | pub use ethereum_types::{BigEndianHash, Bloom as H2048, H128, H160, H256, H512, H520, H64, U128, U256, U64}; 2 | 3 | 4 | #[cfg(test)] 5 | mod tests { 6 | use super::*; 7 | use wasm_bindgen_test::*; 8 | 9 | type Res = Result; 10 | 11 | #[test] 12 | fn should_compare_correctly() { 13 | let mut arr = [0u8; 32]; 14 | arr[31] = 0; 15 | arr[30] = 15; 16 | arr[29] = 1; 17 | arr[28] = 0; 18 | arr[27] = 10; 19 | let a = U256::from(arr.as_ref()); 20 | arr[27] = 9; 21 | let b = U256::from(arr.as_ref()); 22 | let c = U256::from(0); 23 | let d = U256::from(10_000); 24 | 25 | assert!(b < a); 26 | assert!(d < a); 27 | assert!(d < b); 28 | assert!(c < a); 29 | assert!(c < b); 30 | assert!(c < d); 31 | } 32 | 33 | #[test] 34 | fn should_display_correctly() { 35 | let mut arr = [0u8; 32]; 36 | arr[31] = 0; 37 | arr[30] = 15; 38 | arr[29] = 1; 39 | arr[28] = 0; 40 | arr[27] = 10; 41 | let a = U256::from(arr.as_ref()); 42 | let b = U256::from(1023); 43 | let c = U256::from(0); 44 | let d = U256::from(10000); 45 | 46 | // Debug 47 | assert_eq!(&format!("{:?}", a), "42949742336"); 48 | assert_eq!(&format!("{:?}", b), "1023"); 49 | assert_eq!(&format!("{:?}", c), "0"); 50 | assert_eq!(&format!("{:?}", d), "10000"); 51 | 52 | // Display 53 | assert_eq!(&format!("{}", a), "42949742336"); 54 | assert_eq!(&format!("{}", b), "1023"); 55 | assert_eq!(&format!("{}", c), "0"); 56 | assert_eq!(&format!("{}", d), "10000"); 57 | 58 | // Lowerhex 59 | assert_eq!(&format!("{:x}", a), "a00010f00"); 60 | assert_eq!(&format!("{:x}", b), "3ff"); 61 | assert_eq!(&format!("{:x}", c), "0"); 62 | assert_eq!(&format!("{:x}", d), "2710"); 63 | 64 | // Prefixed Lowerhex 65 | assert_eq!(&format!("{:#x}", a), "0xa00010f00"); 66 | assert_eq!(&format!("{:#x}", b), "0x3ff"); 67 | assert_eq!(&format!("{:#x}", c), "0x0"); 68 | assert_eq!(&format!("{:#x}", d), "0x2710"); 69 | } 70 | 71 | #[test] 72 | fn should_display_hash_correctly() { 73 | let mut arr = [0; 16]; 74 | arr[15] = 0; 75 | arr[14] = 15; 76 | arr[13] = 1; 77 | arr[12] = 0; 78 | arr[11] = 10; 79 | let a = H128::from_uint(&arr.into()); 80 | let b = H128::from_uint(&1023.into()); 81 | let c = H128::from_uint(&0.into()); 82 | let d = H128::from_uint(&10000.into()); 83 | 84 | // Debug 85 | assert_eq!(&format!("{:?}", a), "0x00000000000000000000000a00010f00"); 86 | assert_eq!(&format!("{:?}", b), "0x000000000000000000000000000003ff"); 87 | assert_eq!(&format!("{:?}", c), "0x00000000000000000000000000000000"); 88 | assert_eq!(&format!("{:?}", d), "0x00000000000000000000000000002710"); 89 | 90 | // Display 91 | assert_eq!(&format!("{}", a), "0x0000…0f00"); 92 | assert_eq!(&format!("{}", b), "0x0000…03ff"); 93 | assert_eq!(&format!("{}", c), "0x0000…0000"); 94 | assert_eq!(&format!("{}", d), "0x0000…2710"); 95 | 96 | // Lowerhex 97 | assert_eq!(&format!("{:x}", a), "00000000000000000000000a00010f00"); 98 | assert_eq!(&format!("{:x}", b), "000000000000000000000000000003ff"); 99 | assert_eq!(&format!("{:x}", c), "00000000000000000000000000000000"); 100 | assert_eq!(&format!("{:x}", d), "00000000000000000000000000002710"); 101 | } 102 | 103 | #[test] 104 | fn should_deserialize_hash_correctly() { 105 | let deserialized1: H128 = serde_json::from_str(r#""0x00000000000000000000000a00010f00""#).unwrap(); 106 | 107 | assert_eq!(deserialized1, H128::from_low_u64_be(0xa00010f00)); 108 | } 109 | 110 | #[test] 111 | fn should_serialize_u256() { 112 | let serialized1 = serde_json::to_string(&U256::from(0)).unwrap(); 113 | let serialized2 = serde_json::to_string(&U256::from(1)).unwrap(); 114 | let serialized3 = serde_json::to_string(&U256::from(16)).unwrap(); 115 | let serialized4 = serde_json::to_string(&U256::from(256)).unwrap(); 116 | 117 | assert_eq!(serialized1, r#""0x0""#); 118 | assert_eq!(serialized2, r#""0x1""#); 119 | assert_eq!(serialized3, r#""0x10""#); 120 | assert_eq!(serialized4, r#""0x100""#); 121 | } 122 | 123 | #[test] 124 | fn should_succesfully_deserialize_decimals() { 125 | let deserialized1: Res = serde_json::from_str(r#""""#); 126 | let deserialized2: Res = serde_json::from_str(r#""0""#); 127 | let deserialized3: Res = serde_json::from_str(r#""10""#); 128 | let deserialized4: Res = serde_json::from_str(r#""1000000""#); 129 | let deserialized5: Res = serde_json::from_str(r#""1000000000000000000""#); 130 | 131 | assert!(deserialized1.is_err()); 132 | assert!(deserialized2.is_ok()); 133 | assert!(deserialized3.is_ok()); 134 | assert!(deserialized4.is_ok()); 135 | assert!(deserialized5.is_ok()); 136 | } 137 | 138 | #[test] 139 | fn should_deserialize_u256() { 140 | let deserialized1: Result = serde_json::from_str(r#""0x""#); 141 | let deserialized2: U256 = serde_json::from_str(r#""0x0""#).unwrap(); 142 | let deserialized3: U256 = serde_json::from_str(r#""0x1""#).unwrap(); 143 | let deserialized4: U256 = serde_json::from_str(r#""0x01""#).unwrap(); 144 | let deserialized5: U256 = serde_json::from_str(r#""0x100""#).unwrap(); 145 | 146 | assert!(deserialized1.is_err()); 147 | assert_eq!(deserialized2, 0.into()); 148 | assert_eq!(deserialized3, 1.into()); 149 | assert_eq!(deserialized4, 1.into()); 150 | assert_eq!(deserialized5, 256.into()); 151 | } 152 | 153 | #[test] 154 | fn test_to_from_u64() { 155 | assert_eq!(1u64, U256::from(1u64).low_u64()); 156 | assert_eq!(11u64, U256::from(11u64).low_u64()); 157 | assert_eq!(111u64, U256::from(111u64).low_u64()); 158 | } 159 | 160 | // Getting random numbers uses a different code path in JS, so we sanity 161 | // check it here. 162 | #[wasm_bindgen_test] 163 | fn random_doesnt_panic() { 164 | H160::random(); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/types/traces.rs: -------------------------------------------------------------------------------- 1 | //! Types for the Parity Ad-Hoc Trace API 2 | 3 | use crate::types::{Action, ActionType, Bytes, Res, H160, H256, U256}; 4 | use serde::{Deserialize, Serialize}; 5 | use std::collections::BTreeMap; 6 | 7 | #[derive(Debug, Clone, Serialize)] 8 | /// Description of the type of trace to make 9 | pub enum TraceType { 10 | /// Transaction Trace 11 | #[serde(rename = "trace")] 12 | Trace, 13 | /// Virtual Machine Execution Trace 14 | #[serde(rename = "vmTrace")] 15 | VmTrace, 16 | /// State Difference 17 | #[serde(rename = "stateDiff")] 18 | StateDiff, 19 | } 20 | 21 | #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] 22 | /// Ad-Hoc trace API type 23 | pub struct BlockTrace { 24 | /// Output Bytes 25 | pub output: Bytes, 26 | /// Transaction Trace 27 | pub trace: Option>, 28 | /// Virtual Machine Execution Trace 29 | #[serde(rename = "vmTrace")] 30 | pub vm_trace: Option, 31 | /// State Difference 32 | #[serde(rename = "stateDiff")] 33 | pub state_diff: Option, 34 | /// Transaction Hash 35 | #[serde(rename = "transactionHash")] 36 | pub transaction_hash: Option, 37 | } 38 | 39 | //---------------- State Diff ---------------- 40 | /// Aux type for Diff::Changed. 41 | #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] 42 | pub struct ChangedType { 43 | /// Previous value. 44 | pub from: T, 45 | /// Current value. 46 | pub to: T, 47 | } 48 | 49 | /// Serde-friendly `Diff` shadow. 50 | #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] 51 | pub enum Diff { 52 | /// No change. 53 | #[serde(rename = "=")] 54 | Same, 55 | /// A new value has been set. 56 | #[serde(rename = "+")] 57 | Born(T), 58 | /// A value has been removed. 59 | #[serde(rename = "-")] 60 | Died(T), 61 | /// Value changed. 62 | #[serde(rename = "*")] 63 | Changed(ChangedType), 64 | } 65 | 66 | /// Serde-friendly `AccountDiff` shadow. 67 | #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] 68 | pub struct AccountDiff { 69 | /// Account balance. 70 | pub balance: Diff, 71 | /// Account nonce. 72 | pub nonce: Diff, 73 | /// Account code. 74 | pub code: Diff, 75 | /// Account storage. 76 | pub storage: BTreeMap>, 77 | } 78 | 79 | /// Serde-friendly `StateDiff` shadow. 80 | #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] 81 | pub struct StateDiff(pub BTreeMap); 82 | 83 | // ------------------ Trace ------------- 84 | /// Trace 85 | #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] 86 | pub struct TransactionTrace { 87 | /// Trace address 88 | #[serde(rename = "traceAddress")] 89 | pub trace_address: Vec, 90 | /// Subtraces 91 | pub subtraces: usize, 92 | /// Action 93 | pub action: Action, 94 | /// Action Type 95 | #[serde(rename = "type")] 96 | pub action_type: ActionType, 97 | /// Result 98 | pub result: Option, 99 | /// Error 100 | pub error: Option, 101 | } 102 | 103 | // ---------------- VmTrace ------------------------------ 104 | #[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)] 105 | /// A record of a full VM trace for a CALL/CREATE. 106 | pub struct VMTrace { 107 | /// The code to be executed. 108 | pub code: Bytes, 109 | /// The operations executed. 110 | pub ops: Vec, 111 | } 112 | 113 | #[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)] 114 | /// A record of the execution of a single VM operation. 115 | pub struct VMOperation { 116 | /// The program counter. 117 | pub pc: usize, 118 | /// The gas cost for this instruction. 119 | pub cost: u64, 120 | /// Information concerning the execution of the operation. 121 | pub ex: Option, 122 | /// Subordinate trace of the CALL/CREATE if applicable. 123 | // #[serde(bound="VMTrace: Deserialize")] 124 | pub sub: Option, 125 | } 126 | 127 | #[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)] 128 | /// A record of an executed VM operation. 129 | pub struct VMExecutedOperation { 130 | /// The total gas used. 131 | #[serde(rename = "used")] 132 | pub used: u64, 133 | /// The stack item placed, if any. 134 | pub push: Vec, 135 | /// If altered, the memory delta. 136 | #[serde(rename = "mem")] 137 | pub mem: Option, 138 | /// The altered storage value, if any. 139 | #[serde(rename = "store")] 140 | pub store: Option, 141 | } 142 | 143 | #[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)] 144 | /// A diff of some chunk of memory. 145 | pub struct MemoryDiff { 146 | /// Offset into memory the change begins. 147 | pub off: usize, 148 | /// The changed data. 149 | pub data: Bytes, 150 | } 151 | 152 | #[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)] 153 | /// A diff of some storage value. 154 | pub struct StorageDiff { 155 | /// Which key in storage is changed. 156 | pub key: U256, 157 | /// What the value has been changed to. 158 | pub val: U256, 159 | } 160 | 161 | #[cfg(test)] 162 | mod tests { 163 | use super::*; 164 | 165 | // tx: https://etherscan.io/tx/0x4a91b11dbd2b11c308cfe7775eac2036f20c501691e3f8005d83b2dcce62d6b5 166 | // using the 'trace_replayTransaction' API function 167 | // with 'trace', 'vmTrace', 'stateDiff' 168 | const EXAMPLE_TRACE: &str = include!("./example-trace-str.rs"); 169 | 170 | // block: https://etherscan.io/block/46147 171 | // using the 'trace_replayBlockTransactions' API function 172 | // with 'trace', 'vmTrace', 'stateDiff' 173 | const EXAMPLE_TRACES: &str = include!("./example-traces-str.rs"); 174 | 175 | #[test] 176 | fn test_serialize_trace_type() { 177 | let trace_type_str = r#"["trace","vmTrace","stateDiff"]"#; 178 | let trace_type = vec![TraceType::Trace, TraceType::VmTrace, TraceType::StateDiff]; 179 | 180 | let se_trace_str: String = serde_json::to_string(&trace_type).unwrap(); 181 | assert_eq!(trace_type_str, se_trace_str); 182 | } 183 | 184 | #[test] 185 | fn test_deserialize_blocktrace() { 186 | let _trace: BlockTrace = serde_json::from_str(EXAMPLE_TRACE).unwrap(); 187 | } 188 | 189 | #[test] 190 | fn test_deserialize_blocktraces() { 191 | let _traces: Vec = serde_json::from_str(EXAMPLE_TRACES).unwrap(); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/helpers.rs: -------------------------------------------------------------------------------- 1 | //! Web3 helpers. 2 | 3 | use crate::{error, rpc, Error}; 4 | use futures::{ 5 | task::{Context, Poll}, 6 | Future, 7 | }; 8 | use pin_project::pin_project; 9 | use serde::de::DeserializeOwned; 10 | use std::{marker::PhantomData, pin::Pin}; 11 | 12 | /// Takes any type which is deserializable from rpc::Value and such a value and 13 | /// yields the deserialized value 14 | pub fn decode(value: rpc::Value) -> error::Result { 15 | serde_json::from_value(value).map_err(Into::into) 16 | } 17 | 18 | /// Calls decode on the result of the wrapped future. 19 | #[pin_project] 20 | #[derive(Debug)] 21 | pub struct CallFuture { 22 | #[pin] 23 | inner: F, 24 | _marker: PhantomData, 25 | } 26 | 27 | impl CallFuture { 28 | /// Create a new CallFuture wrapping the inner future. 29 | pub fn new(inner: F) -> Self { 30 | CallFuture { 31 | inner, 32 | _marker: PhantomData, 33 | } 34 | } 35 | } 36 | 37 | impl Future for CallFuture 38 | where 39 | T: serde::de::DeserializeOwned, 40 | F: Future>, 41 | { 42 | type Output = error::Result; 43 | 44 | fn poll(self: Pin<&mut Self>, ctx: &mut Context) -> Poll { 45 | let this = self.project(); 46 | let x = ready!(this.inner.poll(ctx)); 47 | Poll::Ready(x.and_then(decode)) 48 | } 49 | } 50 | 51 | /// Serialize a type. Panics if the type is returns error during serialization. 52 | pub fn serialize(t: &T) -> rpc::Value { 53 | serde_json::to_value(t).expect("Types never fail to serialize.") 54 | } 55 | 56 | /// Serializes a request to string. Panics if the type returns error during serialization. 57 | pub fn to_string(request: &T) -> String { 58 | serde_json::to_string(&request).expect("String serialization never fails.") 59 | } 60 | 61 | /// Build a JSON-RPC request. 62 | pub fn build_request(id: usize, method: &str, params: Vec) -> rpc::Call { 63 | rpc::Call::MethodCall(rpc::MethodCall { 64 | jsonrpc: Some(rpc::Version::V2), 65 | method: method.into(), 66 | params: rpc::Params::Array(params), 67 | id: rpc::Id::Num(id as u64), 68 | }) 69 | } 70 | 71 | /// Parse bytes slice into JSON-RPC response. 72 | /// It looks for arbitrary_precision feature as a temporary workaround for https://github.com/tomusdrw/rust-web3/issues/460. 73 | pub fn to_response_from_slice(response: &[u8]) -> error::Result { 74 | arbitrary_precision_deserialize_workaround(response).map_err(|e| Error::InvalidResponse(format!("{:?}", e))) 75 | } 76 | 77 | /// Deserialize bytes into T. 78 | /// It looks for arbitrary_precision feature as a temporary workaround for https://github.com/tomusdrw/rust-web3/issues/460. 79 | pub fn arbitrary_precision_deserialize_workaround(bytes: &[u8]) -> Result 80 | where 81 | T: DeserializeOwned, 82 | { 83 | if cfg!(feature = "arbitrary_precision") { 84 | serde_json::from_value(serde_json::from_slice(bytes)?) 85 | } else { 86 | serde_json::from_slice(bytes) 87 | } 88 | } 89 | 90 | /// Parse bytes slice into JSON-RPC notification. 91 | pub fn to_notification_from_slice(notification: &[u8]) -> error::Result { 92 | serde_json::from_slice(notification).map_err(|e| error::Error::InvalidResponse(format!("{:?}", e))) 93 | } 94 | 95 | /// Parse a Vec of `rpc::Output` into `Result`. 96 | pub fn to_results_from_outputs(outputs: Vec) -> error::Result>> { 97 | Ok(outputs.into_iter().map(to_result_from_output).collect()) 98 | } 99 | 100 | /// Parse `rpc::Output` into `Result`. 101 | pub fn to_result_from_output(output: rpc::Output) -> error::Result { 102 | match output { 103 | rpc::Output::Success(success) => Ok(success.result), 104 | rpc::Output::Failure(failure) => Err(error::Error::Rpc(failure.error)), 105 | } 106 | } 107 | 108 | #[macro_use] 109 | #[cfg(test)] 110 | pub mod tests { 111 | macro_rules! rpc_test { 112 | // With parameters 113 | ( 114 | $namespace: ident : $name: ident : $test_name: ident $(, $param: expr)+ => $method: expr, $results: expr; 115 | $returned: expr => $expected: expr 116 | ) => { 117 | #[test] 118 | fn $test_name() { 119 | // given 120 | let mut transport = $crate::transports::test::TestTransport::default(); 121 | transport.set_response($returned); 122 | let result = { 123 | let eth = $namespace::new(&transport); 124 | 125 | // when 126 | eth.$name($($param.into(), )+) 127 | }; 128 | 129 | // then 130 | transport.assert_request($method, &$results.into_iter().map(Into::into).collect::>()); 131 | transport.assert_no_more_requests(); 132 | let result = futures::executor::block_on(result); 133 | assert_eq!(result, Ok($expected.into())); 134 | } 135 | }; 136 | // With parameters (implicit test name) 137 | ( 138 | $namespace: ident : $name: ident $(, $param: expr)+ => $method: expr, $results: expr; 139 | $returned: expr => $expected: expr 140 | ) => { 141 | rpc_test! ( 142 | $namespace : $name : $name $(, $param)+ => $method, $results; 143 | $returned => $expected 144 | ); 145 | }; 146 | 147 | // No params entry point (explicit name) 148 | ( 149 | $namespace: ident: $name: ident: $test_name: ident => $method: expr; 150 | $returned: expr => $expected: expr 151 | ) => { 152 | #[test] 153 | fn $test_name() { 154 | // given 155 | let mut transport = $crate::transports::test::TestTransport::default(); 156 | transport.set_response($returned); 157 | let result = { 158 | let eth = $namespace::new(&transport); 159 | 160 | // when 161 | eth.$name() 162 | }; 163 | 164 | // then 165 | transport.assert_request($method, &[]); 166 | transport.assert_no_more_requests(); 167 | let result = futures::executor::block_on(result); 168 | assert_eq!(result, Ok($expected.into())); 169 | } 170 | }; 171 | 172 | // No params entry point 173 | ( 174 | $namespace: ident: $name: ident => $method: expr; 175 | $returned: expr => $expected: expr 176 | ) => { 177 | rpc_test! ( 178 | $namespace: $name: $name => $method; 179 | $returned => $expected 180 | ); 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/types/recovery.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{SignedData, SignedTransaction, H256}; 2 | use std::{ 3 | error::Error, 4 | fmt::{Display, Formatter, Result as FmtResult}, 5 | }; 6 | 7 | /// Data for recovering the public address of signed data. 8 | /// 9 | /// Note that the signature data is in 'Electrum' notation and may have chain 10 | /// replay protection applied. That means that `v` is expected to be `27`, `28`, 11 | /// or `35 + chain_id * 2` or `36 + chain_id * 2`. 12 | #[derive(Clone, Debug, PartialEq)] 13 | pub struct Recovery { 14 | /// The message to recover 15 | pub message: RecoveryMessage, 16 | /// V value. 17 | pub v: u64, 18 | /// R value. 19 | pub r: H256, 20 | /// S value. 21 | pub s: H256, 22 | } 23 | 24 | impl Recovery { 25 | /// Creates new recovery data from its parts. 26 | pub fn new(message: M, v: u64, r: H256, s: H256) -> Recovery 27 | where 28 | M: Into, 29 | { 30 | Recovery { 31 | message: message.into(), 32 | v, 33 | r, 34 | s, 35 | } 36 | } 37 | 38 | /// Creates new recovery data from a raw signature. 39 | /// 40 | /// This parses a raw signature which is expected to be 65 bytes long where 41 | /// the first 32 bytes is the `r` value, the second 32 bytes the `s` value 42 | /// and the final byte is the `v` value in 'Electrum' notation. 43 | pub fn from_raw_signature(message: M, raw_signature: B) -> Result 44 | where 45 | M: Into, 46 | B: AsRef<[u8]>, 47 | { 48 | let bytes = raw_signature.as_ref(); 49 | 50 | if bytes.len() != 65 { 51 | return Err(ParseSignatureError); 52 | } 53 | 54 | let v = bytes[64]; 55 | let r = H256::from_slice(&bytes[0..32]); 56 | let s = H256::from_slice(&bytes[32..64]); 57 | 58 | Ok(Recovery::new(message, v as _, r, s)) 59 | } 60 | 61 | /// Retrieve the Recovery Id ("Standard V") 62 | /// 63 | /// Returns `None` if `v` value is invalid 64 | /// (equivalent of returning `4` in some implementaions). 65 | pub fn recovery_id(&self) -> Option { 66 | match self.v { 67 | 27 => Some(0), 68 | 28 => Some(1), 69 | v if v >= 35 => Some(((v - 1) % 2) as _), 70 | _ => None, 71 | } 72 | } 73 | 74 | /// Retrieves the recovery id & compact signature in it's raw form. 75 | pub fn as_signature(&self) -> Option<([u8; 64], i32)> { 76 | let recovery_id = self.recovery_id()?; 77 | let signature = { 78 | let mut sig = [0u8; 64]; 79 | sig[..32].copy_from_slice(self.r.as_bytes()); 80 | sig[32..].copy_from_slice(self.s.as_bytes()); 81 | sig 82 | }; 83 | 84 | Some((signature, recovery_id)) 85 | } 86 | } 87 | 88 | impl<'a> From<&'a SignedData> for Recovery { 89 | fn from(signed: &'a SignedData) -> Self { 90 | Recovery::new(signed.message_hash, signed.v as _, signed.r, signed.s) 91 | } 92 | } 93 | 94 | impl<'a> From<&'a SignedTransaction> for Recovery { 95 | fn from(tx: &'a SignedTransaction) -> Self { 96 | Recovery::new(tx.message_hash, tx.v, tx.r, tx.s) 97 | } 98 | } 99 | 100 | /// Recovery message data. 101 | /// 102 | /// The message data can either be a binary message that is first hashed 103 | /// according to EIP-191 and then recovered based on the signature or a 104 | /// precomputed hash. 105 | #[derive(Clone, Debug, PartialEq)] 106 | pub enum RecoveryMessage { 107 | /// Message bytes 108 | Data(Vec), 109 | /// Message hash 110 | Hash(H256), 111 | } 112 | 113 | impl From<&[u8]> for RecoveryMessage { 114 | fn from(s: &[u8]) -> Self { 115 | s.to_owned().into() 116 | } 117 | } 118 | 119 | impl From> for RecoveryMessage { 120 | fn from(s: Vec) -> Self { 121 | RecoveryMessage::Data(s) 122 | } 123 | } 124 | 125 | impl From<&str> for RecoveryMessage { 126 | fn from(s: &str) -> Self { 127 | s.as_bytes().to_owned().into() 128 | } 129 | } 130 | 131 | impl From for RecoveryMessage { 132 | fn from(s: String) -> Self { 133 | RecoveryMessage::Data(s.into_bytes()) 134 | } 135 | } 136 | 137 | impl From<[u8; 32]> for RecoveryMessage { 138 | fn from(hash: [u8; 32]) -> Self { 139 | H256(hash).into() 140 | } 141 | } 142 | 143 | impl From for RecoveryMessage { 144 | fn from(hash: H256) -> Self { 145 | RecoveryMessage::Hash(hash) 146 | } 147 | } 148 | 149 | /// An error parsing a raw signature. 150 | #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] 151 | pub struct ParseSignatureError; 152 | 153 | impl Display for ParseSignatureError { 154 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 155 | write!(f, "error parsing raw signature: wrong number of bytes, expected 65") 156 | } 157 | } 158 | 159 | impl Error for ParseSignatureError {} 160 | 161 | #[cfg(test)] 162 | mod tests { 163 | use super::*; 164 | use hex_literal::hex; 165 | 166 | #[test] 167 | fn recovery_signature() { 168 | let message = "Some data"; 169 | let v = 0x1cu8; 170 | let r = hex!("b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd").into(); 171 | let s = hex!("6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a029").into(); 172 | 173 | let signed = SignedData { 174 | message: message.as_bytes().to_owned(), 175 | message_hash: hex!("1da44b586eb0729ff70a73c326926f6ed5a25f5b056e7f47fbc6e58d86871655").into(), 176 | v, 177 | r, 178 | s, 179 | signature: hex!("b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c").into(), 180 | }; 181 | let expected_signature = ( 182 | hex!("b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a029").into(), 1 183 | ); 184 | let (sig, id) = Recovery::from(&signed).as_signature().unwrap(); 185 | assert_eq!((sig.to_vec(), id), expected_signature); 186 | let (sig, id) = Recovery::new(message, v as _, r, s).as_signature().unwrap(); 187 | assert_eq!((sig.to_vec(), id), expected_signature); 188 | let (sig, id) = Recovery::from_raw_signature(message, &signed.signature.0) 189 | .unwrap() 190 | .as_signature() 191 | .unwrap(); 192 | assert_eq!((sig.to_vec(), id), expected_signature); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # This template contains all of the possible sections and their default values 2 | 3 | # Note that all fields that take a lint level have these possible values: 4 | # * deny - An error will be produced and the check will fail 5 | # * warn - A warning will be produced, but the check will not fail 6 | # * allow - No warning or error will be produced, though in some cases a note will be 7 | 8 | # If 1 or more target triples (and optionally, target_features) are specified, only 9 | # the specified targets will be checked when running `cargo deny check`. This means, 10 | # if a particular package is only ever used as a target specific dependency, such 11 | # as, for example, the `nix` crate only being used via the `target_family = "unix"` 12 | # configuration, that only having windows targets in this list would mean the nix 13 | # crate, as well as any of its exclusive dependencies not shared by any other 14 | # crates, would be ignored, as the target list here is effectively saying which 15 | # targets you are building for. 16 | targets = [ 17 | # The triple can be any string, but only the target triples built in to 18 | # rustc (as of 1.40) can be checked against actual config expressions 19 | #{ triple = "x86_64-unknown-linux-musl" }, 20 | # You can also specify which target_features you promise are enabled for a particular 21 | # target. target_features are currently not validated against the actual valid 22 | # features supported by the target architecture. 23 | #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, 24 | ] 25 | 26 | # This section is considered when running `cargo deny check advisories` 27 | # More documentation for the advisories section can be found here: 28 | # https://github.com/EmbarkStudios/cargo-deny#the-advisories-section 29 | [advisories] 30 | # The path where the advisory database is cloned/fetched into 31 | db-path = "~/.cargo/advisory-db" 32 | # The url of the advisory database to use 33 | db-url = "https://github.com/rustsec/advisory-db" 34 | # The lint level for security vulnerabilities 35 | vulnerability = "warn" 36 | # The lint level for unmaintained crates 37 | unmaintained = "warn" 38 | # The lint level for crates with security notices. Note that as of 39 | # 2019-12-17 there are no security notice advisories in https://github.com/rustsec/advisory-db 40 | notice = "warn" 41 | # A list of advisory IDs to ignore. Note that ignored advisories will still output 42 | # a note when they are encountered. 43 | ignore = [] 44 | # Threshold for security vulnerabilities, any vulnerability with a CVSS score 45 | # lower than the range specified will be ignored. Note that ignored advisories 46 | # will still output a note when they are encountered. 47 | # * None - CVSS Score 0.0 48 | # * Low - CVSS Score 0.1 - 3.9 49 | # * Medium - CVSS Score 4.0 - 6.9 50 | # * High - CVSS Score 7.0 - 8.9 51 | # * Critical - CVSS Score 9.0 - 10.0 52 | #severity-threshold = 53 | 54 | # This section is considered when running `cargo deny check licenses` 55 | # More documentation for the licenses section can be found here: 56 | # https://github.com/EmbarkStudios/cargo-deny#the-licenses-section 57 | [licenses] 58 | # The lint level for crates which do not have a detectable license 59 | unlicensed = "warn" 60 | # List of explictly allowed licenses 61 | # See https://spdx.org/licenses/ for list of possible licenses 62 | # [possible values: any SPDX 3.7 short identifier (+ optional exception)]. 63 | allow = [] 64 | # List of explictly disallowed licenses 65 | # See https://spdx.org/licenses/ for list of possible licenses 66 | # [possible values: any SPDX 3.7 short identifier (+ optional exception)]. 67 | deny = ["GPL-3.0"] 68 | # The lint level for licenses considered copyleft 69 | copyleft = "allow" 70 | # Blanket approval or denial for OSI-approved or FSF Free/Libre licenses 71 | # * both - The license will only be approved if it is both OSI-approved *AND* FSF/Free 72 | # * either - The license will be approved if it is either OSI-approved *OR* FSF/Free 73 | # * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF/Free 74 | # * fsf-only - The license will be approved if is FSF/Free *AND NOT* OSI-approved 75 | # * neither - The license will be denied if is FSF/Free *OR* OSI-approved 76 | allow-osi-fsf-free = "either" 77 | # The confidence threshold for detecting a license from license text. 78 | # The higher the value, the more closely the license text must be to the 79 | # canonical license text of a valid SPDX license file. 80 | # [possible values: any between 0.0 and 1.0]. 81 | confidence-threshold = 0.8 82 | 83 | # This section is considered when running `cargo deny check bans`. 84 | # More documentation about the 'bans' section can be found here: 85 | # https://github.com/EmbarkStudios/cargo-deny#crate-bans-cargo-deny-check-ban 86 | [bans] 87 | # Lint level for when multiple versions of the same crate are detected 88 | multiple-versions = "warn" 89 | # The graph highlighting used when creating dotgraphs for crates 90 | # with multiple versions 91 | # * lowest-version - The path to the lowest versioned duplicate is highlighted 92 | # * simplest-path - The path to the version with the fewest edges is highlighted 93 | # * all - Both lowest-version and simplest-path are used 94 | highlight = "all" 95 | # List of crates that are allowed. Use with care! 96 | allow = [ 97 | #{ name = "ansi_term", version = "=0.11.0" }, 98 | ] 99 | # List of crates to deny 100 | deny = [ 101 | # Each entry the name of a crate and a version range. If version is 102 | # not specified, all versions will be matched. 103 | #{ name = "ansi_term", version = "=0.11.0" }, 104 | ] 105 | # Certain crates/versions that will be skipped when doing duplicate detection. 106 | skip = [ 107 | #{ name = "ansi_term", version = "=0.11.0" }, 108 | ] 109 | # Similarly to `skip` allows you to skip certain crates during duplicate detection, 110 | # unlike skip, it also includes the entire tree of transitive dependencies starting at 111 | # the specified crate, up to a certain depth, which is by default infinite 112 | skip-tree = [ 113 | #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, 114 | ] 115 | 116 | 117 | # This section is considered when running `cargo deny check sources`. 118 | # More documentation about the 'sources' section can be found here: 119 | # https://github.com/EmbarkStudios/cargo-deny#crate-sources-cargo-deny-check-sources 120 | [sources] 121 | # Lint level for what to happen when a crate from a crate registry that is not in the allow list is encountered 122 | unknown-registry = "warn" 123 | # Lint level for what to happen when a crate from a git repository that is not in the allow list is encountered 124 | unknown-git = "warn" 125 | # List of URLs for allowed crate registries, by default https://github.com/rust-lang/crates.io-index is included 126 | #allow-registry = [] 127 | # List of URLs for allowed Git repositories 128 | allow-git = [] 129 | -------------------------------------------------------------------------------- /src/types/sync_state.rs: -------------------------------------------------------------------------------- 1 | use crate::types::U256; 2 | use serde::{ 3 | de::{Deserializer, Error}, 4 | ser::Serializer, 5 | Deserialize, Serialize, 6 | }; 7 | 8 | /// Information about current blockchain syncing operations. 9 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 10 | #[serde(rename_all = "camelCase")] 11 | pub struct SyncInfo { 12 | /// The block at which import began. 13 | pub starting_block: U256, 14 | 15 | /// The highest currently synced block. 16 | pub current_block: U256, 17 | 18 | /// The estimated highest block. 19 | pub highest_block: U256, 20 | } 21 | 22 | /// The current state of blockchain syncing operations. 23 | #[derive(Debug, Clone, PartialEq)] 24 | pub enum SyncState { 25 | /// Blockchain is syncing. 26 | Syncing(SyncInfo), 27 | 28 | /// Blockchain is not syncing. 29 | NotSyncing, 30 | } 31 | 32 | // Sync info from subscription has a different key format 33 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 34 | #[serde(rename_all = "PascalCase")] 35 | struct SubscriptionSyncInfo { 36 | /// The block at which import began. 37 | pub starting_block: U256, 38 | 39 | /// The highest currently synced block. 40 | pub current_block: U256, 41 | 42 | /// The estimated highest block. 43 | pub highest_block: U256, 44 | } 45 | 46 | impl From for SyncInfo { 47 | fn from(s: SubscriptionSyncInfo) -> Self { 48 | Self { 49 | starting_block: s.starting_block, 50 | current_block: s.current_block, 51 | highest_block: s.highest_block, 52 | } 53 | } 54 | } 55 | 56 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 57 | struct SubscriptionSyncState { 58 | pub syncing: bool, 59 | pub status: Option, 60 | } 61 | 62 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 63 | #[serde(untagged)] 64 | enum SyncStateVariants { 65 | Rpc(SyncInfo), 66 | Subscription(SubscriptionSyncState), 67 | Boolean(bool), 68 | } 69 | 70 | // The `eth_syncing` method returns either `false` or an instance of the sync info object. 71 | // This doesn't play particularly well with the features exposed by `serde_derive`, 72 | // so we use the custom impls below to ensure proper behavior. 73 | impl<'de> Deserialize<'de> for SyncState { 74 | fn deserialize(deserializer: D) -> Result 75 | where 76 | D: Deserializer<'de>, 77 | { 78 | let v: SyncStateVariants = Deserialize::deserialize(deserializer)?; 79 | match v { 80 | SyncStateVariants::Rpc(info) => Ok(SyncState::Syncing(info)), 81 | SyncStateVariants::Subscription(state) => match state.status { 82 | None if !state.syncing => Ok(SyncState::NotSyncing), 83 | Some(ref info) if state.syncing => Ok(SyncState::Syncing(info.clone().into())), 84 | _ => Err(D::Error::custom( 85 | "expected object or `syncing = false`, got `syncing = true`", 86 | )), 87 | }, 88 | SyncStateVariants::Boolean(boolean) => { 89 | if !boolean { 90 | Ok(SyncState::NotSyncing) 91 | } else { 92 | Err(D::Error::custom("expected object or `false`, got `true`")) 93 | } 94 | } 95 | } 96 | } 97 | } 98 | 99 | impl Serialize for SyncState { 100 | fn serialize(&self, serializer: S) -> Result 101 | where 102 | S: Serializer, 103 | { 104 | match *self { 105 | SyncState::Syncing(ref info) => info.serialize(serializer), 106 | SyncState::NotSyncing => false.serialize(serializer), 107 | } 108 | } 109 | } 110 | 111 | #[cfg(test)] 112 | mod tests { 113 | use super::{SyncInfo, SyncState}; 114 | 115 | #[test] 116 | fn should_deserialize_rpc_sync_info() { 117 | let sync_state = r#"{ 118 | "currentBlock": "0x42", 119 | "highestBlock": "0x9001", 120 | "knownStates": "0x1337", 121 | "pulledStates": "0x13", 122 | "startingBlock": "0x0" 123 | }"#; 124 | 125 | let value: SyncState = serde_json::from_str(sync_state).unwrap(); 126 | 127 | assert_eq!( 128 | value, 129 | SyncState::Syncing(SyncInfo { 130 | starting_block: 0x0.into(), 131 | current_block: 0x42.into(), 132 | highest_block: 0x9001.into() 133 | }) 134 | ); 135 | } 136 | 137 | #[test] 138 | fn should_deserialize_subscription_sync_info() { 139 | let sync_state = r#"{ 140 | "syncing": true, 141 | "status": { 142 | "CurrentBlock": "0x42", 143 | "HighestBlock": "0x9001", 144 | "KnownStates": "0x1337", 145 | "PulledStates": "0x13", 146 | "StartingBlock": "0x0" 147 | } 148 | }"#; 149 | 150 | let value: SyncState = serde_json::from_str(sync_state).unwrap(); 151 | 152 | assert_eq!( 153 | value, 154 | SyncState::Syncing(SyncInfo { 155 | starting_block: 0x0.into(), 156 | current_block: 0x42.into(), 157 | highest_block: 0x9001.into() 158 | }) 159 | ); 160 | } 161 | 162 | #[test] 163 | fn should_deserialize_boolean_not_syncing() { 164 | let sync_state = r#"false"#; 165 | let value: SyncState = serde_json::from_str(sync_state).unwrap(); 166 | 167 | assert_eq!(value, SyncState::NotSyncing); 168 | } 169 | 170 | #[test] 171 | fn should_deserialize_subscription_not_syncing() { 172 | let sync_state = r#"{ 173 | "syncing": false 174 | }"#; 175 | 176 | let value: SyncState = serde_json::from_str(sync_state).unwrap(); 177 | 178 | assert_eq!(value, SyncState::NotSyncing); 179 | } 180 | 181 | #[test] 182 | fn should_not_deserialize_invalid_boolean_syncing() { 183 | let sync_state = r#"true"#; 184 | let res: Result = serde_json::from_str(sync_state); 185 | assert!(res.is_err()); 186 | } 187 | 188 | #[test] 189 | fn should_not_deserialize_invalid_subscription_syncing() { 190 | let sync_state = r#"{ 191 | "syncing": true 192 | }"#; 193 | 194 | let res: Result = serde_json::from_str(sync_state); 195 | assert!(res.is_err()); 196 | } 197 | 198 | #[test] 199 | fn should_not_deserialize_invalid_subscription_not_syncing() { 200 | let sync_state = r#"{ 201 | "syncing": false, 202 | "status": { 203 | "CurrentBlock": "0x42", 204 | "HighestBlock": "0x9001", 205 | "KnownStates": "0x1337", 206 | "PulledStates": "0x13", 207 | "StartingBlock": "0x0" 208 | } 209 | }"#; 210 | 211 | let res: Result = serde_json::from_str(sync_state); 212 | assert!(res.is_err()); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Compilation and Testing Suite 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | tags: 9 | - v* 10 | paths-ignore: 11 | - 'README.md' 12 | jobs: 13 | 14 | check-test-build: 15 | name: Check, test and build 16 | runs-on: ubuntu-latest 17 | env: 18 | RUST_BACKTRACE: full 19 | steps: 20 | - name: Cancel Previous Runs 21 | uses: styfle/cancel-workflow-action@0.4.1 22 | with: 23 | access_token: ${{ github.token }} 24 | - name: Checkout sources & submodules 25 | uses: actions/checkout@master 26 | with: 27 | fetch-depth: 5 28 | submodules: recursive 29 | ## Check Stage 30 | - name: Checking rust-stable 31 | uses: actions-rs/cargo@master 32 | with: 33 | command: check 34 | toolchain: stable 35 | args: --all --verbose 36 | 37 | ## Test Stage 38 | - name: Testing rust-stable 39 | uses: actions-rs/cargo@master 40 | with: 41 | command: test 42 | toolchain: stable 43 | args: --all --verbose 44 | - name: Testing rust-stable with arbitrary_precision 45 | uses: actions-rs/cargo@master 46 | with: 47 | command: test 48 | toolchain: stable 49 | args: --all --verbose --features arbitrary_precision 50 | 51 | ## Build Stage 52 | - name: Building rust-stable 53 | uses: actions-rs/cargo@master 54 | if: github.ref == 'refs/heads/master' 55 | with: 56 | command: build 57 | toolchain: stable 58 | args: --all --verbose 59 | 60 | check-wasm: 61 | name: Check WASM 62 | runs-on: ubuntu-latest 63 | env: 64 | RUST_BACKTRACE: full 65 | steps: 66 | - name: Cancel Previous Runs 67 | uses: styfle/cancel-workflow-action@0.4.1 68 | with: 69 | access_token: ${{ github.token }} 70 | - name: Checkout sources & submodules 71 | uses: actions/checkout@master 72 | with: 73 | fetch-depth: 5 74 | submodules: recursive 75 | - name: Add WASM Utilities 76 | run: rustup target add wasm32-unknown-unknown --toolchain stable && cargo install wasm-bindgen-cli 77 | ## Check Stage 78 | - name: Checking wasm32 79 | uses: actions-rs/cargo@master 80 | with: 81 | command: check 82 | toolchain: stable 83 | args: --target wasm32-unknown-unknown --no-default-features --features eip-1193 84 | - name: Checking wasm32 with http 85 | uses: actions-rs/cargo@master 86 | with: 87 | command: check 88 | toolchain: stable 89 | args: --target wasm32-unknown-unknown --no-default-features --features http,wasm 90 | - name: Testing wasm32 91 | uses: actions-rs/cargo@master 92 | with: 93 | command: test 94 | toolchain: stable 95 | args: --target wasm32-unknown-unknown --no-default-features --features eip-1193 --tests 96 | 97 | check-transports: 98 | name: Check Transports 99 | runs-on: ubuntu-latest 100 | env: 101 | RUST_BACKTRACE: full 102 | steps: 103 | - name: Cancel Previous Runs 104 | uses: styfle/cancel-workflow-action@0.4.1 105 | with: 106 | access_token: ${{ github.token }} 107 | - name: Checkout sources & submodules 108 | uses: actions/checkout@master 109 | with: 110 | fetch-depth: 5 111 | submodules: recursive 112 | ## Check Stage 113 | - name: Checking without transports 114 | uses: actions-rs/cargo@master 115 | with: 116 | command: check 117 | toolchain: stable 118 | args: --no-default-features 119 | - name: Checking http 120 | uses: actions-rs/cargo@master 121 | with: 122 | command: check 123 | toolchain: stable 124 | args: --no-default-features --features http 125 | - name: Checking http-tls 126 | uses: actions-rs/cargo@master 127 | with: 128 | command: check 129 | toolchain: stable 130 | args: --no-default-features --features http-tls 131 | - name: Checking http-native-tls 132 | uses: actions-rs/cargo@master 133 | with: 134 | command: check 135 | toolchain: stable 136 | args: --no-default-features --features http-native-tls 137 | - name: Checking http-rustls-tls 138 | uses: actions-rs/cargo@master 139 | with: 140 | command: check 141 | toolchain: stable 142 | args: --no-default-features --features http-rustls-tls 143 | - name: Checking ws-tokio 144 | uses: actions-rs/cargo@master 145 | with: 146 | command: check 147 | toolchain: stable 148 | args: --no-default-features --features ws-tokio 149 | - name: Checking ws-tls-tokio 150 | uses: actions-rs/cargo@master 151 | with: 152 | command: check 153 | toolchain: stable 154 | args: --no-default-features --features ws-tls-tokio 155 | - name: Checking ws-async-std 156 | uses: actions-rs/cargo@master 157 | with: 158 | command: check 159 | toolchain: stable 160 | args: --no-default-features --features ws-async-std 161 | - name: Checking ws-tls-async-std 162 | uses: actions-rs/cargo@master 163 | with: 164 | command: check 165 | toolchain: stable 166 | args: --no-default-features --features ws-tls-async-std 167 | - name: Checking ipc-tokio 168 | uses: actions-rs/cargo@master 169 | with: 170 | command: check 171 | toolchain: stable 172 | args: --no-default-features --features ipc-tokio 173 | -------------------------------------------------------------------------------- /src/transports/ic_http.rs: -------------------------------------------------------------------------------- 1 | //! IC HTTP Transport 2 | 3 | use crate::{ 4 | error::{Error, Result, TransportError}, 5 | helpers, BatchTransport, RequestId, Transport, 6 | }; 7 | #[cfg(not(feature = "wasm"))] 8 | use futures::future::BoxFuture; 9 | #[cfg(feature = "wasm")] 10 | use futures::future::LocalBoxFuture as BoxFuture; 11 | use jsonrpc_core::types::{Call, Output, Request, Value}; 12 | use crate::transports::ICHttpClient; 13 | use serde::de::DeserializeOwned; 14 | use std::{ 15 | collections::HashMap, 16 | sync::{ 17 | atomic::{AtomicUsize, Ordering}, 18 | Arc, 19 | }, 20 | }; 21 | 22 | /// HTTP Transport 23 | #[derive(Clone, Debug)] 24 | pub struct ICHttp { 25 | client: ICHttpClient, 26 | inner: Arc, 27 | } 28 | 29 | #[derive(Debug)] 30 | struct Inner { 31 | url: String, 32 | id: AtomicUsize, 33 | } 34 | 35 | impl ICHttp { 36 | /// Create new HTTP transport connecting to given URL, cycles: cycles amount to perform http call 37 | /// 38 | /// Note that the http [Client] automatically enables some features like setting the basic auth 39 | /// header or enabling a proxy from the environment. You can customize it with 40 | /// [Http::with_client]. 41 | pub fn new(url: &str, max_resp: Option) -> Result { 42 | Ok( 43 | Self { 44 | client: ICHttpClient::new(max_resp), 45 | inner: Arc::new(Inner { 46 | url: url.to_string(), 47 | id: AtomicUsize::new(0), 48 | }), 49 | } 50 | ) 51 | } 52 | 53 | fn next_id(&self) -> RequestId { 54 | self.inner.id.fetch_add(1, Ordering::AcqRel) 55 | } 56 | 57 | fn new_request(&self) -> (ICHttpClient, String) { 58 | (self.client.clone(), self.inner.url.clone()) 59 | } 60 | } 61 | 62 | // Id is only used for logging. 63 | async fn execute_rpc(client: &ICHttpClient, url: String, request: &Request, id: RequestId) -> Result { 64 | let response = client 65 | .post(url, request, None, None) 66 | .await 67 | .map_err(|err| Error::Transport(TransportError::Message(err)))?; 68 | helpers::arbitrary_precision_deserialize_workaround(&response).map_err(|err| { 69 | Error::Transport(TransportError::Message(format!( 70 | "failed to deserialize response: {}: {}", 71 | err, 72 | String::from_utf8_lossy(&response) 73 | ))) 74 | }) 75 | } 76 | 77 | type RpcResult = Result; 78 | 79 | impl Transport for ICHttp { 80 | type Out = BoxFuture<'static, Result>; 81 | 82 | fn prepare(&self, method: &str, params: Vec) -> (RequestId, Call) { 83 | let id = self.next_id(); 84 | let request = helpers::build_request(id, method, params); 85 | (id, request) 86 | } 87 | 88 | fn send(&self, id: RequestId, call: Call) -> Self::Out { 89 | let (client, url) = self.new_request(); 90 | Box::pin(async move { 91 | let output: Output = execute_rpc(&client, url, &Request::Single(call), id).await?; 92 | helpers::to_result_from_output(output) 93 | }) 94 | } 95 | 96 | fn set_max_response_bytes(&mut self, v: u64) { 97 | self.client.set_max_response_bytes(v); 98 | } 99 | } 100 | 101 | impl BatchTransport for ICHttp { 102 | type Batch = BoxFuture<'static, Result>>; 103 | 104 | fn send_batch(&self, requests: T) -> Self::Batch 105 | where 106 | T: IntoIterator, 107 | { 108 | // Batch calls don't need an id but it helps associate the response log with the request log. 109 | let id = self.next_id(); 110 | let (client, url) = self.new_request(); 111 | let (ids, calls): (Vec<_>, Vec<_>) = requests.into_iter().unzip(); 112 | Box::pin(async move { 113 | let outputs: Vec = execute_rpc(&client, url, &Request::Batch(calls), id).await?; 114 | handle_batch_response(&ids, outputs) 115 | }) 116 | } 117 | } 118 | 119 | // According to the jsonrpc specification batch responses can be returned in any order so we need to 120 | // restore the intended order. 121 | fn handle_batch_response(ids: &[RequestId], outputs: Vec) -> Result> { 122 | if ids.len() != outputs.len() { 123 | return Err(Error::InvalidResponse("unexpected number of responses".to_string())); 124 | } 125 | let mut outputs = outputs 126 | .into_iter() 127 | .map(|output| Ok((id_of_output(&output)?, helpers::to_result_from_output(output)))) 128 | .collect::>>()?; 129 | ids.iter() 130 | .map(|id| { 131 | outputs 132 | .remove(id) 133 | .ok_or_else(|| Error::InvalidResponse(format!("batch response is missing id {}", id))) 134 | }) 135 | .collect() 136 | } 137 | 138 | fn id_of_output(output: &Output) -> Result { 139 | let id = match output { 140 | Output::Success(success) => &success.id, 141 | Output::Failure(failure) => &failure.id, 142 | }; 143 | match id { 144 | jsonrpc_core::Id::Num(num) => Ok(*num as RequestId), 145 | _ => Err(Error::InvalidResponse("response id is not u64".to_string())), 146 | } 147 | } 148 | 149 | #[cfg(test)] 150 | mod tests { 151 | use super::*; 152 | 153 | async fn server(req: hyper::Request) -> hyper::Result> { 154 | use hyper::body::HttpBody; 155 | 156 | let expected = r#"{"jsonrpc":"2.0","method":"eth_getAccounts","params":[],"id":0}"#; 157 | let response = r#"{"jsonrpc":"2.0","id":0,"result":"x"}"#; 158 | 159 | assert_eq!(req.method(), &hyper::Method::POST); 160 | assert_eq!(req.uri().path(), "/"); 161 | let mut content: Vec = vec![]; 162 | let mut body = req.into_body(); 163 | while let Some(Ok(chunk)) = body.data().await { 164 | content.extend(&*chunk); 165 | } 166 | assert_eq!(std::str::from_utf8(&*content), Ok(expected)); 167 | 168 | Ok(hyper::Response::new(response.into())) 169 | } 170 | 171 | #[tokio::test] 172 | async fn should_make_a_request() { 173 | use hyper::service::{make_service_fn, service_fn}; 174 | 175 | // given 176 | let addr = "127.0.0.1:3001"; 177 | // start server 178 | let service = make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(server)) }); 179 | let server = hyper::Server::bind(&addr.parse().unwrap()).serve(service); 180 | tokio::spawn(async move { 181 | println!("Listening on http://{}", addr); 182 | server.await.unwrap(); 183 | }); 184 | 185 | // when 186 | let client = Http::new(&format!("http://{}", addr)).unwrap(); 187 | println!("Sending request"); 188 | let response = client.execute("eth_getAccounts", vec![]).await; 189 | println!("Got response"); 190 | 191 | // then 192 | assert_eq!(response, Ok(Value::String("x".into()))); 193 | } 194 | 195 | #[test] 196 | fn handles_batch_response_being_in_different_order_than_input() { 197 | let ids = vec![0, 1, 2]; 198 | // This order is different from the ids. 199 | let outputs = [1u64, 0, 2] 200 | .iter() 201 | .map(|&id| { 202 | Output::Success(jsonrpc_core::Success { 203 | jsonrpc: None, 204 | result: id.into(), 205 | id: jsonrpc_core::Id::Num(id), 206 | }) 207 | }) 208 | .collect(); 209 | let results = handle_batch_response(&ids, outputs) 210 | .unwrap() 211 | .into_iter() 212 | .map(|result| result.unwrap().as_u64().unwrap() as usize) 213 | .collect::>(); 214 | // The order of the ids should have been restored. 215 | assert_eq!(ids, results); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/contract/ens/registry.rs: -------------------------------------------------------------------------------- 1 | //! ENS Registry contract interface. 2 | 3 | use crate::{ 4 | api::Eth, 5 | contract::{Contract, Options}, 6 | signing::NameHash, 7 | types::{Address, TransactionId}, 8 | Transport, 9 | }; 10 | 11 | type ContractError = crate::contract::Error; 12 | 13 | const ENS_REGISTRY_ADDRESS: &str = "00000000000C2E074eC69A0dFb2997BA6C7d2e1e"; 14 | 15 | /// The ENS registry is the core contract that lies at the heart of ENS resolution. 16 | /// 17 | /// All ENS lookups start by querying the registry. 18 | /// The registry maintains a list of domains, recording the owner, resolver, and TTL for each, and allows the owner of a domain to make changes to that data. 19 | /// 20 | /// The ENS registry is specified in [EIP 137](https://eips.ethereum.org/EIPS/eip-137). 21 | /// 22 | /// [Source](https://github.com/ensdomains/ens/blob/master/contracts/ENS.sol) 23 | #[derive(Debug, Clone)] 24 | pub struct Registry { 25 | contract: Contract, 26 | } 27 | 28 | impl Registry { 29 | /// Creates new instance of [`Registry`]. 30 | pub fn new(eth: Eth) -> Self { 31 | let address = ENS_REGISTRY_ADDRESS.parse().expect("Parsing Address"); 32 | 33 | // See https://github.com/ensdomains/ens-contracts for up to date contracts. 34 | let json = include_bytes!("ENSRegistry.json"); 35 | 36 | let contract = Contract::from_json(eth, address, json).expect("Contract Creation"); 37 | 38 | Self { contract } 39 | } 40 | } 41 | 42 | impl Registry { 43 | /// Returns the owner of the name specified by node. 44 | /// 45 | /// [Specification](https://docs.ens.domains/contract-api-reference/ens#get-owner) 46 | pub async fn owner(&self, node: NameHash) -> Result { 47 | let options = Options::default(); 48 | 49 | self.contract.query("owner", node, None, options, None).await 50 | } 51 | 52 | /// Returns the address of the resolver responsible for the name specified by node. 53 | /// 54 | /// [Specification](https://docs.ens.domains/contract-api-reference/ens#get-resolver) 55 | pub async fn resolver(&self, node: NameHash) -> Result { 56 | let options = Options::default(); 57 | 58 | self.contract.query("resolver", node, None, options, None).await 59 | } 60 | 61 | /// Returns the caching time-to-live of the name specified by node. 62 | /// 63 | /// [Specification](https://docs.ens.domains/contract-api-reference/ens#get-ttl) 64 | pub async fn ttl(&self, node: NameHash) -> Result { 65 | let options = Options::default(); 66 | 67 | self.contract.query("ttl", node, None, options, None).await 68 | } 69 | 70 | /// Reassigns ownership of the name identified by node to owner. 71 | /// 72 | /// [Specification](https://docs.ens.domains/contract-api-reference/ens#set-owner) 73 | pub async fn set_owner( 74 | &self, 75 | from: Address, 76 | node: NameHash, 77 | owner: Address, 78 | ) -> Result { 79 | let options = Options::default(); 80 | 81 | let id = self.contract.call("setOwner", (node, owner), from, options).await?; 82 | 83 | Ok(TransactionId::Hash(id)) 84 | } 85 | 86 | /// Updates the resolver associated with the name identified by node to resolver. 87 | /// 88 | /// [Specification](https://docs.ens.domains/contract-api-reference/ens#set-resolver) 89 | pub async fn set_resolver( 90 | &self, 91 | from: Address, 92 | node: NameHash, 93 | resolver: Address, 94 | ) -> Result { 95 | let options = Options::default(); 96 | 97 | let id = self 98 | .contract 99 | .call("setResolver", (node, resolver), from, options) 100 | .await?; 101 | 102 | Ok(TransactionId::Hash(id)) 103 | } 104 | 105 | /// Updates the caching time-to-live of the name identified by node. 106 | /// 107 | /// [Specification](https://docs.ens.domains/contract-api-reference/ens#set-ttl) 108 | pub async fn set_ttl(&self, from: Address, node: NameHash, ttl: u64) -> Result { 109 | let options = Options::default(); 110 | 111 | let id = self.contract.call("setTTL", (node, ttl), from, options).await?; 112 | 113 | Ok(TransactionId::Hash(id)) 114 | } 115 | 116 | /// Creates a new subdomain of node, assigning ownership of it to the specified owner. 117 | /// 118 | /// [Specification](https://docs.ens.domains/contract-api-reference/ens#set-subdomain-owner) 119 | pub async fn set_subnode_owner( 120 | &self, 121 | from: Address, 122 | node: NameHash, 123 | label: [u8; 32], 124 | owner: Address, 125 | ) -> Result { 126 | let options = Options::default(); 127 | 128 | let id = self 129 | .contract 130 | .call("setSubnodeOwner", (node, label, owner), from, options) 131 | .await?; 132 | 133 | Ok(TransactionId::Hash(id)) 134 | } 135 | 136 | /// Sets the owner, resolver, and TTL for an ENS record in a single operation. 137 | /// 138 | /// [Specification](https://docs.ens.domains/contract-api-reference/ens#set-record) 139 | pub async fn set_record( 140 | &self, 141 | from: Address, 142 | node: NameHash, 143 | owner: Address, 144 | resolver: Address, 145 | ttl: u64, 146 | ) -> Result { 147 | let options = Options::default(); 148 | 149 | let id = self 150 | .contract 151 | .call("setRecord", (node, owner, resolver, ttl), from, options) 152 | .await?; 153 | 154 | Ok(TransactionId::Hash(id)) 155 | } 156 | 157 | /// Sets the owner, resolver and TTL for a subdomain, creating it if necessary. 158 | /// 159 | /// [Specification](https://docs.ens.domains/contract-api-reference/ens#set-subdomain-record) 160 | pub async fn set_subnode_record( 161 | &self, 162 | from: Address, 163 | node: NameHash, 164 | label: [u8; 32], 165 | owner: Address, 166 | resolver: Address, 167 | ttl: u64, 168 | ) -> Result { 169 | let options = Options::default(); 170 | 171 | let id = self 172 | .contract 173 | .call("setSubnodeRecord", (node, label, owner, resolver, ttl), from, options) 174 | .await?; 175 | 176 | Ok(TransactionId::Hash(id)) 177 | } 178 | 179 | /// Sets or clears an approval. 180 | /// 181 | /// [Specification](https://docs.ens.domains/contract-api-reference/ens#set-approval) 182 | pub async fn set_approval_for_all( 183 | &self, 184 | from: Address, 185 | operator: Address, 186 | approved: bool, 187 | ) -> Result { 188 | let options = Options::default(); 189 | 190 | let id = self 191 | .contract 192 | .call("setApprovalForAll", (operator, approved), from, options) 193 | .await?; 194 | 195 | Ok(TransactionId::Hash(id)) 196 | } 197 | 198 | /// Returns true if operator is approved to make ENS registry operations on behalf of owner. 199 | /// 200 | /// [Specification](https://docs.ens.domains/contract-api-reference/ens#check-approval) 201 | pub async fn check_approval(&self, owner: Address, operator: Address) -> Result { 202 | let options = Options::default(); 203 | 204 | self.contract 205 | .query("isApprovedForAll", (owner, operator), None, options, None) 206 | .await 207 | } 208 | 209 | /// Returns true if node exists in this ENS registry. 210 | /// 211 | /// [Specification](https://docs.ens.domains/contract-api-reference/ens#check-record-existence) 212 | pub async fn check_record_existence(&self, node: NameHash) -> Result { 213 | let options = Options::default(); 214 | 215 | self.contract.query("recordExists", node, None, options, None).await 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /examples/endpoint.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, collections::{HashSet, HashMap}}; 2 | 3 | use candid::{CandidType, Deserialize, Principal, candid_method}; 4 | use ic_cdk::api::management_canister::http_request::{TransformArgs, HttpResponse, CanisterHttpRequestArgument, HttpHeader, HttpMethod, TransformContext, TransformFunc, http_request}; 5 | use ic_cdk_macros::*; 6 | use jsonrpc_core::Call; 7 | 8 | const MIN_CYCLES_REQUIRED: u128 = 10_000_000_000; // 10B cycles minimum for each call 9 | const SERVICE_FEE: u128 = 100_000_000; // 0.1B cycles for service fee 10 | 11 | #[derive(CandidType, Deserialize)] 12 | struct State { 13 | owner: Principal, 14 | controllers: HashSet, 15 | registered: HashMap, 16 | } 17 | 18 | #[derive(CandidType, Deserialize, Eq, PartialEq, Hash, Clone)] 19 | struct Registered { 20 | chain_id: u64, 21 | api_provider: String, 22 | } 23 | 24 | #[derive(CandidType, Deserialize, Clone)] 25 | enum RpcTarget { 26 | #[serde(rename = "registered")] 27 | Registered(Registered), 28 | #[serde(rename = "url_with_api_key")] 29 | UrlWithApiKey(String), 30 | } 31 | 32 | impl Default for State { 33 | fn default() -> Self { 34 | Self { 35 | owner: Principal::management_canister(), 36 | controllers: Default::default(), 37 | registered: Default::default(), 38 | } 39 | } 40 | } 41 | 42 | thread_local! { 43 | static STATE: RefCell = RefCell::new(State::default()); 44 | } 45 | 46 | #[init] 47 | #[candid_method(init)] 48 | fn init() { 49 | let owner = ic_cdk::caller(); 50 | STATE.with(|s| { 51 | let mut state = s.borrow_mut(); 52 | state.owner = owner; 53 | }); 54 | } 55 | 56 | #[query(name = "transform")] 57 | #[candid_method(query, rename = "transform")] 58 | fn transform(response: TransformArgs) -> HttpResponse { 59 | let res = response.response; 60 | // remove header 61 | HttpResponse { status: res.status, headers: Vec::default(), body: res.body } 62 | } 63 | 64 | // get state info 65 | #[query(name = "registrations")] 66 | #[candid_method(query, rename = "registrations")] 67 | fn registrations() -> Vec { 68 | STATE.with(|s| { 69 | let state = s.borrow(); 70 | state.registered.keys().cloned().collect::>() 71 | }) 72 | } 73 | 74 | // add controllers 75 | #[update(name = "add_controller", guard = "is_owner")] 76 | #[candid_method(update, rename = "add_controller")] 77 | async fn add_controller(controller: Principal) { 78 | STATE.with(|s| { 79 | let mut state = s.borrow_mut(); 80 | state.controllers.insert(controller); 81 | }); 82 | } 83 | 84 | // register api and key 85 | #[update(name = "register_api_key", guard = "is_authorized")] 86 | #[candid_method(update, rename = "register_api_key")] 87 | async fn register_api_key(chain_id: u64, api_provider: String, url_with_key: String) { 88 | STATE.with(|s| { 89 | let mut state = s.borrow_mut(); 90 | state.registered.insert(Registered{ 91 | chain_id, 92 | api_provider, 93 | }, url_with_key); 94 | }); 95 | } 96 | 97 | // json rpc call 98 | #[update(name = "json_rpc")] 99 | #[candid_method(update, rename = "json_rpc")] 100 | async fn json_rpc(payload: String, target: RpcTarget, max_response_bytes: Option) -> Result { 101 | let cycles_call = ic_cdk::api::call::msg_cycles_available128(); 102 | if cycles_call < MIN_CYCLES_REQUIRED { 103 | return Err(format!("requires at least 10B cycles, get {} cycles", cycles_call)); 104 | } 105 | let request_body: Call = serde_json::from_str(payload.as_ref()).map_err(|e| format!("Fail to decode json body: {:?}", e))?; 106 | let max_resp = max_response_bytes.unwrap_or(get_default_max_response_bytes_by_call(&request_body)); 107 | let cycles_estimated = calculate_required_cycles(payload.clone(), max_resp, target.clone()); 108 | if cycles_call < cycles_estimated { 109 | return Err(format!("requires {} cycles, get {} cycles", cycles_estimated, cycles_call)); 110 | } 111 | // charge cycles 112 | let cycles_charged = ic_cdk::api::call::msg_cycles_accept128(cycles_estimated); 113 | ic_cdk::println!("cycles charged: {}", cycles_charged); 114 | 115 | let url_with_key = match target { 116 | RpcTarget::Registered(registered) => { 117 | STATE.with(|s| { 118 | s.borrow().registered.get(®istered).cloned().unwrap_or_default() 119 | }) 120 | } 121 | RpcTarget::UrlWithApiKey(url_with_api_key) => { 122 | url_with_api_key 123 | } 124 | }; 125 | if url_with_key.is_empty() { 126 | return Err("url is empty".to_string()) 127 | }; 128 | 129 | let call_res = json_rpc_call(&request_body, url_with_key, max_resp).await; 130 | 131 | let res = call_res.map_err(|e| format!("{}", e))?; 132 | ic_cdk::println!("result: {}", res); 133 | 134 | Ok(format!("{}", res)) 135 | } 136 | 137 | /// Call json rpc directly 138 | pub async fn json_rpc_call(request_body: &Call, url: String, max_response_bytes: u64) -> Result { 139 | let request_headers = vec![ 140 | HttpHeader { 141 | name: "Content-Type".to_string(), 142 | value: "application/json".to_string(), 143 | }, 144 | ]; 145 | // call http 146 | let request = CanisterHttpRequestArgument { 147 | url: url, 148 | max_response_bytes: Some(max_response_bytes), 149 | method: HttpMethod::POST, 150 | headers: request_headers, 151 | body: Some(serde_json::to_vec(request_body).unwrap()), 152 | transform: Some(TransformContext { 153 | function: TransformFunc(candid::Func { 154 | principal: ic_cdk::api::id(), 155 | method: "transform".to_string(), 156 | }), 157 | context: vec![], 158 | }), 159 | }; 160 | 161 | match http_request(request).await { 162 | Ok((result, )) => { 163 | Ok(String::from_utf8_lossy(result.body.as_ref()).to_string()) 164 | } 165 | Err((r, m)) => { 166 | let message = format!("The http_request resulted into error. RejectionCode: {r:?}, Error: {m}"); 167 | ic_cdk::api::print(message.clone()); 168 | Err(message) 169 | } 170 | } 171 | } 172 | 173 | fn get_default_max_response_bytes_by_call(rpc_call: &Call) -> u64 { 174 | // TODO define the max response bytes by call method 175 | return 500_000; 176 | } 177 | 178 | fn is_owner() -> Result<(), String> { 179 | STATE.with(|s| { 180 | let state = s.borrow(); 181 | if state.owner == ic_cdk::api::caller() { 182 | Ok(()) 183 | } else { 184 | Err("unauthorized".to_string()) 185 | } 186 | }) 187 | } 188 | 189 | fn is_authorized() -> Result<(), String> { 190 | STATE.with(|s| { 191 | let state = s.borrow(); 192 | let caller = ic_cdk::api::caller(); 193 | if state.owner == caller || state.controllers.contains(&caller){ 194 | Ok(()) 195 | } else { 196 | Err("unauthorized".to_string()) 197 | } 198 | }) 199 | } 200 | 201 | // calculate the estimated cycles required 202 | // refer to https://internetcomputer.org/docs/current/developer-docs/gas-cost 203 | fn calculate_required_cycles(payload: String, max_response_bytes: u64, target: RpcTarget) -> u128 { 204 | let arg_raw = candid::utils::encode_args((payload, max_response_bytes, target)).expect("Failed to encode arguments."); 205 | // 1.2M is ingress message received 206 | // 2K per byte received in an ingress message 207 | // 400M is HTTPS outcall request 208 | // assuming ingress message size is almost the same size of http request size, 100K cycles per byte 209 | 1_200_000u128 + 210 | 2_000u128 * arg_raw.len() as u128 + 211 | 400_000_000u128 + 212 | 100_000u128 * (arg_raw.len() as u128 + max_response_bytes as u128) + 213 | SERVICE_FEE 214 | } 215 | 216 | #[pre_upgrade] 217 | fn pre_upgrade() { 218 | let state = STATE.with(|s| { 219 | s.replace(State::default()) 220 | }); 221 | ic_cdk::storage::stable_save((state, )).expect("pre upgrade error"); 222 | } 223 | 224 | #[post_upgrade] 225 | fn post_upgrade() { 226 | let (state, ): (State, ) = ic_cdk::storage::stable_restore().expect("post upgrade error"); 227 | STATE.with(|s| { 228 | s.replace(state); 229 | }); 230 | } 231 | 232 | #[cfg(not(any(target_arch = "wasm32", test)))] 233 | fn main() { 234 | candid::export_service!(); 235 | std::print!("{}", __export_service()); 236 | } 237 | 238 | #[cfg(any(target_arch = "wasm32", test))] 239 | fn main() {} -------------------------------------------------------------------------------- /src/transports/http.rs: -------------------------------------------------------------------------------- 1 | //! HTTP Transport 2 | 3 | use crate::{ 4 | error::{Error, Result, TransportError}, 5 | helpers, BatchTransport, RequestId, Transport, 6 | }; 7 | #[cfg(not(feature = "wasm"))] 8 | use futures::future::BoxFuture; 9 | #[cfg(feature = "wasm")] 10 | use futures::future::LocalBoxFuture as BoxFuture; 11 | use jsonrpc_core::types::{Call, Output, Request, Value}; 12 | use reqwest::{Client, Url}; 13 | use serde::de::DeserializeOwned; 14 | use std::{ 15 | collections::HashMap, 16 | sync::{ 17 | atomic::{AtomicUsize, Ordering}, 18 | Arc, 19 | }, 20 | }; 21 | 22 | /// HTTP Transport 23 | #[derive(Clone, Debug)] 24 | pub struct Http { 25 | // Client is already an Arc so doesn't need to be part of inner. 26 | client: Client, 27 | inner: Arc, 28 | } 29 | 30 | #[derive(Debug)] 31 | struct Inner { 32 | url: Url, 33 | id: AtomicUsize, 34 | } 35 | 36 | impl Http { 37 | /// Create new HTTP transport connecting to given URL. 38 | /// 39 | /// Note that the http [Client] automatically enables some features like setting the basic auth 40 | /// header or enabling a proxy from the environment. You can customize it with 41 | /// [Http::with_client]. 42 | pub fn new(url: &str) -> Result { 43 | #[allow(unused_mut)] 44 | let mut builder = Client::builder(); 45 | #[cfg(not(feature = "wasm"))] 46 | { 47 | builder = builder.user_agent(headers::HeaderValue::from_static("web3.rs")); 48 | } 49 | let client = builder 50 | .build() 51 | .map_err(|err| Error::Transport(TransportError::Message(format!("failed to build client: {}", err))))?; 52 | Ok(Self::with_client(client, url.parse()?)) 53 | } 54 | 55 | /// Like `new` but with a user provided client instance. 56 | pub fn with_client(client: Client, url: Url) -> Self { 57 | Self { 58 | client, 59 | inner: Arc::new(Inner { 60 | url, 61 | id: AtomicUsize::new(0), 62 | }), 63 | } 64 | } 65 | 66 | fn next_id(&self) -> RequestId { 67 | self.inner.id.fetch_add(1, Ordering::AcqRel) 68 | } 69 | 70 | fn new_request(&self) -> (Client, Url) { 71 | (self.client.clone(), self.inner.url.clone()) 72 | } 73 | } 74 | 75 | // Id is only used for logging. 76 | async fn execute_rpc(client: &Client, url: Url, request: &Request, id: RequestId) -> Result { 77 | log::debug!("[id:{}] sending request: {:?}", id, serde_json::to_string(&request)?); 78 | let response = client 79 | .post(url) 80 | .json(request) 81 | .send() 82 | .await 83 | .map_err(|err| Error::Transport(TransportError::Message(format!("failed to send request: {}", err))))?; 84 | let status = response.status(); 85 | let response = response.bytes().await.map_err(|err| { 86 | Error::Transport(TransportError::Message(format!( 87 | "failed to read response bytes: {}", 88 | err 89 | ))) 90 | })?; 91 | log::debug!( 92 | "[id:{}] received response: {:?}", 93 | id, 94 | String::from_utf8_lossy(&response) 95 | ); 96 | if !status.is_success() { 97 | return Err(Error::Transport(TransportError::Code(status.as_u16()))); 98 | } 99 | helpers::arbitrary_precision_deserialize_workaround(&response).map_err(|err| { 100 | Error::Transport(TransportError::Message(format!( 101 | "failed to deserialize response: {}: {}", 102 | err, 103 | String::from_utf8_lossy(&response) 104 | ))) 105 | }) 106 | } 107 | 108 | type RpcResult = Result; 109 | 110 | impl Transport for Http { 111 | type Out = BoxFuture<'static, Result>; 112 | 113 | fn prepare(&self, method: &str, params: Vec) -> (RequestId, Call) { 114 | let id = self.next_id(); 115 | let request = helpers::build_request(id, method, params); 116 | (id, request) 117 | } 118 | 119 | fn send(&self, id: RequestId, call: Call) -> Self::Out { 120 | let (client, url) = self.new_request(); 121 | Box::pin(async move { 122 | let output: Output = execute_rpc(&client, url, &Request::Single(call), id).await?; 123 | helpers::to_result_from_output(output) 124 | }) 125 | } 126 | } 127 | 128 | impl BatchTransport for Http { 129 | type Batch = BoxFuture<'static, Result>>; 130 | 131 | fn send_batch(&self, requests: T) -> Self::Batch 132 | where 133 | T: IntoIterator, 134 | { 135 | // Batch calls don't need an id but it helps associate the response log with the request log. 136 | let id = self.next_id(); 137 | let (client, url) = self.new_request(); 138 | let (ids, calls): (Vec<_>, Vec<_>) = requests.into_iter().unzip(); 139 | Box::pin(async move { 140 | let outputs: Vec = execute_rpc(&client, url, &Request::Batch(calls), id).await?; 141 | handle_batch_response(&ids, outputs) 142 | }) 143 | } 144 | } 145 | 146 | // According to the jsonrpc specification batch responses can be returned in any order so we need to 147 | // restore the intended order. 148 | fn handle_batch_response(ids: &[RequestId], outputs: Vec) -> Result> { 149 | if ids.len() != outputs.len() { 150 | return Err(Error::InvalidResponse("unexpected number of responses".to_string())); 151 | } 152 | let mut outputs = outputs 153 | .into_iter() 154 | .map(|output| Ok((id_of_output(&output)?, helpers::to_result_from_output(output)))) 155 | .collect::>>()?; 156 | ids.iter() 157 | .map(|id| { 158 | outputs 159 | .remove(id) 160 | .ok_or_else(|| Error::InvalidResponse(format!("batch response is missing id {}", id))) 161 | }) 162 | .collect() 163 | } 164 | 165 | fn id_of_output(output: &Output) -> Result { 166 | let id = match output { 167 | Output::Success(success) => &success.id, 168 | Output::Failure(failure) => &failure.id, 169 | }; 170 | match id { 171 | jsonrpc_core::Id::Num(num) => Ok(*num as RequestId), 172 | _ => Err(Error::InvalidResponse("response id is not u64".to_string())), 173 | } 174 | } 175 | 176 | #[cfg(test)] 177 | mod tests { 178 | use super::*; 179 | 180 | async fn server(req: hyper::Request) -> hyper::Result> { 181 | use hyper::body::HttpBody; 182 | 183 | let expected = r#"{"jsonrpc":"2.0","method":"eth_getAccounts","params":[],"id":0}"#; 184 | let response = r#"{"jsonrpc":"2.0","id":0,"result":"x"}"#; 185 | 186 | assert_eq!(req.method(), &hyper::Method::POST); 187 | assert_eq!(req.uri().path(), "/"); 188 | let mut content: Vec = vec![]; 189 | let mut body = req.into_body(); 190 | while let Some(Ok(chunk)) = body.data().await { 191 | content.extend(&*chunk); 192 | } 193 | assert_eq!(std::str::from_utf8(&*content), Ok(expected)); 194 | 195 | Ok(hyper::Response::new(response.into())) 196 | } 197 | 198 | #[tokio::test] 199 | async fn should_make_a_request() { 200 | use hyper::service::{make_service_fn, service_fn}; 201 | 202 | // given 203 | let addr = "127.0.0.1:3001"; 204 | // start server 205 | let service = make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(server)) }); 206 | let server = hyper::Server::bind(&addr.parse().unwrap()).serve(service); 207 | tokio::spawn(async move { 208 | println!("Listening on http://{}", addr); 209 | server.await.unwrap(); 210 | }); 211 | 212 | // when 213 | let client = Http::new(&format!("http://{}", addr)).unwrap(); 214 | println!("Sending request"); 215 | let response = client.execute("eth_getAccounts", vec![]).await; 216 | println!("Got response"); 217 | 218 | // then 219 | assert_eq!(response, Ok(Value::String("x".into()))); 220 | } 221 | 222 | #[test] 223 | fn handles_batch_response_being_in_different_order_than_input() { 224 | let ids = vec![0, 1, 2]; 225 | // This order is different from the ids. 226 | let outputs = [1u64, 0, 2] 227 | .iter() 228 | .map(|&id| { 229 | Output::Success(jsonrpc_core::Success { 230 | jsonrpc: None, 231 | result: id.into(), 232 | id: jsonrpc_core::Id::Num(id), 233 | }) 234 | }) 235 | .collect(); 236 | let results = handle_batch_response(&ids, outputs) 237 | .unwrap() 238 | .into_iter() 239 | .map(|result| result.unwrap().as_u64().unwrap() as usize) 240 | .collect::>(); 241 | // The order of the ids should have been restored. 242 | assert_eq!(ids, results); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/api/personal.rs: -------------------------------------------------------------------------------- 1 | //! `Personal` namespace 2 | 3 | use crate::{ 4 | api::Namespace, 5 | helpers::{self, CallFuture}, 6 | types::{Address, Bytes, RawTransaction, TransactionRequest, H256, H520}, 7 | Transport, 8 | }; 9 | 10 | /// `Personal` namespace 11 | #[derive(Debug, Clone)] 12 | pub struct Personal { 13 | transport: T, 14 | } 15 | 16 | impl Namespace for Personal { 17 | fn new(transport: T) -> Self 18 | where 19 | Self: Sized, 20 | { 21 | Personal { transport } 22 | } 23 | 24 | fn transport(&self) -> &T { 25 | &self.transport 26 | } 27 | } 28 | 29 | impl Personal { 30 | /// Returns a list of available accounts. 31 | pub fn list_accounts(&self) -> CallFuture, T::Out> { 32 | CallFuture::new(self.transport.execute("personal_listAccounts", vec![])) 33 | } 34 | 35 | /// Creates a new account and protects it with given password. 36 | /// Returns the address of created account. 37 | pub fn new_account(&self, password: &str) -> CallFuture { 38 | let password = helpers::serialize(&password); 39 | CallFuture::new(self.transport.execute("personal_newAccount", vec![password])) 40 | } 41 | 42 | /// Unlocks the account with given password for some period of time (or single transaction). 43 | /// Returns `true` if the call was successful. 44 | pub fn unlock_account(&self, address: Address, password: &str, duration: Option) -> CallFuture { 45 | let address = helpers::serialize(&address); 46 | let password = helpers::serialize(&password); 47 | let duration = helpers::serialize(&duration); 48 | CallFuture::new( 49 | self.transport 50 | .execute("personal_unlockAccount", vec![address, password, duration]), 51 | ) 52 | } 53 | 54 | /// Sends a transaction from locked account. 55 | /// Returns transaction hash. 56 | pub fn send_transaction(&self, transaction: TransactionRequest, password: &str) -> CallFuture { 57 | let transaction = helpers::serialize(&transaction); 58 | let password = helpers::serialize(&password); 59 | CallFuture::new( 60 | self.transport 61 | .execute("personal_sendTransaction", vec![transaction, password]), 62 | ) 63 | } 64 | 65 | /// Signs an Ethereum specific message with `sign(keccak256("\x19Ethereum Signed Message: " + len(data) + data)))` 66 | /// 67 | /// The account does not need to be unlocked to make this call, and will not be left unlocked after. 68 | /// Returns encoded signature. 69 | pub fn sign(&self, data: Bytes, account: Address, password: &str) -> CallFuture { 70 | let data = helpers::serialize(&data); 71 | let address = helpers::serialize(&account); 72 | let password = helpers::serialize(&password); 73 | CallFuture::new(self.transport.execute("personal_sign", vec![data, address, password])) 74 | } 75 | 76 | /// Signs a transaction without dispatching it to the network. 77 | /// The account does not need to be unlocked to make this call, and will not be left unlocked after. 78 | /// Returns a signed transaction in raw bytes along with it's details. 79 | pub fn sign_transaction( 80 | &self, 81 | transaction: TransactionRequest, 82 | password: &str, 83 | ) -> CallFuture { 84 | let transaction = helpers::serialize(&transaction); 85 | let password = helpers::serialize(&password); 86 | CallFuture::new( 87 | self.transport 88 | .execute("personal_signTransaction", vec![transaction, password]), 89 | ) 90 | } 91 | 92 | /// Imports a raw key and protects it with the given password. 93 | /// Returns the address of created account. 94 | pub fn import_raw_key(&self, private_key: &[u8; 32], password: &str) -> CallFuture { 95 | let private_key = hex::encode(private_key); 96 | let private_key = helpers::serialize(&private_key); 97 | let password = helpers::serialize(&password); 98 | 99 | CallFuture::new( 100 | self.transport 101 | .execute("personal_importRawKey", vec![private_key, password]), 102 | ) 103 | } 104 | } 105 | 106 | #[cfg(test)] 107 | mod tests { 108 | use super::Personal; 109 | use crate::{ 110 | api::Namespace, 111 | rpc::Value, 112 | types::{Address, Bytes, RawTransaction, TransactionRequest, H160, H520}, 113 | }; 114 | use hex_literal::hex; 115 | 116 | const EXAMPLE_TX: &str = r#"{ 117 | "raw": "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675", 118 | "tx": { 119 | "hash": "0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b", 120 | "nonce": "0x0", 121 | "blockHash": "0xbeab0aa2411b7ab17f30a99d3cb9c6ef2fc5426d6ad6fd9e2a26a6aed1d1055b", 122 | "blockNumber": "0x15df", 123 | "transactionIndex": "0x1", 124 | "from": "0x407d73d8a49eeb85d32cf465507dd71d507100c1", 125 | "to": "0x853f43d8a49eeb85d32cf465507dd71d507100c1", 126 | "value": "0x7f110", 127 | "gas": "0x7f110", 128 | "gasPrice": "0x09184e72a000", 129 | "input": "0x603880600c6000396000f300603880600c6000396000f3603880600c6000396000f360" 130 | } 131 | }"#; 132 | 133 | rpc_test! ( 134 | Personal:list_accounts => "personal_listAccounts"; 135 | Value::Array(vec![Value::String("0x0000000000000000000000000000000000000123".into())]) => vec![Address::from_low_u64_be(0x123)] 136 | ); 137 | 138 | rpc_test! ( 139 | Personal:new_account, "hunter2" => "personal_newAccount", vec![r#""hunter2""#]; 140 | Value::String("0x0000000000000000000000000000000000000123".into()) => Address::from_low_u64_be(0x123) 141 | ); 142 | 143 | rpc_test! ( 144 | Personal:unlock_account, Address::from_low_u64_be(0x123), "hunter2", None 145 | => 146 | "personal_unlockAccount", vec![r#""0x0000000000000000000000000000000000000123""#, r#""hunter2""#, r#"null"#]; 147 | Value::Bool(true) => true 148 | ); 149 | 150 | rpc_test! ( 151 | Personal:send_transaction, TransactionRequest { 152 | from: Address::from_low_u64_be(0x123), to: Some(Address::from_low_u64_be(0x123)), 153 | gas: None, gas_price: Some(0x1.into()), 154 | value: Some(0x1.into()), data: None, 155 | nonce: None, condition: None, 156 | transaction_type: None, access_list: None, 157 | max_fee_per_gas: None, max_priority_fee_per_gas: None, 158 | }, "hunter2" 159 | => 160 | "personal_sendTransaction", vec![r#"{"from":"0x0000000000000000000000000000000000000123","gasPrice":"0x1","to":"0x0000000000000000000000000000000000000123","value":"0x1"}"#, r#""hunter2""#]; 161 | Value::String("0x0000000000000000000000000000000000000000000000000000000000000123".into()) => Address::from_low_u64_be(0x123) 162 | ); 163 | 164 | rpc_test! ( 165 | Personal:sign_transaction, TransactionRequest { 166 | from: hex!("407d73d8a49eeb85d32cf465507dd71d507100c1").into(), 167 | to: Some(hex!("853f43d8a49eeb85d32cf465507dd71d507100c1").into()), 168 | gas: Some(0x7f110.into()), 169 | gas_price: Some(0x09184e72a000u64.into()), 170 | value: Some(0x7f110.into()), 171 | data: Some(hex!("603880600c6000396000f300603880600c6000396000f3603880600c6000396000f360").into()), 172 | nonce: Some(0x0.into()), 173 | condition: None, 174 | transaction_type: None, 175 | access_list: None, 176 | max_fee_per_gas: None, 177 | max_priority_fee_per_gas: None, 178 | }, "hunter2" 179 | => 180 | "personal_signTransaction", vec![r#"{"data":"0x603880600c6000396000f300603880600c6000396000f3603880600c6000396000f360","from":"0x407d73d8a49eeb85d32cf465507dd71d507100c1","gas":"0x7f110","gasPrice":"0x9184e72a000","nonce":"0x0","to":"0x853f43d8a49eeb85d32cf465507dd71d507100c1","value":"0x7f110"}"#, r#""hunter2""#]; 181 | ::serde_json::from_str(EXAMPLE_TX).unwrap() 182 | => ::serde_json::from_str::(EXAMPLE_TX).unwrap() 183 | ); 184 | 185 | rpc_test! { 186 | Personal:import_raw_key, &[0u8; 32], "hunter2" => 187 | "personal_importRawKey", vec![r#""0000000000000000000000000000000000000000000000000000000000000000""#, r#""hunter2""#]; 188 | Value::String("0x0000000000000000000000000000000000000123".into()) => Address::from_low_u64_be(0x123) 189 | } 190 | 191 | rpc_test! { 192 | Personal:sign, Bytes(hex!("7f0d39b8347598e20466233ce2fb3e824f0f93dfbf233125d3ab09b172c62591ec24dc84049242e364895c3abdbbd843d4a0a188").to_vec()), H160(hex!("7f0d39b8347598e20466233ce2fb3e824f0f93df")), "hunter2" 193 | => 194 | "personal_sign", vec![r#""0x7f0d39b8347598e20466233ce2fb3e824f0f93dfbf233125d3ab09b172c62591ec24dc84049242e364895c3abdbbd843d4a0a188""#, r#""0x7f0d39b8347598e20466233ce2fb3e824f0f93df""#, r#""hunter2""#]; 195 | Value::String("0xdac1cba443d72e2088ed0cd2e6608ce696eb4728caf119dcfeea752f57a1163274de0b25007aa70201d0d80190071b26be2287b4a473767e5f7bc443c080b4fc1c".into()) => H520(hex!("dac1cba443d72e2088ed0cd2e6608ce696eb4728caf119dcfeea752f57a1163274de0b25007aa70201d0d80190071b26be2287b4a473767e5f7bc443c080b4fc1c")) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/contract/ens/PublicResolver.json: -------------------------------------------------------------------------------- 1 | [{"inputs":[{"internalType":"contract ENS","name":"_ens","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":true,"internalType":"uint256","name":"contentType","type":"uint256"}],"name":"ABIChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"address","name":"a","type":"address"}],"name":"AddrChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"coinType","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"newAddress","type":"bytes"}],"name":"AddressChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"target","type":"address"},{"indexed":false,"internalType":"bool","name":"isAuthorised","type":"bool"}],"name":"AuthorisationChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"hash","type":"bytes"}],"name":"ContenthashChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"name","type":"bytes"},{"indexed":false,"internalType":"uint16","name":"resource","type":"uint16"},{"indexed":false,"internalType":"bytes","name":"record","type":"bytes"}],"name":"DNSRecordChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"name","type":"bytes"},{"indexed":false,"internalType":"uint16","name":"resource","type":"uint16"}],"name":"DNSRecordDeleted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"DNSZoneCleared","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":true,"internalType":"bytes4","name":"interfaceID","type":"bytes4"},{"indexed":false,"internalType":"address","name":"implementer","type":"address"}],"name":"InterfaceChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"string","name":"name","type":"string"}],"name":"NameChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"bytes32","name":"x","type":"bytes32"},{"indexed":false,"internalType":"bytes32","name":"y","type":"bytes32"}],"name":"PubkeyChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":true,"internalType":"string","name":"indexedKey","type":"string"},{"indexed":false,"internalType":"string","name":"key","type":"string"}],"name":"TextChanged","type":"event"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"uint256","name":"contentTypes","type":"uint256"}],"name":"ABI","outputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"addr","outputs":[{"internalType":"address payable","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"uint256","name":"coinType","type":"uint256"}],"name":"addr","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"authorisations","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"clearDNSZone","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"contenthash","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes32","name":"name","type":"bytes32"},{"internalType":"uint16","name":"resource","type":"uint16"}],"name":"dnsRecord","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes32","name":"name","type":"bytes32"}],"name":"hasDNSRecords","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes4","name":"interfaceID","type":"bytes4"}],"name":"interfaceImplementer","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes[]","name":"data","type":"bytes[]"}],"name":"multicall","outputs":[{"internalType":"bytes[]","name":"results","type":"bytes[]"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"pubkey","outputs":[{"internalType":"bytes32","name":"x","type":"bytes32"},{"internalType":"bytes32","name":"y","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"uint256","name":"contentType","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"setABI","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"uint256","name":"coinType","type":"uint256"},{"internalType":"bytes","name":"a","type":"bytes"}],"name":"setAddr","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"a","type":"address"}],"name":"setAddr","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"target","type":"address"},{"internalType":"bool","name":"isAuthorised","type":"bool"}],"name":"setAuthorisation","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes","name":"hash","type":"bytes"}],"name":"setContenthash","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"setDNSRecords","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes4","name":"interfaceID","type":"bytes4"},{"internalType":"address","name":"implementer","type":"address"}],"name":"setInterface","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"string","name":"name","type":"string"}],"name":"setName","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes32","name":"x","type":"bytes32"},{"internalType":"bytes32","name":"y","type":"bytes32"}],"name":"setPubkey","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"string","name":"key","type":"string"},{"internalType":"string","name":"value","type":"string"}],"name":"setText","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes4","name":"interfaceID","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"string","name":"key","type":"string"}],"name":"text","outputs":[{"internalType":"string","name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"}] --------------------------------------------------------------------------------