diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0e64cd59ce..6128b7074ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,15 +18,29 @@ jobs: matrix: rust: ["stable", "beta", "nightly", "1.65"] # MSRV flags: ["--no-default-features", "", "--all-features"] + exclude: + # Skip because some features have highest MSRV. + - rust: "1.65" # MSRV + flags: "--all-features" steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ matrix.rust }} - - uses: Swatinem/rust-cache@v2 - - name: test - run: cargo test --workspace ${{ matrix.flags }} - + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + - name: Install Anvil + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + # Only run tests on latest stable and above + - name: build + if: ${{ matrix.rust == '1.65' }} # MSRV + run: cargo build --workspace ${{ matrix.flags }} + - name: test + if: ${{ matrix.rust != '1.65' }} # MSRV + run: cargo test --workspace ${{ matrix.flags }} wasm: name: check WASM runs-on: ubuntu-latest diff --git a/crates/json-rpc/src/packet.rs b/crates/json-rpc/src/packet.rs index 308d08757d6..75c6a2c717d 100644 --- a/crates/json-rpc/src/packet.rs +++ b/crates/json-rpc/src/packet.rs @@ -105,13 +105,69 @@ impl RequestPacket { } /// A [`ResponsePacket`] is a [`Response`] or a batch of responses. -#[derive(Debug, Clone, Deserialize)] -#[serde(untagged)] +#[derive(Debug, Clone)] pub enum ResponsePacket, ErrData = Box> { Single(Response), Batch(Vec>), } +use serde::de::{self, Deserializer, MapAccess, SeqAccess, Visitor}; +use std::fmt; +use std::marker::PhantomData; + +impl<'de, Payload, ErrData> Deserialize<'de> for ResponsePacket +where + Payload: Deserialize<'de>, + ErrData: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct ResponsePacketVisitor { + marker: PhantomData ResponsePacket>, + } + + impl<'de, Payload, ErrData> Visitor<'de> for ResponsePacketVisitor + where + Payload: Deserialize<'de>, + ErrData: Deserialize<'de>, + { + type Value = ResponsePacket; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a single response or a batch of responses") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut responses = Vec::new(); + + while let Some(response) = seq.next_element()? { + responses.push(response); + } + + Ok(ResponsePacket::Batch(responses)) + } + + fn visit_map(self, map: M) -> Result + where + M: MapAccess<'de>, + { + let response = + Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))?; + Ok(ResponsePacket::Single(response)) + } + } + + deserializer.deserialize_any(ResponsePacketVisitor { + marker: PhantomData, + }) + } +} + /// A [`BorrowedResponsePacket`] is a [`ResponsePacket`] that has been partially /// deserialized, borrowing its contents from the deserializer. This is used /// primarily for intermediate deserialization. Most users will not require it. diff --git a/crates/json-rpc/src/request.rs b/crates/json-rpc/src/request.rs index e07cb8ebd67..5616e0254b8 100644 --- a/crates/json-rpc/src/request.rs +++ b/crates/json-rpc/src/request.rs @@ -52,7 +52,7 @@ where /// Serialize the request, including the request parameters. pub fn serialize(self) -> serde_json::Result { - let request = serde_json::to_string(&self.params)?; + let request = serde_json::to_string(&self)?; Ok(SerializedRequest { meta: self.meta, request: RawValue::from_string(request)?, diff --git a/crates/providers/Cargo.toml b/crates/providers/Cargo.toml index 3e9fffaa6f8..1c44afd7663 100644 --- a/crates/providers/Cargo.toml +++ b/crates/providers/Cargo.toml @@ -16,6 +16,19 @@ alloy-json-rpc.workspace = true alloy-networks.workspace = true alloy-primitives.workspace = true alloy-transports.workspace = true +alloy-rpc-types.workspace = true async-trait = "0.1.73" futures-util = "0.3.28" serde_json = { workspace = true, features = ["raw_value"] } +serde = { workspace = true, features = ["derive"] } +thiserror = "1.0" +once_cell = "1.17" +reqwest = "0.11.22" +serial_test = "2.0.0" + +[dev-dependencies] +tokio = { version = "1.33.0", features = ["macros"] } +ethers-core = "2.0.10" + +[features] +anvil = [] \ No newline at end of file diff --git a/crates/providers/src/lib.rs b/crates/providers/src/lib.rs index 0621ad365c0..d033c422195 100644 --- a/crates/providers/src/lib.rs +++ b/crates/providers/src/lib.rs @@ -7,6 +7,9 @@ use alloy_primitives::Address; use alloy_transports::{BoxTransport, RpcClient, Transport, TransportError}; use serde_json::value::RawValue; +pub mod provider; +pub mod utils; + use std::{borrow::Cow, marker::PhantomData}; /// A network-wrapped RPC client. diff --git a/crates/providers/src/provider.rs b/crates/providers/src/provider.rs new file mode 100644 index 00000000000..d7e971cd24b --- /dev/null +++ b/crates/providers/src/provider.rs @@ -0,0 +1,622 @@ +//! Alloy main Provider abstraction. + +use alloy_primitives::{Address, BlockHash, Bytes, TxHash, U256, U64}; +use alloy_rpc_types::{ + Block, BlockId, BlockNumberOrTag, FeeHistory, Filter, Log, RpcBlockHash, SyncStatus, + Transaction, TransactionReceipt, TransactionRequest, +}; +use alloy_transports::{BoxTransport, Http, RpcClient, RpcResult, Transport, TransportError}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; +use std::borrow::Cow; +use thiserror::Error; + +use crate::utils::{self, EstimatorFunction}; + +#[derive(Debug, Error, Serialize, Deserialize)] +pub enum ClientError { + #[error("Could not parse URL")] + ParseError, + #[error("Unsupported Tag")] + UnsupportedBlockIdError, +} + +/// 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)] +pub struct Provider { + inner: RpcClient, + from: Option
, +} + +// 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 { + /// Gets the transaction count of the corresponding address. + pub async fn get_transaction_count( + &self, + address: Address, + ) -> RpcResult, TransportError> + where + Self: Sync, + { + self.inner + .prepare( + "eth_getTransactionCount", + Cow::<(Address, &'static str)>::Owned((address, "latest")), + ) + .await + } + + /// Gets the last block number available. + pub async fn get_block_number(&self) -> RpcResult, TransportError> + where + Self: Sync, + { + self.inner + .prepare("eth_blockNumber", Cow::<()>::Owned(())) + .await + } + + /// Gets the balance of the account at the specified tag, which defaults to latest. + pub async fn get_balance( + &self, + address: Address, + tag: Option, + ) -> RpcResult, TransportError> + where + Self: Sync, + { + self.inner + .prepare( + "eth_getBalance", + Cow::<(Address, BlockId)>::Owned(( + 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( + &self, + hash: BlockHash, + full: bool, + ) -> RpcResult, Box, TransportError> + where + Self: Sync, + { + self.inner + .prepare( + "eth_getBlockByHash", + Cow::<(BlockHash, bool)>::Owned((hash, full)), + ) + .await + } + + /// Gets a block by [BlockNumberOrTag], with full transactions or only hashes. + pub async fn get_block_by_number + Send + Sync>( + &self, + number: B, + full: bool, + ) -> RpcResult, Box, TransportError> + where + Self: Sync, + { + self.inner + .prepare( + "eth_getBlockByNumber", + Cow::<(BlockNumberOrTag, bool)>::Owned((number.into(), full)), + ) + .await + } + + /// Gets the chain ID. + pub async fn get_chain_id(&self) -> RpcResult, TransportError> + where + Self: Sync, + { + self.inner + .prepare("eth_chainId", Cow::<()>::Owned(())) + .await + } + /// Gets the bytecode located at the corresponding [Address]. + pub async fn get_code_at + Send + Sync>( + &self, + address: Address, + tag: B, + ) -> RpcResult, TransportError> + where + Self: Sync, + { + self.inner + .prepare( + "eth_getCode", + Cow::<(Address, BlockId)>::Owned((address, tag.into())), + ) + .await + } + + /// Gets a [Transaction] by its [TxHash]. + pub async fn get_transaction_by_hash( + &self, + hash: TxHash, + ) -> RpcResult, TransportError> + where + Self: Sync, + { + 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 + } + + /// Retrieves a [`Vec`] with the given [Filter]. + pub async fn get_logs( + &self, + filter: Filter, + ) -> RpcResult, Box, TransportError> + where + Self: Sync, + { + self.inner + .prepare("eth_getLogs", Cow::>::Owned(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) -> RpcResult, Box, TransportError> + where + Self: Sync, + { + self.inner + .prepare("eth_accounts", Cow::<()>::Owned(())) + .await + } + + /// Gets the current gas price. + pub async fn get_gas_price(&self) -> RpcResult, TransportError> + where + Self: Sync, + { + self.inner + .prepare("eth_gasPrice", Cow::<()>::Owned(())) + .await + } + + /// Gets a [TransactionReceipt] if it exists, by its [TxHash]. + pub async fn get_transaction_receipt( + &self, + hash: TxHash, + ) -> RpcResult, Box, TransportError> + where + Self: Sync, + { + self.inner + .prepare( + "eth_getTransactionReceipt", + Cow::>::Owned(vec![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>( + &self, + block_count: U256, + last_block: B, + reward_percentiles: &[f64], + ) -> RpcResult, TransportError> + where + Self: Sync, + { + self.inner + .prepare( + "eth_feeHistory", + Cow::<(U256, BlockNumberOrTag, Vec)>::Owned(( + block_count, + last_block.into(), + reward_percentiles.to_vec(), + )), + ) + .await + } + + /// Gets the selected block [BlockNumberOrTag] receipts. + pub async fn get_block_receipts( + &self, + block: BlockNumberOrTag, + ) -> RpcResult, Box, TransportError> + where + Self: Sync, + { + self.inner + .prepare( + "eth_getBlockReceipts", + Cow::::Owned(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, + ) -> RpcResult, Box, TransportError> + where + Self: Sync, + { + let tag = tag.into(); + match tag { + BlockId::Hash(hash) => { + self.inner + .prepare( + "eth_getUncleByBlockHashAndIndex", + Cow::<(RpcBlockHash, U64)>::Owned((hash, idx)), + ) + .await + } + BlockId::Number(number) => { + self.inner + .prepare( + "eth_getUncleByBlockNumberAndIndex", + Cow::<(BlockNumberOrTag, U64)>::Owned((number, idx)), + ) + .await + } + } + } + + /// Gets syncing info. + pub async fn syncing(&self) -> RpcResult, TransportError> + where + Self: Sync, + { + self.inner + .prepare("eth_syncing", Cow::<()>::Owned(())) + .await + } + + /// Execute a smart contract call with [TransactionRequest] without publishing a transaction. + pub async fn call( + &self, + tx: TransactionRequest, + block: Option, + ) -> RpcResult, TransportError> + where + Self: Sync, + { + self.inner + .prepare( + "eth_call", + Cow::<(TransactionRequest, BlockId)>::Owned(( + tx, + block.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest)), + )), + ) + .await + } + + /// Estimate the gas needed for a transaction. + pub async fn estimate_gas( + &self, + tx: TransactionRequest, + block: Option, + ) -> RpcResult, TransportError> + where + Self: Sync, + { + if let Some(block_id) = block { + let params = Cow::<(TransactionRequest, BlockId)>::Owned((tx, block_id)); + self.inner.prepare("eth_estimateGas", params).await + } else { + let params = Cow::::Owned(tx); + self.inner.prepare("eth_estimateGas", params).await + } + } + + /// Sends an already-signed transaction. + pub async fn send_raw_transaction( + &self, + tx: Bytes, + ) -> RpcResult, TransportError> + where + Self: Sync, + { + self.inner + .prepare("eth_sendRawTransaction", Cow::::Owned(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( + &self, + estimator: Option, + ) -> RpcResult<(U256, U256), Box, TransportError> + where + Self: Sync, + { + let base_fee_per_gas = match self + .get_block_by_number(BlockNumberOrTag::Latest, false) + .await + { + RpcResult::Success(Some(block)) => match block.header.base_fee_per_gas { + Some(base_fee_per_gas) => base_fee_per_gas, + None => { + return RpcResult::Err(TransportError::Custom("EIP-1559 not activated".into())) + } + }, + RpcResult::Success(None) => { + return RpcResult::Err(TransportError::Custom("Latest block not found".into())) + } + RpcResult::Err(err) => return RpcResult::Err(err), + RpcResult::Failure(err) => return RpcResult::Failure(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 + { + RpcResult::Success(fee_history) => fee_history, + RpcResult::Err(err) => return RpcResult::Err(err), + RpcResult::Failure(err) => return RpcResult::Failure(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(), + ) + }; + + RpcResult::Success((max_fee_per_gas, max_priority_fee_per_gas)) + } + + #[cfg(feature = "anvil")] + pub async fn set_code( + &self, + address: Address, + code: &'static str, + ) -> RpcResult<(), Box, TransportError> + where + Self: Sync, + { + self.inner + .prepare( + "anvil_setCode", + Cow::<(Address, &'static str)>::Owned((address, code)), + ) + .await + } + + pub fn with_sender(mut self, from: Address) -> Self { + self.from = Some(from); + self + } + + pub fn inner(&self) -> &RpcClient { + &self.inner + } +} + +// HTTP Transport Provider implementation +impl Provider> { + pub fn new(url: &str) -> Result { + let inner: RpcClient> = url.parse().map_err(|_e| ClientError::ParseError)?; + Ok(Self { inner, from: None }) + } +} + +impl TryFrom<&str> for Provider> { + type Error = ClientError; + + fn try_from(value: &str) -> Result { + Provider::new(value) + } +} + +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 providers_test { + use crate::{provider::Provider, utils}; + use alloy_primitives::{address, b256, Address, U256, U64}; + use alloy_rpc_types::{BlockId, 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 num = provider.get_block_number().await.unwrap(); + assert_eq!(U256::ZERO, num) + } + + #[tokio::test] + async fn gets_transaction_count() { + let anvil = Anvil::new().spawn(); + let provider = Provider::new(&anvil.endpoint()).unwrap(); + let count = provider + .get_transaction_count(address!("328375e18E7db8F1CA9d9bA8bF3E9C94ee34136A")) + .await + .unwrap(); + assert_eq!(count, U256::from(0)); + } + + #[tokio::test] + async fn gets_block_by_hash() { + let anvil = Anvil::new().spawn(); + let provider = Provider::new(&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_number_full() { + let anvil = Anvil::new().spawn(); + let provider = Provider::new(&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::new(&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_chain_id() { + let anvil = Anvil::new().args(vec!["--chain-id", "13371337"]).spawn(); + let provider = Provider::new(&anvil.endpoint()).unwrap(); + let chain_id = provider.get_chain_id().await.unwrap(); + assert_eq!(chain_id, U64::from(13371337)); + } + + #[tokio::test] + #[cfg(feature = "anvil")] + async fn gets_code_at() { + let anvil = Anvil::new().spawn(); + let provider = Provider::new(&anvil.endpoint()).unwrap(); + // Set the code + let addr = Address::with_last_byte(16); + provider.set_code(addr, "0xbeef").await.unwrap(); + let _code = provider + .get_code_at( + addr, + BlockId::Number(alloy_rpc_types::BlockNumberOrTag::Latest), + ) + .await + .unwrap(); + } + + #[tokio::test] + #[ignore] + async fn gets_transaction_by_hash() { + let anvil = Anvil::new().spawn(); + let provider = Provider::new(&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::new(&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::new(&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::new(&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()), + &[utils::EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE], + ) + .await + .unwrap(); + assert_eq!(fee_history.oldest_block, U256::ZERO); + } +} diff --git a/crates/providers/src/utils.rs b/crates/providers/src/utils.rs new file mode 100644 index 00000000000..e19a8d1d3a2 --- /dev/null +++ b/crates/providers/src/utils.rs @@ -0,0 +1,104 @@ +//! Provider-related utilities. + +use alloy_primitives::{I256, U256}; + +/// The number of blocks from the past for which the fee rewards are fetched for fee estimation. +pub const EIP1559_FEE_ESTIMATION_PAST_BLOCKS: u64 = 10; +/// The default percentile of gas premiums that are fetched for fee estimation. +pub const EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE: f64 = 5.0; +/// The default max priority fee per gas, used in case the base fee is within a threshold. +pub const EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE: u64 = 3_000_000_000; +/// The threshold for base fee below which we use the default priority fee, and beyond which we +/// estimate an appropriate value for priority fee. +pub const EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER: u64 = 100_000_000_000; +/// The threshold max change/difference (in %) at which we will ignore the fee history values +/// under it. +pub const EIP1559_FEE_ESTIMATION_THRESHOLD_MAX_CHANGE: i64 = 200; + +/// An estimator function for EIP1559 fees. +pub type EstimatorFunction = fn(U256, Vec>) -> (U256, U256); + +fn estimate_priority_fee(rewards: Vec>) -> U256 { + let mut rewards: Vec = rewards + .iter() + .map(|r| r[0]) + .filter(|r| *r > U256::ZERO) + .collect(); + if rewards.is_empty() { + return U256::ZERO; + } + if rewards.len() == 1 { + return rewards[0]; + } + // Sort the rewards as we will eventually take the median. + rewards.sort(); + + // A copy of the same vector is created for convenience to calculate percentage change + // between subsequent fee values. + let mut rewards_copy = rewards.clone(); + rewards_copy.rotate_left(1); + + let mut percentage_change: Vec = rewards + .iter() + .zip(rewards_copy.iter()) + .map(|(a, b)| { + let a = I256::try_from(*a).expect("priority fee overflow"); + let b = I256::try_from(*b).expect("priority fee overflow"); + ((b - a) * I256::try_from(100).expect("Unexpected overflow")) / a + }) + .collect(); + percentage_change.pop(); + + // Fetch the max of the percentage change, and that element's index. + let max_change = percentage_change.iter().max().unwrap(); + let max_change_index = percentage_change + .iter() + .position(|&c| c == *max_change) + .unwrap(); + + // If we encountered a big change in fees at a certain position, then consider only + // the values >= it. + let values = if *max_change + >= I256::from_raw(U256::from(EIP1559_FEE_ESTIMATION_THRESHOLD_MAX_CHANGE)) + && (max_change_index >= (rewards.len() / 2)) + { + rewards[max_change_index..].to_vec() + } else { + rewards + }; + + // Return the median. + values[values.len() / 2] +} + +fn base_fee_surged(base_fee_per_gas: U256) -> U256 { + if base_fee_per_gas <= U256::from(40_000_000_000u64) { + base_fee_per_gas * U256::from(2) + } else if base_fee_per_gas <= U256::from(100_000_000_000u64) { + base_fee_per_gas * U256::from(16) / U256::from(10) + } else if base_fee_per_gas <= U256::from(200_000_000_000u64) { + base_fee_per_gas * U256::from(14) / U256::from(10) + } else { + base_fee_per_gas * U256::from(12) / U256::from(10) + } +} + +/// The default EIP-1559 fee estimator which is based on the work by [MyCrypto](https://github.com/MyCryptoHQ/MyCrypto/blob/master/src/services/ApiService/Gas/eip1559.ts) +pub fn eip1559_default_estimator(base_fee_per_gas: U256, rewards: Vec>) -> (U256, U256) { + let max_priority_fee_per_gas = + if base_fee_per_gas < U256::from(EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER) { + U256::from(EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE) + } else { + std::cmp::max( + estimate_priority_fee(rewards), + U256::from(EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE), + ) + }; + let potential_max_fee = base_fee_surged(base_fee_per_gas); + let max_fee_per_gas = if max_priority_fee_per_gas > potential_max_fee { + max_priority_fee_per_gas + potential_max_fee + } else { + potential_max_fee + }; + (max_fee_per_gas, max_priority_fee_per_gas) +} diff --git a/crates/transports/src/transports/json.rs b/crates/transports/src/transports/json.rs index 6601034b62f..7168344b479 100644 --- a/crates/transports/src/transports/json.rs +++ b/crates/transports/src/transports/json.rs @@ -49,7 +49,6 @@ where fn call(&mut self, req: RequestPacket) -> Self::Future { let replacement = self.inner.clone(); let mut client = std::mem::replace(&mut self.inner, replacement); - match to_json_raw_value(&req) { Ok(raw) => JsonRpcFuture { state: States::Pending {