diff --git a/Cargo.toml b/Cargo.toml index 0d88dd62c38..1d5204b9c28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,19 +58,19 @@ tracing-subscriber = "0.3.18" tempfile = "3.8" -auto_impl = "1.1" assert_matches = "1.5" +auto_impl = "1.1" base64 = "0.21" bimap = "0.6" +home = "0.5" itertools = "0.12" pin-project = "1.1" rand = "0.8.5" -reqwest = "0.11.18" +reqwest = { version = "0.11.18", default-features = false } +semver = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_with = "3.4" -home = "0.5" -semver = "1.0" serial_test = "2.0" thiserror = "1.0" url = "2.4" diff --git a/crates/providers/Cargo.toml b/crates/providers/Cargo.toml index 1fed9b3f1fb..66627309beb 100644 --- a/crates/providers/Cargo.toml +++ b/crates/providers/Cargo.toml @@ -18,11 +18,11 @@ alloy-rpc-client.workspace = true alloy-rpc-types.workspace = true alloy-transport-http.workspace = true alloy-transport.workspace = true - async-trait.workspace = true -reqwest.workspace = true serde.workspace = true thiserror.workspace = true +reqwest.workspace = true +auto_impl = "1.1.0" [dev-dependencies] tokio = { version = "1.33.0", features = ["macros"] } diff --git a/crates/providers/src/provider.rs b/crates/providers/src/provider.rs index b1f8b83d7c0..27f721a1855 100644 --- a/crates/providers/src/provider.rs +++ b/crates/providers/src/provider.rs @@ -1,17 +1,19 @@ //! Alloy main Provider abstraction. use crate::utils::{self, EstimatorFunction}; -use alloy_primitives::{Address, BlockHash, Bytes, TxHash, U256, U64}; +use alloy_primitives::{Address, BlockHash, Bytes, StorageKey, StorageValue, TxHash, U256, U64}; use alloy_rpc_client::{ClientBuilder, RpcClient}; use alloy_rpc_types::{ - Block, BlockId, BlockNumberOrTag, FeeHistory, Filter, Log, RpcBlockHash, SyncStatus, - Transaction, TransactionReceipt, TransactionRequest, + trace::{GethDebugTracingOptions, GethTrace, LocalizedTransactionTrace}, + AccessListWithGasUsed, Block, BlockId, BlockNumberOrTag, CallRequest, + EIP1186AccountProofResponse, FeeHistory, Filter, Log, SyncStatus, Transaction, + TransactionReceipt, }; use alloy_transport::{BoxTransport, Transport, TransportErrorKind, TransportResult}; use alloy_transport_http::Http; +use auto_impl::auto_impl; use reqwest::Client; -use serde::{Deserialize, Serialize}; -use std::borrow::Cow; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use thiserror::Error; #[derive(Debug, Error, Serialize, Deserialize)] @@ -31,223 +33,352 @@ pub struct Provider { from: Option
, } +/// Temporary Provider trait to be used until the new Provider trait with +/// the Network abstraction is stable. +/// Once the new Provider trait is stable, this trait will be removed. +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +#[auto_impl(&, &mut, Rc, Arc, Box)] +pub trait TempProvider: Send + Sync { + /// Gets the transaction count of the corresponding address. + async fn get_transaction_count( + &self, + address: Address, + tag: Option, + ) -> TransportResult; + + /// Gets the last block number available. + async fn get_block_number(&self) -> TransportResult; + + /// Gets the balance of the account at the specified tag, which defaults to latest. + async fn get_balance(&self, address: Address, tag: Option) -> TransportResult; + + /// Gets a block by either its hash, tag, or number, with full transactions or only hashes. + async fn get_block(&self, id: BlockId, full: bool) -> TransportResult> { + match id { + BlockId::Hash(hash) => self.get_block_by_hash(hash.into(), full).await, + BlockId::Number(number) => self.get_block_by_number(number, full).await, + } + } + + /// Gets a block by its [BlockHash], with full transactions or only hashes. + async fn get_block_by_hash( + &self, + hash: BlockHash, + full: bool, + ) -> TransportResult>; + + /// Gets a block by [BlockNumberOrTag], with full transactions or only hashes. + async fn get_block_by_number( + &self, + number: BlockNumberOrTag, + full: bool, + ) -> TransportResult>; + + /// Gets the chain ID. + async fn get_chain_id(&self) -> TransportResult; + + /// Gets the specified storage value from [Address]. + async fn get_storage_at( + &self, + address: Address, + key: StorageKey, + tag: Option, + ) -> TransportResult; + + /// Gets the bytecode located at the corresponding [Address]. + async fn get_code_at(&self, address: Address, tag: BlockId) -> TransportResult; + + /// Gets a [Transaction] by its [TxHash]. + async fn get_transaction_by_hash(&self, hash: TxHash) -> TransportResult; + + /// Retrieves a [`Vec`] with the given [Filter]. + async fn get_logs(&self, filter: Filter) -> TransportResult>; + + /// Gets the accounts in the remote node. This is usually empty unless you're using a local + /// node. + async fn get_accounts(&self) -> TransportResult>; + + /// Gets the current gas price. + async fn get_gas_price(&self) -> TransportResult; + + /// Gets a [TransactionReceipt] if it exists, by its [TxHash]. + async fn get_transaction_receipt( + &self, + hash: TxHash, + ) -> TransportResult>; + + /// Returns a collection of historical gas information [FeeHistory] which + /// can be used to calculate the EIP1559 fields `maxFeePerGas` and `maxPriorityFeePerGas`. + async fn get_fee_history( + &self, + block_count: U256, + last_block: BlockNumberOrTag, + reward_percentiles: &[f64], + ) -> TransportResult; + + /// Gets the selected block [BlockNumberOrTag] receipts. + async fn get_block_receipts( + &self, + block: BlockNumberOrTag, + ) -> TransportResult>; + + /// Gets an uncle block through the tag [BlockId] and index [U64]. + async fn get_uncle(&self, tag: BlockId, idx: U64) -> TransportResult>; + + /// Gets syncing info. + async fn syncing(&self) -> TransportResult; + + /// Execute a smart contract call with [CallRequest] without publishing a transaction. + async fn call(&self, tx: CallRequest, block: Option) -> TransportResult; + + /// Estimate the gas needed for a transaction. + async fn estimate_gas(&self, tx: CallRequest, block: Option) -> TransportResult; + + /// Sends an already-signed transaction. + async fn send_raw_transaction(&self, tx: Bytes) -> TransportResult; + + /// Estimates the EIP1559 `maxFeePerGas` and `maxPriorityFeePerGas` fields. + /// Receives an optional [EstimatorFunction] that can be used to modify + /// how to estimate these fees. + async fn estimate_eip1559_fees( + &self, + estimator: Option, + ) -> TransportResult<(U256, U256)>; + + #[cfg(feature = "anvil")] + async fn set_code(&self, address: Address, code: &'static str) -> TransportResult<()>; + + async fn get_proof( + &self, + address: Address, + keys: Vec, + block: Option, + ) -> TransportResult; + + async fn create_access_list( + &self, + request: CallRequest, + block: Option, + ) -> TransportResult; + + /// Parity trace transaction. + async fn trace_transaction( + &self, + hash: TxHash, + ) -> TransportResult>; + + async fn debug_trace_transaction( + &self, + hash: TxHash, + trace_options: GethDebugTracingOptions, + ) -> TransportResult; + + async fn trace_block( + &self, + block: BlockNumberOrTag, + ) -> TransportResult>; + + async fn raw_request(&self, method: &'static str, params: P) -> TransportResult + where + P: Serialize + Send + Sync + Clone, + R: Serialize + DeserializeOwned + Send + Sync + Unpin + 'static, + Self: Sync; +} + +impl Provider { + pub fn new(transport: T) -> Self { + Self { + // todo(onbjerg): do we just default to false + inner: RpcClient::new(transport, false), + from: None, + } + } + + pub fn new_with_client(client: RpcClient) -> Self { + Self { inner: client, from: None } + } + + pub fn with_sender(mut self, from: Address) -> Self { + self.from = Some(from); + self + } + + pub fn inner(&self) -> &RpcClient { + &self.inner + } +} + +// todo: validate usage of BlockId vs BlockNumberOrTag vs Option etc. // Simple JSON-RPC bindings. // In the future, this will be replaced by a Provider trait, // but as the interface is not stable yet, we define the bindings ourselves // until we can use the trait and the client abstraction that will use it. -impl Provider { +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl TempProvider for Provider { /// Gets the transaction count of the corresponding address. - pub async fn get_transaction_count( + async fn get_transaction_count( &self, address: Address, + tag: Option, ) -> TransportResult { self.inner .prepare( "eth_getTransactionCount", - Cow::<(Address, &'static str)>::Owned((address, "latest")), + (address, tag.unwrap_or(BlockNumberOrTag::Latest.into())), ) .await } /// Gets the last block number available. - pub async fn get_block_number(&self) -> TransportResult { - self.inner.prepare("eth_blockNumber", Cow::<()>::Owned(())).await + /// Gets the last block number available. + async fn get_block_number(&self) -> TransportResult { + self.inner.prepare("eth_blockNumber", ()).await.map(|num: U64| num.to::()) } /// Gets the balance of the account at the specified tag, which defaults to latest. - pub async fn get_balance( - &self, - address: Address, - tag: Option, - ) -> TransportResult { + async fn get_balance(&self, address: Address, tag: Option) -> TransportResult { self.inner .prepare( "eth_getBalance", - Cow::<(Address, BlockId)>::Owned(( - address, - tag.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest)), - )), + (address, tag.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest))), ) .await } /// Gets a block by its [BlockHash], with full transactions or only hashes. - pub async fn get_block_by_hash( + async fn get_block_by_hash( &self, hash: BlockHash, full: bool, ) -> TransportResult> { - self.inner - .prepare("eth_getBlockByHash", Cow::<(BlockHash, bool)>::Owned((hash, full))) - .await + self.inner.prepare("eth_getBlockByHash", (hash, full)).await } /// Gets a block by [BlockNumberOrTag], with full transactions or only hashes. - pub async fn get_block_by_number + Send + Sync>( + async fn get_block_by_number( &self, - number: B, + number: BlockNumberOrTag, full: bool, ) -> TransportResult> { - self.inner - .prepare( - "eth_getBlockByNumber", - Cow::<(BlockNumberOrTag, bool)>::Owned((number.into(), full)), - ) - .await + self.inner.prepare("eth_getBlockByNumber", (number, full)).await } /// Gets the chain ID. - pub async fn get_chain_id(&self) -> TransportResult { - self.inner.prepare("eth_chainId", Cow::<()>::Owned(())).await + async fn get_chain_id(&self) -> TransportResult { + self.inner.prepare("eth_chainId", ()).await } - /// Gets the bytecode located at the corresponding [Address]. - pub async fn get_code_at + Send + Sync>( + + /// Gets the specified storage value from [Address]. + async fn get_storage_at( &self, address: Address, - tag: B, - ) -> TransportResult { + key: StorageKey, + tag: Option, + ) -> TransportResult { self.inner - .prepare("eth_getCode", Cow::<(Address, BlockId)>::Owned((address, tag.into()))) + .prepare( + "eth_getStorageAt", + (address, key, tag.unwrap_or(BlockNumberOrTag::Latest.into())), + ) .await } + /// Gets the bytecode located at the corresponding [Address]. + async fn get_code_at(&self, address: Address, tag: BlockId) -> TransportResult { + self.inner.prepare("eth_getCode", (address, tag)).await + } + /// Gets a [Transaction] by its [TxHash]. - pub async fn get_transaction_by_hash(&self, hash: TxHash) -> TransportResult { - self.inner - .prepare( - "eth_getTransactionByHash", - // Force alloy-rs/alloy to encode this an array of strings, - // even if we only need to send one hash. - Cow::>::Owned(vec![hash]), - ) - .await + async fn get_transaction_by_hash(&self, hash: TxHash) -> TransportResult { + self.inner.prepare("eth_getTransactionByHash", (hash,)).await } /// Retrieves a [`Vec`] with the given [Filter]. - pub async fn get_logs(&self, filter: Filter) -> TransportResult> { - self.inner.prepare("eth_getLogs", Cow::>::Owned(vec![filter])).await + async fn get_logs(&self, filter: Filter) -> TransportResult> { + self.inner.prepare("eth_getLogs", vec![filter]).await } /// Gets the accounts in the remote node. This is usually empty unless you're using a local /// node. - pub async fn get_accounts(&self) -> TransportResult> { - self.inner.prepare("eth_accounts", Cow::<()>::Owned(())).await + async fn get_accounts(&self) -> TransportResult> { + self.inner.prepare("eth_accounts", ()).await } /// Gets the current gas price. - pub async fn get_gas_price(&self) -> TransportResult { - self.inner.prepare("eth_gasPrice", Cow::<()>::Owned(())).await + async fn get_gas_price(&self) -> TransportResult { + self.inner.prepare("eth_gasPrice", ()).await } /// Gets a [TransactionReceipt] if it exists, by its [TxHash]. - pub async fn get_transaction_receipt( + async fn get_transaction_receipt( &self, hash: TxHash, ) -> TransportResult> { - self.inner.prepare("eth_getTransactionReceipt", Cow::>::Owned(vec![hash])).await + self.inner.prepare("eth_getTransactionReceipt", (hash,)).await } /// Returns a collection of historical gas information [FeeHistory] which /// can be used to calculate the EIP1559 fields `maxFeePerGas` and `maxPriorityFeePerGas`. - pub async fn get_fee_history + Send + Sync>( + async fn get_fee_history( &self, block_count: U256, - last_block: B, + last_block: BlockNumberOrTag, reward_percentiles: &[f64], ) -> TransportResult { - self.inner - .prepare( - "eth_feeHistory", - Cow::<(U256, BlockNumberOrTag, Vec)>::Owned(( - block_count, - last_block.into(), - reward_percentiles.to_vec(), - )), - ) - .await + self.inner.prepare("eth_feeHistory", (block_count, last_block, reward_percentiles)).await } /// Gets the selected block [BlockNumberOrTag] receipts. - pub async fn get_block_receipts( + async fn get_block_receipts( &self, block: BlockNumberOrTag, - ) -> TransportResult> { - self.inner.prepare("eth_getBlockReceipts", Cow::::Owned(block)).await + ) -> TransportResult> +where { + self.inner.prepare("eth_getBlockReceipts", block).await } /// Gets an uncle block through the tag [BlockId] and index [U64]. - pub async fn get_uncle + Send + Sync>( - &self, - tag: B, - idx: U64, - ) -> TransportResult> { - let tag = tag.into(); + async fn get_uncle(&self, tag: BlockId, idx: U64) -> TransportResult> { match tag { BlockId::Hash(hash) => { - self.inner - .prepare( - "eth_getUncleByBlockHashAndIndex", - Cow::<(RpcBlockHash, U64)>::Owned((hash, idx)), - ) - .await + self.inner.prepare("eth_getUncleByBlockHashAndIndex", (hash, idx)).await } BlockId::Number(number) => { - self.inner - .prepare( - "eth_getUncleByBlockNumberAndIndex", - Cow::<(BlockNumberOrTag, U64)>::Owned((number, idx)), - ) - .await + self.inner.prepare("eth_getUncleByBlockNumberAndIndex", (number, idx)).await } } } /// Gets syncing info. - pub async fn syncing(&self) -> TransportResult { - self.inner.prepare("eth_syncing", Cow::<()>::Owned(())).await + async fn syncing(&self) -> TransportResult { + self.inner.prepare("eth_syncing", ()).await } - /// Execute a smart contract call with [TransactionRequest] without publishing a transaction. - pub async fn call( - &self, - tx: TransactionRequest, - block: Option, - ) -> TransportResult { - self.inner - .prepare( - "eth_call", - Cow::<(TransactionRequest, BlockId)>::Owned(( - tx, - block.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest)), - )), - ) - .await + /// Execute a smart contract call with [CallRequest] without publishing a transaction. + async fn call(&self, tx: CallRequest, block: Option) -> TransportResult { + self.inner.prepare("eth_call", (tx, block.unwrap_or_default())).await } /// Estimate the gas needed for a transaction. - pub async fn estimate_gas( - &self, - tx: TransactionRequest, - block: Option, - ) -> TransportResult { + async fn estimate_gas(&self, tx: CallRequest, block: Option) -> TransportResult { if let Some(block_id) = block { - let params = Cow::<(TransactionRequest, BlockId)>::Owned((tx, block_id)); - self.inner.prepare("eth_estimateGas", params).await + self.inner.prepare("eth_estimateGas", (tx, block_id)).await } else { - let params = Cow::::Owned(tx); - self.inner.prepare("eth_estimateGas", params).await + self.inner.prepare("eth_estimateGas", (tx,)).await } } /// Sends an already-signed transaction. - pub async fn send_raw_transaction(&self, tx: Bytes) -> TransportResult { - self.inner.prepare("eth_sendRawTransaction", Cow::::Owned(tx)).await + async fn send_raw_transaction(&self, tx: Bytes) -> TransportResult { + self.inner.prepare("eth_sendRawTransaction", tx).await } /// Estimates the EIP1559 `maxFeePerGas` and `maxPriorityFeePerGas` fields. /// Receives an optional [EstimatorFunction] that can be used to modify /// how to estimate these fees. - pub async fn estimate_eip1559_fees( + async fn estimate_eip1559_fees( &self, estimator: Option, ) -> TransportResult<(U256, U256)> { @@ -288,38 +419,81 @@ impl Provider { Ok((max_fee_per_gas, max_priority_fee_per_gas)) } - #[cfg(feature = "anvil")] - pub async fn set_code(&self, address: Address, code: &'static str) -> TransportResult<()> { + async fn get_proof( + &self, + address: Address, + keys: Vec, + block: Option, + ) -> TransportResult { + self.inner + .prepare( + "eth_getProof", + (address, keys, block.unwrap_or(BlockNumberOrTag::Latest.into())), + ) + .await + } + + async fn create_access_list( + &self, + request: CallRequest, + block: Option, + ) -> TransportResult { self.inner - .prepare("anvil_setCode", Cow::<(Address, &'static str)>::Owned((address, code))) + .prepare( + "eth_createAccessList", + (request, block.unwrap_or(BlockNumberOrTag::Latest.into())), + ) .await } - pub fn with_sender(mut self, from: Address) -> Self { - self.from = Some(from); - self + /// Parity trace transaction. + async fn trace_transaction( + &self, + hash: TxHash, + ) -> TransportResult> { + self.inner.prepare("trace_transaction", vec![hash]).await } - pub fn inner(&self) -> &RpcClient { - &self.inner + async fn debug_trace_transaction( + &self, + hash: TxHash, + trace_options: GethDebugTracingOptions, + ) -> TransportResult { + self.inner.prepare("debug_traceTransaction", (hash, trace_options)).await } -} -// HTTP Transport Provider implementation -impl Provider> { - pub fn new(url: &str) -> Result { - let url = url.parse().map_err(|_e| ClientError::ParseError)?; - let inner = ClientBuilder::default().reqwest_http(url); + async fn trace_block( + &self, + block: BlockNumberOrTag, + ) -> TransportResult> { + self.inner.prepare("trace_block", block).await + } - Ok(Self { inner, from: None }) + /// Sends a raw request with the methods and params specified to the internal connection, + /// and returns the result. + async fn raw_request(&self, method: &'static str, params: P) -> TransportResult + where + P: Serialize + Send + Sync + Clone, + R: Serialize + DeserializeOwned + Send + Sync + Unpin + 'static, + { + let res: R = self.inner.prepare(method, ¶ms).await?; + Ok(res) + } + + #[cfg(feature = "anvil")] + async fn set_code(&self, address: Address, code: &'static str) -> TransportResult<()> { + self.inner.prepare("anvil_setCode", (address, code)).await } } impl TryFrom<&str> for Provider> { type Error = ClientError; - fn try_from(value: &str) -> Result { - Provider::new(value) + fn try_from(url: &str) -> Result { + let url = url.parse().map_err(|_e| ClientError::ParseError)?; + let inner = ClientBuilder::default().reqwest_http(url); + + Ok(Self { inner, from: None }) } } @@ -341,26 +515,39 @@ impl<'a> TryFrom<&'a String> for Provider> { #[cfg(test)] mod providers_test { - use crate::{provider::Provider, utils}; + use crate::{ + provider::{Provider, TempProvider}, + utils, + }; use alloy_primitives::{address, b256, U256, U64}; - use alloy_rpc_types::{BlockNumberOrTag, Filter}; - + use alloy_rpc_types::{Block, BlockNumberOrTag, Filter}; use ethers_core::utils::Anvil; #[tokio::test] async fn gets_block_number() { let anvil = Anvil::new().spawn(); - let provider = Provider::new(&anvil.endpoint()).unwrap(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); let num = provider.get_block_number().await.unwrap(); - assert_eq!(U64::ZERO, num) + assert_eq!(0, num) + } + + #[tokio::test] + async fn gets_block_number_with_raw_req() { + let anvil = Anvil::new().spawn(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); + let num: U64 = provider.raw_request("eth_blockNumber", ()).await.unwrap(); + assert_eq!(0, num.to::()) } #[tokio::test] async fn gets_transaction_count() { let anvil = Anvil::new().spawn(); - let provider = Provider::new(&anvil.endpoint()).unwrap(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); let count = provider - .get_transaction_count(address!("328375e18E7db8F1CA9d9bA8bF3E9C94ee34136A")) + .get_transaction_count( + address!("328375e18E7db8F1CA9d9bA8bF3E9C94ee34136A"), + Some(BlockNumberOrTag::Latest.into()), + ) .await .unwrap(); assert_eq!(count, U256::from(0)); @@ -369,7 +556,7 @@ mod providers_test { #[tokio::test] async fn gets_block_by_hash() { let anvil = Anvil::new().spawn(); - let provider = Provider::new(&anvil.endpoint()).unwrap(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); let num = 0; let tag: BlockNumberOrTag = num.into(); let block = provider.get_block_by_number(tag, true).await.unwrap().unwrap(); @@ -378,10 +565,28 @@ mod providers_test { assert_eq!(block.header.hash.unwrap(), hash); } + #[tokio::test] + async fn gets_block_by_hash_with_raw_req() { + let anvil = Anvil::new().spawn(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); + let num = 0; + let tag: BlockNumberOrTag = num.into(); + let block = provider.get_block_by_number(tag, true).await.unwrap().unwrap(); + let hash = block.header.hash.unwrap(); + let block: Block = provider + .raw_request::<(alloy_primitives::FixedBytes<32>, bool), Block>( + "eth_getBlockByHash", + (hash, true), + ) + .await + .unwrap(); + assert_eq!(block.header.hash.unwrap(), hash); + } + #[tokio::test] async fn gets_block_by_number_full() { let anvil = Anvil::new().spawn(); - let provider = Provider::new(&anvil.endpoint()).unwrap(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); let num = 0; let tag: BlockNumberOrTag = num.into(); let block = provider.get_block_by_number(tag, true).await.unwrap().unwrap(); @@ -391,7 +596,7 @@ mod providers_test { #[tokio::test] async fn gets_block_by_number() { let anvil = Anvil::new().spawn(); - let provider = Provider::new(&anvil.endpoint()).unwrap(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); let num = 0; let tag: BlockNumberOrTag = num.into(); let block = provider.get_block_by_number(tag, true).await.unwrap().unwrap(); @@ -401,7 +606,7 @@ mod providers_test { #[tokio::test] async fn gets_chain_id() { let anvil = Anvil::new().args(vec!["--chain-id", "13371337"]).spawn(); - let provider = Provider::new(&anvil.endpoint()).unwrap(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); let chain_id = provider.get_chain_id().await.unwrap(); assert_eq!(chain_id, U64::from(13371337)); } @@ -410,7 +615,7 @@ mod providers_test { #[cfg(feature = "anvil")] async fn gets_code_at() { let anvil = Anvil::new().spawn(); - let provider = Provider::new(&anvil.endpoint()).unwrap(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); // Set the code let addr = alloy_primitives::Address::with_last_byte(16); provider.set_code(addr, "0xbeef").await.unwrap(); @@ -427,7 +632,7 @@ mod providers_test { #[ignore] async fn gets_transaction_by_hash() { let anvil = Anvil::new().spawn(); - let provider = Provider::new(&anvil.endpoint()).unwrap(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); let tx = provider .get_transaction_by_hash(b256!( "5c03fab9114ceb98994b43892ade87ddfd9ae7e8f293935c3bd29d435dc9fd95" @@ -445,7 +650,7 @@ mod providers_test { #[ignore] async fn gets_logs() { let anvil = Anvil::new().spawn(); - let provider = Provider::new(&anvil.endpoint()).unwrap(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); let filter = Filter::new() .at_block_hash(b256!( "b20e6f35d4b46b3c4cd72152faec7143da851a0dc281d390bdd50f58bfbdb5d3" @@ -461,7 +666,7 @@ mod providers_test { #[ignore] async fn gets_tx_receipt() { let anvil = Anvil::new().spawn(); - let provider = Provider::new(&anvil.endpoint()).unwrap(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); let receipt = provider .get_transaction_receipt(b256!( "5c03fab9114ceb98994b43892ade87ddfd9ae7e8f293935c3bd29d435dc9fd95" @@ -479,12 +684,12 @@ mod providers_test { #[tokio::test] async fn gets_fee_history() { let anvil = Anvil::new().spawn(); - let provider = Provider::new(&anvil.endpoint()).unwrap(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); let block_number = provider.get_block_number().await.unwrap(); let fee_history = provider .get_fee_history( U256::from(utils::EIP1559_FEE_ESTIMATION_PAST_BLOCKS), - BlockNumberOrTag::Number(block_number.to()), + BlockNumberOrTag::Number(block_number), &[utils::EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE], ) .await diff --git a/crates/rpc-client/src/builder.rs b/crates/rpc-client/src/builder.rs index f6870ee6fe4..1bbcd2baecc 100644 --- a/crates/rpc-client/src/builder.rs +++ b/crates/rpc-client/src/builder.rs @@ -37,7 +37,7 @@ impl ClientBuilder { /// Create a new [`RpcClient`] with the given transport and the configured /// layers. - fn transport(self, transport: T, is_local: bool) -> RpcClient + pub fn transport(self, transport: T, is_local: bool) -> RpcClient where L: Layer, T: Transport, diff --git a/crates/rpc-types/src/eth/block.rs b/crates/rpc-types/src/eth/block.rs index 558972f9cde..d29f966fb6d 100644 --- a/crates/rpc-types/src/eth/block.rs +++ b/crates/rpc-types/src/eth/block.rs @@ -1,6 +1,6 @@ //! Block RPC types. -use crate::{Transaction, Withdrawal}; +use crate::{other::OtherFields, Transaction, Withdrawal}; use alloy_primitives::{ ruint::ParseError, Address, BlockHash, BlockNumber, Bloom, Bytes, B256, B64, U256, U64, }; @@ -64,6 +64,11 @@ impl BlockTransactions { pub fn hashes_mut(&mut self) -> BlockTransactionHashesMut<'_> { BlockTransactionHashesMut::new(self) } + + /// Returns an instance of BlockTransactions with the Uncle special case. + pub fn uncle() -> Self { + Self::Uncle + } } /// An iterator over the transaction hashes of a block. @@ -245,23 +250,27 @@ pub enum BlockError { #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Block { - /// Header of the block + /// Header of the block. #[serde(flatten)] pub header: Header, /// Total difficulty, this field is None only if representing /// an Uncle block. #[serde(skip_serializing_if = "Option::is_none")] pub total_difficulty: Option, - /// Uncles' hashes + /// Uncles' hashes. pub uncles: Vec, - /// Transactions + /// Transactions. #[serde(skip_serializing_if = "BlockTransactions::is_uncle")] + #[serde(default = "BlockTransactions::uncle")] pub transactions: BlockTransactions, /// Integer the size of this block in bytes. pub size: Option, - /// Withdrawals in the block + /// Withdrawals in the block. #[serde(default, skip_serializing_if = "Option::is_none")] pub withdrawals: Option>, + /// Support for arbitrary additional fields. + #[serde(flatten)] + pub other: OtherFields, } impl Block { @@ -390,12 +399,12 @@ pub enum BlockNumberOrTag { /// Pending block (not yet part of the blockchain) Pending, /// Block by number from canon chain - Number(U64), + Number(u64), } impl BlockNumberOrTag { /// Returns the numeric block number if explicitly set - pub const fn as_number(&self) -> Option { + pub const fn as_number(&self) -> Option { match *self { BlockNumberOrTag::Number(num) => Some(num), _ => None, @@ -435,13 +444,13 @@ impl BlockNumberOrTag { impl From for BlockNumberOrTag { fn from(num: u64) -> Self { - BlockNumberOrTag::Number(U64::from(num)) + BlockNumberOrTag::Number(num) } } impl From for BlockNumberOrTag { fn from(num: U64) -> Self { - BlockNumberOrTag::Number(num) + num.to::().into() } } @@ -483,7 +492,7 @@ impl FromStr for BlockNumberOrTag { "pending" => Self::Pending, _number => { if let Some(hex_val) = s.strip_prefix("0x") { - let number = U64::from_str_radix(hex_val, 16); + let number = u64::from_str_radix(hex_val, 16); BlockNumberOrTag::Number(number?) } else { return Err(HexStringMissingPrefixError::default().into()); @@ -559,15 +568,21 @@ impl BlockId { } } +impl Default for BlockId { + fn default() -> Self { + BlockId::Number(BlockNumberOrTag::Latest) + } +} + impl From for BlockId { fn from(num: u64) -> Self { - BlockNumberOrTag::Number(U64::from(num)).into() + BlockNumberOrTag::Number(num).into() } } impl From for BlockId { fn from(num: U64) -> Self { - BlockNumberOrTag::Number(num).into() + BlockNumberOrTag::Number(num.to()).into() } } @@ -998,6 +1013,7 @@ mod tests { transactions: BlockTransactions::Hashes(vec![B256::with_last_byte(18)]), size: Some(U256::from(19)), withdrawals: Some(vec![]), + other: Default::default(), }; let serialized = serde_json::to_string(&block).unwrap(); assert_eq!( @@ -1039,6 +1055,7 @@ mod tests { transactions: BlockTransactions::Hashes(vec![B256::with_last_byte(18)]), size: Some(U256::from(19)), withdrawals: None, + other: Default::default(), }; let serialized = serde_json::to_string(&block).unwrap(); assert_eq!( diff --git a/crates/rpc-types/src/eth/fee.rs b/crates/rpc-types/src/eth/fee.rs index c7717994ea1..3662d63686a 100644 --- a/crates/rpc-types/src/eth/fee.rs +++ b/crates/rpc-types/src/eth/fee.rs @@ -37,8 +37,8 @@ pub struct FeeHistory { /// # Note /// /// The `Option` is only for compatability with Erigon and Geth. - #[serde(skip_serializing_if = "Vec::is_empty")] - #[serde(default)] + /// Empty list is skipped only for compatability with Erigon and Geth. + #[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`. @@ -46,13 +46,11 @@ pub struct FeeHistory { /// # Note /// /// The `Option` is only for compatability with Erigon and Geth. - #[serde(skip_serializing_if = "Vec::is_empty")] - #[serde(default)] pub gas_used_ratio: Vec, /// Lowest number block of the returned range. pub oldest_block: U256, /// An (optional) array of effective priority fee per gas data points from a single /// block. All zeroes are returned if the block is empty. - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub reward: Option>>, } diff --git a/crates/rpc-types/src/eth/filter.rs b/crates/rpc-types/src/eth/filter.rs index fc239c1ba91..fb6007251f7 100644 --- a/crates/rpc-types/src/eth/filter.rs +++ b/crates/rpc-types/src/eth/filter.rs @@ -455,12 +455,12 @@ impl Filter { } /// Returns the numeric value of the `toBlock` field - pub fn get_to_block(&self) -> Option { + pub fn get_to_block(&self) -> Option { self.block_option.get_to_block().and_then(|b| b.as_number()) } /// Returns the numeric value of the `fromBlock` field - pub fn get_from_block(&self) -> Option { + pub fn get_from_block(&self) -> Option { self.block_option.get_from_block().and_then(|b| b.as_number()) } @@ -759,7 +759,7 @@ impl FilteredParams { } /// Returns true if the filter matches the given block number - pub fn filter_block_range(&self, block_number: U64) -> bool { + pub fn filter_block_range(&self, block_number: u64) -> bool { if self.filter.is_none() { return true; } diff --git a/crates/rpc-types/src/eth/mod.rs b/crates/rpc-types/src/eth/mod.rs index 569dde4ca41..9da97f85a79 100644 --- a/crates/rpc-types/src/eth/mod.rs +++ b/crates/rpc-types/src/eth/mod.rs @@ -6,6 +6,7 @@ mod call; mod fee; mod filter; mod log; +pub mod other; pub mod pubsub; pub mod raw_log; pub mod state; diff --git a/crates/rpc-types/src/eth/other.rs b/crates/rpc-types/src/eth/other.rs new file mode 100644 index 00000000000..e6efe12dc53 --- /dev/null +++ b/crates/rpc-types/src/eth/other.rs @@ -0,0 +1,117 @@ +//! Support for capturing other fields +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Map; +use std::{ + collections::BTreeMap, + ops::{Deref, DerefMut}, +}; + +/// A type that is supposed to capture additional fields that are not native to ethereum but included in ethereum adjacent networks, for example fields the [optimism `eth_getTransactionByHash` request](https://docs.alchemy.com/alchemy/apis/optimism/eth-gettransactionbyhash) returns additional fields that this type will capture +/// +/// This type is supposed to be used with [`#[serde(flatten)`](https://serde.rs/field-attrs.html#flatten) +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Default)] +#[serde(transparent)] +pub struct OtherFields { + /// Contains all unknown fields + inner: BTreeMap, +} + +// === impl OtherFields === + +impl OtherFields { + /// Returns the deserialized value of the field, if it exists. + /// Deserializes the value with the given closure + pub fn get_with(&self, key: impl AsRef, with: F) -> Option + where + F: FnOnce(serde_json::Value) -> V, + { + self.inner.get(key.as_ref()).cloned().map(with) + } + + /// Returns the deserialized value of the field, if it exists + pub fn get_deserialized( + &self, + key: impl AsRef, + ) -> Option> { + self.inner.get(key.as_ref()).cloned().map(serde_json::from_value) + } + + /// Removes the deserialized value of the field, if it exists + /// + /// **Note:** this will also remove the value if deserializing it resulted in an error + pub fn remove_deserialized( + &mut self, + key: impl AsRef, + ) -> Option> { + self.inner.remove(key.as_ref()).map(serde_json::from_value) + } + + /// Removes the deserialized value of the field, if it exists. + /// Deserializes the value with the given closure + /// + /// **Note:** this will also remove the value if deserializing it resulted in an error + pub fn remove_with(&mut self, key: impl AsRef, with: F) -> Option + where + F: FnOnce(serde_json::Value) -> V, + { + self.inner.remove(key.as_ref()).map(with) + } + + /// Removes the deserialized value of the field, if it exists and also returns the key + /// + /// **Note:** this will also remove the value if deserializing it resulted in an error + pub fn remove_entry_deserialized( + &mut self, + key: impl AsRef, + ) -> Option<(String, serde_json::Result)> { + self.inner + .remove_entry(key.as_ref()) + .map(|(key, value)| (key, serde_json::from_value(value))) + } + + /// Deserialized this type into another container type + pub fn deserialize_into(self) -> serde_json::Result { + let mut map = Map::with_capacity(self.inner.len()); + map.extend(self); + serde_json::from_value(serde_json::Value::Object(map)) + } +} + +impl Deref for OtherFields { + type Target = BTreeMap; + + #[inline] + fn deref(&self) -> &BTreeMap { + self.as_ref() + } +} + +impl DerefMut for OtherFields { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl AsRef> for OtherFields { + fn as_ref(&self) -> &BTreeMap { + &self.inner + } +} + +impl IntoIterator for OtherFields { + type Item = (String, serde_json::Value); + type IntoIter = std::collections::btree_map::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.inner.into_iter() + } +} + +impl<'a> IntoIterator for &'a OtherFields { + type Item = (&'a String, &'a serde_json::Value); + type IntoIter = std::collections::btree_map::Iter<'a, String, serde_json::Value>; + + fn into_iter(self) -> Self::IntoIter { + self.as_ref().iter() + } +} diff --git a/crates/rpc-types/src/eth/transaction/access_list.rs b/crates/rpc-types/src/eth/transaction/access_list.rs index c4a7e8d1abe..f0b8025a0cc 100644 --- a/crates/rpc-types/src/eth/transaction/access_list.rs +++ b/crates/rpc-types/src/eth/transaction/access_list.rs @@ -1,19 +1,33 @@ -use alloy_primitives::{Address, U256}; +use alloy_primitives::{Address, B256, U256}; +use alloy_rlp::{RlpDecodable, RlpEncodable}; use serde::{Deserialize, Serialize}; +use std::mem; /// A list of addresses and storage keys that the transaction plans to access. /// Accesses outside the list are possible, but become more expensive. -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, Hash, Default)] +#[derive( + Deserialize, Serialize, Clone, Debug, PartialEq, Eq, Hash, Default, RlpEncodable, RlpDecodable, +)] #[serde(rename_all = "camelCase")] pub struct AccessListItem { /// Account addresses that would be loaded at the start of execution pub address: Address, /// Keys of storage that would be loaded at the start of execution - pub storage_keys: Vec, + pub storage_keys: Vec, +} + +impl AccessListItem { + /// Calculates a heuristic for the in-memory size of the [AccessListItem]. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::
() + self.storage_keys.capacity() * mem::size_of::() + } } /// AccessList as defined in EIP-2930 -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, Hash, Default)] +#[derive( + Deserialize, Serialize, Clone, Debug, PartialEq, Eq, Hash, Default, RlpEncodable, RlpDecodable, +)] pub struct AccessList(pub Vec); impl AccessList { @@ -29,12 +43,30 @@ impl AccessList { /// Consumes the type and returns an iterator over the list's addresses and storage keys. pub fn into_flatten(self) -> impl Iterator)> { - self.0.into_iter().map(|item| (item.address, item.storage_keys)) + self.0.into_iter().map(|item| { + ( + item.address, + item.storage_keys.into_iter().map(|slot| U256::from_be_bytes(slot.0)).collect(), + ) + }) } /// Returns an iterator over the list's addresses and storage keys. - pub fn flatten(&self) -> impl Iterator + '_ { - self.0.iter().map(|item| (item.address, item.storage_keys.as_slice())) + pub fn flatten(&self) -> impl Iterator)> + '_ { + self.0.iter().map(|item| { + ( + item.address, + item.storage_keys.iter().map(|slot| U256::from_be_bytes(slot.0)).collect(), + ) + }) + } + + /// Calculates a heuristic for the in-memory size of the [AccessList]. + #[inline] + pub fn size(&self) -> usize { + // take into account capacity + self.0.iter().map(AccessListItem::size).sum::() + + self.0.capacity() * mem::size_of::() } } @@ -55,8 +87,8 @@ mod tests { #[test] fn access_list_serde() { let list = AccessList(vec![ - AccessListItem { address: Address::ZERO, storage_keys: vec![U256::ZERO] }, - AccessListItem { address: Address::ZERO, storage_keys: vec![U256::ZERO] }, + AccessListItem { address: Address::ZERO, storage_keys: vec![B256::ZERO] }, + AccessListItem { address: Address::ZERO, storage_keys: vec![B256::ZERO] }, ]); let json = serde_json::to_string(&list).unwrap(); let list2 = serde_json::from_str::(&json).unwrap(); @@ -67,8 +99,8 @@ mod tests { fn access_list_with_gas_used() { let list = AccessListWithGasUsed { access_list: AccessList(vec![ - AccessListItem { address: Address::ZERO, storage_keys: vec![U256::ZERO] }, - AccessListItem { address: Address::ZERO, storage_keys: vec![U256::ZERO] }, + AccessListItem { address: Address::ZERO, storage_keys: vec![B256::ZERO] }, + AccessListItem { address: Address::ZERO, storage_keys: vec![B256::ZERO] }, ]), gas_used: U256::from(100), }; diff --git a/crates/rpc-types/src/eth/transaction/mod.rs b/crates/rpc-types/src/eth/transaction/mod.rs index 937e0d3b0ff..7ae7e404b55 100644 --- a/crates/rpc-types/src/eth/transaction/mod.rs +++ b/crates/rpc-types/src/eth/transaction/mod.rs @@ -12,8 +12,13 @@ mod common; mod receipt; mod request; mod signature; +mod tx_type; mod typed; +pub use tx_type::*; + +use crate::other::OtherFields; + /// Transaction object used in RPC #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -66,11 +71,11 @@ pub struct Transaction { #[serde(skip_serializing_if = "Option::is_none")] pub access_list: Option>, /// EIP2718 - /// - /// Transaction type, Some(2) for EIP-1559 transaction, - /// Some(1) for AccessList transaction, None for Legacy #[serde(rename = "type", skip_serializing_if = "Option::is_none")] pub transaction_type: Option, + /// Arbitrary extra fields. + #[serde(flatten)] + pub other: OtherFields, } #[cfg(test)] @@ -105,6 +110,7 @@ mod tests { max_fee_per_gas: Some(U128::from(21)), max_priority_fee_per_gas: Some(U128::from(22)), max_fee_per_blob_gas: None, + other: Default::default(), }; let serialized = serde_json::to_string(&transaction).unwrap(); assert_eq!( @@ -142,6 +148,7 @@ mod tests { max_fee_per_gas: Some(U128::from(21)), max_priority_fee_per_gas: Some(U128::from(22)), max_fee_per_blob_gas: None, + other: Default::default(), }; let serialized = serde_json::to_string(&transaction).unwrap(); assert_eq!( diff --git a/crates/rpc-types/src/eth/transaction/receipt.rs b/crates/rpc-types/src/eth/transaction/receipt.rs index 7fae30855eb..557e19316df 100644 --- a/crates/rpc-types/src/eth/transaction/receipt.rs +++ b/crates/rpc-types/src/eth/transaction/receipt.rs @@ -1,4 +1,4 @@ -use crate::Log; +use crate::{other::OtherFields, Log}; use alloy_primitives::{Address, Bloom, B256, U128, U256, U64, U8}; use serde::{Deserialize, Serialize}; @@ -51,4 +51,7 @@ pub struct TransactionReceipt { /// EIP-2718 Transaction type, Some(1) for AccessList transaction, None for Legacy #[serde(rename = "type")] pub transaction_type: U8, + /// Support for arbitrary additional fields. + #[serde(flatten)] + pub other: OtherFields, } diff --git a/crates/rpc-types/src/eth/transaction/signature.rs b/crates/rpc-types/src/eth/transaction/signature.rs index 82866a639a6..072f7c3d64c 100644 --- a/crates/rpc-types/src/eth/transaction/signature.rs +++ b/crates/rpc-types/src/eth/transaction/signature.rs @@ -1,5 +1,6 @@ //! Signature related RPC values use alloy_primitives::U256; +use alloy_rlp::{Bytes, Decodable, Encodable, Error as RlpError}; use serde::{Deserialize, Serialize}; /// Container type for all signature fields in RPC @@ -23,10 +24,118 @@ pub struct Signature { pub y_parity: Option, } +impl Signature { + /// Output the length of the signature without the length of the RLP header, using the legacy + /// scheme with EIP-155 support depends on chain_id. + pub fn payload_len_with_eip155_chain_id(&self, chain_id: Option) -> usize { + self.v(chain_id).length() + self.r.length() + self.s.length() + } + + /// Encode the `v`, `r`, `s` values without a RLP header. + /// Encodes the `v` value using the legacy scheme with EIP-155 support depends on chain_id. + pub fn encode_with_eip155_chain_id( + &self, + out: &mut dyn alloy_rlp::BufMut, + chain_id: Option, + ) { + self.v(chain_id).encode(out); + self.r.encode(out); + self.s.encode(out); + } + + /// Output the `v` of the signature depends on chain_id + #[inline] + pub fn v(&self, chain_id: Option) -> u64 { + if let Some(chain_id) = chain_id { + // EIP-155: v = {0, 1} + CHAIN_ID * 2 + 35 + let y_parity = u64::from(self.y_parity.unwrap_or(Parity(false))); + y_parity + chain_id * 2 + 35 + } else { + u64::from(self.y_parity.unwrap_or(Parity(false))) + 27 + } + } + + /// Decodes the `v`, `r`, `s` values without a RLP header. + /// This will return a chain ID if the `v` value is [EIP-155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md) compatible. + pub fn decode_with_eip155_chain_id(buf: &mut &[u8]) -> alloy_rlp::Result<(Self, Option)> { + let v = u64::decode(buf)?; + let r = Decodable::decode(buf)?; + let s = Decodable::decode(buf)?; + if v >= 35 { + // EIP-155: v = {0, 1} + CHAIN_ID * 2 + 35 + let y_parity = ((v - 35) % 2) != 0; + let chain_id = (v - 35) >> 1; + Ok(( + Signature { r, s, y_parity: Some(Parity(y_parity)), v: U256::from(v) }, + Some(chain_id), + )) + } else { + // non-EIP-155 legacy scheme, v = 27 for even y-parity, v = 28 for odd y-parity + if v != 27 && v != 28 { + return Err(RlpError::Custom("invalid Ethereum signature (V is not 27 or 28)")); + } + let y_parity = v == 28; + Ok((Signature { r, s, y_parity: Some(Parity(y_parity)), v: U256::from(v) }, None)) + } + } + + /// Output the length of the signature without the length of the RLP header + pub fn payload_len(&self) -> usize { + let y_parity_len = match self.y_parity { + Some(parity) => parity.0 as usize, + None => 0_usize, + }; + y_parity_len + self.r.length() + self.s.length() + } + + /// Encode the `y_parity`, `r`, `s` values without a RLP header. + /// Panics if the y parity is not set. + pub fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { + self.y_parity.expect("y_parity not set").encode(out); + self.r.encode(out); + self.s.encode(out); + } + + /// Decodes the `y_parity`, `r`, `s` values without a RLP header. + pub fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + let mut sig = Signature { + y_parity: Some(Decodable::decode(buf)?), + r: Decodable::decode(buf)?, + s: Decodable::decode(buf)?, + v: U256::ZERO, + }; + sig.v = sig.y_parity.unwrap().into(); + Ok(sig) + } + + /// Turn this signature into its byte + /// (hex) representation. + /// Panics: if the y_parity field is not set. + pub fn to_bytes(&self) -> [u8; 65] { + let mut sig = [0u8; 65]; + sig[..32].copy_from_slice(&self.r.to_be_bytes::<32>()); + sig[32..64].copy_from_slice(&self.s.to_be_bytes::<32>()); + let v = u8::from(self.y_parity.expect("y_parity not set")) + 27; + sig[64] = v; + sig + } + + /// Turn this signature into its hex-encoded representation. + pub fn to_hex_bytes(&self) -> Bytes { + alloy_primitives::hex::encode(self.to_bytes()).into() + } + + /// Calculates a heuristic for the in-memory size of the [Signature]. + #[inline] + pub fn size(&self) -> usize { + std::mem::size_of::() + } +} + /// Type that represents the signature parity byte, meant for use in RPC. /// /// This will be serialized as "0x0" if false, and "0x1" if true. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Copy, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Parity( #[serde(serialize_with = "serialize_parity", deserialize_with = "deserialize_parity")] pub bool, ); @@ -37,6 +146,62 @@ impl From for Parity { } } +impl From for Parity { + fn from(value: U256) -> Self { + match value { + U256::ZERO => Self(false), + _ => Self(true), + } + } +} + +impl From for U256 { + fn from(p: Parity) -> Self { + if p.0 { + U256::from(1) + } else { + U256::ZERO + } + } +} + +impl From for u64 { + fn from(p: Parity) -> Self { + if p.0 { + 1 + } else { + 0 + } + } +} + +impl From for u8 { + fn from(value: Parity) -> Self { + match value.0 { + true => 1, + false => 0, + } + } +} + +impl Encodable for Parity { + fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { + let v = u8::from(*self); + v.encode(out); + } + + fn length(&self) -> usize { + 1 + } +} + +impl Decodable for Parity { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + let v = u8::decode(buf)?; + Ok(Self(v != 0)) + } +} + fn serialize_parity(parity: &bool, serializer: S) -> Result where S: serde::Serializer, diff --git a/crates/rpc-types/src/eth/transaction/tx_type.rs b/crates/rpc-types/src/eth/transaction/tx_type.rs new file mode 100644 index 00000000000..f589f639cb0 --- /dev/null +++ b/crates/rpc-types/src/eth/transaction/tx_type.rs @@ -0,0 +1,52 @@ +use alloy_primitives::U8; +use serde::{Deserialize, Serialize}; + +/// Identifier for legacy transaction, however a legacy tx is technically not +/// typed. +pub const LEGACY_TX_TYPE_ID: u8 = 0; + +/// Identifier for an EIP2930 transaction. +pub const EIP2930_TX_TYPE_ID: u8 = 1; + +/// Identifier for an EIP1559 transaction. +pub const EIP1559_TX_TYPE_ID: u8 = 2; + +/// Identifier for an EIP4844 transaction. +pub const EIP4844_TX_TYPE_ID: u8 = 3; + +/// Transaction Type +/// +/// Currently being used as 2-bit type when encoding it to Compact on +/// crate::TransactionSignedNoHash (see Reth's Compact encoding). Adding more transaction types will +/// break the codec and database format on Reth. +/// +/// Other required changes when adding a new type can be seen on [PR#3953](https://github.com/paradigmxyz/reth/pull/3953/files). +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)] +pub enum TxType { + /// Legacy transaction pre EIP-2929 + #[default] + Legacy = 0_isize, + /// AccessList transaction + EIP2930 = 1_isize, + /// Transaction with Priority fee + EIP1559 = 2_isize, + /// Shard Blob Transactions - EIP-4844 + EIP4844 = 3_isize, +} + +impl From for u8 { + fn from(value: TxType) -> Self { + match value { + TxType::Legacy => LEGACY_TX_TYPE_ID, + TxType::EIP2930 => EIP2930_TX_TYPE_ID, + TxType::EIP1559 => EIP1559_TX_TYPE_ID, + TxType::EIP4844 => EIP4844_TX_TYPE_ID, + } + } +} + +impl From for U8 { + fn from(value: TxType) -> Self { + U8::from(u8::from(value)) + } +} diff --git a/crates/rpc-types/src/eth/transaction/typed.rs b/crates/rpc-types/src/eth/transaction/typed.rs index e529d37b9f9..054a3137fdf 100644 --- a/crates/rpc-types/src/eth/transaction/typed.rs +++ b/crates/rpc-types/src/eth/transaction/typed.rs @@ -3,9 +3,14 @@ //! transaction deserialized from the json input of an RPC call. Depending on what fields are set, //! it can be converted into the container type [`TypedTransactionRequest`]. -use crate::eth::transaction::AccessList; -use alloy_primitives::{Address, Bytes, U128, U256, U64}; -use alloy_rlp::{BufMut, Decodable, Encodable, Error as RlpError}; +use std::{cmp::Ordering, mem}; + +use crate::{eth::transaction::AccessList, Signature, TxType}; +use alloy_primitives::{keccak256, Address, Bytes, B256, U128, U256, U64}; +use alloy_rlp::{ + bytes, length_of_length, Buf, BufMut, Decodable, Encodable, Error as RlpError, Header, + EMPTY_LIST_CODE, +}; use serde::{Deserialize, Serialize}; /// Container type for various Ethereum transaction requests @@ -24,6 +29,78 @@ pub enum TypedTransactionRequest { EIP1559(EIP1559TransactionRequest), } +impl Encodable for TypedTransactionRequest { + fn encode(&self, out: &mut dyn BufMut) { + match self { + // Just encode as such + TypedTransactionRequest::Legacy(tx) => tx.encode(out), + // For EIP2930 and EIP1559 txs, we need to "envelop" the RLP encoding with the tx type. + // For EIP2930, it's 1. + TypedTransactionRequest::EIP2930(tx) => { + let id = 1_u8; + id.encode(out); + tx.encode(out) + } + // For EIP1559, it's 2. + TypedTransactionRequest::EIP1559(tx) => { + let id = 2_u8; + id.encode(out); + tx.encode(out) + } + } + } + + fn length(&self) -> usize { + match self { + TypedTransactionRequest::Legacy(tx) => tx.length(), + TypedTransactionRequest::EIP2930(tx) => tx.length(), + TypedTransactionRequest::EIP1559(tx) => tx.length(), + } + } +} + +impl Decodable for TypedTransactionRequest { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + // First, decode the tx type. + let tx_type = u8::decode(buf)?; + // Then, decode the tx based on the type. + match tx_type.cmp(&EMPTY_LIST_CODE) { + Ordering::Less => { + // strip out the string header + // NOTE: typed transaction encodings either contain a "rlp header" which contains + // the type of the payload and its length, or they do not contain a header and + // start with the tx type byte. + // + // This line works for both types of encodings because byte slices starting with + // 0x01 and 0x02 return a Header { list: false, payload_length: 1 } when input to + // Header::decode. + // If the encoding includes a header, the header will be properly decoded and + // consumed. + // Otherwise, header decoding will succeed but nothing is consumed. + let _header = Header::decode(buf)?; + let tx_type = *buf + .first() + .ok_or(RlpError::Custom("typed tx cannot be decoded from an empty slice"))?; + if tx_type == 0x01 { + buf.advance(1); + EIP2930TransactionRequest::decode(buf).map(TypedTransactionRequest::EIP2930) + } else if tx_type == 0x02 { + buf.advance(1); + EIP1559TransactionRequest::decode(buf).map(TypedTransactionRequest::EIP1559) + } else { + Err(RlpError::Custom("invalid tx type")) + } + } + Ordering::Equal => { + Err(RlpError::Custom("an empty list is not a valid transaction encoding")) + } + Ordering::Greater => { + LegacyTransactionRequest::decode(buf).map(TypedTransactionRequest::Legacy) + } + } + } +} + /// Represents a legacy transaction request #[derive(Debug, Clone, PartialEq, Eq)] pub struct LegacyTransactionRequest { @@ -36,6 +113,131 @@ pub struct LegacyTransactionRequest { pub chain_id: Option, } +impl Encodable for LegacyTransactionRequest { + fn encode(&self, out: &mut dyn BufMut) { + self.nonce.encode(out); + self.gas_price.encode(out); + self.gas_limit.encode(out); + self.kind.encode(out); + self.value.encode(out); + self.input.0.encode(out); + } + + fn length(&self) -> usize { + self.nonce.length() + + self.gas_price.length() + + self.gas_limit.length() + + self.kind.length() + + self.value.length() + + self.input.0.length() + } +} + +impl Decodable for LegacyTransactionRequest { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + Ok(Self { + nonce: Decodable::decode(buf)?, + gas_price: Decodable::decode(buf)?, + gas_limit: Decodable::decode(buf)?, + kind: Decodable::decode(buf)?, + value: Decodable::decode(buf)?, + input: Decodable::decode(buf)?, + chain_id: None, + }) + } +} + +impl LegacyTransactionRequest { + /// Calculates a heuristic for the in-memory size of the [LegacyTransactionRequest] transaction. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::>() + // chain_id + mem::size_of::() + // nonce + mem::size_of::() + // gas_price + mem::size_of::() + // gas_limit + self.kind.size() + // to + mem::size_of::() + // value + self.input.len() // input + } + + /// Outputs the length of the transaction's fields, without a RLP header or length of the + /// eip155 fields. + pub fn fields_len(&self) -> usize { + let mut len = 0; + len += self.nonce.length(); + len += self.gas_price.length(); + len += self.gas_limit.length(); + len += self.kind.length(); + len += self.value.length(); + len += self.input.0.length(); + len + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header or + /// eip155 fields. + pub fn encode_fields(&self, out: &mut dyn bytes::BufMut) { + self.nonce.encode(out); + self.gas_price.encode(out); + self.gas_limit.encode(out); + self.kind.encode(out); + self.value.encode(out); + self.input.0.encode(out); + } + + /// Outputs the length of EIP-155 fields. Only outputs a non-zero value for EIP-155 legacy + /// transactions. + pub fn eip155_fields_len(&self) -> usize { + if let Some(id) = self.chain_id { + // EIP-155 encodes the chain ID and two zeroes, so we add 2 to the length of the chain + // ID to get the length of all 3 fields + // len(chain_id) + (0x00) + (0x00) + id.length() + 2 + } else { + // this is either a pre-EIP-155 legacy transaction or a typed transaction + 0 + } + } + + /// Encodes EIP-155 arguments into the desired buffer. Only encodes values for legacy + /// transactions. + pub fn encode_eip155_fields(&self, out: &mut dyn bytes::BufMut) { + // if this is a legacy transaction without a chain ID, it must be pre-EIP-155 + // and does not need to encode the chain ID for the signature hash encoding + if let Some(id) = self.chain_id { + // EIP-155 encodes the chain ID and two zeroes + id.encode(out); + 0x00u8.encode(out); + 0x00u8.encode(out); + } + } + + /// Encodes the legacy transaction in RLP for signing, including the EIP-155 fields if possible. + pub fn encode_for_signing(&self, out: &mut dyn bytes::BufMut) { + Header { list: true, payload_length: self.fields_len() + self.eip155_fields_len() } + .encode(out); + self.encode_fields(out); + self.encode_eip155_fields(out); + } + + /// Outputs the length of the signature RLP encoding for the transaction, including the length + /// of the EIP-155 fields if possible. + pub fn payload_len_for_signature(&self) -> usize { + let payload_length = self.fields_len() + self.eip155_fields_len(); + // 'header length' + 'payload length' + length_of_length(payload_length) + payload_length + } + + /// Outputs the signature hash of the transaction by first encoding without a signature, then + /// hashing. + /// + /// See [Self::encode_for_signing] for more information on the encoding format. + pub fn signature_hash(&self) -> B256 { + let mut buf = bytes::BytesMut::with_capacity(self.payload_len_for_signature()); + self.encode_for_signing(&mut buf); + keccak256(&buf) + } +} + /// Represents an EIP-2930 transaction request #[derive(Debug, Clone, PartialEq, Eq)] pub struct EIP2930TransactionRequest { @@ -49,6 +251,175 @@ pub struct EIP2930TransactionRequest { pub access_list: AccessList, } +impl Encodable for EIP2930TransactionRequest { + fn encode(&self, out: &mut dyn BufMut) { + self.chain_id.encode(out); + self.nonce.encode(out); + self.gas_price.encode(out); + self.gas_limit.encode(out); + self.kind.encode(out); + self.value.encode(out); + self.input.0.encode(out); + self.access_list.encode(out); + } + + fn length(&self) -> usize { + self.chain_id.length() + + self.nonce.length() + + self.gas_price.length() + + self.gas_limit.length() + + self.kind.length() + + self.value.length() + + self.input.0.length() + + self.access_list.length() + } +} + +impl Decodable for EIP2930TransactionRequest { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + Ok(Self { + chain_id: Decodable::decode(buf)?, + nonce: Decodable::decode(buf)?, + gas_price: Decodable::decode(buf)?, + gas_limit: Decodable::decode(buf)?, + kind: Decodable::decode(buf)?, + value: Decodable::decode(buf)?, + input: Decodable::decode(buf)?, + access_list: Decodable::decode(buf)?, + }) + } +} + +impl EIP2930TransactionRequest { + /// Calculates a heuristic for the in-memory size of the transaction. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::() + // chain_id + mem::size_of::() + // nonce + mem::size_of::() + // gas_price + mem::size_of::() + // gas_limit + self.kind.size() + // to + mem::size_of::() + // value + self.access_list.size() + // access_list + self.input.len() // input + } + + /// Decodes the inner fields from RLP bytes. + /// + /// NOTE: This assumes a RLP header has already been decoded, and _just_ decodes the following + /// RLP fields in the following order: + /// + /// - `chain_id` + /// - `nonce` + /// - `gas_price` + /// - `gas_limit` + /// - `to` + /// - `value` + /// - `data` (`input`) + /// - `access_list` + pub fn decode_inner(buf: &mut &[u8]) -> alloy_rlp::Result { + Ok(Self { + chain_id: Decodable::decode(buf)?, + nonce: Decodable::decode(buf)?, + gas_price: Decodable::decode(buf)?, + gas_limit: Decodable::decode(buf)?, + kind: Decodable::decode(buf)?, + value: Decodable::decode(buf)?, + input: Decodable::decode(buf)?, + access_list: Decodable::decode(buf)?, + }) + } + + /// Outputs the length of the transaction's fields, without a RLP header. + pub fn fields_len(&self) -> usize { + let mut len = 0; + len += self.chain_id.length(); + len += self.nonce.length(); + len += self.gas_price.length(); + len += self.gas_limit.length(); + len += self.kind.length(); + len += self.value.length(); + len += self.input.0.length(); + len += self.access_list.length(); + len + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header. + pub fn encode_fields(&self, out: &mut dyn bytes::BufMut) { + self.chain_id.encode(out); + self.nonce.encode(out); + self.gas_price.encode(out); + self.gas_limit.encode(out); + self.kind.encode(out); + self.value.encode(out); + self.input.0.encode(out); + self.access_list.encode(out); + } + + /// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating + /// hash that for eip2718 does not require rlp header + pub fn encode_with_signature( + &self, + signature: &Signature, + out: &mut dyn bytes::BufMut, + with_header: bool, + ) { + let payload_length = self.fields_len() + signature.payload_len(); + if with_header { + Header { + list: false, + payload_length: 1 + length_of_length(payload_length) + payload_length, + } + .encode(out); + } + out.put_u8(self.tx_type() as u8); + let header = Header { list: true, payload_length }; + header.encode(out); + self.encode_fields(out); + signature.encode(out); + } + + /// Output the length of the RLP signed transaction encoding, _without_ a RLP string header. + pub fn payload_len_with_signature_without_header(&self, signature: &Signature) -> usize { + let payload_length = self.fields_len() + signature.payload_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + /// Output the length of the RLP signed transaction encoding. This encodes with a RLP header. + pub fn payload_len_with_signature(&self, signature: &Signature) -> usize { + let len = self.payload_len_with_signature_without_header(signature); + length_of_length(len) + len + } + + /// Get transaction type + pub fn tx_type(&self) -> TxType { + TxType::EIP2930 + } + + /// Encodes the EIP-2930 transaction in RLP for signing. + pub fn encode_for_signing(&self, out: &mut dyn bytes::BufMut) { + out.put_u8(self.tx_type() as u8); + Header { list: true, payload_length: self.fields_len() }.encode(out); + self.encode_fields(out); + } + + /// Outputs the length of the signature RLP encoding for the transaction. + pub fn payload_len_for_signature(&self) -> usize { + let payload_length = self.fields_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + /// Outputs the signature hash of the transaction by first encoding without a signature, then + /// hashing. + pub fn signature_hash(&self) -> B256 { + let mut buf = bytes::BytesMut::with_capacity(self.payload_len_for_signature()); + self.encode_for_signing(&mut buf); + keccak256(&buf) + } +} + /// Represents an EIP-1559 transaction request #[derive(Debug, Clone, PartialEq, Eq)] pub struct EIP1559TransactionRequest { @@ -63,6 +434,183 @@ pub struct EIP1559TransactionRequest { pub access_list: AccessList, } +impl Encodable for EIP1559TransactionRequest { + fn encode(&self, out: &mut dyn BufMut) { + self.chain_id.encode(out); + self.nonce.encode(out); + self.max_priority_fee_per_gas.encode(out); + self.max_fee_per_gas.encode(out); + self.gas_limit.encode(out); + self.kind.encode(out); + self.value.encode(out); + self.input.0.encode(out); + self.access_list.encode(out); + } + + fn length(&self) -> usize { + self.chain_id.length() + + self.nonce.length() + + self.max_priority_fee_per_gas.length() + + self.max_fee_per_gas.length() + + self.gas_limit.length() + + self.kind.length() + + self.value.length() + + self.input.0.length() + + self.access_list.length() + } +} + +impl Decodable for EIP1559TransactionRequest { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + Ok(Self { + chain_id: Decodable::decode(buf)?, + nonce: Decodable::decode(buf)?, + max_priority_fee_per_gas: Decodable::decode(buf)?, + max_fee_per_gas: Decodable::decode(buf)?, + gas_limit: Decodable::decode(buf)?, + kind: Decodable::decode(buf)?, + value: Decodable::decode(buf)?, + input: Decodable::decode(buf)?, + access_list: Decodable::decode(buf)?, + }) + } +} + +impl EIP1559TransactionRequest { + /// Decodes the inner fields from RLP bytes. + /// + /// NOTE: This assumes a RLP header has already been decoded, and _just_ decodes the following + /// RLP fields in the following order: + /// + /// - `chain_id` + /// - `nonce` + /// - `max_priority_fee_per_gas` + /// - `max_fee_per_gas` + /// - `gas_limit` + /// - `to` + /// - `value` + /// - `data` (`input`) + /// - `access_list` + pub fn decode_inner(buf: &mut &[u8]) -> alloy_rlp::Result { + Ok(Self { + chain_id: Decodable::decode(buf)?, + nonce: Decodable::decode(buf)?, + max_priority_fee_per_gas: Decodable::decode(buf)?, + max_fee_per_gas: Decodable::decode(buf)?, + gas_limit: Decodable::decode(buf)?, + kind: Decodable::decode(buf)?, + value: Decodable::decode(buf)?, + input: Decodable::decode(buf)?, + access_list: Decodable::decode(buf)?, + }) + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header. + pub fn fields_len(&self) -> usize { + let mut len = 0; + len += self.chain_id.length(); + len += self.nonce.length(); + len += self.max_priority_fee_per_gas.length(); + len += self.max_fee_per_gas.length(); + len += self.gas_limit.length(); + len += self.kind.length(); + len += self.value.length(); + len += self.input.0.length(); + len += self.access_list.length(); + len + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header. + pub fn encode_fields(&self, out: &mut dyn bytes::BufMut) { + self.chain_id.encode(out); + self.nonce.encode(out); + self.max_priority_fee_per_gas.encode(out); + self.max_fee_per_gas.encode(out); + self.gas_limit.encode(out); + self.kind.encode(out); + self.value.encode(out); + self.input.0.encode(out); + self.access_list.encode(out); + } + + /// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating + /// hash that for eip2718 does not require rlp header + pub fn encode_with_signature( + &self, + signature: &Signature, + out: &mut dyn bytes::BufMut, + with_header: bool, + ) { + let payload_length = self.fields_len() + signature.payload_len(); + if with_header { + Header { + list: false, + payload_length: 1 + length_of_length(payload_length) + payload_length, + } + .encode(out); + } + out.put_u8(self.tx_type() as u8); + let header = Header { list: true, payload_length }; + header.encode(out); + self.encode_fields(out); + signature.encode(out); + } + + /// Output the length of the RLP signed transaction encoding, _without_ a RLP string header. + pub fn payload_len_with_signature_without_header(&self, signature: &Signature) -> usize { + let payload_length = self.fields_len() + signature.payload_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + /// Output the length of the RLP signed transaction encoding. This encodes with a RLP header. + pub fn payload_len_with_signature(&self, signature: &Signature) -> usize { + let len = self.payload_len_with_signature_without_header(signature); + length_of_length(len) + len + } + + /// Get transaction type + pub fn tx_type(&self) -> TxType { + TxType::EIP1559 + } + + /// Calculates a heuristic for the in-memory size of the transaction. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::() + // chain_id + mem::size_of::() + // nonce + mem::size_of::() + // gas_limit + mem::size_of::() + // max_fee_per_gas + mem::size_of::() + // max_priority_fee_per_gas + self.kind.size() + // to + mem::size_of::() + // value + self.access_list.size() + // access_list + self.input.len() // input + } + + /// Encodes the legacy transaction in RLP for signing. + pub fn encode_for_signing(&self, out: &mut dyn bytes::BufMut) { + out.put_u8(self.tx_type() as u8); + Header { list: true, payload_length: self.fields_len() }.encode(out); + self.encode_fields(out); + } + + /// Outputs the length of the signature RLP encoding for the transaction. + pub fn payload_len_for_signature(&self) -> usize { + let payload_length = self.fields_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + /// Outputs the signature hash of the transaction by first encoding without a signature, then + /// hashing. + pub fn signature_hash(&self) -> B256 { + let mut buf = bytes::BytesMut::with_capacity(self.payload_len_for_signature()); + self.encode_for_signing(&mut buf); + keccak256(&buf) + } +} + /// Represents the `to` field of a transaction request /// /// This determines what kind of transaction this is @@ -84,6 +632,12 @@ impl TransactionKind { TransactionKind::Create => None, } } + + /// Calculates a heuristic for the in-memory size of the [TransactionKind]. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::() + } } impl Encodable for TransactionKind {