From 5f13853d296f4e08435da1b40d48f7338f663070 Mon Sep 17 00:00:00 2001 From: Qinxuan Chen Date: Tue, 28 May 2024 22:42:29 +0800 Subject: [PATCH] feat: add rpc v2 types and api (#1427) --- Cargo.lock | 68 +++ Cargo.toml | 7 + client/rpc-v2/Cargo.toml | 13 + client/rpc-v2/api/Cargo.toml | 15 + client/rpc-v2/api/src/debug.rs | 47 ++ client/rpc-v2/api/src/eth/mod.rs | 359 ++++++++++++++ client/rpc-v2/api/src/eth/pubsub.rs | 38 ++ client/rpc-v2/api/src/lib.rs | 35 ++ client/rpc-v2/api/src/net.rs | 37 ++ client/rpc-v2/api/src/txpool.rs | 71 +++ client/rpc-v2/api/src/web3.rs | 35 ++ client/rpc-v2/src/lib.rs | 19 + client/rpc-v2/types/Cargo.toml | 17 + client/rpc-v2/types/src/access_list.rs | 46 ++ client/rpc-v2/types/src/block.rs | 157 +++++++ client/rpc-v2/types/src/block_id.rs | 427 +++++++++++++++++ client/rpc-v2/types/src/bytes.rs | 206 ++++++++ client/rpc-v2/types/src/fee.rs | 45 ++ .../rpc-v2/types/src/filter/block_option.rs | 111 +++++ client/rpc-v2/types/src/filter/mod.rs | 439 ++++++++++++++++++ client/rpc-v2/types/src/filter/utility.rs | 110 +++++ client/rpc-v2/types/src/index.rs | 124 +++++ client/rpc-v2/types/src/lib.rs | 42 ++ client/rpc-v2/types/src/log.rs | 52 +++ client/rpc-v2/types/src/proof.rs | 58 +++ client/rpc-v2/types/src/pubsub.rs | 115 +++++ client/rpc-v2/types/src/state.rs | 53 +++ client/rpc-v2/types/src/sync.rs | 108 +++++ client/rpc-v2/types/src/transaction/mod.rs | 133 ++++++ .../rpc-v2/types/src/transaction/receipt.rs | 67 +++ .../rpc-v2/types/src/transaction/request.rs | 306 ++++++++++++ .../rpc-v2/types/src/transaction/signature.rs | 152 ++++++ client/rpc-v2/types/src/txpool.rs | 198 ++++++++ 33 files changed, 3710 insertions(+) create mode 100644 client/rpc-v2/Cargo.toml create mode 100644 client/rpc-v2/api/Cargo.toml create mode 100644 client/rpc-v2/api/src/debug.rs create mode 100644 client/rpc-v2/api/src/eth/mod.rs create mode 100644 client/rpc-v2/api/src/eth/pubsub.rs create mode 100644 client/rpc-v2/api/src/lib.rs create mode 100644 client/rpc-v2/api/src/net.rs create mode 100644 client/rpc-v2/api/src/txpool.rs create mode 100644 client/rpc-v2/api/src/web3.rs create mode 100644 client/rpc-v2/src/lib.rs create mode 100644 client/rpc-v2/types/Cargo.toml create mode 100644 client/rpc-v2/types/src/access_list.rs create mode 100644 client/rpc-v2/types/src/block.rs create mode 100644 client/rpc-v2/types/src/block_id.rs create mode 100644 client/rpc-v2/types/src/bytes.rs create mode 100644 client/rpc-v2/types/src/fee.rs create mode 100644 client/rpc-v2/types/src/filter/block_option.rs create mode 100644 client/rpc-v2/types/src/filter/mod.rs create mode 100644 client/rpc-v2/types/src/filter/utility.rs create mode 100644 client/rpc-v2/types/src/index.rs create mode 100644 client/rpc-v2/types/src/lib.rs create mode 100644 client/rpc-v2/types/src/log.rs create mode 100644 client/rpc-v2/types/src/proof.rs create mode 100644 client/rpc-v2/types/src/pubsub.rs create mode 100644 client/rpc-v2/types/src/state.rs create mode 100644 client/rpc-v2/types/src/sync.rs create mode 100644 client/rpc-v2/types/src/transaction/mod.rs create mode 100644 client/rpc-v2/types/src/transaction/receipt.rs create mode 100644 client/rpc-v2/types/src/transaction/request.rs create mode 100644 client/rpc-v2/types/src/transaction/signature.rs create mode 100644 client/rpc-v2/types/src/txpool.rs diff --git a/Cargo.lock b/Cargo.lock index d4f40c06a2..cb0da466bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1325,6 +1325,19 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "const-hex" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ba00838774b4ab0233e355d26710fbfc8327a05c017f6dc4873f876d1f79f78" +dependencies = [ + "cfg-if", + "cpufeatures", + "hex", + "proptest", + "serde", +] + [[package]] name = "const-oid" version = "0.9.5" @@ -2583,6 +2596,29 @@ dependencies = [ "sp-crypto-hashing", ] +[[package]] +name = "fc-rpc-v2" +version = "2.0.0-dev" + +[[package]] +name = "fc-rpc-v2-api" +version = "0.1.0" +dependencies = [ + "ethereum-types", + "fc-rpc-v2-types", + "jsonrpsee", +] + +[[package]] +name = "fc-rpc-v2-types" +version = "0.1.0" +dependencies = [ + "const-hex", + "ethereum-types", + "serde", + "serde_json", +] + [[package]] name = "fc-storage" version = "1.0.0-dev" @@ -3639,6 +3675,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hex-conservative" @@ -6864,6 +6903,20 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "proptest" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" +dependencies = [ + "bitflags 2.4.0", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "unarray", +] + [[package]] name = "prost" version = "0.11.9" @@ -7188,6 +7241,15 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "raw-cpuid" version = "11.0.1" @@ -11200,6 +11262,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-bidi" version = "0.3.13" diff --git a/Cargo.toml b/Cargo.toml index eef3f06b9c..9f84b5a2cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,9 @@ members = [ "client/consensus", "client/rpc-core", "client/rpc", + "client/rpc-v2", + "client/rpc-v2/api", + "client/rpc-v2/types", "client/db", "client/storage", "client/mapping-sync", @@ -48,6 +51,7 @@ repository = "https://github.com/paritytech/frontier/" async-trait = "0.1" bn = { package = "substrate-bn", version = "0.6", default-features = false } clap = { version = "4.5", features = ["derive", "deprecated"] } +const-hex = { version = "1.11", default-features = false, features = ["alloc"] } derive_more = "0.99" environmental = { version = "1.1.4", default-features = false } ethereum = { version = "0.15.0", default-features = false } @@ -169,6 +173,9 @@ fc-db = { path = "client/db", default-features = false } fc-mapping-sync = { path = "client/mapping-sync", default-features = false } fc-rpc = { path = "client/rpc", default-features = false } fc-rpc-core = { path = "client/rpc-core" } +fc-rpc-v2 = { path = "client/rpc-v2" } +fc-rpc-v2-api = { path = "client/rpc-v2/api" } +fc-rpc-v2-types = { path = "client/rpc-v2/types" } fc-storage = { path = "client/storage" } # Frontier Primitive fp-account = { path = "primitives/account", default-features = false } diff --git a/client/rpc-v2/Cargo.toml b/client/rpc-v2/Cargo.toml new file mode 100644 index 0000000000..6268318116 --- /dev/null +++ b/client/rpc-v2/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "fc-rpc-v2" +version = "2.0.0-dev" +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" +description = "Ethereum RPC (web3) compatibility layer for Substrate." +authors = { workspace = true } +edition = { workspace = true } +repository = { workspace = true } + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] diff --git a/client/rpc-v2/api/Cargo.toml b/client/rpc-v2/api/Cargo.toml new file mode 100644 index 0000000000..f180557fe2 --- /dev/null +++ b/client/rpc-v2/api/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "fc-rpc-v2-api" +version = "0.1.0" +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" +description = "Ethereum RPC (web3) interfaces." +authors = { workspace = true } +edition = { workspace = true } +repository = { workspace = true } + +[dependencies] +ethereum-types = { workspace = true } +jsonrpsee = { workspace = true, features = ["client-core", "server-core", "macros"] } + +# Frontier +fc-rpc-v2-types = { workspace = true } diff --git a/client/rpc-v2/api/src/debug.rs b/client/rpc-v2/api/src/debug.rs new file mode 100644 index 0000000000..71ab33bb07 --- /dev/null +++ b/client/rpc-v2/api/src/debug.rs @@ -0,0 +1,47 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use ethereum_types::H256; +use jsonrpsee::{core::RpcResult, proc_macros::rpc}; + +use crate::types::{block_id::BlockNumberOrTagOrHash, bytes::Bytes}; + +/// Debug RPC interface. +#[rpc(client, server, namespace = "debug")] +#[async_trait] +pub trait DebugApi { + /// Returns an RLP-encoded header. + #[method(name = "getRawHeader")] + async fn raw_header(&self, block: BlockNumberOrTagOrHash) -> RpcResult>; + + /// Returns an RLP-encoded block. + #[method(name = "getRawBlock")] + async fn raw_block(&self, block: BlockNumberOrTagOrHash) -> RpcResult>; + + /// Returns the EIP-2718 binary-encoded transaction. + #[method(name = "getRawTransaction")] + async fn raw_transaction(&self, transaction_hash: H256) -> RpcResult>; + + /// Returns an array of EIP-2718 binary-encoded receipts. + #[method(name = "getRawReceipts")] + async fn raw_receipts(&self, block: BlockNumberOrTagOrHash) -> RpcResult>; + + /// Returns an array of recent bad blocks that the client has seen on the network. + #[method(name = "getBadBlocks")] + async fn bad_blocks(&self) -> RpcResult>; +} diff --git a/client/rpc-v2/api/src/eth/mod.rs b/client/rpc-v2/api/src/eth/mod.rs new file mode 100644 index 0000000000..704ef9521a --- /dev/null +++ b/client/rpc-v2/api/src/eth/mod.rs @@ -0,0 +1,359 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +/// (Non-standard) Ethereum pubsub interface. +pub mod pubsub; + +use ethereum_types::{Address, H256, U256, U64}; +use jsonrpsee::{core::RpcResult, proc_macros::rpc}; + +pub use self::pubsub::*; +use crate::types::{ + access_list::AccessListResult, + block::Block, + block_id::{BlockNumberOrTag, BlockNumberOrTagOrHash}, + bytes::Bytes, + fee::FeeHistoryResult, + filter::{Filter, FilterChanges}, + index::Index, + log::Log, + proof::AccountProof, + state::StateOverrides, + sync::SyncingStatus, + transaction::{Transaction, TransactionReceipt, TransactionRequest}, +}; + +/// Ethereum RPC client interfaces. +pub trait EthApiClient: + EthBlockApiClient + + EthClientApiClient + + EthExecuteApiClient + + EthFeeMarketApiClient + + EthFilterApiClient + + EthSignApiClient + + EthStateApiClient + + EthSubmitApiClient + + EthTransactionApiClient + + EthPubSubApiClient +{ +} + +impl EthApiClient for T where + T: EthBlockApiClient + + EthClientApiClient + + EthExecuteApiClient + + EthFeeMarketApiClient + + EthFilterApiClient + + EthSignApiClient + + EthStateApiClient + + EthSubmitApiClient + + EthTransactionApiClient + + EthPubSubApiClient +{ +} + +/// Ethereum RPC server interfaces. +pub trait EthApiServer: + EthBlockApiServer + + EthClientApiServer + + EthExecuteApiServer + + EthFeeMarketApiServer + + EthFilterApiServer + + EthSignApiServer + + EthStateApiServer + + EthSubmitApiServer + + EthTransactionApiServer + + EthPubSubApiServer +{ +} + +impl EthApiServer for T where + T: EthBlockApiServer + + EthClientApiServer + + EthExecuteApiServer + + EthFeeMarketApiServer + + EthFilterApiServer + + EthSignApiServer + + EthStateApiServer + + EthSubmitApiServer + + EthTransactionApiServer + + EthPubSubApiServer +{ +} + +/// Ethereum (block) RPC interface. +#[rpc(client, server, namespace = "eth")] +#[async_trait] +pub trait EthBlockApi { + /// Returns information about a block by hash. + #[method(name = "getBlockByHash")] + async fn block_by_hash(&self, hash: H256, full: bool) -> RpcResult>; + + /// Returns information about a block by number. + #[method(name = "getBlockByNumber")] + async fn block_by_number( + &self, + block: BlockNumberOrTag, + full: bool, + ) -> RpcResult>; + + /// Returns the number of transactions in a block from a block matching the given block hash. + #[method(name = "getBlockTransactionCountByHash")] + async fn block_transaction_count_by_hash(&self, block_hash: H256) -> RpcResult>; + + /// Returns the number of transactions in a block matching the given block number. + #[method(name = "getBlockTransactionCountByNumber")] + async fn block_transaction_count_by_number( + &self, + block: BlockNumberOrTag, + ) -> RpcResult>; + + /// Returns the number of uncles in a block from a block matching the given block hash. + #[method(name = "getUncleCountByBlockHash")] + async fn block_uncles_count_by_hash(&self, block_hash: H256) -> RpcResult; + + /// Returns the number of uncles in a block matching the given block number. + #[method(name = "getUncleCountByBlockNumber")] + async fn block_uncles_count_by_number(&self, block: BlockNumberOrTag) -> RpcResult; + + /// Returns the receipts of a block by number or hash. + #[method(name = "getBlockReceipts")] + async fn block_transaction_receipts( + &self, + number_or_hash: BlockNumberOrTagOrHash, + ) -> RpcResult>>; +} + +/// Ethereum (client) RPC interface. +#[rpc(client, server, namespace = "eth")] +#[async_trait] +pub trait EthClientApi { + /// Returns the chain ID of the current network. + #[method(name = "chainId")] + async fn chain_id(&self) -> RpcResult; + + /// Returns an object with data about the sync status or false. + #[method(name = "syncing")] + async fn syncing(&self) -> RpcResult; + + /// Returns the client coinbase address. + #[method(name = "coinbase")] + async fn author(&self) -> RpcResult
; + + /// Returns a list of addresses owned by client. + #[method(name = "accounts")] + async fn accounts(&self) -> RpcResult>; + + /// Returns the number of most recent block. + #[method(name = "blockNumber")] + async fn block_number(&self) -> RpcResult; +} + +/// Ethereum (execute) RPC interface. +#[rpc(client, server, namespace = "eth")] +#[async_trait] +pub trait EthExecuteApi { + /// Executes a new message call immediately without creating a transaction on the blockchain. + #[method(name = "call")] + async fn call( + &self, + request: TransactionRequest, + number_or_hash: Option, + state_overrides: Option, + // block_overrides: Option, + ) -> RpcResult; + + /// Generates and returns an estimate of hou much gas is necessary to allow the transaction to complete. + #[method(name = "estimateGas")] + async fn estimate_gas( + &self, + request: TransactionRequest, + number_or_hash: Option, + state_overrides: Option, + ) -> RpcResult; + + /// Generates an access list for a transaction. + #[method(name = "createAccessList")] + async fn create_access_list( + &self, + request: TransactionRequest, + number_or_hash: Option, + ) -> RpcResult; +} + +/// Ethereum (fee market) RPC interface. +#[rpc(client, server, namespace = "eth")] +#[async_trait] +pub trait EthFeeMarketApi { + /// Returns the current price per gas in wei. + #[method(name = "gasPrice")] + async fn gas_price(&self) -> RpcResult; + + /// Returns the current maxPriorityFeePerGas per gas in wei, which introduced in EIP-1159. + #[method(name = "maxPriorityFeePerGas")] + async fn max_priority_fee_per_gas(&self) -> RpcResult; + + /// Returns transaction base fee per gas and effective priority fee per gas for the requested/supported block range. + /// + /// Transaction fee history, which is introduced in EIP-1159. + #[method(name = "feeHistory")] + async fn fee_history( + &self, + block_count: U256, + newest_block: BlockNumberOrTag, + reward_percentiles: Option>, + ) -> RpcResult; +} + +/// Ethereum (filter) RPC interface. +#[rpc(client, server, namespace = "eth")] +#[async_trait] +pub trait EthFilterApi { + /// Creates a filter object, based on filter options, to notify when the state changes (logs). + #[method(name = "newFilter")] + async fn new_filter(&self, filter: Filter) -> RpcResult; + + /// Creates a filter in the node, to notify when a new block arrives. + #[method(name = "newBlockFilter")] + async fn new_block_filter(&self) -> RpcResult; + + /// Creates a filter in the node, to notify when new pending transactions arrive. + #[method(name = "newPendingTransactionFilter")] + async fn new_pending_transaction_filter(&self, full: Option) -> RpcResult; + + /// Uninstalls a filter with given id. + #[method(name = "uninstallFilter")] + async fn uninstall_filter(&self, filter_id: Index) -> RpcResult; + + /// Polling method for a filter, which returns an array of logs which occurred since last poll. + #[method(name = "getFilterChanges")] + async fn filter_changes(&self, filter_id: Index) -> RpcResult; + + /// Returns an array of all logs matching filter with given id. + #[method(name = "getFilterLogs")] + async fn filter_logs(&self, filter_id: Index) -> RpcResult>; + + /// Returns an array of all logs matching filter with given id. + #[method(name = "getLogs")] + async fn logs(&self, filter: Filter) -> RpcResult>; +} + +/// Ethereum (sign) RPC interface. +#[rpc(client, server, namespace = "eth")] +#[async_trait] +pub trait EthSignApi { + /// Returns an EIP-191 signature over the provided data. + #[method(name = "sign")] + async fn sign(&self, address: Address, message: Bytes) -> RpcResult; + + /// Returns an RLP encoded transaction signed by the specified account. + #[method(name = "signTransaction")] + async fn sign_transaction(&self, request: TransactionRequest) -> RpcResult; +} + +/// Ethereum (state) RPC interface. +#[rpc(client, server, namespace = "eth")] +#[async_trait] +pub trait EthStateApi { + /// Returns the balance of the account of given address. + #[method(name = "getBalance")] + async fn balance( + &self, + address: Address, + block: Option, + ) -> RpcResult; + + /// Returns the value from a storage position at a given address. + #[method(name = "getStorageAt")] + async fn storage_at( + &self, + address: Address, + slot: U256, + block: Option, + ) -> RpcResult; + + /// Returns the number of transactions sent from an address. + #[method(name = "getTransactionCount")] + async fn transaction_count( + &self, + address: Address, + block: Option, + ) -> RpcResult; + + /// Returns the code at a given address. + #[method(name = "getCode")] + async fn code( + &self, + address: Address, + block: Option, + ) -> RpcResult; + + /// Returns the merkle proof for a given account and optionally some storage keys. + #[method(name = "getProof")] + async fn proof( + &self, + address: Address, + storage_keys: H256, + block: Option, + ) -> RpcResult; +} + +/// Ethereum (submit) RPC interface. +#[rpc(client, server, namespace = "eth")] +#[async_trait] +pub trait EthSubmitApi { + /// Signs and submits a transaction; will block waiting for signer to return the transaction hash. + #[method(name = "eth_sendTransaction")] + async fn send_transaction(&self, request: TransactionRequest) -> RpcResult; + + /// Submits a raw signed transaction, returning its hash. + #[method(name = "eth_sendRawTransaction")] + async fn send_raw_transaction(&self, bytes: Bytes) -> RpcResult; +} + +/// Ethereum (transaction) RPC interface. +#[rpc(client, server, namespace = "eth")] +#[async_trait] +pub trait EthTransactionApi { + /// Returns the information about a transaction requested by transaction hash. + #[method(name = "getTransactionByHash")] + async fn transaction_by_hash(&self, transaction_hash: H256) -> RpcResult>; + + /// Returns information about a transaction by block hash and transaction index position. + #[method(name = "getTransactionByBlockHashAndIndex")] + async fn transaction_by_block_hash_and_index( + &self, + block_hash: H256, + transaction_index: Index, + ) -> RpcResult>; + + /// Returns information about a transaction by block number and transaction index position. + #[method(name = "getTransactionByBlockNumberAndIndex")] + async fn transaction_by_block_number_and_index( + &self, + block: BlockNumberOrTag, + transaction_index: Index, + ) -> RpcResult>; + + /// Returns the receipt of a transaction by transaction hash. + #[method(name = "getTransactionReceipt")] + async fn transaction_receipt( + &self, + transaction_hash: H256, + ) -> RpcResult>; +} diff --git a/client/rpc-v2/api/src/eth/pubsub.rs b/client/rpc-v2/api/src/eth/pubsub.rs new file mode 100644 index 0000000000..64b15c2297 --- /dev/null +++ b/client/rpc-v2/api/src/eth/pubsub.rs @@ -0,0 +1,38 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use jsonrpsee::{core::SubscriptionResult, proc_macros::rpc}; + +use crate::types::pubsub::{PubSubKind, PubSubParams, PubSubResult}; + +/// (Non-standard) Ethereum pubsub interface. +/// +/// It's not part of the standard interface for Ethereum clients, but this interface is very useful +/// and almost all Ethereum clients implement this interface. So we also provide the interface +/// compatible with geth. +#[rpc(client, server, namespace = "eth")] +#[async_trait] +pub trait EthPubSubApi { + /// Create an ethereum subscription for the given params + #[subscription( + name = "subscribe" => "subscription", + unsubscribe = "unsubscribe", + item = PubSubResult + )] + async fn sub(&self, kind: PubSubKind, params: Option) -> SubscriptionResult; +} diff --git a/client/rpc-v2/api/src/lib.rs b/client/rpc-v2/api/src/lib.rs new file mode 100644 index 0000000000..bf47d76053 --- /dev/null +++ b/client/rpc-v2/api/src/lib.rs @@ -0,0 +1,35 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Ethereum RPC APIs. + +#![warn(unused_crate_dependencies)] + +/// RPC types. +pub use fc_rpc_v2_types as types; + +/// Debug namespace API +pub mod debug; +/// Eth namespace API +pub mod eth; +/// Net namespace API +pub mod net; +/// Txpool namespace API +pub mod txpool; +/// Web3 namespace API +pub mod web3; diff --git a/client/rpc-v2/api/src/net.rs b/client/rpc-v2/api/src/net.rs new file mode 100644 index 0000000000..d00eab66ee --- /dev/null +++ b/client/rpc-v2/api/src/net.rs @@ -0,0 +1,37 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use ethereum_types::U64; +use jsonrpsee::{core::RpcResult, proc_macros::rpc}; + +/// Net RPC interface. +#[rpc(client, server, namespace = "net")] +#[async_trait] +pub trait NetApi { + /// Returns the network ID (e.g. 1 for mainnet, 5 for goerli). + #[method(name = "version")] + async fn version(&self) -> RpcResult; + + /// Returns the number of connected peers. + #[method(name = "peerCount")] + async fn peer_count(&self) -> RpcResult; + + /// Returns an indication if the node is listening for network connections. + #[method(name = "listening")] + async fn listening(&self) -> RpcResult; +} diff --git a/client/rpc-v2/api/src/txpool.rs b/client/rpc-v2/api/src/txpool.rs new file mode 100644 index 0000000000..532423ec20 --- /dev/null +++ b/client/rpc-v2/api/src/txpool.rs @@ -0,0 +1,71 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use ethereum_types::Address; +use jsonrpsee::{core::RpcResult, proc_macros::rpc}; + +use crate::types::txpool::{TxpoolContent, TxpoolContentFrom, TxpoolInspect, TxpoolStatus}; + +/// TxPool RPC interface. +#[rpc(client, server, namespace = "txpool")] +#[async_trait] +pub trait TxPoolApi { + /// The content inspection property can be queried to list the exact details of all the + /// transactions currently pending for inclusion in the next block(s), as well as the ones that + /// are being scheduled for future execution only. + /// + /// The result is an object with two fields pending and queued. Each of these fields are + /// associative arrays, in which each entry maps an origin-address to a batch of scheduled + /// transactions. These batches themselves are maps associating nonces with actual transactions. + /// + /// Refer to [txpool_content](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-txpool#txpool-content). + #[method(name = "content")] + async fn content(&self) -> RpcResult; + + /// Retrieves the transactions contained within the txpool, returning pending as well as queued + /// transactions of this address, grouped by nonce. + /// + /// Refer to [txpool_contentFrom](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-txpool#txpool-contentfrom). + #[method(name = "contentFrom")] + async fn content_from(&self, address: Address) -> RpcResult; + + /// The inspect inspection property can be queried to list a textual summary of all the + /// transactions currently pending for inclusion in the next block(s), as well as the ones that + /// are being scheduled for future execution only. This is a method specifically tailored to + /// developers to quickly see the transactions in the pool and find any potential issues. + /// + /// The result is an object with two fields pending and queued. Each of these fields are + /// associative arrays, in which each entry maps an origin-address to a batch of scheduled + /// transactions. These batches themselves are maps associating nonces with transactions + /// summary strings. + /// + /// Refer to [txpool_inspect](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-txpool#txpool-inspect). + #[method(name = "inspect")] + async fn inspect(&self) -> RpcResult; + + /// The status inspection property can be queried for the number of transactions currently + /// pending for inclusion in the next block(s), as well as the ones that are being scheduled + /// for future execution only. + /// + /// The result is an object with two fields pending and queued, each of which is a counter + /// representing the number of transactions in that particular state. + /// + /// Refer to [txpool_status](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-txpool#txpool-status). + #[method(name = "status")] + async fn status(&self) -> RpcResult; +} diff --git a/client/rpc-v2/api/src/web3.rs b/client/rpc-v2/api/src/web3.rs new file mode 100644 index 0000000000..0243aba3ac --- /dev/null +++ b/client/rpc-v2/api/src/web3.rs @@ -0,0 +1,35 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use ethereum_types::H256; +use jsonrpsee::{core::RpcResult, proc_macros::rpc}; + +use crate::types::bytes::Bytes; + +/// Web3 RPC interface. +#[rpc(client, server, namespace = "web3")] +#[async_trait] +pub trait Web3Api { + /// Returns the current client version. + #[method(name = "clientVersion")] + async fn client_version(&self) -> RpcResult; + + /// Returns sha3 of the given data. + #[method(name = "sha3")] + async fn sha3(&self, input: Bytes) -> RpcResult; +} diff --git a/client/rpc-v2/src/lib.rs b/client/rpc-v2/src/lib.rs new file mode 100644 index 0000000000..c8a1a8529e --- /dev/null +++ b/client/rpc-v2/src/lib.rs @@ -0,0 +1,19 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#![warn(unused_crate_dependencies)] diff --git a/client/rpc-v2/types/Cargo.toml b/client/rpc-v2/types/Cargo.toml new file mode 100644 index 0000000000..25de936ae1 --- /dev/null +++ b/client/rpc-v2/types/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "fc-rpc-v2-types" +version = "0.1.0" +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" +description = "Ethereum RPC (web3) types." +authors = { workspace = true } +edition = { workspace = true } +repository = { workspace = true } + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +const-hex = { workspace = true, features = ["serde"] } +ethereum-types = { workspace = true, features = ["ethbloom", "serialize"] } +serde = { workspace = true } +serde_json = { workspace = true } diff --git a/client/rpc-v2/types/src/access_list.rs b/client/rpc-v2/types/src/access_list.rs new file mode 100644 index 0000000000..39e13ae544 --- /dev/null +++ b/client/rpc-v2/types/src/access_list.rs @@ -0,0 +1,46 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use ethereum_types::{Address, H256, U256}; +use serde::{Deserialize, Serialize}; + +/// A list of addresses and storage keys. +/// +/// These addresses and storage keys are added into the `accessed_addresses` and +/// `accessed_storage_keys` global sets (introduced in [EIP-2929](https://eips.ethereum.org/EIPS/eip-2929)). +/// +/// A gas cost is charged, though at a discount relative to the cost of accessing outside the list. +pub type AccessList = Vec; + +/// The item of access list. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccessListItem { + pub address: Address, + pub storage_keys: Vec, +} + +/// The response type of `eth_createAccessList`. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccessListResult { + pub access_list: AccessList, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub error: Option, + pub gas_used: U256, +} diff --git a/client/rpc-v2/types/src/block.rs b/client/rpc-v2/types/src/block.rs new file mode 100644 index 0000000000..76f383db8b --- /dev/null +++ b/client/rpc-v2/types/src/block.rs @@ -0,0 +1,157 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use ethereum_types::{Address, Bloom, H256, U256, U64}; +use serde::{Deserialize, Serialize}; + +use crate::{bytes::Bytes, transaction::Transaction}; + +/// Block information. +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Block { + /// Block header. + #[serde(flatten)] + pub header: Header, + + /// Block transactions. + pub transactions: BlockTransactions, + + /// Uncles' hashes. + #[serde(default)] + pub uncles: Vec, + + /// Block size in bytes. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub size: Option, + + /// Withdrawals, see [EIP-4895](https://eips.ethereum.org/EIPS/eip-4895). + #[serde(skip_serializing_if = "Option::is_none")] + pub withdrawals: Option>, +} + +/// Block header representation. +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Header { + /// Block number. + pub number: U256, + /// Block hash. + pub hash: Option, + /// Hash of the parent block. + pub parent_hash: H256, + /// Hash of the uncles. + #[serde(rename = "sha3Uncles")] + pub uncles_hash: H256, + /// Nonce. + pub nonce: Option, + /// Authors address. + #[serde(rename = "miner")] + pub author: Address, + /// State root hash. + pub state_root: H256, + /// Transactions root hash. + pub transactions_root: H256, + /// Transactions receipts root hash. + pub receipts_root: H256, + /// Logs bloom. + pub logs_bloom: Bloom, + /// Gas limit. + pub gas_limit: U256, + /// Gas used + pub gas_used: U256, + /// Timestamp. + pub timestamp: U64, + /// Extra data. + pub extra_data: Bytes, + /// Difficulty. + pub difficulty: U256, + /// Total difficulty. + #[serde(skip_serializing_if = "Option::is_none")] + pub total_difficulty: Option, + /// Mix hash. + pub mix_hash: H256, + + /// Base fee per unit of gas, which is added by [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub base_fee_per_gas: Option, + /// Withdrawals root hash, which is added by [EIP-4895](https://eips.ethereum.org/EIPS/eip-4895). + #[serde(skip_serializing_if = "Option::is_none")] + pub withdrawals_root: Option, + /// Parent beacon block root, which is added by [EIP-4788](https://eips.ethereum.org/EIPS/eip-4788). + #[serde(skip_serializing_if = "Option::is_none")] + pub parent_beacon_block_root: Option, +} + +/// Block Transactions +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum BlockTransactions { + /// Only hashes. + Hashes(Vec), + /// Full transactions. + Full(Vec), +} + +// Withdrawal represents a validator withdrawal from the consensus layer. +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Withdrawal { + /// Monotonically increasing identifier issued by consensus layer. + pub index: U64, + /// Index of validator associated with withdrawal. + pub validator_index: U64, + /// Target address for withdrawn ether. + pub address: Address, + /// Value of withdrawal in Gwei. + pub amount: U64, +} + +/// [`BlockOverrides`] is a set of header fields to override. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct BlockOverrides { + /// Fake block number. + // Note: geth uses `number`, erigon uses `blockNumber` + #[serde( + default, + skip_serializing_if = "Option::is_none", + alias = "blockNumber" + )] + pub number: Option, + /// Fake difficulty. + // Note post-merge difficulty should be 0. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub difficulty: Option, + /// Fake block timestamp. + // Note: geth uses `time`, erigon uses `timestamp` + #[serde(default, skip_serializing_if = "Option::is_none", alias = "timestamp")] + pub time: Option, + /// Block gas capacity. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub gas_limit: Option, + /// Block fee recipient. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub coinbase: Option
, + /// Fake PrevRandao value. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub random: Option, + /// Block base fee. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub base_fee: Option, +} diff --git a/client/rpc-v2/types/src/block_id.rs b/client/rpc-v2/types/src/block_id.rs new file mode 100644 index 0000000000..7d77f5cff2 --- /dev/null +++ b/client/rpc-v2/types/src/block_id.rs @@ -0,0 +1,427 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::{fmt, str}; + +use ethereum_types::H256; +use serde::{Deserialize, Serialize}; + +/// A Block identifier. +/// +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum BlockNumberOrTagOrHash { + /// A block number or tag. + Number(BlockNumberOrTag), + /// A block hash and an optional indication if it's canonical. + Hash(BlockHash), +} + +impl Default for BlockNumberOrTagOrHash { + fn default() -> Self { + Self::Number(BlockNumberOrTag::default()) + } +} + +impl From for BlockNumberOrTagOrHash { + fn from(value: BlockNumberOrTag) -> Self { + Self::Number(value) + } +} + +impl From for BlockNumberOrTagOrHash { + fn from(value: u64) -> Self { + Self::Number(BlockNumberOrTag::Number(value)) + } +} + +impl From for BlockNumberOrTagOrHash { + fn from(value: BlockHash) -> Self { + Self::Hash(value) + } +} + +impl serde::Serialize for BlockNumberOrTagOrHash { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::Number(number) => number.serialize(serializer), + Self::Hash(hash) => hash.serialize(serializer), + } + } +} + +impl<'de> serde::Deserialize<'de> for BlockNumberOrTagOrHash { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de; + + struct BlockNumberOrTagOrHashVisitor; + + impl<'de> de::Visitor<'de> for BlockNumberOrTagOrHashVisitor { + type Value = BlockNumberOrTagOrHash; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("Block number or hash parameter that following EIP-1898") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + // There is no way to clearly distinguish between a DATA parameter and a QUANTITY parameter. + // However, since the hex string should be a QUANTITY, we can safely assume that if the len is 66 bytes, it is in fact a hash, + if v.len() == 66 { + let hash = v.parse::().map_err(de::Error::custom)?; + Ok(BlockNumberOrTagOrHash::Hash(hash.into())) + } else { + // quantity hex string or tag + let number = v.parse::().map_err(de::Error::custom)?; + Ok(BlockNumberOrTagOrHash::Number(number)) + } + } + + fn visit_map(self, mut map: A) -> Result + where + A: de::MapAccess<'de>, + { + let mut number = None; + let mut block_hash = None; + let mut require_canonical = None; + + while let Some(key) = map.next_key::()? { + match key.as_str() { + "blockNumber" => { + if number.is_some() || block_hash.is_some() { + return Err(de::Error::duplicate_field("blockNumber")); + } + if require_canonical.is_some() { + return Err(de::Error::custom("Non-valid require_canonical field")); + } + number = Some(map.next_value::()?) + } + "blockHash" => { + if number.is_some() || block_hash.is_some() { + return Err(de::Error::duplicate_field("blockHash")); + } + block_hash = Some(map.next_value::()?); + } + "requireCanonical" => { + if number.is_some() || require_canonical.is_some() { + return Err(de::Error::duplicate_field("requireCanonical")); + } + require_canonical = Some(map.next_value::()?) + } + key => { + return Err(de::Error::unknown_field( + key, + &["blockNumber", "blockHash", "requireCanonical"], + )) + } + } + } + + if let Some(number) = number { + Ok(BlockNumberOrTagOrHash::Number(number)) + } else if let Some(block_hash) = block_hash { + Ok(BlockNumberOrTagOrHash::Hash(BlockHash { + block_hash, + require_canonical, + })) + } else { + Err(de::Error::custom( + "Expected `blockNumber` or `blockHash` with `requireCanonical` optionally", + )) + } + } + } + + deserializer.deserialize_any(BlockNumberOrTagOrHashVisitor) + } +} + +/// A block number or tag ("latest", "earliest", "pending", "finalized", "safe"). +#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, Hash)] +pub enum BlockNumberOrTag { + /// Latest block. + /// + /// The most recent block in the canonical chain observed by the client, this block may be + /// re-organized out of the canonical chain even under healthy/normal condition. + #[default] + Latest, + /// Finalized block accepted as canonical. + /// + /// The most recent crypto-economically secure block, cannot be re-organized outside manual + /// intervention driven by community coordination. + Finalized, + /// Safe head block. + /// + /// The most recent block that is safe from re-organized under honest majority and certain + /// synchronicity assumptions. + /// + /// There is no difference between Ethereum's `safe` and `finalized` in Substrate finality gadget + Safe, + /// Earliest block (genesis). + /// + /// The lowest numbered block the client has available. + Earliest, + /// Pending block (being mined). + /// + /// A sample next block built by the client on top of `latest` and containing the set of + /// transactions usually taken from local txpool. + Pending, + /// Block number. + Number(u64), +} + +impl From for BlockNumberOrTag { + fn from(value: u64) -> Self { + Self::Number(value) + } +} + +impl str::FromStr for BlockNumberOrTag { + type Err = String; + + fn from_str(s: &str) -> Result { + Ok(match s { + "latest" => Self::Latest, + "finalized" => Self::Finalized, + "safe" => Self::Safe, + "earliest" => Self::Earliest, + "pending" => Self::Pending, + _number => { + if let Some(hex_val) = s.strip_prefix("0x") { + let number = u64::from_str_radix(hex_val, 16).map_err(|err| err.to_string())?; + BlockNumberOrTag::Number(number) + } else { + return Err("hex string without 0x prefix".to_string()); + } + } + }) + } +} + +impl serde::Serialize for BlockNumberOrTag { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::Latest => serializer.serialize_str("latest"), + Self::Finalized => serializer.serialize_str("finalized"), + Self::Safe => serializer.serialize_str("safe"), + Self::Earliest => serializer.serialize_str("earliest"), + Self::Pending => serializer.serialize_str("pending"), + Self::Number(num) => serializer.serialize_str(&format!("0x{num:x}")), + } + } +} + +impl<'de> serde::Deserialize<'de> for BlockNumberOrTag { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?.to_lowercase(); + s.parse().map_err(serde::de::Error::custom) + } +} + +/// A block hash which may have a boolean `requireCanonical` field. +/// +/// If it's false, an RPC call should raise if a block matching the hash is not found. +/// If it's true, an RPC call should additionally raise if the block is not in the canonical chain. +/// +/// +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BlockHash { + /// A block hash. + block_hash: H256, + /// The indication if the block hash is canonical. + #[serde(skip_serializing_if = "Option::is_none")] + require_canonical: Option, +} + +impl From for BlockHash { + fn from(value: H256) -> Self { + Self { + block_hash: value, + require_canonical: None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn eip1898_block_number_serde_impl() { + let cases = [ + ( + BlockNumberOrTagOrHash::Number(BlockNumberOrTag::Latest), + serde_json::json!("latest"), + serde_json::json!({ "blockNumber": "latest" }), + ), + ( + BlockNumberOrTagOrHash::Number(BlockNumberOrTag::Finalized), + serde_json::json!("finalized"), + serde_json::json!({ "blockNumber": "finalized" }), + ), + ( + BlockNumberOrTagOrHash::Number(BlockNumberOrTag::Safe), + serde_json::json!("safe"), + serde_json::json!({ "blockNumber": "safe" }), + ), + ( + BlockNumberOrTagOrHash::Number(BlockNumberOrTag::Earliest), + serde_json::json!("earliest"), + serde_json::json!({ "blockNumber": "earliest" }), + ), + ( + BlockNumberOrTagOrHash::Number(BlockNumberOrTag::Pending), + serde_json::json!("pending"), + serde_json::json!({ "blockNumber": "pending" }), + ), + ( + BlockNumberOrTagOrHash::Number(BlockNumberOrTag::Number(0)), + serde_json::json!("0x0"), + serde_json::json!("0x0"), + ), + ( + BlockNumberOrTagOrHash::Number(BlockNumberOrTag::Number(0)), + serde_json::json!("0x0"), + serde_json::json!({ "blockNumber": "0x0" }), + ), + ( + BlockNumberOrTagOrHash::Number(BlockNumberOrTag::Number(255)), + serde_json::json!("0xff"), + serde_json::json!("0xff"), + ), + ( + BlockNumberOrTagOrHash::Number(BlockNumberOrTag::Number(255)), + serde_json::json!("0xff"), + serde_json::json!({ "blockNumber": "0xff" }), + ), + ]; + for (block_number, ser, de) in cases { + assert_eq!(serde_json::to_value(block_number).unwrap(), ser); + assert_eq!( + serde_json::from_value::(de).unwrap(), + block_number + ); + } + } + + #[test] + fn eip1898_block_hash_serde_impl() { + let cases = [ + ( + BlockNumberOrTagOrHash::Hash(BlockHash { + block_hash: + "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + .parse() + .unwrap(), + require_canonical: None, + }), + serde_json::json!({ "blockHash": "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" }), + serde_json::json!( + "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + ), + ), + ( + BlockNumberOrTagOrHash::Hash(BlockHash { + block_hash: + "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + .parse() + .unwrap(), + require_canonical: None, + }), + serde_json::json!({ "blockHash": "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" }), + serde_json::json!({ "blockHash": "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" }), + ), + ( + BlockNumberOrTagOrHash::Hash(BlockHash { + block_hash: + "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + .parse() + .unwrap(), + require_canonical: Some(true), + }), + serde_json::json!({ + "blockHash": "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3", + "requireCanonical": true + }), + serde_json::json!({ + "blockHash": "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3", + "requireCanonical": true + }), + ), + ( + BlockNumberOrTagOrHash::Hash(BlockHash { + block_hash: + "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + .parse() + .unwrap(), + require_canonical: Some(false), + }), + serde_json::json!({ + "blockHash": "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3", + "requireCanonical": false + }), + serde_json::json!({ + "blockHash": "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3", + "requireCanonical": false + }), + ), + ]; + for (block_hash, ser, de) in cases { + assert_eq!(serde_json::to_value(block_hash).unwrap(), ser); + assert_eq!( + serde_json::from_value::(de).unwrap(), + block_hash + ); + } + } + + #[test] + fn invalid_eip1898_block_parameter_deserialization() { + let invalid_cases = [ + serde_json::json!(0), + serde_json::json!({ "blockNumber": "0" }), + serde_json::json!({ "blockNumber": "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" }), + serde_json::json!({ "blockHash": "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa" }), + serde_json::json!({ + "blockNumber": "0x00", + "blockHash": "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3", + "requireCanonical": false + }), + ]; + for case in invalid_cases { + let res = serde_json::from_value::(case); + // println!("{res:?}"); + assert!(res.is_err()); + } + } +} diff --git a/client/rpc-v2/types/src/bytes.rs b/client/rpc-v2/types/src/bytes.rs new file mode 100644 index 0000000000..e850ecf023 --- /dev/null +++ b/client/rpc-v2/types/src/bytes.rs @@ -0,0 +1,206 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::{fmt, ops}; + +#[derive(Clone, Default, Eq, PartialEq, Hash)] +pub struct Bytes(pub Vec); + +impl Bytes { + pub fn new(bytes: Vec) -> Self { + Self(bytes) + } + + pub fn into_vec(self) -> Vec { + self.0 + } +} + +impl fmt::Debug for Bytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::LowerHex::fmt(self, f) + } +} + +impl fmt::Display for Bytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::LowerHex::fmt(self, f) + } +} + +impl fmt::LowerHex for Bytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad(&const_hex::encode_prefixed(self.as_ref())) + } +} + +impl fmt::UpperHex for Bytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad(&const_hex::encode_upper_prefixed(self.as_ref())) + } +} + +impl ops::Deref for Bytes { + type Target = [u8]; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ops::DerefMut for Bytes { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl AsRef<[u8]> for Bytes { + #[inline] + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl From> for Bytes { + fn from(bytes: Vec) -> Bytes { + Bytes(bytes) + } +} + +impl From for Vec { + fn from(bytes: Bytes) -> Vec { + bytes.0 + } +} + +impl serde::Serialize for Bytes { + #[inline] + fn serialize(&self, serializer: S) -> Result { + if serializer.is_human_readable() { + const_hex::serialize(self, serializer) + } else { + serializer.serialize_bytes(self.as_ref()) + } + } +} + +impl<'de> serde::Deserialize<'de> for Bytes { + #[inline] + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de; + + struct BytesVisitor; + + impl<'de> de::Visitor<'de> for BytesVisitor { + type Value = Bytes; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str( + "a variable number of bytes represented as a hex string, an array of u8, or raw bytes", + ) + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + if v.is_empty() { + return Err(de::Error::invalid_value( + de::Unexpected::Str(v), + &"a valid hex string", + )); + } + + const_hex::decode(v) + .map_err(|_| { + de::Error::invalid_value(de::Unexpected::Str(v), &"a valid hex string") + }) + .map(From::from) + } + + fn visit_string(self, value: String) -> Result + where + E: de::Error, + { + self.visit_str(value.as_ref()) + } + + fn visit_bytes(self, v: &[u8]) -> Result { + Ok(Bytes::from(v.to_vec())) + } + + fn visit_byte_buf(self, v: Vec) -> Result { + Ok(Bytes::from(v)) + } + + fn visit_seq>(self, mut seq: A) -> Result { + let mut bytes = Vec::with_capacity(seq.size_hint().unwrap_or(0)); + + while let Some(byte) = seq.next_element()? { + bytes.push(byte); + } + + Ok(Bytes::from(bytes)) + } + } + + if deserializer.is_human_readable() { + deserializer.deserialize_any(BytesVisitor) + } else { + deserializer.deserialize_byte_buf(BytesVisitor) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bytes_serialize() { + let bytes = const_hex::decode("0123456789abcdef").unwrap(); + let bytes = Bytes::new(bytes); + let serialized = serde_json::to_string(&bytes).unwrap(); + assert_eq!(serialized, r#""0x0123456789abcdef""#); + } + + #[test] + fn bytes_deserialize() { + let bytes0: Result = serde_json::from_str(r#""∀∂""#); + let bytes1: Result = serde_json::from_str(r#""""#); + let bytes2: Result = serde_json::from_str(r#""0x123""#); + let bytes3: Result = serde_json::from_str(r#""0xgg""#); + + let bytes4: Bytes = serde_json::from_str(r#""0x""#).unwrap(); + let bytes5: Bytes = serde_json::from_str(r#""0x12""#).unwrap(); + let bytes6: Bytes = serde_json::from_str(r#""0x0123""#).unwrap(); + + assert!(bytes0.is_err()); + assert!(bytes1.is_err()); + assert!(bytes2.is_err()); + assert!(bytes3.is_err()); + assert_eq!(bytes4, Bytes(vec![])); + assert_eq!(bytes5, Bytes(vec![0x12])); + assert_eq!(bytes6, Bytes(vec![0x1, 0x23])); + } +} diff --git a/client/rpc-v2/types/src/fee.rs b/client/rpc-v2/types/src/fee.rs new file mode 100644 index 0000000000..ba65d75e81 --- /dev/null +++ b/client/rpc-v2/types/src/fee.rs @@ -0,0 +1,45 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use ethereum_types::U256; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FeeHistoryResult { + /// Lowest number block of the returned range. + pub oldest_block: U256, + + /// An array 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. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub base_fee_per_gas: Vec, + + /// An array of block gas used ratios. + /// These are calculated as the ratio of `gasUsed` and `gasLimit`. + pub gas_used_ratio: Vec, + + /// A two-dimensional array of effective priority fees per gas at the requested block percentiles. + /// + /// A given percentile sample of effective priority fees per gas from a single block in + /// ascending order, weighted by gas used. Zeroes are returned if the block is empty. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub reward: Vec>, +} diff --git a/client/rpc-v2/types/src/filter/block_option.rs b/client/rpc-v2/types/src/filter/block_option.rs new file mode 100644 index 0000000000..a0e50b8446 --- /dev/null +++ b/client/rpc-v2/types/src/filter/block_option.rs @@ -0,0 +1,111 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::ops::{RangeFrom, RangeTo}; + +use ethereum_types::H256; + +use crate::block_id::BlockNumberOrTag; + +/// Represents the target range of blocks for the filter. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum FilterBlockOption { + /// A range of blocks with optional from and to blocks. + /// + /// Note: ranges are considered to be __inclusive__. + BlockNumberRange { + /// The block number or tag this filter should start at. + from_block: Option, + /// The block number or tag this filter should end at. + to_block: Option, + }, + /// The hash of the block if the filter only targets a single block. + /// + /// See [EIP-234](https://eips.ethereum.org/EIPS/eip-234) for more details. + BlockHashAt { block_hash: H256 }, +} + +impl Default for FilterBlockOption { + fn default() -> Self { + Self::BlockNumberRange { + from_block: None, + to_block: None, + } + } +} + +impl FilterBlockOption { + /// Sets the block number this range filter should start at. + pub const fn from_block(self, block: BlockNumberOrTag) -> Self { + let to_block = if let Self::BlockNumberRange { to_block, .. } = self { + to_block + } else { + None + }; + Self::BlockNumberRange { + from_block: Some(block), + to_block, + } + } + + /// Sets the block number this range filter should end at. + pub const fn to_block(self, block: BlockNumberOrTag) -> Self { + let from_block = if let Self::BlockNumberRange { from_block, .. } = self { + from_block + } else { + None + }; + Self::BlockNumberRange { + from_block, + to_block: Some(block), + } + } + + /// Pins the block hash this filter should target. + pub const fn block_hash(block_hash: H256) -> Self { + Self::BlockHashAt { block_hash } + } +} + +impl> From> for FilterBlockOption { + fn from(value: RangeFrom) -> Self { + let from_block = Some(value.start.into()); + let to_block = Some(BlockNumberOrTag::Latest); + Self::BlockNumberRange { + from_block, + to_block, + } + } +} + +impl> From> for FilterBlockOption { + fn from(value: RangeTo) -> Self { + let from_block = Some(BlockNumberOrTag::Earliest); + let to_block = Some(value.end.into()); + Self::BlockNumberRange { + from_block, + to_block, + } + } +} + +impl From for FilterBlockOption { + fn from(value: H256) -> Self { + Self::BlockHashAt { block_hash: value } + } +} diff --git a/client/rpc-v2/types/src/filter/mod.rs b/client/rpc-v2/types/src/filter/mod.rs new file mode 100644 index 0000000000..0526877e4b --- /dev/null +++ b/client/rpc-v2/types/src/filter/mod.rs @@ -0,0 +1,439 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +mod block_option; +mod utility; + +use std::fmt; + +use ethereum_types::{Address, H256}; +use serde::{ + de::{self, MapAccess, Visitor}, + ser::SerializeStruct, + Deserialize, +}; + +pub use self::{ + block_option::FilterBlockOption, + utility::{FilterSet, ValueOrArray}, +}; +use crate::{block_id::BlockNumberOrTag, log::Log}; + +/// The maximum number of topics supported in [`Filter`]. +pub const MAX_TOPICS: usize = 4; + +pub type AddressFilter = FilterSet
; +pub type TopicFilter = FilterSet; + +/// Filter parameters of `eth_newFilter` and `eth_getLogs` RPC. +#[derive(Clone, Debug, Eq, PartialEq, Default)] +pub struct Filter { + /// Filter block options, specifying on which blocks the filter should match. + pub block_option: FilterBlockOption, + /// Address filter. + pub address: Option, + /// Topics filter. + pub topics: Option<[TopicFilter; MAX_TOPICS]>, +} + +impl Filter { + /// Creates a new, empty filter. + pub fn new() -> Self { + Self::default() + } + + /// Sets the block number this range filter should start at. + #[allow(clippy::wrong_self_convention)] + pub fn from_block>(mut self, block: T) -> Self { + self.block_option = self.block_option.from_block(block.into()); + self + } + + /// Sets the block number this range filter should end at. + #[allow(clippy::wrong_self_convention)] + pub fn to_block>(mut self, block: T) -> Self { + self.block_option = self.block_option.to_block(block.into()); + self + } + + /// Pins the block hash this filter should target. + pub fn at_block_hash>(mut self, hash: T) -> Self { + self.block_option = FilterBlockOption::block_hash(hash.into()); + self + } + + /// Sets the address filter. + pub fn address>>(mut self, address: T) -> Self { + self.address = Some(address.into().into()); + self + } + + /// Sets event_signature(topic0) (the event name for non-anonymous events). + pub fn event_signature>(self, topic: T) -> Self { + self.topic(0, topic) + } + + /// Sets the 1st indexed topic. + pub fn topic1>(self, topic: T) -> Self { + self.topic(1, topic) + } + + /// Sets the 2nd indexed topic. + pub fn topic2>(self, topic: T) -> Self { + self.topic(2, topic) + } + + /// Sets the 3rd indexed topic. + pub fn topic3>(self, topic: T) -> Self { + self.topic(3, topic) + } + + fn topic>(mut self, index: usize, topic: T) -> Self { + match &mut self.topics { + Some(topics) => { + topics[index] = topic.into(); + } + None => { + let mut topics: [TopicFilter; MAX_TOPICS] = Default::default(); + topics[index] = topic.into(); + self.topics = Some(topics); + } + } + self + } +} + +type RawAddressFilter = ValueOrArray
; +type RawTopicsFilter = Vec>>; + +impl serde::Serialize for Filter { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut s = match &self.block_option { + FilterBlockOption::BlockNumberRange { + from_block, + to_block, + } => { + let mut s = serializer.serialize_struct("Filter", 2 + 1 + 1)?; + s.serialize_field("fromBlock", from_block)?; + s.serialize_field("toBlock", to_block)?; + s + } + FilterBlockOption::BlockHashAt { block_hash } => { + let mut s = serializer.serialize_struct("Filter", 1 + 1 + 1)?; + s.serialize_field("blockHash", block_hash)?; + s + } + }; + + match &self.address { + Some(address) => s.serialize_field("address", &address.to_value_or_array())?, + None => s.serialize_field("address", &Option::::None)?, + } + + match &self.topics { + Some(topics) => { + let mut filtered_topics = Vec::new(); + + let mut filtered_topics_len = 0; + for (idx, topic) in topics.iter().enumerate() { + if !topic.is_empty() { + filtered_topics_len = idx + 1; + } + filtered_topics.push(topic.to_value_or_array()); + } + filtered_topics.truncate(filtered_topics_len); + + s.serialize_field("topics", &filtered_topics)?; + } + None => s.serialize_field("topics", &Option::::None)?, + } + + s.end() + } +} + +impl<'de> serde::Deserialize<'de> for Filter { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct FilterVisitor; + + impl<'de> Visitor<'de> for FilterVisitor { + type Value = Filter; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("Filter object") + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let mut from_block: Option> = None; + let mut to_block: Option> = None; + let mut block_hash: Option> = None; + let mut address: Option> = None; + let mut topics: Option> = None; + + while let Some(key) = map.next_key::()? { + match key.as_str() { + "fromBlock" => { + if from_block.is_some() { + return Err(de::Error::duplicate_field("fromBlock")); + } + if block_hash.is_some() { + return Err(de::Error::custom( + "fromBlock not allowed with blockHash", + )); + } + from_block = Some(map.next_value()?) + } + "toBlock" => { + if to_block.is_some() { + return Err(de::Error::duplicate_field("toBlock")); + } + if block_hash.is_some() { + return Err(de::Error::custom( + "toBlock not allowed with blockHash", + )); + } + to_block = Some(map.next_value()?) + } + "blockHash" => { + if block_hash.is_some() { + return Err(de::Error::duplicate_field("blockHash")); + } + if from_block.is_some() || to_block.is_some() { + return Err(de::Error::custom( + "fromBlock,toBlock not allowed with blockHash", + )); + } + block_hash = Some(map.next_value()?) + } + "address" => { + if address.is_some() { + return Err(de::Error::duplicate_field("address")); + } + address = Some(map.next_value()?) + } + "topics" => { + if topics.is_some() { + return Err(de::Error::duplicate_field("topics")); + } + topics = Some(map.next_value()?) + } + key => { + return Err(de::Error::unknown_field( + key, + &["fromBlock", "toBlock", "blockHash", "address", "topics"], + )) + } + } + } + + let from_block = from_block.unwrap_or_default(); + let to_block = to_block.unwrap_or_default(); + let block_hash = block_hash.unwrap_or_default(); + + let block_option = if let Some(block_hash) = block_hash { + FilterBlockOption::BlockHashAt { block_hash } + } else { + FilterBlockOption::BlockNumberRange { + from_block, + to_block, + } + }; + + let address = address.flatten().map(FilterSet::from); + + let topics = match topics.flatten() { + Some(topics_vec) => { + if topics_vec.len() > MAX_TOPICS { + return Err(de::Error::custom("exceeded maximum topics")); + } + + let mut topics: [TopicFilter; MAX_TOPICS] = Default::default(); + for (idx, topic) in topics_vec.into_iter().enumerate() { + topics[idx] = topic.map(FilterSet::from).unwrap_or_default(); + } + Some(topics) + } + None => None, + }; + + Ok(Filter { + block_option, + address, + topics, + }) + } + } + + deserializer.deserialize_any(FilterVisitor) + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Default)] +pub enum FilterChanges { + /// Empty result. + #[default] + Empty, + /// New logs. + Logs(Vec), + /// New hashes (block or transactions). + Hashes(Vec), +} + +impl serde::Serialize for FilterChanges { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::Empty => (&[] as &[()]).serialize(serializer), + Self::Logs(logs) => logs.serialize(serializer), + Self::Hashes(hashes) => hashes.serialize(serializer), + } + } +} + +impl<'de> serde::Deserialize<'de> for FilterChanges { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum Changes { + Logs(Vec), + Hashes(Vec), + } + + let changes = Changes::deserialize(deserializer)?; + Ok(match changes { + Changes::Logs(logs) => { + if logs.is_empty() { + Self::Empty + } else { + Self::Logs(logs) + } + } + Changes::Hashes(hashes) => { + if hashes.is_empty() { + Self::Empty + } else { + Self::Hashes(hashes) + } + } + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn h256(s: &str) -> H256 { + s.parse().unwrap() + } + + fn address(s: &str) -> Address { + s.parse().unwrap() + } + + #[test] + fn filter_serde_impl() { + let valid_cases = [ + ( + r#"{ + "fromBlock":"earliest", + "toBlock":null, + "address":null, + "topics":null + }"#, + Filter::default().from_block(BlockNumberOrTag::Earliest), + ), + ( + r#"{ + "fromBlock":"earliest", + "toBlock":null, + "address":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "topics":null + }"#, + Filter::default() + .from_block(BlockNumberOrTag::Earliest) + .address(address("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")), + ), + ( + r#"{ + "blockHash":"0x1111111111111111111111111111111111111111111111111111111111111111", + "address":[ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "topics":null + }"#, + Filter::default() + .at_block_hash(h256( + "0x1111111111111111111111111111111111111111111111111111111111111111", + )) + .address(vec![ + address("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + address("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + ]), + ), + ]; + + for (raw, typed) in valid_cases { + let deserialized = serde_json::from_str::(raw).unwrap(); + assert_eq!(deserialized, typed); + + let serialized = serde_json::to_string(&typed).unwrap(); + assert_eq!(serialized, raw.split_whitespace().collect::()); + } + } + + #[test] + fn filter_changes_serde_impl() { + let cases = [ + (r#"[]"#, FilterChanges::Empty), + ( + r#"[ + "0x1111111111111111111111111111111111111111111111111111111111111111", + "0x2222222222222222222222222222222222222222222222222222222222222222" + ]"#, + FilterChanges::Hashes(vec![ + h256("0x1111111111111111111111111111111111111111111111111111111111111111"), + h256("0x2222222222222222222222222222222222222222222222222222222222222222"), + ]), + ), + ]; + + for (raw, typed) in cases { + let deserialized = serde_json::from_str::(raw).unwrap(); + assert_eq!(deserialized, typed); + + let serialized = serde_json::to_string(&typed).unwrap(); + assert_eq!(serialized, raw.split_whitespace().collect::()); + } + } +} diff --git a/client/rpc-v2/types/src/filter/utility.rs b/client/rpc-v2/types/src/filter/utility.rs new file mode 100644 index 0000000000..a46e1177cc --- /dev/null +++ b/client/rpc-v2/types/src/filter/utility.rs @@ -0,0 +1,110 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::collections::BTreeSet; + +use serde::{Deserialize, Serialize}; + +/// Union type for representing a single value or a list of values inside a filter. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ValueOrArray { + /// A single value. + Value(T), + /// A list of values. + Array(Vec), +} + +impl From for ValueOrArray { + fn from(value: T) -> Self { + Self::Value(value) + } +} + +impl From> for ValueOrArray { + fn from(array: Vec) -> Self { + Self::Array(array) + } +} + +/// FilterSet is a set of values that will be used to filter addresses and topics. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct FilterSet(BTreeSet); + +impl From for FilterSet { + fn from(value: T) -> Self { + Self(BTreeSet::from([value])) + } +} + +impl From> for FilterSet { + fn from(value: Vec) -> Self { + Self(value.into_iter().collect()) + } +} + +impl From> for FilterSet { + fn from(value: ValueOrArray) -> Self { + match value { + ValueOrArray::Value(value) => value.into(), + ValueOrArray::Array(array) => array.into(), + } + } +} + +impl From>> for FilterSet { + fn from(src: ValueOrArray>) -> Self { + match src { + ValueOrArray::Value(None) => Self(BTreeSet::new()), + ValueOrArray::Value(Some(value)) => value.into(), + ValueOrArray::Array(array) => { + // If the array contains at least one `null` (i.e. None), as it's considered + // a "wildcard" value, the whole filter should be treated as matching everything, + // thus is empty. + if array.contains(&None) { + Self(BTreeSet::new()) + } else { + // Otherwise, we flatten the array, knowing there are no `None` values + array.into_iter().flatten().collect::>().into() + } + } + } + } +} + +impl FilterSet { + /// Returns whether the filter is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Returns a [`ValueOrArray`] inside an Option: + /// - If the filter is empty, it returns `None` + /// - If the filter has only 1 value, it returns the single value + /// - Otherwise it returns an array of values + pub fn to_value_or_array(&self) -> Option> { + let values_len = self.0.len(); + match values_len { + 0 => None, + 1 => Some(ValueOrArray::Value( + self.0.iter().next().cloned().expect("at least one item"), + )), + _ => Some(ValueOrArray::Array(self.0.iter().cloned().collect())), + } + } +} diff --git a/client/rpc-v2/types/src/index.rs b/client/rpc-v2/types/src/index.rs new file mode 100644 index 0000000000..1baa4b3f7e --- /dev/null +++ b/client/rpc-v2/types/src/index.rs @@ -0,0 +1,124 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::fmt; + +#[derive(Copy, Clone, PartialEq, Eq, Default, Hash)] +pub struct Index(usize); + +impl fmt::Debug for Index { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::LowerHex::fmt(&self.0, f) + } +} + +impl fmt::Display for Index { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::LowerHex::fmt(&self.0, f) + } +} + +impl From for usize { + fn from(idx: Index) -> Self { + idx.0 + } +} + +impl From for Index { + fn from(value: usize) -> Self { + Self(value) + } +} + +impl serde::Serialize for Index { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&format!("0x{:x}", self.0)) + } +} + +impl<'de> serde::Deserialize<'de> for Index { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de; + + struct IndexVisitor; + + impl<'de> de::Visitor<'de> for IndexVisitor { + type Value = Index; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("hex-encoded or decimal index") + } + + fn visit_u64(self, value: u64) -> Result + where + E: de::Error, + { + Ok(Index(value as usize)) + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + if let Some(val) = value.strip_prefix("0x") { + usize::from_str_radix(val, 16) + .map(Index) + .map_err(de::Error::custom) + } else { + value.parse::().map(Index).map_err(de::Error::custom) + } + } + + fn visit_string(self, value: String) -> Result + where + E: de::Error, + { + self.visit_str(value.as_ref()) + } + } + + deserializer.deserialize_any(IndexVisitor) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn index_serialize() { + let indexes = vec![Index(10), Index(10), Index(u32::MAX as usize)]; + let serialized = serde_json::to_string(&indexes).unwrap(); + let expected = r#"["0xa","0xa","0xffffffff"]"#; + assert_eq!(serialized, expected); + } + + #[test] + fn index_deserialize() { + let s = r#"["0xa", "10", "0xffffffff"]"#; + let deserialized: Vec = serde_json::from_str(s).unwrap(); + let expected = vec![Index(10), Index(10), Index(u32::MAX as usize)]; + assert_eq!(deserialized, expected); + } +} diff --git a/client/rpc-v2/types/src/lib.rs b/client/rpc-v2/types/src/lib.rs new file mode 100644 index 0000000000..c3e2b36b4d --- /dev/null +++ b/client/rpc-v2/types/src/lib.rs @@ -0,0 +1,42 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! RPC types + +#![warn(unused_crate_dependencies)] + +pub mod access_list; +pub mod block; +pub mod block_id; +pub mod bytes; +pub mod fee; +pub mod filter; +pub mod index; +pub mod log; +pub mod proof; +pub mod pubsub; +pub mod state; +pub mod sync; +pub mod transaction; +pub mod txpool; + +pub use self::{ + access_list::*, block::*, block_id::*, bytes::Bytes, fee::*, filter::*, index::Index, log::Log, + proof::*, pubsub::*, state::*, sync::*, transaction::*, txpool::*, +}; +pub use ethereum_types::{Address, Bloom, H256, U128, U256, U64}; diff --git a/client/rpc-v2/types/src/log.rs b/client/rpc-v2/types/src/log.rs new file mode 100644 index 0000000000..e28b0e1ae2 --- /dev/null +++ b/client/rpc-v2/types/src/log.rs @@ -0,0 +1,52 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use ethereum_types::{Address, H256, U256}; +use serde::{Deserialize, Serialize}; + +use crate::bytes::Bytes; + +/// Log represents a contract log event, which is emitted by a transaction. +/// These events are generated by the LOG opcode and stored/indexed by the node. +#[derive(Clone, Debug, Eq, PartialEq, Default, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Log { + // Consensus fields: + /// Address of the contract that generated the event. + pub address: Address, + /// List of topics provided by the contract. + pub topics: Vec, + /// Additional data fields of the log. + pub data: Bytes, + + // Derived fields: + /// Hash of block in which the transaction was included. + pub block_hash: Option, + /// Number of the block in which the transaction was included. + pub block_number: Option, + /// Transaction hash. + pub transaction_hash: Option, + /// Index of the transaction in the block. + pub transaction_index: Option, + /// Index of the log in the block. + pub log_index: Option, + + /// Whether this log was removed due to a chain reorganisation. + #[serde(default)] + pub removed: bool, +} diff --git a/client/rpc-v2/types/src/proof.rs b/client/rpc-v2/types/src/proof.rs new file mode 100644 index 0000000000..75a9839ba6 --- /dev/null +++ b/client/rpc-v2/types/src/proof.rs @@ -0,0 +1,58 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use ethereum_types::{Address, H256, U256, U64}; +use serde::{Deserialize, Serialize}; + +use crate::bytes::Bytes; + +/// The response type of `eth_getProof`. +/// +/// See [EIP-1186](https://eips.ethereum.org/EIPS/eip-1186) for more details. +#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountProof { + /// The address of the account. + pub address: Address, + /// Array of rlp-serialized MerkleTree-Nodes, starting with the stateRoot-Node, following the + /// path of the SHA3 (address) as key. + pub account_proof: Vec, + /// The balance of the account. + pub balance: U256, + /// Code hash of the account. + pub code_hash: H256, + /// The nonce of the account. + pub nonce: U64, + /// The hash of storage root. + pub storage_hash: H256, + /// Array of storage-entries as requested. + pub storage_proof: Vec, +} + +/// Data structure with proof for one single storage-entry +#[derive(Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StorageProof { + /// Storage key. + pub key: H256, + /// Storage value. + pub value: U256, + /// Array of rlp-serialized MerkleTree-Nodes, starting with the storageHash-Node, following the + /// path of the SHA3 (key) as path. + pub proof: Vec, +} diff --git a/client/rpc-v2/types/src/pubsub.rs b/client/rpc-v2/types/src/pubsub.rs new file mode 100644 index 0000000000..2a34f231ef --- /dev/null +++ b/client/rpc-v2/types/src/pubsub.rs @@ -0,0 +1,115 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use ethereum_types::H256; +use serde::{de, Deserialize, Serialize}; + +use crate::{block::Header, filter::Filter, log::Log, transaction::Transaction}; + +/// Subscription kind. +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum PubSubKind { + /// New block headers subscription. + NewHeads, + /// Logs subscription. + Logs, + /// New Pending Transactions subscription. + NewPendingTransactions, +} + +/// Any additional parameters for a subscription. +#[derive(Clone, Debug, Eq, PartialEq, Default)] +pub enum PubSubParams { + /// No parameters passed. + #[default] + None, + /// Log parameters. + Logs(Box), + /// Boolean parameter for new pending transactions. + Bool(bool), +} + +impl serde::Serialize for PubSubParams { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::None => serializer.serialize_none(), + Self::Logs(logs) => logs.serialize(serializer), + Self::Bool(full) => full.serialize(serializer), + } + } +} + +impl<'de> serde::Deserialize<'de> for PubSubParams { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let v = serde_json::Value::deserialize(deserializer)?; + + if v.is_null() { + return Ok(Self::None); + } + + if let Some(val) = v.as_bool() { + return Ok(Self::Bool(val)); + } + + serde_json::from_value(v) + .map(|f| Self::Logs(Box::new(f))) + .map_err(|e| de::Error::custom(format!("Invalid Pub-Sub parameters: {e}"))) + } +} + +/// Subscription result. +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum PubSubResult { + /// New block header. + Header(Box
), + /// Log. + Log(Box), + /// Transaction hash. + TransactionHash(H256), + /// Transaction. + FullTransaction(Box), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pubsub_params_serde_impl() { + let cases = [ + ("null", PubSubParams::None), + ("true", PubSubParams::Bool(true)), + ("false", PubSubParams::Bool(false)), + ]; + for (raw, typed) in cases { + let deserialized = serde_json::from_str::(raw).unwrap(); + assert_eq!(deserialized, typed); + + let serialized = serde_json::to_string(&typed).unwrap(); + assert_eq!(serialized, raw); + } + } +} diff --git a/client/rpc-v2/types/src/state.rs b/client/rpc-v2/types/src/state.rs new file mode 100644 index 0000000000..530b0f4137 --- /dev/null +++ b/client/rpc-v2/types/src/state.rs @@ -0,0 +1,53 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::collections::HashMap; + +use ethereum_types::{Address, H256, U256, U64}; +use serde::{Deserialize, Serialize}; + +use crate::bytes::Bytes; + +pub type StateOverrides = HashMap; + +/// Indicates the overriding fields of account during the execution of a message call. +/// +/// Note, state and stateDiff can't be specified at the same time. +/// If state is set, message execution will only use the data in the given state. +/// Otherwise, if statDiff is set, all diff will be applied first and then execute the call message. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct AccountOverride { + /// Fake balance to set for the account before executing the call. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub balance: Option, + /// Fake nonce to set for the account before executing the call. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub nonce: Option, + /// Fake EVM bytecode to inject into the account before executing the call. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub code: Option, + /// Fake key-value mapping to override all slots in the account storage before + /// executing the call. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub state: Option>, + /// Fake key-value mapping to override individual slots in the account storage before + /// executing the call. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub state_diff: Option>, +} diff --git a/client/rpc-v2/types/src/sync.rs b/client/rpc-v2/types/src/sync.rs new file mode 100644 index 0000000000..794a2e7c5c --- /dev/null +++ b/client/rpc-v2/types/src/sync.rs @@ -0,0 +1,108 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use ethereum_types::U64; +use serde::{de, Deserialize, Serialize}; + +/// The syncing status of client. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum SyncingStatus { + /// Progress when syncing. + IsSyncing(SyncingProgress), + /// Not syncing. + NotSyncing, +} + +impl serde::Serialize for SyncingStatus { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::IsSyncing(progress) => progress.serialize(serializer), + Self::NotSyncing => serializer.serialize_bool(false), + } + } +} + +impl<'de> serde::Deserialize<'de> for SyncingStatus { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum Syncing { + IsSyncing(SyncingProgress), + NotSyncing(bool), + } + + match Syncing::deserialize(deserializer)? { + Syncing::IsSyncing(sync) => Ok(Self::IsSyncing(sync)), + Syncing::NotSyncing(false) => Ok(Self::NotSyncing), + Syncing::NotSyncing(true) => Err(de::Error::custom( + "eth_syncing should always return false if not syncing.", + )), + } + } +} + +/// The syncing progress. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SyncingProgress { + /// Block number this node started to synchronize from. + pub starting_block: U64, + /// Block number this node is currently importing. + pub current_block: U64, + /// Block number of the highest block header this node has received from peers. + pub highest_block: U64, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn syncing_status_serde_impl() { + let valid_cases = [ + ( + r#"{"startingBlock":"0x64","currentBlock":"0xc8","highestBlock":"0x12c"}"#, + SyncingStatus::IsSyncing(SyncingProgress { + starting_block: 100.into(), + current_block: 200.into(), + highest_block: 300.into(), + }), + ), + ("false", SyncingStatus::NotSyncing), + ]; + for (raw, typed) in valid_cases { + let deserialized = serde_json::from_str::(raw).unwrap(); + assert_eq!(deserialized, typed); + + let serialized = serde_json::to_string(&typed).unwrap(); + assert_eq!(serialized, raw); + } + + let invalid_cases = ["true"]; + for raw in invalid_cases { + let status: Result = serde_json::from_str(raw); + assert!(status.is_err()); + } + } +} diff --git a/client/rpc-v2/types/src/transaction/mod.rs b/client/rpc-v2/types/src/transaction/mod.rs new file mode 100644 index 0000000000..0677c8d6a4 --- /dev/null +++ b/client/rpc-v2/types/src/transaction/mod.rs @@ -0,0 +1,133 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +mod receipt; +mod request; +mod signature; + +use ethereum_types::{Address, H256, U256, U64}; +use serde::{Deserialize, Serialize}; + +pub use self::{receipt::*, request::*, signature::*}; +use crate::{access_list::AccessList, bytes::Bytes}; + +/// [EIP-2718](https://eips.ethereum.org/EIPS/eip-2718) transaction type. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Default)] +#[repr(u8)] +pub enum TxType { + /// Legacy transaction + #[default] + Legacy = 0u8, + /// [EIP-2930](https://eips.ethereum.org/EIPS/eip-2930) transaction + EIP2930 = 1u8, + /// [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) transaction + EIP1559 = 2u8, +} + +impl TryFrom for TxType { + type Error = &'static str; + + fn try_from(value: u8) -> Result { + match value { + 0u8 => Ok(Self::Legacy), + 1u8 => Ok(Self::EIP2930), + 2u8 => Ok(Self::EIP1559), + _ => Err("Unsupported transaction type"), + } + } +} + +impl serde::Serialize for TxType { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::Legacy => serializer.serialize_str("0x0"), + Self::EIP2930 => serializer.serialize_str("0x1"), + Self::EIP1559 => serializer.serialize_str("0x2"), + } + } +} + +impl<'de> serde::Deserialize<'de> for TxType { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match s.as_str() { + "0x0" => Ok(Self::Legacy), + "0x1" => Ok(Self::EIP2930), + "0x2" => Ok(Self::EIP1559), + _ => Err(serde::de::Error::custom("Unsupported transaction type")), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize)] +pub struct Transaction { + /// [EIP-2718](https://eips.ethereum.org/EIPS/eip-27 gg ) transaction type + #[serde(rename = "type")] + pub tx_type: TxType, + + /// Transaction hash + pub hash: H256, + /// Nonce + pub nonce: U64, + /// Block hash + #[serde(default, skip_serializing_if = "Option::is_none")] + pub block_hash: Option, + /// Block number + #[serde(default, skip_serializing_if = "Option::is_none")] + pub block_number: Option, + /// Transaction index + #[serde(default, skip_serializing_if = "Option::is_none")] + pub transaction_index: Option, + /// Sender + pub from: Address, + /// Recipient + pub to: Option
, + /// Transferred value + pub value: U256, + /// Input data + pub input: Bytes, + + /// Gas limit + pub gas: U64, + /// Gas price + #[serde(default, skip_serializing_if = "Option::is_none")] + pub gas_price: Option, + /// Max BaseFeePerGas the user is willing to pay + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_fee_per_gas: Option, + /// The miner's tip + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_priority_fee_per_gas: Option, + + /// Chain ID that this transaction is valid on + #[serde(default, skip_serializing_if = "Option::is_none")] + pub chain_id: Option, + /// All _flattened_ fields of the transaction signature + #[serde(flatten)] + pub signature: TransactionSignature, + + /// EIP-2930 access list + #[serde(default, skip_serializing_if = "Option::is_none")] + pub access_list: Option, +} diff --git a/client/rpc-v2/types/src/transaction/receipt.rs b/client/rpc-v2/types/src/transaction/receipt.rs new file mode 100644 index 0000000000..bdbd7771bb --- /dev/null +++ b/client/rpc-v2/types/src/transaction/receipt.rs @@ -0,0 +1,67 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use ethereum_types::{Address, Bloom, H256, U256, U64}; +use serde::{Deserialize, Serialize}; + +use crate::{log::Log, transaction::TxType}; + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionReceipt { + /// Hash of the block this transaction was included within. + pub block_hash: Option, + /// Number of the block this transaction was included within. + pub block_number: Option, + + /// Transaction hash. + pub transaction_hash: H256, + /// Transaction index within the block. + pub transaction_index: U64, + #[serde(rename = "type")] + pub tx_type: TxType, + /// Gas used by this transaction. + pub gas_used: U64, + + /// Address of the sender + pub from: Address, + /// Address of the receiver, or None when it's a contract creation transaction. + pub to: Option
, + /// Contract address created, or None if not a deployment. + pub contract_address: Option
, + + /// The price paid post-execution by the transaction. + /// Pre-eip1559 : gas price. + /// Post-eip1559: base fee + priority fee. + pub effective_gas_price: U256, + + /// Transaction execution status. + pub status: U64, + + /// Cumulative gas used. + pub cumulative_gas_used: U64, + + /// Log send from contracts. + pub logs: Vec, + /// [`Log`]'s bloom filter + pub logs_bloom: Bloom, + + /// The post-transaction state root (pre Byzantium). + #[serde(rename = "root", skip_serializing_if = "Option::is_none")] + pub state_root: Option, +} diff --git a/client/rpc-v2/types/src/transaction/request.rs b/client/rpc-v2/types/src/transaction/request.rs new file mode 100644 index 0000000000..e79d275608 --- /dev/null +++ b/client/rpc-v2/types/src/transaction/request.rs @@ -0,0 +1,306 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use ethereum_types::{Address, U128, U256, U64}; +use serde::{ + de, + ser::{self, SerializeStruct}, + Deserialize, Serialize, +}; + +use crate::{access_list::AccessList, bytes::Bytes, transaction::TxType}; + +/// Transaction request from the RPC. +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionRequest { + /// [EIP-2718](https://eips.ethereum.org/EIPS/eip-2718) transaction type + #[serde(default, rename = "type", skip_serializing_if = "Option::is_none")] + pub tx_type: Option, + + /// Sender + #[serde(default, skip_serializing_if = "Option::is_none")] + pub from: Option
, + /// Recipient + #[serde(default, skip_serializing_if = "Option::is_none")] + pub to: Option
, + + /// Value of transaction in wei + #[serde(default, skip_serializing_if = "Option::is_none")] + pub value: Option, + /// Transaction's nonce + #[serde(default, skip_serializing_if = "Option::is_none")] + pub nonce: Option, + + /// Gas limit + #[serde(default, skip_serializing_if = "Option::is_none")] + pub gas: Option, + /// The gas price willing to be paid by the sender in wei + #[serde(default, skip_serializing_if = "Option::is_none")] + pub gas_price: Option, + /// The maximum total fee per gas the sender is willing to pay (includes the network / base fee and miner / priority fee) in wei + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_fee_per_gas: Option, + /// Maximum fee per gas the sender is willing to pay to miners in wei + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_priority_fee_per_gas: Option, + + /// Additional data + #[serde(default, flatten)] + pub input: TransactionInput, + + /// Chain ID that this transaction is valid on + #[serde(default, skip_serializing_if = "Option::is_none")] + pub chain_id: Option, + + /// EIP-2930 access list + #[serde(default, skip_serializing_if = "Option::is_none")] + pub access_list: Option, +} + +impl TransactionRequest { + /// Sets the transactions type for the transactions. + #[inline] + pub const fn tx_type(mut self, tx_type: TxType) -> Self { + self.tx_type = Some(tx_type); + self + } + + /// Sets the `from` field in the call to the provided address + #[inline] + pub const fn from(mut self, from: Address) -> Self { + self.from = Some(from); + self + } + + /// Sets the recipient address for the transaction. + #[inline] + pub const fn to(mut self, to: Address) -> Self { + self.to = Some(to); + self + } + + /// Sets the nonce for the transaction. + #[inline] + pub const fn nonce(mut self, nonce: U64) -> Self { + self.nonce = Some(nonce); + self + } + + /// Sets the value (amount) for the transaction. + #[inline] + pub const fn value(mut self, value: U256) -> Self { + self.value = Some(value); + self + } + + /// Sets the gas limit for the transaction. + #[inline] + pub const fn gas_limit(mut self, gas_limit: U128) -> Self { + self.gas = Some(gas_limit); + self + } + + /// Sets the maximum fee per gas for the transaction. + #[inline] + pub const fn max_fee_per_gas(mut self, max_fee_per_gas: U128) -> Self { + self.max_fee_per_gas = Some(max_fee_per_gas); + self + } + + /// Sets the maximum priority fee per gas for the transaction. + #[inline] + pub const fn max_priority_fee_per_gas(mut self, max_priority_fee_per_gas: U128) -> Self { + self.max_priority_fee_per_gas = Some(max_priority_fee_per_gas); + self + } + + /// Sets the input data for the transaction. + pub fn input(mut self, input: TransactionInput) -> Self { + self.input = input; + self + } + + /// Sets the access list for the transaction. + pub fn access_list(mut self, access_list: AccessList) -> Self { + self.access_list = Some(access_list); + self + } + + /// Returns the configured fee cap, if any. + /// + /// The returns `gas_price` (legacy) if set or `max_fee_per_gas` (EIP1559) + #[inline] + pub fn fee_cap(&self) -> Option { + self.gas_price.or(self.max_fee_per_gas) + } +} + +/// Additional data of the transaction. +/// +/// We accept (older) "data" and (newer) "input" for backwards-compatibility reasons. +/// If both fields are set, it is expected that they contain the same value, otherwise an error is returned. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct TransactionInput { + /// Transaction data + pub input: Option, + /// Transaction data + /// + /// This is the same as `input` but is used for backwards compatibility: + pub data: Option, +} + +impl TransactionInput { + /// Return the additional data of the transaction. + pub fn into_bytes(self) -> Option { + match (self.input, self.data) { + (Some(input), _) => Some(input), + (None, Some(data)) => Some(data), + (None, None) => None, + } + } +} + +impl serde::Serialize for TransactionInput { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match (&self.input, &self.data) { + (Some(input), Some(data)) => { + if input == data { + let mut s = + serde::Serializer::serialize_struct(serializer, "TransactionInput", 2)?; + s.serialize_field("input", input)?; + s.serialize_field("data", data)?; + s.end() + } else { + Err(ser::Error::custom("Ambiguous value for `input` and `data`")) + } + } + (Some(input), None) => { + let mut s = serde::Serializer::serialize_struct(serializer, "TransactionInput", 1)?; + s.serialize_field("input", input)?; + s.skip_field("data")?; + s.end() + } + (None, Some(data)) => { + let mut s = serde::Serializer::serialize_struct(serializer, "TransactionInput", 1)?; + s.skip_field("input")?; + s.serialize_field("data", data)?; + s.end() + } + (None, None) => { + let mut s = serde::Serializer::serialize_struct(serializer, "TransactionInput", 0)?; + s.skip_field("input")?; + s.skip_field("data")?; + s.end() + } + } + } +} + +impl<'de> serde::Deserialize<'de> for TransactionInput { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct InputOrData { + input: Option, + data: Option, + } + + let InputOrData { input, data } = InputOrData::deserialize(deserializer)?; + + match (input, data) { + (Some(input), Some(data)) => { + if input == data { + Ok(Self { + input: Some(input), + data: Some(data), + }) + } else { + Err(de::Error::custom("Ambiguous value for `input` and `data`")) + } + } + (input, data) => Ok(Self { input, data }), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn transaction_input_serde_impl() { + let valid_cases = [ + ( + r#"{"input":"0x12","data":"0x12"}"#, + TransactionInput { + input: Some(Bytes(vec![0x12])), + data: Some(Bytes(vec![0x12])), + }, + ), + ( + r#"{"input":"0x12"}"#, + TransactionInput { + input: Some(Bytes(vec![0x12])), + data: None, + }, + ), + ( + r#"{"data":"0x12"}"#, + TransactionInput { + input: None, + data: Some(Bytes(vec![0x12])), + }, + ), + ( + r#"{}"#, + TransactionInput { + input: None, + data: None, + }, + ), + ]; + for (raw, typed) in valid_cases { + let deserialized = serde_json::from_str::(raw).unwrap(); + assert_eq!(deserialized, typed); + + let serialized = serde_json::to_string(&typed).unwrap(); + assert_eq!(serialized, raw); + } + + let invalid_serialization_cases = [TransactionInput { + input: Some(Bytes(vec![0x12])), + data: Some(Bytes(vec![0x23])), + }]; + for typed in invalid_serialization_cases { + let serialized: Result = serde_json::to_string(&typed); + assert!(serialized.is_err()); + } + + let invalid_deserialization_cases = [r#"{"input":"0x12","data":"0x23"}"#]; + for raw in invalid_deserialization_cases { + let input: Result = serde_json::from_str(raw); + assert!(input.is_err()); + } + } +} diff --git a/client/rpc-v2/types/src/transaction/signature.rs b/client/rpc-v2/types/src/transaction/signature.rs new file mode 100644 index 0000000000..bd4f0d1203 --- /dev/null +++ b/client/rpc-v2/types/src/transaction/signature.rs @@ -0,0 +1,152 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use ethereum_types::U256; +use serde::{Deserialize, Serialize}; + +/// Transaction signature. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionSignature { + /// The R field of the signature + pub r: U256, + /// The S field of the signature + pub s: U256, + + /// The standardised V field of the signature. + /// + /// - For legacy transactions, this is the recovery id. + /// - For typed transactions (EIP-2930, EIP-1559, EIP-4844), this is set to the parity + /// (0 for even, 1 for odd) of the y-value of the secp256k1 signature. + /// + /// # Note + /// + /// For backwards compatibility, `v` is optionally provided as an alternative to `yParity`. + /// This field is DEPRECATED and all use of it should migrate to `yParity`. + pub v: U256, + /// The parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature. + /// + /// This is only used for typed (non-legacy) transactions. + #[serde(skip_serializing_if = "Option::is_none")] + pub y_parity: Option, +} + +/// Type that represents the parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature. +/// +/// This will be serialized as "0x0" if false, and "0x1" if true. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Parity(pub bool); + +impl serde::Serialize for Parity { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(if self.0 { "0x1" } else { "0x0" }) + } +} + +impl<'de> serde::Deserialize<'de> for Parity { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match s.as_str() { + "0x0" => Ok(Self(false)), + "0x1" => Ok(Self(true)), + _ => Err(serde::de::Error::custom(format!( + "invalid parity value, parity should be either \"0x0\" or \"0x1\": {s}", + ))), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parity_serde_impl() { + let valid_cases = [(r#""0x1""#, Parity(true)), (r#""0x0""#, Parity(false))]; + for (raw, typed) in valid_cases { + let deserialized = serde_json::from_str::(raw).unwrap(); + assert_eq!(deserialized, typed); + + let serialized = serde_json::to_string(&typed).unwrap(); + assert_eq!(serialized, raw); + } + + let invalid_cases = [r#""0x2""#, r#""0x""#, r#""0""#, r#""1""#]; + for raw in invalid_cases { + let parity: Result = serde_json::from_str(raw); + assert!(parity.is_err()); + } + } + + #[test] + fn signature_serde_impl() { + let cases = [ + // without parity + ( + r#"{ + "r":"0xab3743210536a011365f73bc6e25668177203562aa53741086f56d1ef3e101c0", + "s":"0x479de4b30541dd1d3b73d5b9d8393d48d91d64ca3ff71f64bd7adaac2657a8e5", + "v":"0x1546d71" + }"#, + TransactionSignature { + r: "0xab3743210536a011365f73bc6e25668177203562aa53741086f56d1ef3e101c0" + .parse() + .unwrap(), + s: "0x479de4b30541dd1d3b73d5b9d8393d48d91d64ca3ff71f64bd7adaac2657a8e5" + .parse() + .unwrap(), + v: "0x1546d71".parse().unwrap(), + y_parity: None, + }, + ), + // with parity + ( + r#"{ + "r":"0x39614515ff2794c0e005b33dd05e2cdce7857ae7ee47e9b6aa739c314c760f5", + "s":"0x32670b1a7dbf2700e5fb65eb8e24c87ba18694a11fae98e5cf731f10f27f1f72", + "v":"0x1", + "yParity":"0x1" + }"#, + TransactionSignature { + r: "0x39614515ff2794c0e005b33dd05e2cdce7857ae7ee47e9b6aa739c314c760f5" + .parse() + .unwrap(), + s: "0x32670b1a7dbf2700e5fb65eb8e24c87ba18694a11fae98e5cf731f10f27f1f72" + .parse() + .unwrap(), + v: "0x1".parse().unwrap(), + y_parity: Some(Parity(true)), + }, + ), + ]; + + for (raw, typed) in cases { + let deserialized = serde_json::from_str::(raw).unwrap(); + assert_eq!(deserialized, typed); + + let serialized = serde_json::to_string(&typed).unwrap(); + assert_eq!(serialized, raw.split_whitespace().collect::()); + } + } +} diff --git a/client/rpc-v2/types/src/txpool.rs b/client/rpc-v2/types/src/txpool.rs new file mode 100644 index 0000000000..d874c08480 --- /dev/null +++ b/client/rpc-v2/types/src/txpool.rs @@ -0,0 +1,198 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::{collections::BTreeMap, fmt}; + +use ethereum_types::{Address, U256, U64}; +use serde::{de, Deserialize, Serialize}; + +use crate::transaction::Transaction; + +pub type TxpoolInspect = TxpoolResult>>; +pub type TxpoolContent = TxpoolResult>>; +pub type TxpoolContentFrom = TxpoolResult>; +pub type TxpoolStatus = TxpoolResult; + +pub type NonceMapping = BTreeMap; +pub type AddressMapping = BTreeMap; + +/// The txpool result type. +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct TxpoolResult { + /// Pending transactions. + pub pending: T, + /// Queued transactions. + pub queued: T, +} + +/// The textual summary of all the transactions currently pending for inclusion in the next block(s). +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Summary { + /// Recipient. + pub to: Option
, + /// Transferred value. + pub value: U256, + /// Gas limit. + pub gas: u128, + /// Gas price. + pub gas_price: u128, +} + +impl serde::Serialize for Summary { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let formatted_to = if let Some(to) = self.to { + format!("{to:?}") + } else { + "contract creation".to_string() + }; + let formatted = format!( + "{}: {} wei + {} gas × {} wei", + formatted_to, self.value, self.gas, self.gas_price + ); + serializer.serialize_str(&formatted) + } +} + +impl<'de> serde::Deserialize<'de> for Summary { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct SummaryVisitor; + impl<'de> de::Visitor<'de> for SummaryVisitor { + type Value = Summary; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("{{to}}: {{value}} wei + {{gas}} gas × {{gas_price}} wei") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + let addr_split: Vec<&str> = v.split(": ").collect(); + if addr_split.len() != 2 { + return Err(de::Error::custom("invalid `to` format")); + } + + let value_split: Vec<&str> = addr_split[1].split(" wei + ").collect(); + if value_split.len() != 2 { + return Err(de::Error::custom("invalid `value` format")); + } + + let gas_split: Vec<&str> = value_split[1].split(" gas × ").collect(); + if gas_split.len() != 2 { + return Err(de::Error::custom("invalid `gas` format")); + } + + let gas_price_split: Vec<&str> = gas_split[1].split(" wei").collect(); + if gas_price_split.len() != 2 { + return Err(de::Error::custom("invalid `gas_price` format")); + } + + let to = match addr_split[0] { + "contract creation" => None, + addr => { + let addr = addr + .trim_start_matches("0x") + .parse::
() + .map_err(de::Error::custom)?; + Some(addr) + } + }; + let value = U256::from_dec_str(value_split[0]).map_err(de::Error::custom)?; + let gas = gas_split[0].parse::().map_err(de::Error::custom)?; + let gas_price = gas_price_split[0] + .parse::() + .map_err(de::Error::custom)?; + + Ok(Summary { + to, + value, + gas, + gas_price, + }) + } + + fn visit_string(self, v: String) -> Result + where + E: de::Error, + { + self.visit_str(&v) + } + } + + deserializer.deserialize_str(SummaryVisitor) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn inspect_summary_serde_impl() { + let valid_cases = [ + ( + r#""contract creation: 2472666000 wei + 21000 gas × 1000 wei""#, + Summary { + to: None, + value: U256::from(2472666000u64), + gas: 21000, + gas_price: 1000, + }, + ), + ( + r#""0x1111111111111111111111111111111111111111: 2472666000 wei + 21000 gas × 1000 wei""#, + Summary { + to: Some( + "0x1111111111111111111111111111111111111111" + .parse::
() + .unwrap(), + ), + value: U256::from(2472666000u64), + gas: 21000, + gas_price: 1000, + }, + ), + ]; + for (raw, typed) in valid_cases { + let deserialized = serde_json::from_str::(raw).unwrap(); + assert_eq!(deserialized, typed); + + let serialized = serde_json::to_string(&typed).unwrap(); + assert_eq!(serialized, raw); + } + + let invalid_cases = [ + r#"": ""#, + r#"" : 2472666000 wei + 21000 gas × 1000 wei""#, + r#""0x: 2472666000 wei + 21000 gas × 1000 wei""#, + r#""0x1111111111111111111111111111111111111111: 2472666000 wei""#, + r#""0x1111111111111111111111111111111111111111: 2472666000 wei + 21000 gas × ""#, + r#""0x1111111111111111111111111111111111111111: 2472666000 wei + 21000 gas × 1000""#, + ]; + for raw in invalid_cases { + let summary: Result = serde_json::from_str(raw); + assert!(summary.is_err()); + } + } +}