diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8051f5b0ed9..1e79538bade 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,4 +3,4 @@ crates/json-rpc @prestwich crates/transports @evalir @prestwich crates/networks @prestwich -crates/providers @evalir @prestwich +crates/provider @evalir @prestwich diff --git a/.github/ISSUE_TEMPLATE/BUG-FORM.yml b/.github/ISSUE_TEMPLATE/BUG-FORM.yml index 7fddd64437a..c4b17f9b2d6 100644 --- a/.github/ISSUE_TEMPLATE/BUG-FORM.yml +++ b/.github/ISSUE_TEMPLATE/BUG-FORM.yml @@ -18,7 +18,7 @@ body: - consensus, eips, genesis - network, json-rpc - nodes - - providers, pubsub + - provider, pubsub - rpc - signers - transports diff --git a/.github/ISSUE_TEMPLATE/FEATURE-FORM.yml b/.github/ISSUE_TEMPLATE/FEATURE-FORM.yml index 1c5f929cbef..2de733ddec7 100644 --- a/.github/ISSUE_TEMPLATE/FEATURE-FORM.yml +++ b/.github/ISSUE_TEMPLATE/FEATURE-FORM.yml @@ -16,7 +16,7 @@ body: - consensus, eips, genesis - network, json-rpc - nodes - - providers, pubsub + - provider, pubsub - rpc - signers - transports diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8e64e23dd9..ce4ec5ad705 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,12 +20,12 @@ jobs: strategy: fail-fast: false matrix: - rust: ["stable", "nightly", "1.75"] # MSRV - flags: ["--no-default-features", "", "--all-features"] + rust: ['stable', 'nightly', '1.75'] # MSRV + flags: ['--no-default-features', '', '--all-features'] exclude: # Some features have higher MSRV. - - rust: "1.75" # MSRV - flags: "--all-features" + - rust: '1.75' # MSRV + flags: '--all-features' steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@master @@ -69,7 +69,7 @@ jobs: cargo hack check --workspace --target wasm32-unknown-unknown \ --exclude alloy-contract \ --exclude alloy-node-bindings \ - --exclude alloy-providers \ + --exclude alloy-provider \ --exclude alloy-signer \ --exclude alloy-signer-aws \ --exclude alloy-signer-gcp \ diff --git a/Cargo.toml b/Cargo.toml index 8c766579c3b..56e0422adf8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ alloy-genesis = { version = "0.1.0", default-features = false, path = "crates/ge alloy-json-rpc = { version = "0.1.0", default-features = false, path = "crates/json-rpc" } alloy-network = { version = "0.1.0", default-features = false, path = "crates/network" } alloy-node-bindings = { version = "0.1.0", default-features = false, path = "crates/node-bindings" } -alloy-providers = { version = "0.1.0", default-features = false, path = "crates/providers" } +alloy-provider = { version = "0.1.0", default-features = false, path = "crates/provider" } alloy-pubsub = { version = "0.1.0", default-features = false, path = "crates/pubsub" } alloy-rpc-client = { version = "0.1.0", default-features = false, path = "crates/rpc-client" } alloy-rpc-engine-types = { version = "0.1.0", default-features = false, path = "crates/rpc-engine-types" } diff --git a/README.md b/README.md index daf33a91787..781d2faf8c8 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ This repository contains the following crates: - [`alloy-json-rpc`] - Core data types for JSON-RPC 2.0 clients - [`alloy-network`] - Network abstraction for RPC types - [`alloy-node-bindings`] - Ethereum execution-layer client bindings -- [`alloy-providers`] - Interface with an Ethereum blockchain +- [`alloy-provider`] - Interface with an Ethereum blockchain - [`alloy-pubsub`] - Ethereum JSON-RPC [publish-subscribe] tower service and type definitions - [`alloy-rpc-client`] - Low-level Ethereum JSON-RPC client implementation - [`alloy-rpc-types`] - Ethereum JSON-RPC types @@ -50,7 +50,7 @@ This repository contains the following crates: [`alloy-json-rpc`]: crates/json-rpc [`alloy-network`]: crates/network [`alloy-node-bindings`]: crates/node-bindings -[`alloy-providers`]: crates/providers +[`alloy-provider`]: crates/provider [`alloy-pubsub`]: crates/pubsub [`alloy-rpc-client`]: crates/rpc-client [`alloy-rpc-engine-types`]: crates/rpc-engine-types @@ -65,7 +65,6 @@ This repository contains the following crates: [`alloy-transport-http`]: crates/transport-http [`alloy-transport-ipc`]: crates/transport-ipc [`alloy-transport-ws`]: crates/transport-ws - [`alloy-core`]: https://docs.rs/alloy-core [publish-subscribe]: https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern [AWS KMS]: https://aws.amazon.com/kms diff --git a/crates/alloy/Cargo.toml b/crates/alloy/Cargo.toml index 1588a33857a..50d9291c575 100644 --- a/crates/alloy/Cargo.toml +++ b/crates/alloy/Cargo.toml @@ -20,7 +20,7 @@ alloy-eips = { workspace = true, default-features = false, optional = true } alloy-genesis = { workspace = true, default-features = false, optional = true } alloy-network = { workspace = true, default-features = false, optional = true } alloy-node-bindings = { workspace = true, default-features = false, optional = true } -alloy-providers = { workspace = true, default-features = false, optional = true } +alloy-provider = { workspace = true, default-features = false, optional = true } alloy-pubsub = { workspace = true, default-features = false, optional = true } # rpc @@ -92,7 +92,7 @@ node-bindings = ["dep:alloy-node-bindings"] contract = ["dep:alloy-contract", "dyn-abi", "json-abi", "json", "sol-types"] ## providers -providers = ["dep:alloy-providers"] +providers = ["dep:alloy-provider"] provider-http = ["providers", "transport-http"] provider-ws = ["providers", "transport-ws"] provider-ipc = ["providers", "transport-ipc"] diff --git a/crates/alloy/src/lib.rs b/crates/alloy/src/lib.rs index b5e78402a78..5441e8a4d88 100644 --- a/crates/alloy/src/lib.rs +++ b/crates/alloy/src/lib.rs @@ -96,11 +96,11 @@ pub use alloy_node_bindings as node_bindings; /// Interface with an Ethereum blockchain. /// -/// See [`alloy_providers`] for more details. +/// See [`alloy_provider`] for more details. #[cfg(feature = "providers")] pub mod providers { #[doc(inline)] - pub use alloy_providers::*; + pub use alloy_provider::*; // TODO: provider type aliases // #[cfg(feature = "provider-http")] diff --git a/crates/contract/Cargo.toml b/crates/contract/Cargo.toml index 23bb2f17f2c..4bfe630441e 100644 --- a/crates/contract/Cargo.toml +++ b/crates/contract/Cargo.toml @@ -12,7 +12,7 @@ repository.workspace = true exclude.workspace = true [dependencies] -alloy-providers.workspace = true +alloy-provider.workspace = true alloy-rpc-types.workspace = true alloy-transport.workspace = true diff --git a/crates/contract/src/call.rs b/crates/contract/src/call.rs index ef5e1b6ac0b..0c0d3999057 100644 --- a/crates/contract/src/call.rs +++ b/crates/contract/src/call.rs @@ -2,7 +2,7 @@ use crate::{Error, Result}; use alloy_dyn_abi::{DynSolValue, FunctionExt, JsonAbiExt}; use alloy_json_abi::Function; use alloy_primitives::{Address, Bytes, U256, U64}; -use alloy_providers::tmp::TempProvider; +use alloy_provider::tmp::TempProvider; use alloy_rpc_types::{ request::{TransactionInput, TransactionRequest}, state::StateOverride, @@ -456,7 +456,7 @@ mod tests { use super::*; use alloy_node_bindings::{Anvil, AnvilInstance}; use alloy_primitives::{address, b256, bytes, hex}; - use alloy_providers::tmp::{HttpProvider, Provider}; + use alloy_provider::tmp::{HttpProvider, Provider}; use alloy_sol_types::sol; #[test] diff --git a/crates/contract/src/instance.rs b/crates/contract/src/instance.rs index e94ee9d9cae..3f6b605d390 100644 --- a/crates/contract/src/instance.rs +++ b/crates/contract/src/instance.rs @@ -2,7 +2,7 @@ use crate::{CallBuilder, Interface, Result}; use alloy_dyn_abi::DynSolValue; use alloy_json_abi::{Function, JsonAbi}; use alloy_primitives::{Address, Selector}; -use alloy_providers::tmp::TempProvider; +use alloy_provider::tmp::TempProvider; /// A handle to an Ethereum contract at a specific address. /// diff --git a/crates/contract/src/lib.rs b/crates/contract/src/lib.rs index 64561fdce9b..07dc8a594e6 100644 --- a/crates/contract/src/lib.rs +++ b/crates/contract/src/lib.rs @@ -34,5 +34,5 @@ pub use call::*; // NOTE: please avoid changing the API of this module due to its use in the `sol!` macro. #[doc(hidden)] pub mod private { - pub use alloy_providers::tmp::TempProvider as Provider; + pub use alloy_provider::tmp::TempProvider as Provider; } diff --git a/crates/network/README.md b/crates/network/README.md index e9ff1d6f6fe..c504858fb35 100644 --- a/crates/network/README.md +++ b/crates/network/README.md @@ -22,7 +22,7 @@ networking. The core model is as follows: ## Usage This crate is not intended to be used directly. It is used by the -[alloy-providers] library and reth to modify the input and output types of the +[alloy-provider] library and reth to modify the input and output types of the RPC methods. This crate will primarily be used by blockchain maintainers to add bespoke RPC @@ -61,4 +61,4 @@ trait FooProviderExt: Provider { } ``` -[alloy-providers]: ../providers +[alloy-provider]: ../provider diff --git a/crates/providers/Cargo.toml b/crates/provider/Cargo.toml similarity index 97% rename from crates/providers/Cargo.toml rename to crates/provider/Cargo.toml index b97c0958f9b..2b23ce766c2 100644 --- a/crates/providers/Cargo.toml +++ b/crates/provider/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "alloy-providers" +name = "alloy-provider" description = "Interface with an Ethereum blockchain" version.workspace = true diff --git a/crates/providers/README.md b/crates/provider/README.md similarity index 98% rename from crates/providers/README.md rename to crates/provider/README.md index 53efa430fef..15a80efc64f 100644 --- a/crates/providers/README.md +++ b/crates/provider/README.md @@ -1,4 +1,4 @@ -# alloy-providers +# alloy-provider diff --git a/crates/providers/src/builder.rs b/crates/provider/src/builder.rs similarity index 100% rename from crates/providers/src/builder.rs rename to crates/provider/src/builder.rs diff --git a/crates/providers/src/chain.rs b/crates/provider/src/chain.rs similarity index 100% rename from crates/providers/src/chain.rs rename to crates/provider/src/chain.rs diff --git a/crates/providers/src/heart.rs b/crates/provider/src/heart.rs similarity index 100% rename from crates/providers/src/heart.rs rename to crates/provider/src/heart.rs diff --git a/crates/providers/src/lib.rs b/crates/provider/src/lib.rs similarity index 100% rename from crates/providers/src/lib.rs rename to crates/provider/src/lib.rs diff --git a/crates/providers/src/new.rs b/crates/provider/src/new.rs similarity index 100% rename from crates/providers/src/new.rs rename to crates/provider/src/new.rs diff --git a/crates/providers/src/tmp.rs b/crates/provider/src/provider.rs similarity index 100% rename from crates/providers/src/tmp.rs rename to crates/provider/src/provider.rs diff --git a/crates/provider/src/tmp.rs b/crates/provider/src/tmp.rs new file mode 100644 index 00000000000..25c054e52dd --- /dev/null +++ b/crates/provider/src/tmp.rs @@ -0,0 +1,790 @@ +//! Alloy main Provider abstraction. + +use crate::utils::{self, EstimatorFunction}; +use alloy_primitives::{Address, BlockHash, Bytes, StorageKey, StorageValue, TxHash, U256, U64}; +use alloy_rpc_client::{ClientBuilder, RpcClient}; +use alloy_rpc_trace_types::{ + geth::{GethDebugTracingOptions, GethTrace}, + parity::LocalizedTransactionTrace, +}; +use alloy_rpc_types::{ + request::TransactionRequest, state::StateOverride, AccessListWithGasUsed, Block, BlockId, + BlockNumberOrTag, 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::{de::DeserializeOwned, Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Debug, Error, Serialize, Deserialize)] +pub enum ClientError { + #[error("Could not parse URL")] + ParseError, + #[error("Unsupported Tag")] + UnsupportedBlockIdError, +} + +/// Type alias for a [`Provider`] using the [`Http`] transport. +pub type HttpProvider = Provider>; + +/// An abstract provider for interacting with the [Ethereum JSON RPC +/// API](https://github.com/ethereum/wiki/wiki/JSON-RPC). Must be instantiated +/// with a transport which implements the [Transport] trait. +#[derive(Debug, Clone)] +pub struct Provider { + inner: RpcClient, + 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 client version of the chain client. + async fn get_client_version(&self) -> TransportResult; + + /// Gets the chain ID. + async fn get_chain_id(&self) -> TransportResult; + + /// Gets the network ID. Same as `eth_chainId`. + async fn get_net_version(&self) -> TransportResult; + + /// Gets the specified storage value from [Address]. + async fn get_storage_at( + &self, + address: Address, + key: U256, + tag: Option, + ) -> TransportResult; + + /// Gets the bytecode located at the corresponding [Address]. + async fn get_code_at(&self, address: Address, tag: Option) -> 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 [TransactionRequest] without publishing a transaction. + async fn call(&self, tx: TransactionRequest, block: Option) -> TransportResult; + + /// Execute a smart contract call with [TransactionRequest] and state overrides, without + /// publishing a transaction. + /// + /// # Note + /// + /// Not all client implementations support state overrides. + async fn call_with_overrides( + &self, + tx: TransactionRequest, + block: Option, + state: StateOverride, + ) -> TransportResult; + + /// Estimate the gas needed for a transaction. + async fn estimate_gas( + &self, + tx: TransactionRequest, + 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: TransactionRequest, + 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. +#[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. + async fn get_transaction_count( + &self, + address: Address, + tag: Option, + ) -> TransportResult { + self.inner.prepare("eth_getTransactionCount", (address, tag.unwrap_or_default())).await + } + + /// Gets the last block number available. + /// 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. + async fn get_balance(&self, address: Address, tag: Option) -> TransportResult { + self.inner + .prepare( + "eth_getBalance", + (address, tag.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest))), + ) + .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> { + self.inner.prepare("eth_getBlockByHash", (hash, full)).await + } + + /// Gets a block by [BlockNumberOrTag], with full transactions or only hashes. + async fn get_block_by_number( + &self, + number: BlockNumberOrTag, + full: bool, + ) -> TransportResult> { + self.inner.prepare("eth_getBlockByNumber", (number, full)).await + } + + /// Gets the client version of the chain client. + async fn get_client_version(&self) -> TransportResult { + self.inner.prepare("web3_clientVersion", ()).await + } + + /// Gets the chain ID. + async fn get_chain_id(&self) -> TransportResult { + self.inner.prepare("eth_chainId", ()).await + } + + async fn get_net_version(&self) -> TransportResult { + self.inner.prepare("net_version", ()).await + } + + /// Gets the specified storage value from [Address]. + async fn get_storage_at( + &self, + address: Address, + key: U256, + tag: Option, + ) -> TransportResult { + self.inner.prepare("eth_getStorageAt", (address, key, tag.unwrap_or_default())).await + } + + /// Gets the bytecode located at the corresponding [Address]. + async fn get_code_at(&self, address: Address, tag: Option) -> TransportResult { + self.inner.prepare("eth_getCode", (address, tag.unwrap_or_default())).await + } + + /// Gets a [Transaction] by its [TxHash]. + async fn get_transaction_by_hash(&self, hash: TxHash) -> TransportResult { + self.inner.prepare("eth_getTransactionByHash", (hash,)).await + } + + /// Retrieves a [`Vec`] with the given [Filter]. + async fn get_logs(&self, filter: Filter) -> TransportResult> { + self.inner.prepare("eth_getLogs", (filter,)).await + } + + /// Gets the accounts in the remote node. This is usually empty unless you're using a local + /// node. + async fn get_accounts(&self) -> TransportResult> { + self.inner.prepare("eth_accounts", ()).await + } + + /// Gets the current gas price. + async fn get_gas_price(&self) -> TransportResult { + self.inner.prepare("eth_gasPrice", ()).await + } + + /// Gets a [TransactionReceipt] if it exists, by its [TxHash]. + async fn get_transaction_receipt( + &self, + hash: TxHash, + ) -> TransportResult> { + 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`. + async fn get_fee_history( + &self, + block_count: U256, + last_block: BlockNumberOrTag, + reward_percentiles: &[f64], + ) -> TransportResult { + self.inner.prepare("eth_feeHistory", (block_count, last_block, reward_percentiles)).await + } + + /// Gets the selected block [BlockNumberOrTag] receipts. + async fn get_block_receipts( + &self, + block: BlockNumberOrTag, + ) -> TransportResult>> { + self.inner.prepare("eth_getBlockReceipts", (block,)).await + } + + /// Gets an uncle block through the tag [BlockId] and index [U64]. + async fn get_uncle(&self, tag: BlockId, idx: U64) -> TransportResult> { + match tag { + BlockId::Hash(hash) => { + self.inner.prepare("eth_getUncleByBlockHashAndIndex", (hash, idx)).await + } + BlockId::Number(number) => { + self.inner.prepare("eth_getUncleByBlockNumberAndIndex", (number, idx)).await + } + } + } + + /// Gets syncing info. + async fn syncing(&self) -> TransportResult { + self.inner.prepare("eth_syncing", ()).await + } + + /// Execute a smart contract call with [TransactionRequest] without publishing a transaction. + async fn call(&self, tx: TransactionRequest, block: Option) -> TransportResult { + self.inner.prepare("eth_call", (tx, block.unwrap_or_default())).await + } + + /// Execute a smart contract call with [TransactionRequest] and state overrides, without + /// publishing a transaction. + /// + /// # Note + /// + /// Not all client implementations support state overrides. + async fn call_with_overrides( + &self, + tx: TransactionRequest, + block: Option, + state: StateOverride, + ) -> TransportResult { + self.inner.prepare("eth_call", (tx, block.unwrap_or_default(), state)).await + } + + /// Estimate the gas needed for a transaction. + async fn estimate_gas( + &self, + tx: TransactionRequest, + block: Option, + ) -> TransportResult { + if let Some(block_id) = block { + self.inner.prepare("eth_estimateGas", (tx, block_id)).await + } else { + self.inner.prepare("eth_estimateGas", (tx,)).await + } + } + + /// Sends an already-signed transaction. + 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. + async fn estimate_eip1559_fees( + &self, + estimator: Option, + ) -> TransportResult<(U256, U256)> { + let base_fee_per_gas = match self.get_block_by_number(BlockNumberOrTag::Latest, false).await + { + Ok(Some(block)) => match block.header.base_fee_per_gas { + Some(base_fee_per_gas) => base_fee_per_gas, + None => return Err(TransportErrorKind::custom_str("EIP-1559 not activated")), + }, + + Ok(None) => return Err(TransportErrorKind::custom_str("Latest block not found")), + + Err(err) => return Err(err), + }; + + let fee_history = match self + .get_fee_history( + U256::from(utils::EIP1559_FEE_ESTIMATION_PAST_BLOCKS), + BlockNumberOrTag::Latest, + &[utils::EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE], + ) + .await + { + Ok(fee_history) => fee_history, + Err(err) => return Err(err), + }; + + // use the provided fee estimator function, or fallback to the default implementation. + let (max_fee_per_gas, max_priority_fee_per_gas) = if let Some(es) = estimator { + es(base_fee_per_gas, fee_history.reward.unwrap_or_default()) + } else { + utils::eip1559_default_estimator( + base_fee_per_gas, + fee_history.reward.unwrap_or_default(), + ) + }; + + Ok((max_fee_per_gas, max_priority_fee_per_gas)) + } + + async fn get_proof( + &self, + address: Address, + keys: Vec, + block: Option, + ) -> TransportResult { + self.inner.prepare("eth_getProof", (address, keys, block.unwrap_or_default())).await + } + + async fn create_access_list( + &self, + request: TransactionRequest, + block: Option, + ) -> TransportResult { + self.inner.prepare("eth_createAccessList", (request, block.unwrap_or_default())).await + } + + /// Parity trace transaction. + async fn trace_transaction( + &self, + hash: TxHash, + ) -> TransportResult> { + self.inner.prepare("trace_transaction", (hash,)).await + } + + async fn debug_trace_transaction( + &self, + hash: TxHash, + trace_options: GethDebugTracingOptions, + ) -> TransportResult { + self.inner.prepare("debug_traceTransaction", (hash, trace_options)).await + } + + async fn trace_block( + &self, + block: BlockNumberOrTag, + ) -> TransportResult> { + self.inner.prepare("trace_block", (block,)).await + } + + /// 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(url: &str) -> Result { + let url = url.parse().map_err(|_e| ClientError::ParseError)?; + let inner = ClientBuilder::default().reqwest_http(url); + + Ok(Self { inner, from: None }) + } +} + +impl TryFrom for Provider> { + type Error = ClientError; + + fn try_from(value: String) -> Result { + Provider::try_from(value.as_str()) + } +} + +impl<'a> TryFrom<&'a String> for Provider> { + type Error = ClientError; + + fn try_from(value: &'a String) -> Result { + Provider::try_from(value.as_str()) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + tmp::{Provider, TempProvider}, + utils, + }; + use alloy_node_bindings::Anvil; + use alloy_primitives::{address, b256, bytes, U256, U64}; + use alloy_rpc_types::{Block, BlockNumberOrTag, Filter}; + + #[tokio::test] + async fn gets_block_number() { + let anvil = Anvil::new().spawn(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); + let num = provider.get_block_number().await.unwrap(); + 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::try_from(&anvil.endpoint()).unwrap(); + let count = provider + .get_transaction_count( + address!("328375e18E7db8F1CA9d9bA8bF3E9C94ee34136A"), + Some(BlockNumberOrTag::Latest.into()), + ) + .await + .unwrap(); + assert_eq!(count, U256::from(0)); + } + + #[tokio::test] + async fn gets_block_by_hash() { + 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 = provider.get_block_by_hash(hash, true).await.unwrap().unwrap(); + 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::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(); + assert_eq!(block.header.number.unwrap(), U256::from(num)); + } + + #[tokio::test] + async fn gets_block_by_number() { + 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(); + assert_eq!(block.header.number.unwrap(), U256::from(num)); + } + + #[tokio::test] + async fn gets_client_version() { + let anvil = Anvil::new().spawn(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); + let version = provider.get_client_version().await.unwrap(); + assert!(version.contains("anvil")); + } + + #[tokio::test] + async fn gets_chain_id() { + let chain_id: u64 = 13371337; + let anvil = Anvil::new().args(["--chain-id", chain_id.to_string().as_str()]).spawn(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); + let chain_id = provider.get_chain_id().await.unwrap(); + assert_eq!(chain_id, U64::from(chain_id)); + } + + #[tokio::test] + async fn gets_network_id() { + let chain_id: u64 = 13371337; + let anvil = Anvil::new().args(["--chain-id", chain_id.to_string().as_str()]).spawn(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); + let chain_id = provider.get_net_version().await.unwrap(); + assert_eq!(chain_id, U64::from(chain_id)); + } + + #[tokio::test] + #[cfg(feature = "anvil")] + async fn gets_code_at() { + let anvil = Anvil::new().spawn(); + 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(); + let code = provider.get_code_at(addr, None).await.unwrap(); + assert_eq!(code, bytes!("beef")); + } + + #[tokio::test] + async fn gets_storage_at() { + let anvil = Anvil::new().spawn(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); + let addr = alloy_primitives::Address::with_last_byte(16); + let storage = provider.get_storage_at(addr, U256::ZERO, None).await.unwrap(); + assert_eq!(storage, U256::ZERO); + } + + #[tokio::test] + #[ignore] + async fn gets_transaction_by_hash() { + let anvil = Anvil::new().spawn(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); + let tx = provider + .get_transaction_by_hash(b256!( + "5c03fab9114ceb98994b43892ade87ddfd9ae7e8f293935c3bd29d435dc9fd95" + )) + .await + .unwrap(); + assert_eq!( + tx.block_hash.unwrap(), + b256!("b20e6f35d4b46b3c4cd72152faec7143da851a0dc281d390bdd50f58bfbdb5d3") + ); + assert_eq!(tx.block_number.unwrap(), U256::from(4571819)); + } + + #[tokio::test] + #[ignore] + async fn gets_logs() { + let anvil = Anvil::new().spawn(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); + let filter = Filter::new() + .at_block_hash(b256!( + "b20e6f35d4b46b3c4cd72152faec7143da851a0dc281d390bdd50f58bfbdb5d3" + )) + .event_signature(b256!( + "e1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c" + )); + let logs = provider.get_logs(filter).await.unwrap(); + assert_eq!(logs.len(), 1); + } + + #[tokio::test] + #[ignore] + async fn gets_tx_receipt() { + let anvil = Anvil::new().spawn(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); + let receipt = provider + .get_transaction_receipt(b256!( + "5c03fab9114ceb98994b43892ade87ddfd9ae7e8f293935c3bd29d435dc9fd95" + )) + .await + .unwrap(); + assert!(receipt.is_some()); + let receipt = receipt.unwrap(); + assert_eq!( + receipt.transaction_hash.unwrap(), + b256!("5c03fab9114ceb98994b43892ade87ddfd9ae7e8f293935c3bd29d435dc9fd95") + ); + } + + #[tokio::test] + async fn gets_fee_history() { + let anvil = Anvil::new().spawn(); + 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), + &[utils::EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE], + ) + .await + .unwrap(); + assert_eq!(fee_history.oldest_block, U256::ZERO); + } + + #[tokio::test] + #[ignore] // Anvil has yet to implement the `eth_getBlockReceipts` method. + async fn gets_block_receipts() { + let anvil = Anvil::new().spawn(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); + let receipts = provider.get_block_receipts(BlockNumberOrTag::Latest).await.unwrap(); + assert!(receipts.is_some()); + } + + #[tokio::test] + async fn gets_block_traces() { + let anvil = Anvil::new().spawn(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); + let traces = provider.trace_block(BlockNumberOrTag::Latest).await.unwrap(); + assert_eq!(traces.len(), 0); + } + + #[tokio::test] + async fn sends_raw_transaction() { + let anvil = Anvil::new().spawn(); + let provider = Provider::try_from(&anvil.endpoint()).unwrap(); + let tx_hash = provider + .send_raw_transaction( + // Transfer 1 ETH from default EOA address to the Genesis address. + bytes!("f865808477359400825208940000000000000000000000000000000000000000018082f4f5a00505e227c1c636c76fac55795db1a40a4d24840d81b40d2fe0cc85767f6bd202a01e91b437099a8a90234ac5af3cb7ca4fb1432e133f75f9a91678eaf5f487c74b") + ) + .await.unwrap(); + assert_eq!( + tx_hash.to_string(), + "0x9dae5cf33694a02e8a7d5de3fe31e9d05ca0ba6e9180efac4ab20a06c9e598a3" + ); + } +} diff --git a/crates/providers/src/utils.rs b/crates/provider/src/utils.rs similarity index 100% rename from crates/providers/src/utils.rs rename to crates/provider/src/utils.rs diff --git a/crates/transport/README.md b/crates/transport/README.md index e74408027e3..03077191e34 100644 --- a/crates/transport/README.md +++ b/crates/transport/README.md @@ -9,7 +9,7 @@ This crate handles RPC connection and request management. It builds an futures for simple and batch RPC requests as well as a unified `TransportError` type. -[alloy-providers]: ../providers/ +[alloy-provider]: ../provider/ [tower `Service`]: https://docs.rs/tower/latest/tower/trait.Service.html ## Usage