diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..d2ae3f3 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,54 @@ +on: + pull_request: + push: + branches: + - master + +name: CI + +jobs: + ci: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + components: rustfmt, clippy + + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + - uses: actions-rs/install@v0.1 + with: + crate: cargo-hack + version: latest + use-tool-cache: true + + - uses: actions-rs/cargo@v1 + with: + command: hack + args: check --workspace --ignore-private --each-feature --no-dev-deps + + - uses: actions-rs/cargo@v1 + with: + command: check + args: --workspace --all-targets --all-features + + - uses: actions-rs/cargo@v1 + with: + command: test + + - uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features + + - uses: actions-rs/audit-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e88a675 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "ethereum-jsonrpc" +version = "0.1.0" +edition = "2021" +description = "Definitions for various Ethereum JSONRPC APIs" +license = "MPL-2.0" + +[dependencies] +arrayvec = { version = "0.7", features = ["serde"] } +ethereum-types = "0.14" +ethnum = { version = "1", default-features = false, features = ["serde"] } +jsonrpsee = { version = "0.16", features = ["macros"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_with = "2" +bytes = "1" +hex = "0.4" + +[dev-dependencies] +ethereum-jsonrpc = { path = ".", features = ["client"] } +hex-literal = "0.3" +tokio = { version = "1", features = ["full"] } + +[features] +client = ["jsonrpsee/client", "jsonrpsee/async-client"] +server = ["jsonrpsee/server"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d0a1fa1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at https://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9b14f6d --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# ethereum-jsonrpc +This crate contains definitions for various Ethereum JSONRPC APIs using [jsonrpsee](https://github.com/paritytech/jsonrpsee) framework. + +## Client usage example +Enable `client` feature of `ethereum-jsonrpc` crate. + +```rust,no_run +use ethereum_jsonrpc::EthApiClient; +use jsonrpsee::http_client::HttpClientBuilder; + +#[tokio::main] +async fn main() { + let client = HttpClientBuilder::default().build("http://localhost:8545").unwrap(); + + let block_number = client.block_number().await.unwrap(); + println!("Current block number is {block_number}"); +} +``` + +## License +The entire code within this repository is licensed under the [Mozilla Public License v2.0](./LICENSE) diff --git a/src/debug.rs b/src/debug.rs new file mode 100644 index 0000000..c9dd410 --- /dev/null +++ b/src/debug.rs @@ -0,0 +1,29 @@ +#[allow(unused_imports)] +use crate::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct AccountRangeResult {} + +#[cfg(any(feature = "client", feature = "server"))] +#[cfg_attr(feature = "client", rpc(client, namespace = "debug"))] +#[cfg_attr(feature = "server", rpc(server, namespace = "debug"))] +pub trait DebugApi { + #[method(name = "accountRange")] + async fn account_range( + &self, + block_id: BlockId, + index: u64, + start: H256, + limit: u64, + ) -> RpcResult<()>; + + #[method(name = "getModifiedAccountsByNumber")] + async fn get_modified_accounts_by_number( + &self, + block_number: BlockNumber, + ) -> RpcResult>; + #[method(name = "getModifiedAccountsByHash")] + async fn get_modified_accounts_by_hash(&self, block_hash: H256) -> RpcResult>; + #[method(name = "traceTransaction")] + async fn trace_transaction(&self, transaction_hash: H256) -> RpcResult<()>; +} diff --git a/src/engine.rs b/src/engine.rs new file mode 100644 index 0000000..fce79cb --- /dev/null +++ b/src/engine.rs @@ -0,0 +1,97 @@ +#[allow(unused_imports)] +use crate::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExecutionPayload { + pub parent_hash: H256, + pub fee_recipient: Address, + pub state_root: H256, + pub receipts_root: H256, + pub logs_bloom: Bloom, + pub prev_randao: H256, + pub block_number: U64, + pub gas_limit: U64, + pub gas_used: U64, + pub timestamp: U64, + pub extra_data: Bytes, + pub base_fee_per_gas: U256, + pub block_hash: H256, + pub transactions: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ForkchoiceState { + pub head_block_hash: H256, + pub safe_block_hash: H256, + pub finalized_block_hash: H256, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PayloadAttributes { + pub timestamp: U64, + pub prev_randao: H256, + pub suggested_fee_recipient: Address, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "status", rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PayloadStatusEnum { + Valid, + Invalid { + #[serde(rename = "validationError")] + validation_error: String, + }, + Syncing, + Accepted, + InvalidBlockHash { + #[serde(rename = "validationError")] + validation_error: String, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PayloadStatus { + #[serde(flatten)] + pub status: PayloadStatusEnum, + pub latest_valid_hash: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ForkchoiceUpdatedResponse { + pub payload_status: PayloadStatus, + pub payload_id: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransitionConfiguration { + pub terminal_total_difficulty: U256, + pub terminal_block_hash: H256, + pub terminal_block_number: BlockNumber, +} + +#[cfg(any(feature = "client", feature = "server"))] +#[cfg_attr(feature = "client", rpc(client, namespace = "engine"))] +#[cfg_attr(feature = "server", rpc(server, namespace = "engine"))] +pub trait EngineApi { + #[method(name = "newPayloadV1")] + async fn new_payload(&self, payload: ExecutionPayload) -> RpcResult; + #[method(name = "forkchoiceUpdatedV1")] + async fn fork_choice_updated( + &self, + fork_choice_state: ForkchoiceState, + payload_attributes: Option, + ) -> RpcResult; + #[method(name = "getPayloadV1")] + async fn get_payload(&self, payload_id: H64) -> RpcResult; + #[method(name = "exchangeTransitionConfigurationV1")] + async fn exchange_transition_configuration( + &self, + transition_configuration: TransitionConfiguration, + ) -> RpcResult; +} diff --git a/src/erigon.rs b/src/erigon.rs new file mode 100644 index 0000000..ec0ca59 --- /dev/null +++ b/src/erigon.rs @@ -0,0 +1,10 @@ +#[allow(unused_imports)] +use crate::prelude::*; + +#[cfg(any(feature = "client", feature = "server"))] +#[cfg_attr(feature = "client", rpc(client, namespace = "erigon"))] +#[cfg_attr(feature = "server", rpc(server, namespace = "erigon"))] +pub trait ErigonApi { + #[method(name = "getHeaderByNumber")] + async fn get_header_by_number(&self, block_number: u64) -> RpcResult>; +} diff --git a/src/eth.rs b/src/eth.rs new file mode 100644 index 0000000..9e3cb55 --- /dev/null +++ b/src/eth.rs @@ -0,0 +1,262 @@ +#[allow(unused_imports)] +use crate::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum BlockFilter { + #[serde(rename_all = "camelCase")] + Exact { block_hash: H256 }, + #[serde(rename_all = "camelCase")] + Bounded { + from_block: Option, + to_block: Option, + }, +} + +#[serde_as] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct LogAddressFilter( + #[serde_as(deserialize_as = "OneOrMany<_, PreferOne>")] pub Vec
, +); + +#[serde_as] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct LogTopicFilter(#[serde_as(deserialize_as = "OneOrMany<_, PreferOne>")] pub Vec); + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LogFilter { + #[serde(flatten)] + pub block_filter: Option, + pub address: Option, + pub topics: Option, 4>>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SyncStatus { + NotSyncing, + Syncing { + highest_block: BlockNumber, + current_block: BlockNumber, + }, +} + +#[derive(Serialize, Deserialize)] +struct Syncing { + highest_block: BlockNumber, + current_block: BlockNumber, +} + +impl Serialize for SyncStatus { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + SyncStatus::NotSyncing => false.serialize(serializer), + SyncStatus::Syncing { + highest_block, + current_block, + } => Syncing { + highest_block, + current_block, + } + .serialize(serializer), + } + } +} + +struct SyncStatusVisitor; + +impl<'de> Visitor<'de> for SyncStatusVisitor { + type Value = SyncStatus; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + formatter, + "false or a struct describing current sync status" + ) + } + + fn visit_bool(self, v: bool) -> Result + where + E: de::Error, + { + if !v { + Ok(Self::Value::NotSyncing) + } else { + Err(de::Error::invalid_type(de::Unexpected::Bool(v), &self)) + } + } + + fn visit_map(self, map: A) -> Result + where + A: de::MapAccess<'de>, + { + Syncing::deserialize(MapAccessDeserializer::new(map)).map( + |Syncing { + highest_block, + current_block, + }| SyncStatus::Syncing { + highest_block, + current_block, + }, + ) + } +} + +impl<'de> Deserialize<'de> for SyncStatus { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(SyncStatusVisitor) + } +} + +#[cfg(any(feature = "client", feature = "server"))] +#[cfg_attr(feature = "client", rpc(client, namespace = "eth"))] +#[cfg_attr(feature = "server", rpc(server, namespace = "eth"))] +pub trait EthApi { + #[method(name = "blockNumber")] + async fn block_number(&self) -> RpcResult; + #[method(name = "chainId")] + async fn chain_id(&self) -> RpcResult; + #[method(name = "call")] + async fn call(&self, call_data: MessageCall, block_number: BlockNumber) -> RpcResult; + #[method(name = "estimateGas")] + async fn estimate_gas( + &self, + call_data: MessageCall, + block_number: BlockNumber, + ) -> RpcResult; + #[method(name = "gasPrice")] + async fn gas_price(&self) -> RpcResult; + #[method(name = "maxPriorityFeePerGas")] + async fn max_priority_fee_per_gas(&self) -> RpcResult; + #[method(name = "getBalance")] + async fn get_balance(&self, address: Address, block_number: BlockNumber) -> RpcResult; + #[method(name = "getBlockByHash")] + async fn get_block_by_hash( + &self, + block_hash: H256, + full_tx_obj: bool, + ) -> RpcResult>; + #[method(name = "getBlockByNumber")] + async fn get_block_by_number( + &self, + block_number: BlockNumber, + full_tx_obj: bool, + ) -> RpcResult>; + #[method(name = "getBlockTransactionCountByHash")] + async fn get_block_transaction_count_by_hash(&self, block_hash: H256) -> RpcResult; + #[method(name = "getBlockTransactionCountByNumber")] + async fn get_block_transaction_count_by_number( + &self, + block_number: BlockNumber, + ) -> RpcResult; + #[method(name = "getCode")] + async fn get_code(&self, address: Address, block_number: BlockNumber) -> RpcResult; + #[method(name = "getStorageAt")] + async fn get_storage_at( + &self, + address: Address, + storage_pos: U256, + block_number: BlockNumber, + ) -> RpcResult; // Storage data is nothing more than 32-bytes + #[method(name = "getTransactionByHash")] + async fn get_transaction_by_hash(&self, hash: H256) -> RpcResult>; + #[method(name = "getTransactionByBlockHashAndIndex")] + async fn get_transaction_by_block_hash_and_index( + &self, + block_hash: H256, + index: U64, + ) -> RpcResult>; + #[method(name = "getTransactionByBlockNumberAndIndex")] + async fn get_transaction_by_block_number_and_index( + &self, + block_number: BlockNumber, + index: U64, + ) -> RpcResult>; + #[method(name = "getTransactionCount")] + async fn get_transaction_count( + &self, + address: Address, + block_number: BlockNumber, + ) -> RpcResult; + #[method(name = "getTransactionReceipt")] + async fn get_transaction_receipt(&self, tx_hash: H256) + -> RpcResult>; + #[method(name = "getUncleByBlockHashAndIndex")] + async fn get_uncle_by_block_hash_and_index( + &self, + block_hash: H256, + index: U64, + ) -> RpcResult>; + #[method(name = "getUncleByBlockNumberAndIndex")] + async fn get_uncle_by_block_number_and_index( + &self, + block_number: BlockNumber, + index: U64, + ) -> RpcResult>; + #[method(name = "getUncleCountByBlockHash")] + async fn get_uncle_count_by_block_hash(&self, block_hash: H256) -> RpcResult; + #[method(name = "getUncleCountByBlockNumber")] + async fn get_uncle_count_by_block_number(&self, block_number: BlockNumber) -> RpcResult; + #[method(name = "getLogs")] + async fn get_logs(&self, filter: LogFilter) -> RpcResult>; + #[method(name = "syncing")] + async fn syncing(&self) -> RpcResult; +} + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + use serde_json::json; + + #[test] + fn log_filter_serialize() { + let encoded = json!({ + "blockHash": "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3", + "address": "0xdeadbeef00000000000000000000000000000000", + "topics": [ + null, + "0xaa00000000000000000000000000000000000000000000000000000000000000", + [ + "0xbb00000000000000000000000000000000000000000000000000000000000000", + "0xcc00000000000000000000000000000000000000000000000000000000000000", + ], + ], + }); + let v = LogFilter { + block_filter: Some(BlockFilter::Exact { + block_hash: hex!( + "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + ) + .into(), + }), + address: Some(LogAddressFilter(vec![hex!( + "deadbeef00000000000000000000000000000000" + ) + .into()])), + topics: Some({ + let mut arr = ArrayVec::new(); + arr.push(None); + arr.push(Some(LogTopicFilter(vec![hex!( + "aa00000000000000000000000000000000000000000000000000000000000000" + ) + .into()]))); + arr.push(Some(LogTopicFilter(vec![ + hex!("bb00000000000000000000000000000000000000000000000000000000000000").into(), + hex!("cc00000000000000000000000000000000000000000000000000000000000000").into(), + ]))); + arr + }), + }; + + assert_eq!(serde_json::from_value::(encoded).unwrap(), v); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b528913 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,42 @@ +#![doc = include_str!("../README.md")] + +mod debug; +mod engine; +mod erigon; +mod eth; +mod net; +mod otterscan; +mod parity; +mod trace; +pub mod types; +mod web3; + +pub use debug::*; +pub use engine::*; +pub use erigon::*; +pub use eth::*; +pub use net::*; +pub use otterscan::*; +pub use parity::*; +pub use trace::*; +pub use web3::*; + +mod prelude { + pub use crate::types::*; + pub use arrayvec::ArrayVec; + pub use ethereum_types::{Address, Bloom, H256, H64, U64}; + pub use ethnum::prelude::*; + pub use std::{ + collections::{BTreeSet, HashSet}, + num::NonZeroUsize, + }; + + #[cfg(any(feature = "client", feature = "server"))] + pub use jsonrpsee::core::RpcResult; + pub use jsonrpsee::proc_macros::rpc; + pub use serde::{ + de::{self, value::MapAccessDeserializer, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, + }; + pub use serde_with::{formats::PreferOne, serde_as, OneOrMany}; +} diff --git a/src/net.rs b/src/net.rs new file mode 100644 index 0000000..ae65b66 --- /dev/null +++ b/src/net.rs @@ -0,0 +1,14 @@ +#[allow(unused_imports)] +use crate::prelude::*; + +#[cfg(any(feature = "client", feature = "server"))] +#[cfg_attr(feature = "client", rpc(client, namespace = "net"))] +#[cfg_attr(feature = "server", rpc(server, namespace = "net"))] +pub trait NetApi { + #[method(name = "listening")] + async fn listening(&self) -> RpcResult; + #[method(name = "peerCount")] + async fn peer_count(&self) -> RpcResult; + #[method(name = "version")] + async fn version(&self) -> RpcResult; +} diff --git a/src/otterscan.rs b/src/otterscan.rs new file mode 100644 index 0000000..f6da5c5 --- /dev/null +++ b/src/otterscan.rs @@ -0,0 +1,163 @@ +#[allow(unused_imports)] +use crate::prelude::*; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum OperationType { + Transfer = 0, + SelfDestruct = 1, + Create = 2, + Create2 = 3, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct InternalOperation { + #[serde(rename = "type")] + pub op_type: OperationType, + pub from: Address, + pub to: Address, + pub value: U256, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ReceiptWithTimestamp { + #[serde(flatten)] + pub base: TransactionReceipt, + pub timestamp: U64, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionsWithReceipts { + pub txs: Vec, + pub receipts: Vec, + pub first_page: bool, + pub last_page: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Issuance { + pub block_reward: U256, + pub uncle_reward: U256, + pub issuance: U256, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BlockData { + #[serde(flatten)] + pub inner: Block, + pub transaction_count: u64, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BlockDetails { + pub block: BlockData, + pub issuance: Issuance, + pub total_fees: U256, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BlockTransactions { + pub fullblock: Block, + pub receipts: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum TraceOperation { + Call, + StaticCall, + DelegateCall, + CallCode, + Create, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TraceEntry { + #[serde(rename = "type")] + pub op_type: TraceOperation, + pub depth: u16, + pub from: Address, + pub to: Address, + pub value: U256, + pub input: Bytes, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ContractCreatorData { + pub tx: H256, + pub creator: Address, +} + +#[cfg(any(feature = "client", feature = "server"))] +#[cfg_attr(feature = "client", rpc(client, namespace = "ots"))] +#[cfg_attr(feature = "server", rpc(server, namespace = "ots"))] +pub trait OtterscanApi { + #[method(name = "getApiLevel")] + async fn get_api_level(&self) -> RpcResult; + #[method(name = "getInternalOperations")] + async fn get_internal_operations(&self, hash: H256) -> RpcResult>; + + /// Search transactions that touch a certain address. + /// + /// It searches back a certain block (excluding); the results are sorted descending. + /// + /// The `page_size` indicates how many txs may be returned. If there are less txs than `page_size`, + /// they are just returned. But it may return a little more than pageSize if there are more txs + /// than the necessary to fill `page_size` in the last found block, i.e., let's say you want `page_size` == 25, + /// you already found 24 txs, the next block contains 4 matches, then this function will return 28 txs. + #[method(name = "searchTransactionsBefore")] + async fn search_transactions_before( + &self, + addr: Address, + block_num: u64, + page_size: usize, + ) -> RpcResult; + + /// Search transactions that touch a certain address. + /// + /// It searches forward a certain block (excluding); the results are sorted descending. + /// + /// The `page_size` indicates how many txs may be returned. If there are less txs than pageSize, + /// they are just returned. But it may return a little more than `page_size` if there are more txs + /// than the necessary to fill `page_size` in the last found block, i.e., let's say you want `page_size` == 25, + /// you already found 24 txs, the next block contains 4 matches, then this function will return 28 txs. + #[method(name = "searchTransactionsAfter")] + async fn search_transactions_after( + &self, + addr: Address, + block_num: u64, + page_size: usize, + ) -> RpcResult; + #[method(name = "getBlockDetails")] + async fn get_block_details(&self, number: u64) -> RpcResult>; + #[method(name = "getBlockDetailsByHash")] + async fn get_block_details_by_hash(&self, hash: H256) -> RpcResult>; + #[method(name = "getBlockTransactions")] + async fn get_block_transactions( + &self, + number: u64, + page_number: usize, + page_size: usize, + ) -> RpcResult>; + #[method(name = "hasCode")] + async fn has_code(&self, address: Address, block_id: BlockId) -> RpcResult; + #[method(name = "traceTransaction")] + async fn trace_transaction(&self, hash: H256) -> RpcResult>; + #[method(name = "getTransactionError")] + async fn get_transaction_error(&self, hash: H256) -> RpcResult; + #[method(name = "getTransactionBySenderAndNonce")] + async fn get_transaction_by_sender_and_nonce( + &self, + addr: Address, + nonce: u64, + ) -> RpcResult>; + #[method(name = "getContractCreator")] + async fn get_contract_creator(&self, addr: Address) -> RpcResult>; +} diff --git a/src/parity.rs b/src/parity.rs new file mode 100644 index 0000000..85d0e99 --- /dev/null +++ b/src/parity.rs @@ -0,0 +1,16 @@ +#[allow(unused_imports)] +use crate::prelude::*; + +#[cfg(any(feature = "client", feature = "server"))] +#[cfg_attr(feature = "client", rpc(client, namespace = "parity"))] +#[cfg_attr(feature = "server", rpc(server, namespace = "parity"))] +pub trait ParityApi { + #[method(name = "listStorageKeys")] + async fn list_storage_keys( + &self, + address: Address, + number_of_slots: NonZeroUsize, + offset: Option, + block: Option, + ) -> RpcResult>>; +} diff --git a/src/trace.rs b/src/trace.rs new file mode 100644 index 0000000..a225099 --- /dev/null +++ b/src/trace.rs @@ -0,0 +1,66 @@ +#[allow(unused_imports)] +use crate::prelude::*; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum TraceFilterMode { + Union, + Intersection, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Filter { + pub from_block: Option, + pub to_block: Option, + pub from_address: Option>, + pub to_address: Option>, + pub after: Option, + pub count: Option, + pub mode: Option, +} + +#[cfg(any(feature = "client", feature = "server"))] +#[cfg_attr(feature = "client", rpc(client, namespace = "trace"))] +#[cfg_attr(feature = "server", rpc(server, namespace = "trace"))] +pub trait TraceApi { + #[method(name = "call")] + async fn call( + &self, + call: MessageCall, + trace_types: HashSet, + block_id: Option, + ) -> RpcResult; + #[method(name = "callMany")] + async fn call_many( + &self, + calls: Vec<(MessageCall, HashSet)>, + block_id: Option, + ) -> RpcResult>; + #[method(name = "rawTransaction")] + async fn raw_transaction( + &self, + rlp: Bytes, + trace_types: HashSet, + block_id: Option, + ) -> RpcResult; + #[method(name = "replayBlockTransactions")] + async fn replay_block_transactions( + &self, + block_id: BlockId, + trace_types: HashSet, + ) -> RpcResult>>; + #[method(name = "replayTransaction")] + async fn replay_transaction( + &self, + hash: H256, + trace_types: HashSet, + ) -> RpcResult; + #[method(name = "block")] + async fn block( + &self, + block_id: BlockId, + ) -> RpcResult>>; + #[method(name = "filter")] + async fn filter(&self, filter: Filter) -> RpcResult>; +} diff --git a/src/types/block.rs b/src/types/block.rs new file mode 100644 index 0000000..a563a87 --- /dev/null +++ b/src/types/block.rs @@ -0,0 +1,211 @@ +use crate::prelude::*; +use std::str::FromStr; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// A 64-bit unsigned integer (or tag - "latest", "earliest", "pending"). +pub enum BlockNumber { + /// Latest block. + Latest, + /// Earliest block (genesis). + Earliest, + /// Pending block (not yet part of the cannonical chain). + Pending, + /// A block number. + Number(U64), +} + +impl Serialize for BlockNumber { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + BlockNumber::Number(ref n) => serializer.serialize_str(&format!("0x{:x}", n)), + BlockNumber::Latest => serializer.serialize_str("latest"), + BlockNumber::Earliest => serializer.serialize_str("earliest"), + BlockNumber::Pending => serializer.serialize_str("pending"), + } + } +} + +impl<'de> Deserialize<'de> for BlockNumber { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?.to_lowercase(); + Ok(match s.as_str() { + "latest" => Self::Latest, + "earliest" => Self::Earliest, + "pending" => Self::Pending, + n => BlockNumber::Number(U64::from_str(n).map_err(serde::de::Error::custom)?), + }) + } +} + +impl> From for BlockNumber { + fn from(n: T) -> Self { + BlockNumber::Number(n.into()) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +/// BlockId is either a Block Number or a Hash. +#[serde(untagged)] +pub enum BlockId { + /// A 256-bit Hash. + Hash(H256), + /// A block number. + Number(BlockNumber), +} + +impl From for BlockId { + fn from(n: BlockNumber) -> Self { + BlockId::Number(n) + } +} + +impl From for BlockId { + fn from(hash: H256) -> Self { + BlockId::Hash(hash) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Block { + /// Number of the block. + pub number: Option, + /// Block's hash. + pub hash: Option, + /// Block's parent's hash. + pub parent_hash: H256, + /// Hash of the block's uncles. + pub sha3_uncles: H256, + /// Logs bloom. + pub logs_bloom: Option, + /// Transactions root hash. + pub transactions_root: H256, + /// State root hash. + pub state_root: H256, + /// Receipts root hash. + pub receipts_root: H256, + /// Block's beneficiary. + pub miner: Address, + /// Block's PoW difficulty. + pub difficulty: U256, + /// Total chain's difficulty at moment of the block inclusion, none if pending. + pub total_difficulty: Option, + /// Seal fields. + pub seal_fields: Option<(H256, H64)>, + /// Block's nonce. + pub nonce: Option, + /// Mix hash. + pub mix_hash: Option, + /// Block's extra data. + pub extra_data: Bytes, + /// Block's size. + pub size: U64, + /// Block's gas limit. + pub gas_limit: U64, + /// Used gas of all transactions within the block. + pub gas_used: U64, + /// Block's timestamp. + pub timestamp: U64, + /// Block's transactions. + pub transactions: Vec, + /// Block's uncles. + pub uncles: ArrayVec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub base_fee_per_gas: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Header { + pub parent_hash: H256, + pub sha3_uncles: H256, + pub miner: Address, + pub state_root: H256, + pub transactions_root: H256, + pub receipts_root: H256, + pub logs_bloom: Bloom, + pub difficulty: U256, + pub number: U64, + pub gas_limit: U64, + pub gas_used: U64, + pub timestamp: U64, + pub extra_data: Bytes, + pub mix_hash: H256, + pub nonce: H64, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub base_fee_per_gas: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_ser_de_block_number() { + let block_number = BlockNumber::Earliest; + let hexstring = r#""earliest""#; + assert_eq!(serde_json::to_string(&block_number).unwrap(), hexstring); + assert_eq!( + serde_json::from_str::(hexstring).unwrap(), + block_number + ); + + let block_number = BlockNumber::Latest; + let hexstring = r#""latest""#; + assert_eq!(serde_json::to_string(&block_number).unwrap(), hexstring); + assert_eq!( + serde_json::from_str::(hexstring).unwrap(), + block_number + ); + + let block_number = BlockNumber::Pending; + let hexstring = r#""pending""#; + assert_eq!(serde_json::to_string(&block_number).unwrap(), hexstring); + assert_eq!( + serde_json::from_str::(hexstring).unwrap(), + block_number + ); + + let block_number = BlockNumber::Number(1.into()); + let hexstring = r#""0x1""#; + assert_eq!(serde_json::to_string(&block_number).unwrap(), hexstring); + assert_eq!( + serde_json::from_str::(hexstring).unwrap(), + block_number + ); + + let block_number = BlockNumber::Number(0x7b.into()); + let hexstring = r#""0x7b""#; + assert_eq!(serde_json::to_string(&block_number).unwrap(), hexstring); + } + + #[test] + fn test_ser_de_block_id() { + let block_id = r#""0x7b""#; + assert_eq!( + serde_json::to_string(&BlockId::Number(123.into())).unwrap(), + block_id + ); + + assert_eq!( + serde_json::from_str::(block_id).unwrap(), + BlockId::Number(123.into()) + ); + + let block_hash = r#""0x0000000000000000000000000000000000000000000000000000000000000000""#; + assert_eq!( + serde_json::to_string(&BlockId::Hash(H256::from([0; 32]))).unwrap(), + block_hash + ); + + assert_eq!( + serde_json::from_str::(block_hash).unwrap(), + BlockId::Hash(H256::from([0; 32])) + ); + } +} diff --git a/src/types/bytes.rs b/src/types/bytes.rs new file mode 100644 index 0000000..394b980 --- /dev/null +++ b/src/types/bytes.rs @@ -0,0 +1,74 @@ +use crate::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, Default)] +/// Wrapper over bytes::Bytes that implements serde::Serialize and serde::Deserialize. +pub struct Bytes(pub bytes::Bytes); + +impl From for bytes::Bytes { + fn from(v: Bytes) -> Self { + v.0 + } +} +impl ToString for Bytes { + fn to_string(&self) -> String { + format!("0x{}", hex::encode(self.0.as_ref())) + } +} +impl AsRef<[u8]> for Bytes { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} +impl From for Bytes { + fn from(v: U256) -> Self { + Bytes(v.to_be_bytes().to_vec().into()) + } +} + +macro_rules! impl_from { + ($type:ty) => { + impl From<$type> for Bytes { + fn from(v: $type) -> Self { + Bytes(v.into()) + } + } + }; +} +impl_from!(Vec); +impl_from!(Box<[u8]>); +impl_from!(&'static [u8]); +impl_from!(&'static str); +impl_from!(bytes::Bytes); +impl_from!(bytes::BytesMut); + +impl Serialize for Bytes { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for Bytes { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(hex::decode(s.strip_prefix("0x").unwrap_or(&s)) + .map_err(serde::de::Error::custom)? + .into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bytes_serialize() { + let bytes = Bytes(vec![1, 2, 3].into()); + assert_eq!(bytes.to_string(), "0x010203"); + } +} diff --git a/src/types/log.rs b/src/types/log.rs new file mode 100644 index 0000000..a842c2b --- /dev/null +++ b/src/types/log.rs @@ -0,0 +1,23 @@ +use crate::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +/// Transaction's log entry. +pub struct TransactionLog { + /// Log's index within transaction. + pub log_index: Option, + /// Transaction's index within block. + pub transaction_index: Option, + /// Transaction's hash. + pub transaction_hash: Option, + /// Block's hash, transaction is included in. + pub block_hash: Option, + /// Block number, transaction is included in. + pub block_number: Option, + /// Log's address. + pub address: Address, + /// Log's data. + pub data: Bytes, + /// Log's Topics. + pub topics: Vec, +} diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..0779f90 --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,45 @@ +mod block; +mod bytes; +mod log; +mod receipt; +mod trace; +mod transaction; + +pub use self::{block::*, bytes::*, log::*, receipt::*, trace::*, transaction::*}; + +use serde::de::Error; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct StringU64(pub u64); + +impl From for StringU64 { + fn from(v: u64) -> Self { + Self(v) + } +} + +impl From for u64 { + fn from(v: StringU64) -> Self { + v.0 + } +} + +impl Serialize for StringU64 { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.0.to_string()) + } +} + +impl<'de> Deserialize<'de> for StringU64 { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + <&str>::deserialize(deserializer) + .and_then(|str| str.parse().map(Self).map_err(D::Error::custom)) + } +} diff --git a/src/types/receipt.rs b/src/types/receipt.rs new file mode 100644 index 0000000..f59110b --- /dev/null +++ b/src/types/receipt.rs @@ -0,0 +1,33 @@ +use crate::types::log::TransactionLog; +use ethereum_types::{Address, Bloom, H256, U64}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +/// Transaction receipt. +pub struct TransactionReceipt { + /// 256-bit transaction hash. + pub transaction_hash: H256, + /// Transaction index. + pub transaction_index: U64, + /// Block hash. + pub block_hash: H256, + /// Block number. + pub block_number: U64, + /// Transaction sender. + pub from: Address, + /// Transaction recipient. + pub to: Option
, + /// Cumulative gas used at the moment this transaction was included. + pub cumulative_gas_used: U64, + /// Gas used in this transaction. + pub gas_used: U64, + /// Contract address if transaction creates contract. + pub contract_address: Option
, + /// Logs generated by this transaction. + pub logs: Vec, + /// Bloom logs. + pub logs_bloom: Bloom, + /// Status, 1 for success, 0 for failure. + pub status: U64, +} diff --git a/src/types/trace.rs b/src/types/trace.rs new file mode 100644 index 0000000..b89d3b5 --- /dev/null +++ b/src/types/trace.rs @@ -0,0 +1,211 @@ +use crate::prelude::*; +use std::collections::BTreeMap; + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum TraceType { + Trace, + VmTrace, + StateDiff, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FullTrace { + pub output: Bytes, + pub trace: Option>, + pub vm_trace: Option, + pub state_diff: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FullTraceWithTransactionHash { + #[serde(flatten)] + pub full_trace: FullTrace, + pub transaction_hash: H256, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct AlteredType { + pub from: T, + pub to: T, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum Delta { + #[default] + #[serde(rename = "=")] + Unchanged, + #[serde(rename = "+")] + Added(T), + #[serde(rename = "-")] + Removed(T), + #[serde(rename = "*")] + Altered(AlteredType), +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountDiff { + pub balance: Delta, + pub nonce: Delta, + pub code: Delta, + pub storage: BTreeMap>, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct StateDiff(pub BTreeMap); + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", tag = "type", content = "action")] +pub enum Action { + Call(CallAction), + Create(CreateAction), + Selfdestruct(SelfdestructAction), + Reward(RewardAction), +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum CallType { + None, + Call, + CallCode, + DelegateCall, + StaticCall, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CallAction { + pub from: Address, + pub to: Address, + pub value: U256, + pub gas: U64, + pub input: Bytes, + pub call_type: CallType, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateAction { + pub from: Address, + pub value: U256, + pub gas: U64, + pub init: Bytes, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum RewardType { + Block, + Uncle, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RewardAction { + pub author: Address, + pub value: U256, + pub reward_type: RewardType, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SelfdestructAction { + pub address: Address, + pub refund_address: Address, + pub balance: U256, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CallOutput { + pub gas_used: U64, + pub output: Bytes, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateOutput { + pub gas_used: U64, + pub code: Bytes, + pub address: Address, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum TraceOutput { + Call(CallOutput), + Create(CreateOutput), +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum TraceResult { + Success { result: TraceOutput }, + Error { error: String }, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionTrace { + pub trace_address: Vec, + pub subtraces: usize, + #[serde(flatten)] + pub action: Action, + #[serde(flatten)] + pub result: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionTraceWithLocation { + #[serde(flatten)] + pub trace: TransactionTrace, + + pub transaction_position: Option, + pub transaction_hash: Option, + pub block_number: U64, + pub block_hash: H256, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VmTrace { + pub code: Bytes, + pub ops: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VmInstruction { + pub pc: usize, + pub cost: u64, + pub ex: Option, + pub sub: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VmExecutedOperation { + pub used: u64, + pub push: Option, + pub mem: Option, + pub store: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MemoryDelta { + pub off: usize, + pub data: Bytes, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StorageDelta { + pub key: U256, + pub val: U256, +} diff --git a/src/types/transaction.rs b/src/types/transaction.rs new file mode 100644 index 0000000..7da9916 --- /dev/null +++ b/src/types/transaction.rs @@ -0,0 +1,329 @@ +use crate::prelude::*; +use serde_with::{DeserializeFromStr, SerializeDisplay}; + +/// Macro used by MessageCall types (LegacyType, EIP2930Type, EIP1155Type) +/// +/// It implements `Display` and `FromStr` to convert to/from the market to the string of its type. +/// +/// The MessageCallTypes must implement the associated constant `TYPE`. +macro_rules! impl_display_and_from_str_for_type { + ($ty:ty) => { + impl std::fmt::Display for $ty { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", <$ty>::TYPE) + } + } + + impl std::str::FromStr for $ty { + type Err = String; + + fn from_str(s: &str) -> Result { + if s == <$ty>::TYPE { + Ok(<$ty>::default()) + } else { + Err(format!("Invalid type {}, expected {}", s, <$ty>::TYPE)) + } + } + } + }; +} + +#[derive(PartialEq, Eq, Debug, Copy, Clone, Default, DeserializeFromStr, SerializeDisplay)] +pub struct LegacyType; +impl LegacyType { + const TYPE: &'static str = "0x00"; +} +impl_display_and_from_str_for_type!(LegacyType); + +#[derive(PartialEq, Eq, Debug, Copy, Clone, Default, DeserializeFromStr, SerializeDisplay)] +pub struct EIP2930Type; +impl EIP2930Type { + const TYPE: &'static str = "0x01"; +} +impl_display_and_from_str_for_type!(EIP2930Type); + +#[derive(PartialEq, Eq, Debug, Copy, Clone, Default, DeserializeFromStr, SerializeDisplay)] +pub struct EIP1559Type; +impl EIP1559Type { + const TYPE: &'static str = "0x02"; +} +impl_display_and_from_str_for_type!(EIP1559Type); + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccessListEntry { + pub address: Address, + pub storage_keys: Vec, +} + +#[serde_as] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +#[serde(untagged, deny_unknown_fields)] +pub enum MessageCall { + #[serde(rename_all = "camelCase")] + Legacy { + #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")] + tag: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + from: Option
, + #[serde(default, skip_serializing_if = "Option::is_none")] + to: Option
, + #[serde(default, skip_serializing_if = "Option::is_none")] + gas: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + gas_price: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + value: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + data: Option, + }, + #[serde(rename_all = "camelCase")] + EIP2930 { + #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")] + tag: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + from: Option
, + #[serde(default, skip_serializing_if = "Option::is_none")] + to: Option
, + #[serde(default, skip_serializing_if = "Option::is_none")] + gas: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + gas_price: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + value: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + data: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + access_list: Option>, + }, + #[serde(rename_all = "camelCase")] + EIP1559 { + #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")] + tag: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + from: Option
, + #[serde(default, skip_serializing_if = "Option::is_none")] + to: Option
, + #[serde(default, skip_serializing_if = "Option::is_none")] + gas: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + max_fee_per_gas: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + max_priority_fee_per_gas: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + value: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + data: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + access_list: Option>, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type", deny_unknown_fields)] +pub enum TransactionMessage { + #[serde(rename = "0x0")] + #[serde(rename_all = "camelCase")] + Legacy { + #[serde(default, skip_serializing_if = "Option::is_none")] + chain_id: Option, + nonce: U64, + #[serde(default, skip_serializing_if = "Option::is_none")] + to: Option
, + gas: U64, + gas_price: U256, + value: U256, + input: Bytes, + }, + #[serde(rename = "0x1")] + #[serde(rename_all = "camelCase")] + EIP2930 { + chain_id: U64, + nonce: U64, + #[serde(default, skip_serializing_if = "Option::is_none")] + to: Option
, + gas: U64, + gas_price: U256, + value: U256, + input: Bytes, + access_list: Vec, + }, + #[serde(rename = "0x2")] + #[serde(rename_all = "camelCase")] + EIP1559 { + chain_id: U64, + nonce: U64, + #[serde(default, skip_serializing_if = "Option::is_none")] + to: Option
, + gas: U64, + max_fee_per_gas: U256, + max_priority_fee_per_gas: U256, + value: U256, + input: Bytes, + access_list: Vec, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Transaction { + #[serde(flatten)] + pub message: TransactionMessage, + /// RLP encoded representation of the transaction. + pub v: U64, + pub r: H256, + pub s: H256, + + pub from: Address, + pub hash: H256, + pub transaction_index: Option, + pub block_number: Option, + pub block_hash: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged)] +/// Tx is either a transaction or a transaction hash. +pub enum Tx { + /// Transaction. + Transaction(Box), + /// Transaction hash. + Hash(H256), +} + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + use serde_json::json; + + #[test] + fn test_ser_de_hexbytes_option() { + let call_data = MessageCall::Legacy { + tag: None, + from: None, + to: Some(Address::from([0; 20])), + gas: None, + gas_price: None, + value: None, + data: None, + }; + let hexstring = json!({ + "to":"0x0000000000000000000000000000000000000000", + }); + assert_eq!(serde_json::to_value(&call_data).unwrap(), hexstring); + assert_eq!( + serde_json::from_value::(hexstring).unwrap(), + call_data + ); + + let call_data_with_data = MessageCall::Legacy { + tag: None, + from: None, + to: Some(Address::from([0; 20])), + gas: None, + gas_price: None, + value: None, + data: Some(Bytes::from(&b"Hello Akula"[..])), + }; + + let hexstring_with_data = json!({ + "to":"0x0000000000000000000000000000000000000000", + "data":"0x48656c6c6f20416b756c61", + }); + assert_eq!( + serde_json::to_value(&call_data_with_data).unwrap(), + hexstring_with_data + ); + assert_eq!( + serde_json::from_value::(hexstring_with_data).unwrap(), + call_data_with_data + ); + } + + #[test] + fn test_deserialize_with_tag() { + let call_data = MessageCall::Legacy { + tag: Some(LegacyType), + from: None, + to: Some(Address::from([0; 20])), + gas: None, + gas_price: None, + value: None, + data: None, + }; + + let hexstring = json!({ + "type": "0x00", + "to":"0x0000000000000000000000000000000000000000", + }); + + assert_eq!( + serde_json::from_value::(hexstring).unwrap(), + call_data, + ); + } + + #[test] + fn test_deserialize_with_tag_fails() { + let hexstring = json!({ + "type": "0xdeadbeef", + "to":"0x0000000000000000000000000000000000000000", + }); + + assert_eq!( + &serde_json::from_value::(hexstring) + .err() + .unwrap() + .to_string(), + "data did not match any variant of untagged enum MessageCall" + ); + } + + #[test] + fn test_tx_ser() { + let tx = Transaction { + message: TransactionMessage::Legacy { + chain_id: Some(2_u64.into()), + nonce: 12_u64.into(), + gas: 21000_u64.into(), + gas_price: 20_000_000_000_u64.into(), + to: Some(hex!("727fc6a68321b754475c668a6abfb6e9e71c169a").into()), + value: 10.as_u256() * 1_000_000_000 * 1_000_000_000, + input: hex!("a9059cbb000000000213ed0f886efd100b67c7e4ec0a85a7d20dc971600000000000000000000015af1d78b58c4000").to_vec().into(), + }, + v: 40_u64.into(), + r: hex!("be67e0a07db67da8d446f76add590e54b6e92cb6b8f9835aeb67540579a27717").into(), + s: hex!("2d690516512020171c1ec870f6ff45398cc8609250326be89915fb538e7bd718").into(), + from: Address::repeat_byte(0xAA), + hash: H256::repeat_byte(0xBB), + transaction_index: Some(0x42.into()), + block_hash: None, + block_number: None, + }; + let serialized = json!({ + "type": "0x0", + "chainId": "0x2", + "nonce": "0xc", + "to": "0x727fc6a68321b754475c668a6abfb6e9e71c169a", + "gas": "0x5208", + "gasPrice":"0x4a817c800", + "value":"0x8ac7230489e80000", + "input":"0xa9059cbb000000000213ed0f886efd100b67c7e4ec0a85a7d20dc971600000000000000000000015af1d78b58c4000", + "v":"0x28", + "r":"0xbe67e0a07db67da8d446f76add590e54b6e92cb6b8f9835aeb67540579a27717", + "s":"0x2d690516512020171c1ec870f6ff45398cc8609250326be89915fb538e7bd718", + "from":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "hash":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "transactionIndex":"0x42", + "blockHash": null, + "blockNumber": null, + }); + + assert_eq!(serde_json::to_value(&tx).unwrap(), serialized); + assert_eq!( + serde_json::from_value::(serialized).unwrap(), + tx + ); + } +} diff --git a/src/web3.rs b/src/web3.rs new file mode 100644 index 0000000..9d5e78a --- /dev/null +++ b/src/web3.rs @@ -0,0 +1,10 @@ +#[allow(unused_imports)] +use crate::prelude::*; + +#[cfg(any(feature = "client", feature = "server"))] +#[cfg_attr(feature = "client", rpc(client, namespace = "web3"))] +#[cfg_attr(feature = "server", rpc(server, namespace = "web3"))] +pub trait Web3Api { + #[method(name = "clientVersion")] + async fn client_version(&self) -> RpcResult; +}